import { sqliteTable, text, integer, uniqueIndex, index } from 'drizzle-orm/sqlite-core' import { sql } from 'drizzle-orm' export const settings = sqliteTable('settings', { key: text('key').primaryKey(), value: text('value').notNull().default(''), updatedAt: text('updated_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }) export const agents = sqliteTable( 'agents', { id: text('id').primaryKey(), name: text('name').notNull(), hostname: text('hostname'), platform: text('platform').notNull().default('linux'), // linux | mikrotik tokenPrefix: text('token_prefix').notNull(), tokenHash: text('token_hash').notNull(), status: text('status').notNull().default('pending'), // invited | pending | approved | revoked /** Packet default when not in deny/allow sets: accept | drop */ defaultAction: text('default_action').notNull().default('accept'), policyGeneration: integer('policy_generation').notNull().default(1), lastSeenAt: text('last_seen_at'), lastSeenIp: text('last_seen_ip'), lastApplyAt: text('last_apply_at'), lastApplyStatus: text('last_apply_status'), lastApplyError: text('last_apply_error'), lastApplyPrefixCount: integer('last_apply_prefix_count').default(0), lastApplyPacketsDropped: integer('last_apply_packets_dropped').notNull().default(0), lastApplyPacketsAccepted: integer('last_apply_packets_accepted').notNull().default(0), /** Lifetime counters until UI/API reset (accumulate across applies). */ totalPacketsDropped: integer('total_packets_dropped').notNull().default(0), totalPacketsAccepted: integer('total_packets_accepted').notNull().default(0), lastApplyKernelMethod: text('last_apply_kernel_method'), clientVersion: text('client_version'), settingsJson: text('settings_json').notNull().default('{}'), createdByUserId: text('created_by_user_id'), createdAt: text('created_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), approvedAt: text('approved_at'), revokedAt: text('revoked_at'), }, (t) => ({ tokenHashIdx: uniqueIndex('idx_agents_token_hash').on(t.tokenHash), statusIdx: index('idx_agents_status').on(t.status), }), ) export const ipLists = sqliteTable('ip_lists', { id: text('id').primaryKey(), name: text('name').notNull(), type: text('type').notNull(), // static | json_url | domains | evobgp_community configJson: text('config_json').notNull().default('{}'), contentHash: text('content_hash'), refreshedAt: text('refreshed_at'), lastError: text('last_error'), createdAt: text('created_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), updatedAt: text('updated_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }) export const ipListEntries = sqliteTable( 'ip_list_entries', { id: text('id').primaryKey(), listId: text('list_id') .notNull() .references(() => ipLists.id, { onDelete: 'cascade' }), cidr: text('cidr').notNull(), createdAt: text('created_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }, (t) => ({ listCidr: uniqueIndex('idx_ip_list_entries_list_cidr').on(t.listId, t.cidr), }), ) /** Named reusable policy sets (M:N with agents). */ export const policySets = sqliteTable('policy_sets', { id: text('id').primaryKey(), name: text('name').notNull(), description: text('description'), enabled: integer('enabled').notNull().default(1), /** Legacy unused; sets no longer carry exclusive mode. */ policyMode: text('policy_mode').notNull().default('blacklist'), createdAt: text('created_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), updatedAt: text('updated_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }) export const agentPolicySets = sqliteTable( 'agent_policy_sets', { agentId: text('agent_id') .notNull() .references(() => agents.id, { onDelete: 'cascade' }), setId: text('set_id') .notNull() .references(() => policySets.id, { onDelete: 'cascade' }), sort: integer('sort').notNull().default(0), }, (t) => ({ pk: uniqueIndex('idx_agent_policy_sets_pk').on(t.agentId, t.setId), setIdx: index('idx_agent_policy_sets_set').on(t.setId), }), ) export const policyRules = sqliteTable( 'policy_rules', { id: text('id').primaryKey(), setId: text('set_id') .notNull() .references(() => policySets.id, { onDelete: 'cascade' }), priority: integer('priority').notNull(), action: text('action').notNull(), // allow | deny enabled: integer('enabled').notNull().default(1), listId: text('list_id').references(() => ipLists.id, { onDelete: 'cascade' }), cidr: text('cidr'), hostname: text('hostname'), comment: text('comment'), createdByUserId: text('created_by_user_id'), createdAt: text('created_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), updatedAt: text('updated_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }, (t) => ({ setPriority: uniqueIndex('idx_policy_rules_set_priority').on(t.setId, t.priority), }), ) /** DNS resolve cache for hostname rules. */ export const policyRuleResolved = sqliteTable( 'policy_rule_resolved', { id: text('id').primaryKey(), ruleId: text('rule_id') .notNull() .references(() => policyRules.id, { onDelete: 'cascade' }), cidr: text('cidr').notNull(), resolvedAt: text('resolved_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }, (t) => ({ ruleCidr: uniqueIndex('idx_policy_rule_resolved_rule_cidr').on(t.ruleId, t.cidr), ruleIdx: index('idx_policy_rule_resolved_rule').on(t.ruleId), }), ) export const ipOverrides = sqliteTable( 'ip_overrides', { id: text('id').primaryKey(), agentId: text('agent_id') .notNull() .references(() => agents.id, { onDelete: 'cascade' }), cidr: text('cidr').notNull(), action: text('action').notNull(), // allow | deny comment: text('comment'), createdByUserId: text('created_by_user_id'), createdAt: text('created_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }, (t) => ({ agentCidr: uniqueIndex('idx_ip_overrides_agent_cidr').on(t.agentId, t.cidr), }), ) export const agentStatsSamples = sqliteTable( 'agent_stats_samples', { id: text('id').primaryKey(), agentId: text('agent_id') .notNull() .references(() => agents.id, { onDelete: 'cascade' }), packetsDropped: integer('packets_dropped').notNull().default(0), packetsAccepted: integer('packets_accepted').notNull().default(0), prefixCount: integer('prefix_count').notNull().default(0), kernelMethod: text('kernel_method'), recordedAt: text('recorded_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }, (t) => ({ agentTime: index('idx_agent_stats_agent_time').on(t.agentId, t.recordedAt), }), ) /** Per-IP/CIDR drop counters reported by Linux agents (nft/ipset element counters). */ export const agentIpBlockStats = sqliteTable( 'agent_ip_block_stats', { id: text('id').primaryKey(), agentId: text('agent_id') .notNull() .references(() => agents.id, { onDelete: 'cascade' }), ip: text('ip').notNull(), packets: integer('packets').notNull().default(0), lastReportedPackets: integer('last_reported_packets').notNull().default(0), firstSeenAt: text('first_seen_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), lastSeenAt: text('last_seen_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }, (t) => ({ agentIp: uniqueIndex('idx_agent_ip_block_stats_agent_ip').on(t.agentId, t.ip), agentPackets: index('idx_agent_ip_block_stats_agent_packets').on( t.agentId, t.packets, ), }), ) /** Per-(ip, proto, dport) deny hits from Linux nft dynamic concat set. */ export const agentPortBlockStats = sqliteTable( 'agent_port_block_stats', { id: text('id').primaryKey(), agentId: text('agent_id') .notNull() .references(() => agents.id, { onDelete: 'cascade' }), ip: text('ip').notNull(), port: integer('port').notNull(), protocol: text('protocol').notNull(), // tcp | udp packets: integer('packets').notNull().default(0), lastReportedPackets: integer('last_reported_packets').notNull().default(0), firstSeenAt: text('first_seen_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), lastSeenAt: text('last_seen_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }, (t) => ({ agentIpPortProto: uniqueIndex( 'idx_agent_port_block_stats_agent_ip_port_proto', ).on(t.agentId, t.ip, t.port, t.protocol), agentPackets: index('idx_agent_port_block_stats_agent_packets').on( t.agentId, t.packets, ), agentPortProto: index('idx_agent_port_block_stats_agent_port_proto').on( t.agentId, t.port, t.protocol, ), }), ) /** 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 agentId: text('agent_id').references(() => agents.id, { onDelete: 'cascade' }), 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), agentIdx: index('idx_agent_install_links_agent').on(t.agentId), }), ) export const auditLog = sqliteTable( 'audit_log', { id: text('id').primaryKey(), eventId: text('event_id'), sourceApp: text('source_app').notNull().default('fw'), action: text('action').notNull(), severity: text('severity').notNull().default('info'), actorUserId: text('actor_user_id'), actorEmail: text('actor_email'), actorName: text('actor_name'), targetType: text('target_type'), targetId: text('target_id'), summary: text('summary').notNull(), detailsJson: text('details_json'), ip: text('ip'), createdAt: text('created_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }, (t) => ({ eventIdIdx: uniqueIndex('idx_audit_log_event_id').on(t.eventId), createdAtIdx: index('idx_audit_log_created_at').on(t.createdAt), actionIdx: index('idx_audit_log_action').on(t.action), }), ) export const SHARED_POLICY_SET_ID = 'set-shared-default' export const schema = { settings, agents, ipLists, ipListEntries, policySets, agentPolicySets, policyRules, policyRuleResolved, ipOverrides, agentStatsSamples, agentIpBlockStats, agentPortBlockStats, agentInstallLinks, auditLog, }