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
@@ -0,0 +1,64 @@
-- Allow invited agents (created in UI before enroll) + link install invites to agents.
-- Rebuild agents to widen status CHECK (FK temporarily off).
PRAGMA foreign_keys = OFF;
CREATE TABLE agents_v2 (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
hostname TEXT,
platform TEXT NOT NULL DEFAULT 'linux',
token_prefix TEXT NOT NULL,
token_hash TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
policy_mode TEXT NOT NULL DEFAULT 'blacklist',
policy_generation INTEGER NOT NULL DEFAULT 1,
last_seen_at TEXT,
last_seen_ip TEXT,
last_apply_at TEXT,
last_apply_status TEXT,
last_apply_error TEXT,
last_apply_prefix_count INTEGER DEFAULT 0,
last_apply_packets_dropped INTEGER NOT NULL DEFAULT 0,
last_apply_packets_accepted INTEGER NOT NULL DEFAULT 0,
last_apply_kernel_method TEXT,
client_version TEXT,
settings_json TEXT NOT NULL DEFAULT '{}',
created_by_user_id TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
approved_at TEXT,
revoked_at TEXT,
CHECK (status IN ('invited', 'pending', 'approved', 'revoked')),
CHECK (platform IN ('linux', 'mikrotik')),
CHECK (policy_mode IN ('blacklist', 'whitelist')),
CHECK (length(trim(name)) > 0)
);
INSERT INTO agents_v2 (
id, name, hostname, platform, token_prefix, token_hash, status, policy_mode,
policy_generation, last_seen_at, last_seen_ip, last_apply_at, last_apply_status,
last_apply_error, last_apply_prefix_count, last_apply_packets_dropped,
last_apply_packets_accepted, last_apply_kernel_method, client_version,
settings_json, created_by_user_id, created_at, approved_at, revoked_at
)
SELECT
id, name, hostname, platform, token_prefix, token_hash, status, policy_mode,
policy_generation, last_seen_at, last_seen_ip, last_apply_at, last_apply_status,
last_apply_error, last_apply_prefix_count, last_apply_packets_dropped,
last_apply_packets_accepted, last_apply_kernel_method, client_version,
settings_json, created_by_user_id, created_at, approved_at, revoked_at
FROM agents;
DROP TABLE agents;
ALTER TABLE agents_v2 RENAME TO agents;
CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_token_hash ON agents (token_hash);
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents (status);
PRAGMA foreign_keys = ON;
-- Bind install links to agents
ALTER TABLE agent_install_links ADD COLUMN agent_id TEXT REFERENCES agents (id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_agent_install_links_agent
ON agent_install_links (agent_id);
+15
View File
@@ -459,6 +459,20 @@ export function getInstallLinkBySlug(db: Db, slug: string) {
.get()
}
export function getInstallLinkByAgentId(db: Db, agentId: string) {
return db
.select()
.from(agentInstallLinks)
.where(
and(
eq(agentInstallLinks.agentId, agentId),
sql`${agentInstallLinks.revokedAt} IS NULL`,
),
)
.orderBy(desc(agentInstallLinks.createdAt))
.get()
}
export function insertInstallLink(
db: Db,
row: typeof agentInstallLinks.$inferInsert,
@@ -538,6 +552,7 @@ export const repos = {
listInstallLinks,
getInstallLink,
getInstallLinkBySlug,
getInstallLinkByAgentId,
insertInstallLink,
revokeInstallLink,
touchInstallLink,
+3 -1
View File
@@ -18,7 +18,7 @@ export const agents = sqliteTable(
platform: text('platform').notNull().default('linux'), // linux | mikrotik
tokenPrefix: text('token_prefix').notNull(),
tokenHash: text('token_hash').notNull(),
status: text('status').notNull().default('pending'), // pending | approved | revoked
status: text('status').notNull().default('pending'), // invited | pending | approved | revoked
policyMode: text('policy_mode').notNull().default('blacklist'), // blacklist | whitelist
policyGeneration: integer('policy_generation').notNull().default(1),
lastSeenAt: text('last_seen_at'),
@@ -202,6 +202,7 @@ export const agentInstallLinks = sqliteTable(
slug: text('slug').notNull(),
clientName: text('client_name').notNull(),
platform: text('platform').notNull().default('linux'), // linux | mikrotik
agentId: text('agent_id').references(() => agents.id, { onDelete: 'cascade' }),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
@@ -211,6 +212,7 @@ export const agentInstallLinks = sqliteTable(
},
(t) => ({
slugIdx: uniqueIndex('idx_agent_install_links_slug').on(t.slug),
agentIdx: index('idx_agent_install_links_agent').on(t.agentId),
}),
)
+10 -1
View File
@@ -1,7 +1,12 @@
import { z } from 'zod'
export const agentPlatformSchema = z.enum(['linux', 'mikrotik'])
export const agentStatusSchema = z.enum(['pending', 'approved', 'revoked'])
export const agentStatusSchema = z.enum([
'invited',
'pending',
'approved',
'revoked',
])
export const policyModeSchema = z.enum(['blacklist', 'whitelist'])
export const policyActionSchema = z.enum(['allow', 'deny'])
export const ipListTypeSchema = z.enum([
@@ -34,6 +39,8 @@ export const agentSchema = z.object({
created_at: z.string(),
approved_at: z.string().nullable().optional(),
revoked_at: z.string().nullable().optional(),
install_curl: z.string().nullable().optional(),
install_link_id: z.string().nullable().optional(),
})
export const ipListSchema = z.object({
@@ -151,6 +158,7 @@ export const enrollBodySchema = z.object({
platform: agentPlatformSchema.optional().default('linux'),
token: z.string().min(16),
client_version: z.string().optional(),
install_link_id: z.string().optional(),
})
export const applyReportBodySchema = z.object({
@@ -192,6 +200,7 @@ export const installLinkSchema = z.object({
slug: z.string(),
client_name: z.string(),
platform: agentPlatformSchema,
agent_id: z.string().nullable().optional(),
created_at: z.string(),
revoked_at: z.string().nullable().optional(),
last_used_at: z.string().nullable().optional(),