Add role base access control for events module
This commit is contained in:
13
src/app.d.ts
vendored
13
src/app.d.ts
vendored
@@ -1,17 +1,30 @@
|
|||||||
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
|
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
|
||||||
import type { Database } from './database.types.ts' // import generated types
|
import type { Database } from './database.types.ts' // import generated types
|
||||||
|
|
||||||
|
// Define the profile type based on the database schema
|
||||||
|
type Profile = {
|
||||||
|
display_name: string | null
|
||||||
|
section_position: string | null
|
||||||
|
section: {
|
||||||
|
name: string | null
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
supabase: SupabaseClient<Database>
|
supabase: SupabaseClient<Database>
|
||||||
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
|
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
|
||||||
|
getUserProfile: (userId: string) => Promise<Profile | null>
|
||||||
session: Session | null
|
session: Session | null
|
||||||
user: User | null
|
user: User | null
|
||||||
|
profile: Profile | null
|
||||||
}
|
}
|
||||||
interface PageData {
|
interface PageData {
|
||||||
session: Session | null
|
session: Session | null
|
||||||
|
user: User | null
|
||||||
|
profile: Profile | null
|
||||||
}
|
}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
|
|||||||
@@ -51,6 +51,22 @@ const supabase: Handle = async ({ event, resolve }) => {
|
|||||||
return { session, user }
|
return { session, user }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch user profile data including display name, section position, and section name
|
||||||
|
*/
|
||||||
|
event.locals.getUserProfile = async (userId) => {
|
||||||
|
if (!userId) return null
|
||||||
|
|
||||||
|
const { data: profile, error } = await event.locals.supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('display_name, section_position, section:sections (name)')
|
||||||
|
.eq('id', userId)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) return null
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
return resolve(event, {
|
return resolve(event, {
|
||||||
filterSerializedResponseHeaders(name) {
|
filterSerializedResponseHeaders(name) {
|
||||||
/**
|
/**
|
||||||
@@ -67,6 +83,11 @@ const authGuard: Handle = async ({ event, resolve }) => {
|
|||||||
event.locals.session = session
|
event.locals.session = session
|
||||||
event.locals.user = user
|
event.locals.user = user
|
||||||
|
|
||||||
|
// Fetch the user's profile if they're authenticated
|
||||||
|
if (user) {
|
||||||
|
event.locals.profile = await event.locals.getUserProfile(user.id)
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.locals.session && event.url.pathname.startsWith('/private')) {
|
if (!event.locals.session && event.url.pathname.startsWith('/private')) {
|
||||||
redirect(303, '/auth')
|
redirect(303, '/auth')
|
||||||
}
|
}
|
||||||
@@ -75,6 +96,13 @@ const authGuard: Handle = async ({ event, resolve }) => {
|
|||||||
redirect(303, '/private/home')
|
redirect(303, '/private/home')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Role-based access control for events routes
|
||||||
|
if (event.url.pathname.startsWith('/private/events')) {
|
||||||
|
if (!event.locals.profile || event.locals.profile.section_position !== 'events_manager') {
|
||||||
|
redirect(303, '/private/errors/events/denied')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return resolve(event)
|
return resolve(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import type { LayoutServerLoad } from './$types'
|
import type { LayoutServerLoad } from './$types'
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
export const load: LayoutServerLoad = async ({ locals: { safeGetSession, getUserProfile }, cookies }) => {
|
||||||
const { session } = await safeGetSession()
|
const { session, user } = await safeGetSession()
|
||||||
|
|
||||||
|
// Get the user profile if the user is authenticated
|
||||||
|
let profile = null
|
||||||
|
if (user) {
|
||||||
|
profile = await getUserProfile(user.id)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
|
user,
|
||||||
|
profile,
|
||||||
cookies: cookies.getAll(),
|
cookies: cookies.getAll(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,5 +39,10 @@ export const load: LayoutLoad = async ({ data, depends, fetch }) => {
|
|||||||
data: { user },
|
data: { user },
|
||||||
} = await supabase.auth.getUser()
|
} = await supabase.auth.getUser()
|
||||||
|
|
||||||
return { session, supabase, user }
|
return {
|
||||||
|
session,
|
||||||
|
supabase,
|
||||||
|
user,
|
||||||
|
profile: data.profile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<div class="min-h-screen flex flex-col justify-center items-center">
|
<div class="min-h-screen flex flex-col justify-center items-center">
|
||||||
<!-- SVG QR Code Art on Top -->
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<img class="w-32 h-auto" src="/qr-code.png" alt="">
|
<img class="w-32 h-auto" src="/qr-code.png" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
||||||
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
@@ -22,7 +24,9 @@
|
|||||||
<ul class="flex space-x-4">
|
<ul class="flex space-x-4">
|
||||||
<li><a href="/private/home">Home</a></li>
|
<li><a href="/private/home">Home</a></li>
|
||||||
<li><a href="/private/scanner">Scanner</a></li>
|
<li><a href="/private/scanner">Scanner</a></li>
|
||||||
|
{#if data.profile?.section_position === 'events_manager'}
|
||||||
<li><a href="/private/events">Events</a></li>
|
<li><a href="/private/events">Events</a></li>
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
src/routes/private/errors/events/denied/+page.svelte
Normal file
1
src/routes/private/errors/events/denied/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Access to events denied!
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// src/routes/my-page/+page.server.ts
|
|
||||||
import type { PageServerLoad } from './$types';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
|
||||||
// get the logged-in user
|
|
||||||
const { data: { user }, error: authError } = await locals.supabase.auth.getUser();
|
|
||||||
|
|
||||||
const { data: user_profile, error: profileError } = await locals.supabase.from('profiles').select('*, section:sections (id, name)').eq('id', user?.id).single();
|
|
||||||
|
|
||||||
if (authError) {
|
|
||||||
console.error('Supabase auth error:', authError);
|
|
||||||
throw new Error('Could not get user');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profileError) {
|
|
||||||
console.error('Supabase profile error:', profileError);
|
|
||||||
throw new Error('Could not get user profile');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user, user_profile };
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { User } from '@supabase/supabase-js';
|
let { data } = $props();
|
||||||
|
|
||||||
export let data: {
|
|
||||||
user: User | null,
|
|
||||||
user_profile: any | null
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="mt-2 mb-4 text-center text-2xl font-bold">User Profile</h1>
|
<h1 class="mt-2 mb-4 text-center text-2xl font-bold">User Profile</h1>
|
||||||
@@ -16,18 +11,18 @@
|
|||||||
{data.user?.user_metadata.display_name?.[0] ?? "U"}
|
{data.user?.user_metadata.display_name?.[0] ?? "U"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-lg font-semibold text-gray-800">{data.user?.user_metadata.display_name}</span>
|
<span class="text-lg font-semibold text-gray-800">{data.profile?.display_name}</span>
|
||||||
<div class="text-sm text-gray-500">{data.user?.email}</div>
|
<div class="text-sm text-gray-500">{data.user?.email}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-700">Section:</span>
|
<span class="font-medium text-gray-700">Section:</span>
|
||||||
<span class="text-gray-900">{data.user_profile?.section.name ?? "N/A"}</span>
|
<span class="text-gray-900">{data.profile?.section.name ?? "N/A"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-700">Position:</span>
|
<span class="font-medium text-gray-700">Position:</span>
|
||||||
<span class="text-gray-900">{data.user_profile?.section_position ?? "N/A"}</span>
|
<span class="text-gray-900">{data.profile?.section_position ?? "N/A"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-lg mb-2 mt-4">User guide</h2>
|
<h2 class="text-lg mb-2 mt-4">User guide</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user