286 lines
7.9 KiB
JavaScript
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;
|
|
|