├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.cjs ├── rome.json ├── src ├── app.d.ts ├── app.html ├── app.postcss ├── global.postcss ├── hooks.client.ts ├── hooks.server.ts ├── index.test.ts ├── lib │ └── supabase.ts └── routes │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +page.svelte │ ├── login │ ├── +page.server.ts │ └── +page.svelte │ ├── logout │ └── +server.ts │ └── register │ ├── +page.server.ts │ └── +page.svelte ├── static └── favicon.png ├── svelte.config.js ├── tests └── test.ts ├── tsconfig.json └── vite.config.js /.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_SUPABASE_URL="" 2 | PUBLIC_SUPABASE_ANON_KEY="" -------------------------------------------------------------------------------- /.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 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | vite.config.js.timestamp-* 8 | vite.config.ts.timestamp-* 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sk-supabase-auth", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "test": "playwright test", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "test:unit": "vitest", 13 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 14 | "format": "prettier --plugin-search-dir . --write ." 15 | }, 16 | "devDependencies": { 17 | "@playwright/test": "^1.28.1", 18 | "@supabase/auth-helpers-sveltekit": "^0.8.6", 19 | "@supabase/supabase-js": "^2.1.3", 20 | "@sveltejs/adapter-auto": "next", 21 | "@sveltejs/kit": "next", 22 | "@typescript-eslint/eslint-plugin": "^5.45.0", 23 | "@typescript-eslint/parser": "^5.45.0", 24 | "autoprefixer": "^10.4.7", 25 | "eslint": "^8.28.0", 26 | "eslint-config-prettier": "^8.5.0", 27 | "eslint-plugin-svelte3": "^4.0.0", 28 | "postcss": "^8.4.14", 29 | "postcss-load-config": "^4.0.1", 30 | "postcss-preset-env": "^7.8.3", 31 | "postcss-simple-vars": "^7.0.1", 32 | "prettier": "^2.8.0", 33 | "prettier-plugin-svelte": "^2.8.1", 34 | "svelte": "^3.54.0", 35 | "svelte-check": "^2.9.2", 36 | "svelte-preprocess": "^4.10.7", 37 | "tslib": "^2.4.1", 38 | "typescript": "^4.9.3", 39 | "vite": "^4.0.0", 40 | "vitest": "^0.25.3" 41 | }, 42 | "type": "module" 43 | } 44 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'npm run build && npm run preview', 6 | port: 4173 7 | }, 8 | testDir: 'tests' 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const autoprefixer = require("autoprefixer") 2 | 3 | const config = { 4 | plugins: [ 5 | require("postcss-simple-vars"), 6 | autoprefixer, 7 | require("postcss-preset-env")({ 8 | stage: 1, 9 | }), 10 | ], 11 | } 12 | 13 | module.exports = config 14 | -------------------------------------------------------------------------------- /rome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "recommended": true 6 | } 7 | }, 8 | "javascript": { 9 | "formatter": { 10 | "semicolons": "asNeeded" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | // and what to do when importing types 4 | 5 | import type { TypedSupabaseClient } from "@supabase/auth-helpers-sveltekit/dist/types" 6 | import type { Session } from "@supabase/supabase-js" 7 | 8 | declare global { 9 | declare namespace App { 10 | // interface Error {} 11 | interface Locals { 12 | sb: TypedSupabaseClient 13 | session: Session | null 14 | } 15 | interface PageData { 16 | session: import("@supabase/supabase-js").Session | null 17 | } 18 | // interface Platform {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app.postcss: -------------------------------------------------------------------------------- 1 | @import 'global.postcss'; 2 | 3 | $dark-300: hsl(213, 24%, 7%); 4 | $dark-200: hsl(222, 15%, 13%); 5 | $dark-100: hsl(222, 15%, 19%); 6 | $primary-300: hsl(149, 77%, 46%); 7 | $primary-200: hsl(149, 77%, 56%); 8 | $primary-100: hsl(149, 77%, 66%); 9 | $secondary-300: hsl(289, 90%, 53%); 10 | $secondary-200: hsl(289, 90%, 63%); 11 | $secondary-100: hsl(289, 90%, 73%); 12 | $gray: hsl(212, 15%, 48%); 13 | 14 | body { 15 | background: $dark-200; 16 | color: #ffffff; 17 | font-size: 18px; 18 | } 19 | 20 | main { 21 | padding: 2rem; 22 | text-align: center; 23 | } 24 | 25 | .container { 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | height: 100vh; 30 | padding: 0 20px; 31 | } 32 | 33 | .auth-buttons { 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | margin-top: 1rem; 38 | gap: 1rem; 39 | } 40 | 41 | .card { 42 | display: flex; 43 | flex-direction: column; 44 | margin: 0 auto; 45 | background-color: $dark-200; 46 | border-radius: 10px; 47 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 48 | padding: 32px; 49 | } 50 | 51 | .btn { 52 | color: $dark-300; 53 | border: none; 54 | border-radius: 5px; 55 | padding: 10px 20px; 56 | font-size: 16px; 57 | cursor: pointer; 58 | transition: all 0.2s ease-in-out; 59 | 60 | &:hover { 61 | text-decoration: none; 62 | } 63 | } 64 | 65 | .btn-primary { 66 | background-color: $primary-200; 67 | color: $dark-300; 68 | 69 | &:hover { 70 | background-color: $primary-300; 71 | } 72 | } 73 | 74 | .btn-secondary { 75 | background-color: $secondary-200; 76 | color: $dark-300; 77 | 78 | &:hover { 79 | background-color: $secondary-300; 80 | } 81 | } 82 | 83 | .btn-ghost { 84 | background-color: transparent; 85 | color: #ffff; 86 | border: 2px solid $gray; 87 | 88 | &:hover { 89 | background-color: $dark-100; 90 | color: #ffffff; 91 | } 92 | } 93 | 94 | input { 95 | background-color: $dark-100; 96 | border: $gray 2px solid; 97 | border-radius: 5px; 98 | padding: 10px 20px; 99 | color: #ffffff; 100 | font-size: 16px; 101 | margin-bottom: 20px; 102 | 103 | &:focus { 104 | outline: $primary-300 2px solid; 105 | } 106 | } 107 | 108 | .auth-form { 109 | display: flex; 110 | flex-direction: column; 111 | width: 100%; 112 | max-width: 400px; 113 | min-width: 400px; 114 | margin: 0 auto; 115 | 116 | & button { 117 | margin-bottom: 10px; 118 | } 119 | 120 | & label { 121 | text-align: left; 122 | padding-bottom: 2px; 123 | font-size: 16px; 124 | } 125 | } 126 | 127 | .socials { 128 | width: 100%; 129 | max-width: 400px; 130 | min-width: 400px; 131 | margin: 0 auto; 132 | display: flex; 133 | align-items: center; 134 | gap: 1rem; 135 | padding-top: 10px; 136 | 137 | & .btn { 138 | width: 100%; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/global.postcss: -------------------------------------------------------------------------------- 1 | /* Write your global styles here, in PostCSS syntax */ 2 | /* 3 | 1. Use a more-intuitive box-sizing model. 4 | */ 5 | *, 6 | *::before, 7 | *::after { 8 | box-sizing: border-box; 9 | } 10 | 11 | /* 12 | 2. Remove default margin 13 | */ 14 | * { 15 | margin: 0; 16 | } 17 | 18 | /* 19 | 3. Allow percentage-based heights in the application 20 | */ 21 | html, 22 | body { 23 | height: 100%; 24 | } 25 | 26 | /* 27 | Typographic tweaks! 28 | 4. Add accessible line-height 29 | 5. Improve text rendering 30 | */ 31 | body { 32 | line-height: 1.5; 33 | -webkit-font-smoothing: antialiased; 34 | font-family: sans-serif; 35 | } 36 | 37 | /* 38 | 6. Improve media defaults 39 | */ 40 | img, 41 | picture, 42 | video, 43 | canvas, 44 | svg { 45 | display: block; 46 | max-width: 100%; 47 | } 48 | 49 | /* 50 | 7. Remove built-in form typography styles 51 | */ 52 | input, 53 | button, 54 | textarea, 55 | select { 56 | font: inherit; 57 | } 58 | 59 | /* 60 | 8. Avoid text overflows 61 | */ 62 | 63 | @custom-selector :--heading h1, h2, h3, h4, h5, h6; 64 | :--heading { 65 | overflow-wrap: break-word; 66 | } 67 | 68 | a { 69 | text-decoration: none; 70 | color: inherit; 71 | } 72 | 73 | a:hover { 74 | text-decoration: underline; 75 | } 76 | 77 | /* 78 | 9. Create a root stacking context 79 | */ 80 | #root { 81 | isolation: isolate; 82 | } 83 | -------------------------------------------------------------------------------- /src/hooks.client.ts: -------------------------------------------------------------------------------- 1 | import "$lib/supabase" 2 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import "$lib/supabase" 2 | import { getSupabase } from "@supabase/auth-helpers-sveltekit" 3 | import type { Handle } from "@sveltejs/kit" 4 | 5 | export const handle: Handle = async ({ event, resolve }) => { 6 | const { session, supabaseClient } = await getSupabase(event) 7 | 8 | event.locals.sb = supabaseClient 9 | event.locals.session = session 10 | 11 | return resolve(event) 12 | } 13 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/auth-helpers-sveltekit" 2 | import { 3 | PUBLIC_SUPABASE_ANON_KEY, 4 | PUBLIC_SUPABASE_URL, 5 | } from "$env/static/public" 6 | 7 | export const supabaseClient = createClient( 8 | PUBLIC_SUPABASE_URL, 9 | PUBLIC_SUPABASE_ANON_KEY, 10 | ) 11 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "@supabase/auth-helpers-sveltekit" 2 | import type { LayoutServerLoad } from "./$types" 3 | 4 | export const load: LayoutServerLoad = async (event) => { 5 | console.log("Ran layout load") 6 | return { 7 | session: await getServerSession(event), 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 |

SvelteKit & Supabase Auth

19 | {#if data.session} 20 |

Welcome, {data.session.user.email}

21 |
22 | 23 |
24 | {:else} 25 |

Let's learn how to register and login users!

26 |
27 | Login 28 | Register 29 |
30 | {/if} 31 |
32 | -------------------------------------------------------------------------------- /src/routes/login/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { AuthApiError, type Provider } from "@supabase/supabase-js" 2 | import { fail, redirect } from "@sveltejs/kit" 3 | import type { Actions } from "./$types" 4 | 5 | const OAUTH_PROVIDERS = ["google", "discord", "github"] 6 | 7 | export const actions: Actions = { 8 | login: async ({ request, locals, url }) => { 9 | const provider = url.searchParams.get("provider") as Provider 10 | 11 | if (provider) { 12 | if (!OAUTH_PROVIDERS.includes(provider)) { 13 | return fail(400, { 14 | error: "Provider not supported.", 15 | }) 16 | } 17 | const { data, error: err } = await locals.sb.auth.signInWithOAuth({ 18 | provider: provider, 19 | }) 20 | 21 | if (err) { 22 | console.log(err) 23 | return fail(400, { 24 | message: "Something went wrong.", 25 | }) 26 | } 27 | 28 | throw redirect(303, data.url) 29 | } 30 | 31 | const body = Object.fromEntries(await request.formData()) 32 | 33 | const { data, error: err } = await locals.sb.auth.signInWithPassword({ 34 | email: body.email as string, 35 | password: body.password as string, 36 | }) 37 | 38 | if (err) { 39 | if (err instanceof AuthApiError && err.status === 400) { 40 | return fail(400, { 41 | error: "Invalid credentials", 42 | }) 43 | } 44 | return fail(500, { 45 | message: "Server error. Try again later.", 46 | }) 47 | } 48 | 49 | throw redirect(303, "/") 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /src/routes/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
31 |

Login

32 |
33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | 41 | 42 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/routes/logout/+server.ts: -------------------------------------------------------------------------------- 1 | import { error, redirect } from "@sveltejs/kit" 2 | import type { RequestHandler } from "./$types" 3 | 4 | export const POST: RequestHandler = async ({ locals }) => { 5 | const { error: err } = await locals.sb.auth.signOut() 6 | 7 | if (err) { 8 | throw error(500, "Something went wrong logging you out.") 9 | } 10 | 11 | throw redirect(303, "/") 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/register/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { AuthApiError } from "@supabase/supabase-js" 2 | import { fail, redirect } from "@sveltejs/kit" 3 | import type { Actions } from "./$types" 4 | 5 | export const actions: Actions = { 6 | register: async ({ request, locals }) => { 7 | const body = Object.fromEntries(await request.formData()) 8 | 9 | const { data, error: err } = await locals.sb.auth.signUp({ 10 | email: body.email as string, 11 | password: body.password as string, 12 | }) 13 | 14 | if (err) { 15 | if (err instanceof AuthApiError && err.status === 400) { 16 | return fail(400, { 17 | error: "Invalid email or password", 18 | }) 19 | } 20 | return fail(500, { 21 | error: "Server error. Please try again later.", 22 | }) 23 | } 24 | 25 | throw redirect(303, "/") 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /src/routes/register/+page.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 |

Register

6 |
7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huntabyte/sk-supabase-oauth/925b82134e25ddd0bc289a96ce4b679fb94fc61b/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import preprocess from 'svelte-preprocess'; 2 | import adapter from '@sveltejs/adapter-auto'; 3 | import { vitePreprocess } from '@sveltejs/kit/vite'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 8 | // for more information about preprocessors 9 | preprocess: [ 10 | vitePreprocess(), 11 | preprocess({ 12 | postcss: true 13 | }) 14 | ], 15 | 16 | kit: { 17 | adapter: adapter() 18 | } 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /tests/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('index page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | expect(await page.textContent('h1')).toBe('Welcome to SvelteKit'); 6 | }); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | 3 | /** @type {import('vite').UserConfig} */ 4 | const config = { 5 | plugins: [sveltekit()], 6 | test: { 7 | include: ['src/**/*.{test,spec}.{js,ts}'] 8 | } 9 | }; 10 | 11 | export default config; 12 | --------------------------------------------------------------------------------