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 try {
46 const oAuthClient = createOAuthClient(event);
47 const { url } = await oAuthClient.authorize({
48 target: { "type": "account", identifier: handle },
49 });
50 // Temporary to remember handle across oauth flow
51 event.cookies.set(
52 HANDLE_COOKIE,
53 handle,
54 {
55 httpOnly: true,
56 maxAge: OAUTH_MAX_AGE,
57 path: "/",
58 sameSite: "lax",
59 secure: !dev,
60 },
61 );
62 return url;
63 } catch (err) {
64 console.log(err);
65 throw new Error("OAuth failed.");
66 }
67};
68
69/**
70 * Store the logged in user data
71 */
72export const updateSession = async (
73 event: AuthEvent,
74 user: PublicUserData,
75) => {
76 const { cookies, platform } = event;
77 if (platform?.env === undefined) {
78 throw new Error();
79 }
80 const encrypted = await encryptText(
81 JSON.stringify(user),
82 platform.env.PRIVATE_COOKIE_KEY,
83 );
84 cookies.set(
85 SESSION_COOKIE,
86 encrypted,
87 {
88 httpOnly: true,
89 maxAge: SESSION_MAX_AGE,
90 path: "/",
91 sameSite: "lax",
92 secure: !dev,
93 },
94 );
95};
96
97/**
98 * Setup OAuth client from cookies
99 */
100export const restoreSession = async (
101 event: AuthEvent,
102): Promise<void> => {
103 const { cookies, platform } = event;
104 if (platform?.env === undefined) {
105 throw new Error();
106 }
107 const encrypted = cookies.get(SESSION_COOKIE);
108 if (encrypted === undefined) {
109 return;
110 }
111 // Parse and validate or delete cookie
112 let data: PublicUserData;
113 try {
114 const decrypted = await decryptText(
115 encrypted,
116 platform?.env.PRIVATE_COOKIE_KEY,
117 );
118 data = parsePublicUser(JSON.parse(decrypted));
119 } catch {
120 cookies.delete(SESSION_COOKIE, { path: "/" });
121 return;
122 }
123 try {
124 const oAuthClient = createOAuthClient(event);
125 const session = await oAuthClient.restore(data.did);
126 const client = new Client({ handler: session });
127 event.locals.user = {
128 ...data,
129 client,
130 session,
131 };
132 } catch {
133 cookies.delete(SESSION_COOKIE, { path: "/" });
134 return;
135 }
136};