diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a3eeed9a..1907f487e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,6 +133,7 @@ jobs: run: 'deno task build:preview' env: TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} + TRAKT_SESSION_SECRET: ${{ secrets.TRAKT_SESSION_SECRET }} - name: Witnessing the Premonition ake Run Preview working-directory: projects/client @@ -202,6 +203,7 @@ jobs: FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} FIREBASE_MEASUREMENT_ID: ${{ secrets.FIREBASE_MEASUREMENT_ID }} FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.FIREBASE_MESSAGING_SENDER_ID }} + TRAKT_SESSION_SECRET: ${{ secrets.TRAKT_SESSION_SECRET }} - name: Delivering the Goods aka Upload Build if: github.ref == 'refs/heads/main' diff --git a/INFRASTRUCTURE.md b/INFRASTRUCTURE.md index 78bf0a9f9..fa48e1b71 100644 --- a/INFRASTRUCTURE.md +++ b/INFRASTRUCTURE.md @@ -45,7 +45,6 @@ intone the following incantation: # This is required if the secrets are not already set or have changed echo "$TRAKT_CLIENT_ID" | npx wrangler pages secret put TRAKT_CLIENT_ID echo "$TRAKT_CLIENT_SECRET" | npx wrangler pages secret put TRAKT_CLIENT_SECRET -echo "$TRAKT_SESSION_SECRET" | npx wrangler pages secret put TRAKT_SESSION_SECRET # This will build the client and deploy it to Cloudflare Pages [deno|npm|bun] task build && npx wrangler pages deploy diff --git a/README.md b/README.md index ca2a14a6f..8a317e17c 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,6 @@ properly: - **`TRAKT_CLIENT_ID`:** The client ID for the Trakt API. - **`TRAKT_CLIENT_SECRET`:** The client secret for the Trakt API. - Required for the `playground` project. -- **`TRAKT_SESSION_SECRET`:** The session encryption secret for the Trakt API. - - `deno task insecurity:generate` ### External Contribution - Unleash Your Inner Code Wizard! diff --git a/projects/client/.scripts/generate-insecurity.ts b/projects/client/.scripts/generate-insecurity.ts deleted file mode 100644 index c1a04a6f4..000000000 --- a/projects/client/.scripts/generate-insecurity.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * DOCS: https://docs.deno.com/examples/aes_encryption/ - */ -export async function generateKey() { - return await crypto.subtle.generateKey( - { - name: 'AES-GCM', - length: 256, - }, - true, - ['encrypt', 'decrypt'], - ); -} - -async function toBase64(key: CryptoKey) { - const exported = await crypto.subtle.exportKey('raw', key); - return btoa(String.fromCharCode(...new Uint8Array(exported))); -} - -if (import.meta.main) { - const key = await generateKey(); - const base64Key = await toBase64(key); - console.log(`export TRAKT_SESSION_SECRET='${base64Key}'`); -} diff --git a/projects/client/package.json b/projects/client/package.json index 13467ebbd..b9bb3ab21 100644 --- a/projects/client/package.json +++ b/projects/client/package.json @@ -18,8 +18,7 @@ "i18n:resolve": "deno run --allow-read --allow-write .scripts/resolve-i18n.ts", "i18n:traktify": "deno run --allow-read --allow-write --allow-net --allow-env .scripts/traktify-i18n.ts", "i18n:delete": "deno run --allow-read --allow-write --allow-env .scripts/delete-i18n.ts", - "cloudflare:cleanse": "deno run --allow-env --allow-net .scripts/cleanse-cloudflare.ts", - "insecurity:generate": "deno run .scripts/generate-insecurity.ts" + "cloudflare:cleanse": "deno run --allow-env --allow-net .scripts/cleanse-cloudflare.ts" }, "devDependencies": { "@cucumber/cucumber": "^11.2.0", diff --git a/projects/client/src/hooks.server.ts b/projects/client/src/hooks.server.ts index f7e4b085f..1b44b77d8 100644 --- a/projects/client/src/hooks.server.ts +++ b/projects/client/src/hooks.server.ts @@ -49,5 +49,5 @@ export const handle: Handle = sequence( }, handleCacheControl, handleMobileOperatingSystem, - handleGateway, + handleGateway, // TODO remove March 31st ); diff --git a/projects/client/src/lib/features/auth/handle.spec.ts b/projects/client/src/lib/features/auth/handle.spec.ts index f3fa3cc1c..fa066c391 100644 --- a/projects/client/src/lib/features/auth/handle.spec.ts +++ b/projects/client/src/lib/features/auth/handle.spec.ts @@ -1,12 +1,11 @@ +import { AuthEndpoint } from '$lib/features/auth/AuthEndpoint.ts'; import { AuthMappedMock } from '$mocks/data/auth/AuthMappedMock.ts'; +import { AuthMock } from '$mocks/data/auth/AuthMock.ts'; import { EncryptedAuthMock } from '$mocks/data/auth/EncryptedAuthMock.ts'; -import { EncryptedExpiredAuthMock } from '$mocks/data/auth/EncryptedExpiredAuthMock.ts'; +import { ExpiredAuthMock } from '$mocks/data/auth/ExpiredAuthMock.ts'; import { mockRequestEvent } from '$test/request/mockRequestEvent.ts'; import { describe, expect, it, vi } from 'vitest'; -import { AuthEndpoint } from './AuthEndpoint.ts'; -import { key } from './environment.ts'; import { AUTH_COOKIE_NAME, handle } from './handle.ts'; -import { decrypt } from './utils/decrypt.ts'; describe('handle: auth', () => { it('should handle logout', async () => { @@ -41,7 +40,7 @@ describe('handle: auth', () => { }); }); - it('should handle decryption failure', async () => { + it('should handle invalid cookie contents', async () => { const event = mockRequestEvent({ url: 'http://localhost', cookieHandler: (key: string) => { @@ -53,27 +52,19 @@ describe('handle: auth', () => { }, }); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); await handle({ event, resolve: vi.fn() }); expect(event.locals.auth).toEqual(null); expect(event.cookies.getAll().find((c) => c.name)?.value) .toEqual(''); - expect(consoleSpy).toHaveBeenCalled(); - consoleSpy.mockRestore(); }); - it('should handle legacy auth cookie', async () => { - const decryptedAuth = await decrypt( - key, - EncryptedAuthMock, - ); - + it('should handle auth cookie', async () => { const event = mockRequestEvent({ url: 'http://localhost', cookieHandler: (key: string) => { if (key === AUTH_COOKIE_NAME) { - return JSON.stringify(decryptedAuth); + return AuthMock; } return null; @@ -82,13 +73,10 @@ describe('handle: auth', () => { await handle({ event, resolve: vi.fn() }); - expect(event.locals.auth).toEqual(decryptedAuth); - expect(event.cookies.set).toHaveBeenCalled(); - expect(event.cookies.getAll().find((c) => c.name)?.value) - .toEqual(EncryptedAuthMock); + expect(event.locals.auth).toEqual(AuthMappedMock); }); - it('should handle encrypted auth cookie', async () => { + it('should handle legacy encrypted auth cookie', async () => { const event = mockRequestEvent({ url: 'http://localhost', cookieHandler: (key: string) => { @@ -102,6 +90,9 @@ describe('handle: auth', () => { await handle({ event, resolve: vi.fn() }); + expect(event.cookies.set).toHaveBeenCalled(); + expect(event.cookies.getAll().find((c) => c.name)?.value) + .toEqual(AuthMock); expect(event.locals.auth).toEqual(AuthMappedMock); }); @@ -110,7 +101,7 @@ describe('handle: auth', () => { url: 'http://localhost', cookieHandler: (key: string) => { if (key === AUTH_COOKIE_NAME) { - return EncryptedExpiredAuthMock; + return ExpiredAuthMock; } return null; diff --git a/projects/client/src/lib/features/auth/handle.ts b/projects/client/src/lib/features/auth/handle.ts index c06d3bf5e..a09c5f3fc 100644 --- a/projects/client/src/lib/features/auth/handle.ts +++ b/projects/client/src/lib/features/auth/handle.ts @@ -9,12 +9,18 @@ import type { } from './models/SerializedAuthResponse.ts'; import { authorize } from './requests/authorize.ts'; import { decrypt } from './utils/decrypt.ts'; -import { encrypt } from './utils/encrypt.ts'; export const AUTH_COOKIE_NAME = 'trakt-auth'; const REFRESH_THRESHOLD_MINUTES = 15; -function getLegacyAuthCookie(event: RequestEvent) { +async function getEncryptedAuth( + event: RequestEvent, +): Promise { + const encrypted = event.cookies.get(AUTH_COOKIE_NAME); + return await decrypt(key, encrypted); +} + +function getAuth(event: RequestEvent): SerializedAuthResponse | null { try { const serializedToken = event.cookies.get(AUTH_COOKIE_NAME) ?? ''; return JSON.parse(serializedToken) as SerializedAuthResponse; @@ -71,7 +77,7 @@ export const handle: Handle = async ({ event, resolve }) => { if (isAuthorized) { const cookie = event.cookies.serialize( AUTH_COOKIE_NAME, - await encrypt(key, result), + JSON.stringify(result), { httpOnly: true, secure: true, @@ -92,18 +98,21 @@ export const handle: Handle = async ({ event, resolve }) => { }); } - //TODO: remove this migration after March 1st 2025 - const legacyAuthCookie = getLegacyAuthCookie(event); - if (legacyAuthCookie != null) { - setAuth(legacyAuthCookie); + const authResponse = getAuth(event); + + // TODO remove this March 31 + const decryptedAuthResponse = await getEncryptedAuth(event); + const isDecryptionFailed = decryptedAuthResponse == null; + if (!authResponse && !isDecryptionFailed) { + setAuth(decryptedAuthResponse); event.cookies.set( AUTH_COOKIE_NAME, - await encrypt(key, legacyAuthCookie), + JSON.stringify(decryptedAuthResponse), { httpOnly: true, secure: true, - expires: new Date(legacyAuthCookie.expiresAt ?? 0), + expires: new Date(decryptedAuthResponse?.expiresAt ?? 0), path: '/', }, ); @@ -111,21 +120,17 @@ export const handle: Handle = async ({ event, resolve }) => { return await resolve(event); } - const encrypted = event.cookies.get(AUTH_COOKIE_NAME); - const decrypted = await decrypt(key, encrypted); - const isDecryptionFailed = decrypted == null && encrypted != null; - const minutesToExpiry = Math.floor( - (new Date(decrypted?.expiresAt ?? 0).getTime() - Date.now()) / + (new Date(authResponse?.expiresAt ?? 0).getTime() - Date.now()) / time.minutes(1), ); const shouldRefresh = minutesToExpiry <= REFRESH_THRESHOLD_MINUTES; - if (shouldRefresh && decrypted?.token.refresh != null) { + if (shouldRefresh && authResponse?.token.refresh != null) { const result = await authorize({ token: { type: 'refresh', - refreshToken: decrypted.token.refresh, + refreshToken: authResponse.token.refresh, }, referrer: getReferrer(), }); @@ -133,7 +138,7 @@ export const handle: Handle = async ({ event, resolve }) => { setAuth(result); event.cookies.set( AUTH_COOKIE_NAME, - await encrypt(key, result), + JSON.stringify(result), { httpOnly: true, secure: true, @@ -145,9 +150,9 @@ export const handle: Handle = async ({ event, resolve }) => { return await resolve(event); } - setAuth(decrypted); + setAuth(authResponse); - if (isDecryptionFailed) { + if (!authResponse) { event.cookies.set(AUTH_COOKIE_NAME, '', { httpOnly: true, secure: true, diff --git a/projects/client/src/lib/features/auth/stores/useAuth.ts b/projects/client/src/lib/features/auth/stores/useAuth.ts index e713f641c..b4ae8ef96 100644 --- a/projects/client/src/lib/features/auth/stores/useAuth.ts +++ b/projects/client/src/lib/features/auth/stores/useAuth.ts @@ -1,4 +1,5 @@ import { AuthEndpoint } from '$lib/features/auth/AuthEndpoint.ts'; +import { setToken } from '$lib/features/auth/token/index.ts'; import { InvalidateAction } from '$lib/requests/models/InvalidateAction.ts'; import { useInvalidator } from '$lib/stores/useInvalidator.ts'; import { assertDefined } from '$lib/utils/assert/assertDefined.ts'; @@ -39,6 +40,7 @@ export function useAuth() { await fetch(AuthEndpoint.Logout, { method: 'POST', }); + setToken(null); isAuthorized.set(false); await invalidate(InvalidateAction.Auth); globalThis.location.href = setCacheBuster( diff --git a/projects/client/src/lib/features/auth/token/index.ts b/projects/client/src/lib/features/auth/token/index.ts new file mode 100644 index 000000000..bc4092765 --- /dev/null +++ b/projects/client/src/lib/features/auth/token/index.ts @@ -0,0 +1,13 @@ +const token: { + value: string | Nil; +} = { + value: null, +}; + +export function getToken() { + return token.value; +} + +export function setToken(value: string | Nil) { + token.value = value; +} diff --git a/projects/client/src/lib/features/auth/utils/decrypt.spec.ts b/projects/client/src/lib/features/auth/utils/decrypt.spec.ts index d9a33a19f..ac01ca346 100644 --- a/projects/client/src/lib/features/auth/utils/decrypt.spec.ts +++ b/projects/client/src/lib/features/auth/utils/decrypt.spec.ts @@ -1,7 +1,7 @@ import { AuthMappedMock } from '$mocks/data/auth/AuthMappedMock.ts'; import { EncryptedAuthMock } from '$mocks/data/auth/EncryptedAuthMock.ts'; import { encryptionKeyMock } from '$mocks/data/auth/encryptionKeyMock.ts'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { decrypt } from './decrypt.ts'; describe('utils: decrypt', () => { @@ -20,23 +20,17 @@ describe('utils: decrypt', () => { }); it('should return null if data is invalid', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const encryptionKey = await encryptionKeyMock(); const data = await decrypt(encryptionKey, 'invalid data'); expect(data).toBeNull(); - expect(consoleSpy).toHaveBeenCalled(); - consoleSpy.mockRestore(); }); it('should return null if crypto.subtle.decrypt fails', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const encryptionKey = await encryptionKeyMock(1337); const data = await decrypt(encryptionKey, EncryptedAuthMock); expect(data).toBeNull(); - expect(consoleSpy).toHaveBeenCalled(); - consoleSpy.mockRestore(); }); }); diff --git a/projects/client/src/lib/features/auth/utils/decrypt.ts b/projects/client/src/lib/features/auth/utils/decrypt.ts index 6a3d38577..c34138adc 100644 --- a/projects/client/src/lib/features/auth/utils/decrypt.ts +++ b/projects/client/src/lib/features/auth/utils/decrypt.ts @@ -1,5 +1,3 @@ -import { error } from '$lib/utils/console/print.ts'; - export async function decrypt( key: CryptoKey, data: string | Nil, @@ -28,7 +26,6 @@ export async function decrypt( return JSON.parse(jsonString); } catch (_) { - error('Failed to decrypt data', _); return null; } } diff --git a/projects/client/src/lib/features/auth/utils/encrypt.spec.ts b/projects/client/src/lib/features/auth/utils/encrypt.spec.ts deleted file mode 100644 index 78903fb74..000000000 --- a/projects/client/src/lib/features/auth/utils/encrypt.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { AuthMappedMock } from '$mocks/data/auth/AuthMappedMock.ts'; -import { EncryptedAuthMock } from '$mocks/data/auth/EncryptedAuthMock.ts'; -import { encryptionKeyMock } from '$mocks/data/auth/encryptionKeyMock.ts'; -import { describe, expect, it } from 'vitest'; -import { encrypt } from './encrypt.ts'; - -describe('utils: encrypt', () => { - it('should encrypt', async () => { - const testCryptoKey = await encryptionKeyMock(); - const encryptedData = await encrypt(testCryptoKey, AuthMappedMock); - - expect(encryptedData).toEqual(EncryptedAuthMock); - }); - - it('should result in different results for different keys', async () => { - const keys = await Promise.all( - Array.from({ length: 5 }, () => - crypto.subtle.generateKey( - { - name: 'AES-GCM', - length: 256, - }, - true, - ['encrypt'], - )), - ); - - const results = await Promise.all( - keys.map((key) => encrypt(key, EncryptedAuthMock)), - ); - expect(new Set(results).size).toBe(results.length); - }); -}); diff --git a/projects/client/src/lib/features/auth/utils/encrypt.ts b/projects/client/src/lib/features/auth/utils/encrypt.ts deleted file mode 100644 index 7af83a7ed..000000000 --- a/projects/client/src/lib/features/auth/utils/encrypt.ts +++ /dev/null @@ -1,16 +0,0 @@ -export async function encrypt( - key: CryptoKey, - data: T, -): Promise { - const jsonString = JSON.stringify(data); - const encryptedBuffer = await crypto.subtle.encrypt( - { - name: 'AES-GCM', - iv: new Uint8Array(12), - }, - key, - new TextEncoder().encode(jsonString), - ); - - return btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer))); -} diff --git a/projects/client/src/lib/features/gateway/handle.ts b/projects/client/src/lib/features/gateway/handle.ts index b129af900..ede6b6a98 100644 --- a/projects/client/src/lib/features/gateway/handle.ts +++ b/projects/client/src/lib/features/gateway/handle.ts @@ -4,9 +4,9 @@ import type { Handle } from '@sveltejs/kit'; export const handle: Handle = async ({ event, resolve }) => { const url = new URL(event.request.url); - if (url.pathname.startsWith(ClientEnvironment.svelte)) { + if (url.pathname.startsWith(ClientEnvironment.legacy)) { const traktUrl = new URL( - url.pathname.replace(ClientEnvironment.svelte, ''), + url.pathname.replace(ClientEnvironment.legacy, ''), TRAKT_TARGET_ENVIRONMENT, ); diff --git a/projects/client/src/lib/requests/ClientEnvironment.ts b/projects/client/src/lib/requests/ClientEnvironment.ts index f33cacc93..5cf0df5b8 100644 --- a/projects/client/src/lib/requests/ClientEnvironment.ts +++ b/projects/client/src/lib/requests/ClientEnvironment.ts @@ -1,4 +1,5 @@ export enum ClientEnvironment { - svelte = '/_api/trakt', + legacy = '/_api/trakt', + development = '/api/trakt', test = 'http://localhost', } diff --git a/projects/client/src/lib/requests/_internal/createAuthenticatedFetch.ts b/projects/client/src/lib/requests/_internal/createAuthenticatedFetch.ts new file mode 100644 index 000000000..282517c3f --- /dev/null +++ b/projects/client/src/lib/requests/_internal/createAuthenticatedFetch.ts @@ -0,0 +1,33 @@ +import { getToken } from '$lib/features/auth/token/index.ts'; +import { error } from '$lib/utils/console/print.ts'; + +export function createAuthenticatedFetch< + T extends typeof fetch, +>(baseFetch: T): T { + return (function authenticatedFetch( + input: Parameters[0], + init?: Parameters[1], + ): Promise { + const modifiedInit = { ...init } as Parameters[1]; + const headers = new Headers(modifiedInit?.headers || {}); + + try { + const token = getToken(); + + if (token) { + headers.set('Authorization', `Bearer ${token}`); + } + + return baseFetch( + input, + { + ...modifiedInit, + headers, + } as Parameters[1], + ); + } catch (e) { + error('Fetch interceptor error:', e); + return baseFetch(input, init); + } + }) as unknown as T; +} diff --git a/projects/client/src/lib/requests/api.ts b/projects/client/src/lib/requests/api.ts index ac8ec25ca..f41410094 100644 --- a/projects/client/src/lib/requests/api.ts +++ b/projects/client/src/lib/requests/api.ts @@ -1,21 +1,22 @@ -import { IS_TEST } from '$lib/utils/env/index.ts'; +import { createAuthenticatedFetch } from '$lib/requests/_internal/createAuthenticatedFetch.ts'; +import { ClientEnvironment } from '$lib/requests/ClientEnvironment.ts'; +import { IS_DEV, IS_PREVIEW, IS_TEST } from '$lib/utils/env/index.ts'; import { traktApi, type TraktApiOptions } from '@trakt/api'; export type ApiParams = Omit & { environment?: HttpsUrl; }; -enum ClientEnvironment { - svelte = '/_api/trakt', - test = 'http://localhost', -} - const ENV = (() => { + if (IS_DEV || IS_PREVIEW) { + return ClientEnvironment.development as unknown as HttpsUrl; + } + if (IS_TEST) { return ClientEnvironment.test as unknown as HttpsUrl; } - return ClientEnvironment.svelte as unknown as HttpsUrl; + return TRAKT_TARGET_ENVIRONMENT as unknown as HttpsUrl; })(); export const api = ({ @@ -27,7 +28,7 @@ export const api = ({ traktApi({ apiKey: TRAKT_CLIENT_ID, environment, - fetch, + fetch: createAuthenticatedFetch(fetch), cancellable, cancellationId, }); diff --git a/projects/client/src/mocks/data/auth/AuthMock.ts b/projects/client/src/mocks/data/auth/AuthMock.ts new file mode 100644 index 000000000..8cb1e0530 --- /dev/null +++ b/projects/client/src/mocks/data/auth/AuthMock.ts @@ -0,0 +1,8 @@ +export const AuthMock = JSON.stringify({ + token: { + access: 'dbaf9757982a9e738f05d249b7b5b4a266b3a139049317c4909f2f263572c781', + refresh: '76ba4c5c75c96f6087f58a4de10be6c00b29ea1ddc3b2022ee2016d1363e3a7c', + }, + isAuthorized: true, + expiresAt: new Date('2100-01-01T00:00:00.000Z').getTime(), +}); diff --git a/projects/client/src/mocks/data/auth/EncryptedExpiredAuthMock.ts b/projects/client/src/mocks/data/auth/EncryptedExpiredAuthMock.ts deleted file mode 100644 index bb304a983..000000000 --- a/projects/client/src/mocks/data/auth/EncryptedExpiredAuthMock.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const EncryptedExpiredAuthMock = - 'xBfssx5ebZ2pArAkEkekMsbkpZLj+wTC+BNqt2zjWyJpjZ+fat2NDEOltEuB1ZDayMCUpK/lozb+MnraLYz/7oKX3YRdbThfJN0WjJ1oJL+06Nw9iRymTD0JqB4nHeWKHXjENM0X/W/UyU+ZoNoHEvPfgPSybipUlCoo/jqOO7HkrENyIFJAdhOwqABWjWK6UX10uIPxfCkxJ4waolkt7hu5Js5PvA1K0R/u17nOD1/15V9MtOJHW61rJLnDGwALOvGCXdWSGxQgDEGNkrFS2Q/TqldmDA=='; diff --git a/projects/client/src/mocks/data/auth/ExpiredAuthMock.ts b/projects/client/src/mocks/data/auth/ExpiredAuthMock.ts new file mode 100644 index 000000000..d5dbd3b1e --- /dev/null +++ b/projects/client/src/mocks/data/auth/ExpiredAuthMock.ts @@ -0,0 +1,8 @@ +export const ExpiredAuthMock = JSON.stringify({ + token: { + access: 'dbaf9757982a9e738f05d249b7b5b4a266b3a139049317c4909f2f263572c781', + refresh: '76ba4c5c75c96f6087f58a4de10be6c00b29ea1ddc3b2022ee2016d1363e3a7c', + }, + isAuthorized: true, + expiresAt: new Date('2001-01-01T00:00:00.000Z').getTime(), +}); diff --git a/projects/client/src/routes/+layout.server.ts b/projects/client/src/routes/+layout.server.ts index 377f606ef..5bddede74 100644 --- a/projects/client/src/routes/+layout.server.ts +++ b/projects/client/src/routes/+layout.server.ts @@ -1,10 +1,12 @@ +import { AUTH_COOKIE_NAME } from '$lib/features/auth/handle.ts'; +import { setToken } from '$lib/features/auth/token/index.ts'; import { isAuthorized } from '$lib/features/auth/utils/isAuthorized.ts'; import { buildOAuthUrl } from '$lib/utils/url/buildOAuthLink.ts'; import { isBotAgent } from '$lib/utils/url/isBotAgent.ts'; import type { LayoutServerLoad } from './$types.ts'; export const load: LayoutServerLoad = ( - { request, locals }, + { cookies, request, locals }, ) => { const requestUrl = new URL(request.url); @@ -13,9 +15,17 @@ export const load: LayoutServerLoad = ( auth: { url: buildOAuthUrl(TRAKT_CLIENT_ID, requestUrl.origin), isAuthorized: isAuthorized(locals), + token: null as string | Nil, }, isBot: isBotAgent(request.headers.get('user-agent')), }; + if (!locals.auth) { + setToken(null); + cookies.delete(AUTH_COOKIE_NAME, { path: '/' }); + return defaultResponse; + } + + defaultResponse.auth.token = locals.auth.token.access; return defaultResponse; }; diff --git a/projects/client/src/routes/+layout.ts b/projects/client/src/routes/+layout.ts index ae17c1a6f..35d993e21 100644 --- a/projects/client/src/routes/+layout.ts +++ b/projects/client/src/routes/+layout.ts @@ -2,6 +2,7 @@ import type { LayoutLoad } from './$types.ts'; import { browser } from '$app/environment'; import { currentUserSettingsQuery } from '$lib/features/auth/queries/currentUserSettingsQuery.ts'; +import { setToken } from '$lib/features/auth/token/index.ts'; import { QueryClient } from '@tanstack/svelte-query'; export const load: LayoutLoad = async ({ data, fetch }) => { @@ -16,6 +17,8 @@ export const load: LayoutLoad = async ({ data, fetch }) => { }, }); + setToken(data.auth.token); + if (data.auth.isAuthorized) { await queryClient.prefetchQuery(currentUserSettingsQuery({ fetch })); } diff --git a/projects/client/vite.config.ts b/projects/client/vite.config.ts index 57cf476db..a6cb02019 100644 --- a/projects/client/vite.config.ts +++ b/projects/client/vite.config.ts @@ -50,6 +50,13 @@ export default defineConfig(({ mode }) => ({ fs: { allow: [MONOREPO_ROOT], }, + proxy: { + '/api/trakt': { + target: TRAKT_TARGET_ENVIRONMENT, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/trakt/, ''), + }, + }, }, plugins: [