diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index cefe3a6..c804e16 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -5,15 +5,13 @@ declare global { // interface Error {} interface Locals { - session: boolean; - } - interface PageData { user?: { id: string; username: string; avatar: string; }; } + // interface PageData {} // interface Platform {} } } diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 4887f85..4402452 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,50 +1,50 @@ +import refresh from '$lib/refreshToken'; import { redirect, type Handle } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; const protectedRoutes = ['/']; -const auth: Handle = async ({ resolve, event }) => { - console.log(`handle auth for route: ${event.url.pathname}`); - +const handleAuth: Handle = async ({ resolve, event }) => { const refreshToken = event.cookies.get('refresh-token'); - let accessToken = event.cookies.get('access-token'); + const accessToken = event.cookies.get('access-token'); - console.log(`refresh token: ${refreshToken}`); - console.log(`access token: ${accessToken}`); + if (refreshToken && !accessToken) await refresh(event); - if (!accessToken && refreshToken) { - const rsp = await event.fetch('/auth/discord', { - method: 'PUT', + return await resolve(event); +}; + +const handleUserSession: Handle = async ({ resolve, event }) => { + if (event.cookies.get('access-token') && event.cookies.get('refresh-token')) { + const rsp = await event.fetch('https://discord.com/api/v10/users/@me', { headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ refreshToken }) + Authorization: `Bearer ${event.cookies.get('access-token')}` + } }); - if (!rsp.ok) throw redirect(302, '/login'); + if (!rsp.ok) console.error(`failed to get user session: ${rsp.status} ${await rsp.text()}`); - accessToken = event.cookies.get('access-token'); + const { id, username, avatar } = await rsp.json(); + + event.locals.user = { + id, + username, + avatar: `https://cdn.discordapp.com/avatars/${id}/${avatar}.png` + }; } - // * grab the access token again, in case it was just refreshed - event.locals.session = !!(event.cookies.get('access-token') && refreshToken); - return await resolve(event); }; -const guard: Handle = async ({ resolve, event }) => { - if (protectedRoutes.includes(event.url.pathname) && !event.locals.session) { - console.warn(`authentication failed for: ${event.url.pathname}`); +const handleGuard: Handle = async ({ resolve, event }) => { + if (!event.locals.user && protectedRoutes.includes(event.url.pathname)) throw redirect(302, '/login'); - } else if ( - (event.url.pathname === '/login' || event.url.pathname.includes('/auth')) && - event.locals.session - ) { - console.log('already authenticated. redirecting to home page'); + else if ( + (event.locals.user && event.url.pathname === '/login') || + (event.locals.user && event.url.pathname.includes('/auth')) + ) throw redirect(302, '/'); - } return await resolve(event); }; -export const handle: Handle = sequence(auth, guard); +export const handle: Handle = sequence(handleAuth, handleUserSession, handleGuard); diff --git a/frontend/src/lib/refreshToken.ts b/frontend/src/lib/refreshToken.ts new file mode 100644 index 0000000..2d21ebd --- /dev/null +++ b/frontend/src/lib/refreshToken.ts @@ -0,0 +1,34 @@ +import { env } from '$env/dynamic/private'; +import { env as publicEnv } from '$env/dynamic/public'; +import { redirect, type RequestEvent } from '@sveltejs/kit'; +import type { OAuth2Response } from './types'; + +export default async (event: RequestEvent) => { + const refreshToken = event.cookies.get('refresh-token'); + if (!refreshToken) throw redirect(302, '/login'); + + const rsp = await fetch('https://discord.com/api/oauth2/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + client_id: env.CLIENT_ID, + client_secret: env.CLIENT_SECRET, + grant_type: 'refresh_token', + refresh_token: refreshToken + }) + }); + + if (!rsp.ok) throw redirect(302, '/login'); + + const { access_token, expires_in }: OAuth2Response = await rsp.json(); + event.cookies.set('access-token', access_token, { + domain: publicEnv.PUBLIC_ORIGIN, + maxAge: expires_in, + expires: new Date(Date.now() + expires_in), + httpOnly: true, + sameSite: true, + path: '/' + }); +}; diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts index 6e1bfb5..f325a7d 100644 --- a/frontend/src/routes/+layout.server.ts +++ b/frontend/src/routes/+layout.server.ts @@ -1,28 +1,5 @@ -import { toast } from '$lib/toast'; import type { LayoutServerLoad } from './$types'; -export const load: LayoutServerLoad = async ({ locals: { session }, cookies }) => { - if (session) { - const rsp = await fetch('https://discord.com/api/v10/users/@me', { - headers: { - Authorization: `Bearer ${cookies.get('access-token')}` - } - }); - - if (!rsp.ok) { - console.log("failed to fetch user's information"); - toast({ type: 'error', message: "failed to fetch user's information" }); - return {}; - } - - const { id, username, avatar } = await rsp.json(); - - return { - id, - username, - avatar: `https://cdn.discordapp.com/avatars/${id}/${avatar}.png` - }; - } - - return {}; +export const load: LayoutServerLoad = async ({ locals: { user } }) => { + return { user }; }; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index b44dd28..6b8a0ee 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -8,7 +8,7 @@ export let data: PageData; - $: ({ username, avatar, id } = data); + $: ({ user } = data); setContextClient( new Client({ @@ -27,13 +27,13 @@
- {#if id && avatar && username} + {#if user}
@@ -43,7 +43,7 @@
- user's avatar + user's avatar
{/if} diff --git a/frontend/src/routes/auth/callback/discord/+server.ts b/frontend/src/routes/auth/callback/discord/+server.ts deleted file mode 100644 index a274338..0000000 --- a/frontend/src/routes/auth/callback/discord/+server.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { env } from '$env/dynamic/private'; -import { env as publicEnv } from '$env/dynamic/public'; -import type { OAuth2Response } from '$lib/types'; -import { redirect } from '@sveltejs/kit'; -import { serialize } from 'cookie'; -import type { RequestHandler } from './$types'; - -export const GET: RequestHandler = async (event) => { - const code = event.url.searchParams.get('code'); - if (!code) { - console.error(`failed to get code in callback url: ${event.url}`); - throw redirect(302, '/login'); - } - - const rsp = await fetch('https://discord.com/api/oauth2/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - client_id: env.CLIENT_ID, - client_secret: env.CLIENT_SECRET, - grant_type: 'authorization_code', - redirect_uri: `${publicEnv.PUBLIC_ORIGIN}/auth/callback/discord`, - code - }) - }); - - if (!rsp.ok) throw redirect(302, '/login'); - - const { access_token, refresh_token, expires_in }: OAuth2Response = await rsp.json(); - - const headers = new Headers(); - headers.set('Location', '/'); - headers.append( - 'Set-Cookie', - serialize('access-token', access_token, { - domain: publicEnv.PUBLIC_ORIGIN, - maxAge: expires_in, - expires: new Date(Date.now() + expires_in), - httpOnly: true, - sameSite: true, - path: '/' - }) - ); - headers.append( - 'Set-Cookie', - serialize('refresh-token', refresh_token, { - domain: publicEnv.PUBLIC_ORIGIN, - maxAge: 60 * 60 * 24 * 7, - expires: new Date(Date.now() + 60 * 60 * 24 * 7), - httpOnly: true, - sameSite: true, - path: '/' - }) - ); - - return new Response(null, { - status: 302, - headers - }); -}; diff --git a/frontend/src/routes/auth/discord/+server.ts b/frontend/src/routes/auth/discord/+server.ts index 1cc9186..9560c6f 100644 --- a/frontend/src/routes/auth/discord/+server.ts +++ b/frontend/src/routes/auth/discord/+server.ts @@ -1,61 +1,14 @@ import { env } from '$env/dynamic/private'; import { env as publicEnv } from '$env/dynamic/public'; -import type { OAuth2Response } from '$lib/types'; -import { serialize } from 'cookie'; -import type { RequestHandler } from './$types'; +import { redirect, type RequestHandler } from '@sveltejs/kit'; -export const GET: RequestHandler = () => { +export const GET: RequestHandler = async () => { const params = new URLSearchParams({ client_id: env.CLIENT_ID, - redirect_uri: `${publicEnv.PUBLIC_ORIGIN}/auth/callback/discord`, + redirect_uri: `${publicEnv.PUBLIC_ORIGIN}/auth/discord/callback`, response_type: 'code', scope: 'identify' }); - return new Response(null, { - headers: { - Location: `https://discord.com/api/oauth2/authorize?${params.toString()}` - }, - status: 302 - }); -}; - -export const PUT: RequestHandler = async ({ request, fetch }) => { - const { refreshToken } = await request.json(); - - const rsp = await fetch('https://discord.com/api/oauth2/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - client_id: env.CLIENT_ID, - client_secret: env.CLIENT_SECRET, - grant_type: 'refresh_token', - refresh_token: refreshToken - }) - }); - - if (!rsp.ok) return new Response(null, { status: 401 }); - - const { access_token, expires_in }: OAuth2Response = await rsp.json(); - - const headers = new Headers(); - headers.set('Location', '/'); - headers.append( - 'Set-Cookie', - serialize('access-token', access_token, { - domain: publicEnv.PUBLIC_ORIGIN, - maxAge: expires_in, - expires: new Date(Date.now() + expires_in), - httpOnly: true, - sameSite: true, - path: '/' - }) - ); - - return new Response(null, { - status: 302, - headers - }); + throw redirect(302, `https://discord.com/api/oauth2/authorize?${params.toString()}`); }; diff --git a/frontend/src/routes/auth/discord/callback/+page.server.ts b/frontend/src/routes/auth/discord/callback/+page.server.ts new file mode 100644 index 0000000..a8597df --- /dev/null +++ b/frontend/src/routes/auth/discord/callback/+page.server.ts @@ -0,0 +1,47 @@ +import { env } from '$env/dynamic/private'; +import { env as publicEnv } from '$env/dynamic/public'; +import type { OAuth2Response } from '$lib/types'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ url, cookies }) => { + const code = url.searchParams.get('code'); + if (!code) { + console.error(`failed to get code in callback url: ${url}`); + return { ok: false, uri: '/login', status: 302 }; + } + + const rsp = await fetch('https://discord.com/api/oauth2/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + client_id: env.CLIENT_ID, + client_secret: env.CLIENT_SECRET, + grant_type: 'authorization_code', + redirect_uri: `${publicEnv.PUBLIC_ORIGIN}/auth/discord/callback`, + code + }) + }); + + if (!rsp.ok) return { ok: false, uri: '/login', status: 302 }; + + const { access_token, refresh_token, expires_in }: OAuth2Response = await rsp.json(); + + cookies.set('access-token', access_token, { + maxAge: expires_in, + httpOnly: true, + sameSite: true, + path: '/', + secure: process.env.NODE_ENV === 'production' + }); + cookies.set('refresh-token', refresh_token, { + maxAge: expires_in * 2, + httpOnly: true, + sameSite: true, + path: '/', + secure: process.env.NODE_ENV === 'production' + }); + + return { ok: true, uri: '/', status: 302 }; +}; diff --git a/frontend/src/routes/auth/discord/callback/+page.svelte b/frontend/src/routes/auth/discord/callback/+page.svelte new file mode 100644 index 0000000..61aed7e --- /dev/null +++ b/frontend/src/routes/auth/discord/callback/+page.svelte @@ -0,0 +1,11 @@ + diff --git a/frontend/src/routes/auth/discord/callback/+page.ts b/frontend/src/routes/auth/discord/callback/+page.ts new file mode 100644 index 0000000..e4ecee0 --- /dev/null +++ b/frontend/src/routes/auth/discord/callback/+page.ts @@ -0,0 +1,5 @@ +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ data: { uri } }) => { + return { uri }; +}; diff --git a/frontend/src/routes/logout/+server.ts b/frontend/src/routes/logout/+server.ts index b14918e..faa5b8d 100644 --- a/frontend/src/routes/logout/+server.ts +++ b/frontend/src/routes/logout/+server.ts @@ -1,9 +1,9 @@ import { redirect } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -export const GET: RequestHandler = async (event) => { - event.cookies.delete('access-token'); - event.cookies.delete('refresh-token'); +export const GET: RequestHandler = async ({ cookies }) => { + cookies.set('access-token', '', { maxAge: -1 }); + cookies.set('refresh-token', '', { maxAge: -1 }); throw redirect(302, '/login'); };