feat(api, web): enhance agent installation process with invited status and policy support
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m48s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Updated the agent enrollment process to include an 'invited' status, allowing for better tracking of agent states.
- Implemented support for install links that can now include an `install_link_id`, facilitating the transition from invited to pending status upon enrollment.
- Enhanced the MikroTik installation script to include the `EvofwInstallLinkId` for better tracking and management.
- Added new API endpoints for fetching agent policies and serving MikroTik-specific installation scripts.
- Improved the web UI to reflect the new agent statuses and provide copyable installation commands for agents.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-21 01:45:38 +07:00
co-authored by Cursor
parent ef56da4d91
commit d5784b9f35
20 changed files with 793 additions and 104 deletions
+95 -11
View File
@@ -42,10 +42,31 @@ export function isValidInstallSlug(segment: string): boolean {
return SLUG_RE.test(segment)
}
export function buildInstallUrls(baseUrl: string, id: string, slug: string) {
export type InstallPlatform = 'linux' | 'mikrotik'
function mikrotikFetchImport(url: string): string {
return `/tool fetch url="${url}" dst-path=evofw-install.rsc; /import file-name=evofw-install.rsc`
}
export function buildInstallUrls(
baseUrl: string,
id: string,
slug: string,
platform: InstallPlatform = 'linux',
) {
const base = baseUrl.replace(/\/$/, '')
const byId = `${base}/agent-install/${id}`
const bySlug = `${base}/${slug}`
if (platform === 'mikrotik') {
return {
by_id: byId,
by_slug: bySlug,
curl: {
by_id: mikrotikFetchImport(byId),
by_slug: mikrotikFetchImport(bySlug),
},
}
}
return {
by_id: byId,
by_slug: bySlug,
@@ -60,12 +81,14 @@ export function mapInstallLink(
row: NonNullable<ReturnType<typeof repos.getInstallLink>>,
baseUrl: string,
) {
const urls = buildInstallUrls(baseUrl, row.id, row.slug)
const platform = (row.platform === 'mikrotik' ? 'mikrotik' : 'linux') as InstallPlatform
const urls = buildInstallUrls(baseUrl, row.id, row.slug, platform)
return {
id: row.id,
slug: row.slug,
client_name: row.clientName,
platform: row.platform as 'linux' | 'mikrotik',
platform,
agent_id: row.agentId ?? null,
created_at: row.createdAt,
revoked_at: row.revokedAt,
last_used_at: row.lastUsedAt,
@@ -79,6 +102,14 @@ function loadInstallSh(): string {
return readFileSync(join(scriptsDir, 'install.sh'), 'utf-8')
}
function loadMikrotikInstallRsc(): string {
return readFileSync(join(scriptsDir, 'mikrotik-install.rsc'), 'utf-8')
}
function escapeRosString(s: string): string {
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}
/**
* Self-contained install script: env exports + full install.sh body.
*/
@@ -87,6 +118,7 @@ export function renderInstallScript(opts: {
seed: string
clientName: string
platform: string
installLinkId: string
}): string {
const cp = opts.cpUrl.replace(/\/$/, '')
const escape = (s: string) => s.replace(/'/g, `'\\''`)
@@ -98,6 +130,7 @@ export function renderInstallScript(opts: {
`export EVOFW_SEED='${escape(opts.seed)}'`,
`export EVOFW_CLIENT_NAME='${escape(opts.clientName)}'`,
`export EVOFW_PLATFORM='${escape(opts.platform)}'`,
`export EVOFW_INSTALL_LINK_ID='${escape(opts.installLinkId)}'`,
'',
].join('\n')
@@ -106,22 +139,73 @@ export function renderInstallScript(opts: {
return `${header}${body}`
}
/**
* Personalized MikroTik RSC: globals + mikrotik-install.rsc body.
*/
export function renderMikrotikInstallScript(opts: {
cpUrl: string
seed: string
clientName: string
installLinkId: string
}): string {
const cp = opts.cpUrl.replace(/\/$/, '')
const header = [
'# EvoFirewall short install link — globals pre-set (RouterOS 7.21+)',
`:global EvofwCpUrl "${escapeRosString(cp)}"`,
`:global EvofwSeed "${escapeRosString(opts.seed)}"`,
`:global EvofwName "${escapeRosString(opts.clientName)}"`,
`:global EvofwInstallLinkId "${escapeRosString(opts.installLinkId)}"`,
'',
].join('\n')
return `${header}${loadMikrotikInstallRsc()}`
}
export type ResolvedInstall = {
body: string
contentType: string
}
/** @deprecated use resolveAndRenderInstall */
export function resolveAndRenderInstallScript(
db: Db,
link: NonNullable<ReturnType<typeof repos.getInstallLink>>,
publicBaseUrl: string,
enrollSeedFallback: string,
): string {
return resolveAndRenderInstall(db, link, publicBaseUrl, enrollSeedFallback)
.body
}
export function resolveAndRenderInstall(
db: Db,
link: NonNullable<ReturnType<typeof repos.getInstallLink>>,
publicBaseUrl: string,
enrollSeedFallback: string,
): ResolvedInstall {
if (link.revokedAt) {
throw new AppError('GONE', 'Install link revoked', 410)
}
const seed =
repos.getSetting(db, 'enroll_seed') || enrollSeedFallback
const seed = repos.getSetting(db, 'enroll_seed') || enrollSeedFallback
repos.touchInstallLink(db, link.id)
return renderInstallScript({
cpUrl: publicBaseUrl,
seed,
clientName: link.clientName,
platform: link.platform,
})
if (link.platform === 'mikrotik') {
return {
body: renderMikrotikInstallScript({
cpUrl: publicBaseUrl,
seed,
clientName: link.clientName,
installLinkId: link.id,
}),
contentType: 'text/plain',
}
}
return {
body: renderInstallScript({
cpUrl: publicBaseUrl,
seed,
clientName: link.clientName,
platform: link.platform,
installLinkId: link.id,
}),
contentType: 'text/x-shellscript',
}
}