├── .npmrc ├── static └── favicon.png ├── src ├── lib │ ├── stores.ts │ ├── components │ │ ├── logo.svelte │ │ ├── footer.svelte │ │ ├── BottomBar.svelte │ │ ├── SignoutForm.svelte │ │ ├── navigation.svelte │ │ ├── sign-in.svelte │ │ └── sign-up.svelte │ ├── _helpers │ │ ├── convertNameToInitials.ts │ │ ├── parseTrack.ts │ │ ├── getAllUrlParams.ts │ │ └── parseMessage.ts │ ├── server │ │ ├── db │ │ │ ├── client.ts │ │ │ └── schema.ts │ │ ├── lucia.ts │ │ ├── email-send.ts │ │ ├── log.ts │ │ └── tokens.ts │ ├── utils │ │ └── string.ts │ └── config │ │ ├── constants.ts │ │ ├── zod-schemas.ts │ │ └── email-messages.ts ├── routes │ ├── +layout.server.ts │ ├── (protected) │ │ ├── +layout.server.ts │ │ ├── dashboard │ │ │ └── +page.svelte │ │ ├── +layout.svelte │ │ └── profile │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── auth │ │ ├── password │ │ │ ├── reset │ │ │ │ ├── success │ │ │ │ │ └── +page.svelte │ │ │ │ ├── +page.server.ts │ │ │ │ └── +page.svelte │ │ │ └── update-[token] │ │ │ │ ├── success │ │ │ │ └── +page.svelte │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.server.ts │ │ ├── +layout.svelte │ │ ├── sign-out │ │ │ └── +page.server.ts │ │ ├── email-verification │ │ │ ├── +page.svelte │ │ │ ├── [token] │ │ │ │ └── +server.ts │ │ │ └── +page.server.ts │ │ ├── sign-in │ │ │ ├── +page.svelte │ │ │ └── +page.server.ts │ │ └── sign-up │ │ │ ├── +page.svelte │ │ │ └── +page.server.ts │ ├── +layout.ts │ ├── inlang │ │ └── [language].json │ │ │ └── +server.ts │ ├── +error.svelte │ ├── (legal) │ │ ├── +layout@.svelte │ │ ├── terms │ │ │ └── +page.svelte │ │ └── privacy │ │ │ └── +page.svelte │ ├── +page.svelte │ └── +layout.svelte ├── app.postcss ├── app.html ├── app.d.ts ├── hooks.server.ts └── theme.postcss ├── postcss.config.cjs ├── .env.example ├── .gitignore ├── .eslintignore ├── .prettierignore ├── .prettierrc ├── drizzle.config.ts ├── migrations ├── meta │ ├── _journal.json │ └── 0000_snapshot.json └── 0000_closed_golden_guardian.sql ├── vite.config.ts ├── .github └── dependabot.yml ├── tsconfig.json ├── .eslintrc.cjs ├── inlang.config.js ├── svelte.config.js ├── tailwind.config.ts ├── README.md ├── LICENSE ├── languages ├── en.json ├── de.json └── es.json └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ak4zh/slide/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const loading = writable(false); -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | export const load = async (event: { locals: { user: any } }) => { 2 | return { user: event.locals.user }; 3 | }; 4 | -------------------------------------------------------------------------------- /src/routes/(protected)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | export const load = async (event: { locals: { user: any } }) => { 2 | return { user: event.locals.user }; 3 | }; 4 | -------------------------------------------------------------------------------- /src/lib/components/logo.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres 2 | 3 | # SMTP config 4 | FROM_EMAIL= 5 | SMTP_HOST= 6 | SMTP_PORT= 7 | SMTP_SECURE= 8 | SMTP_USER= 9 | SMTP_PASS= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | .vscode* -------------------------------------------------------------------------------- /src/app.postcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @tailwind variants; 5 | 6 | /*place global styles here */ 7 | html, 8 | body { 9 | @apply h-full overflow-hidden; 10 | } 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /src/routes/(protected)/dashboard/+page.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Protected Area

3 |
4 | 5 |
6 | 7 |

If you are seeing this page, you are logged in.

8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | import * as dotenv from "dotenv"; 3 | dotenv.config(); 4 | 5 | export default { 6 | schema: "./src/lib/server/db/schema.ts", 7 | out: "./migrations", 8 | connectionString: process.env.DATABASE_URL, 9 | } satisfies Config; -------------------------------------------------------------------------------- /src/lib/_helpers/convertNameToInitials.ts: -------------------------------------------------------------------------------- 1 | export default function convertNameToInitials(firstName: string, lastName: string): string { 2 | const firstInitial = Array.from(firstName)[0]; 3 | const lastInitial = Array.from(lastName)[0]; 4 | return `${firstInitial}${lastInitial}`; 5 | } 6 | -------------------------------------------------------------------------------- /migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1692811865662, 9 | "tag": "0000_closed_golden_guardian", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /src/routes/auth/password/reset/success/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

{i('auth.password.reset.success.emailSent')}

6 |
7 |

8 | {i('auth.password.reset.success.checkEmail')} 9 |

10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | import inlangPlugin from '@inlang/sdk-js/adapter-sveltekit'; 4 | import { purgeCss } from 'vite-plugin-tailwind-purgecss'; 5 | 6 | export default defineConfig({ 7 | plugins: [inlangPlugin(), sveltekit(), purgeCss()] 8 | }); 9 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | /* This file was created by inlang. 2 | It is needed in order to circumvent a current limitation of SvelteKit. See https://github.com/inlang/inlang/issues/647 3 | You can remove this comment and modify the file as you like. We just need to make sure it exists. 4 | Please do not delete it (inlang will recreate it if needed). */ -------------------------------------------------------------------------------- /src/routes/auth/password/update-[token]/success/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

{i('auth.password.update.updated')}

6 |
7 |

8 | Your password has been updated. You may now sign in with your new password. 9 |

10 | -------------------------------------------------------------------------------- /src/routes/inlang/[language].json/+server.ts: -------------------------------------------------------------------------------- 1 | /* This file was created by inlang. 2 | It is needed in order to circumvent a current limitation of SvelteKit. See https://github.com/inlang/inlang/issues/647 3 | You can remove this comment and modify the file as you like. We just need to make sure it exists. 4 | Please do not delete it (inlang will recreate it if needed). */ -------------------------------------------------------------------------------- /src/lib/_helpers/parseTrack.ts: -------------------------------------------------------------------------------- 1 | export default async function parseTrack(track: unknown): Promise { 2 | let trackObj = {}; 3 | try { 4 | if (track) { 5 | if (typeof track === 'string') { 6 | trackObj = { track: track }; 7 | } else { 8 | trackObj = track; 9 | } 10 | } 11 | } catch (error) { 12 | console.log('error: ', error); 13 | } 14 | return trackObj; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/server/db/client.ts: -------------------------------------------------------------------------------- 1 | import postgres from "pg"; 2 | import { drizzle } from "drizzle-orm/node-postgres"; 3 | import { DATABASE_URL } from "$env/static/private"; 4 | import { migrate } from "drizzle-orm/node-postgres/migrator"; 5 | 6 | export const connectionPool = new postgres.Pool({ 7 | connectionString: DATABASE_URL 8 | }); 9 | 10 | export const db = drizzle(connectionPool, { logger: true }); 11 | -------------------------------------------------------------------------------- /src/lib/_helpers/getAllUrlParams.ts: -------------------------------------------------------------------------------- 1 | export default async function getAllUrlParams(url: string): Promise { 2 | let paramsObj = {}; 3 | try { 4 | url = url?.slice(1); //remove leading ? 5 | if (!url) return {}; //if no params return 6 | paramsObj = await Object.fromEntries(await new URLSearchParams(url)); 7 | } catch (error) { 8 | console.log('error: ', error); 9 | } 10 | return paramsObj; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/components/footer.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /src/lib/_helpers/parseMessage.ts: -------------------------------------------------------------------------------- 1 | export default async function parseMessage(message: unknown): Promise { 2 | let messageObj = {}; 3 | try { 4 | if (message) { 5 | if (typeof message === 'string') { 6 | messageObj = { message: message }; 7 | } else { 8 | messageObj = message; 9 | } 10 | } 11 | } catch (error) { 12 | console.log('error: ', error); 13 | } 14 | return messageObj; 15 | } 16 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/auth/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/routes/(protected)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/lib/utils/string.ts: -------------------------------------------------------------------------------- 1 | import type { RequestEvent, ServerLoadEvent } from '@sveltejs/kit'; 2 | 3 | export function getDomain(event: ServerLoadEvent | RequestEvent) { 4 | return event.url.hostname.replace(/^www\./, ''); 5 | } 6 | 7 | export function getBaseURL(url: URL) { 8 | return `${url.protocol}//${url.host}`; 9 | } 10 | 11 | export function getProviderId(provider: string, event: ServerLoadEvent | RequestEvent) { 12 | return `${getDomain(event)}-${provider}`; 13 | } -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {#if $page.status === 404} 7 |

Page Not Found.

8 |

Go Home

9 | {:else} 10 |

Unexpected Error

11 |

We're investigating the issue.

12 | {/if} 13 | 14 | {#if $page.error?.errorId} 15 |

Error ID: {$page.error.errorId}

16 | {/if} 17 |
18 | -------------------------------------------------------------------------------- /src/routes/(legal)/+layout@.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 20 | -------------------------------------------------------------------------------- /src/routes/auth/password/update-[token]/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |

Reset password

7 |
8 | 9 | 10 | 11 |
12 | {#if form?.message} 13 |

{form.message}

14 | {/if} -------------------------------------------------------------------------------- /src/routes/auth/sign-out/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '$lib/server/lucia'; 2 | import { redirect } from '@sveltejs/kit'; 3 | 4 | export const actions = { 5 | default: async ({ locals }) => { 6 | const session = await locals.auth.validate(); 7 | if (!session) { 8 | redirect(302, '/auth/sign-in'); 9 | } 10 | await auth.invalidateSession(session.sessionId); // invalidate session 11 | locals.auth.setSession(null); // remove cookie 12 | redirect(302, '/auth/sign-in'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:svelte/recommended', 7 | 'prettier' 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020, 14 | extraFileExtensions: ['.svelte'] 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | }, 21 | overrides: [ 22 | { 23 | files: ['*.svelte'], 24 | parser: 'svelte-eslint-parser', 25 | parserOptions: { 26 | parser: '@typescript-eslint/parser' 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /src/routes/auth/email-verification/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |

Confirm Your Email Address

11 | 12 |
13 | 14 | Please check your email account for a message to confirm your email address for {APP_NAME}. If you 15 | did not receive the email, 16 |
17 | 18 |
19 | -------------------------------------------------------------------------------- /inlang.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type { import("@inlang/core/config").DefineConfig } 3 | */ 4 | export async function defineConfig(env) { 5 | const { default: jsonPlugin } = await env.$import( 6 | 'https://cdn.jsdelivr.net/gh/samuelstroschein/inlang-plugin-json@2/dist/index.js' 7 | ); 8 | const { default: sdkPlugin } = await env.$import( 9 | 'https://cdn.jsdelivr.net/npm/@inlang/sdk-js-plugin@0.11.8/dist/index.js' 10 | ); 11 | 12 | return { 13 | referenceLanguage: 'en', 14 | plugins: [ 15 | jsonPlugin({ 16 | pathPattern: './languages/{language}.json' 17 | }), 18 | sdkPlugin({ 19 | languageNegotiation: { 20 | strategies: [{ type: 'localStorage' }] 21 | } 22 | }) 23 | ] 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/server/lucia.ts: -------------------------------------------------------------------------------- 1 | // lib/server/lucia.ts 2 | import { lucia } from 'lucia'; 3 | import { sveltekit } from 'lucia/middleware'; 4 | import { dev } from '$app/environment'; 5 | import { pg } from '@lucia-auth/adapter-postgresql'; 6 | import { connectionPool } from './db/client'; 7 | 8 | export const auth = lucia({ 9 | adapter: pg(connectionPool, { 10 | user: 'auth_user', 11 | key: 'auth_key', 12 | session: 'auth_session' 13 | }), 14 | env: dev ? 'DEV' : 'PROD', 15 | middleware: sveltekit(), 16 | getUserAttributes: (data) => { 17 | return { 18 | role: data.role, 19 | firstName: data.first_name, 20 | lastName: data.last_name, 21 | email: data.email, 22 | emailVerified: data.email_verified 23 | }; 24 | } 25 | }); 26 | 27 | export type Auth = typeof auth; 28 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | //import adapter from '@sveltejs/adapter-node'; 3 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 8 | // for more information about preprocessors 9 | preprocess: vitePreprocess(), 10 | 11 | kit: { 12 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 13 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 14 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 15 | adapter: adapter() 16 | } 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import type { Config } from 'tailwindcss'; 3 | import { skeleton } from '@skeletonlabs/tw-plugin'; 4 | 5 | const config = { 6 | // 2. Opt for dark mode to be handled via the class method 7 | darkMode: 'class', 8 | content: [ 9 | './src/**/*.{html,js,svelte,ts}', 10 | // 3. Append the path to the Skeleton package 11 | join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}') 12 | ], 13 | theme: { 14 | extend: {} 15 | }, 16 | plugins: [ 17 | // 4. Append the Skeleton plugin (after other plugins) 18 | skeleton({ 19 | themes: { 20 | // Register each theme within this array: 21 | preset: [ "skeleton" ], 22 | } 23 | }), 24 | require('@tailwindcss/forms') 25 | ] 26 | } satisfies Config; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /src/routes/auth/email-verification/[token]/+server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { validateEmailVerificationToken } from '$lib/server/tokens'; 3 | import { auth } from '$lib/server/lucia'; 4 | 5 | export const GET = async ({ params, locals }) => { 6 | try { 7 | const userId = await validateEmailVerificationToken(params.token); 8 | if (!userId) { 9 | return new Response('Invalid or expired token', { 10 | status: 422 11 | }); 12 | } 13 | await auth.invalidateAllUserSessions(userId); 14 | await auth.updateUserAttributes(userId, { 15 | email_verified: true 16 | }); 17 | const session = await auth.createSession({ userId, attributes: {} }); 18 | console.log(session) 19 | locals.auth.setSession(session); 20 | } catch (err) { 21 | console.log(err) 22 | } 23 | 24 | redirect(302, '/'); 25 | }; 26 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // src/app.d.ts 2 | declare global { 3 | namespace App { 4 | interface Locals { 5 | auth: import('lucia-auth').AuthRequest; 6 | user: Lucia.UserAttributes; 7 | startTimer: number; 8 | error: string; 9 | errorId: string; 10 | errorStackTrace: string; 11 | message: unknown; 12 | track: unknown; 13 | } 14 | interface Error { 15 | code?: string; 16 | errorId?: string; 17 | } 18 | } 19 | } 20 | 21 | /// 22 | declare global { 23 | namespace Lucia { 24 | type Auth = import('$lib/lucia').Auth; 25 | type DatabaseSessionAttributes = {}; 26 | type DatabaseUserAttributes = { 27 | role: string; 28 | first_name: string; 29 | last_name: string; 30 | domain: string; 31 | email: string; 32 | email_verified: boolean; 33 | }; 34 | } 35 | } 36 | 37 | // THIS IS IMPORTANT!!! 38 | export {}; 39 | -------------------------------------------------------------------------------- /src/routes/auth/sign-in/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {i('signin')} 15 | {i('signup')} 16 | 17 | 18 | {#if tabSet === 'signIn'} 19 | 20 | {:else if tabSet === 'signUp'} 21 | 22 | {/if} 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/routes/auth/sign-up/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {i('signin')} 15 | {i('signup')} 16 | 17 | 18 | {#if tabSet === 'signIn'} 19 | 20 | {:else if tabSet === 'signUp'} 21 | 22 | {/if} 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Newer Template Available 2 | Created a newer template featuring Svelte 5 and shadcn-svelte, along with all dependencies updated to their latest versions. 3 | 4 | - [Sveltekit Template 2024](https://github.com/ak4zh/sveltekit-template) 5 | 6 | # SLIDE: SvelteKit + Lucia + i18n using inlang + Drizzle + TailwindCSS using Skeleton 7 | 8 | - Multi Tenant configured 9 | - [Lucia](https://lucia-auth.com/) for authentication 10 | - [inlang](https://inlang.com) for language translation 11 | - [Drizzle ORM](https://orm.drizzle.team/) for database connectivity and type safety 12 | - [Skeleton](https://www.skeleton.dev) for ui elements 13 | - [Lucide](https://lucide.dev) for icons 14 | - [Zod](https://zod.dev) 15 | - [Superforms](https://superforms.vercel.app) to handle form validation and management. 16 | 17 | Highly inspired from [Sveltekit Auth Starter](https://github.com/delay/sveltekit-auth-starter) which uses prisma, I wanted one with drizzle ORM and a [BottomBar](https://github.com/delay/sveltekit-auth-starter/pull/10) as I prefer to use bottom bar in mobile views. 18 | -------------------------------------------------------------------------------- /src/lib/components/BottomBar.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | {#each navItems||[] as nav} 12 | {#if nav.title === 'signout'} 13 | {#if $page.data.user} 14 | 15 | {/if} 16 | {:else if (nav.alwaysVisible || ($page.data.user && nav.protected) || (!$page.data.user && !nav.protected))} 17 | 18 | 19 | {i(nav.title)} 20 | 21 | {/if} 22 | {/each} 23 |
24 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Akash Agarwal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/components/SignoutForm.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
27 | 35 |
-------------------------------------------------------------------------------- /src/routes/auth/email-verification/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { generateEmailVerificationToken } from '$lib/server/tokens'; 2 | import { sendVerificationEmail } from '$lib/config/email-messages'; 3 | import { redirect } from '@sveltejs/kit'; 4 | import { getBaseURL } from '$lib/utils/string'; 5 | import { z } from 'zod'; 6 | import { superValidate } from 'sveltekit-superforms/server'; 7 | 8 | const schema = z.object({}); 9 | 10 | export const load = async ({ locals }) => { 11 | const session = await locals.auth.validate(); 12 | const form = await superValidate(schema); 13 | 14 | if (!session) { 15 | redirect(302, '/login'); 16 | } 17 | if (session.user.emailVerified) { 18 | redirect(302, '/'); 19 | } 20 | return { form }; 21 | }; 22 | 23 | export const actions = { 24 | default: async (event) => { 25 | const form = await superValidate(event.request, schema); 26 | const user = event.locals.user; 27 | if (!user) { 28 | redirect(302, '/auth/login'); 29 | } 30 | if (user.emailVerified) { 31 | redirect(302, '/'); 32 | } 33 | const token = await generateEmailVerificationToken(user.userId); 34 | await sendVerificationEmail(getBaseURL(event.url), user.email, token); 35 | return { form }; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /languages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "signin": "Sign In", 3 | "signinProblem": "Sign In Problem", 4 | "signup": "Sign Up", 5 | "signout": "Sign Out", 6 | "forgotPassword": "Forgot Password?", 7 | "contact": "Contact", 8 | "privacy": "Privacy", 9 | "terms": "Terms", 10 | "email": "Email Address", 11 | "password": "Password", 12 | "firstName": "First Name", 13 | "lastName": "Last Name", 14 | "profile": "Profile", 15 | "home": "Home", 16 | "dashboard": "Dashboard", 17 | "auth": { 18 | "password": { 19 | "reset": { 20 | "success": { 21 | "emailSent": "Password Reset Email Sent", 22 | "checkEmail": "Check your email account for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder." 23 | }, 24 | "resetProblem": "Reset Password Problem", 25 | "sendResetEmail": "Send Password Reset Email" 26 | }, 27 | "update": { 28 | "success": { 29 | "updated": "Password updated successfully" 30 | }, 31 | "changePassword": "Change Your Password", 32 | "passwordProblem": "Change Password Problem", 33 | "updatePassword": "Update Password" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /languages/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "signin": "Anmelden", 3 | "signup": "Registrieren", 4 | "signout": "Abmelden", 5 | "forgotPassword": "Passwort vergessen?", 6 | "contact": "Kontakt", 7 | "privacy": "Datenschutz", 8 | "terms": "Nutzungsbedingungen", 9 | "email": "E-Mail-Adresse", 10 | "password": "Passwort", 11 | "firstName": "Vorname", 12 | "lastName": "Nachname", 13 | "profile": "Profil", 14 | "home": "Startseite", 15 | "dashboard": "Armaturenbrett", 16 | "auth": { 17 | "password": { 18 | "reset": { 19 | "success": { 20 | "emailSent": "Passwort zurücksetzen E-Mail gesendet", 21 | "checkEmail": "Überprüfen Sie Ihr E-Mail-Konto auf einen Link zum Zurücksetzen Ihres Passworts. Wenn er nicht innerhalb weniger Minuten angezeigt wird, überprüfen Sie Ihren Spam-Ordner." 22 | }, 23 | "resetProblem": "Problem beim Zurücksetzen des Passworts", 24 | "sendResetEmail": "Passwort-Zurücksetzen-E-Mail senden" 25 | }, 26 | "update": { 27 | "success": { 28 | "updated": "Passwort erfolgreich aktualisiert" 29 | }, 30 | "changePassword": "Ändern Sie Ihr Passwort", 31 | "passwordProblem": "Problem beim Passwortwechsel", 32 | "updatePassword": "Passwort aktualisieren" 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /languages/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "signin": "Iniciar sesión", 3 | "signup": "Registrarse", 4 | "signout": "Cerrar sesión", 5 | "forgotPassword": "¿Olvidaste tu contraseña?", 6 | "contact": "Contacto", 7 | "privacy": "Privacidad", 8 | "terms": "Términos", 9 | "email": "Dirección de correo electrónico", 10 | "password": "Contraseña", 11 | "firstName": "Nombre", 12 | "lastName": "Apellido", 13 | "profile": "Perfil", 14 | "home": "Inicio", 15 | "dashboard": "Panel", 16 | "auth": { 17 | "password": { 18 | "reset": { 19 | "success": { 20 | "emailSent": "Correo electrónico de restablecimiento de contraseña enviado", 21 | "checkEmail": "Verifique su cuenta de correo electrónico para obtener un enlace para restablecer su contraseña. Si no aparece en unos minutos, verifique su carpeta de correo no deseado." 22 | }, 23 | "resetProblem": "Problema al restablecer la contraseña", 24 | "sendResetEmail": "Enviar correo electrónico de restablecimiento de contraseña" 25 | }, 26 | "update": { 27 | "success": { 28 | "updated": "Contraseña actualizada correctamente" 29 | }, 30 | "changePassword": "Cambiar contraseña", 31 | "passwordProblem": "Problema al cambiar la contraseña", 32 | "updatePassword": "Actualizar contraseña" 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/routes/auth/sign-in/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { fail, redirect } from '@sveltejs/kit'; 2 | import { setError, superValidate } from 'sveltekit-superforms/server'; 3 | import { auth } from '$lib/server/lucia'; 4 | import { userSchema } from '$lib/config/zod-schemas'; 5 | import { getProviderId } from '$lib/utils/string'; 6 | 7 | const signInSchema = userSchema.pick({ 8 | email: true, 9 | password: true 10 | }); 11 | 12 | export const load = async (event) => { 13 | if (event.locals.user) redirect(302, '/dashboard'); 14 | const form = await superValidate(event, signInSchema); 15 | return { form }; 16 | }; 17 | 18 | export const actions = { 19 | default: async (event) => { 20 | const form = await superValidate(event, signInSchema); 21 | //console.log(form); 22 | 23 | if (!form.valid) return fail(400, { form }); 24 | 25 | //add user to db 26 | try { 27 | const key = await auth.useKey( 28 | getProviderId('emailpass', event), 29 | form.data.email, 30 | form.data.password 31 | ); 32 | const session = await auth.createSession({ 33 | userId: key.userId, 34 | attributes: {} 35 | }); 36 | event.locals.auth.setSession(session); 37 | } catch (e) { 38 | //TODO: need to return error message to client 39 | console.error(e); 40 | // email already in use 41 | //const { fieldErrors: errors } = e.flatten(); 42 | return setError(form, 'The email or password is incorrect.'); 43 | } 44 | return { form }; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/lib/config/constants.ts: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | import { LogIn, LogOut, UserCircle2, Home, LayoutDashboard } from 'lucide-svelte'; 3 | 4 | export const BASE_URL = dev ? 'http://localhost:5173' : 'https://sveltekit-auth.uv-ray.com'; 5 | export const APP_NAME = 'Sveltekit Auth Starter'; 6 | export const CONTACT_EMAIL = 'yourname@email.com'; 7 | export const DOMAIN = 'sveltekit-auth.uv-ray.com'; 8 | /* WARNING!!! TERMS AND CONDITIONS AND PRIVACY POLICY 9 | WERE CREATED BY CHATGPT AS AN EXAMPLE ONLY. 10 | CONSULT A LAWYER AND DEVELOP YOUR OWN TERMS AND PRIVACY POLICY!!! */ 11 | export const TERMS_PRIVACY_CONTACT_EMAIL = 'yourname@email.com'; 12 | export const TERMS_PRIVACY_WEBSITE = 'yourdomain.com'; 13 | export const TERMS_PRIVACY_COMPANY = 'Your Company'; 14 | export const TERMS_PRIVACY_EFFECTIVE_DATE = 'January 1, 2023'; 15 | export const TERMS_PRIVACY_APP_NAME = 'Your App'; 16 | export const TERMS_PRIVACY_APP_PRICING_AND_SUBSCRIPTIONS = 17 | '[Details about the pricing, subscription model, refund policy]'; 18 | export const TERMS_PRIVACY_COUNTRY = 'United States'; 19 | 20 | export const navItems = [ 21 | { title: "home", url: "/", icon: Home, alwaysVisible: true }, 22 | { title: 'dashboard', url: '/dashboard', icon: LayoutDashboard, alwaysVisible: true }, 23 | { title: 'profile', url: '/profile', icon: LayoutDashboard, protected: true }, 24 | { title: 'signout', url: '/auth/sign-out', icon: LogOut, protected: true }, 25 | { title: 'signin', url: '/auth/sign-in', icon: LogIn }, 26 | { title: 'signup', url: '/auth/sign-up', icon: UserCircle2 }, 27 | ] 28 | 29 | export type NavItems = typeof navItems; -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '$lib/server/lucia'; 2 | import { redirect, type Handle } from '@sveltejs/kit'; 3 | import type { HandleServerError } from '@sveltejs/kit'; 4 | import log from '$lib/server/log'; 5 | import { db } from "$lib/server/db/client"; 6 | import { migrate } from "drizzle-orm/node-postgres/migrator"; 7 | 8 | await migrate(db, { migrationsFolder: "./migrations" }); 9 | 10 | export const handleError: HandleServerError = async ({ error, event }) => { 11 | const errorId = crypto.randomUUID(); 12 | 13 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 14 | //@ts-ignore 15 | event.locals.error = error?.toString() || undefined; 16 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 17 | //@ts-ignore 18 | event.locals.errorStackTrace = error?.stack || undefined; 19 | event.locals.errorId = errorId; 20 | log(500, event); 21 | 22 | return { 23 | message: 'An unexpected error occurred.', 24 | errorId 25 | }; 26 | }; 27 | 28 | export const handle: Handle = async ({ event, resolve }) => { 29 | const startTimer = Date.now(); 30 | event.locals.startTimer = startTimer; 31 | 32 | event.locals.auth = auth.handleRequest(event); 33 | if (event.locals?.auth) { 34 | const session = await event.locals.auth.validate(); 35 | const user = session?.user; 36 | event.locals.user = user; 37 | if (event.route.id?.startsWith('/(protected)')) { 38 | if (!user) redirect(302, '/auth/sign-in'); 39 | if (!user.emailVerified) redirect(302, '/auth/email-verification'); 40 | } 41 | } 42 | 43 | const response = await resolve(event); 44 | log(response.status, event); 45 | return response; 46 | }; 47 | -------------------------------------------------------------------------------- /src/lib/server/email-send.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import { env } from '$env/dynamic/private'; 3 | 4 | const transporter = nodemailer.createTransport({ 5 | host: env.SMTP_HOST, 6 | port: Number(env.SMTP_PORT), 7 | secure: Number(env.SMTP_SECURE) === 1, 8 | auth: { 9 | user: env.SMTP_USER, 10 | pass: env.SMTP_PASS 11 | } 12 | }); 13 | 14 | export default async function sendEmail( 15 | email: string, 16 | subject: string, 17 | bodyHtml?: string, 18 | bodyText?: string 19 | ) { 20 | if ( 21 | env.SMTP_HOST && 22 | env.SMTP_PORT && 23 | env.SMTP_USER && 24 | env.SMTP_PASS && 25 | env.FROM_EMAIL 26 | ) { 27 | // create Nodemailer SMTP transporter 28 | let info; 29 | try { 30 | if (!bodyText) { 31 | info = await transporter.sendMail({ 32 | from: env.FROM_EMAIL, 33 | to: email, 34 | subject: subject, 35 | html: bodyHtml 36 | }); 37 | } else if (!bodyHtml) { 38 | info = await transporter.sendMail({ 39 | from: env.FROM_EMAIL, 40 | to: email, 41 | subject: subject, 42 | text: bodyText 43 | }); 44 | } else { 45 | info = await transporter.sendMail({ 46 | from: env.FROM_EMAIL, 47 | to: email, 48 | subject: subject, 49 | html: bodyHtml, 50 | text: bodyText 51 | }); 52 | } 53 | console.log('E-mail sent successfully!'); 54 | console.log(info); 55 | return { 56 | statusCode: 200, 57 | message: 'E-mail sent successfully.' 58 | }; 59 | } catch (error) { 60 | throw new Error(`Error sending email: ${JSON.stringify(error)}`); 61 | } 62 | } else { 63 | console.log(`Email in Log:\nSubject: ${subject}\nBody: ${bodyText}`); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/components/navigation.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 53 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Sveltekit Auth Starter 7 | 11 | 12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |

Sveltekit Auth Starter

22 |
23 | 24 |
25 |

26 | This is a sveltekit auth starter. It is an open source auth starter project 29 | utilizing 30 | Lucia for authentication, 31 | Skeleton for ui elements, 32 | Drizzle ORM 33 | for database connectivity and type safety, Lucide for 34 | icons, Inlang for internationalization and 35 | Sveltekit for the javascript framework. It has email verification, 36 | password reset, and will send an email out if the user changes their email address to re-verify 37 | it. It is released as open source under an MIT license. 38 |

39 |
40 |
41 |
42 | 43 | 45 | -------------------------------------------------------------------------------- /src/routes/auth/password/reset/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { sendPasswordResetEmail } from '$lib/config/email-messages'; 2 | import { generatePasswordResetToken } from '$lib/server/tokens'; 3 | import { fail, redirect } from '@sveltejs/kit'; 4 | import { db } from '$lib/server/db/client'; 5 | import * as tables from '$lib/server/db/schema'; 6 | import { eq } from 'drizzle-orm'; 7 | import { userSchema } from '$lib/config/zod-schemas.js'; 8 | import { setError, superValidate } from 'sveltekit-superforms/server'; 9 | import { getBaseURL } from '$lib/utils/string'; 10 | 11 | const resetPasswordSchema = userSchema.pick({ email: true }); 12 | 13 | export const load = async (event) => { 14 | const form = await superValidate(event, resetPasswordSchema); 15 | return { 16 | form 17 | }; 18 | }; 19 | 20 | export const actions = { 21 | default: async (event) => { 22 | const form = await superValidate(event, resetPasswordSchema); 23 | const email = form.data.email; 24 | if (!form.valid) return fail(400, { form }); 25 | 26 | //add user to db 27 | try { 28 | const databaseUsers = await db 29 | .select() 30 | .from(tables.users) 31 | .where(eq(tables.users.email, email)); 32 | if (!databaseUsers.length) return setError(form, 'email', 'Email does not exist'); 33 | const databaseUser = databaseUsers[0]; 34 | const userId = databaseUser.id; 35 | const token = await generatePasswordResetToken(userId); 36 | await sendPasswordResetEmail(getBaseURL(event.url), email, token); 37 | } catch (e) { 38 | console.error(e); 39 | return setError( 40 | form, 41 | 'email', 42 | 'The was a problem resetting your password. Please contact support if you need further help.' 43 | ); 44 | } 45 | redirect(302, '/auth/password/reset/success'); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/lib/config/zod-schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const userSchema = z.object({ 4 | firstName: z 5 | .string({ required_error: 'First Name is required' }) 6 | .min(1, { message: 'First Name is required' }) 7 | .trim(), 8 | lastName: z 9 | .string({ required_error: 'Last Name is required' }) 10 | .min(1, { message: 'Last Name is required' }) 11 | .trim(), 12 | email: z 13 | .string({ required_error: 'Email is required' }) 14 | .email({ message: 'Please enter a valid email address' }), 15 | password: z 16 | .string({ required_error: 'Password is required' }) 17 | .min(6, { message: 'Password must be at least 6 characters' }) 18 | .trim(), 19 | confirmPassword: z 20 | .string({ required_error: 'Password is required' }) 21 | .min(6, { message: 'Password must be at least 6 characters' }) 22 | .trim(), 23 | //terms: z.boolean({ required_error: 'You must accept the terms and privacy policy' }), 24 | role: z 25 | .enum(['USER', 'PREMIUM', 'ADMIN'], { required_error: 'You must have a role' }) 26 | .default('USER'), 27 | verified: z.boolean().default(false), 28 | token: z.string().optional(), 29 | receiveEmail: z.boolean().default(true), 30 | createdAt: z.date().optional(), 31 | updatedAt: z.date().optional() 32 | }); 33 | 34 | export const userUpdatePasswordSchema = userSchema 35 | .pick({ password: true, confirmPassword: true }) 36 | .superRefine(({ confirmPassword, password }, ctx) => { 37 | if (confirmPassword !== password) { 38 | ctx.addIssue({ 39 | code: 'custom', 40 | message: 'Password and Confirm Password must match', 41 | path: ['password'] 42 | }); 43 | ctx.addIssue({ 44 | code: 'custom', 45 | message: 'Password and Confirm Password must match', 46 | path: ['confirmPassword'] 47 | }); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /src/routes/auth/password/update-[token]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { userUpdatePasswordSchema } from '$lib/config/zod-schemas.js'; 2 | import { auth } from '$lib/server/lucia'; 3 | import { isValidPasswordResetToken, validatePasswordResetToken } from '$lib/server/tokens'; 4 | import { fail, redirect } from '@sveltejs/kit'; 5 | import { setError, superValidate } from 'sveltekit-superforms/server'; 6 | 7 | export const load = async (event) => { 8 | const validToken = await isValidPasswordResetToken(event.params.token); 9 | if (!validToken) redirect(302, '/password-reset'); 10 | const form = await superValidate(event, userUpdatePasswordSchema); 11 | return { form }; 12 | }; 13 | 14 | export const actions = { 15 | default: async (event) => { 16 | const form = await superValidate(event, userUpdatePasswordSchema); 17 | if (!form.valid) return fail(400, { form }); 18 | const password = form.data.password; 19 | const token = event.params.token as string; 20 | try { 21 | const userId = await validatePasswordResetToken(token); 22 | if (!userId) { 23 | return setError( 24 | form, 25 | 'password', 26 | 'Email address not found for this token. Please contact support if you need further help.' 27 | ); 28 | } 29 | let user = await auth.getUser(userId); 30 | if (!user.emailVerified) { 31 | user = await auth.updateUserAttributes(user.userId, { 32 | emailVerified: true 33 | }); 34 | } 35 | await auth.invalidateAllUserSessions(user.userId); 36 | await auth.updateKeyPassword('email', user.email, password); 37 | const session = await auth.createSession({ 38 | userId: user.userId, 39 | attributes: {} 40 | }); 41 | event.locals.auth.setSession(session); 42 | } catch (e) { 43 | console.error(e); 44 | return setError( 45 | form, 46 | 'password', 47 | 'The was a problem resetting your password. Please contact support if you need further help.' 48 | ); 49 | } 50 | redirect(302, `/auth/password-reset/update-${token}/success`); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slide", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 12 | "format": "prettier --plugin-search-dir . --write .", 13 | "generate": "drizzle-kit generate:pg --out migrations --schema src/lib/server/db/schema.ts" 14 | }, 15 | "devDependencies": { 16 | "@inlang/core": "^0.9.2", 17 | "@inlang/sdk-js": "^0.11.8", 18 | "@skeletonlabs/skeleton": "2.6.0", 19 | "@skeletonlabs/tw-plugin": "0.3.0", 20 | "@sveltejs/adapter-auto": "^3.0.0", 21 | "@sveltejs/kit": "^2.0.0", 22 | "@sveltejs/vite-plugin-svelte": "^3.0.1", 23 | "@tailwindcss/forms": "^0.5.7", 24 | "@tailwindcss/typography": "^0.5.10", 25 | "@types/node": "^20.10.4", 26 | "@types/nodemailer": "^6.4.14", 27 | "@types/pg": "^8.10.9", 28 | "@typescript-eslint/eslint-plugin": "^6.14.0", 29 | "@typescript-eslint/parser": "^6.14.0", 30 | "autoprefixer": "^10.4.16", 31 | "dotenv": "^16.3.1", 32 | "drizzle-kit": "^0.20.6", 33 | "eslint": "^8.56.0", 34 | "eslint-config-prettier": "^9.1.0", 35 | "eslint-plugin-svelte3": "^4.0.0", 36 | "postcss": "^8.4.32", 37 | "prettier": "^3.1.1", 38 | "prettier-plugin-svelte": "^3.1.2", 39 | "svelte": "^4.2.8", 40 | "svelte-check": "^3.6.2", 41 | "sveltekit-superforms": "1.12.0", 42 | "tailwindcss": "^3.3.6", 43 | "tslib": "^2.6.2", 44 | "typescript": "^5.3.3", 45 | "vite": "^5.0.10", 46 | "vite-plugin-tailwind-purgecss": "^0.2.0", 47 | "zod": "^3.22.4" 48 | }, 49 | "type": "module", 50 | "dependencies": { 51 | "@lucia-auth/adapter-postgresql": "^2.0.2", 52 | "drizzle-orm": "^0.29.1", 53 | "drizzle-zod": "^0.5.1", 54 | "lucia": "^2.7.5", 55 | "lucide-svelte": "^0.298.0", 56 | "nodemailer": "^6.9.7", 57 | "pg": "^8.11.3" 58 | } 59 | } -------------------------------------------------------------------------------- /src/lib/server/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, varchar, boolean, uuid, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core"; 2 | 3 | export const users = pgTable( 4 | 'auth_user', 5 | { 6 | id: uuid('id').primaryKey().defaultRandom(), 7 | role: text('role', { enum: ['admin', 'user'] }) 8 | .notNull() 9 | .default('user'), 10 | createdAt: timestamp('created_at').defaultNow().notNull(), 11 | firstName: text('first_name'), 12 | lastName: text('last_name'), 13 | domain: text('domain').notNull(), 14 | email: text('email').notNull(), 15 | emailVerified: boolean('email_verified').default(false).notNull() 16 | }, 17 | (users) => ({ 18 | emailDomainIdx: uniqueIndex('email_domain_idx').on(users.email, users.domain) 19 | }) 20 | ); 21 | 22 | export const emailVerificationTokens = pgTable('email_verification_token', { 23 | id: uuid('id').primaryKey().defaultRandom(), 24 | userId: uuid('user_id') 25 | .notNull() 26 | .references(() => users.id), 27 | expires: bigint('expires', { mode: 'number' }) 28 | }); 29 | 30 | export const passwordResetTokens = pgTable('password_reset_token', { 31 | id: uuid('id').primaryKey().defaultRandom(), 32 | userId: uuid('user_id') 33 | .notNull() 34 | .references(() => users.id), 35 | expires: bigint('expires', { mode: 'number' }) 36 | }); 37 | 38 | export const sessions = pgTable('auth_session', { 39 | id: varchar('id', { 40 | length: 128 41 | }).primaryKey(), 42 | userId: uuid('user_id') 43 | .notNull() 44 | .references(() => users.id), 45 | activeExpires: bigint('active_expires', { 46 | mode: 'number' 47 | }).notNull(), 48 | idleExpires: bigint('idle_expires', { 49 | mode: 'number' 50 | }).notNull() 51 | }); 52 | 53 | export const keys = pgTable('auth_key', { 54 | id: varchar('id', { 55 | length: 255 56 | }).primaryKey(), 57 | userId: uuid('user_id') 58 | .notNull() 59 | .references(() => users.id), 60 | hashedPassword: varchar('hashed_password', { 61 | length: 255 62 | }), 63 | expires: bigint('expires', { 64 | mode: 'number' 65 | }) 66 | }); 67 | -------------------------------------------------------------------------------- /src/lib/server/log.ts: -------------------------------------------------------------------------------- 1 | import getAllUrlParams from '$lib/_helpers/getAllUrlParams'; 2 | import parseTrack from '$lib/_helpers/parseTrack'; 3 | import parseMessage from '$lib/_helpers/parseMessage'; 4 | import { DOMAIN } from '$lib/config/constants'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 7 | //@ts-ignore 8 | export default async function log(statusCode: number, event) { 9 | try { 10 | let level = 'info'; 11 | if (statusCode >= 400) { 12 | level = 'error'; 13 | } 14 | const error = event?.locals?.error || undefined; 15 | const errorId = event?.locals?.errorId || undefined; 16 | const errorStackTrace = event?.locals?.errorStackTrace || undefined; 17 | let urlParams = {}; 18 | if (event?.url?.search) { 19 | urlParams = await getAllUrlParams(event?.url?.search); 20 | } 21 | let messageEvents = {}; 22 | if (event?.locals?.message) { 23 | messageEvents = await parseMessage(event?.locals?.message); 24 | } 25 | let trackEvents = {}; 26 | if (event?.locals?.track) { 27 | trackEvents = await parseTrack(event?.locals?.track); 28 | } 29 | 30 | let referer = event.request.headers.get('referer'); 31 | if (referer) { 32 | const refererUrl = await new URL(referer); 33 | const refererHostname = refererUrl.hostname; 34 | if (refererHostname === 'localhost' || refererHostname === DOMAIN) { 35 | referer = refererUrl.pathname; 36 | } 37 | } else { 38 | referer = undefined; 39 | } 40 | const logData: object = { 41 | level: level, 42 | method: event.request.method, 43 | path: event.url.pathname, 44 | status: statusCode, 45 | timeInMs: Date.now() - event?.locals?.startTimer, 46 | user: event?.locals?.user?.email, 47 | userId: event?.locals?.user?.userId, 48 | referer: referer, 49 | error: error, 50 | errorId: errorId, 51 | errorStackTrace: errorStackTrace, 52 | ...urlParams, 53 | ...messageEvents, 54 | ...trackEvents 55 | }; 56 | console.log('log: ', JSON.stringify(logData)); 57 | } catch (err) { 58 | throw new Error(`Error Logger: ${JSON.stringify(err)}`); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/routes/auth/password/reset/+page.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |

Reset Your Password

22 | 23 |
24 | 25 |
26 | 27 | {#if $errors._errors} 28 | 37 | {/if} 38 |
39 | 56 |
57 | 58 |
59 | 66 |
67 |
68 | -------------------------------------------------------------------------------- /src/routes/auth/sign-up/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { fail, redirect } from '@sveltejs/kit'; 2 | import { setError, superValidate } from 'sveltekit-superforms/server'; 3 | import { auth } from '$lib/server/lucia'; 4 | import { userSchema } from '$lib/config/zod-schemas'; 5 | import { sendVerificationEmail } from '$lib/config/email-messages'; 6 | import { generateEmailVerificationToken } from '$lib/server/tokens.js'; 7 | import { getBaseURL, getDomain, getProviderId } from '$lib/utils/string'; 8 | 9 | const signUpSchema = userSchema.pick({ 10 | firstName: true, 11 | lastName: true, 12 | email: true, 13 | password: true, 14 | terms: true 15 | }); 16 | 17 | export const load = async (event) => { 18 | if (event.locals.user) redirect(302, '/dashboard'); 19 | const form = await superValidate(event, signUpSchema); 20 | return { 21 | form 22 | }; 23 | }; 24 | 25 | export const actions = { 26 | default: async (event) => { 27 | const form = await superValidate(event, signUpSchema); 28 | if (!form.valid) return fail(400, { form }); 29 | 30 | try { 31 | const user = await auth.createUser({ 32 | userId: crypto.randomUUID(), 33 | key: { 34 | providerId: getProviderId('emailpass', event), 35 | providerUserId: form.data.email, 36 | password: form.data.password 37 | }, 38 | attributes: { 39 | role: 'user', 40 | first_name: form.data.firstName, 41 | last_name: form.data.lastName, 42 | email: form.data.email, 43 | domain: getDomain(event), 44 | email_verified: false 45 | } 46 | }); 47 | const token = await generateEmailVerificationToken(user.userId); 48 | await sendVerificationEmail(getBaseURL(event.url), form.data.email, token); 49 | const session = await auth.createSession({ 50 | userId: user.userId, 51 | attributes: {} 52 | }); 53 | event.locals.auth.setSession(session); 54 | } catch (e) { 55 | console.error(e); 56 | // email already in use 57 | //might be other type of error but this is most common and this is how lucia docs sets the error to duplicate user 58 | return setError(form, 'A user with that email already exists.'); 59 | } 60 | 61 | return { form }; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | {APP_NAME} 48 | 49 | 50 | {#if data?.user} 51 | 52 | {/if} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {#if $navigating || $loading} 61 |
64 | 65 |
66 | {/if} 67 |
68 | 69 |
70 |