Attic is a cozy space with lofty ambitions.
attic.social
1import { dev } from "$app/environment";
2import {
3 HANDLE_COOKIE,
4 OAUTH_MAX_AGE,
5 SESSION_COOKIE,
6 SESSION_MAX_AGE,
7} from "$lib/server/constants";
8import { decryptText, encryptText } from "$lib/server/crypto";
9import { createOAuthClient } from "$lib/server/oauth";
10import type { AuthEvent } from "$lib/types";
11import { parsePublicUser, type PublicUserData } from "$lib/valibot";
12import { Client } from "@atcute/client";
13import { isHandle } from "@atcute/lexicons/syntax";
14
15/**
16 * Logout
17 */
18export const destroySession = async (
19 event: AuthEvent,
20): Promise<void> => {
21 event.cookies.delete(SESSION_COOKIE, { path: "/" });
22 if (event.locals.user) {
23 try {
24 const oAuthClient = createOAuthClient(event);
25 await oAuthClient.revoke(event.locals.user.did);
26 } catch {
27 // Do nothing?
28 }
29 event.locals.user = undefined;
30 }
31 event.locals.oAuthClient = undefined;
32};
33
34/**
35 * Begin auth flow
36 * @returns {URL} OAuth redirect
37 */
38export const startSession = async (
39 event: AuthEvent,
40 handle: string,
41): Promise<URL> => {
42 if (isHandle(handle) === false) {
43 throw new Error("invalid handle");
44 }
45 const oAuthClient = createOAuthClient(event);
46 const { url } = await oAuthClient.authorize({
47 target: { "type": "account", identifier: handle },
48 });
49 // Temporary to remember handle across oauth flow
50 event.cookies.set(
51 HANDLE_COOKIE,
52 handle,
53 {
54 httpOnly: true,
55 maxAge: OAUTH_MAX_AGE,
56 path: "/",
57 sameSite: "lax",
58 secure: !dev,
59 },
60 );
61 return url;
62};
63
64/**
65 * Store the logged in user data
66 */
67export const updateSession = async (
68 event: AuthEvent,
69 user: PublicUserData,
70) => {
71 const { cookies, platform } = event;
72 if (platform?.env === undefined) {
73 throw new Error();
74 }
75 const encrypted = await encryptText(
76 JSON.stringify(user),
77 platform.env.PRIVATE_COOKIE_KEY,
78 );
79 cookies.set(
80 SESSION_COOKIE,
81 encrypted,
82 {
83 httpOnly: true,
84 maxAge: SESSION_MAX_AGE,
85 path: "/",
86 sameSite: "lax",
87 secure: !dev,
88 },
89 );
90};
91
92/**
93 * Setup OAuth client from cookies
94 */
95export const restoreSession = async (
96 event: AuthEvent,
97): Promise<void> => {
98 const { cookies, platform } = event;
99 if (platform?.env === undefined) {
100 throw new Error();
101 }
102 const encrypted = cookies.get(SESSION_COOKIE);
103 if (encrypted === undefined) {
104 return;
105 }
106 // Parse and validate or delete cookie
107 let data: PublicUserData;
108 try {
109 const decrypted = await decryptText(
110 encrypted,
111 platform?.env.PRIVATE_COOKIE_KEY,
112 );
113 data = parsePublicUser(JSON.parse(decrypted));
114 } catch {
115 cookies.delete(SESSION_COOKIE, { path: "/" });
116 return;
117 }
118 try {
119 const oAuthClient = createOAuthClient(event);
120 const session = await oAuthClient.restore(data.did);
121 const client = new Client({ handler: session });
122 event.locals.user = {
123 ...data,
124 client,
125 session,
126 };
127 } catch {
128 cookies.delete(SESSION_COOKIE, { path: "/" });
129 return;
130 }
131};