api-v1/routes/userRoutes.js

286 lines
7.9 KiB
JavaScript

import express from "express";
import { insert, select, update } from "../db/queryHelper/index.js";
import jwtAuthenticate from "../middleware/jwtAuthenticate.js";
import { rateLimiterRead, rateLimiterWrite } from "../middleware/rateLimiter.js";
import createCoarseAuthorize from "../middleware/coarseAuthorize.js";
import { fineAuthorize, authorizeAction } from "../middleware/fineAuthorize.js";
const router = express.Router();
// Apply authentication and rate limiting to all user routes
router.use(jwtAuthenticate);
router.use(rateLimiterRead); // Use read rate limiter for user routes
// Apply coarse-grained authorization (USER and ADMIN can access)
const requireUserOrAdmin = createCoarseAuthorize(['USER', 'ADMIN']);
router.use(requireUserOrAdmin);
// 1. CREATE User (Requires write rate limiter)
router.post("/", rateLimiterWrite, async (req, res) => {
console.log(`[User Route] POST /users - Request received`);
console.log(`[User Route] Authenticated user:`, req.user ? {
userId: req.user.userId,
role: req.user.role,
} : 'NOT AUTHENTICATED');
console.log(`[User Route] Request body:`, {
id: req.body?.id,
name: req.body?.name,
phone_number: req.body?.phone_number ? '***' : undefined,
});
try {
// Parse and extract user data from request body
const {
id, // Optional: if provided, use this UUID; otherwise generate one
name,
phone_number,
avatar_url,
language,
timezone,
country_code = "+91",
} = req.body;
// Validate required fields
if (!name || !phone_number) {
return res.status(400).json({
error: "name and phone_number are required fields",
});
}
// Build user data object using JSON-based structure
const userData = {
name: name.trim(),
phone_number: phone_number.trim(),
avatar_url: avatar_url || null,
language: language || null,
timezone: timezone || null,
country_code: country_code || "+91",
};
// If id is provided, include it; otherwise let the database generate one
if (id) {
userData.id = id;
}
// Use queryHelper insert with JSON-based approach
const user = await insert({
table: 'users',
data: userData,
returning: '*',
});
res.status(201).json(user);
} catch (err) {
console.error("Error creating user:", err);
if (err.code === "23505") {
// Unique constraint violation
return res.status(400).json({
error: err.detail || "A user with this phone number or ID already exists",
});
}
if (err.code === "23503") {
// Foreign key violation
return res.status(400).json({
error: err.detail || "Foreign key constraint violation",
});
}
res.status(500).json({ error: err.message || "Internal server error" });
}
});
// 2. GET All Users
router.get("/", async (req, res) => {
try {
// Parse and validate query parameters
const limit = Math.min(parseInt(req.query.limit) || 100, 100);
const offset = parseInt(req.query.offset) || 0;
const { is_active, phone_number, name } = req.query;
// Build where conditions from query parameters
const where = { deleted: false };
if (is_active !== undefined) {
where.is_active = is_active === 'true' || is_active === true;
}
if (phone_number) {
where.phone_number = phone_number;
}
if (name) {
where.name = { op: 'ilike', value: `%${name}%` };
}
const users = await select({
table: 'users',
columns: ['id', 'name', 'phone_number', 'avatar_url', 'language', 'timezone', 'country_code', 'is_active', 'created_at', 'updated_at'],
where,
orderBy: {
column: 'created_at',
direction: 'desc',
},
limit,
offset,
});
res.json(users);
} catch (err) {
console.error("Error fetching users:", err);
res.status(500).json({ error: "Internal server error" });
}
});
// 3. GET Single User
// Fine-grained authorization: Users can only read their own profile, admins can read any profile
router.get("/:id",
fineAuthorize({
action: 'read',
resource: 'user',
getResourceOwnerId: (req) => req.params.id,
}),
async (req, res) => {
console.log(`[User Route] GET /users/:id - Request received`);
console.log(`[User Route] User ID requested: ${req.params.id}`);
console.log(`[User Route] Authenticated user:`, req.user ? {
userId: req.user.userId,
role: req.user.role,
} : 'NOT AUTHENTICATED');
try {
const { id } = req.params;
// Use queryHelper select with JSON-based where conditions
const user = await select({
table: 'users',
columns: ['id', 'name', 'phone_number', 'avatar_url', 'language', 'timezone', 'country_code', 'is_active', 'created_at', 'updated_at'],
where: {
id,
deleted: false,
},
limit: 1,
});
if (user.length === 0) {
// Log not found
if (req.auditLogger) {
req.auditLogger.logFailure('get_user', 'User not found', { userId: id });
}
return res.status(404).json({ error: "User not found" });
}
// Log success
if (req.auditLogger) {
req.auditLogger.logSuccess('get_user', { userId: id });
}
res.json(user[0]);
} catch (err) {
console.error("Error fetching user:", err);
// Log error
if (req.auditLogger) {
req.auditLogger.logFailure('get_user', err.message, { userId: req.params.id });
}
res.status(500).json({ error: "Internal server error" });
}
}
);
// 4. UPDATE User (Requires write rate limiter and fine-grained authorization)
router.put("/:id",
rateLimiterWrite,
fineAuthorize({
action: 'write',
resource: 'user',
getResourceOwnerId: (req) => req.params.id,
}),
async (req, res) => {
try {
const { id } = req.params;
// Parse and extract update data from request body
const { name, phone_number, avatar_url, language, timezone, country_code, is_active } = req.body;
// Build update data object using JSON-based structure (only include fields that are provided)
const updateData = {};
if (name !== undefined) updateData.name = name.trim();
if (phone_number !== undefined) updateData.phone_number = phone_number.trim();
if (avatar_url !== undefined) updateData.avatar_url = avatar_url || null;
if (language !== undefined) updateData.language = language || null;
if (timezone !== undefined) updateData.timezone = timezone || null;
if (country_code !== undefined) updateData.country_code = country_code;
if (is_active !== undefined) updateData.is_active = is_active === true || is_active === 'true';
// Validate that at least one field is being updated
if (Object.keys(updateData).length === 0) {
return res.status(400).json({ error: "At least one field must be provided for update" });
}
// Use queryHelper update with JSON-based where conditions
const updated = await update({
table: 'users',
data: updateData,
where: {
id,
deleted: false,
},
returning: '*',
});
if (updated.length === 0) {
return res.status(404).json({ error: "User not found" });
}
res.json(updated[0]);
} catch (err) {
console.error("Error updating user:", err);
if (err.code === "23505") {
// Unique constraint violation
return res.status(400).json({
error: err.detail || "A user with this phone number already exists",
});
}
res.status(500).json({ error: err.message || "Internal server error" });
}
});
// 5. DELETE User (Soft Delete) - Requires fine-grained authorization
router.delete("/:id",
fineAuthorize({
action: 'delete',
resource: 'user',
getResourceOwnerId: (req) => req.params.id,
}),
async (req, res) => {
try {
const { id } = req.params;
// Use queryHelper update with JSON-based where conditions for soft delete
const deleted = await update({
table: 'users',
data: {
deleted: true,
},
where: {
id,
deleted: false, // Only delete if not already deleted
},
returning: ['id'],
});
if (deleted.length === 0) {
return res.status(404).json({ error: "User not found or already deleted" });
}
res.json({ message: "User deleted successfully", id: deleted[0].id });
} catch (err) {
console.error("Error deleting user:", err);
res.status(500).json({ error: "Internal server error" });
}
});
export default router;