Skip to content

Commit 22363ff

Browse files
committed
add back authentication (hopefully ts works in prod idk)
1 parent f779037 commit 22363ff

File tree

11 files changed

+833
-42
lines changed

11 files changed

+833
-42
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
"cSpell.words": [
33
"Cloaker",
44
"dbparams",
5+
"esoka",
56
"notif",
67
"notifs",
8+
"sqpi",
79
"unmarshall",
810
"unmarshalled"
911
]

bun.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@sveltejs/adapter-static": "^3.0.9",
1313
"@types/node": "^24.7.2",
1414
"fs": "^0.0.1-security",
15+
"oidc-client-ts": "^3.3.0",
1516
},
1617
"devDependencies": {
1718
"@eslint/compat": "^1.2.5",
@@ -551,6 +552,8 @@
551552

552553
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
553554

555+
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
556+
554557
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
555558

556559
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@@ -593,6 +596,8 @@
593596

594597
"obliterator": ["obliterator@1.6.1", "", {}, "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig=="],
595598

599+
"oidc-client-ts": ["oidc-client-ts@3.3.0", "", { "dependencies": { "jwt-decode": "^4.0.0" } }, "sha512-t13S540ZwFOEZKLYHJwSfITugupW4uYLwuQSSXyKH/wHwZ+7FvgHE7gnNJh1YQIZ1Yd1hKSRjqeXGSUtS0r9JA=="],
600+
596601
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
597602

598603
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@aws-sdk/util-dynamodb": "^3.883.0",
6161
"@sveltejs/adapter-static": "^3.0.9",
6262
"@types/node": "^24.7.2",
63-
"fs": "^0.0.1-security"
63+
"fs": "^0.0.1-security",
64+
"oidc-client-ts": "^3.3.0"
6465
}
6566
}

src/lib/authentication.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { UserManager } from "oidc-client-ts";
2+
3+
4+
5+
export interface Tokens {
6+
accessToken: string | undefined;
7+
idToken: string | undefined;
8+
refreshToken: string | undefined;
9+
}
10+
const cognitoAuthConfig = {
11+
authority: "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_lg1qptg2n",
12+
client_id: "4d6esoka62s46lo4d398o3sqpi",
13+
redirect_uri: "http://localhost:5173/auth/callback",
14+
response_type: "code",
15+
scope: "aws.cognito.signin.user.admin email openid phone profile"
16+
};
17+
18+
export const userManager = new UserManager({
19+
...cognitoAuthConfig
20+
});
21+
22+
23+
export async function signinRequest() {
24+
await userManager.signinRedirect();
25+
}

src/lib/components/Navigation.svelte

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,63 @@
11
<script lang="ts">
22
import { page } from "$app/state";
3-
const links = (typeof window !== "undefined" && window.origin.includes("amazonaws"))
4-
? [
5-
["Notifications", "/notifications"],
6-
["Tab Cloaker", "/tab-cloaker.html"],
7-
[
8-
"Master Doc",
9-
"https://docs.google.com/document/d/11yw7n2F84XOkAwpM8tF-ZYHESuus1Gg7dmJ-WJum1fk",
10-
],
11-
["ROM Library", "/roms.html"],
12-
["Discord", "https://discord.gg/GDEFRBTT3Z"],
13-
]
14-
: [
15-
["Notifications", "/notifications"],
16-
["Tab Cloaker", "/tab-cloaker"],
17-
[
18-
"Master Doc",
19-
"https://docs.google.com/document/d/11yw7n2F84XOkAwpM8tF-ZYHESuus1Gg7dmJ-WJum1fk",
20-
],
21-
["ROM Library", "/roms"],
22-
["Discord", "https://discord.gg/GDEFRBTT3Z"],
23-
];
3+
import { initializeAds } from "$lib/adSlotConfig.js";
4+
import { initializeTooling, SessionState } from "$lib/state.js";
5+
import { onMount } from "svelte";
6+
7+
type Link = [string, string];
8+
let links = $state<Link[]>([
9+
["Notifications", "/notifications"],
10+
["Tab Cloaker", "/tab-cloaker"],
11+
[
12+
"Master Doc",
13+
"https://docs.google.com/document/d/11yw7n2F84XOkAwpM8tF-ZYHESuus1Gg7dmJ-WJum1fk",
14+
],
15+
["ROM Library", "/roms"],
16+
["Discord", "https://discord.gg/GDEFRBTT3Z"],
17+
["Login", "/auth/login"],
18+
]);
19+
20+
onMount(() => {
21+
if (typeof window !== "undefined" && window.origin.includes("amazonaws")) {
22+
links = [
23+
["Notifications", "/notifications"],
24+
["Tab Cloaker", "/tab-cloaker.html"],
25+
[
26+
"Master Doc",
27+
"https://docs.google.com/document/d/11yw7n2F84XOkAwpM8tF-ZYHESuus1Gg7dmJ-WJum1fk",
28+
],
29+
["ROM Library", "/roms.html"],
30+
["Discord", "https://discord.gg/GDEFRBTT3Z"],
31+
]
32+
}
33+
});
34+
35+
// Optional helpers to update links at runtime
36+
export function setLinks(newLinks: Link[]) {
37+
links = newLinks;
38+
}
39+
export function addLink(name: string, url: string) {
40+
links = [...links, [name, url]];
41+
}
42+
export function removeLinkByUrl(url: string) {
43+
links = links.filter(([, u]) => u !== url);
44+
}
2445
2546
const currentPath = page.url.pathname;
2647
let menuOpen = $state(false);
48+
49+
50+
onMount(async () => {
51+
await initializeTooling();
52+
53+
54+
const user = SessionState.user;
55+
56+
if (user) {
57+
removeLinkByUrl("/auth/login");
58+
// addLink("Profile", "/auth/profile");
59+
}
60+
})
2761
</script>
2862

2963
<div class="n-container">

src/lib/state.ts

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,57 @@ import { S3Client } from "@aws-sdk/client-s3";
88
import { detectAdBlockEnabled } from "./helpers.js";
99
import { page } from "$app/state";
1010
import { goto } from "$app/navigation";
11+
import type { User } from "oidc-client-ts";
12+
import { createModal } from "$lib/modal.js";
13+
import { signinRequest } from "$lib/authentication.js";
1114

15+
16+
interface Tokens {
17+
idToken: string;
18+
accessToken: string;
19+
refreshToken: string;
20+
expiresAt?: number; // epoch ms
21+
}
22+
const TOKEN_STORAGE_KEY = 'ccported_tokens';
23+
24+
function readStoredTokens(): Tokens | null {
25+
if (!browser) return null;
26+
try {
27+
const raw = localStorage.getItem(TOKEN_STORAGE_KEY);
28+
if (!raw) return null;
29+
return JSON.parse(raw);
30+
} catch {
31+
return null;
32+
}
33+
}
34+
35+
function clearStoredTokens() {
36+
if (!browser) return;
37+
try { localStorage.removeItem(TOKEN_STORAGE_KEY); } catch {}
38+
}
39+
40+
function isExpired(expiresAt?: number, skewSec = 60): boolean {
41+
if (!expiresAt) return true;
42+
return Date.now() >= (expiresAt - skewSec * 1000);
43+
}
44+
45+
function decodeJwt<T = any>(token?: string): T | null {
46+
if (!token) return null;
47+
try {
48+
const parts = token.split('.');
49+
if (parts.length !== 3) return null;
50+
const payload = atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'));
51+
const json = decodeURIComponent(
52+
payload
53+
.split('')
54+
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
55+
.join('')
56+
);
57+
return JSON.parse(json);
58+
} catch {
59+
return null;
60+
}
61+
}
1262
export const SessionState = {
1363
awsReady: false,
1464
ssr: !browser,
@@ -20,7 +70,10 @@ export const SessionState = {
2070
devMode: (browser && window.location.hostname === "localhost"),
2171
serverResponses: [] as { server: Server; success: boolean; time: number, reason: string }[],
2272
plays: 0,
23-
user: null as null | { name: string; email: string; tokens: any } | undefined,
73+
user: null as null | {
74+
profile?: any;
75+
tokens?: Tokens;
76+
},
2477
loggedIn: false
2578
}
2679

@@ -82,7 +135,7 @@ export async function initializeTooling() {
82135
return;
83136
}
84137
initializingTooling = true;
85-
138+
86139
// Handle SetServer query parameter
87140
if (browser && window) {
88141
const urlParams = new URLSearchParams(window.location.search);
@@ -108,8 +161,50 @@ export async function initializeTooling() {
108161
newUrl.search = urlParams.toString();
109162
window.history.replaceState(null, '', newUrl.toString());
110163
}
164+
165+
// Initialize auth state from persisted tokens (runs client-side only)
166+
const storedTokens = readStoredTokens();
167+
if (storedTokens) {
168+
console.log("[initializeTooling] Found stored tokens.", storedTokens);
169+
if (isExpired(storedTokens.expiresAt)) {
170+
// Tokens expired: inform the user and offer to log in again or continue without login
171+
createModal({
172+
title: 'Session expired',
173+
content: 'Your session has expired and you have been signed out. You can log in again or continue without logging in.',
174+
actions: [
175+
{
176+
label: 'Log in',
177+
onClick: () => {
178+
// Attempt a fresh sign-in redirect
179+
try { signinRequest(); } catch {}
180+
}
181+
},
182+
{
183+
label: 'Continue without login',
184+
onClick: (api) => {
185+
clearStoredTokens();
186+
SessionState.user = null as any;
187+
SessionState.loggedIn = false;
188+
api.close();
189+
}
190+
}
191+
]
192+
});
193+
} else {
194+
// Tokens valid: derive a user profile from the id token claims
195+
const profile = decodeJwt(storedTokens.idToken);
196+
if (!SessionState.user) {
197+
SessionState.user = {};
198+
}
199+
SessionState.user.tokens = storedTokens;
200+
if (profile) {
201+
SessionState.user.profile = profile as any;
202+
SessionState.loggedIn = true;
203+
}
204+
}
205+
}
111206
}
112-
207+
113208
const server = await findServer();
114209
if (!server) {
115210
console.error("No available servers found.");
@@ -145,7 +240,7 @@ export async function initializeTooling() {
145240
});
146241
}
147242

148-
const credentials = await initializeUnathenticated();
243+
const credentials = await initializeUnauthenticated();
149244
const dynamoDBClient = new DynamoDBClient({
150245
region: "us-west-2",
151246
credentials
@@ -562,7 +657,7 @@ export async function getAllAvailableServers(servers: Server[]): Promise<Server[
562657
return availableServers;
563658
}
564659

565-
async function initializeUnathenticated() {
660+
async function initializeUnauthenticated() {
566661

567662
const identityPoolId = "us-west-2:8ffe94a1-9042-4509-8e65-4efe16e61e3e";
568663
const credentials = fromCognitoIdentityPool({
@@ -573,4 +668,3 @@ async function initializeUnathenticated() {
573668
SessionState.awsReady = true;
574669
return credentials;
575670
}
576-

src/routes/auth/callback/+page.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Client-only route to process the OIDC redirect, persist tokens, and bounce home
2+
import { redirect } from '@sveltejs/kit';
3+
export const ssr = false;
4+
export const csr = true;
5+
6+
import { userManager } from '$lib/authentication.js';
7+
import { SessionState } from '$lib/state.js';
8+
9+
const TOKEN_STORAGE_KEY = 'ccported_tokens';
10+
11+
type StoredTokens = {
12+
accessToken?: string;
13+
idToken?: string;
14+
refreshToken?: string;
15+
expiresAt?: number; // epoch ms
16+
};
17+
18+
export async function load() {
19+
// This runs in the browser only (ssr=false)
20+
try {
21+
// Complete the sign-in redirect flow and obtain the user
22+
const user = await userManager.signinCallback(window.location.href);
23+
24+
const tokens: StoredTokens = {
25+
accessToken: user?.access_token,
26+
idToken: user?.id_token,
27+
refreshToken: user?.refresh_token,
28+
// oidc-client-ts provides expires_at in seconds since epoch
29+
expiresAt: user?.expires_at ? user.expires_at * 1000 : undefined
30+
};
31+
32+
// Persist tokens for initializeTooling to read later
33+
try {
34+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(tokens));
35+
} catch (e) {
36+
console.warn('[auth/callback] Failed to store tokens', e);
37+
}
38+
39+
// Stash profile in SessionState for immediate use this navigation cycle
40+
if (user?.profile) {
41+
SessionState.user = user.profile as any;
42+
SessionState.loggedIn = true;
43+
}
44+
} catch (err) {
45+
console.error('[auth/callback] signinCallback failed, trying signinRedirectCallback()', err);
46+
try {
47+
const user = await (userManager as any).signinRedirectCallback?.(window.location.href);
48+
const tokens: StoredTokens = {
49+
accessToken: user?.access_token,
50+
idToken: user?.id_token,
51+
refreshToken: (user as any)?.refresh_token,
52+
expiresAt: user?.expires_at ? user.expires_at * 1000 : undefined
53+
};
54+
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(tokens));
55+
if (user?.profile) {
56+
SessionState.user = user.profile as any;
57+
SessionState.loggedIn = true;
58+
}
59+
} catch (err2) {
60+
console.error('[auth/callback] Redirect callback also failed', err2);
61+
}
62+
}
63+
64+
// Navigate back to the home page (or prior route if desired later)
65+
throw redirect(302, '/');
66+
}
67+

0 commit comments

Comments
 (0)