From 681aac333fd8e2159953fff766cc5057000e2b0c Mon Sep 17 00:00:00 2001 From: elianiva Date: Fri, 19 Jan 2024 03:41:12 +0700 Subject: [PATCH] feat: authentication, form validation --- package.json | 4 + pnpm-lock.yaml | 61 ++++++++++ src/app.d.ts | 13 ++ src/app.html | 2 +- src/app.postcss | 4 + src/hooks.server.ts | 115 +++++++++++++++++- .../ui/alert/alert-description.svelte | 13 ++ .../components/ui/alert/alert-title.svelte | 21 ++++ src/lib/components/ui/alert/alert.svelte | 17 +++ src/lib/components/ui/alert/index.ts | 33 +++++ src/lib/components/ui/label/index.ts | 7 ++ src/lib/components/ui/label/label.svelte | 21 ++++ src/lib/schema/auth.ts | 7 ++ src/lib/utils.ts | 13 ++ src/routes/(auth)/+layout.svelte | 7 ++ src/routes/(auth)/sign-in/+page.svelte | 93 ++++++++++++++ src/routes/+layout.svelte | 3 + src/routes/app/+layout.server.ts | 7 ++ src/routes/app/+layout.svelte | 5 + src/routes/app/+page.svelte | 12 ++ 20 files changed, 452 insertions(+), 6 deletions(-) create mode 100644 src/lib/components/ui/alert/alert-description.svelte create mode 100644 src/lib/components/ui/alert/alert-title.svelte create mode 100644 src/lib/components/ui/alert/alert.svelte create mode 100644 src/lib/components/ui/alert/index.ts create mode 100644 src/lib/components/ui/label/index.ts create mode 100644 src/lib/components/ui/label/label.svelte create mode 100644 src/lib/schema/auth.ts create mode 100644 src/routes/(auth)/+layout.svelte create mode 100644 src/routes/(auth)/sign-in/+page.svelte create mode 100644 src/routes/app/+layout.server.ts create mode 100644 src/routes/app/+layout.svelte create mode 100644 src/routes/app/+page.svelte diff --git a/package.json b/package.json index 27e622f..3bfc330 100644 --- a/package.json +++ b/package.json @@ -28,13 +28,17 @@ }, "type": "module", "dependencies": { + "@auth/core": "^0.21.0", "@auth/sveltekit": "^0.7.0", + "@felte/validator-zod": "^1.0.17", "@types/node": "^20.11.5", "argon2": "^0.31.2", "bits-ui": "^0.5.7", "clsx": "^2.0.0", + "felte": "^1.2.12", "lucide-svelte": "^0.285.0", "mongodb": "^6.3.0", + "svelte-french-toast": "^1.2.0", "tailwind-merge": "^1.14.0", "tailwind-variants": "^0.1.14", "vitest": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a81b8ba..b306b2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: + '@auth/core': + specifier: ^0.21.0 + version: 0.21.0 '@auth/sveltekit': specifier: ^0.7.0 version: 0.7.0(@sveltejs/kit@1.25.2)(svelte@4.2.1) + '@felte/validator-zod': + specifier: ^1.0.17 + version: 1.0.17(zod@3.22.4) '@types/node': specifier: ^20.11.5 version: 20.11.5 @@ -20,12 +26,18 @@ dependencies: clsx: specifier: ^2.0.0 version: 2.0.0 + felte: + specifier: ^1.2.12 + version: 1.2.12(svelte@4.2.1) lucide-svelte: specifier: ^0.285.0 version: 0.285.0(svelte@4.2.1) mongodb: specifier: ^6.3.0 version: 6.3.0 + svelte-french-toast: + specifier: ^1.2.0 + version: 1.2.0(svelte@4.2.1) tailwind-merge: specifier: ^1.14.0 version: 1.14.0 @@ -507,6 +519,28 @@ packages: resolution: {integrity: sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==} engines: {node: '>=14'} + /@felte/common@1.1.8: + resolution: {integrity: sha512-VbEOfNLWfDx0SpCfeE+fNWDpvcntND4MFs7Lxd18RIjrZYH82D0wWe9th2oVF9QT5XzgBEdMF5NGIttcwU4sjg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /@felte/core@1.4.1: + resolution: {integrity: sha512-dUzfzug5cK93kBjG0u9F3zDM781qCJP4QwYPOpJsXbydwVseDM5BXpDqvZUFhqJsd0x1GKftkX69+iWafyASjw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + '@felte/common': 1.1.8 + dev: false + + /@felte/validator-zod@1.0.17(zod@3.22.4): + resolution: {integrity: sha512-rOX1chLfTcixKMPEdrMSi8zsCM685Dsoy1a5qN1G6Fyh7HYK1vSmI6SfJ7m92DOt6kV+vAP4m5rk94Y8UFIeVw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + zod: ^3.2.0 + dependencies: + '@felte/common': 1.1.8 + zod: 3.22.4 + dev: false + /@floating-ui/core@1.5.0: resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} dependencies: @@ -1314,6 +1348,16 @@ packages: dependencies: reusify: 1.0.4 + /felte@1.2.12(svelte@4.2.1): + resolution: {integrity: sha512-llg9ywCgIso48NnO6jZUy8D9vWuKE90dfIFUZPLyNKqbH1WJ0brNU6C4DkdfXtpRKlzWWHvaQkgMIezKoWlzvA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + svelte: ^3.31.0 || ^4.0.0 + dependencies: + '@felte/core': 1.4.1 + svelte: 4.2.1 + dev: false + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -2277,6 +2321,15 @@ packages: - sugarss dev: true + /svelte-french-toast@1.2.0(svelte@4.2.1): + resolution: {integrity: sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==} + peerDependencies: + svelte: ^3.57.0 || ^4.0.0 + dependencies: + svelte: 4.2.1 + svelte-writable-derived: 3.1.0(svelte@4.2.1) + dev: false + /svelte-hmr@0.15.3(svelte@4.2.1): resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} engines: {node: ^12.20 || ^14.13.1 || >= 16} @@ -2334,6 +2387,14 @@ packages: typescript: 5.2.2 dev: true + /svelte-writable-derived@3.1.0(svelte@4.2.1): + resolution: {integrity: sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==} + peerDependencies: + svelte: ^3.2.1 || ^4.0.0-next.1 + dependencies: + svelte: 4.2.1 + dev: false + /svelte@4.2.1: resolution: {integrity: sha512-LpLqY2Jr7cRxkrTc796/AaaoMLF/1ax7cto8Ot76wrvKQhrPmZ0JgajiWPmg9mTSDqO16SSLiD17r9MsvAPTmw==} engines: {node: '>=16'} diff --git a/src/app.d.ts b/src/app.d.ts index f59b884..8724db3 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,3 +1,15 @@ +import "@auth/sveltekit"; + +declare module "@auth/sveltekit" { + interface User { + id: string; + fullname: string; + email: string; + username: string; + role: string; + } +} + // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { @@ -9,4 +21,5 @@ declare global { } } + export {}; diff --git a/src/app.html b/src/app.html index 77a5ff5..fda3997 100644 --- a/src/app.html +++ b/src/app.html @@ -7,6 +7,6 @@ %sveltekit.head% -
%sveltekit.body%
+
%sveltekit.body% diff --git a/src/app.postcss b/src/app.postcss index 76f978c..b025f0a 100644 --- a/src/app.postcss +++ b/src/app.postcss @@ -75,4 +75,8 @@ body { @apply bg-background text-foreground; } + html, + body { + @apply h-full; + } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 9923eee..68033d1 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,10 +1,115 @@ +import * as argon2 from "argon2"; import { SvelteKitAuth } from "@auth/sveltekit"; import CredentialsProvider from "@auth/sveltekit/providers/credentials"; +import { getUserByEmailOrUsername } from "$lib/server/repositories/user-repository"; +import { type Handle, redirect } from "@sveltejs/kit"; +import { sequence } from "@sveltejs/kit/hooks"; +import { wrapResult } from "$lib/utils"; +import { authSchema } from "$lib/schema/auth"; +import { dev } from "$app/environment"; -export const handle = SvelteKitAuth({ - providers: [CredentialsProvider({ - authorize: async (credentials) => { +const authorization: Handle = async ({ event, resolve }) => { + const pathname = event.url.pathname; + // redirect for index path + if (pathname === "/") { + throw redirect(303, "/app"); + } + const session = await event.locals.getSession(); + + if (!session) { + if (pathname.startsWith("/app")) { + throw redirect(303, "/sign-in"); + } + } + + if (session) { + if (pathname.startsWith("/sign-in")) { + throw redirect(303, "/app"); } - })], -}); \ No newline at end of file + } + + return resolve(event); +}; + +export const handle = sequence( + SvelteKitAuth({ + providers: [ + CredentialsProvider({ + id: "credentials", + authorize: async (credentials) => { + const validatedCredentials = authSchema.safeParse(credentials); + if (!validatedCredentials.success) { + return null; + } + + // TODO(elianiva): only for temporary development purpose, please remove later + if (dev) { + if ( + validatedCredentials.data.username === "admin" && + validatedCredentials.data.password === "password" + ) { + return { + id: "admin", + username: "admin", + fullname: "Administrator", + email: "admin@localhost", + role: "admin", + }; + } + } + + const [user, userError] = await wrapResult( + getUserByEmailOrUsername(validatedCredentials.data.username as string), + ); + if (user === null || userError !== null) { + return null; + } + + const [isPasswordMatch, verifyError] = await wrapResult( + argon2.verify(user.password, validatedCredentials.data.password as string, { + type: argon2.argon2i, + }), + ); + if (!isPasswordMatch || verifyError !== null) { + return null; + } + + return { + id: user._id.toHexString(), + username: user.username, + fullname: user.fullname, + email: user.email, + role: user.role, + }; + }, + }), + ], + session: { + strategy: "jwt", + }, + callbacks: { + async jwt({ token, user }) { + if (user) { + token.sub = user.id; + token.user = { + id: user.id, + username: user.username, + fullname: user.fullname, + email: user.email, + role: user.role, + }; + } + return token; + }, + async session({ session, token }) { + session.user = token.user; + return session; + }, + }, + pages: { + signIn: "/sign-in", + }, + }), + authorization, +); diff --git a/src/lib/components/ui/alert/alert-description.svelte b/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000..42a9133 --- /dev/null +++ b/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/src/lib/components/ui/alert/alert-title.svelte b/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000..72d0138 --- /dev/null +++ b/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/alert/alert.svelte b/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000..8c6790f --- /dev/null +++ b/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/alert/index.ts b/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000..cde7987 --- /dev/null +++ b/src/lib/components/ui/alert/index.ts @@ -0,0 +1,33 @@ +import { tv, type VariantProps } from "tailwind-variants"; + +import Root from "./alert.svelte"; +import Description from "./alert-description.svelte"; +import Title from "./alert-title.svelte"; + +export const alertVariants = tv({ + base: "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11", + + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive" + } + }, + defaultVariants: { + variant: "default" + } +}); + +export type Variant = VariantProps["variant"]; +export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + +export { + Root, + Description, + Title, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle +}; diff --git a/src/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..2c3128c --- /dev/null +++ b/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label +}; diff --git a/src/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..264c8fd --- /dev/null +++ b/src/lib/components/ui/label/label.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/schema/auth.ts b/src/lib/schema/auth.ts new file mode 100644 index 0000000..87df3c3 --- /dev/null +++ b/src/lib/schema/auth.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const authSchema = z.object({ + username: z.string().trim(), + password: z.string().min(8).max(32), +}); +export type AuthSchema = z.infer; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ccaec55..8610b3e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -54,3 +54,16 @@ export const flyAndScale = ( easing: cubicOut, }; }; + +export async function wrapResult(promise: Promise): Promise<[TResult | null, Error | null]> { + try { + const result = await promise; + return [result, null]; + } catch (error) { + if (error instanceof Error) { + return [null, error]; + } + const wrappedError = new Error(error as string); + return [null, wrappedError]; + } +} diff --git a/src/routes/(auth)/+layout.svelte b/src/routes/(auth)/+layout.svelte new file mode 100644 index 0000000..f0266b3 --- /dev/null +++ b/src/routes/(auth)/+layout.svelte @@ -0,0 +1,7 @@ + + Sign In | OnlyForms + + +
+ +
\ No newline at end of file diff --git a/src/routes/(auth)/sign-in/+page.svelte b/src/routes/(auth)/sign-in/+page.svelte new file mode 100644 index 0000000..3a557cb --- /dev/null +++ b/src/routes/(auth)/sign-in/+page.svelte @@ -0,0 +1,93 @@ + + +
+ + + Sign In to OnlyForms + Sign In to access your OnlyForms account + + +
+
+ + + {#if $errors.username} +

{$errors.username}

+ {/if} +
+
+ + + {#if $errors.password} +

{$errors.password}

+ {/if} +
+
+
+ + + +
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2816bcf..348d8dd 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,8 @@ + + diff --git a/src/routes/app/+layout.server.ts b/src/routes/app/+layout.server.ts new file mode 100644 index 0000000..6b8848e --- /dev/null +++ b/src/routes/app/+layout.server.ts @@ -0,0 +1,7 @@ +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async (event) => { + return { + session: await event.locals.getSession(), + }; +}; diff --git a/src/routes/app/+layout.svelte b/src/routes/app/+layout.svelte new file mode 100644 index 0000000..6e5e5bd --- /dev/null +++ b/src/routes/app/+layout.svelte @@ -0,0 +1,5 @@ + + Dashboard | OnlyForms + + + diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte new file mode 100644 index 0000000..29fd106 --- /dev/null +++ b/src/routes/app/+page.svelte @@ -0,0 +1,12 @@ + + +
+

+ Hello there, you're authenticated as {$page.data.session?.user?.email} +

+ +