├── .env.example ├── .eslintrc.json ├── .github └── workflows │ └── linter.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── app.vue ├── assets ├── _custom.scss ├── _fonts.scss ├── _mixins.scss └── tailwind.scss ├── components └── atoms │ ├── div │ └── Card.vue │ └── input │ ├── Password.vue │ ├── Text.vue │ └── common-input-style.scss ├── composables └── useAuth.ts ├── layouts └── default.vue ├── lib └── axios │ ├── endpoints.ts │ ├── http.ts │ └── model.ts ├── nuxt.config.ts ├── package.json ├── pages ├── 404.vue ├── README.md ├── index.vue ├── profile │ └── edit.vue ├── signin.vue └── signup.vue ├── plugins └── index.ts ├── postcss.config.js ├── public ├── favicon.ico ├── icon.png └── robots.txt ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Please copy this and create new `.env` file, 2 | API_ENDPOINT=http://localhost:8000 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:vue/essential", 10 | "plugin:@typescript-eslint/recommended", 11 | "@nuxtjs/eslint-config-typescript", 12 | "prettier" 13 | ], 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "parser": "@typescript-eslint/parser", 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["vue", "@typescript-eslint"], 20 | "rules": { 21 | "vue/multi-word-component-names": "off", 22 | "@typescript-eslint/no-unused-vars": "warn", 23 | "import/named": "warn", 24 | "no-console": "off", 25 | "camelcase": "off" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: linter 2 | 3 | on: 4 | pull_request: 5 | branches: [ master, main ] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: "16" 15 | - run: npm install 16 | - run: npm run build 17 | - run: npm run lint 18 | - run: npm run tsc 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Front-end 2 | node_modules 3 | *.log 4 | .nuxt 5 | nuxt.d.ts 6 | .output 7 | .env 8 | package-lock.json 9 | 10 | # API Server 11 | __pycache__ 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["vue.volar", "esbenp.prettier-vscode"], 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Masaki Yoshiiwa 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 | # nuxt3-tailwind-auth-app 2 | 3 | An example app using Nuxt3, TailwindCSS, ESLint & Prettier with GitHub Actions, etc. 4 | 5 | ## Elements 6 | 7 | Base: 8 | - [Nuxt3 (Vue3)](https://v3.nuxtjs.org/) 9 | - [Tailwind CSS](https://tailwindcss.com/) 10 | 11 | Linter/Formatter: 12 | - [ESLint](https://eslint.org/) 13 | - [Prettier](https://prettier.io/) 14 | 15 | Validation Library: 16 | - [VeeValidate](https://vee-validate.logaretm.com/v4/) 17 | - [yup](https://www.npmjs.com/package/yup) 18 | - [zxcvbn](https://github.com/dropbox/zxcvbn) 19 | 20 | Recommended Editors: 21 | - [VSCode](https://code.visualstudio.com/) 22 | 23 | Recommended VSCode Plugins: 24 | - [Volor](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) 25 | 26 | ## Setup 27 | 28 | Need to install `Node.js >= 16` first. 29 | 30 | Make sure to install the dependencies. 31 | 32 | ```bash 33 | npm i 34 | ``` 35 | 36 | ### Development 37 | 38 | ```bash 39 | npm run dev 40 | ``` 41 | 42 | ### Production 43 | 44 | Build the application for production: 45 | 46 | ```bash 47 | npm run build 48 | ``` 49 | 50 | Checkout the [deployment documentation](https://v3.nuxtjs.org/docs/deployment). 51 | 52 | 53 | ## API Server & DB Server 54 | 55 | If you need API & DB Server, please clone from following: 56 | https://github.com/qlawmarq/fastapi-mysql-docker 57 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | -------------------------------------------------------------------------------- /assets/_custom.scss: -------------------------------------------------------------------------------- 1 | // custom styles while using tailwind theme 2 | 3 | // using tailwind theme variables inside scss with the theme() function 4 | // https://tailwindcss.com/docs/using-with-preprocessors#variables 5 | // https://tailwindcss.com/docs/functions-and-directives#theme 6 | 7 | html, body { 8 | background-color: theme('colors.green.500'); 9 | @apply text-gray-700; 10 | } 11 | 12 | // if you need to use dark theme 13 | // @media (prefers-color-scheme: dark) { 14 | // html, body { 15 | // background-color: theme('colors.indigo.900'); 16 | // @apply text-white; 17 | // } 18 | // } 19 | 20 | -------------------------------------------------------------------------------- /assets/_fonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap'); 2 | 3 | html, body { 4 | font-family: 'Open Sans', sans-serif; 5 | }; -------------------------------------------------------------------------------- /assets/_mixins.scss: -------------------------------------------------------------------------------- 1 | // mixins 2 | 3 | // using tailwind theme variables inside scss with the theme() function 4 | // https://tailwindcss.com/docs/using-with-preprocessors#variables 5 | // https://tailwindcss.com/docs/functions-and-directives#theme 6 | 7 | $breakpoints: ( 8 | sm: theme('screens.sm'), 9 | md: theme('screens.md'), 10 | lg: theme('screens.lg'), 11 | xl: theme('screens.xl') 12 | ); 13 | 14 | @mixin breakpoint($size) { 15 | @media screen and (min-width: map-get($breakpoints, $size)) { 16 | // insert content inside 17 | @content 18 | } 19 | }; 20 | 21 | // @include breakpoint('sm') { 22 | // html, body { 23 | // background-color: theme('colors.pink.900'); 24 | // color: theme('colors.white'); 25 | // }; 26 | // }; 27 | -------------------------------------------------------------------------------- /assets/tailwind.scss: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | // https://tailwindcss.com/docs/using-with-preprocessors#build-time-imports 5 | // https://tailwindcss.com/docs/using-with-preprocessors#using-sass-less-or-stylus 6 | @import './_custom.scss'; 7 | @import './_mixins.scss'; 8 | @import './_fonts.scss'; -------------------------------------------------------------------------------- /components/atoms/div/Card.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /components/atoms/input/Password.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 284 | 285 | 288 | -------------------------------------------------------------------------------- /components/atoms/input/Text.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 111 | 112 | 115 | -------------------------------------------------------------------------------- /components/atoms/input/common-input-style.scss: -------------------------------------------------------------------------------- 1 | 2 | [v-cloak] { 3 | display: none; 4 | } 5 | 6 | .Input { 7 | @apply relative; 8 | } 9 | 10 | .Input__label { 11 | @apply block text-gray-700 text-sm font-bold mb-2; 12 | } 13 | 14 | .Input__field { 15 | @apply shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight; 16 | } 17 | 18 | .Input__field--disabled { 19 | @apply bg-gray-300; 20 | } 21 | 22 | .Input__badge--error { 23 | @apply bg-red-500; 24 | } 25 | 26 | .Input__badge--success { 27 | @apply bg-green-500; 28 | } 29 | 30 | 31 | .Password__strength-meter { 32 | position: relative; 33 | height: 3px; 34 | background: #ddd; 35 | margin: 10px auto 20px; 36 | border-radius: 3px; 37 | } 38 | 39 | .Password__strength-meter:before, 40 | .Password__strength-meter:after { 41 | content: ""; 42 | height: inherit; 43 | background: transparent; 44 | display: block; 45 | border-color: #fff; 46 | border-style: solid; 47 | border-width: 0 5px 0 5px; 48 | position: absolute; 49 | width: 20%; 50 | z-index: 10; 51 | } 52 | 53 | .Password__strength-meter:before { 54 | left: 20%; 55 | } 56 | 57 | .Password__strength-meter:after { 58 | right: 20%; 59 | } 60 | 61 | .Password__strength-meter--fill { 62 | background: transparent; 63 | height: inherit; 64 | position: absolute; 65 | width: 0; 66 | border-radius: inherit; 67 | transition: width 0.5s ease-in-out, background 0.25s; 68 | } 69 | 70 | .Password__strength-meter--fill[data-score="0"] { 71 | background: darkred; 72 | width: 20%; 73 | } 74 | 75 | .Password__strength-meter--fill[data-score="1"] { 76 | background: orangered; 77 | width: 40%; 78 | } 79 | 80 | .Password__strength-meter--fill[data-score="2"] { 81 | background: orange; 82 | width: 60%; 83 | } 84 | 85 | .Password__strength-meter--fill[data-score="3"] { 86 | background: yellowgreen; 87 | width: 80%; 88 | } 89 | 90 | .Password__strength-meter--fill[data-score="4"] { 91 | background: green; 92 | width: 100%; 93 | } 94 | 95 | 96 | .Password__icons { 97 | position: absolute; 98 | top: 0; 99 | right: 0; 100 | height: 95px; 101 | display: flex; 102 | flex-direction: row; 103 | justify-content: flex-end; 104 | align-items: center; 105 | } 106 | 107 | .Password__toggle { 108 | position: relative; 109 | width: 30px; 110 | height: 20px; 111 | font-size: 14px; 112 | } 113 | 114 | .Password__badge { 115 | position: relative; 116 | color: white; 117 | border-radius: 6px; 118 | width: 30px; 119 | height: 15px; 120 | font-size: 14px; 121 | margin-right: 13px; 122 | display: flex; 123 | justify-content: center; 124 | align-items: center; 125 | } 126 | 127 | .Password__badge--error { 128 | @apply bg-red-500; 129 | } 130 | 131 | .Password__badge--success { 132 | @apply bg-green-500; 133 | } 134 | 135 | .Password__eye { 136 | position: relative; 137 | color: #777777; 138 | 139 | svg { 140 | fill: currentColor; 141 | } 142 | 143 | &:hover, 144 | &:focus { 145 | color: #404b69; 146 | } 147 | } -------------------------------------------------------------------------------- /composables/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "#app"; 2 | 3 | export const useAuth = () => { 4 | const authState = useState("auth", () => { 5 | try { 6 | return JSON.parse(localStorage.getItem("nuxt3_auth") || ""); 7 | } catch (error) { 8 | return null; 9 | } 10 | }); 11 | const setAuthState = (newState: any) => { 12 | authState.value = newState; 13 | }; 14 | 15 | const userState = useState("user", () => { 16 | try { 17 | return JSON.parse(localStorage.getItem("nuxt3_user") || ""); 18 | } catch (error) { 19 | return null; 20 | } 21 | }); 22 | const setUserState = (newState: any) => { 23 | userState.value = newState; 24 | }; 25 | 26 | return { authState, setAuthState, userState, setUserState }; 27 | }; 28 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /lib/axios/endpoints.ts: -------------------------------------------------------------------------------- 1 | import http from "./http"; 2 | import { userModel, SigninModel } from "./model"; 3 | 4 | class ApiService { 5 | signup(data: userModel) { 6 | return http.post("/v1/signup", data); 7 | } 8 | 9 | signin(data: SigninModel) { 10 | return http.post("/v1/signin", data); 11 | } 12 | 13 | updateUser(data: userModel) { 14 | return http.post("/v1/user/update", data); 15 | } 16 | 17 | getUsers(): Promise { 18 | return http.get("/v1/users"); 19 | } 20 | } 21 | 22 | export default new ApiService(); 23 | -------------------------------------------------------------------------------- /lib/axios/http.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | 3 | const apiClient: AxiosInstance = axios.create({ 4 | baseURL: process.env?.API_ENDPOINT || "http://localhost:8000", 5 | headers: { 6 | "Content-Type": "application/json", 7 | }, 8 | }); 9 | 10 | export default apiClient; 11 | -------------------------------------------------------------------------------- /lib/axios/model.ts: -------------------------------------------------------------------------------- 1 | export interface SigninModel { 2 | email: string; 3 | password: string; 4 | } 5 | 6 | export interface userModel { 7 | email: string; 8 | first_name: string; 9 | last_name: string; 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from "nuxt/config"; 2 | import { resolve } from "pathe"; 3 | 4 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 5 | export default defineNuxtConfig({ 6 | ssr: false, 7 | telemetry: false, 8 | alias: { 9 | lib: resolve(__dirname, "./lib"), 10 | }, 11 | postcss: { 12 | plugins: { 13 | tailwindcss: {}, 14 | autoprefixer: {}, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt3-tailwind-auth-app", 3 | "version": "0.0.2", 4 | "description": "An example app using Nuxt3 with TailwindCSS.", 5 | "engines": { 6 | "node": ">=16.0.0" 7 | }, 8 | "author": "Masaki Yoshiiwa", 9 | "private": true, 10 | "scripts": { 11 | "dev": "nuxi dev -o", 12 | "tailwind": "tailwind-config-viewer -o -p 4000", 13 | "build": "nuxi build", 14 | "start": "node .output/server/index.mjs", 15 | "format": "prettier --write --ignore-path .gitignore './**/*.{js,jsx,ts,tsx,json,css,vue}'", 16 | "lint": "eslint . --ext .vue,.ts", 17 | "lint:fix": "eslint . --ext .vue,.ts --fix", 18 | "tsc": "tsc" 19 | }, 20 | "devDependencies": { 21 | "@fortawesome/fontawesome-svg-core": "^6.2.1", 22 | "@fortawesome/free-regular-svg-icons": "^6.2.1", 23 | "@fortawesome/free-solid-svg-icons": "^6.2.1", 24 | "@fortawesome/vue-fontawesome": "^3.0.3", 25 | "@nuxt/vite-builder": "^3.1.1", 26 | "@nuxtjs/eslint-config-typescript": "^12.0.0", 27 | "@types/blueimp-md5": "^2.18.0", 28 | "@types/node": "^18.11.18", 29 | "@types/zxcvbn": "^4.4.1", 30 | "@typescript-eslint/parser": "^5.49.0", 31 | "autoprefixer": "^10.4.13", 32 | "axios": "^1.2.6", 33 | "blueimp-md5": "^2.19.0", 34 | "eslint": "^8.33.0", 35 | "eslint-config-prettier": "^8.6.0", 36 | "nuxt": "^3.1.1", 37 | "postcss": "^8.4.21", 38 | "prettier": "^2.8.3", 39 | "sass": "^1.57.1", 40 | "tailwind-config-viewer": "^1.7.2", 41 | "tailwindcss": "^3.2.4", 42 | "typescript": "^4.9.4", 43 | "vee-validate": "^4.7.3", 44 | "vite-plugin-eslint": "^1.8.1", 45 | "yup": "^0.32.11", 46 | "zxcvbn": "^4.4.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pages/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 58 | -------------------------------------------------------------------------------- /pages/profile/edit.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 142 | -------------------------------------------------------------------------------- /pages/signin.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 99 | -------------------------------------------------------------------------------- /pages/signup.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 149 | -------------------------------------------------------------------------------- /plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin, useState } from "#app"; 2 | import { library } from "@fortawesome/fontawesome-svg-core"; 3 | import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; 4 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 5 | 6 | library.add(faEye); 7 | library.add(faEyeSlash); 8 | 9 | export default defineNuxtPlugin((nuxtApp) => { 10 | nuxtApp.vueApp.component("fa", FontAwesomeIcon); 11 | // router middleware, which block unauthoricated access. 12 | const router = nuxtApp.$router; 13 | const authState = useState("auth"); 14 | const authRoutes = ["/", "/profile/edit"]; 15 | const unauthRoutes = ["/signup", "/signin"]; 16 | router.beforeEach((to: { path: string }, from: any, next: () => void) => { 17 | console.log(to.path); 18 | console.log(authState.value); 19 | if (authRoutes.includes(to.path) && !authState.value) { 20 | console.log("Redirect to sign-in page"); 21 | router.push("/signin"); 22 | } 23 | if (unauthRoutes.includes(to.path) && authState.value) { 24 | console.log("Redirect to auth page"); 25 | router.push("/"); 26 | } 27 | next(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlawmarq/nuxt3-tailwind-auth-app/be0720e9be3f555eb97ae207a913f295b11d1e96/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlawmarq/nuxt3-tailwind-auth-app/be0720e9be3f555eb97ae207a913f295b11d1e96/public/icon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | user-agent: * 2 | allow: / 3 | 4 | # specify your sitemap location 5 | # Sitemap: https:/example.dev/sitemap.xml 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // mode: "jit", 3 | purge: [ 4 | "./components/**/*.{vue,js,ts}", 5 | "./layouts/**/*.{vue,js,ts}", 6 | "./pages/**/*.{vue,js,ts}", 7 | "./plugins/**/*.{vue,js,ts}", 8 | "./nuxt.config.{vue,js,ts}", 9 | "./app.vue", 10 | ], 11 | darkMode: false, // or 'media' or 'class' 12 | theme: { 13 | extend: {}, 14 | }, 15 | variants: { 16 | extend: {}, 17 | }, 18 | plugins: [], 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | --------------------------------------------------------------------------------