Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@trisers/cli-node-template

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@trisers/cli-node-template - npm Package Compare versions

Comparing version
1.1.2
to
1.2.0
+9
templates/js/base/configs/prisma.js
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) {

@@ -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}`));

{
"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