diff --git a/src/controllers/department.controller.js b/src/controllers/department.controller.js new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 2673f4a..026c5b4 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -1,14 +1,33 @@ import prisma from '../config/prismaClient.js'; +import { + deleteFromCloudinary, + uploadToCloudinary, +} from '../utils/cloudinary.utils.js'; import { comparePassword, hashPassword } from '../utils/password.utils.js'; import { updatePasswordValidation, updateUserAccountValidation, } from '../validations/user.validation.js'; /* eslint no-undef:off */ +/** + * @desc Get all users with pagination + * @route GET /api/users/all?page=1 + * @method GET + * @access Private (Admin only) + */ export const getAllUsers = async (req, res, next) => { try { - // Fetch all users from the database + const page = parseInt(req.query.page) || 1; + const limit = 10; + const skip = (page - 1) * limit; + + // Get total number of users + const totalUsers = await prisma.user.count(); + + // Fetch users with pagination const users = await prisma.user.findMany({ + skip, + take: limit, select: { id: true, email: true, @@ -20,38 +39,107 @@ export const getAllUsers = async (req, res, next) => { return res.status(200).json({ message: 'Users retrieved successfully', + currentPage: page, + totalPages: Math.ceil(totalUsers / limit), + totalUsers, users, }); } catch (error) { - next(error); // Ensure next is called with the error + next(error); } }; +/** + * @desc Get full user profile by ID + * @route GET /api/users/:id + * @method GET + * @access Private (User or Admin) + */ export const getUserById = async (req, res, next) => { const { id } = req.params; + try { - // Ensure req.user is defined const user = await prisma.user.findFirst({ where: { id }, select: { id: true, email: true, + username: true, firstName: true, lastName: true, role: true, + profilePic: true, + phoneNumber: true, + jobTitle: true, + timezone: true, + bio: true, + preferences: true, + isActive: true, + isOwner: true, + createdAt: true, + updatedAt: true, + lastLogin: true, // Keep valid fields + // Removed invalid field `lastLogout` + department: { + select: { + id: true, + name: true, + }, + }, + organization: { + select: { + id: true, + name: true, + }, + }, + permissions: { + select: { + entityType: true, + entityId: true, + permissions: true, + }, + }, + teamMemberships: { + select: { + team: { + select: { + id: true, + name: true, + }, + }, + }, + }, + activityLogs: { + take: 5, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + action: true, + createdAt: true, + }, + }, }, }); if (!user) { - return next(error); // Ensure next is called with the error + return res.status(404).json({ message: 'User not found' }); } - return res.status(200).json(user); + return res.status(200).json({ + message: 'User retrieved successfully', + user, + }); } catch (error) { - next(error); // Ensure next is called with the error + next(error); } }; +/** + * @desc Update user account + * @route PUT /api/users/:id + * @method PUT + * @access Private (User or Admin) + */ export const updateUserAccount = async (req, res, next) => { try { const userId = req.params.id; @@ -146,6 +234,12 @@ export const updateUserAccount = async (req, res, next) => { } }; +/** + * @desc Update user password + * @route PUT /api/users/update-password/:id + * @method PUT + * @access Private (User only) + */ export const updateUserPassword = async (req, res, next) => { try { // Validate the request body @@ -197,6 +291,12 @@ export const updateUserPassword = async (req, res, next) => { } }; +/** + * @desc Soft delete a user + * @route DELETE /api/users/:id + * @method DELETE + * @access Private (Admin only) + */ export const softDeleteUser = async (req, res, next) => { try { const { id } = req.params; @@ -222,6 +322,13 @@ export const softDeleteUser = async (req, res, next) => { next(error); } }; + +/** + * @desc Restore a soft-deleted user + * @route PATCH /api/users/restore/:id + * @method PATCH + * @access Private (Admin only) + */ export const restoreUser = async (req, res, next) => { try { const { id } = req.params; @@ -245,3 +352,10 @@ export const restoreUser = async (req, res, next) => { next(error); } }; + +/** + * @desc Upload a profile picture for a user + * @route POST /api/users/:id/profile-picture + * @method POST + * @access Private (User or Admin) + */ diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 4921c22..afe3207 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -960,13 +960,24 @@ "/api/users/all": { "get": { "tags": ["Users"], - "summary": "List users", - "description": "Get list of all users in the system", + "summary": "List all users", + "description": "Retrieve a paginated list of all users.", "operationId": "getAllUsers", "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "schema": { + "type": "integer", + "default": 1 + } + } + ], "responses": { "200": { - "description": "List of users", + "description": "Users retrieved successfully", "content": { "application/json": { "schema": { @@ -976,7 +987,6 @@ } }, "401": { - "description": "Unauthorized", "$ref": "#/components/responses/UnauthorizedError" } } @@ -986,7 +996,7 @@ "get": { "tags": ["Users"], "summary": "Get user by ID", - "description": "Get detailed information about a specific user", + "description": "Retrieve detailed information about a specific user.", "operationId": "getUserById", "security": [{ "bearerAuth": [] }], "parameters": [ @@ -994,7 +1004,7 @@ "name": "id", "in": "path", "required": true, - "description": "ID of the user to retrieve", + "description": "User ID", "schema": { "type": "string", "format": "uuid" @@ -1003,7 +1013,7 @@ ], "responses": { "200": { - "description": "User details", + "description": "User retrieved successfully", "content": { "application/json": { "schema": { @@ -1012,12 +1022,7 @@ } } }, - "400": { - "description": "Invalid user ID", - "$ref": "#/components/responses/ValidationError" - }, "404": { - "description": "User not found", "$ref": "#/components/responses/NotFoundError" } } @@ -1025,7 +1030,7 @@ "put": { "tags": ["Users"], "summary": "Update user account", - "description": "Update user profile information. Admins can update additional fields like role, department, and organization.", + "description": "Update user profile information.", "operationId": "updateUserAccount", "security": [{ "bearerAuth": [] }], "parameters": [ @@ -1033,7 +1038,7 @@ "name": "id", "in": "path", "required": true, - "description": "ID of the user to update", + "description": "User ID", "schema": { "type": "string", "format": "uuid" @@ -1042,37 +1047,11 @@ ], "requestBody": { "description": "User update data", - "required": false, + "required": true, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "firstName": { - "type": "string", - "example": "John" - }, - "lastName": { - "type": "string", - "example": "Doe" - }, - "phoneNumber": { - "type": "string", - "example": "+1234567890" - }, - "jobTitle": { - "type": "string", - "example": "Software Engineer" - }, - "timezone": { - "type": "string", - "example": "UTC+2" - }, - "bio": { - "type": "string", - "example": "Experienced software engineer with expertise in backend development." - } - } + "$ref": "#/components/schemas/UpdateUserRequest" } } } @@ -1083,66 +1062,20 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "example": "User account updated successfully" - }, - "user": { - "$ref": "#/components/schemas/UserResponse" - } - } - } - } - } - }, - "400": { - "description": "Invalid input", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Forbidden - Account is not active", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UserResponse" } } } }, "404": { - "description": "User not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/NotFoundError" } } }, "delete": { "tags": ["Users"], "summary": "Soft delete a user", - "description": "Marks a user as deleted by setting a `deletedAt` timestamp and deactivating the account.", + "description": "Mark a user as deleted.", "operationId": "softDeleteUser", "security": [{ "bearerAuth": [] }], "parameters": [ @@ -1150,7 +1083,7 @@ "name": "id", "in": "path", "required": true, - "description": "ID of the user to delete", + "description": "User ID", "schema": { "type": "string", "format": "uuid" @@ -1175,58 +1108,37 @@ } }, "404": { - "description": "User not found or already deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "$ref": "#/components/responses/NotFoundError" } } } }, - "/api/users/update-password": { + "/api/users/update-password/{id}": { "put": { "tags": ["Users"], "summary": "Update user password", - "description": "Allows a user to update their password by providing the old and new passwords.", + "description": "Update a user's password.", "operationId": "updateUserPassword", "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "User ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "requestBody": { "description": "Password update data", "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": ["email", "oldPassword", "newPassword"], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "user@example.com" - }, - "oldPassword": { - "type": "string", - "example": "oldPassword123" - }, - "newPassword": { - "type": "string", - "example": "newPassword456" - } - } + "$ref": "#/components/schemas/UpdatePasswordRequest" } } } @@ -1248,35 +1160,143 @@ } } }, - "400": { - "description": "Bad request - Validation error or incorrect old password", + "404": { + "$ref": "#/components/responses/NotFoundError" + } + } + } + }, + "/api/users/restore/{id}": { + "patch": { + "tags": ["Users"], + "summary": "Restore a soft-deleted user", + "description": "Restore a user that was previously soft-deleted.", + "operationId": "restoreUser", + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "User ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "User restored successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "User restored successfully" + } + } } } } }, "404": { - "description": "User not found", + "$ref": "#/components/responses/NotFoundError" + } + } + } + }, + "/api/users/{id}/profile-picture": { + "post": { + "tags": ["Users"], + "summary": "Upload profile picture", + "description": "Upload a profile picture for a user.", + "operationId": "uploadProfilePicture", + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "User ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary", + "description": "Profile picture file" + } + }, + "required": ["image"] + } + } + } + }, + "responses": { + "200": { + "description": "Profile picture uploaded successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "$ref": "#/components/schemas/UserResponse" } } } }, - "500": { - "description": "Internal server error", + "404": { + "$ref": "#/components/responses/NotFoundError" + } + } + }, + "delete": { + "tags": ["Users"], + "summary": "Delete profile picture", + "description": "Delete a user's profile picture.", + "operationId": "deleteProfilePicture", + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "User ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Profile picture deleted successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Profile picture deleted successfully" + } + } } } } + }, + "404": { + "$ref": "#/components/responses/NotFoundError" } } } @@ -2158,34 +2178,71 @@ } } }, + "UserResponse": { "type": "object", "properties": { "id": { "type": "string", - "format": "uuid" + "format": "uuid", + "example": "550e8400-e29b-41d4-a716-446655440000" }, "email": { "type": "string", - "format": "email" + "format": "email", + "example": "user@example.com" + }, + "username": { + "type": "string", + "example": "johndoe" }, "firstName": { - "type": "string" + "type": "string", + "example": "John" }, "lastName": { - "type": "string" + "type": "string", + "example": "Doe" }, "role": { "type": "string", - "enum": ["MEMBER", "ADMIN", "MANAGER"] + "enum": ["MEMBER", "ADMIN", "MANAGER"], + "example": "MEMBER" + }, + "profilePic": { + "type": "string", + "format": "uri", + "example": "https://example.com/profile.jpg" + }, + "phoneNumber": { + "type": "string", + "example": "+1234567890" + }, + "jobTitle": { + "type": "string", + "example": "Software Engineer" + }, + "timezone": { + "type": "string", + "example": "UTC+2" + }, + "bio": { + "type": "string", + "example": "Experienced software developer" }, - "gender": { + "isActive": { + "type": "boolean", + "example": true + }, + "createdAt": { "type": "string", - "enum": ["MALE", "FEMALE", "OTHER"] + "format": "date-time", + "example": "2023-01-01T00:00:00Z" }, - "DOB": { + "updatedAt": { "type": "string", - "format": "date" + "format": "date-time", + "example": "2023-01-02T00:00:00Z" } } }, @@ -2196,14 +2253,54 @@ "type": "string", "example": "Users retrieved successfully" }, + "currentPage": { + "type": "integer", + "example": 1 + }, + "totalPages": { + "type": "integer", + "example": 5 + }, + "totalUsers": { + "type": "integer", + "example": 50 + }, "users": { "type": "array", "items": { - "$ref": "#/components/schemas/UserResponse" + "$ref": "#/components/schemas/BasicUserInfo" } } } }, + "BasicUserInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "example": "550e8400-e29b-41d4-a716-446655440000" + }, + "email": { + "type": "string", + "format": "email", + "example": "user@example.com" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "lastName": { + "type": "string", + "example": "Doe" + }, + "role": { + "type": "string", + "enum": ["MEMBER", "ADMIN", "MANAGER"], + "example": "MEMBER" + } + } + }, "UpdateUserRequest": { "type": "object", "properties": { @@ -2217,18 +2314,36 @@ "minLength": 2, "example": "Doe" }, - "gender": { + "phoneNumber": { "type": "string", - "enum": ["MALE", "FEMALE", "OTHER"] + "example": "+1234567890" }, - "DOB": { + "jobTitle": { "type": "string", - "format": "date", - "example": "1990-01-01" + "example": "Software Engineer" }, - "mobileNumber": { + "timezone": { "type": "string", - "example": "+1234567890" + "example": "UTC+2" + }, + "bio": { + "type": "string", + "example": "Experienced software developer" + }, + "role": { + "type": "string", + "enum": ["MEMBER", "ADMIN", "MANAGER"], + "example": "MEMBER" + }, + "departmentId": { + "type": "string", + "format": "uuid", + "example": "550e8400-e29b-41d4-a716-446655440001" + }, + "organizationId": { + "type": "string", + "format": "uuid", + "example": "550e8400-e29b-41d4-a716-446655440002" } } }, @@ -2242,12 +2357,41 @@ }, "newPassword": { "type": "string", - "example": "newPassword456" + "minLength": 8, + "example": "newSecurePassword456" + } + } + }, + "ProfilePicUploadResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Profile picture uploaded successfully" + }, + "profilePicUrl": { + "type": "string", + "format": "uri", + "example": "https://example.com/profile.jpg" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + } + } + }, + "ProfilePicDeleteResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Profile picture deleted successfully" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" } } } }, - "responses": { "UnauthorizedError": { "description": "Unauthorized", diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js index e67adc2..696094a 100644 --- a/src/routes/user.routes.js +++ b/src/routes/user.routes.js @@ -1,15 +1,18 @@ import { Router } from 'express'; import { + deleteUserProfilePic, getAllUsers, getUserById, restoreUser, softDeleteUser, updateUserAccount, updateUserPassword, + uploadUserProfilePic, } from '../controllers/user.controller.js'; import { verifyAccessToken } from '../middlewares/auth.middleware.js'; import { verifyAdminPermission } from '../middlewares/verifyAdminPermission.middleware.js'; import { verifyUserPermission } from '../middlewares/verifyUserPermission.middleware.js'; +import upload from '../middlewares/upload.middleware.js'; // import{authorizeUser} from '../middlewares/auth.middleware.js'; const router = Router(); @@ -43,13 +46,13 @@ router.put( ); router.delete( - '/users/:id', + '/api/users/:id', verifyAccessToken, verifyAdminPermission, softDeleteUser, ); router.patch( - '/users/restore/:id', + '/api/users/restore/:id', verifyAccessToken, verifyAdminPermission, restoreUser, diff --git a/src/validations/user.validation.js b/src/validations/user.validation.js index eb40769..c19a11a 100644 --- a/src/validations/user.validation.js +++ b/src/validations/user.validation.js @@ -44,3 +44,7 @@ export const updatePasswordValidation = (obj) => { return schema.validate(obj); }; + +export const profilePictureValidation = Joi.object({ + profilePicture: Joi.any(), // This will be handled by multer +});