├── .eslintrc.cjs ├── .gitignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── global.d.ts ├── guards │ └── auth.ts ├── main.ts ├── router │ └── index.ts ├── stores │ └── auth.ts ├── supabase.ts ├── types.d.ts └── views │ ├── HomeView.vue │ ├── ProfileView.vue │ └── SignInView.vue ├── tsconfig.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | require('@rushstack/eslint-patch/modern-module-resolution'); 4 | 5 | module.exports = { 6 | root: true, 7 | parser: 'vue-eslint-parser', 8 | parserOptions: { 9 | parser: '@typescript-eslint/parser', 10 | sourceType: 'module', 11 | ecmaVersion: 2021 12 | }, 13 | extends: [ 14 | 'plugin:vue/vue3-essential', 15 | 'eslint:recommended', 16 | '@vue/eslint-config-typescript/recommended', 17 | '@vue/eslint-config-prettier', 18 | 'prettier' 19 | ], 20 | rules: { 21 | 'prettier/prettier': 2 22 | }, 23 | env: { 24 | 'vue/setup-compiler-macros': true 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | /cypress/videos/ 17 | /cypress/screenshots/ 18 | 19 | # Editor directories and files 20 | .vscode/* 21 | !.vscode/extensions.json 22 | .idea 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 80, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "trailingComma": "none", 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mac (Maciej Pędzich) 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 | # vue-supabase-tpa-demo 2 | 3 | Demo application utilising Supabase for third party authentication (here with Github). Bear in mind **it is NOT** 100% production ready, it's just a demo after all. 4 | 5 | [Read about it on Hashnode](https://maciejpedzich.hashnode.dev/vue-supabase-github-oauth) 6 | 7 | ## Recommended IDE Setup 8 | 9 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin). 10 | 11 | ## Type Support for `.vue` Imports in TS 12 | 13 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 14 | 15 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471) that is more performant. You can enable it by the following steps: 16 | 17 | 1. Disable the built-in TypeScript Extension 18 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 19 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 20 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 21 | 22 | ## Customize configuration 23 | 24 | See [Vite Configuration Reference](https://vitejs.dev/config/). 25 | 26 | ## Project Setup 27 | 28 | ```sh 29 | npm install 30 | ``` 31 | 32 | ### Compile and Hot-Reload for Development 33 | 34 | ```sh 35 | npm run dev 36 | ``` 37 | 38 | ### Type-Check, Compile and Minify for Production 39 | 40 | ```sh 41 | npm run build 42 | ``` 43 | 44 | ### Lint with [ESLint](https://eslint.org/) 45 | 46 | ```sh 47 | npm run lint 48 | ``` 49 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Supabase TPA Demo 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-project", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vue-tsc --noEmit && vite build", 7 | "preview": "vite preview --port 5050", 8 | "typecheck": "vue-tsc --noEmit", 9 | "prettier-format": "prettier --config .prettierrc --write \"src/**/*.{vue,ts}\"", 10 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 11 | }, 12 | "dependencies": { 13 | "@supabase/supabase-js": "^1.29.1", 14 | "pinia": "^2.0.9", 15 | "vue": "^3.2.26", 16 | "vue-router": "^4.0.12" 17 | }, 18 | "devDependencies": { 19 | "@rushstack/eslint-patch": "^1.1.0", 20 | "@types/node": "^16.11.17", 21 | "@typescript-eslint/eslint-plugin": "^5.9.1", 22 | "@typescript-eslint/parser": "^5.9.1", 23 | "@vitejs/plugin-vue": "^2.0.1", 24 | "@vue/eslint-config-prettier": "^7.0.0", 25 | "@vue/eslint-config-typescript": "^10.0.0", 26 | "eslint": "^8.5.0", 27 | "eslint-config-prettier": "^8.3.0", 28 | "eslint-plugin-prettier": "^4.0.0", 29 | "eslint-plugin-vue": "^8.2.0", 30 | "prettier": "^2.5.1", 31 | "typescript": "~4.5.4", 32 | "vite": "^2.9.13", 33 | "vite-plugin-eslint": "^1.3.0", 34 | "vue-eslint-parser": "^8.0.1", 35 | "vue-tsc": "^0.29.8" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maciejpedzich/vue-supabase-tpa-demo/de44055a5aac54c0549b97e69071f97856d14fda/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 39 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | MODE: 'development' | 'production'; 3 | BASE_URL: string; 4 | VITE_SUPABASE_URL: string; 5 | VITE_SUPABASE_ANON_KEY: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/guards/auth.ts: -------------------------------------------------------------------------------- 1 | import { RouteLocationNormalized, NavigationGuardNext } from 'vue-router'; 2 | import { useAuthStore } from '@/stores/auth'; 3 | 4 | export function authGuard( 5 | to: RouteLocationNormalized, 6 | from: RouteLocationNormalized, 7 | next: NavigationGuardNext 8 | ): Promise | void { 9 | const authStore = useAuthStore(); 10 | 11 | if (!to.meta.authRequired || authStore.isAuthenticated) { 12 | return next(); 13 | } else { 14 | authStore.saveRedirectRoute(to); 15 | return next({ name: 'SignIn' }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | 4 | import App from './App.vue'; 5 | import router from './router'; 6 | 7 | const app = createApp(App); 8 | 9 | app.use(createPinia()); 10 | app.use(router); 11 | 12 | app.mount('#app'); 13 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { authGuard } from '@/guards/auth'; 2 | import { createRouter, createWebHistory } from 'vue-router'; 3 | import HomeView from '../views/HomeView.vue'; 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(import.meta.env.BASE_URL), 7 | routes: [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | meta: { authRequired: false }, 12 | component: HomeView 13 | }, 14 | { 15 | path: '/sign-in', 16 | name: 'SignIn', 17 | meta: { authRequired: false }, 18 | component: () => import('../views/SignInView.vue') 19 | }, 20 | { 21 | path: '/profile', 22 | name: 'Profile', 23 | meta: { authRequired: true }, 24 | component: () => import('../views/ProfileView.vue') 25 | } 26 | ] 27 | }); 28 | 29 | router.beforeEach(authGuard); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /src/stores/auth.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { RouteLocation } from 'vue-router'; 3 | 4 | import { User } from '@supabase/supabase-js'; 5 | import { supabase } from '@/supabase'; 6 | 7 | type State = { 8 | currentUser: User | null; 9 | redirectRoute: Partial | null; 10 | }; 11 | 12 | type Getters = { 13 | isAuthenticated(): boolean; 14 | }; 15 | 16 | type Actions = { 17 | loadUser(): void; 18 | clearUser(): void; 19 | saveRedirectRoute(route: Partial): void; 20 | loadRedirectRoute(): void; 21 | clearRedirectRoute(): void; 22 | }; 23 | 24 | export const useAuthStore = defineStore<'auth', State, Getters, Actions>( 25 | 'auth', 26 | { 27 | state() { 28 | return { 29 | currentUser: null, 30 | redirectRoute: null 31 | }; 32 | }, 33 | getters: { 34 | isAuthenticated() { 35 | return !!this.currentUser; 36 | } 37 | }, 38 | actions: { 39 | loadUser() { 40 | this.currentUser = supabase.auth.user(); 41 | }, 42 | clearUser() { 43 | this.currentUser = null; 44 | }, 45 | saveRedirectRoute(route: Partial) { 46 | const { name, params, query, hash } = route; 47 | 48 | localStorage.setItem( 49 | 'redirectRoute', 50 | JSON.stringify({ 51 | name, 52 | params, 53 | query, 54 | hash 55 | }) 56 | ); 57 | }, 58 | loadRedirectRoute() { 59 | const route = JSON.parse( 60 | localStorage.getItem('redirectRoute') || 'null' 61 | ) as Partial | null; 62 | 63 | this.redirectRoute = route; 64 | }, 65 | clearRedirectRoute() { 66 | localStorage.removeItem('redirectRoute'); 67 | this.redirectRoute = null; 68 | } 69 | } 70 | } 71 | ); 72 | -------------------------------------------------------------------------------- /src/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | 3 | const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; 4 | const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; 5 | 6 | export const supabase = createClient(supabaseUrl, supabaseAnonKey); 7 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router'; 2 | 3 | declare module 'vue-router' { 4 | interface RouteMeta { 5 | authRequired: boolean; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/views/ProfileView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /src/views/SignInView.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "jsx": "preserve", 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"], 18 | "skipLibCheck": true 19 | }, 20 | "include": ["vite.config.*", "env.d.ts", "src/**/*", "src/**/*.vue"] 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./src', import.meta.url)) 12 | } 13 | } 14 | }) 15 | --------------------------------------------------------------------------------