From 2719cd4a6fd7addb95d3d0128937f69006292599 Mon Sep 17 00:00:00 2001 From: Bintang Murtifandy <21844@student.stembayo.sch.id> Date: Tue, 26 May 2026 08:37:26 +0700 Subject: [PATCH] feat: add profile update endpoint with avatar upload --- src/controllers/authController.js | 22 ++++++++++++++++++++++ src/routes/auths.js | 2 ++ src/services/authService.js | 27 +++++++++++++++++++++++++++ src/utils/multer.js | 18 +++++++++++++++++- 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/controllers/authController.js b/src/controllers/authController.js index d51b7c4..f6e0a85 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -3,6 +3,7 @@ import { loginUser, refreshSession, registerUser, + updateProfile, } from '../services/authService.js'; const accessCookieName = 'noctune_access'; @@ -92,4 +93,25 @@ export const authController = { clearAuthCookies(res); res.json({ success: true, data: { message: 'Logged out' } }); }, + + async updateProfile(req, res, next) { + try { + const { name, username, email } = req.body; + const avatarFile = req.file; + const avatarUrl = avatarFile + ? `/uploads/avatars/${avatarFile.filename}` + : undefined; + + const user = await updateProfile(req.userId, { + name, + username, + email, + avatarUrl, + }); + + return res.json({ success: true, data: { user } }); + } catch (error) { + next(error); + } + }, }; diff --git a/src/routes/auths.js b/src/routes/auths.js index ee1b665..db8a2d8 100644 --- a/src/routes/auths.js +++ b/src/routes/auths.js @@ -1,6 +1,7 @@ import { Router } from 'express'; import { authController } from '../controllers/authController.js'; import { auth } from '../middlewares/auth.js'; +import { uploadAvatar } from '../utils/multer.js'; const router = Router(); @@ -9,5 +10,6 @@ router.post('/login', authController.login); router.post('/refresh', authController.refresh); router.post('/logout', authController.logout); router.get('/me', auth, authController.me); +router.put('/profile', auth, uploadAvatar.single('avatar'), authController.updateProfile); export default router; diff --git a/src/services/authService.js b/src/services/authService.js index 706bf70..4016669 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -168,3 +168,30 @@ export const getSessionUser = async (userId) => { return user; }; + +export const updateProfile = async (userId, { name, username, email, avatarUrl }) => { + const updateFields = {}; + if (name !== undefined) updateFields.name = name.trim(); + if (username !== undefined) updateFields.username = username.trim().toLowerCase(); + if (email !== undefined) updateFields.email = email.trim().toLowerCase(); + if (avatarUrl !== undefined) updateFields.avatarUrl = avatarUrl; + + if (updateFields.username || updateFields.email) { + const existing = await User.findOne({ + _id: { $ne: userId }, + $or: [ + ...(updateFields.username ? [{ username: updateFields.username }] : []), + ...(updateFields.email ? [{ email: updateFields.email }] : []), + ], + }).lean(); + + if (existing) { + throw createError(409, 'Username or email already in use'); + } + } + + await User.updateOne({ _id: userId }, { $set: updateFields }); + const user = await getPublicUser(userId); + + return user; +}; diff --git a/src/utils/multer.js b/src/utils/multer.js index d938846..cdc672b 100644 --- a/src/utils/multer.js +++ b/src/utils/multer.js @@ -5,6 +5,7 @@ import multer from 'multer'; const coversDir = path.join(process.cwd(), 'uploads', 'covers'); const audioDir = path.join(process.cwd(), 'uploads', 'audio'); const playlistCoversDir = path.join(process.cwd(), 'uploads', 'playlists'); +const avatarsDir = path.join(process.cwd(), 'uploads', 'avatars'); if (!fs.existsSync(coversDir)) { fs.mkdirSync(coversDir, { recursive: true }); @@ -18,6 +19,10 @@ if (!fs.existsSync(playlistCoversDir)) { fs.mkdirSync(playlistCoversDir, { recursive: true }); } +if (!fs.existsSync(avatarsDir)) { + fs.mkdirSync(avatarsDir, { recursive: true }); +} + const storage = multer.diskStorage({ destination: (req, file, cb) => { if (file.fieldname === 'cover') { @@ -32,6 +37,10 @@ const storage = multer.diskStorage({ return cb(null, playlistCoversDir); } + if (file.fieldname === 'avatar') { + return cb(null, avatarsDir); + } + return cb(new Error('Invalid upload field')); }, filename: (req, file, cb) => { @@ -75,9 +84,16 @@ const imageFilter = (req, file, cb) => { return cb(null, true); } - return cb(new Error('Invalid upload field')); + return cb(new Error('Invalid upload field')); + }; }; +export const uploadAvatar = multer({ + storage, + fileFilter: imageFilter, + limits: { fileSize: 5 * 1024 * 1024 }, +}); + export const uploadTrackAssets = multer({ storage, fileFilter: imageFilter,