From 23693db9ec8c6ab8f561f78495e4cd4d57141137 Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Sat, 7 Feb 2026 22:29:09 +0200 Subject: [PATCH] Dashbaord fixes --- src/lib/components/ui/Input.svelte | 3 + src/routes/admin/+page.server.ts | 172 +++++++++- src/routes/admin/+page.svelte | 321 ++++++++++++++++-- .../migrations/031_platform_admin_rls.sql | 56 +++ 4 files changed, 509 insertions(+), 43 deletions(-) create mode 100644 supabase/migrations/031_platform_admin_rls.sql diff --git a/src/lib/components/ui/Input.svelte b/src/lib/components/ui/Input.svelte index 65a576b..fb8a2a1 100644 --- a/src/lib/components/ui/Input.svelte +++ b/src/lib/components/ui/Input.svelte @@ -19,6 +19,7 @@ required?: boolean; autocomplete?: AutoFill; icon?: string; + name?: string; oninput?: (e: Event) => void; onchange?: (e: Event) => void; onkeydown?: (e: KeyboardEvent) => void; @@ -35,6 +36,7 @@ required = false, autocomplete, icon, + name, oninput, onchange, onkeydown, @@ -79,6 +81,7 @@ {disabled} {required} {autocomplete} + {name} {oninput} {onchange} {onkeydown} diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts index 64bfd69..916f7ca 100644 --- a/src/routes/admin/+page.server.ts +++ b/src/routes/admin/+page.server.ts @@ -1,19 +1,15 @@ -import { error, redirect } from '@sveltejs/kit'; -import type { PageServerLoad } from './$types'; +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; // Cast helper for columns not yet in generated types function db(supabase: any) { return supabase as any; } -export const load: PageServerLoad = async ({ locals }) => { +async function requireAdmin(locals: any) { const { session, user } = await locals.safeGetSession(); + if (!session || !user) redirect(303, '/login'); - if (!session || !user) { - redirect(303, '/login'); - } - - // Check platform admin status const { data: profile } = await db(locals.supabase) .from('profiles') .select('is_platform_admin') @@ -24,6 +20,12 @@ export const load: PageServerLoad = async ({ locals }) => { error(403, 'Access denied. Platform admin only.'); } + return { session, user }; +} + +export const load: PageServerLoad = async ({ locals }) => { + const { user } = await requireAdmin(locals); + // Fetch all platform data in parallel const [ orgsResult, @@ -45,8 +47,7 @@ export const load: PageServerLoad = async ({ locals }) => { .order('created_at', { ascending: false }), db(locals.supabase) .from('org_members') - .select('id, user_id, org_id, role') - .order('created_at', { ascending: false }), + .select('id, user_id, org_id, role'), ]); const organizations = orgsResult.data ?? []; @@ -83,6 +84,7 @@ export const load: PageServerLoad = async ({ locals }) => { } return { + currentUserId: user.id, organizations: organizations.map((o: any) => ({ ...o, memberCount: orgMemberCounts[o.id] || 0, @@ -90,6 +92,7 @@ export const load: PageServerLoad = async ({ locals }) => { })), profiles, events, + orgMembers, stats: { totalUsers: profiles.length, totalOrgs: organizations.length, @@ -102,3 +105,152 @@ export const load: PageServerLoad = async ({ locals }) => { }, }; }; + +export const actions: Actions = { + deleteOrg: async ({ request, locals }) => { + await requireAdmin(locals); + const formData = await request.formData(); + const orgId = formData.get('id') as string; + if (!orgId) return fail(400, { error: 'Missing org ID' }); + + const { error: err } = await db(locals.supabase) + .from('organizations') + .delete() + .eq('id', orgId); + + if (err) return fail(500, { error: err.message }); + return { success: true }; + }, + + deleteUser: async ({ request, locals }) => { + await requireAdmin(locals); + const formData = await request.formData(); + const userId = formData.get('id') as string; + if (!userId) return fail(400, { error: 'Missing user ID' }); + + // Delete profile (cascades from auth.users FK) + const { error: err } = await db(locals.supabase) + .from('profiles') + .delete() + .eq('id', userId); + + if (err) return fail(500, { error: err.message }); + return { success: true }; + }, + + deleteEvent: async ({ request, locals }) => { + await requireAdmin(locals); + const formData = await request.formData(); + const eventId = formData.get('id') as string; + if (!eventId) return fail(400, { error: 'Missing event ID' }); + + const { error: err } = await db(locals.supabase) + .from('events') + .delete() + .eq('id', eventId); + + if (err) return fail(500, { error: err.message }); + return { success: true }; + }, + + updateEventStatus: async ({ request, locals }) => { + await requireAdmin(locals); + const formData = await request.formData(); + const eventId = formData.get('id') as string; + const status = formData.get('status') as string; + if (!eventId || !status) return fail(400, { error: 'Missing event ID or status' }); + + const { error: err } = await db(locals.supabase) + .from('events') + .update({ status }) + .eq('id', eventId); + + if (err) return fail(500, { error: err.message }); + return { success: true }; + }, + + toggleAdmin: async ({ request, locals }) => { + await requireAdmin(locals); + const formData = await request.formData(); + const userId = formData.get('id') as string; + const isAdmin = formData.get('is_admin') === 'true'; + if (!userId) return fail(400, { error: 'Missing user ID' }); + + const { error: err } = await db(locals.supabase) + .from('profiles') + .update({ is_platform_admin: isAdmin }) + .eq('id', userId); + + if (err) return fail(500, { error: err.message }); + return { success: true }; + }, + + updateOrg: async ({ request, locals }) => { + await requireAdmin(locals); + const formData = await request.formData(); + const orgId = formData.get('id') as string; + const name = formData.get('name') as string; + const slug = formData.get('slug') as string; + if (!orgId) return fail(400, { error: 'Missing org ID' }); + + const updates: Record = {}; + if (name) updates.name = name; + if (slug) updates.slug = slug; + + const { error: err } = await db(locals.supabase) + .from('organizations') + .update(updates) + .eq('id', orgId); + + if (err) return fail(500, { error: err.message }); + return { success: true }; + }, + + updateEvent: async ({ request, locals }) => { + await requireAdmin(locals); + const formData = await request.formData(); + const eventId = formData.get('id') as string; + const name = formData.get('name') as string; + const slug = formData.get('slug') as string; + const status = formData.get('status') as string; + const startDate = formData.get('start_date') as string; + const endDate = formData.get('end_date') as string; + if (!eventId) return fail(400, { error: 'Missing event ID' }); + + const updates: Record = {}; + if (name) updates.name = name; + if (slug) updates.slug = slug; + if (status) updates.status = status; + if (startDate) updates.start_date = startDate; + if (endDate) updates.end_date = endDate; + + const { error: err } = await db(locals.supabase) + .from('events') + .update(updates) + .eq('id', eventId); + + if (err) return fail(500, { error: err.message }); + return { success: true }; + }, + + updateUser: async ({ request, locals }) => { + await requireAdmin(locals); + const formData = await request.formData(); + const userId = formData.get('id') as string; + const fullName = formData.get('full_name') as string; + const email = formData.get('email') as string; + if (!userId) return fail(400, { error: 'Missing user ID' }); + + const updates: Record = {}; + if (fullName !== null) updates.full_name = fullName; + if (email) updates.email = email; + + const { error: err } = await db(locals.supabase) + .from('profiles') + .update(updates) + .eq('id', userId); + + if (err) return fail(500, { error: err.message }); + return { success: true }; + }, +}; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index c4af90b..5680cfa 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -1,5 +1,7 @@ @@ -104,13 +141,12 @@
-
+
- - - + +
@@ -134,13 +170,7 @@

Recent Organizations

- +
{#each data.organizations.slice(0, 5) as org} @@ -168,13 +198,7 @@

Recent Users

- +
{#each data.profiles.slice(0, 5) as profile} @@ -204,13 +228,7 @@

Recent Events

- +
{#if data.events.length > 0}
@@ -252,6 +270,29 @@

No events yet

{/if}
+ + +
+

Event Status Guide

+
+
+ planning +

Event is being prepared. Team is setting up departments, tasks, and logistics.

+
+
+ active +

Event is currently live/happening. All systems go.

+
+
+ completed +

Event has finished. Data is preserved for review.

+
+
+ archived +

Event is hidden from normal views. Can be restored.

+
+
+
{:else if activeTab === "organizations"} @@ -268,6 +309,7 @@ Members Events Created + Actions @@ -287,11 +329,31 @@ {org.eventCount} {formatDate(org.created_at)} + +
+ + +
+ {/each} {#if filteredOrgs.length === 0} - + {orgSearch ? "No organizations match your search" : "No organizations yet"} @@ -313,8 +375,10 @@ User Email + Organizations Role Joined + Actions @@ -327,6 +391,9 @@
{profile.email} + + {userOrgCounts()[profile.id] ?? 0} + {#if profile.is_platform_admin} Platform Admin @@ -335,11 +402,50 @@ {/if} {formatDate(profile.created_at)} + +
+ + {#if profile.is_platform_admin && profile.id !== data.currentUserId} +
+ + + +
+ {:else if !profile.is_platform_admin} +
+ + + +
+ {/if} + {#if profile.id !== data.currentUserId} + + {/if} +
+ {/each} {#if filteredUsers.length === 0} - + {userSearch ? "No users match your search" : "No users yet"} @@ -365,6 +471,7 @@ Start End Created + Actions @@ -380,18 +487,47 @@ {orgMap[event.org_id]?.name ?? "—"} - - {event.status} - +
+ + +
{formatDate(event.start_date)} {formatDate(event.end_date)} {timeAgo(event.created_at)} + +
+ + +
+ {/each} {#if filteredEvents.length === 0} - + {eventSearch ? "No events match your search" : "No events yet"} @@ -404,3 +540,122 @@ {/if}
+ + + (confirmModal = null)} title="Confirm Delete"> + {#if confirmModal} +

+ Are you sure you want to delete {confirmModal.name}? +

+

This action cannot be undone. All associated data will be permanently removed.

+
{ actionLoading = true; return enhanceAction(); }} + > + +
+ + +
+
+ {/if} +
+ + + (editOrgModal = null)} title="Edit Organization"> + {#if editOrgModal} +
{ actionLoading = true; return enhanceAction(); }} + class="space-y-4" + > + + + +
+ + +
+
+ {/if} +
+ + + (editUserModal = null)} title="Edit User"> + {#if editUserModal} +
{ actionLoading = true; return enhanceAction(); }} + class="space-y-4" + > + + + +
+ +
+
+ + +
+
+ {/if} +
+ + + (editEventModal = null)} title="Edit Event"> + {#if editEventModal} +
{ actionLoading = true; return enhanceAction(); }} + class="space-y-4" + > + + + +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ {/if} +
diff --git a/supabase/migrations/031_platform_admin_rls.sql b/supabase/migrations/031_platform_admin_rls.sql new file mode 100644 index 0000000..87afd6c --- /dev/null +++ b/supabase/migrations/031_platform_admin_rls.sql @@ -0,0 +1,56 @@ +-- Platform admins can read/write all data across the platform +-- This bypasses org-membership-based RLS for users with is_platform_admin = true + +-- Helper function to check if current user is a platform admin +CREATE OR REPLACE FUNCTION is_platform_admin() +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT 1 FROM profiles + WHERE id = auth.uid() + AND is_platform_admin = true + ); +$$ LANGUAGE sql SECURITY DEFINER STABLE; + +-- Organizations: platform admins can do everything +CREATE POLICY "Platform admins full access to organizations" ON organizations + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Org Members: platform admins can see all memberships +CREATE POLICY "Platform admins full access to org_members" ON org_members + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Profiles: platform admins can update any profile +CREATE POLICY "Platform admins can update profiles" ON profiles FOR UPDATE + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Events: platform admins can do everything +CREATE POLICY "Platform admins full access to events" ON events + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Event members: platform admins can do everything +CREATE POLICY "Platform admins full access to event_members" ON event_members + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Documents: platform admins can do everything +CREATE POLICY "Platform admins full access to documents" ON documents + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Kanban boards: platform admins can do everything +CREATE POLICY "Platform admins full access to kanban_boards" ON kanban_boards + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Calendar events: platform admins can do everything +CREATE POLICY "Platform admins full access to calendar_events" ON calendar_events + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Org roles: platform admins can do everything +CREATE POLICY "Platform admins full access to org_roles" ON org_roles + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Org invites: platform admins can do everything +CREATE POLICY "Platform admins full access to org_invites" ON org_invites + USING (is_platform_admin()) WITH CHECK (is_platform_admin()); + +-- Event departments: platform admins can do everything +CREATE POLICY "Platform admins full access to event_departments" ON event_departments + USING (is_platform_admin()) WITH CHECK (is_platform_admin());