mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
feat(backend): add standalone data plane
Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
This commit is contained in:
1 parent
01bb8225ae
commit
006761d3b0
294 files changed
+16259
-6138
No files matched your search
@@ -0,0 +1,143 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { Pool } from 'pg';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { PostgresAdminRepository } from '../src/domains/admin/repository.js';
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const isLocal = databaseUrl ? ['127.0.0.1', 'localhost', '::1'].includes(new URL(databaseUrl).hostname) : false;
|
||||
if (databaseUrl && !isLocal) throw new Error('Admin integration tests only permit a localhost DATABASE_URL');
|
||||
|
||||
describe.skipIf(!databaseUrl || !isLocal)('admin repository authorization + audit', () => {
|
||||
const adminPool = new Pool({ connectionString: databaseUrl });
|
||||
const schema = `admin_test_${randomUUID().replaceAll('-', '')}`;
|
||||
const creatorUser = '11111111-1111-4111-8111-111111111111';
|
||||
const superUser = '22222222-2222-4222-8222-222222222222';
|
||||
const ordinaryUser = '33333333-3333-4333-8333-333333333333';
|
||||
const moderatorUser = '44444444-4444-4444-8444-444444444444';
|
||||
let pool: Pool; let repository: PostgresAdminRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
await adminPool.query(`create schema "${schema}"`);
|
||||
const isolated = new URL(databaseUrl!); isolated.searchParams.set('options', `-c search_path=${schema}`);
|
||||
pool = new Pool({ connectionString: isolated.toString() }); repository = new PostgresAdminRepository(isolated.toString());
|
||||
for (const name of ['0000_identity_and_feature_interests.sql','0001_couples.sql','0002_packs_catalog_progress.sql','0003_answers_matches.sql','0004_chat.sql','0005_profile_settings.sql','0006_media_storage.sql','0007_billing_redemption.sql','0008_residual_realtime.sql','0009_operations_workers.sql','0010_admin.sql']) {
|
||||
const migration = await readFile(new URL(`../drizzle/${name}`, import.meta.url), 'utf8');
|
||||
for (const statement of migration.split('--> statement-breakpoint')) if (statement.trim()) await pool.query(statement);
|
||||
}
|
||||
await pool.query('insert into profiles(id,name) values($1,$5),($2,$6),($3,$7),($4,$8)', [creatorUser, superUser, ordinaryUser, moderatorUser, 'Creator', 'Super', 'User', 'Moderator']);
|
||||
await pool.query("insert into admin_users(user_id,role,permissions) values($1,'pack_creator',array['manage_packs']),($2,'super_admin','{}'),($3,'pack_creator',array['view_chats','view_media'])", [creatorUser, superUser, moderatorUser]);
|
||||
});
|
||||
|
||||
afterAll(async () => { await repository.close(); await pool.end(); await adminPool.query(`drop schema "${schema}" cascade`); await adminPool.end(); });
|
||||
|
||||
it('denies ordinary authenticated users and permission escalation', async () => {
|
||||
await expect(repository.principal(ordinaryUser)).rejects.toMatchObject({ code: 'admin_access_denied' });
|
||||
const creator = await repository.principal(creatorUser);
|
||||
await expect(repository.query(creator, 'profiles', {})).rejects.toMatchObject({ code: 'forbidden' });
|
||||
await expect(repository.query(creator, 'audit_logs', {})).rejects.toMatchObject({ code: 'forbidden' });
|
||||
});
|
||||
|
||||
it('allows content permissions but isolates privileged resources', async () => {
|
||||
const creator = await repository.principal(creatorUser);
|
||||
const inserted = await repository.insert(creator, 'question_packs', [{ name: 'Creator pack' }]);
|
||||
const result = await repository.query(creator, 'question_packs', { filters: [{ column: 'id', op: 'eq', value: inserted[0]!.id }] });
|
||||
expect(result).toMatchObject({ count: 1, rows: [expect.objectContaining({ name: 'Creator pack' })] });
|
||||
});
|
||||
|
||||
it('writes immutable audit evidence in the same transaction as mutations', async () => {
|
||||
const creator = await repository.principal(creatorUser);
|
||||
const [pack] = await repository.insert(creator, 'question_packs', [{ name: 'Audited pack' }]);
|
||||
await repository.update(creator, 'question_packs', String(pack!.id), { name: 'Updated pack' });
|
||||
await repository.delete(creator, 'question_packs', String(pack!.id));
|
||||
const audit = await pool.query('select action,actor_user_id,old_values,new_values from audit_logs where record_id=$1 order by created_at', [pack!.id]);
|
||||
expect(audit.rows.map(row => row.action)).toEqual(['INSERT', 'UPDATE', 'DELETE']);
|
||||
expect(audit.rows.every(row => row.actor_user_id === creatorUser)).toBe(true);
|
||||
expect(audit.rows[1]).toMatchObject({ old_values: expect.objectContaining({ name: 'Audited pack' }), new_values: expect.objectContaining({ name: 'Updated pack' }) });
|
||||
});
|
||||
|
||||
it('preserves legacy AI config columns while redacting provider credentials from audit logs', async () => {
|
||||
const principal = await repository.principal(superUser);
|
||||
const [config] = await repository.insert(principal, 'ai_config', [{
|
||||
openrouter_api_key: 'sk-sensitive', default_model: 'openai/gpt-4o-mini',
|
||||
model_generate: 'anthropic/claude-3.5-sonnet', classifier_enabled: true,
|
||||
heuristics_enabled: true, heuristic_min_text_length: 18,
|
||||
}]);
|
||||
expect(config).toMatchObject({
|
||||
openrouter_api_key: 'sk-sensitive', model_generate: 'anthropic/claude-3.5-sonnet',
|
||||
classifier_enabled: true, heuristics_enabled: true, heuristic_min_text_length: 18,
|
||||
});
|
||||
const audit = await pool.query("select new_values from audit_logs where table_name='ai_config' and record_id=$1", [config!.id]);
|
||||
expect(audit.rows[0]?.new_values).toMatchObject({ openrouter_api_key: '[REDACTED]', default_model: 'openai/gpt-4o-mini' });
|
||||
});
|
||||
|
||||
it('grants super admins read access without trusting client role claims', async () => {
|
||||
const principal = await repository.principal(superUser);
|
||||
expect((await repository.query(principal, 'profiles', {})).count).toBe(4);
|
||||
expect((await repository.query(principal, 'audit_logs', {})).count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('honors validated projections and never returns generic secret-bearing fields', async () => {
|
||||
await pool.query("update profiles set push_token='ExponentPushToken[secret]',public_key_jwk='{\"kty\":\"RSA\"}' where id=$1", [ordinaryUser]);
|
||||
const principal = await repository.principal(superUser);
|
||||
const projected = await repository.query(principal, 'profiles', { columns: ['id', 'name'], filters: [{ column: 'id', op: 'eq', value: ordinaryUser }] });
|
||||
expect(projected.rows).toEqual([{ id: ordinaryUser, name: 'User' }]);
|
||||
const defaultRow = (await repository.query(principal, 'profiles', { filters: [{ column: 'id', op: 'eq', value: ordinaryUser }] })).rows[0]!;
|
||||
expect(defaultRow).not.toHaveProperty('push_token');
|
||||
expect(defaultRow).not.toHaveProperty('public_key_jwk');
|
||||
await expect(repository.query(principal, 'profiles', { columns: ['id', 'push_token'] })).rejects.toMatchObject({ code: 'restricted_column' });
|
||||
});
|
||||
|
||||
it('supports null-safe filters and scoped feature-interest aggregates', async () => {
|
||||
await pool.query("insert into feature_interests(user_id,feature) values($1,'better-chat'),($2,'better-chat')", [creatorUser, ordinaryUser]);
|
||||
const principal = await repository.principal(superUser);
|
||||
expect((await repository.query(principal, 'profiles', { filters: [{ column: 'email', op: 'neq', value: null }] })).count).toBe(0);
|
||||
expect(await repository.featureInterestCounts(principal)).toEqual([{
|
||||
feature_name: 'better-chat', opt_in_count: 2, opt_in_count_last_7_days: 2,
|
||||
}]);
|
||||
});
|
||||
|
||||
it('gifts premium atomically and audits the explicit service action', async () => {
|
||||
const principal = await repository.principal(superUser);
|
||||
const result = await repository.giftPremium(principal, ordinaryUser, 30, 'support grant');
|
||||
expect(result.subscription_id).toEqual(expect.any(String));
|
||||
expect((await pool.query('select is_premium from profiles where id=$1', [ordinaryUser])).rows[0]?.is_premium).toBe(true);
|
||||
expect((await pool.query("select new_values from audit_logs where record_id=$1 and action='ACTION'", [result.subscription_id])).rows[0]?.new_values).toMatchObject({ action: 'gift_premium', target_user_id: ordinaryUser, days: 30 });
|
||||
});
|
||||
|
||||
it('supports lifetime gifts without inventing an expiry', async () => {
|
||||
const principal = await repository.principal(superUser);
|
||||
const result = await repository.giftPremium(principal, ordinaryUser, null, 'lifetime support grant');
|
||||
expect(result.expires_at).toBeNull();
|
||||
expect((await pool.query('select expires_at from subscriptions where id=$1', [result.subscription_id])).rows[0]?.expires_at).toBeNull();
|
||||
});
|
||||
|
||||
it('gates message moderation and media access independently from AI configuration', async () => {
|
||||
const creator = await repository.principal(creatorUser);
|
||||
const moderator = await repository.principal(moderatorUser);
|
||||
await expect(repository.query(creator, 'messages', {})).rejects.toMatchObject({ code: 'forbidden' });
|
||||
await expect(repository.query(creator, 'message_reports', {})).rejects.toMatchObject({ code: 'forbidden' });
|
||||
expect((await repository.query(moderator, 'messages', {})).count).toBe(0);
|
||||
expect((await repository.query(moderator, 'message_reports', {})).count).toBe(0);
|
||||
await expect(repository.authorizeMedia(creator, randomUUID())).rejects.toMatchObject({ code: 'forbidden' });
|
||||
await expect(repository.authorizeMedia(moderator, randomUUID())).rejects.toMatchObject({ code: 'media_not_found' });
|
||||
});
|
||||
|
||||
it('creates all legacy admin and dare cutover tables with preserved foreign keys', async () => {
|
||||
const tables = await pool.query<{ table_name: string }>(
|
||||
`select table_name from information_schema.tables where table_schema=current_schema()
|
||||
and table_name=any($1::text[]) order by table_name`,
|
||||
[['admin_users','audit_logs','topics','pack_topics','ai_config','dare_packs','dares','master_keys','sent_dares','dare_messages']],
|
||||
);
|
||||
expect(tables.rows.map(row => row.table_name)).toEqual([
|
||||
'admin_users','ai_config','audit_logs','dare_messages','dare_packs','dares','master_keys','pack_topics','sent_dares','topics',
|
||||
]);
|
||||
const fks = await pool.query<{ constraint_name: string }>(
|
||||
`select constraint_name from information_schema.table_constraints
|
||||
where table_schema=current_schema() and constraint_type='FOREIGN KEY'
|
||||
and table_name in ('sent_dares','dare_messages')`,
|
||||
);
|
||||
expect(fks.rows).toHaveLength(6);
|
||||
expect((await pool.query(`select column_name from information_schema.columns where table_schema=current_schema() and table_name='feedback' and column_name='admin_notes'`)).rowCount).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user