Add account hierarchy framework with migrations, RLS policies, and UI components

This commit is contained in:
T. Zehetbauer
2026-03-31 22:18:04 +02:00
parent 7e7da0b465
commit 59546ad6d2
262 changed files with 11671 additions and 3927 deletions

View File

@@ -11,6 +11,7 @@ import { EllipsisVertical } from 'lucide-react';
import { useForm, useWatch } from 'react-hook-form';
import * as z from 'zod';
import { formatDateTime } from '@kit/shared/dates';
import { Tables } from '@kit/supabase/database';
import { Button } from '@kit/ui/button';
import {
@@ -208,31 +209,14 @@ function getColumns(): ColumnDef<Account>[] {
id: 'created_at',
header: 'Created At',
cell: ({ row }) => {
return new Date(row.original.created_at!).toLocaleDateString(
undefined,
{
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
},
);
return formatDateTime(row.original.created_at);
},
},
{
id: 'updated_at',
header: 'Updated At',
cell: ({ row }) => {
return row.original.updated_at
? new Date(row.original.updated_at).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
: '-';
return formatDateTime(row.original.updated_at);
},
},
{

View File

@@ -55,7 +55,7 @@ const TENANT_MAPPING: Record<number, { type: string; name: string }> = {
*/
async function createMysqlConnection(config: MysqlConfig) {
// Dynamic import — mysql2 must be installed separately: pnpm add mysql2
const mysql = await import('mysql2/promise' as string) as any;
const mysql = (await import('mysql2/promise' as string)) as any;
return mysql.createConnection({
host: config.host,
port: config.port,
@@ -75,7 +75,11 @@ export async function runMigration(
): Promise<MigrationProgress> {
const db = supabase as any;
const mysql = await createMysqlConnection(mysqlConfig);
const progress: MigrationProgress = { steps: [], totalMigrated: 0, totalErrors: 0 };
const progress: MigrationProgress = {
steps: [],
totalMigrated: 0,
totalErrors: 0,
};
try {
// Step 1: Migrate users
@@ -99,8 +103,14 @@ export async function runMigration(
progress.steps.push(courseResult);
// Calculate totals
progress.totalMigrated = progress.steps.reduce((sum, s) => sum + s.count, 0);
progress.totalErrors = progress.steps.reduce((sum, s) => sum + s.errors.length, 0);
progress.totalMigrated = progress.steps.reduce(
(sum, s) => sum + s.count,
0,
);
progress.totalErrors = progress.steps.reduce(
(sum, s) => sum + s.errors.length,
0,
);
} finally {
await mysql.end();
}
@@ -113,10 +123,17 @@ async function migrateUsers(
supabase: any,
onProgress?: (step: string, count: number) => void,
): Promise<MigrationResult> {
const result: MigrationResult = { step: 'users', success: true, count: 0, errors: [] };
const result: MigrationResult = {
step: 'users',
success: true,
count: 0,
errors: [],
};
try {
const [rows] = await mysql.execute('SELECT * FROM cms_user WHERE active = 1');
const [rows] = await mysql.execute(
'SELECT * FROM cms_user WHERE active = 1',
);
onProgress?.('Migrating users', (rows as any[]).length);
for (const row of rows as any[]) {
@@ -125,12 +142,16 @@ async function migrateUsers(
// This creates a record for mapping; actual auth user creation uses supabase.auth.admin
result.count++;
} catch (err) {
result.errors.push(`User ${row.login}: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`User ${row.login}: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
}
} catch (err) {
result.success = false;
result.errors.push(`Failed to read users: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`Failed to read users: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
return result;
@@ -141,7 +162,12 @@ async function migrateAccounts(
supabase: any,
onProgress?: (step: string, count: number) => void,
): Promise<MigrationResult> {
const result: MigrationResult = { step: 'accounts', success: true, count: 0, errors: [] };
const result: MigrationResult = {
step: 'accounts',
success: true,
count: 0,
errors: [],
};
onProgress?.('Creating team accounts', Object.keys(TENANT_MAPPING).length);
@@ -150,7 +176,9 @@ async function migrateAccounts(
// Create account_settings entry for each tenant
result.count++;
} catch (err) {
result.errors.push(`Tenant ${profileId}: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`Tenant ${profileId}: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
}
@@ -162,10 +190,17 @@ async function migrateModules(
supabase: any,
onProgress?: (step: string, count: number) => void,
): Promise<MigrationResult> {
const result: MigrationResult = { step: 'modules', success: true, count: 0, errors: [] };
const result: MigrationResult = {
step: 'modules',
success: true,
count: 0,
errors: [],
};
try {
const [modules] = await mysql.execute('SELECT * FROM m_module ORDER BY sort_order');
const [modules] = await mysql.execute(
'SELECT * FROM m_module ORDER BY sort_order',
);
onProgress?.('Migrating modules', (modules as any[]).length);
for (const mod of modules as any[]) {
@@ -178,12 +213,16 @@ async function migrateModules(
);
result.count += 1 + (fields as any[]).length;
} catch (err) {
result.errors.push(`Module ${mod.name}: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`Module ${mod.name}: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
}
} catch (err) {
result.success = false;
result.errors.push(`Failed to read modules: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`Failed to read modules: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
return result;
@@ -194,7 +233,12 @@ async function migrateMembers(
supabase: any,
onProgress?: (step: string, count: number) => void,
): Promise<MigrationResult> {
const result: MigrationResult = { step: 'members', success: true, count: 0, errors: [] };
const result: MigrationResult = {
step: 'members',
success: true,
count: 0,
errors: [],
};
try {
const [rows] = await mysql.execute('SELECT * FROM ve_mitglieder');
@@ -209,12 +253,16 @@ async function migrateMembers(
// beitragskategorie→dues_category_id, iban→iban, bic→bic
result.count++;
} catch (err) {
result.errors.push(`Member ${row.nachname}: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`Member ${row.nachname}: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
}
} catch (err) {
result.success = false;
result.errors.push(`Failed to read members: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`Failed to read members: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
return result;
@@ -225,7 +273,12 @@ async function migrateCourses(
supabase: any,
onProgress?: (step: string, count: number) => void,
): Promise<MigrationResult> {
const result: MigrationResult = { step: 'courses', success: true, count: 0, errors: [] };
const result: MigrationResult = {
step: 'courses',
success: true,
count: 0,
errors: [],
};
try {
const [rows] = await mysql.execute('SELECT * FROM ve_kurse');
@@ -238,12 +291,16 @@ async function migrateCourses(
// beginn→start_date, ende→end_date, gebuehr→fee, max_teilnehmer→capacity
result.count++;
} catch (err) {
result.errors.push(`Course ${row.kursname}: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`Course ${row.kursname}: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
}
} catch (err) {
result.success = false;
result.errors.push(`Failed to read courses: ${err instanceof Error ? err.message : 'Unknown error'}`);
result.errors.push(
`Failed to read courses: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
return result;