feat(api, web): implement short install link functionality for agents
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m37s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Added new API endpoints for creating, retrieving, and revoking install links for agents.
- Enhanced the agent installation process with short links accessible via `/agent-install/:id` and `/:slug`.
- Updated the README and documentation to reflect the new installation method and usage instructions.
- Refactored relevant components in the web application to support the new install link feature.
- Improved error handling and validation for install link operations.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-21 01:25:05 +07:00
co-authored by Cursor
parent 14f516d5cc
commit ef56da4d91
13 changed files with 677 additions and 117 deletions
@@ -0,0 +1,19 @@
-- Agent install short-links (one-liner invites)
CREATE TABLE IF NOT EXISTS agent_install_links (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
client_name TEXT NOT NULL,
platform TEXT NOT NULL DEFAULT 'linux',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
revoked_at TEXT,
last_used_at TEXT,
use_count INTEGER NOT NULL DEFAULT 0,
CHECK (platform IN ('linux', 'mikrotik')),
CHECK (length(trim(client_name)) > 0),
CHECK (length(trim(id)) > 0),
CHECK (length(trim(slug)) > 0)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_install_links_slug
ON agent_install_links (slug);
+60
View File
@@ -10,6 +10,7 @@ import {
policyRuleResolved,
ipOverrides,
agentStatsSamples,
agentInstallLinks,
settings,
SHARED_POLICY_SET_ID,
} from '../schema.js'
@@ -434,6 +435,59 @@ export function cloneRulesFrom(
return getAgent(db, targetAgentId)
}
export function listInstallLinks(db: Db) {
return db
.select()
.from(agentInstallLinks)
.orderBy(desc(agentInstallLinks.createdAt))
.all()
}
export function getInstallLink(db: Db, id: string) {
return db
.select()
.from(agentInstallLinks)
.where(eq(agentInstallLinks.id, id))
.get()
}
export function getInstallLinkBySlug(db: Db, slug: string) {
return db
.select()
.from(agentInstallLinks)
.where(eq(agentInstallLinks.slug, slug))
.get()
}
export function insertInstallLink(
db: Db,
row: typeof agentInstallLinks.$inferInsert,
) {
db.insert(agentInstallLinks).values(row).run()
return getInstallLink(db, row.id)
}
export function revokeInstallLink(db: Db, id: string) {
const now = new Date().toISOString()
db.update(agentInstallLinks)
.set({ revokedAt: now })
.where(eq(agentInstallLinks.id, id))
.run()
return getInstallLink(db, id)
}
export function touchInstallLink(db: Db, id: string) {
const now = new Date().toISOString()
db.update(agentInstallLinks)
.set({
lastUsedAt: now,
useCount: sql`${agentInstallLinks.useCount} + 1`,
})
.where(eq(agentInstallLinks.id, id))
.run()
return getInstallLink(db, id)
}
export const repos = {
listAgents,
getAgent,
@@ -481,6 +535,12 @@ export const repos = {
setSetting,
listSettings,
cloneRulesFrom,
listInstallLinks,
getInstallLink,
getInstallLinkBySlug,
insertInstallLink,
revokeInstallLink,
touchInstallLink,
}
export { SHARED_POLICY_SET_ID }
+21
View File
@@ -194,6 +194,26 @@ export const agentStatsSamples = sqliteTable(
}),
)
/** Short install invite links (`/agent-install/:id` and `/:slug`). */
export const agentInstallLinks = sqliteTable(
'agent_install_links',
{
id: text('id').primaryKey(),
slug: text('slug').notNull(),
clientName: text('client_name').notNull(),
platform: text('platform').notNull().default('linux'), // linux | mikrotik
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
revokedAt: text('revoked_at'),
lastUsedAt: text('last_used_at'),
useCount: integer('use_count').notNull().default(0),
},
(t) => ({
slugIdx: uniqueIndex('idx_agent_install_links_slug').on(t.slug),
}),
)
export const SHARED_POLICY_SET_ID = 'set-shared-default'
export const schema = {
@@ -207,4 +227,5 @@ export const schema = {
policyRuleResolved,
ipOverrides,
agentStatsSamples,
agentInstallLinks,
}
+29
View File
@@ -182,6 +182,34 @@ export const dashboardStatsSchema = z.object({
lists_total: z.number().int(),
})
export const createInstallLinkBodySchema = z.object({
name: z.string().min(1),
platform: agentPlatformSchema.optional().default('linux'),
})
export const installLinkSchema = z.object({
id: z.string(),
slug: z.string(),
client_name: z.string(),
platform: agentPlatformSchema,
created_at: z.string(),
revoked_at: z.string().nullable().optional(),
last_used_at: z.string().nullable().optional(),
use_count: z.number().int(),
urls: z
.object({
by_id: z.string(),
by_slug: z.string(),
})
.optional(),
curl: z
.object({
by_id: z.string(),
by_slug: z.string(),
})
.optional(),
})
export type Agent = z.infer<typeof agentSchema>
export type IpList = z.infer<typeof ipListSchema>
export type PolicyRule = z.infer<typeof policyRuleSchema>
@@ -189,3 +217,4 @@ export type PolicySet = z.infer<typeof policySetSchema>
export type IpOverride = z.infer<typeof ipOverrideSchema>
export type AgentPolicy = z.infer<typeof agentPolicySchema>
export type DashboardStats = z.infer<typeof dashboardStatsSchema>
export type InstallLink = z.infer<typeof installLinkSchema>