Compare commits

..

4 Commits

7 changed files with 73 additions and 61 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = tab
indent_size = 2
max_line_length = 100
quote_type = single
trim_trailing_whitespace = true

7
.zed/settings.json Normal file
View File

@ -0,0 +1,7 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"formatter": "prettier"
}

View File

@ -18,7 +18,7 @@ and [wg-quick](https://www.wireguard.com/quickstart/) for standalone setups.
## Development ## Development
Development uses bun. Development uses bun.
An additional prepare step is needed to set up typia for type validation. An additional prepare step is needed to set up typia for type validation.
For example .env settings, see [.env.example](.env.example) For example .env settings, see [.env.example](.env.example)
@ -27,3 +27,14 @@ bun install
bun run prepare bun run prepare
bun run dev bun run dev
``` ```
## To Do
- [ ] Proper invite page
- [ ] Proper error page for login without invite
- [ ] Support file provider (for wg-quick)
- [ ] Fix accessibility issues (lighthouse)
- [ ] Add title to profile page (lighthouse)
- [ ] wg-quick scripts (maybe?)
- [ ] Get rid of api routes (maybe?)
- [ ] Get rid of custom result types (maybe?)

View File

@ -3,7 +3,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding'; import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema'; import * as table from '$lib/server/db/schema';
import type { RequestEvent } from '@sveltejs/kit'; import type { Cookies } from '@sveltejs/kit';
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
@ -22,14 +22,14 @@ export async function createSession(userId: string): Promise<table.Session> {
const session: table.Session = { const session: table.Session = {
id: sessionId, id: sessionId,
userId, userId,
expiresAt: new Date(Date.now() + DAY_IN_MS * 30) expiresAt: new Date(Date.now() + DAY_IN_MS * 30),
}; };
await db.insert(table.sessions).values(session); await db.insert(table.sessions).values(session);
return session; return session;
} }
export function setSessionTokenCookie(event: RequestEvent, sessionId: string, expiresAt: Date) { export function setSessionTokenCookie(cookies: Cookies, sessionId: string, expiresAt: Date) {
event.cookies.set(sessionCookieName, sessionId, { cookies.set(sessionCookieName, sessionId, {
path: '/', path: '/',
sameSite: 'lax', sameSite: 'lax',
httpOnly: true, httpOnly: true,
@ -42,16 +42,21 @@ export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(table.sessions).where(eq(table.sessions.id, sessionId)); await db.delete(table.sessions).where(eq(table.sessions.id, sessionId));
} }
export function deleteSessionTokenCookie(event: RequestEvent) { export function deleteSessionTokenCookie(cookies: Cookies) {
event.cookies.delete(sessionCookieName, { path: '/' }); cookies.delete(sessionCookieName, { path: '/' });
} }
export async function validateSession(sessionId: string) { export async function validateSession(sessionId: string) {
const [result] = await db const [result] = await db
.select({ .select({
// Adjust user table here to tweak returned data // Adjust user table here to tweak returned data
user: { id: table.users.id, authSource: table.users.authSource, username: table.users.username, name: table.users.name }, user: {
session: table.sessions id: table.users.id,
authSource: table.users.authSource,
username: table.users.username,
name: table.users.name,
},
session: table.sessions,
}) })
.from(table.sessions) .from(table.sessions)
.innerJoin(table.users, eq(table.sessions.userId, table.users.id)) .innerJoin(table.users, eq(table.sessions.userId, table.users.id))

View File

@ -1,14 +1,14 @@
import { fail, redirect } from "@sveltejs/kit"; import { fail, redirect } from '@sveltejs/kit';
import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/auth"; import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth';
import type { Actions } from "./$types"; import type { Actions } from './$types';
export const actions: Actions = { export const actions: Actions = {
logout: async (event) => { logout: async ({ locals, cookies }) => {
if (event.locals.session === null) { if (locals.session === null) {
return fail(401); return fail(401);
} }
await invalidateSession(event.locals.session.id); await invalidateSession(locals.session.id);
deleteSessionTokenCookie(event); deleteSessionTokenCookie(cookies);
return redirect(302, "/"); redirect(302, '/');
} },
}; };

View File

@ -48,7 +48,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
if (existingUser) { if (existingUser) {
const session = await createSession(existingUser.id); const session = await createSession(existingUser.id);
setSessionTokenCookie(event, session.id, session.expiresAt); setSessionTokenCookie(event.cookies, session.id, session.expiresAt);
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
@ -67,7 +67,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
try { try {
await db.insert(table.users).values(user); await db.insert(table.users).values(user);
const session = await createSession(user.id); const session = await createSession(user.id);
setSessionTokenCookie(event, session.id, session.expiresAt); setSessionTokenCookie(event.cookies, session.id, session.expiresAt);
} catch (e) { } catch (e) {
console.error('failed to create user', e); console.error('failed to create user', e);
return new Response(null, { return new Response(null, {

View File

@ -1,32 +1,30 @@
import type { RequestHandler } from './$types';
import * as arctic from 'arctic';
import { google } from '$lib/server/oauth';
import { db } from '$lib/server/db';
import { eq } from 'drizzle-orm';
import * as table from '$lib/server/db/schema';
import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth'; import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import { google } from '$lib/server/oauth';
import { error, redirect } from '@sveltejs/kit';
import type { OAuth2Tokens } from 'arctic'; import type { OAuth2Tokens } from 'arctic';
import * as arctic from 'arctic';
import { eq } from 'drizzle-orm';
import { assertGuard } from 'typia'; import { assertGuard } from 'typia';
import type { PageServerLoad } from './$types';
export const GET: RequestHandler = async (event) => { export const load: PageServerLoad = async (event) => {
const { url, cookies } = event; const { url, cookies } = event;
const code = url.searchParams.get('code'); const code = url.searchParams.get('code');
const state = url.searchParams.get('state'); const state = url.searchParams.get('state');
const storedState = cookies.get('google_oauth_state') ?? null; const storedState = cookies.get('google_oauth_state') ?? null;
const codeVerifier = cookies.get('google_code_verifier') ?? null; const codeVerifier = cookies.get('google_code_verifier') ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) { if (code === null || state === null || storedState === null || codeVerifier === null) {
return new Response(null, { error(400, 'Missing url parameters');
status: 400,
});
} }
const stateGeneratedToken = state.slice(0, storedState.length); const stateGeneratedToken = state.slice(0, storedState.length);
const stateInviteToken = state.slice(storedState.length); const stateInviteToken = state.slice(storedState.length);
if (stateGeneratedToken !== storedState) { if (stateGeneratedToken !== storedState) {
return new Response(null, { error(400, 'Invalid state in url');
status: 400,
});
} }
let tokens: OAuth2Tokens; let tokens: OAuth2Tokens;
@ -35,20 +33,14 @@ export const GET: RequestHandler = async (event) => {
} catch (e) { } catch (e) {
if (e instanceof arctic.OAuth2RequestError) { if (e instanceof arctic.OAuth2RequestError) {
console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e); console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e);
return new Response(null, { throw error(400, 'Invalid authorization code');
status: 400,
});
} }
if (e instanceof arctic.ArcticFetchError) { if (e instanceof arctic.ArcticFetchError) {
console.debug('Arctic: failed to call `fetch()`', e); console.debug('Arctic: failed to call `fetch()`', e);
return new Response(null, { error(400, 'Failed to validate authorization code');
status: 400,
});
} }
console.error('Unxepcted error validating authorization code', code, e);
return new Response(null, { error(500);
status: 500,
});
} }
const idToken = tokens.idToken(); const idToken = tokens.idToken();
@ -67,22 +59,14 @@ export const GET: RequestHandler = async (event) => {
if (existingUser) { if (existingUser) {
const session = await createSession(existingUser.id); const session = await createSession(existingUser.id);
setSessionTokenCookie(event, session.id, session.expiresAt); setSessionTokenCookie(cookies, session.id, session.expiresAt);
return new Response(null, { redirect(302, '/');
status: 302,
headers: {
Location: '/',
},
});
} }
if (!isValidInviteToken(stateInviteToken)) { if (!isValidInviteToken(stateInviteToken)) {
const message = const message =
stateInviteToken.length === 0 ? 'sign up with an invite link first' : 'invalid invite link'; stateInviteToken.length === 0 ? 'sign up with an invite link first' : 'invalid invite link';
error(403, 'Not Authorized: ' + message);
return new Response('Not Authorized: ' + message, {
status: 403,
});
} }
const user: table.User = { const user: table.User = {
@ -98,12 +82,7 @@ export const GET: RequestHandler = async (event) => {
const session = await createSession(user.id); const session = await createSession(user.id);
setSessionTokenCookie(event, session.id, session.expiresAt); setSessionTokenCookie(cookies, session.id, session.expiresAt);
return new Response(null, { redirect(302, '/');
status: 302,
headers: {
Location: '/',
},
});
}; };