Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
This commit is contained in:
94
wacrm/src/lib/api-keys/store.ts
Normal file
94
wacrm/src/lib/api-keys/store.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
// ============================================================
|
||||
// API key store — the *auth-path* data access for public API keys.
|
||||
//
|
||||
// Only the read side lives here, and deliberately so: it runs with
|
||||
// the service-role client because a public-API caller has no Supabase
|
||||
// session, so RLS (which keys off `auth.uid()`) can't scope the
|
||||
// lookup. The management side (list / create / revoke) runs in the
|
||||
// dashboard under a real cookie session and goes through the RLS
|
||||
// client *inline* in the route handlers — same pattern as
|
||||
// `/api/account/invitations`. Keeping the RLS-bypassing surface tiny
|
||||
// and read-only here makes it easy to audit.
|
||||
// ============================================================
|
||||
|
||||
import { supabaseAdmin } from '@/lib/flows/admin-client';
|
||||
|
||||
/** Shape of an `api_keys` row as the auth path consumes it. */
|
||||
export interface ApiKeyRow {
|
||||
id: string;
|
||||
account_id: string;
|
||||
created_by: string | null;
|
||||
name: string;
|
||||
scopes: string[];
|
||||
expires_at: string | null;
|
||||
revoked_at: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up an *active* key by its SHA-256 hash. Returns null if no
|
||||
* row matches, or if the matching row is revoked or expired — so
|
||||
* callers never have to re-check liveness. Uses the service-role
|
||||
* client (RLS-bypassing); the hash is the only credential, so this
|
||||
* is the moment that establishes the caller's account.
|
||||
*/
|
||||
export async function findActiveKeyByHash(
|
||||
hash: string
|
||||
): Promise<ApiKeyRow | null> {
|
||||
const { data, error } = await supabaseAdmin()
|
||||
.from('api_keys')
|
||||
.select('id, account_id, created_by, name, scopes, expires_at, revoked_at')
|
||||
.eq('key_hash', hash)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
console.error('[api-keys/store] lookup error:', error.message);
|
||||
return null;
|
||||
}
|
||||
if (!data) return null;
|
||||
|
||||
// Liveness checks in JS rather than SQL so the failure modes are
|
||||
// explicit and the index stays a simple equality lookup.
|
||||
if (data.revoked_at) return null;
|
||||
if (data.expires_at && new Date(data.expires_at).getTime() <= Date.now()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as ApiKeyRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the account name for a resolved key, so `/api/v1/me` and any
|
||||
* future endpoint can echo it without a second round trip in the
|
||||
* route. Service-role; the key already proved account membership.
|
||||
*/
|
||||
export async function getAccountName(
|
||||
accountId: string
|
||||
): Promise<string | null> {
|
||||
const { data, error } = await supabaseAdmin()
|
||||
.from('accounts')
|
||||
.select('name')
|
||||
.eq('id', accountId)
|
||||
.maybeSingle();
|
||||
if (error || !data) return null;
|
||||
return (data.name as string) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort `last_used_at` bump. Fire-and-forget from the auth
|
||||
* path — a failed update just means the "last used" column lags;
|
||||
* it must never fail the request the caller is actually making.
|
||||
*/
|
||||
export function touchLastUsed(id: string): void {
|
||||
void supabaseAdmin()
|
||||
.from('api_keys')
|
||||
.update({ last_used_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.then(({ error }) => {
|
||||
if (error) {
|
||||
console.warn(
|
||||
'[api-keys/store] last_used_at bump failed:',
|
||||
error.message
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user