Spaces:
Paused
Paused
File size: 5,039 Bytes
35743bd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | import { v4 as uuidv4 } from "uuid";
import { getDbInstance, rowToCamel } from "./core";
import { backupDbFile } from "./backup";
type JsonRecord = Record<string, unknown>;
export interface SyncTokenRecord {
id: string;
name: string;
tokenHash: string;
syncApiKeyId: string | null;
revokedAt: string | null;
lastUsedAt: string | null;
createdAt: string;
updatedAt: string;
}
interface StatementLike<TRow = unknown> {
all: (...params: unknown[]) => TRow[];
get: (...params: unknown[]) => TRow | undefined;
run: (...params: unknown[]) => { changes?: number };
}
interface DbLike {
prepare: <TRow = unknown>(sql: string) => StatementLike<TRow>;
exec: (sql: string) => void;
}
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toSyncTokenRecord(value: unknown): SyncTokenRecord | null {
const record = asRecord(rowToCamel(value));
if (typeof record.id !== "string" || typeof record.name !== "string") {
return null;
}
return {
id: record.id,
name: record.name,
tokenHash: typeof record.tokenHash === "string" ? record.tokenHash : "",
syncApiKeyId: typeof record.syncApiKeyId === "string" ? record.syncApiKeyId : null,
revokedAt: typeof record.revokedAt === "string" ? record.revokedAt : null,
lastUsedAt: typeof record.lastUsedAt === "string" ? record.lastUsedAt : null,
createdAt: typeof record.createdAt === "string" ? record.createdAt : new Date().toISOString(),
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : new Date().toISOString(),
};
}
function ensureSyncTokensTable(db: DbLike) {
db.exec(`
CREATE TABLE IF NOT EXISTS sync_tokens (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
sync_api_key_id TEXT,
revoked_at TEXT,
last_used_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_sync_tokens_created_at ON sync_tokens(created_at);
CREATE INDEX IF NOT EXISTS idx_sync_tokens_last_used_at ON sync_tokens(last_used_at);
CREATE INDEX IF NOT EXISTS idx_sync_tokens_revoked_at ON sync_tokens(revoked_at);
CREATE INDEX IF NOT EXISTS idx_sync_tokens_sync_api_key_id ON sync_tokens(sync_api_key_id);
`);
}
export async function listSyncTokens() {
const db = getDbInstance() as unknown as DbLike;
ensureSyncTokensTable(db);
const rows = db
.prepare(
"SELECT * FROM sync_tokens ORDER BY datetime(created_at) DESC, name COLLATE NOCASE ASC"
)
.all();
return rows
.map((row) => toSyncTokenRecord(row))
.filter((row): row is SyncTokenRecord => row !== null);
}
export async function getSyncTokenById(id: string) {
const db = getDbInstance() as unknown as DbLike;
ensureSyncTokensTable(db);
const row = db.prepare("SELECT * FROM sync_tokens WHERE id = ?").get(id);
return toSyncTokenRecord(row);
}
export async function getSyncTokenByHash(tokenHash: string) {
const db = getDbInstance() as unknown as DbLike;
ensureSyncTokensTable(db);
const row = db.prepare("SELECT * FROM sync_tokens WHERE token_hash = ?").get(tokenHash);
return toSyncTokenRecord(row);
}
export async function createSyncTokenRecord(data: {
name: string;
tokenHash: string;
syncApiKeyId?: string | null;
}) {
const db = getDbInstance() as unknown as DbLike;
ensureSyncTokensTable(db);
const now = new Date().toISOString();
const record: SyncTokenRecord = {
id: uuidv4(),
name: data.name,
tokenHash: data.tokenHash,
syncApiKeyId: data.syncApiKeyId || null,
revokedAt: null,
lastUsedAt: null,
createdAt: now,
updatedAt: now,
};
db.prepare(
`INSERT INTO sync_tokens (
id, name, token_hash, sync_api_key_id, revoked_at, last_used_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run(
record.id,
record.name,
record.tokenHash,
record.syncApiKeyId,
record.revokedAt,
record.lastUsedAt,
record.createdAt,
record.updatedAt
);
backupDbFile("pre-write");
return record;
}
export async function revokeSyncToken(id: string) {
const db = getDbInstance() as unknown as DbLike;
ensureSyncTokensTable(db);
const existing = await getSyncTokenById(id);
if (!existing) return null;
if (existing.revokedAt) return existing;
const now = new Date().toISOString();
db.prepare("UPDATE sync_tokens SET revoked_at = ?, updated_at = ? WHERE id = ?").run(
now,
now,
id
);
backupDbFile("pre-write");
return await getSyncTokenById(id);
}
export async function touchSyncTokenLastUsed(id: string, usedAt = new Date().toISOString()) {
const db = getDbInstance() as unknown as DbLike;
ensureSyncTokensTable(db);
const result = db
.prepare("UPDATE sync_tokens SET last_used_at = ?, updated_at = ? WHERE id = ?")
.run(usedAt, usedAt, id);
return Number(result.changes || 0) > 0;
}
|