@authrim/setup 0.1.134 → 0.1.136

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ -- =============================================================================
2
+ -- Migration: Admin Security - IP Allowlist (D1_ADMIN)
3
+ -- =============================================================================
4
+ -- Created: 2025-01-22
5
+ -- Description: Creates admin_ip_allowlist table for IP-based access control.
6
+ -- Provides network-level security for Admin access.
7
+ --
8
+ -- IMPORTANT: This migration is for D1_ADMIN (dedicated Admin database).
9
+ -- Implements IP restriction for Admin panel access.
10
+ --
11
+ -- Architecture:
12
+ -- - admin_ip_allowlist: IP addresses/ranges allowed to access Admin
13
+ -- - Empty list = all IPs allowed (default behavior)
14
+ -- - Supports CIDR notation (192.168.1.0/24) and single IPs (10.0.0.1)
15
+ -- =============================================================================
16
+
17
+ -- =============================================================================
18
+ -- admin_ip_allowlist Table
19
+ -- =============================================================================
20
+ -- IP-based access control for Admin panel.
21
+ -- When the table is empty, all IPs are allowed.
22
+ -- When entries exist, only matching IPs can access Admin.
23
+ --
24
+ -- IP formats supported:
25
+ -- - Single IPv4: 192.168.1.100
26
+ -- - IPv4 CIDR: 192.168.1.0/24
27
+ -- - Single IPv6: 2001:db8::1
28
+ -- - IPv6 CIDR: 2001:db8::/32
29
+ -- =============================================================================
30
+
31
+ CREATE TABLE IF NOT EXISTS admin_ip_allowlist (
32
+ -- Entry ID (UUID v4)
33
+ id TEXT PRIMARY KEY,
34
+
35
+ -- Multi-tenant support
36
+ tenant_id TEXT NOT NULL DEFAULT 'default',
37
+
38
+ -- IP address or CIDR range
39
+ ip_range TEXT NOT NULL,
40
+
41
+ -- IP version for easier filtering
42
+ ip_version INTEGER NOT NULL DEFAULT 4, -- 4 or 6
43
+
44
+ -- Human-readable description
45
+ description TEXT, -- e.g., 'Office VPN', 'Home IP', 'CI/CD server'
46
+
47
+ -- Enable/disable without deleting
48
+ enabled INTEGER DEFAULT 1,
49
+
50
+ -- Audit fields
51
+ created_by TEXT, -- Admin user ID who added this entry
52
+ created_at INTEGER NOT NULL,
53
+ updated_at INTEGER NOT NULL,
54
+
55
+ -- Unique constraint for IP range per tenant
56
+ UNIQUE(tenant_id, ip_range)
57
+ );
58
+
59
+ -- =============================================================================
60
+ -- Indexes for admin_ip_allowlist
61
+ -- =============================================================================
62
+
63
+ -- Tenant-scoped lookup (main query pattern)
64
+ CREATE INDEX IF NOT EXISTS idx_admin_ip_allowlist_tenant ON admin_ip_allowlist(tenant_id, enabled);
65
+
66
+ -- IP version filtering (for IPv4/IPv6 specific queries)
67
+ CREATE INDEX IF NOT EXISTS idx_admin_ip_allowlist_version ON admin_ip_allowlist(tenant_id, ip_version, enabled);
68
+
69
+ -- Enabled entries only (for authorization checks)
70
+ CREATE INDEX IF NOT EXISTS idx_admin_ip_allowlist_enabled ON admin_ip_allowlist(enabled, tenant_id);
71
+
72
+ -- =============================================================================
73
+ -- admin_login_attempts Table (Optional - for rate limiting)
74
+ -- =============================================================================
75
+ -- Tracks failed login attempts for rate limiting and security monitoring.
76
+ -- Used to implement progressive delays and account lockout.
77
+ -- =============================================================================
78
+
79
+ CREATE TABLE IF NOT EXISTS admin_login_attempts (
80
+ -- Attempt ID (UUID v4)
81
+ id TEXT PRIMARY KEY,
82
+
83
+ -- Multi-tenant support
84
+ tenant_id TEXT NOT NULL DEFAULT 'default',
85
+
86
+ -- Target email (even if user doesn't exist)
87
+ email TEXT NOT NULL,
88
+
89
+ -- Request context
90
+ ip_address TEXT NOT NULL,
91
+ user_agent TEXT,
92
+
93
+ -- Result
94
+ success INTEGER NOT NULL DEFAULT 0, -- 0 = failed, 1 = success
95
+ failure_reason TEXT, -- e.g., 'invalid_password', 'user_not_found', 'account_locked'
96
+
97
+ -- Timestamp
98
+ created_at INTEGER NOT NULL
99
+ );
100
+
101
+ -- =============================================================================
102
+ -- Indexes for admin_login_attempts
103
+ -- =============================================================================
104
+
105
+ -- Email-based lookup (for rate limiting per email)
106
+ CREATE INDEX IF NOT EXISTS idx_admin_login_attempts_email ON admin_login_attempts(tenant_id, email, created_at DESC);
107
+
108
+ -- IP-based lookup (for rate limiting per IP)
109
+ CREATE INDEX IF NOT EXISTS idx_admin_login_attempts_ip ON admin_login_attempts(ip_address, created_at DESC);
110
+
111
+ -- Time-based cleanup
112
+ CREATE INDEX IF NOT EXISTS idx_admin_login_attempts_time ON admin_login_attempts(created_at);
113
+
114
+ -- Success tracking (for security monitoring)
115
+ CREATE INDEX IF NOT EXISTS idx_admin_login_attempts_success ON admin_login_attempts(success, created_at DESC);
116
+
117
+ -- =============================================================================
118
+ -- Migration Complete
119
+ -- =============================================================================
120
+ -- IP allowlist is now ready for use.
121
+ --
122
+ -- Usage:
123
+ -- 1. When admin_ip_allowlist is empty for a tenant, all IPs are allowed
124
+ -- 2. When entries exist, only enabled entries are checked
125
+ -- 3. Client IP is obtained from CF-Connecting-IP header (Cloudflare)
126
+ -- 4. CIDR matching is done in application code
127
+ --
128
+ -- Security notes:
129
+ -- - Always use CF-Connecting-IP for real client IP (not X-Forwarded-For)
130
+ -- - Consider adding office VPN and CI/CD IPs before restricting
131
+ -- - Keep at least one admin with IP access to prevent lockout
132
+ -- =============================================================================
@@ -0,0 +1,345 @@
1
+ -- =============================================================================
2
+ -- Migration: Admin ABAC & ReBAC (D1_ADMIN)
3
+ -- =============================================================================
4
+ -- Created: 2025-01-22
5
+ -- Description: Creates tables for Admin ABAC (Attribute-Based Access Control)
6
+ -- and ReBAC (Relationship-Based Access Control).
7
+ --
8
+ -- IMPORTANT: This migration is for D1_ADMIN (dedicated Admin database).
9
+ -- Separate from EndUser ABAC/ReBAC in D1_CORE.
10
+ --
11
+ -- Architecture:
12
+ -- - admin_attributes: Attribute type definitions
13
+ -- - admin_attribute_values: Attribute values assigned to Admin users
14
+ -- - admin_relationships: Relationships between Admin users/entities
15
+ -- - admin_policies: Policy definitions combining RBAC/ABAC/ReBAC
16
+ -- =============================================================================
17
+
18
+ -- =============================================================================
19
+ -- admin_attributes Table
20
+ -- =============================================================================
21
+ -- Attribute definitions for Admin ABAC.
22
+ -- Examples: department, location, clearance_level, project_access
23
+ -- =============================================================================
24
+
25
+ CREATE TABLE IF NOT EXISTS admin_attributes (
26
+ -- Attribute ID (UUID v4)
27
+ id TEXT PRIMARY KEY,
28
+
29
+ -- Multi-tenant support
30
+ tenant_id TEXT NOT NULL DEFAULT 'default',
31
+
32
+ -- Attribute identification
33
+ name TEXT NOT NULL, -- Machine-readable name (e.g., 'department')
34
+ display_name TEXT, -- Human-readable name (e.g., 'Department')
35
+ description TEXT,
36
+
37
+ -- Attribute type (determines value validation)
38
+ -- string: Free-form text
39
+ -- enum: Must be one of allowed_values
40
+ -- number: Numeric value (with optional min/max)
41
+ -- boolean: true/false
42
+ -- date: ISO 8601 date
43
+ -- array: Multiple values allowed
44
+ attribute_type TEXT NOT NULL DEFAULT 'string',
45
+
46
+ -- For enum type: JSON array of allowed values
47
+ -- e.g., ["engineering", "sales", "support"]
48
+ allowed_values_json TEXT,
49
+
50
+ -- Validation constraints
51
+ min_value INTEGER, -- For number type
52
+ max_value INTEGER, -- For number type
53
+ regex_pattern TEXT, -- For string type
54
+
55
+ -- Whether this attribute is required for all Admin users
56
+ is_required INTEGER DEFAULT 0,
57
+
58
+ -- Whether this attribute can have multiple values
59
+ is_multi_valued INTEGER DEFAULT 0,
60
+
61
+ -- System attribute flag (cannot be modified or deleted)
62
+ is_system INTEGER DEFAULT 0,
63
+
64
+ -- Lifecycle
65
+ created_at INTEGER NOT NULL,
66
+ updated_at INTEGER NOT NULL,
67
+
68
+ -- Unique constraint for attribute name per tenant
69
+ UNIQUE(tenant_id, name)
70
+ );
71
+
72
+ -- =============================================================================
73
+ -- Indexes for admin_attributes
74
+ -- =============================================================================
75
+
76
+ CREATE INDEX IF NOT EXISTS idx_admin_attributes_tenant ON admin_attributes(tenant_id);
77
+ CREATE INDEX IF NOT EXISTS idx_admin_attributes_name ON admin_attributes(tenant_id, name);
78
+ CREATE INDEX IF NOT EXISTS idx_admin_attributes_type ON admin_attributes(attribute_type);
79
+
80
+ -- =============================================================================
81
+ -- admin_attribute_values Table
82
+ -- =============================================================================
83
+ -- Attribute values assigned to Admin users.
84
+ -- Links admin_users to admin_attributes with specific values.
85
+ -- =============================================================================
86
+
87
+ CREATE TABLE IF NOT EXISTS admin_attribute_values (
88
+ -- Value assignment ID (UUID v4)
89
+ id TEXT PRIMARY KEY,
90
+
91
+ -- Multi-tenant support
92
+ tenant_id TEXT NOT NULL DEFAULT 'default',
93
+
94
+ -- References
95
+ admin_user_id TEXT NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
96
+ admin_attribute_id TEXT NOT NULL REFERENCES admin_attributes(id) ON DELETE CASCADE,
97
+
98
+ -- The actual value (stored as text, parsed according to attribute_type)
99
+ value TEXT NOT NULL,
100
+
101
+ -- For multi-valued attributes, this is the index (0, 1, 2, ...)
102
+ value_index INTEGER DEFAULT 0,
103
+
104
+ -- Source of this value (manual, idp_sync, api, etc.)
105
+ source TEXT DEFAULT 'manual',
106
+
107
+ -- Expiration (for temporary attribute assignments)
108
+ expires_at INTEGER,
109
+
110
+ -- Audit fields
111
+ assigned_by TEXT, -- Admin user ID who assigned this value
112
+ created_at INTEGER NOT NULL,
113
+ updated_at INTEGER NOT NULL,
114
+
115
+ -- Unique constraint for single-valued attributes
116
+ -- For multi-valued, use UNIQUE(admin_user_id, admin_attribute_id, value_index)
117
+ UNIQUE(admin_user_id, admin_attribute_id, value_index)
118
+ );
119
+
120
+ -- =============================================================================
121
+ -- Indexes for admin_attribute_values
122
+ -- =============================================================================
123
+
124
+ CREATE INDEX IF NOT EXISTS idx_admin_attr_values_user ON admin_attribute_values(admin_user_id);
125
+ CREATE INDEX IF NOT EXISTS idx_admin_attr_values_attr ON admin_attribute_values(admin_attribute_id);
126
+ CREATE INDEX IF NOT EXISTS idx_admin_attr_values_tenant ON admin_attribute_values(tenant_id);
127
+ CREATE INDEX IF NOT EXISTS idx_admin_attr_values_expires ON admin_attribute_values(expires_at);
128
+
129
+ -- Combined index for policy evaluation
130
+ CREATE INDEX IF NOT EXISTS idx_admin_attr_values_lookup
131
+ ON admin_attribute_values(admin_user_id, admin_attribute_id, value);
132
+
133
+ -- =============================================================================
134
+ -- admin_relationships Table
135
+ -- =============================================================================
136
+ -- Relationships between Admin users/entities for ReBAC.
137
+ -- Examples: manager_of, delegate_of, team_member
138
+ -- =============================================================================
139
+
140
+ CREATE TABLE IF NOT EXISTS admin_relationships (
141
+ -- Relationship ID (UUID v4)
142
+ id TEXT PRIMARY KEY,
143
+
144
+ -- Multi-tenant support
145
+ tenant_id TEXT NOT NULL DEFAULT 'default',
146
+
147
+ -- Relationship type (e.g., 'manager_of', 'delegate_of', 'team_member')
148
+ relationship_type TEXT NOT NULL,
149
+
150
+ -- Source entity (from)
151
+ from_type TEXT NOT NULL DEFAULT 'admin_user', -- admin_user, admin_role, team
152
+ from_id TEXT NOT NULL,
153
+
154
+ -- Target entity (to)
155
+ to_type TEXT NOT NULL DEFAULT 'admin_user', -- admin_user, admin_role, team
156
+ to_id TEXT NOT NULL,
157
+
158
+ -- Permission level granted by this relationship
159
+ -- full: All permissions of target
160
+ -- limited: Subset of permissions
161
+ -- read_only: Read-only access
162
+ permission_level TEXT NOT NULL DEFAULT 'full',
163
+
164
+ -- For hierarchical relationships (e.g., transitive manager relationship)
165
+ is_transitive INTEGER DEFAULT 0,
166
+
167
+ -- Expiration (for temporary relationships)
168
+ expires_at INTEGER,
169
+
170
+ -- Bidirectional flag (if true, relationship works both ways)
171
+ is_bidirectional INTEGER DEFAULT 0,
172
+
173
+ -- Additional metadata (JSON)
174
+ metadata_json TEXT,
175
+
176
+ -- Audit fields
177
+ created_by TEXT, -- Admin user ID who created this relationship
178
+ created_at INTEGER NOT NULL,
179
+ updated_at INTEGER NOT NULL
180
+ );
181
+
182
+ -- =============================================================================
183
+ -- Indexes for admin_relationships
184
+ -- =============================================================================
185
+
186
+ CREATE INDEX IF NOT EXISTS idx_admin_rel_tenant ON admin_relationships(tenant_id);
187
+ CREATE INDEX IF NOT EXISTS idx_admin_rel_from ON admin_relationships(from_type, from_id);
188
+ CREATE INDEX IF NOT EXISTS idx_admin_rel_to ON admin_relationships(to_type, to_id);
189
+ CREATE INDEX IF NOT EXISTS idx_admin_rel_type ON admin_relationships(relationship_type);
190
+ CREATE INDEX IF NOT EXISTS idx_admin_rel_expires ON admin_relationships(expires_at);
191
+
192
+ -- Unique constraint: prevent duplicate relationships
193
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_admin_rel_unique
194
+ ON admin_relationships(tenant_id, relationship_type, from_type, from_id, to_type, to_id);
195
+
196
+ -- =============================================================================
197
+ -- admin_policies Table
198
+ -- =============================================================================
199
+ -- Policy definitions combining RBAC/ABAC/ReBAC conditions.
200
+ -- Policies define access rules using role, attribute, and relationship conditions.
201
+ -- =============================================================================
202
+
203
+ CREATE TABLE IF NOT EXISTS admin_policies (
204
+ -- Policy ID (UUID v4)
205
+ id TEXT PRIMARY KEY,
206
+
207
+ -- Multi-tenant support
208
+ tenant_id TEXT NOT NULL DEFAULT 'default',
209
+
210
+ -- Policy identification
211
+ name TEXT NOT NULL, -- Machine-readable name
212
+ display_name TEXT, -- Human-readable name
213
+ description TEXT,
214
+
215
+ -- Policy effect: allow or deny
216
+ effect TEXT NOT NULL DEFAULT 'allow', -- allow, deny
217
+
218
+ -- Priority (higher = evaluated first, useful for deny policies)
219
+ priority INTEGER DEFAULT 0,
220
+
221
+ -- Resource this policy applies to (supports wildcards)
222
+ -- e.g., "admin:users:*", "admin:settings:security", "admin:*"
223
+ resource_pattern TEXT NOT NULL,
224
+
225
+ -- Actions this policy applies to (supports wildcards)
226
+ -- e.g., ["read", "write"], ["*"]
227
+ actions_json TEXT NOT NULL DEFAULT '["*"]',
228
+
229
+ -- Conditions (JSON object with RBAC/ABAC/ReBAC conditions)
230
+ -- Format:
231
+ -- {
232
+ -- "roles": ["admin", "security_admin"], // RBAC: Any of these roles
233
+ -- "attributes": { // ABAC: Attribute conditions
234
+ -- "department": {"equals": "engineering"},
235
+ -- "clearance_level": {"gte": 3}
236
+ -- },
237
+ -- "relationships": { // ReBAC: Relationship conditions
238
+ -- "manager_of": {"target_type": "admin_user"}
239
+ -- },
240
+ -- "condition_type": "all" // "all" (AND) or "any" (OR)
241
+ -- }
242
+ conditions_json TEXT NOT NULL DEFAULT '{}',
243
+
244
+ -- Whether this policy is active
245
+ is_active INTEGER DEFAULT 1,
246
+
247
+ -- System policy flag (cannot be modified or deleted)
248
+ is_system INTEGER DEFAULT 0,
249
+
250
+ -- Lifecycle
251
+ created_at INTEGER NOT NULL,
252
+ updated_at INTEGER NOT NULL,
253
+
254
+ -- Unique constraint for policy name per tenant
255
+ UNIQUE(tenant_id, name)
256
+ );
257
+
258
+ -- =============================================================================
259
+ -- Indexes for admin_policies
260
+ -- =============================================================================
261
+
262
+ CREATE INDEX IF NOT EXISTS idx_admin_policies_tenant ON admin_policies(tenant_id);
263
+ CREATE INDEX IF NOT EXISTS idx_admin_policies_name ON admin_policies(tenant_id, name);
264
+ CREATE INDEX IF NOT EXISTS idx_admin_policies_resource ON admin_policies(resource_pattern);
265
+ CREATE INDEX IF NOT EXISTS idx_admin_policies_active ON admin_policies(is_active);
266
+ CREATE INDEX IF NOT EXISTS idx_admin_policies_priority ON admin_policies(priority DESC);
267
+
268
+ -- =============================================================================
269
+ -- Default Attributes
270
+ -- =============================================================================
271
+
272
+ -- Department attribute
273
+ INSERT OR IGNORE INTO admin_attributes (
274
+ id, tenant_id, name, display_name, description,
275
+ attribute_type, allowed_values_json, is_required, is_system,
276
+ created_at, updated_at
277
+ ) VALUES (
278
+ 'attr_department',
279
+ 'default',
280
+ 'department',
281
+ 'Department',
282
+ 'The department this admin belongs to',
283
+ 'enum',
284
+ '["engineering", "security", "operations", "support", "management"]',
285
+ 0,
286
+ 1,
287
+ strftime('%s', 'now') * 1000,
288
+ strftime('%s', 'now') * 1000
289
+ );
290
+
291
+ -- Clearance Level attribute
292
+ INSERT OR IGNORE INTO admin_attributes (
293
+ id, tenant_id, name, display_name, description,
294
+ attribute_type, min_value, max_value, is_required, is_system,
295
+ created_at, updated_at
296
+ ) VALUES (
297
+ 'attr_clearance_level',
298
+ 'default',
299
+ 'clearance_level',
300
+ 'Clearance Level',
301
+ 'Security clearance level (1-5, higher = more access)',
302
+ 'number',
303
+ 1,
304
+ 5,
305
+ 0,
306
+ 1,
307
+ strftime('%s', 'now') * 1000,
308
+ strftime('%s', 'now') * 1000
309
+ );
310
+
311
+ -- Location attribute
312
+ INSERT OR IGNORE INTO admin_attributes (
313
+ id, tenant_id, name, display_name, description,
314
+ attribute_type, is_required, is_system,
315
+ created_at, updated_at
316
+ ) VALUES (
317
+ 'attr_location',
318
+ 'default',
319
+ 'location',
320
+ 'Location',
321
+ 'Physical or regional location of the admin',
322
+ 'string',
323
+ 0,
324
+ 1,
325
+ strftime('%s', 'now') * 1000,
326
+ strftime('%s', 'now') * 1000
327
+ );
328
+
329
+ -- =============================================================================
330
+ -- Migration Complete
331
+ -- =============================================================================
332
+ --
333
+ -- This migration adds ABAC and ReBAC support for Admin users:
334
+ --
335
+ -- ABAC (Attribute-Based):
336
+ -- - admin_attributes: Define attribute types (department, clearance_level, etc.)
337
+ -- - admin_attribute_values: Assign values to Admin users
338
+ --
339
+ -- ReBAC (Relationship-Based):
340
+ -- - admin_relationships: Define relationships (manager_of, delegate_of, etc.)
341
+ --
342
+ -- Combined Policies:
343
+ -- - admin_policies: Define access rules using RBAC + ABAC + ReBAC conditions
344
+ --
345
+ -- =============================================================================
@@ -0,0 +1,91 @@
1
+ -- =============================================================================
2
+ -- Migration: Admin Setup Tokens (D1_ADMIN)
3
+ -- =============================================================================
4
+ -- Created: 2025-01-25
5
+ -- Description: Creates admin_setup_tokens table for secure Admin UI passkey
6
+ -- registration during initial setup.
7
+ --
8
+ -- IMPORTANT: This migration is for D1_ADMIN (dedicated Admin database).
9
+ --
10
+ -- Use Case:
11
+ -- - After initial setup on Router, redirect to Admin UI with a setup token
12
+ -- - Admin UI verifies the token and allows passkey registration
13
+ -- - Token is single-use and time-limited for security
14
+ -- =============================================================================
15
+
16
+ -- =============================================================================
17
+ -- admin_setup_tokens Table
18
+ -- =============================================================================
19
+ -- Stores one-time setup tokens for Admin UI passkey registration.
20
+ -- These tokens allow secure passkey registration after initial setup.
21
+ --
22
+ -- Lifecycle:
23
+ -- 1. Created during initial setup (or via CLI for recovery)
24
+ -- 2. Used when admin visits Admin UI /setup/complete?token=xxx
25
+ -- 3. Invalidated after successful passkey registration
26
+ -- 4. Auto-expires after 24 hours
27
+ -- =============================================================================
28
+
29
+ CREATE TABLE IF NOT EXISTS admin_setup_tokens (
30
+ -- Token ID (the actual token value, UUID v4)
31
+ id TEXT PRIMARY KEY,
32
+
33
+ -- Multi-tenant support
34
+ tenant_id TEXT NOT NULL DEFAULT 'default',
35
+
36
+ -- Reference to admin user
37
+ admin_user_id TEXT NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
38
+
39
+ -- Token status
40
+ -- pending: Created, waiting for use
41
+ -- used: Successfully used for passkey registration
42
+ -- expired: Expired without use
43
+ -- revoked: Manually revoked
44
+ status TEXT NOT NULL DEFAULT 'pending',
45
+
46
+ -- Expiration (UNIX timestamp in milliseconds)
47
+ expires_at INTEGER NOT NULL,
48
+
49
+ -- Usage tracking
50
+ used_at INTEGER, -- When the token was used
51
+ used_ip TEXT, -- IP address that used the token
52
+
53
+ -- Audit fields
54
+ created_at INTEGER NOT NULL,
55
+ created_by TEXT -- 'initial_setup' | 'cli' | admin_user_id
56
+ );
57
+
58
+ -- =============================================================================
59
+ -- Indexes for admin_setup_tokens
60
+ -- =============================================================================
61
+
62
+ -- User's tokens lookup (for checking existing tokens)
63
+ CREATE INDEX IF NOT EXISTS idx_admin_setup_tokens_user ON admin_setup_tokens(admin_user_id);
64
+
65
+ -- Tenant-scoped lookup
66
+ CREATE INDEX IF NOT EXISTS idx_admin_setup_tokens_tenant ON admin_setup_tokens(tenant_id);
67
+
68
+ -- Status filter (for cleanup jobs)
69
+ CREATE INDEX IF NOT EXISTS idx_admin_setup_tokens_status ON admin_setup_tokens(status);
70
+
71
+ -- Expiration lookup (for cleanup jobs)
72
+ CREATE INDEX IF NOT EXISTS idx_admin_setup_tokens_expires ON admin_setup_tokens(expires_at);
73
+
74
+ -- =============================================================================
75
+ -- admin_users: Add passkey_setup_completed column
76
+ -- =============================================================================
77
+ -- Track whether the admin user has completed passkey setup on Admin UI.
78
+ -- This is separate from having passkeys - it tracks the initial setup flow.
79
+ -- =============================================================================
80
+
81
+ ALTER TABLE admin_users ADD COLUMN passkey_setup_completed INTEGER DEFAULT 0;
82
+
83
+ -- =============================================================================
84
+ -- Migration Complete
85
+ -- =============================================================================
86
+ -- Usage:
87
+ -- 1. Router creates token during initial setup
88
+ -- 2. Admin visits Admin UI with token
89
+ -- 3. Admin UI verifies token and registers passkey
90
+ -- 4. Token is marked as 'used' and passkey_setup_completed is set to 1
91
+ -- =============================================================================