Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
172 changes: 170 additions & 2 deletions backend/src/controllers/auth/profile.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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<string, any> = {};

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<string, any> = {};

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);
}
};
5 changes: 4 additions & 1 deletion backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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'
}
});
});
Expand Down Expand Up @@ -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;
25 changes: 25 additions & 0 deletions backend/src/routes/profileRoutes.ts
Original file line number Diff line number Diff line change
@@ -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;