├── src ├── db │ ├── schema.js │ ├── index.js │ ├── tokens.schema.js │ └── users.schema.js ├── services │ ├── index.js │ ├── user.service.js │ └── token.service.js ├── controllers │ ├── index.js │ ├── user.controller.js │ └── auth.controller.js ├── config │ ├── tokens.js │ ├── logger.js │ ├── passport.js │ └── config.js ├── utils │ └── api-error.js ├── middlewares │ ├── morgan.middleware.js │ ├── auth.middleware.js │ ├── validate.middleware.js │ └── error.middleware.js ├── routes │ ├── index.js │ ├── user.route.js │ └── auth.route.js └── app.js ├── drizzle ├── migrations │ ├── meta │ │ ├── _journal.json │ │ └── 0000_snapshot.json │ └── 0000_lowly_scorpion.sql └── migrate.js ├── ecosystem.config.json ├── compose.yaml ├── drizzle.config.js ├── .env.example ├── docs ├── swagger-def.js └── components.yml ├── index.js └── .gitignore /src/db/schema.js: -------------------------------------------------------------------------------- 1 | export * from "./users.schema.js"; 2 | export * from "./tokens.schema.js"; 3 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | export * as userService from "./user.service.js"; 2 | export * as tokenService from "./token.service.js"; 3 | -------------------------------------------------------------------------------- /src/controllers/index.js: -------------------------------------------------------------------------------- 1 | export * as authController from "./auth.controller.js"; 2 | export * as userController from "./user.controller.js"; 3 | -------------------------------------------------------------------------------- /src/config/tokens.js: -------------------------------------------------------------------------------- 1 | export const TOKEN_TYPES = { 2 | ACCESS: "access", 3 | REFRESH: "refresh", 4 | RESET_PASSWORD: "resetPassword", 5 | }; 6 | -------------------------------------------------------------------------------- /src/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | import { userService } from "../services/index.js"; 2 | 3 | export async function getUserProfile(req, res) { 4 | const result = await userService.getUserById(req.user.id); 5 | res.send(result); 6 | } 7 | -------------------------------------------------------------------------------- /drizzle/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1714731308024, 9 | "tag": "0000_lowly_scorpion", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /ecosystem.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "app", 5 | "script": "index.js", 6 | "instances": 1, 7 | "autorestart": true, 8 | "watch": false, 9 | "time": true, 10 | "env": { 11 | "NODE_ENV": "production" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | postgres: 5 | image: postgres 6 | restart: unless-stopped 7 | container_name: node-auth-postgres 8 | environment: 9 | POSTGRES_DB: ${POSTGRESQL_DB_NAME} 10 | POSTGRES_USER: ${POSTGRESQL_USER} 11 | POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD} 12 | ports: 13 | - 5432:5432 14 | -------------------------------------------------------------------------------- /src/utils/api-error.js: -------------------------------------------------------------------------------- 1 | class ApiError extends Error { 2 | constructor(statusCode, message, isOperational = true, stack = "") { 3 | super(message); 4 | this.statusCode = statusCode; 5 | this.isOperational = isOperational; 6 | if (stack) { 7 | this.stack = stack; 8 | } else { 9 | Error.captureStackTrace(this, this.constructor); 10 | } 11 | } 12 | } 13 | 14 | export default ApiError; 15 | -------------------------------------------------------------------------------- /src/middlewares/morgan.middleware.js: -------------------------------------------------------------------------------- 1 | import morgan from "morgan"; 2 | 3 | import logger from "../config/logger.js"; 4 | 5 | export const morganMiddleware = morgan( 6 | ":method :url :status :res[content-length] - :response-time ms", 7 | { 8 | stream: { 9 | // Configure Morgan to use our custom logger with the http severity 10 | write: (message) => logger.http(message.trim()), 11 | }, 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /drizzle.config.js: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | export default { 4 | schema: "src/db/schema.js", 5 | out: "drizzle/migrations", 6 | driver: "pg", 7 | dbCredentials: { 8 | host: process.env.POSTGRESQL_HOST, 9 | port: +(process.env.POSTGRESQL_PORT || "5432"), 10 | database: process.env.POSTGRESQL_DB_NAME, 11 | user: process.env.POSTGRESQL_USER, 12 | password: process.env.POSTGRESQL_PASSWORD, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import { userRoutes } from "./user.route.js"; 4 | import { authRoutes } from "./auth.route.js"; 5 | import { authMiddleware } from "../middlewares/auth.middleware.js"; 6 | 7 | const router = Router(); 8 | 9 | // auth routes 10 | router.use("/auth", authRoutes); 11 | 12 | // user routes 13 | router.use("/users", authMiddleware, userRoutes); 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Port number 2 | PORT=3000 3 | API_HOST=http://localhost:3000 4 | 5 | # Database Credentials 6 | POSTGRESQL_HOST= 7 | POSTGRESQL_PORT= 8 | POSTGRESQL_DB_NAME= 9 | POSTGRESQL_USER= 10 | POSTGRESQL_PASSWORD= 11 | 12 | # JWT 13 | JWT_SECRET= 14 | JWT_ACCESS_EXPIRATION_MINUTES=30 15 | JWT_REFRESH_EXPIRATION_DAYS=30 16 | JWT_RESET_PASSWORD_EXPIRATION_MINUTES=10 17 | JWT_VERIFY_EMAIL_EXPIRATION_MINUTES=10 -------------------------------------------------------------------------------- /docs/swagger-def.js: -------------------------------------------------------------------------------- 1 | // !NODE: --experimental-json-modules 2 | // import packageData from "../package.json" with { type: "json" }; 3 | import fs from "node:fs"; 4 | import env from "../src/config/config.js"; 5 | 6 | const packageData = JSON.parse(fs.readFileSync("./package.json")); 7 | 8 | const swaggerDef = { 9 | openapi: "3.1.0", 10 | info: { 11 | title: "node-express-drizzle API documentation", 12 | version: packageData.version, 13 | }, 14 | servers: [ 15 | { 16 | url: `${env.apiHost}/api`, 17 | }, 18 | ], 19 | }; 20 | 21 | export default swaggerDef; 22 | -------------------------------------------------------------------------------- /src/middlewares/auth.middleware.js: -------------------------------------------------------------------------------- 1 | import passport from "passport"; 2 | import httpStatus from "http-status"; 3 | 4 | import ApiError from "../utils/api-error.js"; 5 | 6 | export function authMiddleware(req, res, next) { 7 | const authenticateOption = { session: false }; 8 | 9 | passport.authenticate("jwt", authenticateOption, (err, user, info) => { 10 | if (err || info || !user) { 11 | next( 12 | new ApiError(httpStatus.UNAUTHORIZED, err || info || "no user found") 13 | ); 14 | } 15 | req.user = user; 16 | 17 | next(); 18 | })(req, res, next); 19 | } 20 | -------------------------------------------------------------------------------- /src/middlewares/validate.middleware.js: -------------------------------------------------------------------------------- 1 | import httpStatus from "http-status"; 2 | 3 | export function validateMiddleware(schema) { 4 | return async function validate(req, res, next) { 5 | try { 6 | const data = await schema.parseAsync({ 7 | body: req.body, 8 | query: req.query, 9 | params: req.params, 10 | }); 11 | req.body = data.body; 12 | req.query = data.query; 13 | req.params = data.params; 14 | return next(); 15 | } catch (error) { 16 | return res.status(httpStatus.BAD_REQUEST).json(error); 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /drizzle/migrate.js: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { drizzle } from "drizzle-orm/postgres-js"; 3 | import { migrate } from "drizzle-orm/postgres-js/migrator"; 4 | import postgres from "postgres"; 5 | 6 | async function main() { 7 | const client = postgres({ 8 | max: 1, 9 | host: process.env.POSTGRESQL_HOST, 10 | port: +(process.env.POSTGRESQL_PORT || "5432"), 11 | database: process.env.POSTGRESQL_DB_NAME, 12 | user: process.env.POSTGRESQL_USER, 13 | password: process.env.POSTGRESQL_PASSWORD, 14 | }); 15 | 16 | const db = drizzle(client); 17 | 18 | await migrate(db, { migrationsFolder: "./drizzle/migrations" }); 19 | await client.end(); 20 | } 21 | 22 | main(); 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { sql } from "drizzle-orm"; 2 | 3 | import app from "./src/app.js"; 4 | import env from "./src/config/config.js"; 5 | import logger from "./src/config/logger.js"; 6 | import { db } from "./src/db/index.js"; 7 | 8 | // start the server 9 | const server = app.listen(env.port, () => { 10 | logger.info(`Listening to port ${env.port}`); 11 | }); 12 | 13 | // check database connection 14 | try { 15 | const res = await db 16 | .execute(sql`select 1 as connection`) 17 | .then((res) => res[0]); 18 | if (!res.connection) { 19 | throw new Error("Not connected to database"); 20 | } 21 | logger.info("Connected to database"); 22 | } catch (err) { 23 | logger.error("Failed to connect to database:", err); 24 | server.close(); 25 | } 26 | -------------------------------------------------------------------------------- /src/db/index.js: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/postgres-js"; 2 | import postgres from "postgres"; 3 | 4 | import env from "../config/config.js"; 5 | import logger from "../config/logger.js"; 6 | import * as schema from "./schema.js"; 7 | 8 | const postgresClient = postgres({ 9 | host: env.db.host, 10 | port: env.db.port, 11 | user: env.db.user, 12 | password: env.db.password, 13 | database: env.db.database, 14 | }); 15 | 16 | export const db = drizzle(postgresClient, { 17 | schema, 18 | logger: { 19 | logQuery(query, params) { 20 | if (env.nodeEnv !== "production") { 21 | const message = query + (params.length ? ` -- params: ${params}` : ""); 22 | logger.info(message); 23 | } 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /src/config/logger.js: -------------------------------------------------------------------------------- 1 | import { format, transports, createLogger } from "winston"; 2 | import "winston-daily-rotate-file"; 3 | 4 | const { combine, json, colorize, simple } = format; 5 | 6 | const fileRotateTransport = new transports.DailyRotateFile({ 7 | filename: "logs/combined-%DATE%.log", 8 | datePattern: "YYYY-MM-DD", 9 | maxFiles: "14d", 10 | }); 11 | 12 | const logger = createLogger({ 13 | level: "silly", 14 | format: json(), 15 | transports: [ 16 | fileRotateTransport, 17 | new transports.File({ 18 | filename: "logs/error.log", 19 | level: "error", 20 | }), 21 | ], 22 | }); 23 | 24 | if (process.env.NODE_ENV !== "production") { 25 | logger.add( 26 | new transports.Console({ 27 | format: combine(colorize(), simple()), 28 | }) 29 | ); 30 | } 31 | 32 | export default logger; 33 | -------------------------------------------------------------------------------- /src/config/passport.js: -------------------------------------------------------------------------------- 1 | import { Strategy, ExtractJwt } from "passport-jwt"; 2 | import { eq } from "drizzle-orm"; 3 | 4 | import env from "./config.js"; 5 | import { TOKEN_TYPES } from "./tokens.js"; 6 | import { users } from "../db/schema.js"; 7 | import { db } from "../db/index.js"; 8 | 9 | const jwtOptions = { 10 | secretOrKey: env.jwt.secret, 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | }; 13 | 14 | async function jwtVerify(payload, done) { 15 | try { 16 | if (payload.type !== TOKEN_TYPES.ACCESS) { 17 | throw new Error("Invalid token type"); 18 | } 19 | const user = await db.query.users.findFirst({ 20 | where: eq(users.id, payload.sub), 21 | }); 22 | if (!user) { 23 | return done(null, false); 24 | } 25 | done(null, user); 26 | } catch (error) { 27 | done(error, false); 28 | } 29 | } 30 | 31 | export const jwtStrategy = new Strategy(jwtOptions, jwtVerify); 32 | -------------------------------------------------------------------------------- /src/routes/user.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import { userController } from "../controllers/index.js"; 4 | import { authMiddleware } from "../middlewares/auth.middleware.js"; 5 | 6 | /** 7 | * @swagger 8 | * tags: 9 | * name: User 10 | * description: User 11 | */ 12 | const userRoutes = Router(); 13 | 14 | /** 15 | * @swagger 16 | * /users/profile: 17 | * get: 18 | * summary: Get a user 19 | * description: Logged in users can fetch only their own user information. 20 | * tags: [User] 21 | * security: 22 | * - bearerAuth: [] 23 | * responses: 24 | * "200": 25 | * description: OK 26 | * content: 27 | * application/json: 28 | * schema: 29 | * $ref: '#/components/schemas/User' 30 | * "404": 31 | * $ref: '#/components/responses/NotFound' 32 | */ 33 | userRoutes.route("/profile").get(userController.getUserProfile); 34 | 35 | export { userRoutes }; 36 | -------------------------------------------------------------------------------- /src/db/tokens.schema.js: -------------------------------------------------------------------------------- 1 | import { sql } from "drizzle-orm"; 2 | import { text, timestamp, pgTable, uuid, pgEnum } from "drizzle-orm/pg-core"; 3 | import { createInsertSchema } from "drizzle-zod"; 4 | import { z } from "zod"; 5 | 6 | import { users } from "./users.schema.js"; 7 | import { TOKEN_TYPES } from "../config/tokens.js"; 8 | 9 | export const tokenTypes = pgEnum("token_types", [ 10 | TOKEN_TYPES.REFRESH, 11 | TOKEN_TYPES.RESET_PASSWORD, 12 | ]); 13 | 14 | export const tokens = pgTable("tokens", { 15 | id: uuid("id") 16 | .primaryKey() 17 | .default(sql`gen_random_uuid()`), 18 | userId: uuid("user_id") 19 | .notNull() 20 | .references(() => users.id, { onDelete: "cascade" }), 21 | token: text("token").notNull(), 22 | type: tokenTypes("type").notNull(), 23 | expires: timestamp("expires").notNull(), 24 | createdAt: timestamp("created_at").notNull().defaultNow(), 25 | }); 26 | 27 | export const insertTokenSchema = z.object({ body: createInsertSchema(tokens) }); 28 | 29 | export const refreshTokenSchema = z.object({ 30 | body: z.object({ 31 | refreshToken: z.string(), 32 | }), 33 | }); 34 | 35 | export const logoutTokenSchema = refreshTokenSchema; 36 | -------------------------------------------------------------------------------- /drizzle/migrations/0000_lowly_scorpion.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | CREATE TYPE "token_types" AS ENUM('refresh', 'resetPassword'); 3 | EXCEPTION 4 | WHEN duplicate_object THEN null; 5 | END $$; 6 | --> statement-breakpoint 7 | CREATE TABLE IF NOT EXISTS "users" ( 8 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 9 | "username" text NOT NULL, 10 | "email" text NOT NULL, 11 | "password" text NOT NULL, 12 | "first_name" text, 13 | "last_name" text, 14 | "address" text, 15 | "phone" text, 16 | "image" text, 17 | "created_at" timestamp DEFAULT now() NOT NULL, 18 | "updated_at" timestamp DEFAULT now() NOT NULL, 19 | CONSTRAINT "users_username_unique" UNIQUE("username"), 20 | CONSTRAINT "users_email_unique" UNIQUE("email") 21 | ); 22 | --> statement-breakpoint 23 | CREATE TABLE IF NOT EXISTS "tokens" ( 24 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 25 | "user_id" uuid NOT NULL, 26 | "token" text NOT NULL, 27 | "type" "token_types" NOT NULL, 28 | "expires" timestamp NOT NULL, 29 | "created_at" timestamp DEFAULT now() NOT NULL 30 | ); 31 | --> statement-breakpoint 32 | DO $$ BEGIN 33 | ALTER TABLE "tokens" ADD CONSTRAINT "tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; 34 | EXCEPTION 35 | WHEN duplicate_object THEN null; 36 | END $$; 37 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | import "dotenv/config.js"; 2 | import z from "zod"; 3 | 4 | import logger from "./logger.js"; 5 | 6 | const EnvVariables = z.object({ 7 | PORT: z.preprocess((a) => +a, z.number().optional().default(5000)), 8 | POSTGRESQL_HOST: z.string(), 9 | POSTGRESQL_PORT: z.preprocess((a) => +a, z.number().optional().default(5432)), 10 | POSTGRESQL_DB_NAME: z.string(), 11 | POSTGRESQL_USER: z.string(), 12 | POSTGRESQL_PASSWORD: z.string(), 13 | JWT_SECRET: z.string(), 14 | JWT_ACCESS_EXPIRATION_MINUTES: z.preprocess((a) => +a, z.number().default(30)), 15 | JWT_REFRESH_EXPIRATION_DAYS: z.preprocess((a) => +a, z.number().default(30)), 16 | JWT_RESET_PASSWORD_EXPIRATION_MINUTES: z.preprocess((a) => +a, z.number().default(10)), 17 | JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: z.preprocess((a) => +a, z.number().default(10)), 18 | }); 19 | 20 | let envVars; 21 | 22 | try { 23 | envVars = EnvVariables.parse(process.env); 24 | } catch (error) { 25 | logger.error(`Env validation error: ${error}`); 26 | process.exit(1); 27 | } 28 | 29 | const env = { 30 | nodeEnv: process.env.NODE_ENV, 31 | apiHost: process.env.API_HOST, 32 | port: envVars.PORT, 33 | db: { 34 | host: envVars.POSTGRESQL_HOST, 35 | port: envVars.POSTGRESQL_PORT, 36 | database: envVars.POSTGRESQL_DB_NAME, 37 | user: envVars.POSTGRESQL_USER, 38 | password: envVars.POSTGRESQL_PASSWORD, 39 | }, 40 | jwt: { 41 | secret: envVars.JWT_SECRET, 42 | accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES, 43 | refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS, 44 | resetPasswordExpirationMinutes: 45 | envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES, 46 | verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES, 47 | }, 48 | }; 49 | 50 | export default env; 51 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import helmet from "helmet"; 3 | import bodyParser from "body-parser"; 4 | import compression from "compression"; 5 | import cors from "cors"; 6 | import passport from "passport"; 7 | import swaggerJsdoc from "swagger-jsdoc"; 8 | import swaggerUi from "swagger-ui-express"; 9 | 10 | import routes from "./routes/index.js"; 11 | import swaggerDefinition from "../docs/swagger-def.js"; 12 | import * as errorMiddleware from "./middlewares/error.middleware.js"; 13 | import { morganMiddleware } from "./middlewares/morgan.middleware.js"; 14 | import { jwtStrategy } from "./config/passport.js"; 15 | 16 | const app = express(); 17 | 18 | // request logging. 19 | app.use(morganMiddleware); 20 | 21 | // set security HTTP headers 22 | app.use(helmet()); 23 | 24 | // parse body params and attache them to req.body 25 | app.use(bodyParser.json()); 26 | app.use(bodyParser.urlencoded({ extended: true })); 27 | 28 | // compress all responses 29 | app.use(compression()); 30 | 31 | // secure apps by setting various HTTP headers 32 | app.use(helmet()); 33 | 34 | // enable cors 35 | app.use(cors()); 36 | 37 | // jwt authentication 38 | app.use(passport.initialize()); 39 | passport.use("jwt", jwtStrategy); 40 | 41 | // api routes 42 | app.use("/api", routes); 43 | 44 | // api-docs 45 | const specs = swaggerJsdoc({ 46 | swaggerDefinition, 47 | apis: ["docs/*.yml", "src/routes/*.js"], 48 | }); 49 | app.use("/api-docs", swaggerUi.serve); 50 | app.get("/api-docs", swaggerUi.setup(specs, { explorer: true })); 51 | 52 | // if error is not an instanceOf APIError, convert it. 53 | app.use(errorMiddleware.converter); 54 | 55 | // catch 404 and forward 56 | app.use(errorMiddleware.notFound); 57 | 58 | // error handler, send stacktrace only during development/local env 59 | app.use(errorMiddleware.handler); 60 | 61 | export default app; 62 | -------------------------------------------------------------------------------- /src/services/user.service.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | import { eq, or } from "drizzle-orm"; 3 | 4 | import { db } from "../db/index.js"; 5 | import { users } from "../db/schema.js"; 6 | 7 | /** 8 | * Create a user 9 | * @param {Object} data 10 | * @returns {Promise} 11 | */ 12 | export async function createUser(data) { 13 | // !NOTE: hashing password in the orm events like mongoose and sequelize will be great but currently drizzle orm doesn't support events 14 | data.password = await bcrypt.hash(data.password, 10); 15 | return db 16 | .insert(users) 17 | .values(data) 18 | .returning({ id: users.id, username: users.username, email: users.email }) 19 | .then((res) => res[0]); 20 | } 21 | 22 | /** 23 | * Create a user 24 | * @param {string} usernameOrEmail 25 | * @returns {Promise} 26 | */ 27 | export function getUserByUsernameOrEmail(usernameOrEmail) { 28 | return db.query.users.findFirst({ 29 | where: or( 30 | eq(users.email, usernameOrEmail), 31 | eq(users.username, usernameOrEmail) 32 | ), 33 | }); 34 | } 35 | 36 | /** 37 | * Get user by id 38 | * @param {ObjectId} id 39 | * @returns {Promise} 40 | */ 41 | export function getUserById(id) { 42 | return db.query.users.findFirst({ where: eq(users.id, id) }).then((res) => { 43 | delete res.password; 44 | return res; 45 | }); 46 | } 47 | 48 | /** 49 | * Get user by id 50 | * @param {string} id 51 | * @param {Object} data 52 | * @returns {Promise} 53 | */ 54 | export async function updateUserById(id, data) { 55 | if (data.password) { 56 | // !NOTE: hashing password in the orm events like mongoose and sequelize will be great but currently drizzle orm doesn't support events 57 | data.password = await bcrypt.hash(data.password, 10); 58 | } 59 | return db.update(users).set(data).where(eq(users.id, id)); 60 | } 61 | -------------------------------------------------------------------------------- /src/db/users.schema.js: -------------------------------------------------------------------------------- 1 | import { sql } from "drizzle-orm"; 2 | import { text, timestamp, pgTable, uuid } from "drizzle-orm/pg-core"; 3 | import { createInsertSchema } from "drizzle-zod"; 4 | import { z } from "zod"; 5 | 6 | export const users = pgTable("users", { 7 | id: uuid("id") 8 | .primaryKey() 9 | .default(sql`gen_random_uuid()`), 10 | username: text("username").notNull().unique(), 11 | email: text("email").notNull().unique(), 12 | password: text("password").notNull(), 13 | firstName: text("first_name"), 14 | lastName: text("last_name"), 15 | address: text("address"), 16 | phone: text("phone"), 17 | image: text("image"), 18 | createdAt: timestamp("created_at").notNull().defaultNow(), 19 | updatedAt: timestamp("updated_at") 20 | .notNull() 21 | .defaultNow() 22 | .$onUpdateFn(() => new Date()), 23 | }); 24 | 25 | const validPassword = z 26 | .string() 27 | .min(6, "Password must have 6 characters") 28 | .refine( 29 | (value) => /[-._!"`'#%&,:;<>=@{}~\$\(\)\*\+\/\\\?\[\]\^\|]+/.test(value), 30 | "Password must contain atleast one special character" 31 | ) 32 | .refine( 33 | (value) => /[A-Z]/.test(value), 34 | "Password must contain at least one uppercase letter" 35 | ) 36 | .refine( 37 | (value) => /[0-9]/.test(value), 38 | "Password must contain at least one number" 39 | ) 40 | .refine( 41 | (value) => /[a-z]/.test(value), 42 | "Password must contain at least one lowercase letter" 43 | ); 44 | 45 | export const insertUserSchema = z.object({ 46 | body: createInsertSchema(users, { 47 | email: (schema) => schema.email.email(), 48 | password: validPassword, 49 | }), 50 | }); 51 | 52 | export const loginUserSchema = z.object({ 53 | body: z.object({ 54 | usernameOrEmail: z.string(), 55 | password: z.string(), 56 | }), 57 | }); 58 | 59 | export const forgotPasswordSchema = z.object({ 60 | body: z.object({ 61 | usernameOrEmail: z.string(), 62 | }), 63 | }); 64 | 65 | export const resetPasswordSchema = z.object({ 66 | body: z.object({ 67 | token: z.string(), 68 | password: validPassword, 69 | }), 70 | }); 71 | -------------------------------------------------------------------------------- /src/middlewares/error.middleware.js: -------------------------------------------------------------------------------- 1 | import httpStatus from "http-status"; 2 | 3 | import APIError from "../utils/api-error.js"; 4 | import env from "../config/config.js"; 5 | import logger from "../config/logger.js"; 6 | import ApiError from "../utils/api-error.js"; 7 | 8 | /** 9 | * Error handler. Send stacktrace only during local 10 | * @public 11 | */ 12 | export const handler = (err, req, res, next) => { 13 | let { statusCode, message } = err; 14 | 15 | const response = { 16 | code: statusCode, 17 | message, 18 | ...(env.nodeEnv !== "production" && { stack: err.stack }), 19 | }; 20 | 21 | if (env.nodeEnv !== "production") { 22 | logger.log( 23 | statusCode === httpStatus.NOT_FOUND ? "silly" : "error", 24 | "Error handler error log:", 25 | err 26 | ); 27 | } 28 | 29 | res.status(statusCode).send(response); 30 | }; 31 | 32 | /** 33 | * If error is not an instanceOf APIError, convert it. 34 | * @public 35 | */ 36 | export const converter = (error, req, res, next) => { 37 | // !NOTE: error instanceof PostgresError not working PostgresError is not exported from postgres package 38 | if (error.constructor.name === "PostgresError") { 39 | if (error.routine === "_bt_check_unique") { 40 | const [key, value] = error.detail 41 | .match(/\(([^)]+)\)/g) 42 | .map((x) => x.replace(/[()]/g, "")); 43 | const message = `${key} ${value} already exists. Please try again using different value`; 44 | throw new ApiError(httpStatus.BAD_REQUEST, message, false, error.stack); 45 | } 46 | const message = error.message || httpStatus[statusCode]; 47 | throw new ApiError( 48 | httpStatus.INTERNAL_SERVER_ERROR, 49 | message, 50 | false, 51 | error.stack 52 | ); 53 | } 54 | 55 | if (!(error instanceof ApiError)) { 56 | const statusCode = error.statusCode 57 | ? httpStatus.BAD_REQUEST 58 | : httpStatus.INTERNAL_SERVER_ERROR; 59 | const message = error.message || httpStatus[statusCode]; 60 | throw new ApiError(statusCode, message, false, error.stack); 61 | } 62 | 63 | next(error); 64 | }; 65 | 66 | /** 67 | * Catch 404 and forward to error handler 68 | * @public 69 | */ 70 | export const notFound = (req, res, next) => { 71 | const error = new APIError(httpStatus.NOT_FOUND, "Not found"); 72 | next(error); 73 | }; 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # turbo 133 | .turbo -------------------------------------------------------------------------------- /docs/components.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | User: 4 | type: object 5 | properties: 6 | id: 7 | type: string 8 | username: 9 | type: string 10 | email: 11 | type: string 12 | format: email 13 | firstName: 14 | type: string 15 | lastName: 16 | type: string 17 | address: 18 | type: string 19 | phone: 20 | type: string 21 | image: 22 | type: string 23 | createdAt: 24 | type: string 25 | format: date-time 26 | updatedAt: 27 | type: string 28 | format: date-time 29 | example: 30 | id: bb85787c-cfa9-431a-ae63-29878e2f9141 31 | username: test 32 | email: test@test.com 33 | firstName: test 34 | lastName: test 35 | address: some place 36 | phone: +91 00000000 37 | image: some image link 38 | createdAt: 2024-05-2T16:18:04.793Z 39 | updatedAt: 2024-05-2T16:18:04.793Z 40 | 41 | Token: 42 | type: object 43 | properties: 44 | token: 45 | type: string 46 | expires: 47 | type: string 48 | format: date-time 49 | example: 50 | token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YzcxYWQ0Zi05MDY5LTRkNTEtOTI4Ni1mM2FkYTQzNzI5MTAiLCJpYXQiOjE3MTQ3MzE0NTcsImV4cCI6MTcxNDczMzI1NywidHlwZSI6ImFjY2VzcyJ9.ZygGnanFeO-96I7RAV6HLMm5vGlfSxm1BKr4Ag-FwH8 51 | expires: 2024-05-2T16:18:04.793Z 52 | 53 | AuthTokens: 54 | type: object 55 | properties: 56 | access: 57 | $ref: "#/components/schemas/Token" 58 | refresh: 59 | $ref: "#/components/schemas/Token" 60 | 61 | Error: 62 | type: object 63 | properties: 64 | code: 65 | type: number 66 | message: 67 | type: string 68 | 69 | responses: 70 | DuplicateError: 71 | description: ${key} ${value} already exists. Please try again using different value 72 | content: 73 | application/json: 74 | schema: 75 | $ref: "#/components/schemas/Error" 76 | example: 77 | code: 400 78 | message: ${key} ${value} already exists. Please try again using different value 79 | Unauthorized: 80 | description: Unauthorized 81 | content: 82 | application/json: 83 | schema: 84 | $ref: "#/components/schemas/Error" 85 | example: 86 | code: 401 87 | message: Please authenticate 88 | NotFound: 89 | description: Not found 90 | content: 91 | application/json: 92 | schema: 93 | $ref: "#/components/schemas/Error" 94 | example: 95 | code: 404 96 | message: Not found 97 | 98 | securitySchemes: 99 | bearerAuth: 100 | type: http 101 | scheme: bearer 102 | bearerFormat: JWT 103 | -------------------------------------------------------------------------------- /src/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | import httpStatus from "http-status"; 3 | 4 | import { tokenService, userService } from "../services/index.js"; 5 | import ApiError from "../utils/api-error.js"; 6 | import { TOKEN_TYPES } from "../config/tokens.js"; 7 | 8 | export async function register(req, res, next) { 9 | try { 10 | const user = await userService.createUser(req.body); 11 | const tokens = await tokenService.generateAuthTokens(user); 12 | res.status(httpStatus.CREATED).send({ user, tokens }); 13 | } catch (error) { 14 | next(error); 15 | } 16 | } 17 | 18 | export async function login(req, res, next) { 19 | try { 20 | const user = await userService.getUserByUsernameOrEmail( 21 | req.body.usernameOrEmail 22 | ); 23 | if (!user) { 24 | throw new ApiError(httpStatus.UNAUTHORIZED, "User not found"); 25 | } 26 | const compare = await bcrypt.compare(req.body.password, user.password); 27 | if (!compare) { 28 | throw new ApiError(httpStatus.UNAUTHORIZED, "username/email and password didn't match"); 29 | } 30 | const tokens = await tokenService.generateAuthTokens(user); 31 | delete user.password; 32 | return res.json({ user, tokens }); 33 | } catch (error) { 34 | next(error); 35 | } 36 | } 37 | 38 | export async function logout(req, res, next) { 39 | try { 40 | const token = await tokenService.findToken( 41 | req.body.refreshToken, 42 | TOKEN_TYPES.REFRESH 43 | ); 44 | if (!token) { 45 | throw new ApiError(httpStatus.BAD_REQUEST, "token not found"); 46 | } 47 | await tokenService.deleteToken(token.id); 48 | res.status(httpStatus.NO_CONTENT).send(); 49 | } catch (error) { 50 | next(error); 51 | } 52 | } 53 | 54 | 55 | export async function refreshTokens(req, res, next) { 56 | try { 57 | const tokens = await tokenService.refreshAuth(req.body.refreshToken); 58 | return res.json({ tokens }); 59 | } catch (error) { 60 | next(error); 61 | } 62 | } 63 | 64 | export async function forgotPassword(req, res, next) { 65 | try { 66 | const user = await userService.getUserByUsernameOrEmail( 67 | req.body.usernameOrEmail 68 | ); 69 | if (!user) { 70 | throw new ApiError(httpStatus.UNAUTHORIZED, "User not found"); 71 | } 72 | const resetPasswordToken = await tokenService.generateResetPasswordToken( 73 | user.email 74 | ); 75 | // ? mail the resetPasswordToken 76 | console.log("===========resetPasswordToken=========="); 77 | console.log(resetPasswordToken); 78 | console.log("===========resetPasswordToken=========="); 79 | 80 | // !NOTE: change the logic and send the resetPasswordToken in email instead in response 81 | res 82 | .status(httpStatus.METHOD_NOT_ALLOWED) 83 | .send({ 84 | message: 85 | "not a real server error", 86 | description: "copy the resetPasswordToken from server console or use UNSAFE_RESET_PASSWORD_TOKEN", 87 | UNSAFE_RESET_PASSWORD_TOKEN: resetPasswordToken, 88 | }); 89 | } catch (error) { 90 | next(error); 91 | } 92 | } 93 | 94 | export async function resetPassword(req, res, next) { 95 | try { 96 | const resetPasswordTokenDoc = await tokenService.verifyToken(req.body.token, TOKEN_TYPES.RESET_PASSWORD); 97 | console.log(resetPasswordTokenDoc); 98 | const user = await userService.getUserById(resetPasswordTokenDoc.userId); 99 | if (!user) { 100 | throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed'); 101 | } 102 | await userService.updateUserById(user.id, { password: req.body.password }); 103 | await tokenService.deleteMany(user.id, TOKEN_TYPES.RESET_PASSWORD); 104 | 105 | res.status(httpStatus.NO_CONTENT).send(); 106 | } catch (error) { 107 | next(error); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /drizzle/migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "c1ab315a-ea99-4fd5-a405-f7f4a8eef9a3", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "uuid", 14 | "primaryKey": true, 15 | "notNull": true, 16 | "default": "gen_random_uuid()" 17 | }, 18 | "username": { 19 | "name": "username", 20 | "type": "text", 21 | "primaryKey": false, 22 | "notNull": true 23 | }, 24 | "email": { 25 | "name": "email", 26 | "type": "text", 27 | "primaryKey": false, 28 | "notNull": true 29 | }, 30 | "password": { 31 | "name": "password", 32 | "type": "text", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "first_name": { 37 | "name": "first_name", 38 | "type": "text", 39 | "primaryKey": false, 40 | "notNull": false 41 | }, 42 | "last_name": { 43 | "name": "last_name", 44 | "type": "text", 45 | "primaryKey": false, 46 | "notNull": false 47 | }, 48 | "address": { 49 | "name": "address", 50 | "type": "text", 51 | "primaryKey": false, 52 | "notNull": false 53 | }, 54 | "phone": { 55 | "name": "phone", 56 | "type": "text", 57 | "primaryKey": false, 58 | "notNull": false 59 | }, 60 | "image": { 61 | "name": "image", 62 | "type": "text", 63 | "primaryKey": false, 64 | "notNull": false 65 | }, 66 | "created_at": { 67 | "name": "created_at", 68 | "type": "timestamp", 69 | "primaryKey": false, 70 | "notNull": true, 71 | "default": "now()" 72 | }, 73 | "updated_at": { 74 | "name": "updated_at", 75 | "type": "timestamp", 76 | "primaryKey": false, 77 | "notNull": true, 78 | "default": "now()" 79 | } 80 | }, 81 | "indexes": {}, 82 | "foreignKeys": {}, 83 | "compositePrimaryKeys": {}, 84 | "uniqueConstraints": { 85 | "users_username_unique": { 86 | "name": "users_username_unique", 87 | "nullsNotDistinct": false, 88 | "columns": [ 89 | "username" 90 | ] 91 | }, 92 | "users_email_unique": { 93 | "name": "users_email_unique", 94 | "nullsNotDistinct": false, 95 | "columns": [ 96 | "email" 97 | ] 98 | } 99 | } 100 | }, 101 | "tokens": { 102 | "name": "tokens", 103 | "schema": "", 104 | "columns": { 105 | "id": { 106 | "name": "id", 107 | "type": "uuid", 108 | "primaryKey": true, 109 | "notNull": true, 110 | "default": "gen_random_uuid()" 111 | }, 112 | "user_id": { 113 | "name": "user_id", 114 | "type": "uuid", 115 | "primaryKey": false, 116 | "notNull": true 117 | }, 118 | "token": { 119 | "name": "token", 120 | "type": "text", 121 | "primaryKey": false, 122 | "notNull": true 123 | }, 124 | "type": { 125 | "name": "type", 126 | "type": "token_types", 127 | "primaryKey": false, 128 | "notNull": true 129 | }, 130 | "expires": { 131 | "name": "expires", 132 | "type": "timestamp", 133 | "primaryKey": false, 134 | "notNull": true 135 | }, 136 | "created_at": { 137 | "name": "created_at", 138 | "type": "timestamp", 139 | "primaryKey": false, 140 | "notNull": true, 141 | "default": "now()" 142 | } 143 | }, 144 | "indexes": {}, 145 | "foreignKeys": { 146 | "tokens_user_id_users_id_fk": { 147 | "name": "tokens_user_id_users_id_fk", 148 | "tableFrom": "tokens", 149 | "tableTo": "users", 150 | "columnsFrom": [ 151 | "user_id" 152 | ], 153 | "columnsTo": [ 154 | "id" 155 | ], 156 | "onDelete": "cascade", 157 | "onUpdate": "no action" 158 | } 159 | }, 160 | "compositePrimaryKeys": {}, 161 | "uniqueConstraints": {} 162 | } 163 | }, 164 | "enums": { 165 | "token_types": { 166 | "name": "token_types", 167 | "values": { 168 | "refresh": "refresh", 169 | "resetPassword": "resetPassword" 170 | } 171 | } 172 | }, 173 | "schemas": {}, 174 | "_meta": { 175 | "columns": {}, 176 | "schemas": {}, 177 | "tables": {} 178 | } 179 | } -------------------------------------------------------------------------------- /src/services/token.service.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import dayjs from "dayjs"; 3 | import httpStatus from "http-status"; 4 | import { and, eq } from "drizzle-orm"; 5 | 6 | import env from "../config/config.js"; 7 | import * as userService from "./user.service.js"; 8 | import ApiError from "../utils/api-error.js"; 9 | import { TOKEN_TYPES } from "../config/tokens.js"; 10 | import { tokens, users, insertTokenSchema } from "../db/schema.js"; 11 | import { db } from "../db/index.js"; 12 | 13 | /** 14 | * Generate token 15 | * @param {ObjectId} userId 16 | * @param {dayjs.Dayjs} expires 17 | * @param {string} type 18 | * @param {string} [secret] 19 | * @returns {string} 20 | */ 21 | export function generateToken(userId, expires, type, secret = env.jwt.secret) { 22 | const payload = { 23 | sub: userId, 24 | iat: dayjs(new Date()).unix(), 25 | exp: expires.unix(), 26 | type, 27 | }; 28 | return jwt.sign(payload, secret); 29 | } 30 | 31 | /** 32 | * Save a token 33 | * @param {insertTokenSchema} data 34 | * @returns {Promise} 35 | */ 36 | export function saveToken(data) { 37 | return db.insert(tokens).values(data).returning(); 38 | } 39 | 40 | /** 41 | * Verify token and return token doc (or throw an error if it is not valid) 42 | * @param {string} token 43 | * @param {string} type 44 | * @returns {Promise} 45 | */ 46 | export async function verifyToken(token, type) { 47 | const payload = jwt.verify(token, env.jwt.secret); 48 | console.log(payload); 49 | const tokenDoc = await db.query.tokens.findFirst({ 50 | where: and( 51 | eq(tokens.userId, payload.sub), 52 | eq(tokens.token, token), 53 | eq(tokens.type, type) 54 | ), 55 | }); 56 | if (!tokenDoc) { 57 | throw new Error("Token not found"); 58 | } 59 | return tokenDoc; 60 | } 61 | 62 | /** 63 | * Generate auth tokens 64 | * @param {users} user 65 | * @returns {Promise} 66 | */ 67 | export async function generateAuthTokens(user) { 68 | const accessTokenExpires = dayjs().add(env.jwt.accessExpirationMinutes, "m"); 69 | console.log("generateAuthTokens", user); 70 | const accessToken = generateToken( 71 | user.id, 72 | accessTokenExpires, 73 | TOKEN_TYPES.ACCESS 74 | ); 75 | 76 | const refreshTokenExpires = dayjs().add(env.jwt.refreshExpirationDays, "d"); 77 | const refreshToken = generateToken( 78 | user.id, 79 | refreshTokenExpires, 80 | TOKEN_TYPES.REFRESH 81 | ); 82 | await saveToken({ 83 | token: refreshToken, 84 | userId: user.id, 85 | expires: refreshTokenExpires, 86 | type: TOKEN_TYPES.REFRESH, 87 | }); 88 | 89 | return { 90 | access: { 91 | token: accessToken, 92 | expires: accessTokenExpires.toDate(), 93 | }, 94 | refresh: { 95 | token: refreshToken, 96 | expires: refreshTokenExpires.toDate(), 97 | }, 98 | }; 99 | } 100 | 101 | /** 102 | * Generate reset password token 103 | * @param {string} usernameOrEmail 104 | * @returns {Promise} 105 | */ 106 | export async function generateResetPasswordToken(usernameOrEmail) { 107 | const user = await userService.getUserByUsernameOrEmail(usernameOrEmail); 108 | if (!user) { 109 | throw new ApiError(httpStatus.NOT_FOUND, "No users"); 110 | } 111 | const expires = dayjs().add(env.jwt.resetPasswordExpirationMinutes, "m"); 112 | const resetPasswordToken = generateToken( 113 | user.id, 114 | expires, 115 | TOKEN_TYPES.RESET_PASSWORD 116 | ); 117 | await saveToken({ 118 | token: resetPasswordToken, 119 | userId: user.id, 120 | expires, 121 | type: TOKEN_TYPES.RESET_PASSWORD, 122 | }); 123 | return resetPasswordToken; 124 | } 125 | 126 | /** 127 | * Refresh auth tokens 128 | * @param {string} refreshToken 129 | * @returns {Promise} 130 | */ 131 | export async function refreshAuth(refreshToken) { 132 | try { 133 | const refreshTokenDoc = await verifyToken( 134 | refreshToken, 135 | TOKEN_TYPES.REFRESH 136 | ); 137 | const user = await userService.getUserById(refreshTokenDoc.userId); 138 | if (!user) { 139 | throw new Error(); 140 | } 141 | await deleteToken(refreshTokenDoc.id); 142 | return generateAuthTokens(user); 143 | } catch (error) { 144 | throw new ApiError(httpStatus.UNAUTHORIZED, "Please authenticate"); 145 | } 146 | } 147 | 148 | /** 149 | * find token 150 | * @param {string} userId 151 | * @param {string} token 152 | * @param {string} type 153 | * @returns {Promise} 154 | */ 155 | export async function findToken(token, type) { 156 | return db 157 | .delete(tokens) 158 | .where(and(eq(tokens.token, token)), eq(tokens.type, type)); 159 | } 160 | 161 | /** 162 | * delete token 163 | * @param {users} user 164 | * @returns {Promise} 165 | */ 166 | export async function deleteToken(id) { 167 | return db.delete(tokens).where(eq(tokens.id, id)); 168 | } 169 | 170 | /** 171 | * delete token 172 | * @param {string} userId 173 | * @param {string} type 174 | * @returns {Promise} 175 | */ 176 | export async function deleteMany(userId, type) { 177 | return db 178 | .delete(tokens) 179 | .where(and(eq(tokens.userId, userId), eq(tokens.type, type))); 180 | } 181 | -------------------------------------------------------------------------------- /src/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import { authController } from "../controllers/index.js"; 4 | import { validateMiddleware } from "../middlewares/validate.middleware.js"; 5 | import { 6 | forgotPasswordSchema, 7 | insertUserSchema, 8 | loginUserSchema, 9 | logoutTokenSchema, 10 | refreshTokenSchema, 11 | resetPasswordSchema, 12 | } from "../db/schema.js"; 13 | 14 | /** 15 | * @swagger 16 | * tags: 17 | * name: Auth 18 | * description: Authentication 19 | */ 20 | const authRoutes = Router(); 21 | 22 | /** 23 | * @swagger 24 | * /auth/register: 25 | * post: 26 | * summary: Register as user 27 | * tags: [Auth] 28 | * requestBody: 29 | * required: true 30 | * content: 31 | * application/json: 32 | * schema: 33 | * type: object 34 | * required: 35 | * - username 36 | * - email 37 | * - password 38 | * properties: 39 | * username: 40 | * type: string 41 | * description: must be unique 42 | * email: 43 | * type: string 44 | * format: email 45 | * description: must be unique 46 | * password: 47 | * type: string 48 | * format: password 49 | * minLength: 6 50 | * description: At least one number, one special character, one Uppercase and one lowercase 51 | * firstName: 52 | * type: string 53 | * lastName: 54 | * type: string 55 | * address: 56 | * type: string 57 | * phone: 58 | * type: string 59 | * image: 60 | * type: string 61 | * example: 62 | * username: test 63 | * email: test@test.com 64 | * password: P@ssword123 65 | * firstName: test 66 | * lastName: test 67 | * responses: 68 | * "201": 69 | * description: Created 70 | * content: 71 | * application/json: 72 | * schema: 73 | * type: object 74 | * properties: 75 | * user: 76 | * $ref: '#/components/schemas/User' 77 | * tokens: 78 | * $ref: '#/components/schemas/AuthTokens' 79 | * "400": 80 | * $ref: '#/components/responses/DuplicateError' 81 | */ 82 | authRoutes 83 | .route("/register") 84 | .post(validateMiddleware(insertUserSchema), authController.register); 85 | 86 | /** 87 | * @swagger 88 | * /auth/login: 89 | * post: 90 | * summary: Login 91 | * tags: [Auth] 92 | * requestBody: 93 | * required: true 94 | * content: 95 | * application/json: 96 | * schema: 97 | * type: object 98 | * required: 99 | * - usernameOrEmail 100 | * - password 101 | * properties: 102 | * usernameOrEmail: 103 | * type: string 104 | * format: email | string 105 | * password: 106 | * type: string 107 | * format: password 108 | * example: 109 | * usernameOrEmail: test 110 | * password: P@ssword123 111 | * responses: 112 | * "200": 113 | * description: OK 114 | * content: 115 | * application/json: 116 | * schema: 117 | * type: object 118 | * properties: 119 | * user: 120 | * $ref: '#/components/schemas/User' 121 | * tokens: 122 | * $ref: '#/components/schemas/AuthTokens' 123 | * "401": 124 | * description: Invalid email or password 125 | * content: 126 | * application/json: 127 | * schema: 128 | * $ref: '#/components/schemas/Error' 129 | * example: 130 | * code: 401 131 | * message: username/email and password didn't match 132 | */ 133 | authRoutes 134 | .route("/login") 135 | .post(validateMiddleware(loginUserSchema), authController.login); 136 | 137 | /** 138 | * @swagger 139 | * /auth/logout: 140 | * post: 141 | * summary: Logout 142 | * tags: [Auth] 143 | * requestBody: 144 | * required: true 145 | * content: 146 | * application/json: 147 | * schema: 148 | * type: object 149 | * required: 150 | * - refreshToken 151 | * properties: 152 | * refreshToken: 153 | * type: string 154 | * example: 155 | * refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YzcxYWQ0Zi05MDY5LTRkNTEtOTI4Ni1mM2FkYTQzNzI5MTAiLCJpYXQiOjE3MTQ3MzE0NTcsImV4cCI6MTcxNDczMzI1NywidHlwZSI6ImFjY2VzcyJ9.ZygGnanFeO-96I7RAV6HLMm5vGlfSxm1BKr4Ag-FwH8 156 | * responses: 157 | * "204": 158 | * description: No content 159 | * "404": 160 | * $ref: '#/components/responses/NotFound' 161 | */ 162 | authRoutes 163 | .route("/logout") 164 | .post(validateMiddleware(logoutTokenSchema), authController.logout); 165 | 166 | /** 167 | * @swagger 168 | * /auth/refresh-tokens: 169 | * post: 170 | * summary: Refresh auth tokens 171 | * tags: [Auth] 172 | * requestBody: 173 | * required: true 174 | * content: 175 | * application/json: 176 | * schema: 177 | * type: object 178 | * required: 179 | * - refreshToken 180 | * properties: 181 | * refreshToken: 182 | * type: string 183 | * example: 184 | * refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YzcxYWQ0Zi05MDY5LTRkNTEtOTI4Ni1mM2FkYTQzNzI5MTAiLCJpYXQiOjE3MTQ3MzE0NTcsImV4cCI6MTcxNDczMzI1NywidHlwZSI6ImFjY2VzcyJ9.ZygGnanFeO-96I7RAV6HLMm5vGlfSxm1BKr4Ag-FwH8 185 | * responses: 186 | * "200": 187 | * description: OK 188 | * content: 189 | * application/json: 190 | * schema: 191 | * $ref: '#/components/schemas/AuthTokens' 192 | * "401": 193 | * $ref: '#/components/responses/Unauthorized' 194 | */ 195 | authRoutes 196 | .route("/refresh-tokens") 197 | .post(validateMiddleware(refreshTokenSchema), authController.refreshTokens); 198 | 199 | /** 200 | * @swagger 201 | * /auth/forgot-password: 202 | * post: 203 | * summary: Forgot password 204 | * description: An email will be sent to reset password. 205 | * tags: [Auth] 206 | * requestBody: 207 | * required: true 208 | * content: 209 | * application/json: 210 | * schema: 211 | * type: object 212 | * required: 213 | * - usernameOrEmail 214 | * properties: 215 | * usernameOrEmail: 216 | * type: string 217 | * format: email | username 218 | * example: 219 | * usernameOrEmail: test 220 | * responses: 221 | * "204": 222 | * description: No content 223 | * "404": 224 | * $ref: '#/components/responses/NotFound' 225 | */ 226 | authRoutes 227 | .route("/forgot-password") 228 | .post( 229 | validateMiddleware(forgotPasswordSchema), 230 | authController.forgotPassword 231 | ); 232 | 233 | /** 234 | * @swagger 235 | * /auth/reset-password: 236 | * post: 237 | * summary: Reset password 238 | * tags: [Auth] 239 | * requestBody: 240 | * required: true 241 | * content: 242 | * application/json: 243 | * schema: 244 | * type: object 245 | * required: 246 | * - token 247 | * - password 248 | * properties: 249 | * token: 250 | * type: string 251 | * password: 252 | * type: string 253 | * format: password 254 | * minLength: 6 255 | * description: At least one number, one special character, one Uppercase and one lowercase 256 | * example: 257 | * token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YzcxYWQ0Zi05MDY5LTRkNTEtOTI4Ni1mM2FkYTQzNzI5MTAiLCJpYXQiOjE3MTQ3MzE0NTcsImV4cCI6MTcxNDczMzI1NywidHlwZSI6ImFjY2VzcyJ9.ZygGnanFeO-96I7RAV6HLMm5vGlfSxm1BKr4Ag-FwH8 258 | * password: P@ssword123 259 | * responses: 260 | * "204": 261 | * description: No content 262 | * "401": 263 | * description: Password reset failed 264 | * content: 265 | * application/json: 266 | * schema: 267 | * $ref: '#/components/schemas/Error' 268 | * example: 269 | * code: 401 270 | * message: Password reset failed 271 | */ 272 | authRoutes 273 | .route("/reset-password") 274 | .post(validateMiddleware(resetPasswordSchema), authController.resetPassword); 275 | 276 | export { authRoutes }; 277 | --------------------------------------------------------------------------------