diff --git a/src/routes/api/auth/+server.ts b/src/routes/api/auth/+server.ts index 9dddf4b..a3665a4 100644 --- a/src/routes/api/auth/+server.ts +++ b/src/routes/api/auth/+server.ts @@ -25,10 +25,13 @@ async function handleJoin(body: Record) { if (!group) return json({ error: 'Invalid invite code' }, { status: 404 }); const userId = uuid(); + // Use a unique placeholder phone so the unique constraint isn't violated + // by multiple pre-onboarding users. Onboarding replaces this with the real phone. + const placeholderPhone = `pending:${userId}`; await db.insert(users).values({ id: userId, username: '', - phone: '', + phone: placeholderPhone, groupId: group.id, createdAt: new Date() }); diff --git a/src/routes/join/[code]/+page.server.ts b/src/routes/join/[code]/+page.server.ts new file mode 100644 index 0000000..16ee474 --- /dev/null +++ b/src/routes/join/[code]/+page.server.ts @@ -0,0 +1,59 @@ +import { redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { validateInviteCode, createSessionToken } from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import { users, notificationPreferences } from '$lib/server/db/schema'; +import { v4 as uuid } from 'uuid'; +import { checkRateLimit } from '$lib/server/rate-limit'; + +export const load: PageServerLoad = async ({ params }) => { + const group = await validateInviteCode(params.code); + if (!group) { + redirect(302, '/join'); + } + + return { + groupName: group.name, + inviteCode: params.code + }; +}; + +export const actions: Actions = { + default: async ({ params, cookies, getClientAddress }) => { + const ip = getClientAddress(); + const result = checkRateLimit(`join:${ip}`, { windowMs: 15 * 60 * 1000, maxRequests: 5 }); + if (!result.allowed) { + return fail(429, { error: 'Too many attempts. Please try again later.' }); + } + + const group = await validateInviteCode(params.code); + if (!group) { + redirect(302, '/join'); + } + + const userId = uuid(); + // Use a unique placeholder phone so the unique constraint isn't violated + // by multiple pre-onboarding users. Onboarding replaces this with the real phone. + const placeholderPhone = `pending:${userId}`; + await db.insert(users).values({ + id: userId, + username: '', + phone: placeholderPhone, + groupId: group.id, + createdAt: new Date() + }); + + await db.insert(notificationPreferences).values({ userId }); + + const token = createSessionToken(userId); + cookies.set('scrolly_session', token, { + httpOnly: true, + secure: true, + sameSite: 'lax', + path: '/', + maxAge: 60 * 60 * 24 * 365 * 10 + }); + + redirect(302, '/onboard'); + } +}; diff --git a/src/routes/join/[code]/+page.svelte b/src/routes/join/[code]/+page.svelte new file mode 100644 index 0000000..779218b --- /dev/null +++ b/src/routes/join/[code]/+page.svelte @@ -0,0 +1,228 @@ + + + + Join {data.groupName} — scrolly + + + + + +
+
+ +
+
+ + +

scrolly

+

your crew's private feed

+
+ +
+

You've been invited to

+

{data.groupName}

+ +
(loading = true)}> + +
+
+
+ + +
+ + diff --git a/src/routes/join/[code]/+server.ts b/src/routes/join/[code]/+server.ts deleted file mode 100644 index 4d880cf..0000000 --- a/src/routes/join/[code]/+server.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { redirect } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { validateInviteCode, createSessionCookie } from '$lib/server/auth'; -import { db } from '$lib/server/db'; -import { users, notificationPreferences } from '$lib/server/db/schema'; -import { v4 as uuid } from 'uuid'; - -export const GET: RequestHandler = async ({ params }) => { - const group = await validateInviteCode(params.code); - if (!group) { - redirect(302, '/join'); - } - - const userId = uuid(); - await db.insert(users).values({ - id: userId, - username: '', - phone: '', - groupId: group.id, - createdAt: new Date() - }); - - await db.insert(notificationPreferences).values({ userId }); - - const cookie = createSessionCookie(userId); - return new Response(null, { - status: 302, - headers: { - Location: '/onboard', - 'Set-Cookie': cookie - } - }); -}; diff --git a/src/routes/onboard/+page.server.ts b/src/routes/onboard/+page.server.ts new file mode 100644 index 0000000..d0ed69f --- /dev/null +++ b/src/routes/onboard/+page.server.ts @@ -0,0 +1,24 @@ +import { redirect } from '@sveltejs/kit'; +import type { ServerLoad } from '@sveltejs/kit'; +import { getUserIdFromCookies, getUserWithGroup } from '$lib/server/auth'; + +export const load: ServerLoad = async ({ request }) => { + const userId = getUserIdFromCookies(request.headers.get('cookie')); + + if (!userId) { + // No session — they haven't joined via invite code + redirect(302, '/join'); + } + + const data = await getUserWithGroup(userId); + + if (!data || data.user.removedAt) { + // Invalid or removed user + redirect(302, '/join'); + } + + if (data.user.username) { + // Already onboarded — send to feed + redirect(302, '/'); + } +};