├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── inlang.config.js ├── languages ├── de.json ├── en.json └── es.json ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── prisma └── schema.prisma ├── sample.env ├── src ├── app.d.ts ├── app.html ├── app.postcss ├── hooks.server.ts ├── lib │ ├── _helpers │ │ ├── convertNameToInitials.ts │ │ ├── getAllUrlParams.ts │ │ ├── parseMessage.ts │ │ └── parseTrack.ts │ ├── components │ │ ├── footer.svelte │ │ ├── logo.svelte │ │ ├── navigation.svelte │ │ ├── sign-in.svelte │ │ └── sign-up.svelte │ ├── config │ │ ├── constants.ts │ │ ├── email-messages.ts │ │ ├── prisma.ts │ │ └── zod-schemas.ts │ └── server │ │ ├── email-send.ts │ │ ├── log.ts │ │ └── lucia.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 │ │ ├── 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 │ │ └── verify │ │ │ ├── email-[token] │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ │ ├── email │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ │ └── resend-email-[email] │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ └── inlang │ │ └── [language].json │ │ └── +server.ts └── theme.postcss ├── static └── favicon.png ├── svelte.config.js ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.ts /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.documentSelectors": ["**/*.svelte"], 3 | "tailwindCSS.classAttributes": [ 4 | "class", 5 | "accent", 6 | "active", 7 | "background", 8 | "badge", 9 | "bgBackdrop", 10 | "bgDark", 11 | "bgDrawer", 12 | "bgLight", 13 | "blur", 14 | "border", 15 | "button", 16 | "buttonAction", 17 | "buttonBack", 18 | "buttonClasses", 19 | "buttonComplete", 20 | "buttonDismiss", 21 | "buttonNeutral", 22 | "buttonNext", 23 | "buttonPositive", 24 | "buttonTextCancel", 25 | "buttonTextConfirm", 26 | "buttonTextNext", 27 | "buttonTextPrevious", 28 | "buttonTextSubmit", 29 | "caretClosed", 30 | "caretOpen", 31 | "chips", 32 | "color", 33 | "cursor", 34 | "display", 35 | "element", 36 | "fill", 37 | "fillDark", 38 | "fillLight", 39 | "flex", 40 | "gap", 41 | "gridColumns", 42 | "height", 43 | "hover", 44 | "invalid", 45 | "justify", 46 | "meter", 47 | "padding", 48 | "position", 49 | "regionBackdrop", 50 | "regionBody", 51 | "regionCaption", 52 | "regionCaret", 53 | "regionCell", 54 | "regionCone", 55 | "regionContent", 56 | "regionControl", 57 | "regionDefault", 58 | "regionDrawer", 59 | "regionFoot", 60 | "regionFooter", 61 | "regionHead", 62 | "regionHeader", 63 | "regionIcon", 64 | "regionInterface", 65 | "regionInterfaceText", 66 | "regionLabel", 67 | "regionLead", 68 | "regionLegend", 69 | "regionList", 70 | "regionNavigation", 71 | "regionPage", 72 | "regionPanel", 73 | "regionRowHeadline", 74 | "regionRowMain", 75 | "regionTrail", 76 | "ring", 77 | "rounded", 78 | "select", 79 | "shadow", 80 | "slotDefault", 81 | "slotFooter", 82 | "slotHeader", 83 | "slotLead", 84 | "slotMessage", 85 | "slotMeta", 86 | "slotPageContent", 87 | "slotPageFooter", 88 | "slotPageHeader", 89 | "slotSidebarLeft", 90 | "slotSidebarRight", 91 | "slotTrail", 92 | "spacing", 93 | "text", 94 | "track", 95 | "width" 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jeff McMorris 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 | # Sveltekit Auth Starter 2 | 3 | ![Sveltekit Auth User Interface](https://miro.medium.com/v2/resize:fit:4772/1*v-oJRLXc299bFOzDS-fnyA.png) 4 | 5 | ## IMPORTANT UPDATE. I recently switched UI components to [shadcn-svelte](https://www.shadcn-svelte.com/). I made a new [starter project here](https://github.com/delay/sveltekit-auth). Any new features will be added to that project. I will still accept bug fixes for this project. 6 | 7 | This is a Sveltekit Auth Starter Project. An example website is currently deployed [here](https://sveltekit-auth.uv-ray.com/). It is an open source auth starter project utilizing [Lucia](https://lucia-auth.com/) for authentication, [Skeleton](https://www.skeleton.dev) for ui elements, [Lucide](https://lucide.dev) for icons, [Prisma](https://www.prisma.io) for database connectivity and type safety, [inlang](https://inlang.com) for language translation and [Sveltekit](https://kit.svelte.dev) for the javascript framework. I also used [Zod](https://zod.dev) and [Superforms](https://superforms.vercel.app) to handle form validation and management. It has email verification, password reset, and will send an email if the user changes their email address to re-verify it. It also has a custom error logging system which I wrote about in [this blog post](https://jeffmcmorris.medium.com/awesome-logging-in-sveltekit-6afa29c5892c). The log results are sent to [Axiom](https://axiom.co). It is released as open source under an MIT license. 8 | 9 | While creating this project, I made use of several great videos and tutorials from [Huntabyte](https://www.youtube.com/@huntabyte) and [Joy of Code](https://www.youtube.com/@JoyofCodeDev). Both have great tutorials for all things related to Sveltekit. 10 | 11 | This project creates an email and password user log in system and is my attempt to make something production ready with all of the usual features you expect. You can create new users and sign them in. It has user roles. It will verify a users email address. You can edit your profile including changing your email address and password. You can reset your password in case you forgot it. You can also change themes and have a light and dark mode. I didn’t see any examples utilizing these frameworks that had all of these necessary features. 12 | 13 | I picked [Lucia](https://lucia-auth.com/) for auth because it had great documentation and seemed to be in active development and was very full featured. It can provide authentication for OAuth providers as well. I always want to have a fallback for email and password, so that is what I chose to make for this project. 14 | 15 | [Skeleton](https://www.skeleton.dev) is another great project with a really nice development experience. Perhaps the coolest feature is it makes use of [design tokens](https://www.skeleton.dev/docs/tokens). This allows for lots of customization just by modifying the [theme.css](https://www.skeleton.dev/docs/generator). 16 | 17 | ![skeleton themes](https://miro.medium.com/v2/resize:fit:1400/1*qo_ZLFxpaijwvOK_7vbyFw.png) 18 | 19 | [Prisma](https://www.prisma.io) is another great package and it is used for database connectivity and type safety. It works with many databases so it’s easy to change your database with one line of code. It has an easy to use ORM that cuts back on the amount of code you need to write. 20 | 21 | [Zod](https://zod.dev) is a typescript schema validation that allows you to easily validate your input in projects. It is very easy to setup what your data should look like to validate against. 22 | 23 | Finally [Superforms](https://superforms.vercel.app/) makes it easy to work with forms in Sveltekit. It cuts down a lot on boilerplate code when working with forms. 24 | 25 | This was the first time working with many of these packages, but they really do streamline much of the Sveltekit development process. If there are any mistakes, please open up an issue on the project. Also I was pleasantly surprised at the scores from [Google PageSpeed Insights](https://pagespeed.web.dev). This project scored a 100% in all metrics. 26 | 27 | ![pagespeed insights metrics](https://miro.medium.com/v2/resize:fit:1400/1*SlpRE8RL3KIJW8af7P8otw.png) 28 | 29 | ## File Structure for the App 30 | 31 | **sample.env** — private environmental server side variables that must be set. Rename to.env and supply your personal project settings. 32 | 33 | **/prisma/schema.prisma** — holds the prism schema which is the design of your data in the app and db. Currently holds the auth schema for Lucia auth. 34 | 35 | ## **/src/** 36 | 37 | **app.d.ts** — holds type definitions for lucia and can hold your additional types for other features. 38 | 39 | **hooks.server.ts** — holds a Lucia auth handle function. 40 | 41 | **theme.postcss** — holds a custom theme for skeleton. This can be set in /routes/+layout.svelte. Comment out the theme-skeleton and add in theme.postcss. You can create your own custom theme [https://www.skeleton.dev/docs/generator](https://www.skeleton.dev/docs/generator). There are also lots of premade themes included with sveltekit. To use those, change theme-skeleton.css to theme-modern.css or another theme name. 42 | 43 | ## /lib 44 | 45 | ## /\_helpers 46 | 47 | **convertNameToInitials.ts** — function for making initials from first and last name of user for the avatar. 48 | 49 | **getAllUrlParams.ts** - puts any url parameters in an object to be used in our log system. 50 | **parseMessage** - puts event.locals.message message into object or string and for our log. 51 | **parseTrack** - puts event.locals.track message into object or string for our log. 52 | 53 | ## **/components** 54 | 55 | **footer.svelte** — footer in the app, used in /routes/+layout.svelte 56 | 57 | **logo.svelte** — used as the logo throughout the app. 58 | 59 | **navigation.svelte** — navigation menu links used in /routes/+layout.svelte. They change based on whether user is logged in or not. 60 | 61 | **sign-in.svelte** — sign in form component used in /auth/sign-in/+page.svelte 62 | 63 | **sign-up.svelte** — sign up form component used in /auth/sign-up/+page.svelte 64 | 65 | ## /config 66 | 67 | **constants.ts** — all of the public constants that do not need to be hidden server side. I prefer this to naming constants PUBLIC_WHATEVER in the .env file, which is another option. I prefer to keep my .env file with only server side env variables. 68 | 69 | **email-messages.ts** — this is where I keep all of the email messages that need to be sent. It makes it easier in case changes need to be made to the emails that are sent out. 70 | 71 | **prima.ts —** file used to initialize the prisma client. 72 | 73 | **zod-schema.ts** — holds the schema used in zod. This defines how our form data needs to be validated. 74 | 75 | ## /server 76 | 77 | **email-send.ts** — this handles our email sending with AWS SES. It only runs server side to keep your credentials hidden. It also allows you to use SMTP settings in case you are not using AWS through nodemailer. 78 | 79 | **lucia.ts**- this initializes the lucia-auth package for handling our auth functions. It also holds the extra custom fields we added to the user. 80 | 81 | **log.ts** - This is used by our hook to get the log data and send to our log service. 82 | 83 | ## /routes 84 | 85 | **+layout.server.ts** — gets the user info from lucia-auth if available so we can access it in our app. 86 | 87 | **+layout.svelte** — overall site layout which is inherited by the rest of our app. 88 | 89 | **+page.svelte** - basic info about our app 90 | 91 | **+error.svelte** - custom error page. 92 | 93 | ## /auth 94 | 95 | **+layout.svelte**\-handles our layout for the auth section. 96 | 97 | ## /legal 98 | 99 | **+layout@.svelte** — resets our layout for the legal section so it doesn’t inherit the auth layout. add the @ at the end to do this. 100 | 101 | ## /legal/terms 102 | 103 | Holds our terms and conditions page. Do not use this for your own website as I just used ChatGPT to make this. You should consult a legal professional to develop the terms for your own app. 104 | 105 | ## /legal/privacy 106 | 107 | Holds our privacy policy page. Do not use this for your own website as I just used ChatGPT to make this. You should consult a legal professional to develop the privacy policy for your own app. 108 | 109 | ## /password/reset 110 | 111 | This holds the password reset form and function to send a password reset email when the user enters there email address, 112 | 113 | ## /password/update-\[token\] 114 | 115 | This allows the user to actually put in the new password, the token comes from the email from the users reset request. Anything in \[\] is able to be accessed as a parameter in Sveltekit, so \[token\] can be accessed via (token = event.params.token). 116 | 117 | ## /password/update-\[token\]/success 118 | 119 | This is the message the user sees if there reset was successful. 120 | 121 | ## /profile 122 | 123 | This allows the user to update their profile with new information. If they change their email address we also un-verify them and send them an email asking them to reconfirm their email. We also send an email to their old address telling them this change was made with the old and new address so that the data can be reset manually if the users account was hacked. 124 | 125 | ## /sign-in 126 | 127 | Page and functions for signing in the user. 128 | 129 | ## /sign-out 130 | 131 | Function for signing out the user. 132 | 133 | ## /sign-up 134 | 135 | Page and functions for signing up the user. 136 | 137 | ## /verify/email 138 | 139 | This page asks user to check there email and verify it. 140 | 141 | ## /verify/email-\[token\] 142 | 143 | This page confirms the email address by verifying the token the user received in his email account. 144 | 145 | ## /verify/resend-email-\[token\] 146 | 147 | This resends the verify email token email in case the user didn’t receive or lost the email. 148 | 149 | ## /(protected) 150 | 151 | This route group is only allowed to be accessed when a user is logged in. 152 | 153 | Hopefully you may find some of this code useful for your own project. Please feel free to use it in any project. 154 | -------------------------------------------------------------------------------- /inlang.config.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @type { import("@inlang/core/config").DefineConfig } 4 | */ 5 | export async function defineConfig(env) { 6 | const { default: jsonPlugin } = await env.$import( 7 | "https://cdn.jsdelivr.net/gh/samuelstroschein/inlang-plugin-json@2/dist/index.js" 8 | ) 9 | const { default: sdkPlugin } = await env.$import( 10 | "https://cdn.jsdelivr.net/npm/@inlang/sdk-js-plugin@0.11.8/dist/index.js" 11 | ) 12 | 13 | return { 14 | referenceLanguage: "en", 15 | plugins: [ 16 | jsonPlugin({ 17 | pathPattern: "./languages/{language}.json", 18 | }), 19 | sdkPlugin({ 20 | languageNegotiation: { 21 | strategies: [{ type: "localStorage" }] 22 | } 23 | }), 24 | ], 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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 | "protected": "Geschützt", 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 | "protected": "Protected", 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 | "protected": "Protegido", 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 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-auth-start", 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 | }, 14 | "devDependencies": { 15 | "@inlang/core": "^0.9.2", 16 | "@inlang/sdk-js": "^0.11.8", 17 | "@skeletonlabs/skeleton": "^1.2.5", 18 | "@sveltejs/adapter-auto": "^2.0.0", 19 | "@sveltejs/kit": "^1.20.4", 20 | "@tailwindcss/forms": "^0.5.3", 21 | "@tailwindcss/typography": "^0.5.9", 22 | "@types/nodemailer": "^6.4.8", 23 | "@typescript-eslint/eslint-plugin": "^5.45.0", 24 | "@typescript-eslint/parser": "^5.45.0", 25 | "autoprefixer": "^10.4.14", 26 | "eslint": "^8.28.0", 27 | "eslint-config-prettier": "^9.1.0", 28 | "eslint-plugin-svelte": "^2.35.1", 29 | "postcss": "^8.4.23", 30 | "prettier": "^2.1.1", 31 | "prettier-plugin-svelte": "^2.10.1", 32 | "prisma": "^5.7.0", 33 | "svelte": "^4.0.0", 34 | "svelte-check": "^3.4.3", 35 | "sveltekit-superforms": "^1.11.0", 36 | "tailwindcss": "^3.3.2", 37 | "tslib": "^2.4.1", 38 | "typescript": "^5.0.0", 39 | "vite": "^4.3.0", 40 | "zod": "^3.22.4" 41 | }, 42 | "type": "module", 43 | "dependencies": { 44 | "@aws-sdk/client-ses": "^3.328.0", 45 | "@axiomhq/js": "^1.0.0-rc.1", 46 | "@lucia-auth/adapter-prisma": "^3.0.2", 47 | "@prisma/client": "^5.7.0", 48 | "@sentry/node": "^7.51.0", 49 | "@sentry/svelte": "^7.51.0", 50 | "@sveltejs/adapter-node": "^1.2.4", 51 | "lucia-auth": "^1.5.0", 52 | "lucide-svelte": "^0.294.0", 53 | "nodemailer": "^6.9.2" 54 | } 55 | } -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | datasource db { 4 | provider = "postgresql" 5 | url = env("PRISMA_URL") 6 | } 7 | 8 | generator client { 9 | provider = "prisma-client-js" 10 | } 11 | 12 | model AuthUser { 13 | id String @id @unique 14 | email String @unique 15 | firstName String 16 | lastName String 17 | role Role @default(USER) 18 | verified Boolean @default(false) 19 | receiveEmail Boolean @default(true) 20 | token String? @unique 21 | createdAt DateTime @default(now()) @db.Timestamp(6) 22 | updatedAt DateTime @updatedAt @db.Timestamp(6) 23 | auth_session AuthSession[] 24 | auth_key AuthKey[] 25 | 26 | @@map("auth_user") 27 | } 28 | 29 | model AuthSession { 30 | id String @id @unique 31 | user_id String 32 | active_expires BigInt 33 | idle_expires BigInt 34 | auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) 35 | 36 | @@index([user_id]) 37 | @@map("auth_session") 38 | } 39 | 40 | model AuthKey { 41 | id String @id @unique 42 | hashed_password String? 43 | user_id String 44 | auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) 45 | 46 | @@index([user_id]) 47 | @@map("auth_key") 48 | } 49 | 50 | enum Role { 51 | USER 52 | PREMIUM 53 | ADMIN 54 | } -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | # General 2 | # I used postgresql for the PRISMA_URL for this project 3 | # but you should be able to use any DB prisma supports. 4 | # https://www.prisma.io/docs/reference/database-reference/supported-databases 5 | PRISMA_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE" 6 | SVELTEKIT_PORT=3000 7 | 8 | # Email 9 | FROM_EMAIL = 'Your email name ' 10 | # use blank values in AWS variables if you want to use SMTP 11 | #AWS SES KEYS 12 | AWS_ACCESS_KEY_ID= your_aws_access_key_id 13 | AWS_SECRET_ACCESS_KEY= your_aws_secret_access_key 14 | AWS_REGION= your-region # us-east-1 15 | AWS_API_VERSION= your-api-version # 2010-12-01 16 | # if AWS SES not set the SMTP will be a fallback 17 | SMTP_HOST=localhost 18 | SMTP_PORT=1025 19 | SMTP_SECURE=0 # use 1 for secure 20 | SMTP_USER=your-smtp-username 21 | SMTP_PASS=your-smtp-password 22 | 23 | # Logging 24 | # Clear these to fallback to console.log 25 | AXIOM_TOKEN=your-axiom-token 26 | AXIOM_ORG_ID=your-axiom-org-id 27 | AXIOM_DATASET = your-axiom-dataset 28 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // src/app.d.ts 2 | declare global { 3 | namespace App { 4 | interface Locals { 5 | auth: import('lucia').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/server/lucia').Auth; 25 | type UserAttributes = { 26 | email: string; 27 | firstName: string; 28 | lastName: string; 29 | role: string; 30 | verified: boolean; 31 | receiveEmail: boolean; 32 | token: string; 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 | /*place global styles here */ 2 | html, 3 | body { 4 | @apply h-full overflow-hidden; 5 | } 6 | -------------------------------------------------------------------------------- /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 | 6 | export const handleError: HandleServerError = async ({ error, event }) => { 7 | const errorId = crypto.randomUUID(); 8 | 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | //@ts-ignore 11 | event.locals.error = error?.toString() || undefined; 12 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 13 | //@ts-ignore 14 | event.locals.errorStackTrace = error?.stack || undefined; 15 | event.locals.errorId = errorId; 16 | log(500, event); 17 | 18 | return { 19 | message: 'An unexpected error occurred.', 20 | errorId 21 | }; 22 | }; 23 | 24 | export const handle: Handle = async ({ event, resolve }) => { 25 | const startTimer = Date.now(); 26 | event.locals.startTimer = startTimer; 27 | 28 | event.locals.auth = auth.handleRequest(event); 29 | if (event.locals?.auth) { 30 | const session = await event.locals.auth.validate(); 31 | const user = session?.user; 32 | if(user) { 33 | event.locals.user = user; 34 | } 35 | if (event.route.id?.startsWith('/(protected)')) { 36 | if (!user) throw redirect(302, '/auth/sign-in'); 37 | if (!user.verified) throw redirect(302, '/auth/verify/email'); 38 | } 39 | } 40 | 41 | const response = await resolve(event); 42 | log(response.status, event); 43 | return response; 44 | }; 45 | -------------------------------------------------------------------------------- /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/footer.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /src/lib/components/logo.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/components/navigation.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 80 | -------------------------------------------------------------------------------- /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 | export const BASE_URL = dev ? 'http://localhost:5173' : 'https://sveltekit-auth.uv-ray.com'; 3 | export const APP_NAME = 'Sveltekit Auth Starter'; 4 | export const CONTACT_EMAIL = 'yourname@email.com'; 5 | export const DOMAIN = 'sveltekit-auth.uv-ray.com'; 6 | /* WARNING!!! TERMS AND CONDITIONS AND PRIVACY POLICY 7 | WERE CREATED BY CHATGPT AS AN EXAMPLE ONLY. 8 | CONSULT A LAWYER AND DEVELOP YOUR OWN TERMS AND PRIVACY POLICY!!! */ 9 | export const TERMS_PRIVACY_CONTACT_EMAIL = 'yourname@email.com'; 10 | export const TERMS_PRIVACY_WEBSITE = 'yourdomain.com'; 11 | export const TERMS_PRIVACY_COMPANY = 'Your Company'; 12 | export const TERMS_PRIVACY_EFFECTIVE_DATE = 'January 1, 2023'; 13 | export const TERMS_PRIVACY_APP_NAME = 'Your App'; 14 | export const TERMS_PRIVACY_APP_PRICING_AND_SUBSCRIPTIONS = 15 | '[Details about the pricing, subscription model, refund policy]'; 16 | export const TERMS_PRIVACY_COUNTRY = 'United States'; 17 | -------------------------------------------------------------------------------- /src/lib/config/email-messages.ts: -------------------------------------------------------------------------------- 1 | import sendEmail from '$lib/server/email-send'; 2 | import { BASE_URL, APP_NAME } from '$lib/config/constants'; 3 | 4 | // Send an email to verify the user's address 5 | export const sendVerificationEmail = async (email: string, token: string) => { 6 | const verifyEmailURL = `${BASE_URL}/auth/verify/email-${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 = sendEmail(email, subject, htmlEmail, textEmail); 12 | return resultSend; 13 | }; 14 | 15 | // Send an email to welcome the new user 16 | export const sendWelcomeEmail = async (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${BASE_URL}/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 = 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 (email: string, token: string) => { 26 | const updatePasswordURL = `${BASE_URL}/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 = 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 | email: string, 40 | oldEmail: string, 41 | token: string 42 | ) => { 43 | const verifyEmailURL = `${BASE_URL}/auth/verify/email-${token}`; 44 | const textEmail = `Please visit the link below to verify your email address for your ${APP_NAME} account.\n\n ${verifyEmailURL}`; 45 | const htmlEmail = `

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

You can also visit the link below.

${verifyEmailURL}

`; 46 | const subject = `Please confirm your email address for ${APP_NAME}`; 47 | sendEmail(email, subject, htmlEmail, textEmail); 48 | 49 | //send email to user about email change. 50 | 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: ${BASE_URL} to revert the changes.`; 51 | 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: ${BASE_URL} to revert the changes.

`; 52 | const subjectChange = `Your email address for ${APP_NAME} has changed.`; 53 | sendEmail(oldEmail, subjectChange, htmlEmailChange, textEmailChange); 54 | }; 55 | -------------------------------------------------------------------------------- /src/lib/config/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | export default prisma; 6 | -------------------------------------------------------------------------------- /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/email-send.ts: -------------------------------------------------------------------------------- 1 | import nodemailer, { type Transporter } from 'nodemailer'; 2 | import * as aws from '@aws-sdk/client-ses'; 3 | import { 4 | FROM_EMAIL, 5 | AWS_ACCESS_KEY_ID, 6 | AWS_SECRET_ACCESS_KEY, 7 | AWS_REGION, 8 | AWS_API_VERSION, 9 | SMTP_HOST, 10 | SMTP_PORT, 11 | SMTP_SECURE, 12 | SMTP_USER, 13 | SMTP_PASS 14 | } from '$env/static/private'; 15 | //import { z } from "zod"; 16 | export default async function sendEmail( 17 | email: string, 18 | subject: string, 19 | bodyHtml?: string, 20 | bodyText?: string 21 | ) { 22 | const hasAccessKeys = AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY; 23 | let transporter: Transporter; 24 | if (hasAccessKeys) { 25 | const ses = new aws.SES({ 26 | apiVersion: AWS_API_VERSION, 27 | region: AWS_REGION, 28 | ...(hasAccessKeys 29 | ? { 30 | credentials: { 31 | accessKeyId: AWS_ACCESS_KEY_ID || '', 32 | secretAccessKey: AWS_SECRET_ACCESS_KEY || '' 33 | } 34 | } 35 | : {}) 36 | }); 37 | 38 | // create Nodemailer SES transporter 39 | transporter = nodemailer.createTransport({ 40 | SES: { ses, aws } 41 | }); 42 | } else { 43 | // create Nodemailer SMTP transporter 44 | transporter = nodemailer.createTransport({ 45 | // @ts-ignore 46 | host: SMTP_HOST, 47 | port: Number(SMTP_PORT), 48 | secure: Number(SMTP_SECURE) === 1, 49 | auth: { 50 | user: SMTP_USER, 51 | pass: SMTP_PASS 52 | } 53 | }); 54 | } 55 | interface MailConfig { 56 | recipient: string; 57 | subject: string; 58 | htmlMessage: string; 59 | } 60 | 61 | try { 62 | if (!bodyText) { 63 | transporter.sendMail( 64 | { 65 | from: FROM_EMAIL, 66 | to: email, 67 | subject: subject, 68 | html: bodyHtml 69 | }, 70 | (err, info) => { 71 | if (err) { 72 | throw new Error(`Error sending email: ${JSON.stringify(err)}`); 73 | } 74 | } 75 | ); 76 | } else if (!bodyHtml) { 77 | transporter.sendMail( 78 | { 79 | from: FROM_EMAIL, 80 | to: email, 81 | subject: subject, 82 | text: bodyText 83 | }, 84 | (err, info) => { 85 | if (err) { 86 | throw new Error(`Error sending email: ${JSON.stringify(err)}`); 87 | } 88 | } 89 | ); 90 | } else { 91 | transporter.sendMail( 92 | { 93 | from: FROM_EMAIL, 94 | to: email, 95 | subject: subject, 96 | html: bodyHtml, 97 | text: bodyText 98 | }, 99 | (err, info) => { 100 | if (err) { 101 | throw new Error(`Error sending email: ${JSON.stringify(err)}`); 102 | } 103 | } 104 | ); 105 | } 106 | console.log('E-mail sent successfully!'); 107 | return { 108 | statusCode: 200, 109 | message: 'E-mail sent successfully.' 110 | }; 111 | } catch (error) { 112 | throw new Error(`Error sending email: ${JSON.stringify(error)}`); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/lib/server/log.ts: -------------------------------------------------------------------------------- 1 | import { Axiom } from '@axiomhq/js'; 2 | import { AXIOM_TOKEN, AXIOM_ORG_ID, AXIOM_DATASET } from '$env/static/private'; 3 | import getAllUrlParams from '$lib/_helpers/getAllUrlParams'; 4 | import parseTrack from '$lib/_helpers/parseTrack'; 5 | import parseMessage from '$lib/_helpers/parseMessage'; 6 | import { DOMAIN } from '$lib/config/constants'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 9 | //@ts-ignore 10 | export default async function log(statusCode: number, event) { 11 | try { 12 | let level = 'info'; 13 | if (statusCode >= 400) { 14 | level = 'error'; 15 | } 16 | const error = event?.locals?.error || undefined; 17 | const errorId = event?.locals?.errorId || undefined; 18 | const errorStackTrace = event?.locals?.errorStackTrace || undefined; 19 | let urlParams = {}; 20 | if (event?.url?.search) { 21 | urlParams = await getAllUrlParams(event?.url?.search); 22 | } 23 | let messageEvents = {}; 24 | if (event?.locals?.message) { 25 | messageEvents = await parseMessage(event?.locals?.message); 26 | } 27 | let trackEvents = {}; 28 | if (event?.locals?.track) { 29 | trackEvents = await parseTrack(event?.locals?.track); 30 | } 31 | 32 | let referer = event.request.headers.get('referer'); 33 | if (referer) { 34 | const refererUrl = await new URL(referer); 35 | const refererHostname = refererUrl.hostname; 36 | if (refererHostname === 'localhost' || refererHostname === DOMAIN) { 37 | referer = refererUrl.pathname; 38 | } 39 | } else { 40 | referer = undefined; 41 | } 42 | const logData: object = { 43 | level: level, 44 | method: event.request.method, 45 | path: event.url.pathname, 46 | status: statusCode, 47 | timeInMs: Date.now() - event?.locals?.startTimer, 48 | user: event?.locals?.user?.email, 49 | userId: event?.locals?.user?.userId, 50 | referer: referer, 51 | error: error, 52 | errorId: errorId, 53 | errorStackTrace: errorStackTrace, 54 | ...urlParams, 55 | ...messageEvents, 56 | ...trackEvents 57 | }; 58 | console.log('log: ', JSON.stringify(logData)); 59 | if (!AXIOM_TOKEN || !AXIOM_ORG_ID || !AXIOM_DATASET) { 60 | return; 61 | } 62 | const client = new Axiom({ 63 | token: AXIOM_TOKEN, 64 | orgId: AXIOM_ORG_ID 65 | }); 66 | client.ingest(AXIOM_DATASET, [logData]); 67 | } catch (err) { 68 | throw new Error(`Error Logger: ${JSON.stringify(err)}`); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/server/lucia.ts: -------------------------------------------------------------------------------- 1 | // lib/server/lucia.ts 2 | import { lucia } from 'lucia'; 3 | import { sveltekit } from 'lucia/middleware'; 4 | import { prisma } from '@lucia-auth/adapter-prisma'; 5 | import { PrismaClient } from '@prisma/client'; 6 | import { dev } from '$app/environment'; 7 | 8 | export const auth = lucia({ 9 | adapter: prisma(new PrismaClient(), { 10 | user: 'authUser', 11 | key: 'authKey', 12 | session: 'authSession' 13 | }), 14 | env: dev ? 'DEV' : 'PROD', 15 | middleware: sveltekit(), 16 | getUserAttributes: (data) => { 17 | return { 18 | userId: data.id, 19 | email: data.email, 20 | firstName: data.firstName, 21 | lastName: data.lastName, 22 | role: data.role, 23 | verified: data.verified, 24 | receiveEmail: data.receiveEmail, 25 | token: data.token 26 | }; 27 | } 28 | }); 29 | 30 | export type Auth = typeof auth; 31 | -------------------------------------------------------------------------------- /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, redirect } 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 prisma from '$lib/config/prisma'; 7 | 8 | const profileSchema = userSchema.pick({ 9 | firstName: true, 10 | lastName: true, 11 | email: true 12 | }); 13 | 14 | export const load = async (event) => { 15 | const form = await superValidate(event, profileSchema); 16 | const session = await event.locals.auth.validate(); 17 | const user = session?.user; 18 | form.data = { 19 | firstName: user?.firstName, 20 | lastName: user?.lastName, 21 | email: user?.email 22 | }; 23 | return { 24 | form 25 | }; 26 | }; 27 | 28 | export const actions = { 29 | default: async (event) => { 30 | const form = await superValidate(event, profileSchema); 31 | //console.log(form); 32 | 33 | if (!form.valid) { 34 | return fail(400, { 35 | form 36 | }); 37 | } 38 | 39 | //add user to db 40 | try { 41 | console.log('updating profile'); 42 | const session = await event.locals.auth.validate(); 43 | const user = session?.user; 44 | 45 | auth.updateUserAttributes(user?.userId, { 46 | firstName: form.data.firstName, 47 | lastName: form.data.lastName, 48 | email: form.data.email 49 | }); 50 | //await auth.invalidateAllUserSessions(user.userId); 51 | 52 | if (user?.email !== form.data.email) { 53 | //TODO: get emailaddress to change for prisma not just in attributes. setUser not working... weird 54 | // worse comes to worse, update the auth_key manually in the db 55 | //auth.setKey(user.userId, 'emailpassword', form.data.email); 56 | //auth.setUser(user.userId, 'email', form.data.email); 57 | //remove this once bug is fixed and setKey or setUser works 58 | //https://github.com/pilcrowOnPaper/lucia/issues/606 59 | console.log('user: ' + JSON.stringify(user)); 60 | await prisma.authKey.update({ 61 | where: { 62 | id: 'email:' + user?.email 63 | }, 64 | data: { 65 | id: 'email:' + form.data.email 66 | } 67 | }); 68 | 69 | auth.updateUserAttributes(user?.userId, { 70 | verified: false 71 | }); 72 | //await auth.invalidateAllUserSessions(user.userId); 73 | await updateEmailAddressSuccessEmail(form.data.email, user?.email, user?.token); 74 | } 75 | } catch (e) { 76 | console.error(e); 77 | return setError(form, null, 'There was a problem updating your profile.'); 78 | } 79 | console.log('profile updated successfully'); 80 | return message(form, 'Profile updated successfully.'); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /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 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 56 | {APP_NAME} 57 | 58 | 59 | {#if data?.user}{/if} 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 |
70 |