feat: añadir migración calendar-tokens y servicio ICS de tokens
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>webui
parent
c3737b967b
commit
73ae69892f
@ -0,0 +1,109 @@
|
||||
import { getDb } from './db';
|
||||
import { randomTokenBase64Url, sha256Hex } from './crypto';
|
||||
import { WEB_BASE_URL } from './env';
|
||||
|
||||
export type CalendarTokenType = 'personal' | 'group' | 'aggregate';
|
||||
|
||||
function toIsoSql(d: Date = new Date()): string {
|
||||
return d.toISOString().replace('T', ' ').replace('Z', '');
|
||||
}
|
||||
|
||||
function requireBaseUrl(): string {
|
||||
const base = (WEB_BASE_URL || '').trim();
|
||||
if (!base) {
|
||||
throw new Error('[calendar-tokens] WEB_BASE_URL no está configurado');
|
||||
}
|
||||
return base.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export function buildCalendarIcsUrl(type: CalendarTokenType, token: string): string {
|
||||
const base = requireBaseUrl();
|
||||
const segment = type === 'personal' ? 'personal' : type === 'group' ? 'group' : 'aggregate';
|
||||
return `${base}/ics/${segment}/${token}.ics`;
|
||||
}
|
||||
|
||||
export async function findActiveToken(
|
||||
type: CalendarTokenType,
|
||||
userId: string,
|
||||
groupId?: string | null
|
||||
): Promise<{
|
||||
id: number;
|
||||
type: CalendarTokenType;
|
||||
user_id: string;
|
||||
group_id: string | null;
|
||||
token_hash: string;
|
||||
created_at: string;
|
||||
revoked_at: string | null;
|
||||
last_used_at: string | null;
|
||||
} | null> {
|
||||
const db = await getDb();
|
||||
const sql = groupId
|
||||
? `
|
||||
SELECT id, type, user_id, group_id, token_hash, created_at, revoked_at, last_used_at
|
||||
FROM calendar_tokens
|
||||
WHERE type = ? AND user_id = ? AND group_id = ? AND revoked_at IS NULL
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`
|
||||
: `
|
||||
SELECT id, type, user_id, group_id, token_hash, created_at, revoked_at, last_used_at
|
||||
FROM calendar_tokens
|
||||
WHERE type = ? AND user_id = ? AND group_id IS NULL AND revoked_at IS NULL
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
const row = groupId
|
||||
? (await db.query(sql).get(type, userId, groupId))
|
||||
: (await db.query(sql).get(type, userId));
|
||||
return (row as any) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un nuevo token ICS y devuelve la URL completa (no se guarda el token en claro).
|
||||
* Lanza si existe una entrada activa y se viola la unicidad; usar findActiveToken antes si quieres evitar error.
|
||||
*/
|
||||
export async function createCalendarTokenUrl(
|
||||
type: CalendarTokenType,
|
||||
userId: string,
|
||||
groupId?: string | null
|
||||
): Promise<{ url: string; token: string; id: number }> {
|
||||
const db = await getDb();
|
||||
|
||||
const token = randomTokenBase64Url(32);
|
||||
const tokenHash = await sha256Hex(token);
|
||||
const createdAt = toIsoSql(new Date());
|
||||
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO calendar_tokens (type, user_id, group_id, token_hash, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
const res = insert.run(type, userId, groupId ?? null, tokenHash, createdAt);
|
||||
const id = Number(res.lastInsertRowid || 0);
|
||||
|
||||
return { url: buildCalendarIcsUrl(type, token), token, id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoca el token activo (si existe) y crea uno nuevo. Devuelve la nueva URL completa.
|
||||
*/
|
||||
export async function rotateCalendarTokenUrl(
|
||||
type: CalendarTokenType,
|
||||
userId: string,
|
||||
groupId?: string | null
|
||||
): Promise<{ url: string; token: string; id: number; revoked: number | null }> {
|
||||
const db = await getDb();
|
||||
const now = toIsoSql(new Date());
|
||||
|
||||
const existing = await findActiveToken(type, userId, groupId ?? null);
|
||||
let revoked: number | null = null;
|
||||
if (existing) {
|
||||
db.prepare(`UPDATE calendar_tokens SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL`).run(
|
||||
now,
|
||||
existing.id
|
||||
);
|
||||
revoked = existing.id;
|
||||
}
|
||||
|
||||
const created = await createCalendarTokenUrl(type, userId, groupId ?? null);
|
||||
return { ...created, revoked };
|
||||
}
|
||||
Loading…
Reference in New Issue