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
+69 -15
View File
@@ -29,10 +29,15 @@ import {
import {
mapInstallLink,
randomToken,
buildInstallUrls,
} from '../services/install-links.js'
import { hashToken } from '../plugins/auth.js'
import type { AppConfig } from '../config.js'
function mapAgent(a: NonNullable<ReturnType<typeof repos.getAgent>>) {
function mapAgent(
a: NonNullable<ReturnType<typeof repos.getAgent>>,
opts?: { installCurl?: string | null; installLinkId?: string | null },
) {
return {
id: a.id,
name: a.name,
@@ -55,6 +60,8 @@ function mapAgent(a: NonNullable<ReturnType<typeof repos.getAgent>>) {
created_at: a.createdAt,
approved_at: a.approvedAt,
revoked_at: a.revokedAt,
install_curl: opts?.installCurl ?? null,
install_link_id: opts?.installLinkId ?? null,
}
}
@@ -148,20 +155,49 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
app.post('/install-links', async (req, reply) => {
const body = createInstallLinkBodySchema.parse(req.body)
const id = randomToken(10)
const linkId = randomToken(10)
const slug = randomToken(16)
if (repos.getInstallLink(app.db, id) || repos.getInstallLinkBySlug(app.db, slug)) {
if (
repos.getInstallLink(app.db, linkId) ||
repos.getInstallLinkBySlug(app.db, slug)
) {
throw new AppError('CONFLICT', 'Retry create (id collision)', 409)
}
const row = repos.insertInstallLink(app.db, {
id,
slug,
clientName: body.name.trim(),
platform: body.platform ?? 'linux',
createdAt: new Date().toISOString(),
useCount: 0,
})
return reply.code(201).send(mapInstallLink(row!, config.publicBaseUrl))
const agentId = crypto.randomUUID()
const inviteToken = `invite:${agentId}`
const now = new Date().toISOString()
const name = body.name.trim()
const platform = body.platform ?? 'linux'
app.sqlite.transaction(() => {
repos.insertAgent(app.db, {
id: agentId,
name,
hostname: null,
platform,
tokenPrefix: inviteToken.slice(0, 12),
tokenHash: hashToken(inviteToken),
status: 'invited',
policyMode: 'blacklist',
policyGeneration: 1,
clientVersion: null,
settingsJson: '{}',
createdAt: now,
})
repos.insertInstallLink(app.db, {
id: linkId,
slug,
clientName: name,
platform,
agentId,
createdAt: now,
useCount: 0,
})
})()
const row = repos.getInstallLink(app.db, linkId)!
return reply.code(201).send(mapInstallLink(row, config.publicBaseUrl))
})
app.delete<{ Params: { id: string } }>(
@@ -175,9 +211,27 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
)
// Agents
app.get('/agents', async () => ({
items: repos.listAgents(app.db).map(mapAgent),
}))
app.get('/agents', async () => {
const all = repos.listAgents(app.db)
return {
items: all.map((a) => {
const link = repos.getInstallLinkByAgentId(app.db, a.id)
if (!link || link.revokedAt) {
return mapAgent(a)
}
const urls = buildInstallUrls(
config.publicBaseUrl,
link.id,
link.slug,
link.platform === 'mikrotik' ? 'mikrotik' : 'linux',
)
return mapAgent(a, {
installCurl: urls.curl.by_slug,
installLinkId: link.id,
})
}),
}
})
app.get<{ Params: { id: string } }>('/agents/:id', async (req) => {
const a = repos.getAgent(app.db, req.params.id)