├── .gitignore ├── README.md ├── assets └── css │ └── tailwind.css ├── auth └── index.ts ├── components └── Navbar.vue ├── layouts └── default.vue ├── nuxt.config.ts ├── package.json ├── pages ├── index.vue ├── login.vue ├── refresh_token.vue └── signup.vue ├── postcss.config.js ├── prisma ├── migrations │ ├── 20211214125858_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── server-middleware ├── api │ ├── auth │ │ ├── auth.routes.ts │ │ └── auth.services.ts │ ├── index.ts │ └── users │ │ └── users.services.ts └── index.ts ├── tailwind.config.js ├── tsconfig.json ├── utils ├── db.ts ├── jwt.ts └── sendRefreshToken.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .nuxt 4 | nuxt.d.ts 5 | .output 6 | .env 7 | /prisma/dev.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Auth with express 2 | 3 | The frontend design is copied from Vuex 4 & Firebase Auth tutorial on the Net Ninja YouTube channel. 4 | -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /auth/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from "vue"; 2 | 3 | const state = ref({ accessToken: "" }); 4 | 5 | function setAccessToken(accessToken: string) { 6 | state.value.accessToken = accessToken; 7 | } 8 | 9 | const getAccessToken = computed(() => state.value.accessToken); 10 | 11 | export { setAccessToken, getAccessToken }; 12 | -------------------------------------------------------------------------------- /components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 41 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from "nuxt3"; 2 | 3 | // https://v3.nuxtjs.org/docs/directory-structure/nuxt.config 4 | export default defineNuxtConfig({ 5 | css: ["@/assets/css/tailwind.css"], 6 | build: { 7 | postcss: { 8 | postcssOptions: require("./postcss.config.js"), 9 | }, 10 | }, 11 | serverMiddleware: [ 12 | // Will register file from project server-middleware directory to handle /server-middleware/* requires 13 | { path: "/server-api", handler: "~/server-middleware/index.ts" }, 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nuxi dev", 5 | "build": "nuxi build", 6 | "start": "node .output/server/index.mjs" 7 | }, 8 | "devDependencies": { 9 | "@types/bcrypt": "^5.0.0", 10 | "@types/jsonwebtoken": "^8.5.6", 11 | "autoprefixer": "^10.4.0", 12 | "nuxt3": "latest", 13 | "postcss": "^8.4.5", 14 | "prisma": "^3.6.0", 15 | "tailwindcss": "^3.0.2" 16 | }, 17 | "dependencies": { 18 | "@prisma/client": "^3.6.0", 19 | "@types/express": "^4.17.13", 20 | "axios": "^0.24.0", 21 | "bcrypt": "^5.0.1", 22 | "express": "^4.17.1", 23 | "jsonwebtoken": "^8.5.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 77 | -------------------------------------------------------------------------------- /pages/refresh_token.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | -------------------------------------------------------------------------------- /pages/signup.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 77 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/20211214125858_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "email" TEXT NOT NULL, 5 | "password" TEXT NOT NULL 6 | ); 7 | 8 | -- CreateTable 9 | CREATE TABLE "RefreshToken" ( 10 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 11 | "token" TEXT NOT NULL, 12 | "userId" TEXT NOT NULL, 13 | CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 14 | ); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 21 | 22 | -- CreateIndex 23 | CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token"); 24 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /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 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id String @id @unique @default(uuid()) 15 | email String @unique 16 | password String 17 | refreshTokens RefreshToken[] 18 | } 19 | 20 | model RefreshToken { 21 | id Int @id @default(autoincrement()) 22 | token String @unique 23 | userId String 24 | User User @relation(fields: [userId], references: [id]) 25 | } 26 | -------------------------------------------------------------------------------- /server-middleware/api/auth/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { Prisma } from "@prisma/client"; 3 | import bcrypt from "bcrypt"; 4 | import { useCookie } from "h3"; 5 | import jwt from "jsonwebtoken"; 6 | 7 | import { 8 | createUserByEmailAndPassword, 9 | findUserByEmail, 10 | findUserById, 11 | } from "../users/users.services"; 12 | import { generateTokens } from "~/utils/jwt"; 13 | import { sendRefreshToken } from "~~/utils/sendRefreshToken"; 14 | import { 15 | createRefreshToken, 16 | deleteRefreshToken, 17 | findRefreshToken, 18 | } from "./auth.services"; 19 | 20 | const router = express.Router(); 21 | 22 | router.post("/register", async (req, res, next) => { 23 | try { 24 | const { email, password } = req.body; 25 | 26 | if (!email || !password) { 27 | res.status(403); 28 | throw new Error("Email or password is empty."); 29 | } 30 | 31 | const createUser: Prisma.UserCreateInput = { email, password }; 32 | 33 | const existingUser = await findUserByEmail(email); 34 | 35 | if (existingUser) { 36 | res.status(403); 37 | throw new Error("Email already in use."); 38 | } 39 | 40 | createUser.password = await bcrypt.hash(password, 12); 41 | 42 | const user = await createUserByEmailAndPassword(createUser); 43 | 44 | const { accessToken, refreshToken } = generateTokens(user); 45 | 46 | await createRefreshToken({ token: refreshToken, userId: user.id }); 47 | 48 | sendRefreshToken(res, refreshToken); 49 | 50 | res.json({ 51 | accessToken, 52 | }); 53 | } catch (error) { 54 | if (res.statusCode === 200) { 55 | res.status(403); 56 | } 57 | console.log(error); 58 | res.status(403).json({ message: error.message }); 59 | } 60 | }); 61 | 62 | router.post("/login", async (req, res, next) => { 63 | try { 64 | const { email, password } = req.body; 65 | 66 | if (!email || !password) { 67 | res.status(403); 68 | throw new Error("Email or password is empty."); 69 | } 70 | 71 | const createUser: Prisma.UserCreateInput = { email, password }; 72 | 73 | const existingUser = await findUserByEmail(email); 74 | 75 | if (!existingUser) { 76 | res.status(403); 77 | throw new Error("Invalid login credentials."); 78 | } 79 | 80 | const validPassword = await bcrypt.compare(password, existingUser.password); 81 | 82 | if (!validPassword) { 83 | const error = new Error("Invalid login credentials."); 84 | res.status(403); 85 | throw error; 86 | } 87 | 88 | const { accessToken, refreshToken } = generateTokens(existingUser); 89 | 90 | await createRefreshToken({ 91 | token: refreshToken, 92 | userId: existingUser.id, 93 | }); 94 | 95 | sendRefreshToken(res, refreshToken); 96 | 97 | res.json({ 98 | accessToken, 99 | }); 100 | } catch (error) { 101 | if (res.statusCode === 200) { 102 | res.status(403); 103 | } 104 | 105 | res.status(403).json({ message: error.message }); 106 | } 107 | }); 108 | 109 | router.post("/refresh_token", async (req, res, next) => { 110 | try { 111 | const token = useCookie(req, "refresh_token"); 112 | if (!token) { 113 | res.status(401); 114 | throw new Error("Token missing"); 115 | } 116 | let payload: any = null; 117 | payload = jwt.verify(token, process.env.JWT_REFRESH_SECRET!); 118 | 119 | const user = await findUserById(payload.userId); 120 | 121 | if (!user) { 122 | res.status(401); 123 | throw new Error("Not authorized"); 124 | } 125 | 126 | const savedRefreshToken = await findRefreshToken(token); 127 | 128 | if (!savedRefreshToken) { 129 | res.status(401); 130 | throw new Error("Not authorized."); 131 | } 132 | 133 | if (savedRefreshToken.userId !== payload.userId) { 134 | res.status(401); 135 | throw new Error("Not authorized"); 136 | } 137 | 138 | const { accessToken, refreshToken } = generateTokens(user); 139 | 140 | await deleteRefreshToken(token); 141 | await createRefreshToken({ token: refreshToken, userId: payload.userId }); 142 | 143 | sendRefreshToken(res, refreshToken); 144 | 145 | res.json({ accessToken }); 146 | } catch (error) { 147 | if (res.statusCode === 200) { 148 | res.status(401); 149 | } 150 | sendRefreshToken(res, ""); 151 | res.status(401).json({ message: error.message }); 152 | } 153 | }); 154 | 155 | router.post("/logout", async (req, res, next) => { 156 | const token = useCookie(req, "refresh_token"); 157 | await deleteRefreshToken(token); 158 | sendRefreshToken(res, ""); 159 | res.status(200); 160 | res.json({ 161 | data: "success", 162 | }); 163 | }); 164 | 165 | export default router; 166 | -------------------------------------------------------------------------------- /server-middleware/api/auth/auth.services.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { db } from "~~/utils/db"; 3 | 4 | export const createRefreshToken = ( 5 | refreshTokenPayload: Prisma.RefreshTokenUncheckedCreateInput 6 | ) => { 7 | return db.refreshToken.create({ 8 | data: refreshTokenPayload, 9 | }); 10 | }; 11 | 12 | export const findRefreshToken = (token: string) => { 13 | return db.refreshToken.findUnique({ 14 | where: { 15 | token, 16 | }, 17 | }); 18 | }; 19 | 20 | export const deleteRefreshToken = (token: string) => { 21 | return db.refreshToken.delete({ 22 | where: { 23 | token, 24 | }, 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /server-middleware/api/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import auth from "./auth/auth.routes"; 3 | 4 | const router = express.Router(); 5 | 6 | router.get("/", (req, res) => { 7 | res.json({ 8 | message: "API - 👋🌎🌍🌏", 9 | }); 10 | }); 11 | 12 | router.use("/auth", auth); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /server-middleware/api/users/users.services.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { db } from "~~/utils/db"; 3 | 4 | export const findUserByEmail = (email) => { 5 | return db.user.findUnique({ 6 | where: { 7 | email, 8 | }, 9 | }); 10 | }; 11 | 12 | export const createUserByEmailAndPassword = (user: Prisma.UserCreateInput) => { 13 | return db.user.create({ 14 | data: user, 15 | }); 16 | }; 17 | 18 | export const findUserById = (id: string) => { 19 | return db.user.findUnique({ 20 | where: { 21 | id, 22 | }, 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /server-middleware/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import api from "./api"; 3 | const app = express(); 4 | 5 | app.use(express.json()); 6 | app.use("/v1", api); 7 | 8 | app.get("/", (req, res) => { 9 | res.json({ 10 | message: "🦄🌈✨👋🌎🌍🌏✨🌈🦄", 11 | }); 12 | }); 13 | 14 | export default app; 15 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./components/**/*.{js,vue,ts}", 4 | "./layouts/**/*.vue", 5 | "./pages/**/*.vue", 6 | "./plugins/**/*.{js,ts}", 7 | "./nuxt.config.{js,ts}", 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /utils/db.ts: -------------------------------------------------------------------------------- 1 | // https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#prismaclient-in-long-running-applications 2 | 3 | import Prisma from "@prisma/client"; 4 | const { PrismaClient } = Prisma; 5 | // add prisma to the NodeJS global type 6 | interface CustomNodeJsGlobal extends NodeJS.Global { 7 | __db: Prisma.PrismaClient; 8 | } 9 | 10 | // Prevent multiple instances of Prisma Client in development 11 | declare const global: CustomNodeJsGlobal; 12 | 13 | const db = global.__db || new PrismaClient(); 14 | 15 | if (process.env.NODE_ENV === "development") global.__db = db; 16 | 17 | export { db }; 18 | -------------------------------------------------------------------------------- /utils/jwt.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import jwt from "jsonwebtoken"; 3 | 4 | export function generateAccessToken(user: User) { 5 | return jwt.sign({ userId: user.id }, process.env.JWT_ACCESS_SECRET!, { 6 | expiresIn: "10m", 7 | }); 8 | } 9 | 10 | export function generateRefreshToken(user: User) { 11 | return jwt.sign({ userId: user.id }, process.env.JWT_REFRESH_SECRET!, { 12 | expiresIn: "4h", 13 | }); 14 | } 15 | 16 | export function generateTokens(user: User) { 17 | const accessToken = generateAccessToken(user); 18 | const refreshToken = generateRefreshToken(user); 19 | 20 | return { 21 | accessToken, 22 | refreshToken, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /utils/sendRefreshToken.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import { setCookie } from "h3"; 3 | 4 | export const sendRefreshToken = (res: Response, token: string) => { 5 | setCookie(res, "refresh_token", token, { 6 | httpOnly: true, 7 | sameSite: true, 8 | path: "/server-api/v1/auth", 9 | }); 10 | }; 11 | --------------------------------------------------------------------------------