diff --git a/src/handlers/accounts.ts b/src/handlers/accounts.ts index 49ef7af..dd78d1c 100644 --- a/src/handlers/accounts.ts +++ b/src/handlers/accounts.ts @@ -366,9 +366,20 @@ export async function handleRegister(request: Request, env: Env): Promise { + void userId; const now = new Date().toISOString(); const result = await db .prepare( - "UPDATE invites SET status = 'used', used_by = ?, updated_at = ? WHERE code = ? AND status = 'active' AND expires_at > ?" + "UPDATE invites SET status = 'used', used_by = NULL, updated_at = ? WHERE code = ? AND status = 'active' AND expires_at > ?" ) - .bind(userId, now, code, now) + .bind(now, code, now) + .run(); + return (result.meta.changes ?? 0) > 0; +} + +export async function assignInviteUsedBy(db: D1Database, code: string, userId: string): Promise { + const now = new Date().toISOString(); + const result = await db + .prepare( + "UPDATE invites SET used_by = ?, updated_at = ? WHERE code = ? AND status = 'used' AND used_by IS NULL" + ) + .bind(userId, now, code) .run(); return (result.meta.changes ?? 0) > 0; } export async function revertInviteUsed(db: D1Database, code: string, userId: string): Promise { + void userId; const now = new Date().toISOString(); const result = await db .prepare( - "UPDATE invites SET status = 'active', used_by = NULL, updated_at = ? WHERE code = ? AND status = 'used' AND used_by = ?" + "UPDATE invites SET status = 'active', used_by = NULL, updated_at = ? WHERE code = ? AND status = 'used' AND used_by IS NULL" ) - .bind(now, code, userId) + .bind(now, code) .run(); return (result.meta.changes ?? 0) > 0; } diff --git a/src/services/storage-schema.ts b/src/services/storage-schema.ts index bdf6458..0192f93 100644 --- a/src/services/storage-schema.ts +++ b/src/services/storage-schema.ts @@ -78,6 +78,7 @@ const SCHEMA_STATEMENTS: readonly string[] = [ 'code TEXT PRIMARY KEY, created_by TEXT NOT NULL, used_by TEXT, expires_at TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, ' + 'FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE, ' + 'FOREIGN KEY (used_by) REFERENCES users(id) ON DELETE SET NULL)', + 'ALTER TABLE invites ADD COLUMN used_by TEXT', 'CREATE INDEX IF NOT EXISTS idx_invites_status_expires ON invites(status, expires_at)', 'CREATE INDEX IF NOT EXISTS idx_invites_created_by ON invites(created_by, created_at)', diff --git a/src/services/storage.ts b/src/services/storage.ts index 70efe29..337497b 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -22,6 +22,7 @@ import { type AuditLogListOptions, createAuditLog as createStoredAuditLog, clearAuditLogs as clearStoredAuditLogs, + assignInviteUsedBy as assignStoredInviteUsedBy, createInvite as createStoredInvite, deleteAllInvites as deleteStoredInvites, getInvite as findStoredInvite, @@ -149,7 +150,7 @@ const STORAGE_SCHEMA_VERSION_KEY = 'schema.version'; // Bump this whenever src/services/storage-schema.ts or migrations/0001_init.sql // changes. Existing D1 installs only rerun ensureStorageSchema() when this value // differs from config.schema.version. -const STORAGE_SCHEMA_VERSION = '2026-06-22-push-notifications'; +const STORAGE_SCHEMA_VERSION = '2026-06-23-invite-used-by'; const REQUIRED_SCHEMA_TABLES = ['webauthn_credentials', 'webauthn_challenges', 'auth_requests'] as const; // D1-backed storage. @@ -314,6 +315,10 @@ export class StorageService { return markStoredInviteUsed(this.db, code, userId); } + async assignInviteUsedBy(code: string, userId: string): Promise { + return assignStoredInviteUsedBy(this.db, code, userId); + } + async revertInviteUsed(code: string, userId: string): Promise { return revertStoredInviteUsed(this.db, code, userId); } diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index dca5581..d27aa54 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -2046,11 +2046,15 @@ export default function App() { }), onSaveBackupSettings: async (masterPassword: string, settings: AdminBackupSettings) => { const hash = await deriveCurrentMasterPasswordHash(masterPassword); - return backupActions.saveSettings(hash, settings); + const saved = await backupActions.saveSettings(hash, settings); + queryClient.setQueryData(['admin-backup-settings', vaultCacheKey], saved); + return saved; }, onRunRemoteBackup: async (masterPassword: string, destinationId?: string | null) => { const hash = await deriveCurrentMasterPasswordHash(masterPassword); - return backupActions.runRemoteBackup(hash, destinationId); + const result = await backupActions.runRemoteBackup(hash, destinationId); + queryClient.setQueryData(['admin-backup-settings', vaultCacheKey], result.settings); + return result; }, onListRemoteBackups: backupActions.listRemoteBackups, onDownloadRemoteBackup: async (masterPassword: string, destinationId: string, path: string, onProgress?: (percent: number | null) => void) => {