diff --git a/.env.example b/.env.example index 55ac2c0..f850476 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,6 @@ DATABASE_FILE=./database.db GITHUB_CLIENT_ID="" GITHUB_CLIENT_SECRET="" + +# Set this to 1 or true to enable query logging +LOGGER_ENABLED="" \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index eb29c9d..352a0ea 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/drizzle/0001_premium_khan.sql b/drizzle/0001_premium_khan.sql index 760ec75..9dea765 100644 --- a/drizzle/0001_premium_khan.sql +++ b/drizzle/0001_premium_khan.sql @@ -2,12 +2,12 @@ CREATE TABLE `oauth_connections` ( `type` text NOT NULL, `oauth_identifier` text NOT NULL, `user_id` text NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE TABLE `sessions` ( `id` text NOT NULL, `user_id` text NOT NULL, `expires_at` integer NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade ); diff --git a/package.json b/package.json index ccf9fab..7cb8d23 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "lucide-svelte": "^0.428.0", "mode-watcher": "^0.4.1", "svelte-radix": "^1.1.0", + "svelte-sonner": "^0.3.27", "tailwind-merge": "^2.5.2", "tailwind-variants": "^0.2.1", "trpc-svelte-query-adapter": "^2.3.14", diff --git a/src/lib/client/index.ts b/src/lib/client/index.ts index a038bbb..4ccd712 100644 --- a/src/lib/client/index.ts +++ b/src/lib/client/index.ts @@ -5,30 +5,21 @@ export * from "./errors"; import { InvalidKeyError } from "./errors"; import { getContext, setContext } from "svelte"; -const private_key_usages: KeyUsage[] = [ - "decrypt", - "unwrapKey", -]; +const private_key_usages: KeyUsage[] = ["decrypt", "unwrapKey"]; -const public_key_usages: KeyUsage[] = [ - "wrapKey", - "encrypt", -]; +const public_key_usages: KeyUsage[] = ["wrapKey", "encrypt"]; -const key_usages = [ - ...private_key_usages, - ...public_key_usages -]; +const key_usages = [...private_key_usages, ...public_key_usages]; const runtime_client_context_key = "$$_theAmalgamation"; export const setRuntimeClientContext = (client: TheAmalgamation) => { - setContext(runtime_client_context_key, client); -} + setContext(runtime_client_context_key, client); +}; export const getRuntimeClientContext = () => { - return getContext(runtime_client_context_key) as TheAmalgamation; -} + return getContext(runtime_client_context_key) as TheAmalgamation; +}; /** * Behold, the amalgamation, the client that does literally everything on the client. @@ -38,32 +29,38 @@ export class TheAmalgamation { constructor() {} - public initialise_from_localstorage = async () => { - this.key_pairs = []; - console.info("Initialising Runtime Client from Localhost"); - - for (let i = 0; i < window.localStorage.length; i++) { - const item_name = window.localStorage.key(i)!; - if (item_name.startsWith("cached_key-")) { - const cached_key_string = window.localStorage.getItem(item_name)!; - const parsed_cached_key = JSON.parse(cached_key_string); - - const kid = item_name.replace("cached_key-", ""); - - if ( - "publicKey" in parsed_cached_key && typeof parsed_cached_key.publicKey === "object" && - "privateKey" in parsed_cached_key && typeof parsed_cached_key.privateKey === "object" - ) { - const decoded_key = await this.decode_key_pair(parsed_cached_key.publicKey, parsed_cached_key.privateKey); - - this.add_key_pair_to_client(kid, decoded_key); - console.info(`Imported Cached key of KID ${kid}`); - } else { - console.warn(`Cached key of KID ${kid} is invalid`); - } - } - } - } + public initialise_from_localstorage = async () => { + this.key_pairs = []; + console.info("Initialising Runtime Client from Localhost"); + + for (let i = 0; i < window.localStorage.length; i++) { + const item_name = window.localStorage.key(i)!; + if (item_name.startsWith("cached_key-")) { + const cached_key_string = + window.localStorage.getItem(item_name)!; + const parsed_cached_key = JSON.parse(cached_key_string); + + const kid = item_name.replace("cached_key-", ""); + + if ( + "publicKey" in parsed_cached_key && + typeof parsed_cached_key.publicKey === "object" && + "privateKey" in parsed_cached_key && + typeof parsed_cached_key.privateKey === "object" + ) { + const decoded_key = await this.decode_key_pair( + parsed_cached_key.publicKey, + parsed_cached_key.privateKey, + ); + + this.add_key_pair_to_client(kid, decoded_key); + console.info(`Imported Cached key of KID ${kid}`); + } else { + console.warn(`Cached key of KID ${kid} is invalid`); + } + } + } + }; private get_key = (kid: string): JailBirdKey | undefined => { return this.key_pairs.find((val) => val.kid === kid); @@ -75,12 +72,15 @@ export class TheAmalgamation { key, }); - window.localStorage.setItem(`cached_key-${kid}`, JSON.stringify(await this.encode_key_pair(key))); - }; + window.localStorage.setItem( + `cached_key-${kid}`, + JSON.stringify(await this.encode_key_pair(key)), + ); + }; - private encode_key_pair = async (key: CryptoKeyPair) => { - return { - publicKey: await window.crypto.subtle.exportKey( + private encode_key_pair = async (key: CryptoKeyPair) => { + return { + publicKey: await window.crypto.subtle.exportKey( "jwk", key.publicKey, ), @@ -88,38 +88,40 @@ export class TheAmalgamation { "jwk", key.privateKey, ), - } - } - - private decode_key_pair = async (public_key: object, private_key: object): Promise => { - try { - return { - privateKey: await window.crypto.subtle.importKey( - "jwk", - private_key, - { name: "ECDSA", namedCurve: "P-521" }, - true, - private_key_usages, - ), - publicKey: await window.crypto.subtle.importKey( - "jwk", - public_key, - { name: "ECDSA", namedCurve: "P-521" }, - true, - public_key_usages, - ), - } - } catch (e) { - if (e instanceof TypeError || e instanceof SyntaxError) { - // Invalid Keys - throw new InvalidKeyError(); - } else { - // Invalid Use of key usages - throw new InvalidKeyError() - } - } - - } + }; + }; + + private decode_key_pair = async ( + public_key: object, + private_key: object, + ): Promise => { + try { + return { + privateKey: await window.crypto.subtle.importKey( + "jwk", + private_key, + { name: "ECDSA", namedCurve: "P-521" }, + true, + private_key_usages, + ), + publicKey: await window.crypto.subtle.importKey( + "jwk", + public_key, + { name: "ECDSA", namedCurve: "P-521" }, + true, + public_key_usages, + ), + }; + } catch (e) { + if (e instanceof TypeError || e instanceof SyntaxError) { + // Invalid Keys + throw new InvalidKeyError(); + } else { + // Invalid Use of key usages + throw new InvalidKeyError(); + } + } + }; public export_key_for_download = async ( kid: string, @@ -129,7 +131,7 @@ export class TheAmalgamation { return JSON.stringify({ kid, - ...this.encode_key_pair(key.key) + ...this.encode_key_pair(key.key), }); }; @@ -156,8 +158,11 @@ export class TheAmalgamation { const public_key = deserialised_key.publicKey as JsonWebKey; const private_key = deserialised_key.privateKey as JsonWebKey; - const decoded_key = await this.decode_key_pair(public_key, private_key); - if (!decoded_key) throw new InvalidKeyError(); + const decoded_key = await this.decode_key_pair( + public_key, + private_key, + ); + if (!decoded_key) throw new InvalidKeyError(); this.add_key_pair_to_client(kid, decoded_key); } else { diff --git a/src/lib/components/account/account-header-component.svelte b/src/lib/components/account/account-header-component.svelte index 11e633f..4e090ae 100644 --- a/src/lib/components/account/account-header-component.svelte +++ b/src/lib/components/account/account-header-component.svelte @@ -8,7 +8,7 @@ interface Props { user: UserType | null; - avatar_size: "default" | "smol"; + avatar_size?: "default" | "smol"; } const { user = null, avatar_size = "default" }: Props = $props(); diff --git a/src/lib/components/headers/index.ts b/src/lib/components/headers/index.ts index 105e4cf..ad4d39e 100644 --- a/src/lib/components/headers/index.ts +++ b/src/lib/components/headers/index.ts @@ -1,4 +1,4 @@ -import Icons from "../../icons"; +import * as Icons from "../../icons"; export const APPLICATION_NAME = "Jail Bird"; export const APPLICATION_ICON = Icons.Lock; diff --git a/src/lib/components/ui/sonner/index.ts b/src/lib/components/ui/sonner/index.ts new file mode 100644 index 0000000..1ad9f4a --- /dev/null +++ b/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./sonner.svelte"; diff --git a/src/lib/components/ui/sonner/sonner.svelte b/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 0000000..8373ff2 --- /dev/null +++ b/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/lib/icons/AnimatedLoading.svelte b/src/lib/icons/AnimatedLoading.svelte new file mode 100644 index 0000000..50f7708 --- /dev/null +++ b/src/lib/icons/AnimatedLoading.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/icons/index.ts b/src/lib/icons/index.ts index 9752d13..9e32030 100644 --- a/src/lib/icons/index.ts +++ b/src/lib/icons/index.ts @@ -1,8 +1,7 @@ import Lock from "./lock.svelte"; +import AnimatedLoading from "./AnimatedLoading.svelte"; -export default { - Lock, -}; +export { Lock, AnimatedLoading }; export interface IconProps { size?: string; diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index eb13d4b..754695b 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -15,7 +15,7 @@ sqlite.exec("PRAGMA journal_mode = WAL;"); sqlite.exec("PRAGMA foreign_keys = on;"); export const DB = drizzle(sqlite, { - logger: process.env.NODE_ENV == "development", + logger: process.env.NODE_ENV == "development" && ["1", "true"].includes(process.env.LOGGER_ENABLED ?? ""), }); /** diff --git a/src/lib/server/trpc/context.ts b/src/lib/server/trpc/context.ts index ecce27b..66c52c5 100644 --- a/src/lib/server/trpc/context.ts +++ b/src/lib/server/trpc/context.ts @@ -1,8 +1,12 @@ import type { RequestEvent } from "@sveltejs/kit"; export const createContext = async (event: RequestEvent) => { + const { user, session } = event.locals; + return { event, + user, + session, }; }; diff --git a/src/lib/server/trpc/middleware.ts b/src/lib/server/trpc/middleware.ts new file mode 100644 index 0000000..dc9b1a4 --- /dev/null +++ b/src/lib/server/trpc/middleware.ts @@ -0,0 +1,23 @@ +import { experimental_standaloneMiddleware, TRPCError } from "@trpc/server"; + +import type { Context } from "./context"; + +export const authMiddleware = experimental_standaloneMiddleware<{ + ctx: Context; +}>().create(async (opts) => { + const { user, session } = opts.ctx; + + if (!user || !session) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); + } else { + return opts.next({ + ctx: { + event: opts.ctx.event, + user, + session, + }, + }); + } +}); diff --git a/src/lib/server/trpc/router/auth.ts b/src/lib/server/trpc/router/auth.ts index e491103..29b909b 100644 --- a/src/lib/server/trpc/router/auth.ts +++ b/src/lib/server/trpc/router/auth.ts @@ -25,7 +25,7 @@ const checkAvailability = async (username: string) => { export const authRouter = trpcInstance.router({ // #region Check Username - check_username_availability: trpcInstance.procedure + checkUsernameAvailability: trpcInstance.procedure .input(USERNAME_SCHEMA) .query(async (opts) => { return { @@ -34,7 +34,7 @@ export const authRouter = trpcInstance.router({ }), // #endregion // #region Sign Up - sign_up: trpcInstance.procedure + signUp: trpcInstance.procedure .input( z.object({ username: USERNAME_SCHEMA, @@ -83,7 +83,7 @@ export const authRouter = trpcInstance.router({ }), // #endregion // #region Log In - log_in: trpcInstance.procedure + logIn: trpcInstance.procedure .input( z.object({ username: USERNAME_SCHEMA, diff --git a/src/lib/server/trpc/router/greeter.ts b/src/lib/server/trpc/router/greeter.ts index df29684..eeab8a0 100644 --- a/src/lib/server/trpc/router/greeter.ts +++ b/src/lib/server/trpc/router/greeter.ts @@ -13,7 +13,7 @@ export const greeterRouter = trpcInstance.router({ return `Hello ${opts.input.name}`; }), - odd_or_even: trpcInstance.procedure + oddOrEven: trpcInstance.procedure .input( z.object({ num: z.number(), diff --git a/src/lib/server/trpc/router/index.ts b/src/lib/server/trpc/router/index.ts index 2f19c58..50b2fc7 100644 --- a/src/lib/server/trpc/router/index.ts +++ b/src/lib/server/trpc/router/index.ts @@ -1,13 +1,22 @@ +import type { RequestEvent } from "@sveltejs/kit"; + import { trpcInstance } from "./init"; import { authRouter } from "./auth"; import { greeterRouter } from "./greeter"; +import { userRouter } from "./user"; +import { createContext } from "../context"; export const router = trpcInstance.router({ greeter: greeterRouter, auth: authRouter, + user: userRouter, }); -export const createCaller = trpcInstance.createCallerFactory(router); - export type Router = typeof router; + +const factory = trpcInstance.createCallerFactory(router); + +export const createCaller = async (event: RequestEvent) => { + return factory(await createContext(event)); +}; diff --git a/src/lib/server/trpc/router/user.ts b/src/lib/server/trpc/router/user.ts new file mode 100644 index 0000000..31c5972 --- /dev/null +++ b/src/lib/server/trpc/router/user.ts @@ -0,0 +1,15 @@ +// import z from "zod"; +import { trpcInstance } from "./init"; +import { authMiddleware } from "../middleware"; + +export const userRouter = trpcInstance.router({ + getUploadedFiles: trpcInstance.procedure + .use(authMiddleware) + .query(async () => { + await new Promise((res) => setTimeout(res, 1000)); + + return { + uploadedFiles: Math.floor(Math.random() * 100), + }; + }), +}); diff --git a/src/routes/(app)/app/+page.svelte b/src/routes/(app)/app/+page.svelte index 4b80ec6..1408797 100644 --- a/src/routes/(app)/app/+page.svelte +++ b/src/routes/(app)/app/+page.svelte @@ -1,6 +1,6 @@
-

Welcome to SvelteKit

+

Welcome to SvelteKit

-

- Visit kit.svelte.dev - to read the documentation -

+

+ Visit kit.svelte.dev + to read the documentation +

- - -
\ No newline at end of file + + + diff --git a/src/routes/(app)/app/account/+page.svelte b/src/routes/(app)/app/account/+page.svelte index 555baf3..1f654d1 100644 --- a/src/routes/(app)/app/account/+page.svelte +++ b/src/routes/(app)/app/account/+page.svelte @@ -1,28 +1,22 @@ -{#snippet account_stats()} -
- - - Uploaded Files - - {Math.floor(Math.random() * 100)} - - - -
-{/snippet} + const uploadedFileCount = data.uploadedFileCount(); +
diff --git a/src/routes/(app)/app/account/+page.ts b/src/routes/(app)/app/account/+page.ts new file mode 100644 index 0000000..42328df --- /dev/null +++ b/src/routes/(app)/app/account/+page.ts @@ -0,0 +1,14 @@ +import { trpc } from "$lib/trpc/client"; +import type { PageLoad } from "./$types"; + +export const load = (async (event) => { + const { queryClient } = await event.parent(); + const client = trpc(event, queryClient); + + return { + uploadedFileCount: + await client.user.getUploadedFiles.createServerQuery(undefined, { + ssr: false + }), + }; +}) satisfies PageLoad; diff --git a/src/routes/(auth)/auth/login/+page.svelte b/src/routes/(auth)/auth/login/+page.svelte index 85e8244..a17f5b0 100644 --- a/src/routes/(auth)/auth/login/+page.svelte +++ b/src/routes/(auth)/auth/login/+page.svelte @@ -33,7 +33,7 @@ } }); - const log_in_mutation = rpc.auth.log_in.createMutation({ + const log_in_mutation = rpc.auth.logIn.createMutation({ onSuccess: () => { goto("/app"); }, @@ -51,9 +51,9 @@ }; /** - * Sign Up with Github Button Handler + * Log In with Github Button Handler */ - const github_signup_onclick = (ev: MouseEvent) => { + const githubLoginOnclick = (ev: MouseEvent) => { ev.stopPropagation(); ev.preventDefault(); @@ -174,7 +174,7 @@