initial
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user