This commit is contained in:
2026-04-18 23:11:35 +02:00
commit f7cfd2a86e
196 changed files with 35538 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
// Admin-only helpers. Every call is RLS-gated server-side via the
// `current_user_is_admin()` helper + admin-specific policies. Non-admins
// trying to call these still get clean PostgREST 403/empty-result responses.
import type { AppSupabaseClient } from '../supabase/client.js';
// --- Admin settings -------------------------------------------------------
export interface AdminSetting {
key: string;
value: unknown;
updatedAt: string;
}
export async function listAdminSettings(client: AppSupabaseClient): Promise<AdminSetting[]> {
const { data, error } = await client
.from('admin_settings')
.select('key, value, updated_at')
.order('key', { ascending: true });
if (error) throw error;
return (data ?? []).map((r) => ({ key: r.key, value: r.value, updatedAt: r.updated_at }));
}
export async function updateAdminSetting(
client: AppSupabaseClient,
key: string,
value: unknown,
): Promise<void> {
const { error } = await client
.from('admin_settings')
.upsert(
{
key,
value: value as never,
updated_at: new Date().toISOString(),
} as never,
{ onConflict: 'key' },
);
if (error) throw error;
}
// --- Invite codes ---------------------------------------------------------
export interface InviteRecord {
code: string;
createdBy: string | null;
usesLimit: number | null;
usesCount: number;
expiresAt: string | null;
disabled: boolean;
createdAt: string;
}
function mapInvite(row: {
code: string;
created_by: string | null;
uses_limit: number | null;
uses_count: number;
expires_at: string | null;
disabled: boolean;
created_at: string;
}): InviteRecord {
return {
code: row.code,
createdBy: row.created_by,
usesLimit: row.uses_limit,
usesCount: row.uses_count,
expiresAt: row.expires_at,
disabled: row.disabled,
createdAt: row.created_at,
};
}
export async function listInvites(client: AppSupabaseClient): Promise<InviteRecord[]> {
const { data, error } = await client
.from('invites')
.select('code, created_by, uses_limit, uses_count, expires_at, disabled, created_at')
.order('created_at', { ascending: false });
if (error) throw error;
return (data ?? []).map(mapInvite);
}
export interface CreateInviteParams {
code?: string; // omit to auto-generate
usesLimit?: number | null;
expiresAt?: string | null; // ISO
}
const INVITE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
function randomCode(length = 10): string {
const buf =
typeof crypto !== 'undefined' && 'getRandomValues' in crypto
? crypto.getRandomValues(new Uint8Array(length))
: null;
let out = '';
for (let i = 0; i < length; i++) {
const v = buf ? buf[i]! : Math.floor(Math.random() * 256);
const ch = INVITE_ALPHABET[v % INVITE_ALPHABET.length];
if (ch !== undefined) out += ch;
}
return out;
}
export async function createInvite(
client: AppSupabaseClient,
params: CreateInviteParams = {},
): Promise<InviteRecord> {
const { data: session, error: aErr } = await client.auth.getUser();
if (aErr) throw aErr;
if (!session.user) throw new Error('not authenticated');
const code = params.code?.trim() || randomCode(10);
const { data, error } = await client
.from('invites')
.insert({
code,
created_by: session.user.id,
uses_limit: params.usesLimit ?? null,
expires_at: params.expiresAt ?? null,
disabled: false,
} as never)
.select('code, created_by, uses_limit, uses_count, expires_at, disabled, created_at')
.single();
if (error) throw error;
return mapInvite(data as never);
}
export async function setInviteDisabled(
client: AppSupabaseClient,
code: string,
disabled: boolean,
): Promise<void> {
const { error } = await client
.from('invites')
.update({ disabled } as never)
.eq('code', code);
if (error) throw error;
}
export async function deleteInvite(client: AppSupabaseClient, code: string): Promise<void> {
const { error } = await client.from('invites').delete().eq('code', code);
if (error) throw error;
}
// --- User admin ops -------------------------------------------------------
export interface AdminProfileRow {
userId: string;
username: string;
displayName: string;
isAdmin: boolean;
banned: boolean;
blockedFromInviting: boolean;
createdAt: string;
}
export async function listAllProfiles(client: AppSupabaseClient): Promise<AdminProfileRow[]> {
const { data, error } = await client
.from('profiles')
.select('user_id, username, display_name, is_admin, banned, blocked_from_inviting, created_at')
.order('created_at', { ascending: false });
if (error) throw error;
return (data ?? []).map((r) => ({
userId: r.user_id,
username: r.username,
displayName: r.display_name,
isAdmin: r.is_admin,
banned: r.banned,
blockedFromInviting: r.blocked_from_inviting,
createdAt: r.created_at,
}));
}
export type AdminProfileFlag = 'is_admin' | 'banned' | 'blocked_from_inviting';
export async function setUserFlag(
client: AppSupabaseClient,
userId: string,
flag: AdminProfileFlag,
value: boolean,
): Promise<void> {
const patch: Record<string, boolean> = {};
patch[flag] = value;
const { error } = await client
.from('profiles')
.update(patch as never)
.eq('user_id', userId);
if (error) throw error;
}