From 35f3e92b5ac1ed3ffc89a53446bf3b8f2bbd4df6 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 11 Oct 2026 18:26:47 +0900 Subject: [PATCH] =?UTF-8?q?SSR=EC=9D=84=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=ED=95=98=EC=97=AC=20SPA=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- package.json | 1 + src/lib/modules/file/thumbnail.ts | 6 +- src/lib/modules/file/upload.svelte.ts | 10 ++-- src/lib/modules/filesystem/category.ts | 2 +- src/lib/modules/filesystem/directory.ts | 2 +- src/lib/modules/filesystem/file.ts | 4 +- src/lib/server/middlewares/authenticate.ts | 6 ++ src/lib/services/auth.ts | 6 +- src/lib/services/category.ts | 4 +- src/lib/services/file.ts | 6 +- src/lib/services/key.ts | 10 ++-- .../auth/changePassword/service.ts | 2 +- .../(fullscreen)/auth/login/+page.server.ts | 12 ---- src/routes/(fullscreen)/auth/login/+page.ts | 6 ++ src/routes/(fullscreen)/auth/login/service.ts | 2 +- src/routes/(fullscreen)/file/[id]/service.ts | 6 +- .../(fullscreen)/gallery/+page.server.ts | 7 --- src/routes/(fullscreen)/gallery/+page.ts | 8 +++ src/routes/(fullscreen)/search/service.ts | 2 +- .../settings/thumbnail/+page.server.ts | 7 --- .../(fullscreen)/settings/thumbnail/+page.ts | 8 +++ .../(main)/category/[[id]]/service.svelte.ts | 4 +- .../(main)/directory/[[id]]/service.svelte.ts | 20 +++---- src/routes/(main)/favorites/+page.server.ts | 7 --- src/routes/(main)/favorites/+page.ts | 8 +++ src/routes/(main)/favorites/service.ts | 4 +- src/routes/(main)/menu/+page.server.ts | 7 --- src/routes/(main)/menu/+page.ts | 8 +++ src/routes/+layout.svelte | 20 ------- src/routes/+layout.ts | 19 ++++++ src/routes/+page.ts | 6 ++ src/routes/+server.ts | 6 -- src/trpc/client.ts | 60 ++++++++++++------- src/trpc/router.server.ts | 5 +- 34 files changed, 152 insertions(+), 139 deletions(-) delete mode 100644 src/routes/(fullscreen)/auth/login/+page.server.ts create mode 100644 src/routes/(fullscreen)/auth/login/+page.ts delete mode 100644 src/routes/(fullscreen)/gallery/+page.server.ts create mode 100644 src/routes/(fullscreen)/gallery/+page.ts delete mode 100644 src/routes/(fullscreen)/settings/thumbnail/+page.server.ts create mode 100644 src/routes/(fullscreen)/settings/thumbnail/+page.ts delete mode 100644 src/routes/(main)/favorites/+page.server.ts create mode 100644 src/routes/(main)/favorites/+page.ts delete mode 100644 src/routes/(main)/menu/+page.server.ts create mode 100644 src/routes/(main)/menu/+page.ts create mode 100644 src/routes/+layout.ts create mode 100644 src/routes/+page.ts delete mode 100644 src/routes/+server.ts diff --git a/package.json b/package.json index ac242e6..c3893cd 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", + "dev:remote": "__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=.ts.net vite dev --host 0.0.0.0", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", diff --git a/src/lib/modules/file/thumbnail.ts b/src/lib/modules/file/thumbnail.ts index b33a4af..2414f8d 100644 --- a/src/lib/modules/file/thumbnail.ts +++ b/src/lib/modules/file/thumbnail.ts @@ -1,6 +1,5 @@ import { LRUCache } from "lru-cache"; import { writable, type Writable } from "svelte/store"; -import { browser } from "$app/environment"; import { decryptChunk } from "$lib/modules/crypto"; import type { SummarizedFileInfo } from "$lib/modules/filesystem"; import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs"; @@ -27,10 +26,7 @@ const fetchFromServer = async (fileId: number, dataKey: CryptoKey) => { }; export const getFileThumbnail = (file: SummarizedFileInfo) => { - if ( - !browser || - !(file.contentType.startsWith("image/") || file.contentType.startsWith("video/")) - ) { + if (!(file.contentType.startsWith("image/") || file.contentType.startsWith("video/"))) { return undefined; } diff --git a/src/lib/modules/file/upload.svelte.ts b/src/lib/modules/file/upload.svelte.ts index 691f239..17b78d1 100644 --- a/src/lib/modules/file/upload.svelte.ts +++ b/src/lib/modules/file/upload.svelte.ts @@ -60,7 +60,7 @@ const requestDuplicateFileScan = limitFunction( state.status = "encryption-pending"; const fileSigned = encodeToBase64(await signMessageHmac(file, hmacSecret.secret)); - const files = await trpc().file.listByHash.query({ + const files = await trpc.file.listByHash.query({ hskVersion: hmacSecret.version, contentHmac: fileSigned, }); @@ -134,7 +134,7 @@ const requestFileMetadataEncryption = limitFunction( generateThumbnail(file).then((blob) => blob?.arrayBuffer()), ]); - const { uploadId } = await trpc().upload.startFileUpload.mutate({ + const { uploadId } = await trpc.upload.startFileUpload.mutate({ chunks: Math.ceil(file.size / CHUNK_SIZE), parent: fileMetadata.parentId, mekVersion: masterKey.version, @@ -175,7 +175,7 @@ const requestFileUpload = limitFunction( }, }); - const { file: fileId } = await trpc().upload.completeFileUpload.mutate({ + const { file: fileId } = await trpc.upload.completeFileUpload.mutate({ uploadId, contentHmac: fileSigned, encContentHash, @@ -183,14 +183,14 @@ const requestFileUpload = limitFunction( if (thumbnailBuffer) { try { - const { uploadId } = await trpc().upload.startFileThumbnailUpload.mutate({ + const { uploadId } = await trpc.upload.startFileThumbnailUpload.mutate({ file: fileId, dekVersion: dataKeyVersion, }); await uploadBlob(uploadId, new Blob([thumbnailBuffer]), dataKey); - await trpc().upload.completeFileThumbnailUpload.mutate({ uploadId }); + await trpc.upload.completeFileThumbnailUpload.mutate({ uploadId }); } catch (e) { console.error(e); } diff --git a/src/lib/modules/filesystem/category.ts b/src/lib/modules/filesystem/category.ts index 16285ff..9224628 100644 --- a/src/lib/modules/filesystem/category.ts +++ b/src/lib/modules/filesystem/category.ts @@ -50,7 +50,7 @@ const cache = new FilesystemCache({ async fetchFromServer(id, cachedInfo, masterKey) { try { - const category = await trpc().category.get.query({ id, recurse: true }); + const category = await trpc.category.get.query({ id, recurse: true }); const [subCategories, files, metadata] = await Promise.all([ Promise.all( category.subCategories.map(async (category) => ({ diff --git a/src/lib/modules/filesystem/directory.ts b/src/lib/modules/filesystem/directory.ts index e42b1aa..95a3953 100644 --- a/src/lib/modules/filesystem/directory.ts +++ b/src/lib/modules/filesystem/directory.ts @@ -33,7 +33,7 @@ const cache = new FilesystemCache({ async fetchFromServer(id, _cachedInfo, masterKey) { try { - const directory = await trpc().directory.get.query({ id }); + const directory = await trpc.directory.get.query({ id }); const [subDirectories, files, metadata] = await Promise.all([ Promise.all( directory.subDirectories.map(async (directory) => ({ diff --git a/src/lib/modules/filesystem/file.ts b/src/lib/modules/filesystem/file.ts index fdd2c8a..a9e7e4c 100644 --- a/src/lib/modules/filesystem/file.ts +++ b/src/lib/modules/filesystem/file.ts @@ -34,7 +34,7 @@ const cache = new FilesystemCache({ async fetchFromServer(id, _cachedInfo, masterKey) { try { - const file = await trpc().file.get.query({ id }); + const file = await trpc.file.get.query({ id }); const [categories, metadata] = await Promise.all([ Promise.all( file.categories.map(async (category) => ({ @@ -101,7 +101,7 @@ const cache = new FilesystemCache({ async bulkFetchFromServer(ids, masterKey) { const idsArray = [...ids.keys()]; - const filesRaw = await trpc().file.bulkGet.query({ ids: idsArray }); + const filesRaw = await trpc.file.bulkGet.query({ ids: idsArray }); const files = await Promise.all( filesRaw.map(async ({ id, categories: categoriesRaw, ...metadataRaw }) => { const [categories, metadata] = await Promise.all([ diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts index ecd1adf..32c283d 100644 --- a/src/lib/server/middlewares/authenticate.ts +++ b/src/lib/server/middlewares/authenticate.ts @@ -25,6 +25,12 @@ export const authenticateMiddleware: Handle = async ({ event, resolve }) => { throw e; } + if (event.url.pathname === "/auth/login") { + const redirectPath = event.url.searchParams.get("redirect"); + // Allow same-origin paths only + redirect(302, redirectPath?.match(/^\/(?![/\\])/) ? redirectPath : "/home"); + } + return await resolve(event); }; diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index 95f153e..d01055f 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -10,7 +10,7 @@ export const requestSessionUpgrade = async ( ) => { let id, challenge; try { - ({ id, challenge } = await trpc().auth.upgrade.mutate({ + ({ id, challenge } = await trpc.auth.upgrade.mutate({ encPubKey: encryptKeyBase64, sigPubKey: verifyKeyBase64, })); @@ -24,7 +24,7 @@ export const requestSessionUpgrade = async ( const answerSig = await signMessageRSA(answer, signKey); try { - await trpc().auth.verifyUpgrade.mutate({ + await trpc.auth.verifyUpgrade.mutate({ id, answerSig: encodeToBase64(answerSig), force, @@ -41,7 +41,7 @@ export const requestSessionUpgrade = async ( export const requestLogout = async () => { try { - await trpc().auth.logout.mutate(); + await trpc.auth.logout.mutate(); return true; } catch { // TODO: Error Handling diff --git a/src/lib/services/category.ts b/src/lib/services/category.ts index c53a6f2..afb3449 100644 --- a/src/lib/services/category.ts +++ b/src/lib/services/category.ts @@ -11,7 +11,7 @@ export const requestCategoryCreation = async ( const nameEncrypted = await encryptString(name, dataKey); try { - await trpc().category.create.mutate({ + await trpc.category.create.mutate({ parent: parentId, mekVersion: masterKey.version, dek: await wrapDataKey(dataKey, masterKey.key), @@ -28,7 +28,7 @@ export const requestCategoryCreation = async ( export const requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { try { - await trpc().category.removeFile.mutate({ id: categoryId, file: fileId }); + await trpc.category.removeFile.mutate({ id: categoryId, file: fileId }); return true; } catch { // TODO: Error Handling diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts index b8db243..ee6690b 100644 --- a/src/lib/services/file.ts +++ b/src/lib/services/file.ts @@ -29,14 +29,14 @@ export const requestFileThumbnailUpload = async ( dataKeyVersion: Date, ) => { try { - const { uploadId } = await trpc().upload.startFileThumbnailUpload.mutate({ + const { uploadId } = await trpc.upload.startFileThumbnailUpload.mutate({ file: fileId, dekVersion: dataKeyVersion, }); await uploadBlob(uploadId, thumbnail, dataKey); - await trpc().upload.completeFileThumbnailUpload.mutate({ uploadId }); + await trpc.upload.completeFileThumbnailUpload.mutate({ uploadId }); return true; } catch { // TODO: Error Handling @@ -47,7 +47,7 @@ export const requestFileThumbnailUpload = async ( export const requestDeletedFilesCleanup = async () => { let liveFiles; try { - liveFiles = await trpc().file.list.query(); + liveFiles = await trpc.file.list.query(); } catch { // TODO: Error Handling return; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 4e97546..b458554 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -19,13 +19,13 @@ export const requestClientRegistration = async ( signKey: CryptoKey, ) => { try { - const { id, challenge } = await trpc().client.register.mutate({ + const { id, challenge } = await trpc.client.register.mutate({ encPubKey: encryptKeyBase64, sigPubKey: verifyKeyBase64, }); const answer = await decryptChallenge(challenge, decryptKey); const answerSig = await signMessageRSA(answer, signKey); - await trpc().client.verify.mutate({ + await trpc.client.verify.mutate({ id, answerSig: encodeToBase64(answerSig), }); @@ -68,7 +68,7 @@ export const requestClientRegistrationAndSessionUpgrade = async ( export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verifyKey: CryptoKey) => { let masterKeysWrapped; try { - masterKeysWrapped = await trpc().mek.list.query(); + masterKeysWrapped = await trpc.mek.list.query(); } catch { // TODO: Error Handling return false; @@ -106,7 +106,7 @@ export const requestInitialMasterKeyAndHmacSecretRegistration = async ( signKey: CryptoKey, ) => { try { - await trpc().mek.registerInitial.mutate({ + await trpc.mek.registerInitial.mutate({ mek: masterKeyWrapped, mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), }); @@ -119,7 +119,7 @@ export const requestInitialMasterKeyAndHmacSecretRegistration = async ( } try { - await trpc().hsk.registerInitial.mutate({ + await trpc.hsk.registerInitial.mutate({ mekVersion: 1, hsk: hmacSecretWrapped, }); diff --git a/src/routes/(fullscreen)/auth/changePassword/service.ts b/src/routes/(fullscreen)/auth/changePassword/service.ts index 4673611..29336c6 100644 --- a/src/routes/(fullscreen)/auth/changePassword/service.ts +++ b/src/routes/(fullscreen)/auth/changePassword/service.ts @@ -2,7 +2,7 @@ import { trpc } from "$trpc/client"; export const requestPasswordChange = async (oldPassword: string, newPassword: string) => { try { - await trpc().auth.changePassword.mutate({ oldPassword, newPassword }); + await trpc.auth.changePassword.mutate({ oldPassword, newPassword }); return true; } catch { // TODO: Error Handling diff --git a/src/routes/(fullscreen)/auth/login/+page.server.ts b/src/routes/(fullscreen)/auth/login/+page.server.ts deleted file mode 100644 index 935dd9d..0000000 --- a/src/routes/(fullscreen)/auth/login/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { redirect } from "@sveltejs/kit"; -import type { PageServerLoad } from "./$types"; - -export const load: PageServerLoad = async ({ locals, url }) => { - const redirectPath = url.searchParams.get("redirect") || "/home"; - - if (locals.session) { - redirect(302, redirectPath); - } - - return { redirectPath }; -}; diff --git a/src/routes/(fullscreen)/auth/login/+page.ts b/src/routes/(fullscreen)/auth/login/+page.ts new file mode 100644 index 0000000..455f322 --- /dev/null +++ b/src/routes/(fullscreen)/auth/login/+page.ts @@ -0,0 +1,6 @@ +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ url }) => { + const redirectPath = url.searchParams.get("redirect") || "/home"; + return { redirectPath }; +}; diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 5c8219a..6449ce4 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -9,7 +9,7 @@ export { export const requestLogin = async (email: string, password: string) => { try { - await trpc().auth.login.mutate({ email, password }); + await trpc.auth.login.mutate({ email, password }); return true; } catch { // TODO: Error Handling diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index 41eb630..3309799 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -41,7 +41,7 @@ export const requestThumbnailUpload = async ( export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => { try { - await trpc().category.addFile.mutate({ id: categoryId, file: fileId }); + await trpc.category.addFile.mutate({ id: categoryId, file: fileId }); return true; } catch { // TODO: Error Handling @@ -52,9 +52,9 @@ export const requestFileAdditionToCategory = async (fileId: number, categoryId: export const requestFavoriteToggle = async (fileId: number, isFavorite: boolean) => { try { if (isFavorite) { - await trpc().favorites.removeFile.mutate({ id: fileId }); + await trpc.favorites.removeFile.mutate({ id: fileId }); } else { - await trpc().favorites.addFile.mutate({ id: fileId }); + await trpc.favorites.addFile.mutate({ id: fileId }); } return true; } catch { diff --git a/src/routes/(fullscreen)/gallery/+page.server.ts b/src/routes/(fullscreen)/gallery/+page.server.ts deleted file mode 100644 index 84d5819..0000000 --- a/src/routes/(fullscreen)/gallery/+page.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCaller } from "$trpc/router.server"; -import type { PageServerLoad } from "./$types"; - -export const load: PageServerLoad = async (event) => { - const files = await createCaller(event).file.list(); - return { files }; -}; diff --git a/src/routes/(fullscreen)/gallery/+page.ts b/src/routes/(fullscreen)/gallery/+page.ts new file mode 100644 index 0000000..e3b27be --- /dev/null +++ b/src/routes/(fullscreen)/gallery/+page.ts @@ -0,0 +1,8 @@ +import { trpc } from "$trpc/client"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ parent }) => { + await parent(); // Wait for the key guard in the root layout + const files = await trpc.file.list.query(); + return { files }; +}; diff --git a/src/routes/(fullscreen)/search/service.ts b/src/routes/(fullscreen)/search/service.ts index 9359001..52b0abb 100644 --- a/src/routes/(fullscreen)/search/service.ts +++ b/src/routes/(fullscreen)/search/service.ts @@ -20,7 +20,7 @@ export interface SearchResult { } export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey) => { - const { directories: directoriesRaw, files: filesRaw } = await trpc().search.search.query({ + const { directories: directoriesRaw, files: filesRaw } = await trpc.search.search.query({ ancestor: filter.ancestorId, inFavorites: filter.inFavorites, includeCategories: filter.categories diff --git a/src/routes/(fullscreen)/settings/thumbnail/+page.server.ts b/src/routes/(fullscreen)/settings/thumbnail/+page.server.ts deleted file mode 100644 index dc32e56..0000000 --- a/src/routes/(fullscreen)/settings/thumbnail/+page.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCaller } from "$trpc/router.server"; -import type { PageServerLoad } from "./$types"; - -export const load: PageServerLoad = async (event) => { - const files = await createCaller(event).file.listWithoutThumbnail(); - return { files }; -}; diff --git a/src/routes/(fullscreen)/settings/thumbnail/+page.ts b/src/routes/(fullscreen)/settings/thumbnail/+page.ts new file mode 100644 index 0000000..bfa954f --- /dev/null +++ b/src/routes/(fullscreen)/settings/thumbnail/+page.ts @@ -0,0 +1,8 @@ +import { trpc } from "$trpc/client"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ parent }) => { + await parent(); // Wait for the key guard in the root layout + const files = await trpc.file.listWithoutThumbnail.query(); + return { files }; +}; diff --git a/src/routes/(main)/category/[[id]]/service.svelte.ts b/src/routes/(main)/category/[[id]]/service.svelte.ts index b44d50d..676bb3a 100644 --- a/src/routes/(main)/category/[[id]]/service.svelte.ts +++ b/src/routes/(main)/category/[[id]]/service.svelte.ts @@ -30,7 +30,7 @@ export const requestCategoryRename = async (category: SelectedCategory, newName: const newNameEncrypted = await encryptString(newName, category.dataKey.key); try { - await trpc().category.rename.mutate({ + await trpc.category.rename.mutate({ id: category.id, dekVersion: category.dataKey.version, name: newNameEncrypted.ciphertext, @@ -45,7 +45,7 @@ export const requestCategoryRename = async (category: SelectedCategory, newName: export const requestCategoryDeletion = async (category: SelectedCategory) => { try { - await trpc().category.delete.mutate({ id: category.id }); + await trpc.category.delete.mutate({ id: category.id }); return true; } catch { // TODO: Error Handling diff --git a/src/routes/(main)/directory/[[id]]/service.svelte.ts b/src/routes/(main)/directory/[[id]]/service.svelte.ts index 82a7456..17f6c16 100644 --- a/src/routes/(main)/directory/[[id]]/service.svelte.ts +++ b/src/routes/(main)/directory/[[id]]/service.svelte.ts @@ -36,7 +36,7 @@ export const requestHmacSecretDownload = async (masterKey: CryptoKey) => { let hmacSecretsWrapped; try { - hmacSecretsWrapped = await trpc().hsk.list.query(); + hmacSecretsWrapped = await trpc.hsk.list.query(); } catch { // TODO: Error Handling return false; @@ -64,7 +64,7 @@ export const requestDirectoryCreation = async ( const nameEncrypted = await encryptString(name, dataKey); try { - await trpc().directory.create.mutate({ + await trpc.directory.create.mutate({ parent: parentId, mekVersion: masterKey.version, dek: await wrapDataKey(dataKey, masterKey.key), @@ -109,14 +109,14 @@ export const requestEntryRename = async (entry: SelectedEntry, newName: string) try { if (entry.type === "directory") { - await trpc().directory.rename.mutate({ + await trpc.directory.rename.mutate({ id: entry.id, dekVersion: entry.dataKey.version, name: newNameEncrypted.ciphertext, nameIv: newNameEncrypted.iv, }); } else { - await trpc().file.rename.mutate({ + await trpc.file.rename.mutate({ id: entry.id, dekVersion: entry.dataKey.version, name: newNameEncrypted.ciphertext, @@ -133,7 +133,7 @@ export const requestEntryRename = async (entry: SelectedEntry, newName: string) export const requestEntryDeletion = async (entry: SelectedEntry) => { try { if (entry.type === "directory") { - const { deletedFiles } = await trpc().directory.delete.mutate({ id: entry.id }); + const { deletedFiles } = await trpc.directory.delete.mutate({ id: entry.id }); await Promise.all( deletedFiles.flatMap((fileId) => [ deleteFileCache(fileId), @@ -141,7 +141,7 @@ export const requestEntryDeletion = async (entry: SelectedEntry) => { ]), ); } else { - await trpc().file.delete.mutate({ id: entry.id }); + await trpc.file.delete.mutate({ id: entry.id }); await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]); } return true; @@ -155,15 +155,15 @@ export const requestFavoriteToggle = async (entry: SelectedEntry) => { try { if (entry.type === "directory") { if (entry.isFavorite) { - await trpc().favorites.removeDirectory.mutate({ id: entry.id }); + await trpc.favorites.removeDirectory.mutate({ id: entry.id }); } else { - await trpc().favorites.addDirectory.mutate({ id: entry.id }); + await trpc.favorites.addDirectory.mutate({ id: entry.id }); } } else { if (entry.isFavorite) { - await trpc().favorites.removeFile.mutate({ id: entry.id }); + await trpc.favorites.removeFile.mutate({ id: entry.id }); } else { - await trpc().favorites.addFile.mutate({ id: entry.id }); + await trpc.favorites.addFile.mutate({ id: entry.id }); } } return true; diff --git a/src/routes/(main)/favorites/+page.server.ts b/src/routes/(main)/favorites/+page.server.ts deleted file mode 100644 index 484016d..0000000 --- a/src/routes/(main)/favorites/+page.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCaller } from "$trpc/router.server"; -import type { PageServerLoad } from "./$types"; - -export const load: PageServerLoad = async (event) => { - const favorites = await createCaller(event).favorites.get(); - return { favorites }; -}; diff --git a/src/routes/(main)/favorites/+page.ts b/src/routes/(main)/favorites/+page.ts new file mode 100644 index 0000000..5923f3a --- /dev/null +++ b/src/routes/(main)/favorites/+page.ts @@ -0,0 +1,8 @@ +import { trpc } from "$trpc/client"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ parent }) => { + await parent(); // Wait for the key guard in the root layout + const favorites = await trpc.favorites.get.query(); + return { favorites }; +}; diff --git a/src/routes/(main)/favorites/service.ts b/src/routes/(main)/favorites/service.ts index cb44834..d936b13 100644 --- a/src/routes/(main)/favorites/service.ts +++ b/src/routes/(main)/favorites/service.ts @@ -52,9 +52,9 @@ export const requestFavoriteEntries = async ( export const requestRemoveFavorite = async (type: "file" | "directory", id: number) => { try { if (type === "directory") { - await trpc().favorites.removeDirectory.mutate({ id }); + await trpc.favorites.removeDirectory.mutate({ id }); } else { - await trpc().favorites.removeFile.mutate({ id }); + await trpc.favorites.removeFile.mutate({ id }); } return true; } catch { diff --git a/src/routes/(main)/menu/+page.server.ts b/src/routes/(main)/menu/+page.server.ts deleted file mode 100644 index d27816b..0000000 --- a/src/routes/(main)/menu/+page.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCaller } from "$trpc/router.server"; -import type { PageServerLoad } from "./$types"; - -export const load: PageServerLoad = async (event) => { - const { nickname } = await createCaller(event).user.get(); - return { nickname }; -}; diff --git a/src/routes/(main)/menu/+page.ts b/src/routes/(main)/menu/+page.ts new file mode 100644 index 0000000..445a91d --- /dev/null +++ b/src/routes/(main)/menu/+page.ts @@ -0,0 +1,8 @@ +import { trpc } from "$trpc/client"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ parent }) => { + await parent(); // Wait for the key guard in the root layout + const { nickname } = await trpc.user.get.query(); + return { nickname }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9aadffd..e96b84a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,8 +1,5 @@ diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..2863db7 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,19 @@ +import { redirect } from "@sveltejs/kit"; +import { get } from "svelte/store"; +import { clientKeyStore, masterKeyStore } from "$lib/stores"; +import type { LayoutLoad } from "./$types"; + +export const ssr = false; + +const KEYLESS_PATHS = ["/auth/login", "/key", "/client/pending"]; + +export const load: LayoutLoad = ({ url }) => { + if (KEYLESS_PATHS.some((path) => url.pathname.startsWith(path))) return; + + const redirectQuery = `?redirect=${encodeURIComponent(url.pathname + url.search)}`; + if (!get(clientKeyStore)) { + redirect(307, "/key/generate" + redirectQuery); + } else if (!get(masterKeyStore)) { + redirect(307, "/client/pending" + redirectQuery); + } +}; diff --git a/src/routes/+page.ts b/src/routes/+page.ts new file mode 100644 index 0000000..0d28d92 --- /dev/null +++ b/src/routes/+page.ts @@ -0,0 +1,6 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = () => { + redirect(302, "/home"); +}; diff --git a/src/routes/+server.ts b/src/routes/+server.ts deleted file mode 100644 index e02bb98..0000000 --- a/src/routes/+server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { redirect } from "@sveltejs/kit"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = () => { - redirect(302, "/home"); -}; diff --git a/src/trpc/client.ts b/src/trpc/client.ts index f62916a..85a472e 100644 --- a/src/trpc/client.ts +++ b/src/trpc/client.ts @@ -1,31 +1,47 @@ -import { createTRPCClient, httpBatchLink, TRPCClientError } from "@trpc/client"; +import { createTRPCClient, httpBatchLink, TRPCClientError, type TRPCLink } from "@trpc/client"; +import { observable } from "@trpc/server/observable"; import superjson from "superjson"; -import { browser } from "$app/environment"; +import { goto } from "$app/navigation"; +import { navigating } from "$app/state"; import type { AppRouter } from "./router.server"; -const createClient = (fetch: typeof globalThis.fetch) => - createTRPCClient({ - links: [ - httpBatchLink({ - url: "/api/trpc", - maxURLLength: 4096, - methodOverride: "POST", - transformer: superjson, - fetch, +// Procedures that report invalid credentials as UNAUTHORIZED, not an expired session +const CREDENTIAL_PROCEDURES = new Set(["auth.login", "auth.upgrade"]); + +const sessionExpiryLink: TRPCLink = () => { + return ({ op, next }) => + observable((observer) => + next(op).subscribe({ + next: (value) => observer.next(value), + error: (err) => { + // Prefer the destination when the request failed during a navigation's load + const url = navigating.to?.url ?? location; + if ( + err.data?.code === "UNAUTHORIZED" && + !CREDENTIAL_PROCEDURES.has(op.path) && + url.pathname !== "/auth/login" + ) { + void goto(`/auth/login?redirect=${encodeURIComponent(url.pathname + url.search)}`); + } + observer.error(err); + }, + complete: () => observer.complete(), }), - ], - }); - -let browserClient: ReturnType; - -export const trpc = (fetch = globalThis.fetch) => { - const client = browserClient ?? createClient(fetch); - if (browser) { - browserClient ??= client; - } - return client; + ); }; +export const trpc = createTRPCClient({ + links: [ + sessionExpiryLink, + httpBatchLink({ + url: "/api/trpc", + maxURLLength: 4096, + methodOverride: "POST", + transformer: superjson, + }), + ], +}); + export const isTRPCClientError = (e: unknown): e is TRPCClientError => { return e instanceof TRPCClientError; }; diff --git a/src/trpc/router.server.ts b/src/trpc/router.server.ts index 09a3918..8797c79 100644 --- a/src/trpc/router.server.ts +++ b/src/trpc/router.server.ts @@ -1,6 +1,5 @@ -import type { RequestEvent } from "@sveltejs/kit"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; -import { createContext, router } from "./init.server"; +import { router } from "./init.server"; import { authRouter, categoryRouter, @@ -29,8 +28,6 @@ export const appRouter = router({ user: userRouter, }); -export const createCaller = (event: RequestEvent) => appRouter.createCaller(createContext(event)); - export type AppRouter = typeof appRouter; export type RouterInputs = inferRouterInputs; export type RouterOutputs = inferRouterOutputs;