From a5b05f4562ca11faefe5802b030d68cb89e10833 Mon Sep 17 00:00:00 2001 From: BenOkojie <83321573+BenOkojie@users.noreply.github.com> Date: Sat, 28 Feb 2026 13:50:16 -0500 Subject: [PATCH] add userprofile routes, edited profile controller --- backend/src/controllers/auth/profile.ts | 172 +++++++++++++++++++++++- backend/src/routes/index.ts | 5 +- backend/src/routes/profileRoutes.ts | 25 ++++ 3 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 backend/src/routes/profileRoutes.ts diff --git a/backend/src/controllers/auth/profile.ts b/backend/src/controllers/auth/profile.ts index 14b7615..5873d80 100644 --- a/backend/src/controllers/auth/profile.ts +++ b/backend/src/controllers/auth/profile.ts @@ -1,6 +1,7 @@ import { Request, Response, NextFunction } from 'express'; -import User from '../../models/user'; +import User, {UserRole} from '../../models/user'; import { AuthenticationError } from '../../errors'; +import { prefault } from 'zod'; /** * @route GET /api/auth/me @@ -33,7 +34,6 @@ export const me = async (req: Request, res: Response, next: NextFunction) => { groupIds: user.groupIds, profileImage: user.profileImage, isActive: user.isActive, - isEmailVerified: user.isEmailVerified, lastLogin: user.lastLogin } } @@ -43,3 +43,171 @@ export const me = async (req: Request, res: Response, next: NextFunction) => { next(error); } }; + +export const updateProfile = async (req: Request, res: Response, next: NextFunction) => { + try { + // Only allow these fields + const { name, phone, bio, role } = req.body; + + const updates: Record = {}; + + if (name !== undefined) updates.name = String(name).trim(); + if (phone !== undefined) updates.phone = phone ? String(phone).trim() : undefined; + if (bio !== undefined) updates.bio = bio ? String(bio).trim() : undefined; + + // Role validation (only allow values in your enum) + if (role !== undefined) { + const r = String(role).toLowerCase(); + const allowed = Object.values(UserRole); + if (!allowed.includes(r as UserRole)) { + return res.status(400).json({ + success: false, + message: `Invalid role. Allowed roles: ${allowed.join(", ")}` + }); + } + updates.role = r; + } + + // Optional: extra validation + if (updates.name !== undefined && updates.name.length === 0) { + return res.status(400).json({ success: false, message: "Name cannot be empty" }); + } + if (updates.bio !== undefined && updates.bio.length > 500) { + return res.status(400).json({ success: false, message: "Bio must be 500 characters or less" }); + } + + const user = await User.findByIdAndUpdate( + req.user!._id, + { $set: updates }, + { new: true, runValidators: true } + ).select("-password"); + + if (!user) { + throw new AuthenticationError("User not found"); + } + + res.json({ + success: true, + data: { + user: { + id: user._id, + name: user.name, + email: user.email, + role: user.role, + teamId: user.teamId, + groupIds: user.groupIds, + profileImage: user.profileImage, + isActive: user.isActive, + lastLogin: user.lastLogin, + phone: user.phone, + bio: user.bio + } + } + }); + } catch (error) { + next(error); + } +}; +export const getPreferences = async (req: Request,res: Response,next: NextFunction) => { + try { + // Only fetch preferences (nothing else) + const user = await User.findById(req.user!._id).select("preferences"); + + if (!user) { + throw new AuthenticationError("User not found"); + } + + res.json({ + success: true, + data: { + preferences: user.preferences, + }, + }); + } catch (error) { + next(error); + } +}; +export const updatePreferences = async (req: Request, res: Response, next: NextFunction) => { + try { + const { timezone, notifications } = req.body; + const updates: Record = {}; + + if (timezone !== undefined) { + const tz = String(timezone).trim(); + if (!tz) { + return res.status(400).json({ + success: false, + message: "timezone cannot be empty", + }); + } + updates["preferences.timezone"] = tz; + } + + // notifications (nested) + if (notifications !== undefined) { + if (typeof notifications !== "object" || notifications === null) { + return res.status(400).json({ + success: false, + message: "notifications must be an object", + }); + } + + if (notifications.email !== undefined) { + if (typeof notifications.email !== "boolean") { + return res.status(400).json({ + success: false, + message: "notifications.email must be a boolean", + }); + } + updates["preferences.notifications.email"] = notifications.email; + } + + if (notifications.sms !== undefined) { + if (typeof notifications.sms !== "boolean") { + return res.status(400).json({ + success: false, + message: "notifications.sms must be a boolean", + }); + } + updates["preferences.notifications.sms"] = notifications.sms; + } + + if (notifications.push !== undefined) { + if (typeof notifications.push !== "boolean") { + return res.status(400).json({ + success: false, + message: "notifications.push must be a boolean", + }); + } + updates["preferences.notifications.push"] = notifications.push; + } + } + + // Nothing valid provided + if (Object.keys(updates).length === 0) { + return res.status(400).json({ + success: false, + message: "No valid preference fields provided", + }); + } + + const user = await User.findByIdAndUpdate( + req.user!._id, + { $set: updates }, + { new: true, runValidators: true } + ).select("preferences"); + + if (!user) { + throw new AuthenticationError("User not found"); + } + + return res.json({ + success: true, + data: { + preferences: user.preferences, + }, + }); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index b8aa8d7..51e9fb1 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -14,6 +14,7 @@ import groupRoutes from './groupRoutes'; import availablilityRoutes from './availabilityRoutes'; import meetingRoutes from './meetingRoutes'; import scheduleRoutes from './scheduleRoutes'; +import profileRoutes from './profileRoutes'; const router = Router(); @@ -38,7 +39,8 @@ router.get('/', (req, res) => { '/groups': 'group management endpoints', '/availability': 'availability management endpoints', '/meetings': 'meeting management endpoints', - '/schedules': 'schedule management endpoints' + '/schedules': 'schedule management endpoints', + '/profile': 'user profile management endpoints' } }); }); @@ -141,5 +143,6 @@ router.use('/groups', groupRoutes); router.use('/availability', availablilityRoutes); router.use('/meetings', meetingRoutes); router.use('/schedules', scheduleRoutes); +router.use('/profile', profileRoutes); export default router; \ No newline at end of file diff --git a/backend/src/routes/profileRoutes.ts b/backend/src/routes/profileRoutes.ts new file mode 100644 index 0000000..f1851a7 --- /dev/null +++ b/backend/src/routes/profileRoutes.ts @@ -0,0 +1,25 @@ +import { Router } from "express"; +import { authenticate } from "../middleware/authMiddleware"; // adjust path/name if yours differs +import {me,updateProfile,getPreferences, updatePreferences} from "../controllers/auth/profile"; + +const router = Router(); + +// All routes here require auth +router.use(authenticate); + +// GET /api/users/profile +router.get("/profile", me); + +// PUT /api/users/profile +router.put("/profile", updateProfile); + +// GET /api/users/preferences +router.get("/preferences", getPreferences); + +// PUT /api/users/preferences +router.put("/preferences", updatePreferences); + +// POST /api/users/profile/image (optional - not implemented now) +// router.post("/profile/image", uploadProfileImage); + +export default router;