feat: add profile update endpoint with avatar upload

This commit is contained in:
2026-05-26 08:37:26 +07:00
parent ba293fd140
commit 2719cd4a6f
4 changed files with 68 additions and 1 deletions
+22
View File
@@ -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);
}
},
};
+2
View File
@@ -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;
+27
View File
@@ -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;
};
+16
View File
@@ -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) => {
@@ -76,8 +85,15 @@ const imageFilter = (req, file, cb) => {
}
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,