Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
155c8f3
feat: add custom error classes for API error handling
Jul 10, 2026
d2aa521
feat: add pagination utility for Prisma queries
Jul 10, 2026
00fe7ba
feat: add Zod validation schemas for authentication
Jul 10, 2026
bc8522b
feat: add Zod validation schemas for user profile
Jul 10, 2026
53aeb70
feat: add Zod validation schemas for invoices
Jul 10, 2026
2f7a0d6
feat: add Zod validation schemas for escrow lifecycle
Jul 10, 2026
ecd9c35
feat: add JWT authentication and role authorization middleware
Jul 10, 2026
dd5299c
feat: add Zod validation middleware for request validation
Jul 10, 2026
d1cd610
feat: add global error handler middleware
Jul 10, 2026
08b5502
feat: add Stellar blockchain service for escrow operations
Jul 10, 2026
cc49f20
feat: implement auth controller with register, login, logout, refresh
Jul 10, 2026
dcff726
feat: implement user profile CRUD with role-based access
Jul 10, 2026
a80aade
feat: implement invoice CRUD with status transitions and pagination
Jul 10, 2026
25bbd29
feat: implement escrow lifecycle with Stellar integration
Jul 10, 2026
4700d6b
feat: add auth routes for register, login, logout, refresh, getMe
Jul 10, 2026
16eb0cb
feat: add user routes for profile CRUD and avatar upload
Jul 10, 2026
f64bd55
feat: add invoice routes for CRUD with JWT authentication
Jul 10, 2026
85ca9a5
feat: add escrow routes for lifecycle management with JWT auth
Jul 10, 2026
963337c
feat: add JWT refresh token configuration
Jul 10, 2026
7810976
feat: wire auth, user, invoice, and escrow routes to Express app
Jul 10, 2026
e3b9ff7
chore: update package-lock.json after dependency installation
Jul 10, 2026
4a3d02a
Merge branch 'main' into feat/resolve-issues-15-18
TheCodingChef-eth Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,068 changes: 822 additions & 246 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ export const config = {
nodeEnv: process.env["NODE_ENV"] ?? "development",
jwt: {
secret: process.env["JWT_SECRET"] ?? "",
expiresIn: process.env["JWT_EXPIRES_IN"] ?? "7d",
expiresIn: 900,
refreshSecret: process.env["JWT_REFRESH_SECRET"] ?? "",
refreshExpiresIn: 604800,
},
stellar: {
network: process.env["STELLAR_NETWORK"] ?? "testnet",
Expand Down
200 changes: 200 additions & 0 deletions src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import type { Request, Response } from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { prisma } from "../config/prisma.js";
import { config } from "../config/index.js";
import { ConflictError, UnauthorizedError, NotFoundError } from "../utils/errors.js";
import type { AuthRequest } from "../middleware/auth.js";

function generateTokens(payload: { userId: string; email: string; role: string }) {
const accessToken = jwt.sign(payload, config.jwt.secret, {
expiresIn: config.jwt.expiresIn,
});
const refreshToken = jwt.sign(payload, config.jwt.refreshSecret, {
expiresIn: config.jwt.refreshExpiresIn,
});
return { accessToken, refreshToken };
}

function setRefreshTokenCookie(res: Response, refreshToken: string): void {
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: config.nodeEnv === "production",
sameSite: "strict",
path: "/",
maxAge: 7 * 24 * 60 * 60 * 1000,
});
}

export async function register(req: Request, res: Response): Promise<void> {
const { fullname, email, password, role, walletAddress, country } = req.body;

const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
throw new ConflictError("Email already registered");
}

if (walletAddress) {
const existingWallet = await prisma.user.findUnique({ where: { walletAddress } });
if (existingWallet) {
throw new ConflictError("Wallet address already registered");
}
}

const hashedPassword = await bcrypt.hash(password, 12);

const user = await prisma.user.create({
data: {
fullname,
email,
password: hashedPassword,
role,
...(walletAddress != null && { walletAddress }),
...(country != null && { country }),
},
select: {
id: true,
fullname: true,
email: true,
role: true,
walletAddress: true,
country: true,
isVerified: true,
createdAt: true,
},
});

const tokens = generateTokens({
userId: user.id,
email: user.email,
role: user.role,
});

setRefreshTokenCookie(res, tokens.refreshToken);

res.status(201).json({
success: true,
data: {
user,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
},
});
}

export async function login(req: Request, res: Response): Promise<void> {
const { email, password } = req.body;

const user = await prisma.user.findUnique({ where: { email } });
if (!user || !user.password) {
throw new UnauthorizedError("Invalid email or password");
}

const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedError("Invalid email or password");
}

const tokens = generateTokens({
userId: user.id,
email: user.email,
role: user.role,
});

setRefreshTokenCookie(res, tokens.refreshToken);

res.json({
success: true,
data: {
user: {
id: user.id,
fullname: user.fullname,
email: user.email,
role: user.role,
walletAddress: user.walletAddress,
country: user.country,
isVerified: user.isVerified,
},
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
},
});
}

export function logout(_req: Request, res: Response): void {
res.clearCookie("refreshToken", {
httpOnly: true,
secure: config.nodeEnv === "production",
sameSite: "strict",
path: "/",
});

res.json({
success: true,
data: { message: "Logged out successfully" },
});
}

export async function refreshToken(req: Request, res: Response): Promise<void> {
const token = req.cookies?.refreshToken ?? req.body?.refreshToken;

if (!token) {
throw new UnauthorizedError("No refresh token provided");
}

let decoded: { userId: string; email: string; role: string };
try {
decoded = jwt.verify(token, config.jwt.refreshSecret) as { userId: string; email: string; role: string };
} catch {
throw new UnauthorizedError("Invalid or expired refresh token");
}

const user = await prisma.user.findUnique({ where: { id: decoded.userId } });
if (!user) {
throw new NotFoundError("User not found");
}

const tokens = generateTokens({
userId: user.id,
email: user.email,
role: user.role,
});

setRefreshTokenCookie(res, tokens.refreshToken);

res.json({
success: true,
data: {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
},
});
}

export async function getMe(req: Request, res: Response): Promise<void> {
const authReq = req as AuthRequest;
const user = await prisma.user.findUnique({
where: { id: authReq.user.userId },
select: {
id: true,
fullname: true,
email: true,
role: true,
walletAddress: true,
profileImage: true,
country: true,
isVerified: true,
createdAt: true,
updatedAt: true,
},
});

if (!user) {
throw new NotFoundError("User not found");
}

res.json({
success: true,
data: { user },
});
}
Loading
Loading