├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .github └── dependabot.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── drizzle.config.ts ├── inlang.config.js ├── languages ├── de.json ├── en.json └── es.json ├── migrations ├── 0000_closed_golden_guardian.sql └── meta │ ├── 0000_snapshot.json │ └── _journal.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── src ├── app.d.ts ├── app.html ├── app.postcss ├── hooks.server.ts ├── lib │ ├── _helpers │ │ ├── convertNameToInitials.ts │ │ ├── getAllUrlParams.ts │ │ ├── parseMessage.ts │ │ └── parseTrack.ts │ ├── components │ │ ├── BottomBar.svelte │ │ ├── SignoutForm.svelte │ │ ├── footer.svelte │ │ ├── logo.svelte │ │ ├── navigation.svelte │ │ ├── sign-in.svelte │ │ └── sign-up.svelte │ ├── config │ │ ├── constants.ts │ │ ├── email-messages.ts │ │ └── zod-schemas.ts │ ├── server │ │ ├── db │ │ │ ├── client.ts │ │ │ └── schema.ts │ │ ├── email-send.ts │ │ ├── log.ts │ │ ├── lucia.ts │ │ └── tokens.ts │ ├── stores.ts │ └── utils │ │ └── string.ts ├── routes │ ├── (legal) │ │ ├── +layout@.svelte │ │ ├── privacy │ │ │ └── +page.svelte │ │ └── terms │ │ │ └── +page.svelte │ ├── (protected) │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── dashboard │ │ │ └── +page.svelte │ │ └── profile │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── +error.svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ ├── auth │ │ ├── +layout.svelte │ │ ├── email-verification │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── [token] │ │ │ │ └── +server.ts │ │ ├── password │ │ │ ├── reset │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ └── success │ │ │ │ │ └── +page.svelte │ │ │ └── update-[token] │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ └── success │ │ │ │ └── +page.svelte │ │ ├── sign-in │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── sign-out │ │ │ └── +page.server.ts │ │ └── sign-up │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ └── inlang │ │ └── [language].json │ │ └── +server.ts └── theme.postcss ├── static └── favicon.png ├── svelte.config.js ├── tailwind.config.ts ├── tsconfig.json └── vite.config.ts /.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= -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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* -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /migrations/0000_closed_golden_guardian.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "email_verification_token" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "user_id" uuid NOT NULL, 4 | "expires" bigint 5 | ); 6 | --> statement-breakpoint 7 | CREATE TABLE IF NOT EXISTS "auth_key" ( 8 | "id" varchar(255) PRIMARY KEY NOT NULL, 9 | "user_id" uuid NOT NULL, 10 | "hashed_password" varchar(255), 11 | "expires" bigint 12 | ); 13 | --> statement-breakpoint 14 | CREATE TABLE IF NOT EXISTS "password_reset_token" ( 15 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 16 | "user_id" uuid NOT NULL, 17 | "expires" bigint 18 | ); 19 | --> statement-breakpoint 20 | CREATE TABLE IF NOT EXISTS "auth_session" ( 21 | "id" varchar(128) PRIMARY KEY NOT NULL, 22 | "user_id" uuid NOT NULL, 23 | "active_expires" bigint NOT NULL, 24 | "idle_expires" bigint NOT NULL 25 | ); 26 | --> statement-breakpoint 27 | CREATE TABLE IF NOT EXISTS "auth_user" ( 28 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 29 | "role" text DEFAULT 'user' NOT NULL, 30 | "created_at" timestamp DEFAULT now() NOT NULL, 31 | "first_name" text, 32 | "last_name" text, 33 | "domain" text NOT NULL, 34 | "email" text NOT NULL, 35 | "email_verified" boolean DEFAULT false NOT NULL 36 | ); 37 | --> statement-breakpoint 38 | CREATE UNIQUE INDEX IF NOT EXISTS "email_domain_idx" ON "auth_user" ("email","domain");--> statement-breakpoint 39 | DO $$ BEGIN 40 | ALTER TABLE "email_verification_token" ADD CONSTRAINT "email_verification_token_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE no action ON UPDATE no action; 41 | EXCEPTION 42 | WHEN duplicate_object THEN null; 43 | END $$; 44 | --> statement-breakpoint 45 | DO $$ BEGIN 46 | ALTER TABLE "auth_key" ADD CONSTRAINT "auth_key_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE no action ON UPDATE no action; 47 | EXCEPTION 48 | WHEN duplicate_object THEN null; 49 | END $$; 50 | --> statement-breakpoint 51 | DO $$ BEGIN 52 | ALTER TABLE "password_reset_token" ADD CONSTRAINT "password_reset_token_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE no action ON UPDATE no action; 53 | EXCEPTION 54 | WHEN duplicate_object THEN null; 55 | END $$; 56 | --> statement-breakpoint 57 | DO $$ BEGIN 58 | ALTER TABLE "auth_session" ADD CONSTRAINT "auth_session_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE no action ON UPDATE no action; 59 | EXCEPTION 60 | WHEN duplicate_object THEN null; 61 | END $$; 62 | -------------------------------------------------------------------------------- /migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "id": "95f7d7d3-a7f7-43f0-bfa0-c73ad35d2c2c", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "email_verification_token": { 8 | "name": "email_verification_token", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "uuid", 14 | "primaryKey": true, 15 | "notNull": true, 16 | "default": "gen_random_uuid()" 17 | }, 18 | "user_id": { 19 | "name": "user_id", 20 | "type": "uuid", 21 | "primaryKey": false, 22 | "notNull": true 23 | }, 24 | "expires": { 25 | "name": "expires", 26 | "type": "bigint", 27 | "primaryKey": false, 28 | "notNull": false 29 | } 30 | }, 31 | "indexes": {}, 32 | "foreignKeys": { 33 | "email_verification_token_user_id_auth_user_id_fk": { 34 | "name": "email_verification_token_user_id_auth_user_id_fk", 35 | "tableFrom": "email_verification_token", 36 | "tableTo": "auth_user", 37 | "columnsFrom": [ 38 | "user_id" 39 | ], 40 | "columnsTo": [ 41 | "id" 42 | ], 43 | "onDelete": "no action", 44 | "onUpdate": "no action" 45 | } 46 | }, 47 | "compositePrimaryKeys": {}, 48 | "uniqueConstraints": {} 49 | }, 50 | "auth_key": { 51 | "name": "auth_key", 52 | "schema": "", 53 | "columns": { 54 | "id": { 55 | "name": "id", 56 | "type": "varchar(255)", 57 | "primaryKey": true, 58 | "notNull": true 59 | }, 60 | "user_id": { 61 | "name": "user_id", 62 | "type": "uuid", 63 | "primaryKey": false, 64 | "notNull": true 65 | }, 66 | "hashed_password": { 67 | "name": "hashed_password", 68 | "type": "varchar(255)", 69 | "primaryKey": false, 70 | "notNull": false 71 | }, 72 | "expires": { 73 | "name": "expires", 74 | "type": "bigint", 75 | "primaryKey": false, 76 | "notNull": false 77 | } 78 | }, 79 | "indexes": {}, 80 | "foreignKeys": { 81 | "auth_key_user_id_auth_user_id_fk": { 82 | "name": "auth_key_user_id_auth_user_id_fk", 83 | "tableFrom": "auth_key", 84 | "tableTo": "auth_user", 85 | "columnsFrom": [ 86 | "user_id" 87 | ], 88 | "columnsTo": [ 89 | "id" 90 | ], 91 | "onDelete": "no action", 92 | "onUpdate": "no action" 93 | } 94 | }, 95 | "compositePrimaryKeys": {}, 96 | "uniqueConstraints": {} 97 | }, 98 | "password_reset_token": { 99 | "name": "password_reset_token", 100 | "schema": "", 101 | "columns": { 102 | "id": { 103 | "name": "id", 104 | "type": "uuid", 105 | "primaryKey": true, 106 | "notNull": true, 107 | "default": "gen_random_uuid()" 108 | }, 109 | "user_id": { 110 | "name": "user_id", 111 | "type": "uuid", 112 | "primaryKey": false, 113 | "notNull": true 114 | }, 115 | "expires": { 116 | "name": "expires", 117 | "type": "bigint", 118 | "primaryKey": false, 119 | "notNull": false 120 | } 121 | }, 122 | "indexes": {}, 123 | "foreignKeys": { 124 | "password_reset_token_user_id_auth_user_id_fk": { 125 | "name": "password_reset_token_user_id_auth_user_id_fk", 126 | "tableFrom": "password_reset_token", 127 | "tableTo": "auth_user", 128 | "columnsFrom": [ 129 | "user_id" 130 | ], 131 | "columnsTo": [ 132 | "id" 133 | ], 134 | "onDelete": "no action", 135 | "onUpdate": "no action" 136 | } 137 | }, 138 | "compositePrimaryKeys": {}, 139 | "uniqueConstraints": {} 140 | }, 141 | "auth_session": { 142 | "name": "auth_session", 143 | "schema": "", 144 | "columns": { 145 | "id": { 146 | "name": "id", 147 | "type": "varchar(128)", 148 | "primaryKey": true, 149 | "notNull": true 150 | }, 151 | "user_id": { 152 | "name": "user_id", 153 | "type": "uuid", 154 | "primaryKey": false, 155 | "notNull": true 156 | }, 157 | "active_expires": { 158 | "name": "active_expires", 159 | "type": "bigint", 160 | "primaryKey": false, 161 | "notNull": true 162 | }, 163 | "idle_expires": { 164 | "name": "idle_expires", 165 | "type": "bigint", 166 | "primaryKey": false, 167 | "notNull": true 168 | } 169 | }, 170 | "indexes": {}, 171 | "foreignKeys": { 172 | "auth_session_user_id_auth_user_id_fk": { 173 | "name": "auth_session_user_id_auth_user_id_fk", 174 | "tableFrom": "auth_session", 175 | "tableTo": "auth_user", 176 | "columnsFrom": [ 177 | "user_id" 178 | ], 179 | "columnsTo": [ 180 | "id" 181 | ], 182 | "onDelete": "no action", 183 | "onUpdate": "no action" 184 | } 185 | }, 186 | "compositePrimaryKeys": {}, 187 | "uniqueConstraints": {} 188 | }, 189 | "auth_user": { 190 | "name": "auth_user", 191 | "schema": "", 192 | "columns": { 193 | "id": { 194 | "name": "id", 195 | "type": "uuid", 196 | "primaryKey": true, 197 | "notNull": true, 198 | "default": "gen_random_uuid()" 199 | }, 200 | "role": { 201 | "name": "role", 202 | "type": "text", 203 | "primaryKey": false, 204 | "notNull": true, 205 | "default": "'user'" 206 | }, 207 | "created_at": { 208 | "name": "created_at", 209 | "type": "timestamp", 210 | "primaryKey": false, 211 | "notNull": true, 212 | "default": "now()" 213 | }, 214 | "first_name": { 215 | "name": "first_name", 216 | "type": "text", 217 | "primaryKey": false, 218 | "notNull": false 219 | }, 220 | "last_name": { 221 | "name": "last_name", 222 | "type": "text", 223 | "primaryKey": false, 224 | "notNull": false 225 | }, 226 | "domain": { 227 | "name": "domain", 228 | "type": "text", 229 | "primaryKey": false, 230 | "notNull": true 231 | }, 232 | "email": { 233 | "name": "email", 234 | "type": "text", 235 | "primaryKey": false, 236 | "notNull": true 237 | }, 238 | "email_verified": { 239 | "name": "email_verified", 240 | "type": "boolean", 241 | "primaryKey": false, 242 | "notNull": true, 243 | "default": false 244 | } 245 | }, 246 | "indexes": { 247 | "email_domain_idx": { 248 | "name": "email_domain_idx", 249 | "columns": [ 250 | "email", 251 | "domain" 252 | ], 253 | "isUnique": true 254 | } 255 | }, 256 | "foreignKeys": {}, 257 | "compositePrimaryKeys": {}, 258 | "uniqueConstraints": {} 259 | } 260 | }, 261 | "enums": {}, 262 | "schemas": {}, 263 | "_meta": { 264 | "schemas": {}, 265 | "tables": {}, 266 | "columns": {} 267 | } 268 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /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/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/_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 | -------------------------------------------------------------------------------- /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/_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/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/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 |
-------------------------------------------------------------------------------- /src/lib/components/SignoutForm.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
27 | 35 |
-------------------------------------------------------------------------------- /src/lib/components/footer.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /src/lib/components/logo.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/components/navigation.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 53 | -------------------------------------------------------------------------------- /src/lib/components/sign-in.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | 24 | {#if $errors._errors} 25 | 34 | {/if} 35 |
36 | 53 |
54 | 55 |
56 | 72 |
73 | 74 |
75 | 78 |
79 | 82 |
83 | -------------------------------------------------------------------------------- /src/lib/components/sign-up.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 | 34 |
35 | 52 |
53 |
54 | 71 |
72 |
73 | 90 |
91 | 92 |
93 | 109 |
110 |
111 | 123 |
124 |
125 | 128 |
129 |
130 | -------------------------------------------------------------------------------- /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/lib/config/email-messages.ts: -------------------------------------------------------------------------------- 1 | import sendEmail from '$lib/server/email-send'; 2 | import { APP_NAME } from '$lib/config/constants'; 3 | 4 | // Send an email to verify the user's address 5 | export const sendVerificationEmail = async (baseUrl: string, email: string, token: string) => { 6 | const verifyEmailURL = `${baseUrl}/auth/email-verification/${token}`; 7 | const textEmail = `Please visit the link below to verify your email address for your ${APP_NAME} account.\n\n 8 | ${verifyEmailURL} \n\nIf you did not create this account, you can disregard this email.`; 9 | const htmlEmail = `

Please click this link to verify your email address for your ${APP_NAME} account.

You can also visit the link below.

${verifyEmailURL}

If you did not create this account, you can disregard this email.

`; 10 | const subject = `Please confirm your email address for ${APP_NAME}`; 11 | const resultSend = await sendEmail(email, subject, htmlEmail, textEmail); 12 | return resultSend; 13 | }; 14 | 15 | // Send an email to welcome the new user 16 | export const sendWelcomeEmail = async (baseUrl: string, email: string) => { 17 | const textEmail = `Thanks for verifying your account with ${APP_NAME}.\nYou can now sign in to your account at the link below.\n\n${baseUrl}/auth/sign-in`; 18 | const htmlEmail = `

Thanks for verifying your account with ${APP_NAME}.

You can now sign in to your account.

`; 19 | const subject = `Welcome to ${APP_NAME}`; 20 | const resultSend = await sendEmail(email, subject, htmlEmail, textEmail); 21 | return resultSend; 22 | }; 23 | 24 | // Send an email to reset the user's password 25 | export const sendPasswordResetEmail = async (baseUrl: string, email: string, token: string) => { 26 | const updatePasswordURL = `${baseUrl}/auth/password/update-${token}`; 27 | const textEmail = `Please visit the link below to change your password for ${APP_NAME}.\n\n 28 | ${updatePasswordURL} \n\nIf you did not request to change your password, you can disregard this email.`; 29 | const htmlEmail = `

Please click this link to change your password for ${APP_NAME}.

30 |

You can also visit the link below.

${updatePasswordURL}

If you did not request to change your password, you can disregard this email.

`; 31 | const subject = `Change your password for ${APP_NAME}`; 32 | const resultSend = await sendEmail(email, subject, htmlEmail, textEmail); 33 | return resultSend; 34 | }; 35 | 36 | // Send an email to confirm the user's password reset 37 | // and also send an email to the user's old email account in case of a hijack attempt 38 | export const updateEmailAddressSuccessEmail = async ( 39 | baseUrl: string, 40 | email: string, 41 | oldEmail: string, 42 | token: string 43 | ) => { 44 | const verifyEmailURL = `${baseUrl}/auth/email-verification/${token}`; 45 | const textEmail = `Please visit the link below to verify your email address for your ${APP_NAME} account.\n\n ${verifyEmailURL}`; 46 | const htmlEmail = `

Please click this link to verify your email address for your ${APP_NAME} account.

You can also visit the link below.

${verifyEmailURL}

`; 47 | const subject = `Please confirm your email address for ${APP_NAME}`; 48 | await sendEmail(email, subject, htmlEmail, textEmail); 49 | 50 | //send email to user about email change. 51 | const textEmailChange = `Your ${APP_NAME} account email has been updated from ${oldEmail} to ${email}. If you DID NOT request this change, please contact support at: ${baseUrl} to revert the changes.`; 52 | const htmlEmailChange = `

Your ${APP_NAME} account email has been updated from ${oldEmail} to ${email}.

If you DID NOT request this change, please contact support at: ${baseUrl} to revert the changes.

`; 53 | const subjectChange = `Your email address for ${APP_NAME} has changed.`; 54 | await sendEmail(oldEmail, subjectChange, htmlEmailChange, textEmailChange); 55 | }; 56 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /src/lib/server/tokens.ts: -------------------------------------------------------------------------------- 1 | import { db } from '$lib/server/db/client'; 2 | import * as tables from '$lib/server/db/schema'; 3 | import { eq } from 'drizzle-orm'; 4 | import { isWithinExpiration } from 'lucia/utils'; 5 | 6 | const EMAIL_VERIFICATION_TOKEN_EXPIRES_IN = 1000 * 60 * 60 * 2; // 2 hours 7 | 8 | export const generateEmailVerificationToken = async (userId: string) => { 9 | const storedUserTokens = await db 10 | .select() 11 | .from(tables.emailVerificationTokens) 12 | .where(eq(tables.emailVerificationTokens.userId, userId)); 13 | if (storedUserTokens.length > 0) { 14 | const reusableStoredToken = storedUserTokens.find((token) => { 15 | // check if expiration is within 1 hour 16 | // and reuse the token if true 17 | return isWithinExpiration(Number(token.expires) - EMAIL_VERIFICATION_TOKEN_EXPIRES_IN / 2); 18 | }); 19 | if (reusableStoredToken) return reusableStoredToken.id; 20 | } 21 | const tokens = await db 22 | .insert(tables.emailVerificationTokens) 23 | .values({ 24 | expires: new Date().getTime() + EMAIL_VERIFICATION_TOKEN_EXPIRES_IN, 25 | userId 26 | }) 27 | .returning({ id: tables.emailVerificationTokens.id }); 28 | return tokens[0].id; 29 | }; 30 | 31 | export const validateEmailVerificationToken = async (token: string) => { 32 | const storedTokens = await db 33 | .select() 34 | .from(tables.emailVerificationTokens) 35 | .where(eq(tables.emailVerificationTokens.id, token)); 36 | if (!storedTokens.length) return null; 37 | const storedToken = storedTokens[0]; 38 | const tokenExpires = Number(storedToken.expires); 39 | if (!isWithinExpiration(tokenExpires)) return null; 40 | // we can invalidate all tokens since a user only verifies their email once 41 | await db 42 | .delete(tables.emailVerificationTokens) 43 | .where(eq(tables.emailVerificationTokens.userId, storedToken.userId)); 44 | return storedToken.userId; 45 | }; 46 | 47 | const PASSWORD_RESET_TOKEN_EXPIRES_IN = 1000 * 60 * 60 * 2; // 2 hours 48 | 49 | export const generatePasswordResetToken = async (userId: string) => { 50 | const storedUserTokens = await db 51 | .select() 52 | .from(tables.passwordResetTokens) 53 | .where(eq(tables.passwordResetTokens.userId, userId)); 54 | 55 | if (storedUserTokens.length > 0) { 56 | const reusableStoredToken = storedUserTokens.find((token) => { 57 | // check if expiration is within 1 hour 58 | // and reuse the token if true 59 | return isWithinExpiration(Number(token.expires) - PASSWORD_RESET_TOKEN_EXPIRES_IN / 2); 60 | }); 61 | if (reusableStoredToken) return reusableStoredToken.id; 62 | } 63 | const tokens = await db 64 | .insert(tables.passwordResetTokens) 65 | .values({ 66 | expires: new Date().getTime() + PASSWORD_RESET_TOKEN_EXPIRES_IN, 67 | userId 68 | }) 69 | .returning({ id: tables.passwordResetTokens.id }); 70 | return tokens[0].id; 71 | }; 72 | 73 | export const validatePasswordResetToken = async (token: string) => { 74 | const storedTokens = await db 75 | .select() 76 | .from(tables.passwordResetTokens) 77 | .where(eq(tables.passwordResetTokens.id, token)); 78 | if (!storedTokens.length) return null; 79 | const storedToken = storedTokens[0]; 80 | const tokenExpires = Number(storedToken.expires); 81 | if (!isWithinExpiration(tokenExpires)) return null; 82 | // invalidate all user password reset tokens 83 | await db 84 | .delete(tables.passwordResetTokens) 85 | .where(eq(tables.passwordResetTokens.userId, storedToken.userId)); 86 | return storedToken.userId; 87 | }; 88 | 89 | export const isValidPasswordResetToken = async (token: string) => { 90 | const storedTokens = await db 91 | .select() 92 | .from(tables.passwordResetTokens) 93 | .where(eq(tables.passwordResetTokens.id, token)); 94 | if (!storedTokens.length) return false; 95 | const storedToken = storedTokens[0]; 96 | const tokenExpires = Number(storedToken.expires); 97 | if (!isWithinExpiration(tokenExpires)) return false; 98 | return true; 99 | }; 100 | -------------------------------------------------------------------------------- /src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const loading = writable(false); -------------------------------------------------------------------------------- /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/(legal)/+layout@.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 20 | -------------------------------------------------------------------------------- /src/routes/(legal)/privacy/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |

Privacy Policy for {TERMS_PRIVACY_WEBSITE}

11 | 12 |

Effective Date: {TERMS_PRIVACY_EFFECTIVE_DATE}

13 | 14 |

15 | This privacy policy (the “Policy”) sets out the privacy policies and practices for {TERMS_PRIVACY_COMPANY} 16 | and its subsidiaries and affiliates (collectively, “we”, “us”, “our”) with respect to how we collect 17 | personal information. It also describes how we maintain, use, and disclose personal information. 18 |

19 | 20 |

Information We Collect

21 | 22 |

23 | We collect and store information that you voluntarily provide to us as well as data related to 24 | your website visit and usage. 25 |

26 |

27 | We collect personally identifiable information (including, but not limited to, name, address and 28 | phone number) that is voluntarily provided to us by you. For example, you voluntarily provide 29 | personally identifiable information when you send us an email, use certain features of the website 30 | like the contact us form, or register with our site. 31 |

32 |

33 | In addition, during your visit we automatically collect certain aggregate information related to 34 | your website visit. Aggregate information is non-personally identifiable or anonymous information 35 | about you, including the date and time of your visit, your IP address, your computer browser 36 | information, the Internet address that you visited prior to and after reaching our site, the name 37 | of the domain and host you used to access the Internet, and the features of our site which you 38 | accessed. 39 |

40 |

Use of Information

41 | 42 |

43 | We use this information in order to serve the needs of our customers. We may use your information 44 | to meet your requests for our products, programs, and services, to respond to your inquiries about 45 | our offerings, to offer you other products or services that we believe may be of interest to you, 46 | to enforce the legal terms that govern your use of our site, and/or for the purposes for which you 47 | provided the information. 48 |

49 |

Information Sharing and Disclosure

50 | 51 |

52 | {TERMS_PRIVACY_COMPANY} does not sell or rent your personally identifiable information to anyone. We 53 | only disclose personally identifiable information about our users when we believe, in good faith, that 54 | either the law requires it, to protect the rights or property of {TERMS_PRIVACY_COMPANY}, or we 55 | must to provide you with the services or products you requested. 56 |

57 |

Cookies

58 | 59 |

60 | We may use cookies to manage our users’ sessions and to store preferences, tracking information, 61 | and language selection. Cookies may be used whether you register with us or not. 62 |

63 |

Security

64 | 65 |

66 | We employ reasonable and current security methods to prevent unauthorized access, maintain data 67 | accuracy, and ensure correct use of information. 68 |

69 |

Your Ability to Edit and Delete Your Account Information and Preferences

70 | 71 |

72 | You may request deletion of your email address by sending an e-mail to {TERMS_PRIVACY_CONTACT_EMAIL}. 73 | However, please note that your identification, billing and contact information will remain on our 74 | records for some period. 75 |

76 |

Privacy Policy Updates

77 | 78 |

79 | We may update this policy from time to time. If we make significant changes, we will notify you of 80 | the changes through our website or through others means, such as email. 81 |

82 |

How to Contact Us

83 |

84 | If you have any questions about this privacy policy, please contact us at {TERMS_PRIVACY_CONTACT_EMAIL}. 85 |

86 | -------------------------------------------------------------------------------- /src/routes/(legal)/terms/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |

Terms and Conditions for {TERMS_PRIVACY_APP_NAME}

13 | 14 |

Effective Date: {TERMS_PRIVACY_EFFECTIVE_DATE}

15 | 16 |

Acceptance of Terms

17 | 18 | By accessing and using {TERMS_PRIVACY_APP_NAME} ("Service"), you accept and agree to be bound by the 19 | terms and provision of this agreement. 20 | 21 |

Changes to Terms

22 | 23 | We reserve the right, at our sole discretion, to amend these Terms of Service at any time and will 24 | update these Terms of Service in the event of any such amendments. 25 | 26 |

Account and Access

27 | 28 | To access and use our Service, you may need to register with us and set up an account with your 29 | email address and a password. You are solely responsible for maintaining the confidentiality of your 30 | account and password and for all activities associated with or occurring under your account. 31 | 32 |

Your Responsibilities

33 | 34 | You are responsible for all content you upload, post, email or otherwise transmit via the Service. 35 | You agree to comply with all laws and regulations applicable to your use of the Service. 36 | 37 |

Payment and Fees

38 | 39 | {TERMS_PRIVACY_APP_PRICING_AND_SUBSCRIPTIONS} 40 | 41 |

Intellectual Property

42 | 43 | The Service and its original content, features and functionality are and will remain the exclusive 44 | property of {TERMS_PRIVACY_COMPANY}. The Service is protected by copyright, trademark, and other 45 | laws of both the {TERMS_PRIVACY_COUNTRY} and foreign countries. 46 | 47 |

Termination

48 | 49 | We may terminate or suspend your access to the Service immediately, without prior notice or 50 | liability, under our sole discretion, for any reason whatsoever and without limitation, including 51 | but not limited to a breach of the Terms. 52 | 53 |

Limitation Of Liability

54 | 55 | In no event shall {TERMS_PRIVACY_COMPANY}, nor its directors, employees, partners, agents, 56 | suppliers, or affiliates, be liable for any indirect, incidental, special, consequential or punitive 57 | damages, including without limitation, loss of profits, data, use, goodwill, or other intangible 58 | losses, resulting from your access to or use of or inability to access or use the Service. 59 | 60 |

Governing Law

61 | 62 | These Terms shall be governed and construed in accordance with the laws of {TERMS_PRIVACY_COUNTRY}, 63 | without regard to its conflict of law provisions. 64 | 65 |

Contact Us

66 | 67 | If you have any questions about these Terms, please contact us at {TERMS_PRIVACY_CONTACT_EMAIL}. 68 | -------------------------------------------------------------------------------- /src/routes/(protected)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | export const load = async (event: { locals: { user: any } }) => { 2 | return { user: event.locals.user }; 3 | }; 4 | -------------------------------------------------------------------------------- /src/routes/(protected)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/routes/(protected)/profile/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { fail } from '@sveltejs/kit'; 2 | import { setError, superValidate, message } from 'sveltekit-superforms/server'; 3 | import { auth } from '$lib/server/lucia'; 4 | import { userSchema } from '$lib/config/zod-schemas'; 5 | import { updateEmailAddressSuccessEmail } from '$lib/config/email-messages'; 6 | import { db } from '$lib/server/db/client'; 7 | import * as table from '$lib/server/db/schema'; 8 | import { eq } from 'drizzle-orm'; 9 | 10 | const profileSchema = userSchema.pick({ 11 | firstName: true, 12 | lastName: true, 13 | email: true 14 | }); 15 | 16 | export const load = async (event) => { 17 | const form = await superValidate(event, profileSchema); 18 | const session = await event.locals.auth.validate(); 19 | const user = session?.user; 20 | form.data = { 21 | firstName: user.firstName, 22 | lastName: user.lastName, 23 | email: user.email 24 | }; 25 | return { 26 | form 27 | }; 28 | }; 29 | 30 | export const actions = { 31 | default: async (event) => { 32 | const form = await superValidate(event, profileSchema); 33 | //console.log(form); 34 | 35 | if (!form.valid) { 36 | return fail(400, { 37 | form 38 | }); 39 | } 40 | 41 | //add user to db 42 | try { 43 | console.log('updating profile'); 44 | const session = await event.locals.auth.validate(); 45 | const user = session?.user; 46 | auth.updateUserAttributes(user.userId, { 47 | first_name: form.data.firstName, 48 | last_name: form.data.lastName, 49 | email: form.data.email 50 | }); 51 | //await auth.invalidateAllUserSessions(user.userId); 52 | 53 | if (user.email !== form.data.email) { 54 | //TODO: get emailaddress to change for orm not just in attributes. setUser not working... weird 55 | // worse comes to worse, update the auth_key manually in the db 56 | //auth.setKey(user.userId, 'emailpassword', form.data.email); 57 | //auth.setUser(user.userId, 'email', form.data.email); 58 | //remove this once bug is fixed and setKey or setUser works 59 | //https://github.com/pilcrowOnPaper/lucia/issues/606 60 | console.log('user: ' + JSON.stringify(user)); 61 | await db.update(table.keys).set({ 62 | id: 'emailpassword:' + form.data.email 63 | }).where(eq(table.users.id, 'emailpassword:' + user.email)) 64 | auth.updateUserAttributes(user.userId, { 65 | verified: false 66 | }); 67 | //await auth.invalidateAllUserSessions(user.userId); 68 | await updateEmailAddressSuccessEmail(form.data.email, user.email, user.token); 69 | } 70 | } catch (e) { 71 | console.error(e); 72 | return setError(form, null, 'There was a problem updating your profile.'); 73 | } 74 | console.log('profile updated successfully'); 75 | return message(form, 'Profile updated successfully.'); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/routes/(protected)/profile/+page.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 | 30 |

Profile

31 |
32 | {#if $message} 33 | 39 | {/if} 40 | {#if $errors._errors} 41 | 50 | {/if} 51 |
52 | 69 |
70 |
71 | 88 |
89 |
90 | 107 |
108 |
109 | Change Password 110 |
111 | 112 |
113 | 116 |
117 |
118 | -------------------------------------------------------------------------------- /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/+layout.server.ts: -------------------------------------------------------------------------------- 1 | export const load = async (event: { locals: { user: any } }) => { 2 | return { user: event.locals.user }; 3 | }; 4 | -------------------------------------------------------------------------------- /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 |