Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -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);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user