@trisers/cli-node-template
Advanced tools
| import { PrismaClient } from "@prisma/client"; | ||
| const prisma = new PrismaClient({ | ||
| log: ["error", "warn"], | ||
| }); | ||
| export default prisma; | ||
Sorry, the diff of this file is not supported yet
| # Prisma Integration | ||
| This directory contains Prisma configurations for PostgreSQL only in this boilerplate. | ||
| ## Overview | ||
| Prisma is a modern database toolkit that provides: | ||
| - Type-safe database queries | ||
| - Auto-generated database client | ||
| - Database migrations | ||
| - Schema management | ||
| ## Files Structure | ||
| ``` | ||
| prisma/ | ||
| ├── schema.prisma # PostgreSQL schema template | ||
| └── prisma.env.example # Environment variables example | ||
| ``` | ||
| ## Database-Specific Schemas | ||
| ### MongoDB Schema | ||
| - Uses `@db.ObjectId` for ID fields | ||
| - Supports MongoDB-specific data types | ||
| - Optimized for MongoDB collections | ||
| ### MySQL Schema | ||
| - Uses `Json` type for array fields (refreshTokens) | ||
| - Compatible with MySQL constraints | ||
| - Optimized for MySQL tables | ||
| ### PostgreSQL Schema | ||
| - Uses native array types for refreshTokens | ||
| - Leverages PostgreSQL-specific features | ||
| - Optimized for PostgreSQL tables | ||
| ## Services | ||
| PostgreSQL Prisma auth service: | ||
| - `auth.prisma.postgresql.service.js` | ||
| ## Controllers | ||
| Controllers reuse the same HTTP handlers; only the service changes. | ||
| ## Setup Instructions | ||
| 1. **Install Prisma dependencies:** | ||
| ```bash | ||
| npm install @prisma/client prisma | ||
| ``` | ||
| 2. **Copy schema file:** | ||
| ```bash | ||
| # For PostgreSQL | ||
| cp templates/js/base/prisma/schema.postgresql.prisma prisma/schema.prisma | ||
| ``` | ||
| 3. **Set up environment variables:** | ||
| ```bash | ||
| cp configs/prisma/prisma.env.example .env | ||
| # Edit .env with your database credentials | ||
| ``` | ||
| 4. **Generate Prisma client:** | ||
| ```bash | ||
| npx prisma generate | ||
| ``` | ||
| 5. **Run database migrations:** | ||
| ```bash | ||
| npx prisma migrate dev --name init | ||
| ``` | ||
| 6. **Import the appropriate service in your routes:** | ||
| ```javascript | ||
| // For MongoDB | ||
| import { authPrismaService } from '../services/auth.prisma.mongodb.service.js'; | ||
| // For MySQL | ||
| import { authPrismaService } from '../services/auth.prisma.mysql.service.js'; | ||
| // For PostgreSQL | ||
| import { authPrismaService } from '../services/auth.prisma.postgresql.service.js'; | ||
| ``` | ||
| ## Environment Variables | ||
| Required environment variables: | ||
| ```env | ||
| # Database Configuration | ||
| DATABASE_URL=your_database_connection_string | ||
| # JWT Configuration | ||
| JWT_SECRET=your_jwt_secret | ||
| JWT_REFRESH_SECRET=your_refresh_secret | ||
| JWT_EXPIRES_IN=1h | ||
| JWT_REFRESH_EXPIRES_IN=7d | ||
| # Node Environment | ||
| NODE_ENV=development|production | ||
| ``` | ||
| ## Database Connection Strings | ||
| ### MongoDB | ||
| ``` | ||
| mongodb://username:password@host:port/database | ||
| ``` | ||
| ### MySQL | ||
| ``` | ||
| mysql://username:password@host:port/database | ||
| ``` | ||
| ### PostgreSQL | ||
| ``` | ||
| postgresql://username:password@host:port/database | ||
| ``` | ||
| ## Features | ||
| - **Type-safe queries** - All database operations are type-safe | ||
| - **Auto-generated client** - Prisma generates a client based on your schema | ||
| - **Migration support** - Easy database schema migrations | ||
| - **Connection pooling** - Built-in connection management | ||
| - **Query optimization** - Prisma optimizes queries automatically | ||
| ## Benefits over Traditional ORMs | ||
| 1. **Type Safety** - Compile-time type checking | ||
| 2. **Auto-completion** - IDE support for all queries | ||
| 3. **Migration Safety** - Safe schema changes | ||
| 4. **Performance** - Optimized query generation | ||
| 5. **Developer Experience** - Better debugging and error messages | ||
| ## Usage Examples | ||
| ### Basic User Operations | ||
| ```javascript | ||
| import prisma from '../configs/prisma.config.js'; | ||
| // Create user | ||
| const user = await prisma.user.create({ | ||
| data: { | ||
| email: 'user@example.com', | ||
| password: hashedPassword, | ||
| firstName: 'John', | ||
| lastName: 'Doe' | ||
| } | ||
| }); | ||
| // Find user | ||
| const user = await prisma.user.findUnique({ | ||
| where: { email: 'user@example.com' } | ||
| }); | ||
| // Update user | ||
| const updatedUser = await prisma.user.update({ | ||
| where: { id: userId }, | ||
| data: { firstName: 'Jane' } | ||
| }); | ||
| ``` | ||
| ### Auth Service Usage | ||
| ```javascript | ||
| import { authPrismaService } from '../services/auth.prisma.mongodb.service.js'; | ||
| // Register user | ||
| const result = await authPrismaService.register({ | ||
| email: 'user@example.com', | ||
| password: 'password123', | ||
| firstName: 'John', | ||
| lastName: 'Doe' | ||
| }); | ||
| // Login user | ||
| const result = await authPrismaService.login({ | ||
| email: 'user@example.com', | ||
| password: 'password123' | ||
| }); | ||
| ``` | ||
| ## Troubleshooting | ||
| ### Common Issues | ||
| 1. **Connection Errors** | ||
| - Verify DATABASE_URL is correct | ||
| - Check if database server is running | ||
| - Ensure network connectivity | ||
| 2. **Schema Generation Errors** | ||
| - Run `npx prisma generate` after schema changes | ||
| - Check for syntax errors in schema.prisma | ||
| 3. **Migration Issues** | ||
| - Use `npx prisma migrate reset` to reset database | ||
| - Check migration history with `npx prisma migrate status` | ||
| ### Debug Commands | ||
| ```bash | ||
| # Check Prisma client status | ||
| npx prisma generate | ||
| # View database schema | ||
| npx prisma db pull | ||
| # Reset database | ||
| npx prisma migrate reset | ||
| # Open Prisma Studio | ||
| npx prisma studio | ||
| ``` | ||
| ## Migration from Traditional ORMs | ||
| If migrating from Mongoose, MySQL2, or pg: | ||
| 1. Replace database connection imports | ||
| 2. Update service imports to use Prisma services | ||
| 3. Update controller imports to use Prisma controllers | ||
| 4. Update environment variables | ||
| 5. Run database migrations | ||
| ## Support | ||
| For Prisma-specific issues, refer to the [Prisma documentation](https://www.prisma.io/docs/). |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| import prisma from "../configs/prisma.js"; | ||
| import { | ||
| hashPassword, | ||
| comparePassword, | ||
| generateToken, | ||
| generateRefreshToken, | ||
| verifyRefreshToken, | ||
| generatePasswordResetToken, | ||
| sanitizeUser, | ||
| } from "../utils/auth.utils.js"; | ||
| import { ApiError, asyncFunctionHandler } from "../middlewares/errorHandler.js"; | ||
| export const authService = { | ||
| register: asyncFunctionHandler(async (userData) => { | ||
| const { email, password, firstName, lastName } = userData; | ||
| const existing = await prisma.user.findUnique({ where: { email } }); | ||
| if (existing) { | ||
| throw ApiError("User with this email already exists", 409, {}, "userAlreadyExists"); | ||
| } | ||
| const hashedPassword = await hashPassword(password); | ||
| const user = await prisma.user.create({ | ||
| data: { | ||
| email, | ||
| password: hashedPassword, | ||
| firstName: firstName || null, | ||
| lastName: lastName || null, | ||
| isActive: true, | ||
| refreshTokens: [], | ||
| }, | ||
| }); | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.isActive }); | ||
| const refreshToken = generateRefreshToken({ id: user.id, email: user.email, is_active: user.isActive }); | ||
| await prisma.user.update({ where: { id: user.id }, data: { refreshTokens: [refreshToken] } }); | ||
| return { user: sanitizeUser(user), token, refreshToken }; | ||
| }), | ||
| login: asyncFunctionHandler(async ({ email, password }) => { | ||
| const user = await prisma.user.findUnique({ where: { email } }); | ||
| if (!user) throw ApiError("Invalid email or password", 401, {}, "invalidCredentials"); | ||
| if (!user.isActive) throw ApiError("Account is deactivated", 401, {}, "accountDeactivated"); | ||
| const ok = await comparePassword(password, user.password); | ||
| if (!ok) throw ApiError("Invalid email or password", 401, {}, "invalidCredentials"); | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.isActive }); | ||
| const refreshToken = generateRefreshToken({ id: user.id, email: user.email, is_active: user.isActive }); | ||
| await prisma.user.update({ where: { id: user.id }, data: { refreshTokens: [refreshToken] } }); | ||
| return { user: sanitizeUser(user), token, refreshToken }; | ||
| }), | ||
| logout: asyncFunctionHandler(async (id) => { | ||
| await prisma.user.update({ where: { id }, data: { refreshTokens: [] } }); | ||
| return { message: "Logged out successfully" }; | ||
| }), | ||
| refreshToken: asyncFunctionHandler(async (refreshToken) => { | ||
| const decoded = verifyRefreshToken(refreshToken); | ||
| if (!decoded) throw ApiError("Invalid refresh token", 401, {}, "invalidRefreshToken"); | ||
| const user = await prisma.user.findUnique({ where: { id: decoded.id } }); | ||
| if (!user || !user.isActive) throw ApiError("User not found or inactive", 401, {}, "userNotFound"); | ||
| const newToken = generateToken({ id: user.id, email: user.email, is_active: user.isActive }); | ||
| const newRefreshToken = generateRefreshToken({ id: user.id, email: user.email, is_active: user.isActive }); | ||
| const updated = await prisma.user.update({ where: { id: user.id }, data: { refreshTokens: [newRefreshToken] } }); | ||
| return { user: sanitizeUser(updated), token: newToken, refreshToken: newRefreshToken }; | ||
| }), | ||
| forgotPassword: asyncFunctionHandler(async (email) => { | ||
| const found = await prisma.user.findUnique({ where: { email }, select: { id: true } }); | ||
| if (!found) throw ApiError("User with this email does not exist", 404, {}, "userNotFound"); | ||
| const resetToken = generatePasswordResetToken(); | ||
| const resetExpires = new Date(Date.now() + 3600000); | ||
| await prisma.user.update({ where: { id: found.id }, data: { passwordResetToken: resetToken, passwordResetExpires: resetExpires } }); | ||
| return { message: "Password reset email sent", resetToken }; | ||
| }), | ||
| resetPassword: asyncFunctionHandler(async (token, newPassword) => { | ||
| const user = await prisma.user.findFirst({ where: { passwordResetToken: token, passwordResetExpires: { gt: new Date() } } }); | ||
| if (!user) throw ApiError("Invalid or expired reset token", 400, {}, "invalidResetToken"); | ||
| const hashed = await hashPassword(newPassword); | ||
| await prisma.user.update({ where: { id: user.id }, data: { password: hashed, passwordResetToken: null, passwordResetExpires: null } }); | ||
| return { message: "Password reset successfully" }; | ||
| }), | ||
| changePassword: asyncFunctionHandler(async (id, oldPassword, newPassword) => { | ||
| const user = await prisma.user.findUnique({ where: { id } }); | ||
| if (!user) throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| const ok = await comparePassword(oldPassword, user.password); | ||
| if (!ok) throw ApiError("Invalid old password", 400, {}, "invalidOldPassword"); | ||
| const hashed = await hashPassword(newPassword); | ||
| await prisma.user.update({ where: { id }, data: { password: hashed } }); | ||
| return { message: "Password changed successfully" }; | ||
| }), | ||
| verifyToken: asyncFunctionHandler(async () => ({ valid: true })), | ||
| getUserById: asyncFunctionHandler(async (id) => { | ||
| const user = await prisma.user.findUnique({ where: { id } }); | ||
| if (!user) throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| return sanitizeUser(user); | ||
| }), | ||
| updateUserById: asyncFunctionHandler(async (id, firstName, lastName) => { | ||
| const user = await prisma.user.update({ where: { id }, data: { firstName, lastName } }); | ||
| return user ? sanitizeUser(user) : null; | ||
| }), | ||
| }; |
| // PostgreSQL (Prisma) | ||
| export { authService } from "./auth.prisma.service.js"; | ||
| // MySQL (Prisma) | ||
| export { authService } from "./auth.prisma.service.js"; | ||
| // MongoDB | ||
| export { authService } from "./auth.mongodb.service.js"; |
| import { PrismaClient } from "@prisma/client"; | ||
| export const prisma = new PrismaClient({ | ||
| log: ["error", "warn"], | ||
| }); | ||
| export default prisma; | ||
Sorry, the diff of this file is not supported yet
| # Prisma Integration (TypeScript) | ||
| This directory contains Prisma configurations for PostgreSQL only in this boilerplate. | ||
| ## Overview | ||
| Prisma is a modern database toolkit that provides: | ||
| - Type-safe database queries with TypeScript | ||
| - Auto-generated database client with types | ||
| - Database migrations | ||
| - Schema management | ||
| ## Files Structure | ||
| ``` | ||
| prisma/ | ||
| ├── schema.prisma # PostgreSQL schema template | ||
| └── prisma.env.example # Environment variables example | ||
| ``` | ||
| ## Database-Specific Schemas | ||
| ### MongoDB Schema | ||
| - Uses `@db.ObjectId` for ID fields | ||
| - Supports MongoDB-specific data types | ||
| - Optimized for MongoDB collections | ||
| ### MySQL Schema | ||
| - Uses `Json` type for array fields (refreshTokens) | ||
| - Compatible with MySQL constraints | ||
| - Optimized for MySQL tables | ||
| ### PostgreSQL Schema | ||
| - Uses native array types for refreshTokens | ||
| - Leverages PostgreSQL-specific features | ||
| - Optimized for PostgreSQL tables | ||
| ## Services | ||
| PostgreSQL Prisma service with TypeScript types: | ||
| - `auth.prisma.postgresql.service.ts` | ||
| ## Controllers | ||
| Controllers reuse the same HTTP handlers; only the service changes. | ||
| ## Setup Instructions | ||
| 1. **Install Prisma dependencies:** | ||
| ```bash | ||
| npm install @prisma/client prisma | ||
| npm install -D @types/node | ||
| ``` | ||
| 2. **Copy schema file:** | ||
| ```bash | ||
| # For PostgreSQL | ||
| cp templates/ts/base/prisma/schema.postgresql.prisma prisma/schema.prisma | ||
| ``` | ||
| 3. **Set up environment variables:** | ||
| ```bash | ||
| cp configs/prisma/prisma.env.example .env | ||
| # Edit .env with your database credentials | ||
| ``` | ||
| 4. **Generate Prisma client:** | ||
| ```bash | ||
| npx prisma generate | ||
| ``` | ||
| 5. **Run database migrations:** | ||
| ```bash | ||
| npx prisma migrate dev --name init | ||
| ``` | ||
| 6. **Import the appropriate service in your routes:** | ||
| ```typescript | ||
| // For MongoDB | ||
| import { authPrismaService } from '../services/auth.prisma.mongodb.service.js'; | ||
| // For MySQL | ||
| import { authPrismaService } from '../services/auth.prisma.mysql.service.js'; | ||
| // For PostgreSQL | ||
| import { authPrismaService } from '../services/auth.prisma.postgresql.service.js'; | ||
| ``` | ||
| ## Environment Variables | ||
| Required environment variables: | ||
| ```env | ||
| # Database Configuration | ||
| DATABASE_URL=your_database_connection_string | ||
| # JWT Configuration | ||
| JWT_SECRET=your_jwt_secret | ||
| JWT_REFRESH_SECRET=your_refresh_secret | ||
| JWT_EXPIRES_IN=1h | ||
| JWT_REFRESH_EXPIRES_IN=7d | ||
| # Node Environment | ||
| NODE_ENV=development|production | ||
| ``` | ||
| ## Database Connection Strings | ||
| ### MongoDB | ||
| ``` | ||
| mongodb://username:password@host:port/database | ||
| ``` | ||
| ### MySQL | ||
| ``` | ||
| mysql://username:password@host:port/database | ||
| ``` | ||
| ### PostgreSQL | ||
| ``` | ||
| postgresql://username:password@host:port/database | ||
| ``` | ||
| ## TypeScript Features | ||
| - **Full type safety** - All database operations are fully typed | ||
| - **Auto-completion** - IDE support for all queries and types | ||
| - **Type inference** - Automatic type inference from schema | ||
| - **Interface generation** - Auto-generated interfaces from Prisma schema | ||
| ## Features | ||
| - **Type-safe queries** - All database operations are type-safe | ||
| - **Auto-generated client** - Prisma generates a typed client based on your schema | ||
| - **Migration support** - Easy database schema migrations | ||
| - **Connection pooling** - Built-in connection management | ||
| - **Query optimization** - Prisma optimizes queries automatically | ||
| ## Benefits over Traditional ORMs | ||
| 1. **Type Safety** - Compile-time type checking with TypeScript | ||
| 2. **Auto-completion** - Full IDE support for all queries and types | ||
| 3. **Migration Safety** - Safe schema changes with type checking | ||
| 4. **Performance** - Optimized query generation | ||
| 5. **Developer Experience** - Better debugging and error messages | ||
| ## Usage Examples | ||
| ### Basic User Operations with Types | ||
| ```typescript | ||
| import prisma from '../configs/prisma.config.js'; | ||
| import { User } from '@prisma/client'; | ||
| // Create user with full type safety | ||
| const user: User = await prisma.user.create({ | ||
| data: { | ||
| email: 'user@example.com', | ||
| password: hashedPassword, | ||
| firstName: 'John', | ||
| lastName: 'Doe' | ||
| } | ||
| }); | ||
| // Find user with type inference | ||
| const user = await prisma.user.findUnique({ | ||
| where: { email: 'user@example.com' } | ||
| }); | ||
| // Update user with type safety | ||
| const updatedUser = await prisma.user.update({ | ||
| where: { id: userId }, | ||
| data: { firstName: 'Jane' } | ||
| }); | ||
| ``` | ||
| ### Auth Service Usage with Types | ||
| ```typescript | ||
| import { authPrismaService } from '../services/auth.prisma.mongodb.service.js'; | ||
| interface UserData { | ||
| email: string; | ||
| password: string; | ||
| firstName: string; | ||
| lastName: string; | ||
| } | ||
| // Register user with type safety | ||
| const result = await authPrismaService.register({ | ||
| email: 'user@example.com', | ||
| password: 'password123', | ||
| firstName: 'John', | ||
| lastName: 'Doe' | ||
| } as UserData); | ||
| // Login user with type safety | ||
| const result = await authPrismaService.login({ | ||
| email: 'user@example.com', | ||
| password: 'password123' | ||
| }); | ||
| ``` | ||
| ### Controller Usage with Express Types | ||
| ```typescript | ||
| import { Request, Response } from 'express'; | ||
| import { authPrismaService } from '../services/auth.prisma.mongodb.service.js'; | ||
| export const register = async (req: Request, res: Response) => { | ||
| const { email, password, firstName, lastName }: UserData = req.body; | ||
| const result = await authPrismaService.register({ | ||
| email, | ||
| password, | ||
| firstName, | ||
| lastName | ||
| }); | ||
| res.status(201).json({ | ||
| message: "User registered successfully", | ||
| data: result, | ||
| }); | ||
| }; | ||
| ``` | ||
| ## TypeScript Configuration | ||
| Make sure your `tsconfig.json` includes: | ||
| ```json | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2020", | ||
| "module": "ESNext", | ||
| "moduleResolution": "node", | ||
| "esModuleInterop": true, | ||
| "strict": true, | ||
| "skipLibCheck": true, | ||
| "forceConsistentCasingInFileNames": true | ||
| } | ||
| } | ||
| ``` | ||
| ## Troubleshooting | ||
| ### Common Issues | ||
| 1. **Connection Errors** | ||
| - Verify DATABASE_URL is correct | ||
| - Check if database server is running | ||
| - Ensure network connectivity | ||
| 2. **Schema Generation Errors** | ||
| - Run `npx prisma generate` after schema changes | ||
| - Check for syntax errors in schema.prisma | ||
| 3. **TypeScript Errors** | ||
| - Ensure `@prisma/client` is installed | ||
| - Run `npx prisma generate` to regenerate types | ||
| - Check TypeScript configuration | ||
| 4. **Migration Issues** | ||
| - Use `npx prisma migrate reset` to reset database | ||
| - Check migration history with `npx prisma migrate status` | ||
| ### Debug Commands | ||
| ```bash | ||
| # Check Prisma client status | ||
| npx prisma generate | ||
| # View database schema | ||
| npx prisma db pull | ||
| # Reset database | ||
| npx prisma migrate reset | ||
| # Open Prisma Studio | ||
| npx prisma studio | ||
| # Check TypeScript compilation | ||
| npx tsc --noEmit | ||
| ``` | ||
| ## Migration from Traditional ORMs | ||
| If migrating from Mongoose, MySQL2, or pg: | ||
| 1. Replace database connection imports | ||
| 2. Update service imports to use Prisma services | ||
| 3. Update controller imports to use Prisma controllers | ||
| 4. Update environment variables | ||
| 5. Run database migrations | ||
| 6. Update TypeScript types and interfaces | ||
| ## Support | ||
| For Prisma-specific issues, refer to the [Prisma documentation](https://www.prisma.io/docs/). | ||
| For TypeScript integration, see [Prisma TypeScript guide](https://www.prisma.io/docs/concepts/components/prisma-client/advanced-usage). |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| import prisma from "../configs/prisma.js"; | ||
| import { | ||
| hashPassword, | ||
| comparePassword, | ||
| generateToken, | ||
| generateRefreshToken, | ||
| verifyRefreshToken, | ||
| generatePasswordResetToken, | ||
| sanitizeUser, | ||
| } from "../utils/auth.utils.js"; | ||
| import { ApiError, asyncFunctionHandler } from "../middlewares/errorHandler.js"; | ||
| import { AuthResponse } from "../types/auth.js"; | ||
| export const authService = { | ||
| register: asyncFunctionHandler( | ||
| async (userData: { email: string; password: string; firstName?: string; lastName?: string; }): Promise<AuthResponse> => { | ||
| const { email, password, firstName, lastName } = userData; | ||
| const existing = await prisma.user.findUnique({ where: { email } }); | ||
| if (existing) throw ApiError("User with this email already exists", 409, {}, "userAlreadyExists"); | ||
| const hashed = await hashPassword(password); | ||
| const user = await prisma.user.create({ | ||
| data: { email, | ||
| password: hashed, | ||
| firstName: firstName || null, | ||
| lastName: lastName || null, | ||
| isActive: true, | ||
| refreshTokens: [] } | ||
| }); | ||
| const token = generateToken({ | ||
| id: user.id, email: user.email, is_active: user.isActive | ||
| }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, email: user.email, is_active: user.isActive | ||
| }); | ||
| await prisma.user.update({ | ||
| where: { id: user.id }, data: { refreshTokens: [refreshToken] } | ||
| }); | ||
| return { user: sanitizeUser(user), token, refreshToken } as AuthResponse; | ||
| }), | ||
| login: asyncFunctionHandler( | ||
| async ({ email, password }: { email: string; password: string; }): Promise<AuthResponse> => { | ||
| const user = await prisma.user.findUnique({ | ||
| where: { email } | ||
| }); | ||
| if (!user) throw ApiError("Invalid email or password", 401, {}, "invalidCredentials"); | ||
| if (!user.isActive) throw ApiError("Account is deactivated", 401, {}, "accountDeactivated"); | ||
| const ok = await comparePassword(password, user.password); | ||
| if (!ok) throw ApiError("Invalid email or password", 401, {}, "invalidCredentials"); | ||
| const token = generateToken({ | ||
| id: user.id, email: user.email, is_active: user.isActive | ||
| }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, email: user.email, is_active: user.isActive | ||
| }); | ||
| await prisma.user.update({ | ||
| where: { id: user.id }, data: { refreshTokens: [refreshToken] } | ||
| }); | ||
| return { user: sanitizeUser(user), token, refreshToken } as AuthResponse; | ||
| }), | ||
| logout: asyncFunctionHandler(async (id: string): Promise<{ message: string }> => { | ||
| await prisma.user.update({ where: { id }, data: { refreshTokens: [] } }); | ||
| return { message: "Logged out successfully" }; | ||
| }), | ||
| refreshToken: asyncFunctionHandler(async (refreshToken: string): Promise<AuthResponse> => { | ||
| const decoded = verifyRefreshToken(refreshToken); | ||
| if (!decoded) throw ApiError("Invalid refresh token", 401, {}, "invalidRefreshToken"); | ||
| const user = await prisma.user.findUnique({ where: { id: decoded.id } }); | ||
| if (!user || !user.isActive) throw ApiError("User not found or inactive", 401, {}, "userNotFound"); | ||
| const newToken = generateToken({ id: user.id, email: user.email, is_active: user.isActive }); | ||
| const newRefreshToken = generateRefreshToken({ id: user.id, email: user.email, is_active: user.isActive }); | ||
| const updated = await prisma.user.update({ where: { id: user.id }, data: { refreshTokens: [newRefreshToken] } }); | ||
| return { user: sanitizeUser(updated), token: newToken, refreshToken: newRefreshToken } as AuthResponse; | ||
| }), | ||
| forgotPassword: asyncFunctionHandler(async (email: string): Promise<{ message: string; resetToken: string }> => { | ||
| const found = await prisma.user.findUnique({ where: { email }, select: { id: true } }); | ||
| if (!found) throw ApiError("User with this email does not exist", 404, {}, "userNotFound"); | ||
| const resetToken = generatePasswordResetToken(); | ||
| const resetExpires = new Date(Date.now() + 3600000); | ||
| await prisma.user.update({ where: { id: found.id }, data: { passwordResetToken: resetToken, passwordResetExpires: resetExpires } }); | ||
| return { message: "Password reset email sent", resetToken }; | ||
| }), | ||
| resetPassword: asyncFunctionHandler(async (token: string, newPassword: string): Promise<{ message: string }> => { | ||
| const user = await prisma.user.findFirst({ where: { passwordResetToken: token, passwordResetExpires: { gt: new Date() } } }); | ||
| if (!user) throw ApiError("Invalid or expired reset token", 400, {}, "invalidResetToken"); | ||
| const hashed = await hashPassword(newPassword); | ||
| await prisma.user.update({ where: { id: user.id }, data: { password: hashed, passwordResetToken: null, passwordResetExpires: null } }); | ||
| return { message: "Password reset successfully" }; | ||
| }), | ||
| changePassword: asyncFunctionHandler(async (id: string, oldPassword: string, newPassword: string): Promise<{ message: string }> => { | ||
| const user = await prisma.user.findUnique({ where: { id } }); | ||
| if (!user) throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| const ok = await comparePassword(oldPassword, user.password); | ||
| if (!ok) throw ApiError("Invalid old password", 400, {}, "invalidOldPassword"); | ||
| const hashed = await hashPassword(newPassword); | ||
| await prisma.user.update({ where: { id }, data: { password: hashed } }); | ||
| return { message: "Password changed successfully" }; | ||
| }), | ||
| verifyToken: asyncFunctionHandler(async () => ({ valid: true } as any)), | ||
| getUserById: asyncFunctionHandler(async (id: string) => { | ||
| const user = await prisma.user.findUnique({ where: { id } }); | ||
| if (!user) throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| return sanitizeUser(user) as any; | ||
| }), | ||
| updateUserById: asyncFunctionHandler(async (id: string, firstName: string, lastName: string) => { | ||
| const user = await prisma.user.update({ where: { id }, data: { firstName, lastName } }); | ||
| return user ? (sanitizeUser(user) as any) : null; | ||
| }), | ||
| }; | ||
| // PostgreSQL (Prisma) | ||
| export { authService } from "./auth.prisma.service.ts"; | ||
| // MySQL (Prisma) | ||
| export { authService } from "./auth.prisma.service.ts"; | ||
| // MongoDB | ||
| export { authService } from "./auth.mongodb.service.ts"; |
+235
-175
@@ -47,7 +47,12 @@ import inquirer from "inquirer"; | ||
| mongodb: ["services/auth.mongodb.service.js"], | ||
| postgresql: ["services/auth.postgresql.service.js"], | ||
| mysql: ["services/auth.mysql.service.js"], | ||
| }, | ||
| }; | ||
| const DB_SERVICES_PRISMA = { | ||
| auth: { | ||
| postgresql: ["services/auth.prisma.service.js"], | ||
| mysql: ["services/auth.prisma.service.js"], | ||
| }, | ||
| }; | ||
| const DB_TABLES = { | ||
@@ -61,7 +66,2 @@ auth: ["users"] | ||
| const DB_SCHEMAS = { | ||
| postgresql: ["schemas/postgresql.sql"], | ||
| mysql: ["schemas/mysql.sql"], | ||
| }; | ||
| const DB_CONFIG_FILES = { | ||
@@ -120,2 +120,8 @@ mongodb: ["configs/mongodb.config.js"], | ||
| } | ||
| if (updates.scripts) { | ||
| pkg.scripts = { ...pkg.scripts, ...updates.scripts }; | ||
| } | ||
| if (updates.removeScripts) { | ||
| updates.removeScripts.forEach((name) => delete pkg.scripts?.[name]); | ||
| } | ||
| if (updates.removeDeps) { | ||
@@ -140,4 +146,50 @@ updates.removeDeps.forEach((dep) => delete pkg.dependencies?.[dep]); | ||
| async createEnvExample(projectPath, db, modules, templateDir) { | ||
| async configureServiceIndex(projectPath, db, usePrisma) { | ||
| const serviceIndexPath = path.join(projectPath, "services/index.service.js"); | ||
| if (!(await fs.pathExists(serviceIndexPath))) return; | ||
| const raw = await fs.readFile(serviceIndexPath, "utf-8"); | ||
| const lines = raw.split("\n"); | ||
| const marker = (() => { | ||
| if (db === "postgresql") return usePrisma | ||
| ? "// PostgreSQL (Prisma)" | ||
| : "// PostgreSQL (native)"; | ||
| if (db === "mysql") return usePrisma | ||
| ? "// MySQL (Prisma)" | ||
| : "// MySQL (native)"; | ||
| if (db === "mongodb") return "// MongoDB"; | ||
| return null; | ||
| })(); | ||
| if (!marker) { | ||
| await fs.writeFile(serviceIndexPath, "// No database selected\n"); | ||
| return; | ||
| } | ||
| let output = ""; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i].trim(); | ||
| if (line.startsWith(marker)) { | ||
| const comment = lines[i]; | ||
| let exportLine = ""; | ||
| for (let j = i + 1; j < lines.length; j++) { | ||
| const t = lines[j].trim(); | ||
| if (!t) continue; | ||
| if (t.startsWith("export ")) { | ||
| exportLine = lines[j]; | ||
| break; | ||
| } | ||
| if (t.startsWith("//")) break; | ||
| } | ||
| output = exportLine ? `${comment}\n${exportLine}\n` : `${comment}\n`; | ||
| break; | ||
| } | ||
| } | ||
| await fs.writeFile(serviceIndexPath, output || "// No matching service found\n"); | ||
| }, | ||
| async createEnvExample(projectPath, db, modules, templateDir, usePrisma) { | ||
| const dbEnvFileMap = { | ||
@@ -155,4 +207,16 @@ mongodb: "mongodb.env.example", | ||
| const keepMarkers = new Set([`#${db}`]); | ||
| const keepMarkers = new Set(); | ||
| // DB selection | ||
| if (db === 'postgresql' || db === 'mysql') { | ||
| if (usePrisma) { | ||
| keepMarkers.add('#prisma'); | ||
| } else { | ||
| keepMarkers.add(`#${db}`); | ||
| } | ||
| } else { | ||
| keepMarkers.add(`#${db}`); | ||
| } | ||
| // Module markers (e.g., #jwt token) | ||
| modules.forEach((mod) => { | ||
@@ -176,3 +240,8 @@ const markers = CONFIG.ENV_MODULES[mod]; | ||
| await fs.writeFile(path.join(projectPath, ".env.example"), finalEnvExample); | ||
| await fs.writeFile(path.join(projectPath, ".env"), `DB_TYPE=${db}\n`); | ||
| // Seed a minimal .env | ||
| let envSeed = `DB_TYPE=${db}\n`; | ||
| if ((db === 'postgresql' || db === 'mysql') && usePrisma) { | ||
| envSeed += `USE_PRISMA=true\n`; | ||
| } | ||
| await fs.writeFile(path.join(projectPath, ".env"), envSeed); | ||
| }, | ||
@@ -207,147 +276,26 @@ | ||
| fs.writeFileSync(filePath, finalContent); | ||
| }, | ||
| async removeDatabaseInitLogic(filePath) { | ||
| if (!(await fs.pathExists(filePath))) return; | ||
| let content = await fs.readFile(filePath, "utf-8"); | ||
| // Split content into lines | ||
| const lines = content.split('\n'); | ||
| const filteredLines = []; | ||
| let skipDatabaseSection = false; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| // Check if this is the start of database initialization section | ||
| if (line.trim() === "// Initialize database and authentication service") { | ||
| skipDatabaseSection = true; | ||
| continue; | ||
| } | ||
| // Check if we're in the database section and should skip it | ||
| if (skipDatabaseSection) { | ||
| // Continue skipping until we hit the next non-database section | ||
| if (line.trim() === "initializeServer(app);") { | ||
| // End of database section, include this line | ||
| skipDatabaseSection = false; | ||
| filteredLines.push(line); | ||
| } | ||
| continue; | ||
| } | ||
| // Add the line if we're not skipping | ||
| if (!skipDatabaseSection) { | ||
| filteredLines.push(line); | ||
| } | ||
| } | ||
| const finalContent = filteredLines.join('\n'); | ||
| await fs.writeFile(filePath, finalContent); | ||
| }, | ||
| async modifyInitDatabaseForDB(projectPath, selectedDb) { | ||
| const initDbPath = path.join(projectPath, "configs/initDatabase.js"); | ||
| if (!(await fs.pathExists(initDbPath))) return; | ||
| async updatePrismaSchema(projectPath, modules) { | ||
| const schemaPath = path.join(projectPath, "prisma", "schema.prisma"); | ||
| if (!(await fs.pathExists(schemaPath))) return; | ||
| let content = await fs.readFile(initDbPath, "utf-8"); | ||
| const content = await fs.readFile(schemaPath, "utf-8"); | ||
| // Split content into lines | ||
| const lines = content.split('\n'); | ||
| const filteredLines = []; | ||
| let skipBlock = false; | ||
| let currentDb = null; | ||
| let braceCount = 0; | ||
| const firstModelMatch = content.match(/\nmodel\s+\w+\s*\{/); | ||
| const firstModelIdx = firstModelMatch ? content.indexOf(firstModelMatch[0]) : -1; | ||
| const header = firstModelIdx >= 0 ? content.slice(0, firstModelIdx).trimEnd() + "\n\n" : content; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| // Check if this is a database comment | ||
| if (line.trim().startsWith('// MySQL') || line.trim().startsWith('// MongoDB') || line.trim().startsWith('// PostgreSQL')) { | ||
| currentDb = line.trim().substring(3).toLowerCase(); // Extract database name | ||
| skipBlock = currentDb !== selectedDb; | ||
| if (!skipBlock) { | ||
| filteredLines.push(line); | ||
| } | ||
| continue; | ||
| } | ||
| // Check if we're in a database block and should skip it | ||
| if (skipBlock && currentDb) { | ||
| // Count braces to track function scope | ||
| const openBraces = (line.match(/\{/g) || []).length; | ||
| const closeBraces = (line.match(/\}/g) || []).length; | ||
| braceCount += openBraces - closeBraces; | ||
| // Continue skipping until we hit the next database comment or end of function | ||
| if (line.trim().startsWith('// ') && braceCount === 0) { | ||
| // End of current database block | ||
| skipBlock = false; | ||
| currentDb = null; | ||
| braceCount = 0; | ||
| if (line.trim() !== '') { | ||
| filteredLines.push(line); | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| // Add the line if we're not skipping | ||
| if (!skipBlock) { | ||
| filteredLines.push(line); | ||
| } | ||
| const modelRegex = /model\s+(\w+)\s*\{[\s\S]*?\}/g; | ||
| const modelBlocks = {}; | ||
| let m; | ||
| while ((m = modelRegex.exec(content)) !== null) { | ||
| modelBlocks[m[1]] = m[0]; | ||
| } | ||
| const finalContent = filteredLines.join('\n'); | ||
| await fs.writeFile(initDbPath, finalContent); | ||
| }, | ||
| const keepModels = modules.includes("auth") ? ["User"] : ["Example"]; | ||
| const keptBlocks = keepModels.map((name) => modelBlocks[name]).filter(Boolean); | ||
| async modifyIndexForDB(projectPath, selectedDb) { | ||
| const indexPath = path.join(projectPath, "index.js"); | ||
| if (!(await fs.pathExists(indexPath))) return; | ||
| let content = await fs.readFile(indexPath, "utf-8"); | ||
| // Split content into lines | ||
| const lines = content.split('\n'); | ||
| const filteredLines = []; | ||
| let skipBlock = false; | ||
| let currentDb = null; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| // Check if this is a database comment | ||
| if (line.trim().startsWith('// MySQL') || line.trim().startsWith('// MongoDB') || line.trim().startsWith('// PostgreSQL')) { | ||
| currentDb = line.trim().substring(3).toLowerCase(); // Extract database name | ||
| skipBlock = currentDb !== selectedDb; | ||
| if (!skipBlock) { | ||
| filteredLines.push(line); | ||
| } | ||
| continue; | ||
| } | ||
| // Check if we're in a database block and should skip it | ||
| if (skipBlock && currentDb) { | ||
| // Continue skipping until we hit the next database comment or end of block | ||
| if (line.trim().startsWith('// ') || line.trim() === '') { | ||
| // End of current database block | ||
| skipBlock = false; | ||
| currentDb = null; | ||
| if (line.trim() !== '') { | ||
| filteredLines.push(line); | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| // Add the line if we're not skipping | ||
| if (!skipBlock) { | ||
| filteredLines.push(line); | ||
| } | ||
| } | ||
| const finalContent = filteredLines.join('\n'); | ||
| await fs.writeFile(indexPath, finalContent); | ||
| const final = header + keptBlocks.join("\n\n") + "\n"; | ||
| await fs.writeFile(schemaPath, final); | ||
| }, | ||
@@ -410,3 +358,3 @@ | ||
| showSuccessMessage(projectName, db, modules) { | ||
| showSuccessMessage(projectName, db, modules, usePrisma = false) { | ||
| console.log(chalk.blue("\n✅ Project created successfully!")); | ||
@@ -419,6 +367,12 @@ console.log(`\n📁 ${projectName}`); | ||
| if (db !== "none" && db !== "mongodb" && modules.includes("auth")) { | ||
| console.log(chalk.cyan("\n🗄️ Database Setup:")); | ||
| console.log("npm run init-schema"); | ||
| console.log(chalk.gray(" # This will create the required database tables")); | ||
| if (db !== "none" && db !== "mongodb") { | ||
| if (usePrisma) { | ||
| console.log(chalk.cyan("\n🧩 Prisma Setup:")); | ||
| console.log("npm run prisma:generate"); | ||
| console.log("npm run prisma:db-push"); | ||
| } else { | ||
| console.log(chalk.cyan("\n🗄️ Database Setup:")); | ||
| console.log("npm run init-schema"); | ||
| console.log(chalk.gray(" # This will create the required database tables")); | ||
| } | ||
| } | ||
@@ -444,5 +398,11 @@ | ||
| const db = database.toLowerCase(); | ||
| let usePrisma = false; | ||
| let modules = ["none"]; | ||
| if (db !== "none") { | ||
| if (db === "postgresql" || db === "mysql") { | ||
| usePrisma = true; | ||
| } else { | ||
| usePrisma = false; | ||
| } | ||
| const { selectedModules } = await inquirer.prompt([ | ||
@@ -469,3 +429,20 @@ { | ||
| await fs.copy(baseTemplatePath, projectPath); | ||
| await fs.remove(path.join(projectPath, "configs/prisma.js")); | ||
| // Remove service index when no DB is selected | ||
| if (db === "none") { | ||
| await utils.removeFiles(["services/index.service.js"], projectPath); | ||
| } | ||
| // Remove example files if a real module is selected | ||
| if (!modules.includes("none")) { | ||
| await utils.removeFiles( | ||
| [ | ||
| "controllers/example.controller.js", | ||
| "services/example.service.js", | ||
| ], | ||
| projectPath | ||
| ); | ||
| } | ||
| // Remove all DB folders and config/env files by default | ||
@@ -499,7 +476,8 @@ await utils.removeFolders(["models", "schemas", "scripts"], projectPath); | ||
| await utils.removeFiles([...unselectedDbConfigFiles, ...Object.values(DB_ENV_FILES).flat()], projectPath); | ||
| // await utils.removeFiles(Object.values(DB_AUTH_SERVICES).flat(), projectPath); | ||
| const allServiceFiles = Object.values(DB_SERVICES) | ||
| .flatMap((byDb) => Object.values(byDb)) | ||
| .flat(); | ||
| const allServiceFiles = [ | ||
| ...Object.values(DB_SERVICES).flatMap((byDb) => Object.values(byDb)).flat(), | ||
| ...Object.values(DB_SERVICES_PRISMA).flatMap((byDb) => Object.values(byDb)).flat(), | ||
| ]; | ||
| await utils.removeFiles(allServiceFiles, projectPath); | ||
@@ -527,2 +505,45 @@ | ||
| if (!modules.includes("none")) { | ||
| await utils.updateFileContent(routesPath, [ | ||
| { | ||
| pattern: /import\s*\{\s*listData\s*\}\s*from\s*"\.\.\/controllers\/example\.controller\.js";\n?/g, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /\/\/\s*Example route\s*\n?/g, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /router\.get\("\/example",\s*listData\);\n?/g, | ||
| replacement: "" | ||
| }, | ||
| // collapse excessive blank lines left by removals | ||
| { | ||
| pattern: /\n{3,}/g, | ||
| replacement: "\n\n" | ||
| }, | ||
| ]); | ||
| }; | ||
| if (db !== "mongodb") { | ||
| await utils.updateFileContent(indexPath, [ | ||
| { | ||
| pattern: /import\s*\{\s*connectMongoDB\s*\}\s*from\s*"\.\/configs\/mongodb\.config\.js";\n?/gm, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /\/\/\s*Initialize\s*connection\s*\n?/gm, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /connectMongoDB\(\);\n?/gm, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /\n{3,}/g, | ||
| replacement: "\n\n" | ||
| }, | ||
| ]); | ||
| } | ||
| for (const module of Object.keys(CONFIG.FILES)) { | ||
@@ -538,13 +559,12 @@ const importPattern = new RegExp(`import ${module}Routes from "\\./${module}\\.routes\\.js";\\n?`, "g"); | ||
| // Remove database initialization logic from index.js if no modules selected | ||
| if (modules.includes("none")) { | ||
| await utils.removeDatabaseInitLogic(indexPath); | ||
| } | ||
| await utils.cleanEnvironmentConstants(envFilePath, db, modules); | ||
| // Remove any pre-existing prisma directory from template to avoid extra schemas | ||
| await fs.remove(path.join(projectPath, "prisma")); | ||
| // Setup DB folder/files based on selection | ||
| if (db === "mongodb") { | ||
| await utils.generateModelsFromModules(modules, projectPath, __dirname); | ||
| } else if (["postgresql", "mysql"].includes(db)) { | ||
| } else if (["postgresql", "mysql"].includes(db) && !usePrisma) { | ||
| await fs.ensureDir(path.join(projectPath, "schemas")); | ||
@@ -558,10 +578,38 @@ await fs.ensureDir(path.join(projectPath, "scripts")); | ||
| // Modify initDatabase.js to only include selected database | ||
| if (db !== "none" && !modules.includes("none")) { | ||
| await utils.modifyInitDatabaseForDB(projectPath, db); | ||
| if (db !== "none") { | ||
| const initScriptSrc = path.join(__dirname, "templates/js/base/services/index.service.js"); | ||
| const initScriptDest = path.join(projectPath, "services/index.service.js"); | ||
| await fs.copy(initScriptSrc, initScriptDest); | ||
| await utils.configureServiceIndex(projectPath, db, usePrisma); | ||
| } | ||
| // Modify index.js to only include selected database initialization | ||
| if (db !== "none" && !modules.includes("none")) { | ||
| await utils.modifyIndexForDB(projectPath, db); | ||
| // Prisma setup (PostgreSQL/MySQL) | ||
| if ((db === "postgresql" || db === "mysql") && usePrisma) { | ||
| const prismaConfigSrc = path.join(__dirname, "templates/js/base/configs/prisma.js"); | ||
| const prismaConfigDest = path.join(projectPath, "configs/prisma.js"); | ||
| await fs.copy(prismaConfigSrc, prismaConfigDest); | ||
| const prismaDir = path.join(projectPath, "prisma"); | ||
| await fs.ensureDir(prismaDir); | ||
| // Ensure a clean prisma directory with only the selected schema | ||
| await fs.emptyDir(prismaDir); | ||
| const schemaSrc = path.join(__dirname, `templates/js/base/prisma/schema.${db}.prisma`); | ||
| const schemaDest = path.join(prismaDir, "schema.prisma"); | ||
| await fs.copy(schemaSrc, schemaDest); | ||
| await utils.updatePrismaSchema(projectPath, modules); | ||
| await utils.updatePackageJson(projectPath, { | ||
| deps: { "@prisma/client": "latest" }, | ||
| devDeps: { prisma: "latest" }, | ||
| scripts: { | ||
| "prisma:generate": "prisma generate", | ||
| "prisma:db-push": "prisma db push", | ||
| "prisma:migrate": "prisma migrate dev --name init", | ||
| }, | ||
| removeScripts: ["init-schema"], | ||
| }); | ||
| // Remove native schema init script folder since Prisma is used | ||
| await fs.remove(path.join(projectPath, "scripts")); | ||
| } | ||
@@ -581,4 +629,10 @@ | ||
| // Remove all DB-specific auth service files | ||
| await utils.removeFiles(Object.values(DB_SERVICES.auth).flat(), projectPath); | ||
| // Remove all DB-specific auth service files (native and prisma) | ||
| await utils.removeFiles( | ||
| [ | ||
| ...Object.values(DB_SERVICES.auth).flat(), | ||
| ...Object.values(DB_SERVICES_PRISMA.auth).flat(), | ||
| ], | ||
| projectPath | ||
| ); | ||
@@ -589,3 +643,3 @@ // Get common auth files | ||
| // Get DB-specific service files only if db is selected | ||
| const dbSpecificAuthFiles = DB_SERVICES.auth[db] || []; | ||
| const dbSpecificAuthFiles = (usePrisma ? DB_SERVICES_PRISMA.auth[db] : DB_SERVICES.auth[db]) || []; | ||
@@ -616,7 +670,5 @@ const allAuthFiles = [...commonAuthFiles, ...dbSpecificAuthFiles]; | ||
| ]); | ||
| }, | ||
| }; | ||
| // Run selected modules | ||
@@ -628,5 +680,13 @@ for (const module of modules) { | ||
| // Create env example and setup environment | ||
| await utils.createEnvExample(projectPath, db, modules, __dirname); | ||
| await utils.createEnvExample(projectPath, db, modules, __dirname, usePrisma); | ||
| if ((db === "postgresql" || db === "mysql") && usePrisma) { | ||
| const envPath = path.join(projectPath, ".env"); | ||
| let envContent = (await fs.readFile(envPath, "utf-8")) || ""; | ||
| if (!envContent.includes("USE_PRISMA")) { | ||
| envContent += `USE_PRISMA=true\n`; | ||
| } | ||
| await fs.writeFile(envPath, envContent); | ||
| } | ||
| await utils.updatePackageJson(projectPath, { deps: { dotenv: "latest" } }); | ||
| await utils.showSuccessMessage(projectName, db, modules); | ||
| await utils.showSuccessMessage(projectName, db, modules, usePrisma); | ||
@@ -633,0 +693,0 @@ } catch (error) { |
+278
-46
@@ -47,7 +47,12 @@ import inquirer from "inquirer"; | ||
| mongodb: ["services/auth.mongodb.service.ts"], | ||
| postgresql: ["services/auth.postgresql.service.ts"], | ||
| mysql: ["services/auth.mysql.service.ts"], | ||
| }, | ||
| }; | ||
| const DB_SERVICES_PRISMA = { | ||
| auth: { | ||
| postgresql: ["services/auth.prisma.service.ts"], | ||
| mysql: ["services/auth.prisma.service.ts"], | ||
| }, | ||
| }; | ||
| const DB_TABLES = { | ||
@@ -119,2 +124,5 @@ auth: ["users"], | ||
| } | ||
| if (updates.scripts) { | ||
| pkg.scripts = { ...pkg.scripts, ...updates.scripts }; | ||
| } | ||
| if (updates.removeDeps) { | ||
@@ -139,3 +147,49 @@ updates.removeDeps.forEach((dep) => delete pkg.dependencies?.[dep]); | ||
| async createEnvExample(projectPath, db, modules, templateDir) { | ||
| async configureServiceIndex(projectPath, db, usePrisma) { | ||
| const serviceIndexPath = path.join(projectPath, "services/index.service.ts"); | ||
| if (!(await fs.pathExists(serviceIndexPath))) return; | ||
| const raw = await fs.readFile(serviceIndexPath, "utf-8"); | ||
| const lines = raw.split("\n"); | ||
| const marker = (() => { | ||
| if (db === "postgresql") return usePrisma | ||
| ? "// PostgreSQL (Prisma)" | ||
| : "// PostgreSQL (native)"; | ||
| if (db === "mysql") return usePrisma | ||
| ? "// MySQL (Prisma)" | ||
| : "// MySQL (native)"; | ||
| if (db === "mongodb") return "// MongoDB"; | ||
| return null; | ||
| })(); | ||
| if (!marker) { | ||
| await fs.writeFile(serviceIndexPath, "// No database selected\n"); | ||
| return; | ||
| } | ||
| let output = ""; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i].trim(); | ||
| if (line.startsWith(marker)) { | ||
| const comment = lines[i]; | ||
| let exportLine = ""; | ||
| for (let j = i + 1; j < lines.length; j++) { | ||
| const t = lines[j].trim(); | ||
| if (!t) continue; | ||
| if (t.startsWith("export ")) { | ||
| exportLine = lines[j]; | ||
| break; | ||
| } | ||
| if (t.startsWith("//")) break; | ||
| } | ||
| output = exportLine ? `${comment}\n${exportLine}\n` : `${comment}\n`; | ||
| break; | ||
| } | ||
| } | ||
| await fs.writeFile(serviceIndexPath, output || "// No matching service found\n"); | ||
| }, | ||
| async createEnvExample(projectPath, db, modules, templateDir, usePrisma) { | ||
| const dbEnvFileMap = { | ||
@@ -147,6 +201,22 @@ mongodb: "mongodb.env.example", | ||
| }; | ||
| const templateFile = dbEnvFileMap[db]; | ||
| const templatePath = path.join(templateDir, "templates/ts/base/configs/", templateFile); | ||
| let content = await fs.readFile(templatePath, "utf-8"); | ||
| const keepMarkers = new Set([`#${db}`]); | ||
| const keepMarkers = new Set(); | ||
| // DB selection | ||
| if (db === 'postgresql' || db === 'mysql') { | ||
| if (usePrisma) { | ||
| keepMarkers.add('#prisma'); | ||
| } else { | ||
| keepMarkers.add(`#${db}`); | ||
| } | ||
| } else { | ||
| keepMarkers.add(`#${db}`); | ||
| } | ||
| // Module markers (e.g., #jwt token) | ||
| modules.forEach((mod) => { | ||
@@ -158,2 +228,3 @@ const markers = CONFIG.ENV_MODULES[mod]; | ||
| }); | ||
| const blocks = content | ||
@@ -166,5 +237,13 @@ .split(/\n(?=#)/g) | ||
| }); | ||
| const finalEnvExample = blocks.join("\n\n") + "\n"; | ||
| await fs.writeFile(path.join(projectPath, ".env.example"), finalEnvExample); | ||
| await fs.writeFile(path.join(projectPath, ".env"), `DB_TYPE=${db}\n`); | ||
| // Seed a minimal .env | ||
| let envSeed = `DB_TYPE=${db}\n`; | ||
| if ((db === 'postgresql' || db === 'mysql') && usePrisma) { | ||
| envSeed += `USE_PRISMA=true\n`; | ||
| } | ||
| await fs.writeFile(path.join(projectPath, ".env"), envSeed); | ||
| }, | ||
@@ -192,2 +271,26 @@ | ||
| async updatePrismaSchema(projectPath, modules) { | ||
| const schemaPath = path.join(projectPath, "prisma", "schema.prisma"); | ||
| if (!(await fs.pathExists(schemaPath))) return; | ||
| const content = await fs.readFile(schemaPath, "utf-8"); | ||
| const firstModelMatch = content.match(/\nmodel\s+\w+\s*\{/); | ||
| const firstModelIdx = firstModelMatch ? content.indexOf(firstModelMatch[0]) : -1; | ||
| const header = firstModelIdx >= 0 ? content.slice(0, firstModelIdx).trimEnd() + "\n\n" : content; | ||
| const modelRegex = /model\s+(\w+)\s*\{[\s\S]*?\}/g; | ||
| const modelBlocks = {}; | ||
| let m; | ||
| while ((m = modelRegex.exec(content)) !== null) { | ||
| modelBlocks[m[1]] = m[0]; | ||
| } | ||
| const keepModels = modules.includes("auth") ? ["User"] : ["Example"]; | ||
| const keptBlocks = keepModels.map((name) => modelBlocks[name]).filter(Boolean); | ||
| const final = header + keptBlocks.join("\n\n") + "\n"; | ||
| await fs.writeFile(schemaPath, final); | ||
| }, | ||
| async removeDatabaseInitLogic(filePath) { | ||
@@ -381,3 +484,3 @@ if (!(await fs.pathExists(filePath))) return; | ||
| showSuccessMessage(projectName, db, modules) { | ||
| showSuccessMessage(projectName, db, modules, usePrisma = false) { | ||
| console.log(chalk.blue("\n✅ Project created successfully!")); | ||
@@ -388,8 +491,16 @@ console.log(`\n📁 ${projectName}`); | ||
| console.log("npm install"); | ||
| console.log(chalk.cyan("\n👉 Copy the .env.example file to .env and fill in your credentials and values before running the app.")); | ||
| if (db !== "none" && db !== "mongodb" && modules.includes("auth")) { | ||
| console.log(chalk.cyan("\n🗄️ Database Setup:")); | ||
| console.log("npm run init-schema"); | ||
| console.log(chalk.gray(" # This will create the required database tables")); | ||
| console.log(chalk.cyan("\n👉 Copy the .env.example file to .env and fill in your credentials and values before running the app.`")); | ||
| if (db !== "none" && db !== "mongodb") { | ||
| if (usePrisma) { | ||
| console.log(chalk.cyan("\n🧩 Prisma Setup:")); | ||
| console.log("npm run prisma:generate"); | ||
| console.log("npm run prisma:db-push"); | ||
| } else { | ||
| console.log(chalk.cyan("\n🗄️ Database Setup:")); | ||
| console.log("npm run init-schema"); | ||
| console.log(chalk.gray(" # This will create the required database tables")); | ||
| } | ||
| } | ||
| console.log("\n npm run dev\n"); | ||
@@ -409,5 +520,13 @@ }, | ||
| ]); | ||
| const db = database.toLowerCase(); | ||
| let usePrisma = false; | ||
| let modules = ["none"]; | ||
| if (db !== "none") { | ||
| if (db === "postgresql" || db === "mysql") { | ||
| usePrisma = true; | ||
| } else { | ||
| usePrisma = false; | ||
| } | ||
| const { selectedModules } = await inquirer.prompt([ | ||
@@ -425,3 +544,2 @@ { | ||
| ]); | ||
| // If no modules are selected, treat it as "none" | ||
| modules = selectedModules.length === 0 ? ["none"] : selectedModules; | ||
@@ -431,5 +549,24 @@ } else { | ||
| } | ||
| // Copy base project | ||
| const baseTemplatePath = path.join(__dirname, `templates/${CONFIG.LANG}/base`); | ||
| await fs.copy(baseTemplatePath, projectPath); | ||
| await fs.remove(path.join(projectPath, "configs/prisma.ts")); | ||
| // Remove service index when no DB is selected | ||
| if (db === "none") { | ||
| await utils.removeFiles(["services/index.service.ts"], projectPath); | ||
| } | ||
| // Remove example files if a real module is selected | ||
| if (!modules.includes("none")) { | ||
| await utils.removeFiles( | ||
| [ | ||
| "controllers/example.controller.ts", | ||
| "services/example.service.ts", | ||
| ], | ||
| projectPath | ||
| ); | ||
| } | ||
| // Remove all DB folders and config/env files by default | ||
@@ -463,6 +600,8 @@ await utils.removeFolders(["models", "schemas", "scripts"], projectPath); | ||
| await utils.removeFiles([...unselectedDbConfigFiles, ...Object.values(DB_ENV_FILES).flat()], projectPath); | ||
| const allServiceFiles = Object.values(DB_SERVICES) | ||
| .flatMap((byDb) => Object.values(byDb)) | ||
| .flat(); | ||
| const allServiceFiles = [ | ||
| ...Object.values(DB_SERVICES).flatMap((byDb) => Object.values(byDb)).flat(), | ||
| ...Object.values(DB_SERVICES_PRISMA).flatMap((byDb) => Object.values(byDb)).flat(), | ||
| ]; | ||
| await utils.removeFiles(allServiceFiles, projectPath); | ||
@@ -490,6 +629,48 @@ | ||
| if (!modules.includes("none")) { | ||
| await utils.updateFileContent(routesPath, [ | ||
| { | ||
| pattern: /import\s*\{\s*listData\s*\}\s*from\s*"\.\.\/controllers\/example\.controller\.ts";\n?/g, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /\/\/\s*Example route\s*\n?/g, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /router\.get\("\/example",\s*listData\);\n?/g, | ||
| replacement: "" | ||
| }, | ||
| // collapse excessive blank lines left by removals | ||
| { | ||
| pattern: /\n{3,}/g, | ||
| replacement: "\n\n" | ||
| }, | ||
| ]); | ||
| } | ||
| if (db !== "mongodb") { | ||
| await utils.updateFileContent(indexPath, [ | ||
| { | ||
| pattern: /import\s*\{\s*connectMongoDB\s*\}\s*from\s*"\.\/configs\/mongodb\.config\.ts";\n?/gm, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /\/\/\s*Initialize\s*connection\s*\n?/gm, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /connectMongoDB\(\);\n?/gm, | ||
| replacement: "" | ||
| }, | ||
| { | ||
| pattern: /\n{3,}/g, | ||
| replacement: "\n\n" | ||
| }, | ||
| ]); | ||
| } | ||
| for (const module of Object.keys(CONFIG.FILES)) { | ||
| // Remove import and use statements for the module | ||
| const importPattern = new RegExp(`import ${module}Routes from \\\"\\./${module}\\.routes\\.js\\\";\\n?`, "g"); | ||
| const usePattern = new RegExp(`// .* routes\\nrouter\\.use\\(\\"/${module}\\", ${module}Routes\\);\\n?`, "g"); | ||
| const importPattern = new RegExp(`import ${module}Routes from "\\./${module}\\.routes\\.ts";\\n?`, "g"); | ||
| const usePattern = new RegExp(`// .* routes\\nrouter\\.use\\("/${module}", ${module}Routes\\);\\n?`, "g"); | ||
@@ -502,13 +683,11 @@ await utils.updateFileContent(routesPath, [ | ||
| // Remove database initialization logic from index.ts if no modules selected | ||
| if (modules.includes("none")) { | ||
| await utils.removeDatabaseInitLogic(indexPath); | ||
| } | ||
| await utils.cleanEnvironmentConstants(envFilePath, db, modules); | ||
| // Remove any pre-existing prisma directory from template to avoid extra schemas | ||
| await fs.remove(path.join(projectPath, "prisma")); | ||
| // Setup DB folder/files based on selection | ||
| if (db === "mongodb") { | ||
| await utils.generateModelsFromModules(modules, projectPath, __dirname); | ||
| } else if (["postgresql", "mysql"].includes(db)) { | ||
| } else if (["postgresql", "mysql"].includes(db) && !usePrisma) { | ||
| await fs.ensureDir(path.join(projectPath, "schemas")); | ||
@@ -522,10 +701,38 @@ await fs.ensureDir(path.join(projectPath, "scripts")); | ||
| // Modify initDatabase.ts to only include selected database | ||
| if (db !== "none" && !modules.includes("none")) { | ||
| await utils.modifyInitDatabaseForDB(projectPath, db); | ||
| if (db !== "none") { | ||
| const initScriptSrc = path.join(__dirname, "templates/ts/base/services/index.service.ts"); | ||
| const initScriptDest = path.join(projectPath, "services/index.service.ts"); | ||
| await fs.copy(initScriptSrc, initScriptDest); | ||
| await utils.configureServiceIndex(projectPath, db, usePrisma); | ||
| } | ||
| // Modify index.ts to only include selected database initialization | ||
| if (db !== "none" && !modules.includes("none")) { | ||
| await utils.modifyIndexForDB(projectPath, db); | ||
| // Prisma setup (PostgreSQL/MySQL) | ||
| if ((db === "postgresql" || db === "mysql") && usePrisma) { | ||
| const prismaConfigSrc = path.join(__dirname, "templates/ts/base/configs/prisma.ts"); | ||
| const prismaConfigDest = path.join(projectPath, "configs/prisma.ts"); | ||
| await fs.copy(prismaConfigSrc, prismaConfigDest); | ||
| const prismaDir = path.join(projectPath, "prisma"); | ||
| await fs.ensureDir(prismaDir); | ||
| // Ensure a clean prisma directory with only the selected schema | ||
| await fs.emptyDir(prismaDir); | ||
| const schemaSrc = path.join(__dirname, `templates/ts/base/prisma/schema.${db}.prisma`); | ||
| const schemaDest = path.join(prismaDir, "schema.prisma"); | ||
| await fs.copy(schemaSrc, schemaDest); | ||
| await utils.updatePrismaSchema(projectPath, modules); | ||
| await utils.updatePackageJson(projectPath, { | ||
| deps: { "@prisma/client": "latest" }, | ||
| devDeps: { prisma: "latest" }, | ||
| scripts: { | ||
| "prisma:generate": "prisma generate", | ||
| "prisma:db-push": "prisma db push", | ||
| "prisma:migrate": "prisma migrate dev --name init", | ||
| }, | ||
| removeScripts: ["init-schema"], | ||
| }); | ||
| // Remove native schema init script folder since Prisma is used | ||
| await fs.remove(path.join(projectPath, "scripts")); | ||
| } | ||
@@ -539,2 +746,3 @@ | ||
| } | ||
| // MODULE REGISTRY SYSTEM | ||
@@ -544,9 +752,20 @@ const MODULES = { | ||
| if (!modules.includes("auth")) return; | ||
| // Remove all DB-specific auth service files | ||
| await utils.removeFiles(Object.values(DB_SERVICES.auth).flat(), projectPath); | ||
| // Remove all DB-specific auth service files (native and prisma) | ||
| await utils.removeFiles( | ||
| [ | ||
| ...Object.values(DB_SERVICES.auth).flat(), | ||
| ...Object.values(DB_SERVICES_PRISMA.auth).flat(), | ||
| ], | ||
| projectPath | ||
| ); | ||
| // Get common auth files | ||
| const commonAuthFiles = CONFIG.FILES.auth; | ||
| // Get DB-specific service files only if db is selected | ||
| const dbSpecificAuthFiles = DB_SERVICES.auth[db] || []; | ||
| const dbSpecificAuthFiles = (usePrisma ? DB_SERVICES_PRISMA.auth[db] : DB_SERVICES.auth[db]) || []; | ||
| const allAuthFiles = [...commonAuthFiles, ...dbSpecificAuthFiles]; | ||
| for (const file of allAuthFiles) { | ||
@@ -557,19 +776,22 @@ const src = path.join(__dirname, `templates/ts/base/${file}`); | ||
| } | ||
| // Add dependencies | ||
| await utils.updatePackageJson(projectPath, { | ||
| deps: { bcryptjs: "latest", jsonwebtoken: "latest" }, | ||
| }); | ||
| await utils.updatePackageJson(projectPath, { | ||
| deps: { bcryptjs: "latest", jsonwebtoken: "latest" }, | ||
| }); | ||
| // Inject auth routes | ||
| await utils.updateFileContent(routesPath, [ | ||
| { | ||
| pattern: /import express from "express";/, | ||
| await utils.updateFileContent(routesPath, [ | ||
| { | ||
| pattern: /import express from "express";/, | ||
| replacement: `import express from "express";\nimport authRoutes from "./auth.routes.ts";`, | ||
| }, | ||
| { | ||
| pattern: /export default router;/, | ||
| }, | ||
| { | ||
| pattern: /export default router;/, | ||
| replacement: `// Authentication routes\nrouter.use("/auth", authRoutes);\n\nexport default router;`, | ||
| }, | ||
| ]); | ||
| }, | ||
| ]); | ||
| }, | ||
| }; | ||
| // Run selected modules | ||
@@ -579,6 +801,16 @@ for (const module of modules) { | ||
| } | ||
| // Create env example and setup environment | ||
| await utils.createEnvExample(projectPath, db, modules, __dirname); | ||
| await utils.createEnvExample(projectPath, db, modules, __dirname, usePrisma); | ||
| if ((db === "postgresql" || db === "mysql") && usePrisma) { | ||
| const envPath = path.join(projectPath, ".env"); | ||
| let envContent = (await fs.readFile(envPath, "utf-8")) || ""; | ||
| if (!envContent.includes("USE_PRISMA")) { | ||
| envContent += `USE_PRISMA=true\n`; | ||
| } | ||
| await fs.writeFile(envPath, envContent); | ||
| } | ||
| await utils.updatePackageJson(projectPath, { deps: { dotenv: "latest" } }); | ||
| await utils.showSuccessMessage(projectName, db, modules); | ||
| await utils.showSuccessMessage(projectName, db, modules, usePrisma); | ||
| } catch (error) { | ||
@@ -585,0 +817,0 @@ console.error(chalk.red(`\n❌ Error creating project: ${error.message}`)); |
+17
-17
| { | ||
| "name": "@trisers/cli-node-template", | ||
| "version": "1.1.2", | ||
| "version": "1.2.0", | ||
| "main": "index.js", | ||
@@ -13,17 +13,17 @@ "type": "module", | ||
| "files": [ | ||
| "index.js", | ||
| "package.json", | ||
| "README.md", | ||
| "LICENSE", | ||
| "handlers", | ||
| "handlers/**/*", | ||
| "templates", | ||
| "templates/**/*", | ||
| "templates/**/*.md", | ||
| "templates/**/*.json", | ||
| "templates/**/*.env", | ||
| "templates/**/*.env.example", | ||
| "templates/**/*.env.example.md", | ||
| "templates/**/*.env.example.json" | ||
| ], | ||
| "index.js", | ||
| "package.json", | ||
| "README.md", | ||
| "LICENSE", | ||
| "handlers", | ||
| "handlers/**/*", | ||
| "templates", | ||
| "templates/**/*", | ||
| "templates/**/*.md", | ||
| "templates/**/*.json", | ||
| "templates/**/*.env", | ||
| "templates/**/*.env.example", | ||
| "templates/**/*.env.example.md", | ||
| "templates/**/*.env.example.json" | ||
| ], | ||
| "preferGlobal": true, | ||
@@ -37,5 +37,5 @@ "keywords": [], | ||
| "fs-extra": "^11.3.0", | ||
| "inquirer": "^12.7.0", | ||
| "inquirer": "^12.9.2", | ||
| "tree": "^0.1.3" | ||
| } | ||
| } |
@@ -5,3 +5,3 @@ import mongoose from "mongoose"; | ||
| // Connect to MongoDB | ||
| const connectMongoDB = async () => { | ||
| export const connectMongoDB = async () => { | ||
| try { | ||
@@ -16,5 +16,2 @@ await mongoose.connect(MONGODB_URI); | ||
| // Initialize connection | ||
| connectMongoDB(); | ||
| export default mongoose; |
@@ -6,2 +6,5 @@ import { configDotenv } from "dotenv"; | ||
| // Prisma / ORM | ||
| export const { USE_PRISMA, DATABASE_URL } = process.env; | ||
| // JWT Environment Variables | ||
@@ -8,0 +11,0 @@ export const { |
| import { asyncHandler } from "../middlewares/errorHandler.js"; | ||
| import { ApiError } from "../middlewares/errorHandler.js"; | ||
| import { isValidEmail, validatePassword } from "../utils/auth.utils.js"; | ||
| import { authService } from "../services/index.service.js"; | ||
| // This will be injected based on the selected database | ||
| let authService; | ||
| // Setter function to inject the appropriate auth service | ||
| export const setAuthService = (service) => { | ||
| authService = service; | ||
| }; | ||
| /** | ||
@@ -14,0 +7,0 @@ * Register a new user |
@@ -5,2 +5,3 @@ import express from "express"; | ||
| import initializeServer from "./configs/initServer.js"; | ||
| import { connectMongoDB } from "./configs/mongodb.config.js"; | ||
@@ -16,32 +17,6 @@ const app = express(); | ||
| // Initialize database and authentication service | ||
| const databaseType = process.env.DB_TYPE || "none"; | ||
| // MySQL | ||
| if (databaseType === "mysql") { | ||
| const { initializeMySQLDatabase } = await import("./configs/initDatabase.js"); | ||
| initializeMySQLDatabase().catch((err) => { | ||
| console.error("Failed to initialize database:", err.message); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| // Initialize connection | ||
| connectMongoDB(); | ||
| // MongoDB | ||
| if (databaseType === "mongodb") { | ||
| const { initializeMongoDBDatabase } = await import("./configs/initDatabase.js"); | ||
| initializeMongoDBDatabase().catch((err) => { | ||
| console.error("Failed to initialize database:", err.message); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| // PostgreSQL | ||
| if (databaseType === "postgresql") { | ||
| const { initializePostgreSQLDatabase } = await import("./configs/initDatabase.js"); | ||
| initializePostgreSQLDatabase().catch((err) => { | ||
| console.error("Failed to initialize database:", err.message); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| initializeServer(app); |
| # Node.js JavaScript Boilerplate | ||
| A modern Node.js boilerplate with Express.js and ES modules support. | ||
| ## Setup | ||
| 1. Copy `.env.example` to `.env` and fill values. | ||
| 2. Select your database in `.env` via `DB_TYPE`. | ||
| 3. Optional: set `USE_PRISMA=true` to use Prisma instead of native drivers and define `DATABASE_URL`. | ||
| ### Database | ||
| - MySQL/PostgreSQL (native): run `npm run init-schema` to create tables. | ||
| - Prisma: run `npm run prisma:generate` then `npm run prisma:db-push` to create tables. |
@@ -5,3 +5,3 @@ import mongoose from "mongoose"; | ||
| // Connect to MongoDB | ||
| const connectMongoDB = async () => { | ||
| export const connectMongoDB = async () => { | ||
| try { | ||
@@ -16,5 +16,3 @@ await mongoose.connect(MONGODB_URI); | ||
| // Initialize connection | ||
| connectMongoDB(); | ||
| export default mongoose; |
@@ -6,2 +6,5 @@ import { configDotenv } from "dotenv"; | ||
| // Prisma / ORM | ||
| export const { USE_PRISMA, DATABASE_URL } = process.env as Record<string, string>; | ||
| // JWT Environment Variables | ||
@@ -8,0 +11,0 @@ export const { |
@@ -7,11 +7,4 @@ import { Response } from "express"; | ||
| import { isValidEmail, validatePassword } from "../utils/auth.utils.js"; | ||
| import { authService } from "../services/index.service.js"; | ||
| // This will be injected based on the selected database | ||
| let authService: IAuthService; | ||
| // Setter function to inject the appropriate auth service | ||
| export const setAuthService = (service: IAuthService) => { | ||
| authService = service; | ||
| }; | ||
| /** | ||
@@ -18,0 +11,0 @@ * Register a new user |
@@ -5,2 +5,3 @@ import express from "express"; | ||
| import initializeServer from "./configs/initServer.js"; | ||
| import { connectMongoDB } from "./configs/mongodb.config.js"; | ||
@@ -16,32 +17,5 @@ const app = express(); | ||
| // Initialize database and authentication service | ||
| const databaseType = process.env.DB_TYPE || "none"; | ||
| // Initialize connection | ||
| connectMongoDB(); | ||
| // MySQL | ||
| if (databaseType === "mysql") { | ||
| const { initializeMySQLDatabase } = await import("./configs/initDatabase.js"); | ||
| initializeMySQLDatabase().catch((err) => { | ||
| console.error("Failed to initialize database:", err.message); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| // MongoDB | ||
| if (databaseType === "mongodb") { | ||
| const { initializeMongoDBDatabase } = await import("./configs/initDatabase.js"); | ||
| initializeMongoDBDatabase().catch((err) => { | ||
| console.error("Failed to initialize database:", err.message); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| // PostgreSQL | ||
| if (databaseType === "postgresql") { | ||
| const { initializePostgreSQLDatabase } = await import("./configs/initDatabase.js"); | ||
| initializePostgreSQLDatabase().catch((err) => { | ||
| console.error("Failed to initialize database:", err.message); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| initializeServer(app); |
| # Node.js TypeScript Boilerplate | ||
| A modern Node.js boilerplate with Express.js, TypeScript and ES modules support. | ||
| A modern Node.js boilerplate with Express.js, TypeScript and ES modules support. | ||
| ## Setup | ||
| 1. Copy `.env.example` to `.env` and fill values. | ||
| 2. Select your database in `.env` via `DB_TYPE`. | ||
| 3. Optional: set `USE_PRISMA=true` to use Prisma instead of native drivers and define `DATABASE_URL`. | ||
| ### Database | ||
| - MySQL/PostgreSQL (native): run `npm run init-schema` to create tables. | ||
| - Prisma: run `npm run prisma:generate` then `npm run prisma:db-push` to create tables. |
| import { setAuthService } from "../controllers/auth.controller.js"; | ||
| // MySQL | ||
| import mysqlPool from "./mysql.config.js"; | ||
| import { authService as mysqlAuthService } from "../services/auth.mysql.service.js"; | ||
| export const initializeMySQLDatabase = async () => { | ||
| try { | ||
| const mysqlConnection = await mysqlPool.getConnection(); | ||
| console.log("MySQL connection has been established"); | ||
| mysqlConnection.release(); | ||
| await mysqlAuthService.initializeTables(); | ||
| setAuthService(mysqlAuthService); | ||
| console.log("✅ Authentication service initialized with MySQL"); | ||
| } catch (err) { | ||
| console.error("Error initializing MySQL:", err.message); | ||
| throw err; | ||
| } | ||
| }; | ||
| // PostgreSQL | ||
| import postgresqlPool from "./postgresql.config.js"; | ||
| import { authService as postgresqlAuthService } from "../services/auth.postgresql.service.js"; | ||
| export const initializePostgreSQLDatabase = async () => { | ||
| try { | ||
| const postgresqlClient = await postgresqlPool.connect(); | ||
| console.log("PostgreSQL connection has been established"); | ||
| postgresqlClient.release(); | ||
| await postgresqlAuthService.initializeTables(); | ||
| setAuthService(postgresqlAuthService); | ||
| console.log("✅ Authentication service initialized with PostgreSQL"); | ||
| } catch (err) { | ||
| console.error("Error initializing PostgreSQL:", err.message); | ||
| throw err; | ||
| } | ||
| }; | ||
| // MongoDB | ||
| import mongoose from "./mongodb.config.js"; | ||
| import { authService as mongodbAuthService } from "../services/auth.mongodb.service.js"; | ||
| export const initializeMongoDBDatabase = async () => { | ||
| try { | ||
| // Wait for mongoose connection to be ready | ||
| if (mongoose.connection.readyState === 0) { | ||
| // If not connected, wait for connection | ||
| await new Promise((resolve, reject) => { | ||
| mongoose.connection.once('connected', () => resolve()); | ||
| mongoose.connection.once('error', (err) => reject(err)); | ||
| // Timeout after 10 seconds | ||
| setTimeout(() => reject(new Error('MongoDB connection timeout')), 10000); | ||
| }); | ||
| } | ||
| setAuthService(mongodbAuthService); | ||
| console.log("✅ Authentication service initialized with MongoDB"); | ||
| } catch (err) { | ||
| console.error("Error initializing MongoDB:", err.message); | ||
| throw err; | ||
| } | ||
| }; |
| import bcrypt from "bcryptjs"; | ||
| import jwt from "jsonwebtoken"; | ||
| import { | ||
| generateToken, | ||
| generateRefreshToken, | ||
| verifyRefreshToken, | ||
| generatePasswordResetToken, | ||
| hashPassword, | ||
| comparePassword, | ||
| sanitizeUser, | ||
| } from "../utils/auth.utils.js"; | ||
| import { ApiError, asyncFunctionHandler } from "../middlewares/errorHandler.js"; | ||
| import pool from "../configs/mysql.config.js"; | ||
| /** | ||
| * Auth service with asyncHandler pattern | ||
| * All methods are wrapped with asyncFunctionHandler to avoid try-catch blocks | ||
| * Pool is imported directly from config | ||
| */ | ||
| export const authService = { | ||
| // Initialize database tables | ||
| initializeTables: asyncFunctionHandler(async () => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| await connection.execute(` | ||
| CREATE TABLE IF NOT EXISTS users ( | ||
| id INT AUTO_INCREMENT PRIMARY KEY, | ||
| email VARCHAR(255) UNIQUE NOT NULL, | ||
| password VARCHAR(255) NOT NULL, | ||
| first_name VARCHAR(100), | ||
| last_name VARCHAR(100), | ||
| is_active BOOLEAN DEFAULT true, | ||
| refresh_tokens JSON, | ||
| password_reset_token VARCHAR(255), | ||
| password_reset_expires DATETIME, | ||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||
| ) | ||
| `); | ||
| console.log("✅ Users table initialized"); | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Register a new user | ||
| register: asyncFunctionHandler(async (userData) => { | ||
| const { email, password, firstName, lastName } = userData; | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Check if user already exists | ||
| const [existingUsers] = await connection.execute( | ||
| "SELECT id FROM users WHERE email = ?", | ||
| [email] | ||
| ); | ||
| if (existingUsers.length > 0) { | ||
| throw ApiError( | ||
| "User with this email already exists", | ||
| 409, | ||
| {}, | ||
| "userAlreadyExists" | ||
| ); | ||
| } | ||
| // Hash password | ||
| const hashedPassword = await hashPassword(password); | ||
| // Create user | ||
| const [result] = await connection.execute( | ||
| ` | ||
| INSERT INTO users (email, password, first_name, last_name) | ||
| VALUES (?, ?, ?, ?) | ||
| `, | ||
| [email, hashedPassword, firstName, lastName] | ||
| ); | ||
| const id = result.insertId; | ||
| // Get the created user | ||
| const [users] = await connection.execute( | ||
| "SELECT id, email, first_name, last_name, is_active, created_at, updated_at FROM users WHERE id = ?", | ||
| [id] | ||
| ); | ||
| const user = users[0]; | ||
| // Generate tokens | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.is_active }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // Save refresh token | ||
| await connection.execute( | ||
| "UPDATE users SET refresh_tokens = JSON_ARRAY(?) WHERE id = ?", | ||
| [refreshToken, user.id] | ||
| ); | ||
| return { | ||
| user: sanitizeUser(user), | ||
| token, | ||
| refreshToken, | ||
| }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Login user | ||
| login: asyncFunctionHandler(async (credentials) => { | ||
| const { email, password } = credentials; | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Find user by email | ||
| const [users] = await connection.execute( | ||
| "SELECT * FROM users WHERE email = ?", | ||
| [email] | ||
| ); | ||
| if (users.length === 0) { | ||
| throw ApiError( | ||
| "Invalid email or password", | ||
| 401, | ||
| {}, | ||
| "invalidCredentials" | ||
| ); | ||
| } | ||
| const user = users[0]; | ||
| // Check if user is active | ||
| if (!user.is_active) { | ||
| throw ApiError("Account is deactivated", 401, {}, "accountDeactivated"); | ||
| } | ||
| // Verify password | ||
| const isPasswordValid = await comparePassword(password, user.password); | ||
| if (!isPasswordValid) { | ||
| throw ApiError( | ||
| "Invalid email or password", | ||
| 401, | ||
| {}, | ||
| "invalidCredentials" | ||
| ); | ||
| } | ||
| // Generate tokens | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.is_active }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // Save refresh token | ||
| await connection.execute( | ||
| "UPDATE users SET refresh_tokens = JSON_ARRAY(?) WHERE id = ?", | ||
| [refreshToken, user.id] | ||
| ); | ||
| return { | ||
| user: sanitizeUser(user), | ||
| token, | ||
| refreshToken, | ||
| }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Logout user | ||
| logout: asyncFunctionHandler(async (id) => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Clear refresh tokens | ||
| await connection.execute( | ||
| "UPDATE users SET refresh_tokens = JSON_ARRAY() WHERE id = ?", | ||
| [id] | ||
| ); | ||
| return { message: "Logged out successfully" }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Refresh token | ||
| refreshToken: asyncFunctionHandler(async (refreshToken) => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // 1. Verify refresh token | ||
| const decoded = verifyRefreshToken(refreshToken); | ||
| if (!decoded) { | ||
| throw ApiError("Invalid refresh token", 401, {}, "invalidRefreshToken"); | ||
| } | ||
| // 2. Find the user | ||
| const [users] = await connection.execute( | ||
| "SELECT * FROM users WHERE id = ? AND is_active = true", | ||
| [decoded.id] | ||
| ); | ||
| if (users.length === 0) { | ||
| throw ApiError("User not found or inactive", 401, {}, "userNotFound"); | ||
| } | ||
| const user = users[0]; | ||
| // 3. Generate new tokens | ||
| const newToken = generateToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| const newRefreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // 4. Replace refresh_tokens with ONLY the new token | ||
| await connection.execute( | ||
| "UPDATE users SET refresh_tokens = JSON_ARRAY(?) WHERE id = ?", | ||
| [newRefreshToken, user.id] | ||
| ); | ||
| // 5. Fetch updated user to include correct refresh_tokens | ||
| const [updatedUsers] = await connection.execute( | ||
| "SELECT * FROM users WHERE id = ?", | ||
| [user.id] | ||
| ); | ||
| const updatedUser = updatedUsers[0]; | ||
| // 6. Return new token pair | ||
| return { | ||
| user: sanitizeUser(updatedUser), | ||
| token: newToken, | ||
| refreshToken: newRefreshToken, | ||
| }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Forgot password | ||
| forgotPassword: asyncFunctionHandler(async (email) => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Check if user exists | ||
| const [users] = await connection.execute( | ||
| "SELECT id, email FROM users WHERE email = ?", | ||
| [email] | ||
| ); | ||
| if (users.length === 0) { | ||
| throw ApiError( | ||
| "User with this email does not exist", | ||
| 404, | ||
| {}, | ||
| "userNotFound" | ||
| ); | ||
| } | ||
| const user = users[0]; | ||
| // Generate password reset token | ||
| const resetToken = generatePasswordResetToken(user.id); | ||
| const resetExpires = new Date(Date.now() + 3600000); // 1 hour | ||
| // Save reset token | ||
| await connection.execute( | ||
| "UPDATE users SET password_reset_token = ?, password_reset_expires = ? WHERE id = ?", | ||
| [resetToken, resetExpires, user.id] | ||
| ); | ||
| return { | ||
| message: "Password reset email sent", | ||
| resetToken, // In production, this would be sent via email | ||
| }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Reset password | ||
| resetPassword: asyncFunctionHandler(async (token, newPassword) => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Find user with valid reset token | ||
| const [users] = await connection.execute( | ||
| "SELECT id FROM users WHERE password_reset_token = ? AND password_reset_expires > NOW()", | ||
| [token] | ||
| ); | ||
| if (users.length === 0) { | ||
| throw ApiError( | ||
| "Invalid or expired reset token", | ||
| 400, | ||
| {}, | ||
| "invalidResetToken" | ||
| ); | ||
| } | ||
| const user = users[0]; | ||
| // Hash new password | ||
| const hashedPassword = await hashPassword(newPassword); | ||
| // Update password and clear reset token | ||
| await connection.execute( | ||
| "UPDATE users SET password = ?, password_reset_token = NULL, password_reset_expires = NULL WHERE id = ?", | ||
| [hashedPassword, user.id] | ||
| ); | ||
| return { message: "Password reset successfully" }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Change password | ||
| changePassword: asyncFunctionHandler( | ||
| async (id, oldPassword, newPassword) => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Get user's current password | ||
| const [users] = await connection.execute( | ||
| "SELECT password FROM users WHERE id = ?", | ||
| [id] | ||
| ); | ||
| if (users.length === 0) { | ||
| throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| } | ||
| const user = users[0]; | ||
| // Verify old password | ||
| const isOldPasswordValid = await comparePassword( | ||
| oldPassword, | ||
| user.password | ||
| ); | ||
| if (!isOldPasswordValid) { | ||
| throw ApiError("Invalid old password", 400, {}, "invalidOldPassword"); | ||
| } | ||
| // Hash new password | ||
| const hashedPassword = await hashPassword(newPassword); | ||
| // Update password | ||
| await connection.execute("UPDATE users SET password = ? WHERE id = ?", [ | ||
| hashedPassword, | ||
| id, | ||
| ]); | ||
| return { message: "Password changed successfully" }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| } | ||
| ), | ||
| // Verify token | ||
| verifyToken: asyncFunctionHandler(async (token) => { | ||
| // This would typically verify the JWT token | ||
| // For now, we'll just return a success response | ||
| return { id: "", email: "" }; | ||
| }), | ||
| // Get user by ID | ||
| getUserById: asyncFunctionHandler(async (id) => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| const [users] = await connection.execute( | ||
| "SELECT id, email, first_name, last_name, is_active, created_at, updated_at FROM users WHERE id = ?", | ||
| [id] | ||
| ); | ||
| if (users.length === 0) { | ||
| throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| } | ||
| return sanitizeUser(users[0]); | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Update user by ID | ||
| updateUserById: asyncFunctionHandler(async (id, firstName, lastName) => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| const [result] = await connection.execute( | ||
| "UPDATE users SET first_name = ?, last_name = ? WHERE id = ?", | ||
| [firstName, lastName, id] | ||
| ); | ||
| if (result.affectedRows === 0) { | ||
| return null; | ||
| } | ||
| // Fetch updated user | ||
| const [users] = await connection.execute( | ||
| "SELECT * FROM users WHERE id = ?", | ||
| [id] | ||
| ); | ||
| if (users.length === 0) { | ||
| return null; | ||
| } | ||
| return sanitizeUser(users[0]); | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| }; |
| import { | ||
| hashPassword, | ||
| comparePassword, | ||
| generateToken, | ||
| generateRefreshToken, | ||
| verifyRefreshToken, | ||
| generatePasswordResetToken, | ||
| sanitizeUser, | ||
| } from "../utils/auth.utils.js"; | ||
| import { ApiError, asyncFunctionHandler } from "../middlewares/errorHandler.js"; | ||
| import pool from "../configs/postgresql.config.js"; | ||
| /** | ||
| * Auth service with asyncHandler pattern | ||
| * All methods are wrapped with asyncFunctionHandler to avoid try-catch blocks | ||
| * Pool is imported directly from config | ||
| */ | ||
| export const authService = { | ||
| // Initialize database tables | ||
| initializeTables: asyncFunctionHandler(async () => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| await client.query(` | ||
| CREATE TABLE IF NOT EXISTS users ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| email VARCHAR(255) UNIQUE NOT NULL, | ||
| password VARCHAR(255) NOT NULL, | ||
| first_name VARCHAR(100), | ||
| last_name VARCHAR(100), | ||
| is_active BOOLEAN DEFAULT true, | ||
| password_reset_token VARCHAR(255), | ||
| password_reset_expires TIMESTAMP, | ||
| refresh_tokens TEXT[] DEFAULT '{}', | ||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); | ||
| CREATE INDEX IF NOT EXISTS idx_users_password_reset_token ON users(password_reset_token); | ||
| `); | ||
| console.log("✅ Users table initialized"); | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Register a new user | ||
| register: asyncFunctionHandler(async (userData) => { | ||
| const { email, password, firstName, lastName } = userData; | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Check if user already exists | ||
| const existingUser = await client.query( | ||
| "SELECT id FROM users WHERE email = $1", | ||
| [email] | ||
| ); | ||
| if (existingUser.rows.length > 0) { | ||
| throw ApiError( | ||
| "User with this email already exists", | ||
| 409, | ||
| {}, | ||
| "userAlreadyExists" | ||
| ); | ||
| } | ||
| // Hash password | ||
| const hashedPassword = await hashPassword(password); | ||
| // Create user | ||
| const result = await client.query( | ||
| ` | ||
| INSERT INTO users (email, password, first_name, last_name) | ||
| VALUES ($1, $2, $3, $4) | ||
| RETURNING id, email, first_name, last_name, is_active, created_at, updated_at | ||
| `, | ||
| [email, hashedPassword, firstName, lastName] | ||
| ); | ||
| const user = result.rows[0]; | ||
| // Generate tokens | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.is_active }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // Save refresh token | ||
| await client.query( | ||
| "UPDATE users SET refresh_tokens = array_append(refresh_tokens, $1) WHERE id = $2", | ||
| [refreshToken, user.id] | ||
| ); | ||
| return { | ||
| user: sanitizeUser(user), | ||
| token, | ||
| refreshToken, | ||
| }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Login user | ||
| login: asyncFunctionHandler(async (credentials) => { | ||
| const { email, password } = credentials; | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Find user | ||
| const result = await client.query( | ||
| "SELECT * FROM users WHERE email = $1", | ||
| [email] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError( | ||
| "Invalid email or password", | ||
| 401, | ||
| {}, | ||
| "invalidCredentials" | ||
| ); | ||
| } | ||
| const user = result.rows[0]; | ||
| // Check if user is active | ||
| if (!user.is_active) { | ||
| throw ApiError("Account is deactivated", 401, {}, "accountDeactivated"); | ||
| } | ||
| // Verify password | ||
| const isPasswordValid = await comparePassword(password, user.password); | ||
| if (!isPasswordValid) { | ||
| throw ApiError( | ||
| "Invalid email or password", | ||
| 401, | ||
| {}, | ||
| "invalidCredentials" | ||
| ); | ||
| } | ||
| // Generate tokens | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.is_active }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // Save refresh token | ||
| await client.query( | ||
| "UPDATE users SET refresh_tokens = array_append(refresh_tokens, $1) WHERE id = $2", | ||
| [refreshToken, user.id] | ||
| ); | ||
| return { | ||
| user: sanitizeUser(user), | ||
| token, | ||
| refreshToken, | ||
| }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Logout user | ||
| logout: asyncFunctionHandler(async (id) => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Clear refresh tokens | ||
| await client.query( | ||
| "UPDATE users SET refresh_tokens = '{}' WHERE id = $1", | ||
| [id] | ||
| ); | ||
| return { message: "Logged out successfully" }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Refresh token | ||
| refreshToken: asyncFunctionHandler(async (refreshToken) => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // 1. Verify refresh token | ||
| const decoded = verifyRefreshToken(refreshToken); | ||
| if (!decoded) { | ||
| throw ApiError("Invalid refresh token", 401, {}, "invalidRefreshToken"); | ||
| } | ||
| // 2. Find the user | ||
| const result = await client.query( | ||
| "SELECT * FROM users WHERE id = $1 AND is_active = true", | ||
| [decoded.id] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError("User not found or inactive", 401, {}, "userNotFound"); | ||
| } | ||
| const user = result.rows[0]; | ||
| // 3. Generate new tokens | ||
| const newToken = generateToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| const newRefreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // 4. Replace refresh_tokens with ONLY the new token | ||
| await client.query( | ||
| "UPDATE users SET refresh_tokens = ARRAY[$1] WHERE id = $2", | ||
| [newRefreshToken, user.id] | ||
| ); | ||
| // 5. Fetch updated user to include correct refresh_tokens | ||
| const updatedResult = await client.query( | ||
| "SELECT * FROM users WHERE id = $1", | ||
| [user.id] | ||
| ); | ||
| const updatedUser = updatedResult.rows[0]; | ||
| // 6. Return new token pair | ||
| return { | ||
| user: sanitizeUser(updatedUser), | ||
| token: newToken, | ||
| refreshToken: newRefreshToken, | ||
| }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Forgot password | ||
| forgotPassword: asyncFunctionHandler(async (email) => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Check if user exists | ||
| const result = await client.query( | ||
| "SELECT id, email FROM users WHERE email = $1", | ||
| [email] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError( | ||
| "User with this email does not exist", | ||
| 404, | ||
| {}, | ||
| "userNotFound" | ||
| ); | ||
| } | ||
| const user = result.rows[0]; | ||
| // Generate password reset token | ||
| const resetToken = generatePasswordResetToken(user.id); | ||
| const resetExpires = new Date(Date.now() + 3600000); // 1 hour | ||
| // Save reset token | ||
| await client.query( | ||
| "UPDATE users SET password_reset_token = $1, password_reset_expires = $2 WHERE id = $3", | ||
| [resetToken, resetExpires, user.id] | ||
| ); | ||
| return { | ||
| message: "Password reset email sent", | ||
| resetToken, // In production, this would be sent via email | ||
| }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Reset password | ||
| resetPassword: asyncFunctionHandler(async (token, newPassword) => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Find user with valid reset token | ||
| const result = await client.query( | ||
| "SELECT id FROM users WHERE password_reset_token = $1 AND password_reset_expires > NOW()", | ||
| [token] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError( | ||
| "Invalid or expired reset token", | ||
| 400, | ||
| {}, | ||
| "invalidResetToken" | ||
| ); | ||
| } | ||
| const user = result.rows[0]; | ||
| // Hash new password | ||
| const hashedPassword = await hashPassword(newPassword); | ||
| // Update password and clear reset token | ||
| await client.query( | ||
| "UPDATE users SET password = $1, password_reset_token = NULL, password_reset_expires = NULL WHERE id = $2", | ||
| [hashedPassword, user.id] | ||
| ); | ||
| return { message: "Password reset successfully" }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Change password | ||
| changePassword: asyncFunctionHandler( | ||
| async (id, oldPassword, newPassword) => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Get user's current password | ||
| const result = await client.query( | ||
| "SELECT password FROM users WHERE id = $1", | ||
| [id] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| } | ||
| const user = result.rows[0]; | ||
| // Verify old password | ||
| const isOldPasswordValid = await comparePassword( | ||
| oldPassword, | ||
| user.password | ||
| ); | ||
| if (!isOldPasswordValid) { | ||
| throw ApiError("Invalid old password", 400, {}, "invalidOldPassword"); | ||
| } | ||
| // Hash new password | ||
| const hashedPassword = await hashPassword(newPassword); | ||
| // Update password | ||
| await client.query("UPDATE users SET password = $1 WHERE id = $2", [ | ||
| hashedPassword, | ||
| id, | ||
| ]); | ||
| return { message: "Password changed successfully" }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| } | ||
| ), | ||
| // Verify token | ||
| verifyToken: asyncFunctionHandler(async (token) => { | ||
| // This would typically verify the JWT token | ||
| // For now, we'll just return a success response | ||
| return { valid: true }; | ||
| }), | ||
| // Get user by ID | ||
| getUserById: asyncFunctionHandler(async (id) => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| const result = await client.query( | ||
| "SELECT id, email, first_name, last_name, is_active, created_at, updated_at FROM users WHERE id = $1", | ||
| [id] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| } | ||
| return sanitizeUser(result.rows[0]); | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Update user by ID | ||
| updateUserById: asyncFunctionHandler(async (id, firstName, lastName) => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| const result = await client.query( | ||
| "UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3 RETURNING *", | ||
| [firstName, lastName, id] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| return null; | ||
| } | ||
| return sanitizeUser(result.rows[0]); | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| }; |
| import { setAuthService } from "../controllers/auth.controller.js"; | ||
| // MySQL | ||
| import mysqlPool from "./mysql.config.js"; | ||
| import { authService as mysqlAuthService } from "../services/auth.mysql.service.js"; | ||
| export const initializeMySQLDatabase = async (): Promise<void> => { | ||
| try { | ||
| const mysqlConnection = await mysqlPool.getConnection(); | ||
| console.log("MySQL connection has been established"); | ||
| mysqlConnection.release(); | ||
| await mysqlAuthService.initializeTables(); | ||
| setAuthService(mysqlAuthService); | ||
| console.log("✅ Authentication service initialized with MySQL"); | ||
| } catch (err) { | ||
| console.error("Error initializing MySQL:", err.message); | ||
| throw err; | ||
| } | ||
| }; | ||
| // PostgreSQL | ||
| import postgresqlPool from "./postgresql.config.js"; | ||
| import { authService as postgresqlAuthService } from "../services/auth.postgresql.service.js"; | ||
| export const initializePostgreSQLDatabase = async (): Promise<void> => { | ||
| try { | ||
| const postgresqlClient = await postgresqlPool.connect(); | ||
| console.log("PostgreSQL connection has been established"); | ||
| postgresqlClient.release(); | ||
| await postgresqlAuthService.initializeTables(); | ||
| setAuthService(postgresqlAuthService); | ||
| console.log("✅ Authentication service initialized with PostgreSQL"); | ||
| } catch (err) { | ||
| console.error("Error initializing PostgreSQL:", err.message); | ||
| throw err; | ||
| } | ||
| }; | ||
| // MongoDB | ||
| import mongoose from "./mongodb.config.js"; | ||
| import { authService as mongodbAuthService } from "../services/auth.mongodb.service.js"; | ||
| export const initializeMongoDBDatabase = async (): Promise<void> => { | ||
| try { | ||
| // Wait for mongoose connection to be ready | ||
| if (mongoose.connection.readyState === 0) { | ||
| // If not connected, wait for connection | ||
| await new Promise<void>((resolve, reject) => { | ||
| mongoose.connection.once('connected', () => resolve()); | ||
| mongoose.connection.once('error', (err) => reject(err)); | ||
| // Timeout after 10 seconds | ||
| setTimeout(() => reject(new Error('MongoDB connection timeout')), 10000); | ||
| }); | ||
| } | ||
| setAuthService(mongodbAuthService); | ||
| console.log("✅ Authentication service initialized with MongoDB"); | ||
| } catch (err) { | ||
| console.error("Error initializing MongoDB:", err.message); | ||
| throw err; | ||
| } | ||
| }; |
| import bcrypt from "bcryptjs"; | ||
| import jwt from "jsonwebtoken"; | ||
| import { | ||
| generateToken, | ||
| generateRefreshToken, | ||
| verifyRefreshToken, | ||
| generatePasswordResetToken, | ||
| hashPassword, | ||
| comparePassword, | ||
| sanitizeUser, | ||
| } from "../utils/auth.utils.js"; | ||
| import { ApiError, asyncFunctionHandler } from "../middlewares/errorHandler.js"; | ||
| import pool from "../configs/mysql.config.js"; | ||
| // Types for the auth service | ||
| interface IUser { | ||
| id: string; | ||
| email: string; | ||
| password: string; | ||
| first_name?: string; | ||
| last_name?: string; | ||
| is_active: boolean; | ||
| password_reset_token?: string; | ||
| password_reset_expires?: Date; | ||
| refresh_tokens?: string; | ||
| created_at: Date; | ||
| updated_at: Date; | ||
| } | ||
| interface RegisterRequest { | ||
| email: string; | ||
| password: string; | ||
| firstName?: string; | ||
| lastName?: string; | ||
| } | ||
| interface LoginRequest { | ||
| email: string; | ||
| password: string; | ||
| } | ||
| interface AuthResponse { | ||
| user: Omit<IUser, 'password'>; | ||
| token: string; | ||
| refreshToken: string; | ||
| } | ||
| interface JWTPayload { | ||
| id: string; | ||
| email: string; | ||
| } | ||
| /** | ||
| * Auth service with asyncHandler pattern | ||
| * All methods are wrapped with asyncFunctionHandler to avoid try-catch blocks | ||
| * Pool is imported directly from config | ||
| */ | ||
| export const authService = { | ||
| // Initialize database tables | ||
| initializeTables: asyncFunctionHandler(async () => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| await connection.execute(` | ||
| CREATE TABLE IF NOT EXISTS users ( | ||
| id INT AUTO_INCREMENT PRIMARY KEY, | ||
| email VARCHAR(255) UNIQUE NOT NULL, | ||
| password VARCHAR(255) NOT NULL, | ||
| first_name VARCHAR(100), | ||
| last_name VARCHAR(100), | ||
| is_active BOOLEAN DEFAULT true, | ||
| refresh_tokens JSON, | ||
| password_reset_token VARCHAR(255), | ||
| password_reset_expires DATETIME, | ||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||
| ) | ||
| `); | ||
| console.log("✅ Users table initialized"); | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Register a new user | ||
| register: asyncFunctionHandler(async (userData: RegisterRequest): Promise<AuthResponse> => { | ||
| const { email, password, firstName, lastName } = userData; | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Check if user already exists | ||
| const [existingUsers] = await connection.execute( | ||
| "SELECT id FROM users WHERE email = ?", | ||
| [email] | ||
| ) as [any[], any]; | ||
| if (existingUsers.length > 0) { | ||
| throw ApiError( | ||
| "User with this email already exists", | ||
| 409, | ||
| {}, | ||
| "userAlreadyExists" | ||
| ); | ||
| } | ||
| // Hash password | ||
| const hashedPassword = await hashPassword(password); | ||
| // Create user | ||
| const [result] = await connection.execute( | ||
| ` | ||
| INSERT INTO users (email, password, first_name, last_name) | ||
| VALUES (?, ?, ?, ?) | ||
| `, | ||
| [email, hashedPassword, firstName, lastName] | ||
| ) as [any, any]; | ||
| const id = result.insertId; | ||
| // Get the created user | ||
| const [users] = await connection.execute( | ||
| "SELECT id, email, first_name, last_name, is_active, created_at, updated_at FROM users WHERE id = ?", | ||
| [id] | ||
| ) as [any[], any]; | ||
| const user = users[0]; | ||
| // Generate tokens | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.is_active }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // Save refresh token | ||
| await connection.execute( | ||
| "UPDATE users SET refresh_tokens = JSON_ARRAY(?) WHERE id = ?", | ||
| [refreshToken, user.id] | ||
| ); | ||
| return { | ||
| user: sanitizeUser(user), | ||
| token, | ||
| refreshToken, | ||
| }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Login user | ||
| login: asyncFunctionHandler(async (credentials: LoginRequest): Promise<AuthResponse> => { | ||
| const { email, password } = credentials; | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Find user by email | ||
| const [users] = await connection.execute( | ||
| "SELECT * FROM users WHERE email = ?", | ||
| [email] | ||
| ) as [any[], any]; | ||
| if (users.length === 0) { | ||
| throw ApiError( | ||
| "Invalid email or password", | ||
| 401, | ||
| {}, | ||
| "invalidCredentials" | ||
| ); | ||
| } | ||
| const user = users[0]; | ||
| // Check if user is active | ||
| if (!user.is_active) { | ||
| throw ApiError("Account is deactivated", 401, {}, "accountDeactivated"); | ||
| } | ||
| // Verify password | ||
| const isPasswordValid = await comparePassword(password, user.password); | ||
| if (!isPasswordValid) { | ||
| throw ApiError( | ||
| "Invalid email or password", | ||
| 401, | ||
| {}, | ||
| "invalidCredentials" | ||
| ); | ||
| } | ||
| // Generate tokens | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.is_active }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // Save refresh token | ||
| await connection.execute( | ||
| "UPDATE users SET refresh_tokens = JSON_ARRAY(?) WHERE id = ?", | ||
| [refreshToken, user.id] | ||
| ); | ||
| return { | ||
| user: sanitizeUser(user), | ||
| token, | ||
| refreshToken, | ||
| }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Logout user | ||
| logout: asyncFunctionHandler(async (id: string): Promise<{ message: string }> => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Clear refresh tokens | ||
| await connection.execute( | ||
| "UPDATE users SET refresh_tokens = JSON_ARRAY() WHERE id = ?", | ||
| [id] | ||
| ); | ||
| return { message: "Logged out successfully" }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Refresh token | ||
| refreshToken: asyncFunctionHandler(async (refreshToken: string): Promise<AuthResponse> => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // 1. Verify refresh token | ||
| const decoded = verifyRefreshToken(refreshToken); | ||
| if (!decoded) { | ||
| throw ApiError("Invalid refresh token", 401, {}, "invalidRefreshToken"); | ||
| } | ||
| // 2. Find the user | ||
| const [users] = await connection.execute( | ||
| "SELECT * FROM users WHERE id = ? AND is_active = true", | ||
| [decoded.id] | ||
| ) as [any[], any]; | ||
| if (users.length === 0) { | ||
| throw ApiError("User not found or inactive", 401, {}, "userNotFound"); | ||
| } | ||
| const user = users[0]; | ||
| // 3. Generate new tokens | ||
| const newToken = generateToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| const newRefreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // 4. Replace refresh_tokens with ONLY the new token | ||
| await connection.execute( | ||
| "UPDATE users SET refresh_tokens = JSON_ARRAY(?) WHERE id = ?", | ||
| [newRefreshToken, user.id] | ||
| ); | ||
| // 5. Fetch updated user to include correct refresh_tokens | ||
| const [updatedUsers] = await connection.execute( | ||
| "SELECT * FROM users WHERE id = ?", | ||
| [user.id] | ||
| ) as [any[], any]; | ||
| const updatedUser = updatedUsers[0]; | ||
| // 6. Return new token pair | ||
| return { | ||
| user: sanitizeUser(updatedUser), | ||
| token: newToken, | ||
| refreshToken: newRefreshToken, | ||
| }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Forgot password | ||
| forgotPassword: asyncFunctionHandler(async (email: string): Promise<{ message: string; resetToken: string }> => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Check if user exists | ||
| const [users] = await connection.execute( | ||
| "SELECT id, email FROM users WHERE email = ?", | ||
| [email] | ||
| ) as [any[], any]; | ||
| if (users.length === 0) { | ||
| throw ApiError( | ||
| "User with this email does not exist", | ||
| 404, | ||
| {}, | ||
| "userNotFound" | ||
| ); | ||
| } | ||
| const user = users[0]; | ||
| // Generate password reset token | ||
| const resetToken = generatePasswordResetToken(user.id); | ||
| const resetExpires = new Date(Date.now() + 3600000); // 1 hour | ||
| // Save reset token | ||
| await connection.execute( | ||
| "UPDATE users SET password_reset_token = ?, password_reset_expires = ? WHERE id = ?", | ||
| [resetToken, resetExpires, user.id] | ||
| ); | ||
| return { | ||
| message: "Password reset email sent", | ||
| resetToken, // In production, this would be sent via email | ||
| }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Reset password | ||
| resetPassword: asyncFunctionHandler(async (token: string, newPassword: string): Promise<{ message: string }> => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Find user with valid reset token | ||
| const [users] = await connection.execute( | ||
| "SELECT id FROM users WHERE password_reset_token = ? AND password_reset_expires > NOW()", | ||
| [token] | ||
| ) as [any[], any]; | ||
| if (users.length === 0) { | ||
| throw ApiError( | ||
| "Invalid or expired reset token", | ||
| 400, | ||
| {}, | ||
| "invalidResetToken" | ||
| ); | ||
| } | ||
| const user = users[0]; | ||
| // Hash new password | ||
| const hashedPassword = await hashPassword(newPassword); | ||
| // Update password and clear reset token | ||
| await connection.execute( | ||
| "UPDATE users SET password = ?, password_reset_token = NULL, password_reset_expires = NULL WHERE id = ?", | ||
| [hashedPassword, user.id] | ||
| ); | ||
| return { message: "Password reset successfully" }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Change password | ||
| changePassword: asyncFunctionHandler( | ||
| async (id: string, oldPassword: string, newPassword: string): Promise<{ message: string }> => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| // Get user's current password | ||
| const [users] = await connection.execute( | ||
| "SELECT password FROM users WHERE id = ?", | ||
| [id] | ||
| ) as [any[], any]; | ||
| if (users.length === 0) { | ||
| throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| } | ||
| const user = users[0]; | ||
| // Verify old password | ||
| const isOldPasswordValid = await comparePassword( | ||
| oldPassword, | ||
| user.password | ||
| ); | ||
| if (!isOldPasswordValid) { | ||
| throw ApiError("Invalid old password", 400, {}, "invalidOldPassword"); | ||
| } | ||
| // Hash new password | ||
| const hashedPassword = await hashPassword(newPassword); | ||
| // Update password | ||
| await connection.execute("UPDATE users SET password = ? WHERE id = ?", [ | ||
| hashedPassword, | ||
| id, | ||
| ]); | ||
| return { message: "Password changed successfully" }; | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| } | ||
| ), | ||
| // Verify token | ||
| verifyToken: asyncFunctionHandler(async (token: string): Promise<JWTPayload> => { | ||
| // This would typically verify the JWT token | ||
| // For now, we'll just return a success response | ||
| return { id: "", email: "" }; | ||
| }), | ||
| // Get user by ID | ||
| getUserById: asyncFunctionHandler(async (id: string): Promise<Omit<IUser, 'password'> | null> => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| const [users] = await connection.execute( | ||
| "SELECT id, email, first_name, last_name, is_active, created_at, updated_at FROM users WHERE id = ?", | ||
| [id] | ||
| ) as [any[], any]; | ||
| if (users.length === 0) { | ||
| throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| } | ||
| return sanitizeUser(users[0]); | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| // Update user by ID | ||
| updateUserById: asyncFunctionHandler(async (id: string, firstName: string, lastName: string): Promise<Omit<IUser, 'password'> | null> => { | ||
| const connection = await pool.getConnection(); | ||
| try { | ||
| const [result] = await connection.execute( | ||
| "UPDATE users SET first_name = ?, last_name = ? WHERE id = ?", | ||
| [firstName, lastName, id] | ||
| ) as [any, any]; | ||
| if (result.affectedRows === 0) { | ||
| return null; | ||
| } | ||
| // Fetch updated user | ||
| const [users] = await connection.execute( | ||
| "SELECT * FROM users WHERE id = ?", | ||
| [id] | ||
| ) as [any[], any]; | ||
| if (users.length === 0) { | ||
| return null; | ||
| } | ||
| return sanitizeUser(users[0]); | ||
| } finally { | ||
| connection.release(); | ||
| } | ||
| }), | ||
| }; |
| import { Pool, PoolClient } from "pg"; | ||
| import { | ||
| hashPassword, | ||
| comparePassword, | ||
| generateToken, | ||
| generateRefreshToken, | ||
| verifyRefreshToken, | ||
| generatePasswordResetToken, | ||
| sanitizeUser, | ||
| } from "../utils/auth.utils.js"; | ||
| import { ApiError, asyncFunctionHandler } from "../middlewares/errorHandler.js"; | ||
| import pool from "../configs/postgresql.config.js"; | ||
| // Types for the auth service | ||
| interface IUser { | ||
| id: string; | ||
| email: string; | ||
| password: string; | ||
| first_name?: string; | ||
| last_name?: string; | ||
| is_active: boolean; | ||
| password_reset_token?: string; | ||
| password_reset_expires?: Date; | ||
| refresh_tokens?: string[]; | ||
| created_at: Date; | ||
| updated_at: Date; | ||
| } | ||
| interface RegisterRequest { | ||
| email: string; | ||
| password: string; | ||
| firstName?: string; | ||
| lastName?: string; | ||
| } | ||
| interface LoginRequest { | ||
| email: string; | ||
| password: string; | ||
| } | ||
| interface AuthResponse { | ||
| user: Omit<IUser, 'password'>; | ||
| token: string; | ||
| refreshToken: string; | ||
| } | ||
| interface JWTPayload { | ||
| id: string; | ||
| email: string; | ||
| } | ||
| /** | ||
| * Auth service with asyncHandler pattern | ||
| * All methods are wrapped with asyncFunctionHandler to avoid try-catch blocks | ||
| * Pool is imported directly from config | ||
| */ | ||
| export const authService = { | ||
| // Initialize database tables | ||
| initializeTables: asyncFunctionHandler(async () => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| await client.query(` | ||
| CREATE TABLE IF NOT EXISTS users ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| email VARCHAR(255) UNIQUE NOT NULL, | ||
| password VARCHAR(255) NOT NULL, | ||
| first_name VARCHAR(100), | ||
| last_name VARCHAR(100), | ||
| is_active BOOLEAN DEFAULT true, | ||
| password_reset_token VARCHAR(255), | ||
| password_reset_expires TIMESTAMP, | ||
| refresh_tokens TEXT[] DEFAULT '{}', | ||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); | ||
| CREATE INDEX IF NOT EXISTS idx_users_password_reset_token ON users(password_reset_token); | ||
| `); | ||
| console.log("✅ Users table initialized"); | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Register a new user | ||
| register: asyncFunctionHandler(async (userData: RegisterRequest): Promise<AuthResponse> => { | ||
| const { email, password, firstName, lastName } = userData; | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Check if user already exists | ||
| const existingUser = await client.query( | ||
| "SELECT id FROM users WHERE email = $1", | ||
| [email] | ||
| ); | ||
| if (existingUser.rows.length > 0) { | ||
| throw ApiError( | ||
| "User with this email already exists", | ||
| 409, | ||
| {}, | ||
| "userAlreadyExists" | ||
| ); | ||
| } | ||
| // Hash password | ||
| const hashedPassword = await hashPassword(password); | ||
| // Create user | ||
| const result = await client.query( | ||
| ` | ||
| INSERT INTO users (email, password, first_name, last_name) | ||
| VALUES ($1, $2, $3, $4) | ||
| RETURNING id, email, first_name, last_name, is_active, created_at, updated_at | ||
| `, | ||
| [email, hashedPassword, firstName, lastName] | ||
| ); | ||
| const user = result.rows[0]; | ||
| // Generate tokens | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.is_active }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // Save refresh token | ||
| await client.query( | ||
| "UPDATE users SET refresh_tokens = array_append(refresh_tokens, $1) WHERE id = $2", | ||
| [refreshToken, user.id] | ||
| ); | ||
| return { | ||
| user: sanitizeUser(user), | ||
| token, | ||
| refreshToken, | ||
| }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Login user | ||
| login: asyncFunctionHandler(async (credentials: LoginRequest): Promise<AuthResponse> => { | ||
| const { email, password } = credentials; | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Find user | ||
| const result = await client.query( | ||
| "SELECT * FROM users WHERE email = $1", | ||
| [email] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError( | ||
| "Invalid email or password", | ||
| 401, | ||
| {}, | ||
| "invalidCredentials" | ||
| ); | ||
| } | ||
| const user = result.rows[0]; | ||
| // Check if user is active | ||
| if (!user.is_active) { | ||
| throw ApiError("Account is deactivated", 401, {}, "accountDeactivated"); | ||
| } | ||
| // Verify password | ||
| const isPasswordValid = await comparePassword(password, user.password); | ||
| if (!isPasswordValid) { | ||
| throw ApiError( | ||
| "Invalid email or password", | ||
| 401, | ||
| {}, | ||
| "invalidCredentials" | ||
| ); | ||
| } | ||
| // Generate tokens | ||
| const token = generateToken({ id: user.id, email: user.email, is_active: user.is_active }); | ||
| const refreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // Save refresh token | ||
| await client.query( | ||
| "UPDATE users SET refresh_tokens = array_append(refresh_tokens, $1) WHERE id = $2", | ||
| [refreshToken, user.id] | ||
| ); | ||
| return { | ||
| user: sanitizeUser(user), | ||
| token, | ||
| refreshToken, | ||
| }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Logout user | ||
| logout: asyncFunctionHandler(async (id: string): Promise<{ message: string }> => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Clear refresh tokens | ||
| await client.query( | ||
| "UPDATE users SET refresh_tokens = '{}' WHERE id = $1", | ||
| [id] | ||
| ); | ||
| return { message: "Logged out successfully" }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Refresh token | ||
| refreshToken: asyncFunctionHandler(async (refreshToken: string): Promise<AuthResponse> => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // 1. Verify refresh token | ||
| const decoded = verifyRefreshToken(refreshToken); | ||
| if (!decoded) { | ||
| throw ApiError("Invalid refresh token", 401, {}, "invalidRefreshToken"); | ||
| } | ||
| // 2. Find the user | ||
| const result = await client.query( | ||
| "SELECT * FROM users WHERE id = $1 AND is_active = true", | ||
| [decoded.id] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError("User not found or inactive", 401, {}, "userNotFound"); | ||
| } | ||
| const user = result.rows[0]; | ||
| // 3. Generate new tokens | ||
| const newToken = generateToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| const newRefreshToken = generateRefreshToken({ | ||
| id: user.id, | ||
| email: user.email, | ||
| is_active: user.is_active, | ||
| }); | ||
| // 4. Replace refresh_tokens with ONLY the new token | ||
| await client.query( | ||
| "UPDATE users SET refresh_tokens = ARRAY[$1] WHERE id = $2", | ||
| [newRefreshToken, user.id] | ||
| ); | ||
| // 5. Fetch updated user to include correct refresh_tokens | ||
| const updatedResult = await client.query( | ||
| "SELECT * FROM users WHERE id = $1", | ||
| [user.id] | ||
| ); | ||
| const updatedUser = updatedResult.rows[0]; | ||
| // 6. Return new token pair | ||
| return { | ||
| user: sanitizeUser(updatedUser), | ||
| token: newToken, | ||
| refreshToken: newRefreshToken, | ||
| }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Forgot password | ||
| forgotPassword: asyncFunctionHandler(async (email: string): Promise<{ message: string; resetToken: string }> => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Check if user exists | ||
| const result = await client.query( | ||
| "SELECT id, email FROM users WHERE email = $1", | ||
| [email] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError( | ||
| "User with this email does not exist", | ||
| 404, | ||
| {}, | ||
| "userNotFound" | ||
| ); | ||
| } | ||
| const user = result.rows[0]; | ||
| // Generate password reset token | ||
| const resetToken = generatePasswordResetToken(user.id); | ||
| const resetExpires = new Date(Date.now() + 3600000); // 1 hour | ||
| // Save reset token | ||
| await client.query( | ||
| "UPDATE users SET password_reset_token = $1, password_reset_expires = $2 WHERE id = $3", | ||
| [resetToken, resetExpires, user.id] | ||
| ); | ||
| return { | ||
| message: "Password reset email sent", | ||
| resetToken, // In production, this would be sent via email | ||
| }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Reset password | ||
| resetPassword: asyncFunctionHandler(async (token: string, newPassword: string): Promise<{ message: string }> => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Find user with valid reset token | ||
| const result = await client.query( | ||
| "SELECT id FROM users WHERE password_reset_token = $1 AND password_reset_expires > NOW()", | ||
| [token] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError( | ||
| "Invalid or expired reset token", | ||
| 400, | ||
| {}, | ||
| "invalidResetToken" | ||
| ); | ||
| } | ||
| const user = result.rows[0]; | ||
| // Hash new password | ||
| const hashedPassword = await hashPassword(newPassword); | ||
| // Update password and clear reset token | ||
| await client.query( | ||
| "UPDATE users SET password = $1, password_reset_token = NULL, password_reset_expires = NULL WHERE id = $2", | ||
| [hashedPassword, user.id] | ||
| ); | ||
| return { message: "Password reset successfully" }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Change password | ||
| changePassword: asyncFunctionHandler( | ||
| async (id: string, oldPassword: string, newPassword: string): Promise<{ message: string }> => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| // Get user's current password | ||
| const result = await client.query( | ||
| "SELECT password FROM users WHERE id = $1", | ||
| [id] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| } | ||
| const user = result.rows[0]; | ||
| // Verify old password | ||
| const isOldPasswordValid = await comparePassword( | ||
| oldPassword, | ||
| user.password | ||
| ); | ||
| if (!isOldPasswordValid) { | ||
| throw ApiError("Invalid old password", 400, {}, "invalidOldPassword"); | ||
| } | ||
| // Hash new password | ||
| const hashedPassword = await hashPassword(newPassword); | ||
| // Update password | ||
| await client.query( | ||
| "UPDATE users SET password = $1 WHERE id = $2", | ||
| [hashedPassword, id] | ||
| ); | ||
| return { message: "Password changed successfully" }; | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| } | ||
| ), | ||
| // Verify token | ||
| verifyToken: asyncFunctionHandler(async (token: string): Promise<JWTPayload> => { | ||
| // This would typically verify the JWT token | ||
| // For now, we'll just return a success response | ||
| return { id: "", email: "" }; | ||
| }), | ||
| // Get user by ID | ||
| getUserById: asyncFunctionHandler(async (id: string): Promise<Omit<IUser, 'password'> | null> => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| const result = await client.query( | ||
| "SELECT id, email, first_name, last_name, is_active, created_at, updated_at FROM users WHERE id = $1", | ||
| [id] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| throw ApiError("User not found", 404, {}, "userNotFound"); | ||
| } | ||
| return sanitizeUser(result.rows[0]); | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| // Update user by ID | ||
| updateUserById: asyncFunctionHandler(async (id: string, firstName: string, lastName: string): Promise<Omit<IUser, 'password'> | null> => { | ||
| const client = await pool.connect(); | ||
| try { | ||
| const result = await client.query( | ||
| "UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3 RETURNING *", | ||
| [firstName, lastName, id] | ||
| ); | ||
| if (result.rows.length === 0) { | ||
| return null; | ||
| } | ||
| return sanitizeUser(result.rows[0]); | ||
| } finally { | ||
| client.release(); | ||
| } | ||
| }), | ||
| }; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
88
10%483106
-4.05%12598
-8.61%Updated