From 424a03f177755fd116882faf9f3a11a2d9d84ee9 Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Thu, 5 Feb 2026 00:02:40 +0200 Subject: [PATCH] Added invite page and fixed inviting logic --- src/lib/supabase/types.ts | 114 ++++++++++++- src/routes/auth/callback/+server.ts | 2 +- src/routes/invite/[token]/+page.server.ts | 47 ++++++ src/routes/invite/[token]/+page.svelte | 188 ++++++++++++++++++++++ src/routes/login/+page.svelte | 12 +- 5 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 src/routes/invite/[token]/+page.server.ts create mode 100644 src/routes/invite/[token]/+page.svelte diff --git a/src/lib/supabase/types.ts b/src/lib/supabase/types.ts index a9b011c..664726b 100644 --- a/src/lib/supabase/types.ts +++ b/src/lib/supabase/types.ts @@ -34,7 +34,8 @@ export interface Database { id: string; org_id: string; user_id: string; - role: 'owner' | 'admin' | 'editor' | 'viewer'; + role: string; + role_id: string | null; invited_at: string; joined_at: string | null; }; @@ -42,7 +43,8 @@ export interface Database { id?: string; org_id: string; user_id: string; - role: 'owner' | 'admin' | 'editor' | 'viewer'; + role?: string; + role_id?: string | null; invited_at?: string; joined_at?: string | null; }; @@ -50,7 +52,8 @@ export interface Database { id?: string; org_id?: string; user_id?: string; - role?: 'owner' | 'admin' | 'editor' | 'viewer'; + role?: string; + role_id?: string | null; invited_at?: string; joined_at?: string | null; }; @@ -240,6 +243,111 @@ export interface Database { status?: 'pending' | 'accepted' | 'declined'; }; }; + org_roles: { + Row: { + id: string; + org_id: string; + name: string; + color: string; + permissions: string[]; + is_default: boolean; + is_system: boolean; + position: number; + created_at: string; + updated_at: string; + }; + Insert: { + id?: string; + org_id: string; + name: string; + color?: string; + permissions?: string[]; + is_default?: boolean; + is_system?: boolean; + position?: number; + created_at?: string; + updated_at?: string; + }; + Update: { + id?: string; + org_id?: string; + name?: string; + color?: string; + permissions?: string[]; + is_default?: boolean; + is_system?: boolean; + position?: number; + created_at?: string; + updated_at?: string; + }; + }; + org_invites: { + Row: { + id: string; + org_id: string; + email: string; + role_id: string | null; + role: string; + invited_by: string; + token: string; + expires_at: string; + accepted_at: string | null; + created_at: string; + }; + Insert: { + id?: string; + org_id: string; + email: string; + role_id?: string | null; + role?: string; + invited_by: string; + token?: string; + expires_at?: string; + accepted_at?: string | null; + created_at?: string; + }; + Update: { + id?: string; + org_id?: string; + email?: string; + role_id?: string | null; + role?: string; + invited_by?: string; + token?: string; + expires_at?: string; + accepted_at?: string | null; + created_at?: string; + }; + }; + org_google_calendars: { + Row: { + id: string; + org_id: string; + calendar_id: string; + calendar_name: string | null; + connected_by: string; + created_at: string; + updated_at: string; + }; + Insert: { + id?: string; + org_id: string; + calendar_id: string; + calendar_name?: string | null; + connected_by: string; + created_at?: string; + updated_at?: string; + }; + Update: { + id?: string; + org_id?: string; + calendar_id?: string; + calendar_name?: string | null; + connected_by?: string; + created_at?: string; + updated_at?: string; + }; + }; profiles: { Row: { id: string; diff --git a/src/routes/auth/callback/+server.ts b/src/routes/auth/callback/+server.ts index bee598f..90084fa 100644 --- a/src/routes/auth/callback/+server.ts +++ b/src/routes/auth/callback/+server.ts @@ -3,7 +3,7 @@ import type { RequestHandler } from './$types'; export const GET: RequestHandler = async ({ url, locals }) => { const code = url.searchParams.get('code'); - const next = url.searchParams.get('next') ?? '/'; + const next = url.searchParams.get('next') ?? url.searchParams.get('redirect') ?? '/'; if (code) { const { error } = await locals.supabase.auth.exchangeCodeForSession(code); diff --git a/src/routes/invite/[token]/+page.server.ts b/src/routes/invite/[token]/+page.server.ts new file mode 100644 index 0000000..da5f259 --- /dev/null +++ b/src/routes/invite/[token]/+page.server.ts @@ -0,0 +1,47 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params, locals }) => { + const { token } = params; + + // Get invite details + const { data: invite, error } = await locals.supabase + .from('org_invites') + .select(` + *, + organizations (id, name, slug) + `) + .eq('token', token) + .is('accepted_at', null) + .single(); + + if (error || !invite) { + return { + error: 'Invalid or expired invite link', + token + }; + } + + const inv = invite as any; + + // Check if invite is expired + if (new Date(inv.expires_at) < new Date()) { + return { + error: 'This invite has expired', + token + }; + } + + // Get current user + const { data: { user } } = await locals.supabase.auth.getUser(); + + return { + invite: { + id: inv.id, + email: inv.email, + role: inv.role, + org: inv.organizations + }, + user, + token + }; +}; diff --git a/src/routes/invite/[token]/+page.svelte b/src/routes/invite/[token]/+page.svelte new file mode 100644 index 0000000..ea4c1a8 --- /dev/null +++ b/src/routes/invite/[token]/+page.svelte @@ -0,0 +1,188 @@ + + +
+
+
+ {#if data.error} + +
+ + + + + +
+

+ Invalid Invite +

+

{data.error}

+ + {:else if data.invite} + +
+ + + + + + +
+ +

+ You're Invited! +

+

You've been invited to join

+

+ {data.invite.org.name} +

+

as {data.invite.role}

+ + {#if error} +
+ {error} +
+ {/if} + + {#if data.user} + +

+ Signed in as {data.user.email} +

+
+ +
+

+ Wrong account? Sign out +

+ {:else} + +

+ Sign in or create an account to accept this invite. +

+
+ + +
+ {/if} + {/if} +
+
+
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index b67f723..2cc93b7 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -2,6 +2,7 @@ import { Button, Input, Card } from "$lib/components/ui"; import { createClient } from "$lib/supabase"; import { goto } from "$app/navigation"; + import { page } from "$app/stores"; let email = $state(""); let password = $state(""); @@ -11,6 +12,9 @@ const supabase = createClient(); + // Get redirect URL from query params (for invite flow) + const redirectUrl = $derived($page.url.searchParams.get("redirect") || "/"); + async function handleSubmit() { if (!email || !password) { error = "Please fill in all fields"; @@ -38,7 +42,7 @@ }); if (authError) throw authError; } - goto("/"); + goto(redirectUrl); } catch (e: unknown) { error = e instanceof Error ? e.message : "An error occurred"; } finally { @@ -47,10 +51,14 @@ } async function handleOAuth(provider: "google" | "github") { + const callbackUrl = + redirectUrl !== "/" + ? `${window.location.origin}/auth/callback?redirect=${encodeURIComponent(redirectUrl)}` + : `${window.location.origin}/auth/callback`; const { error: authError } = await supabase.auth.signInWithOAuth({ provider, options: { - redirectTo: `${window.location.origin}/auth/callback`, + redirectTo: callbackUrl, }, }); if (authError) {