feat: add profile update endpoint with avatar upload
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
|||||||
loginUser,
|
loginUser,
|
||||||
refreshSession,
|
refreshSession,
|
||||||
registerUser,
|
registerUser,
|
||||||
|
updateProfile,
|
||||||
} from '../services/authService.js';
|
} from '../services/authService.js';
|
||||||
|
|
||||||
const accessCookieName = 'noctune_access';
|
const accessCookieName = 'noctune_access';
|
||||||
@@ -92,4 +93,25 @@ export const authController = {
|
|||||||
clearAuthCookies(res);
|
clearAuthCookies(res);
|
||||||
res.json({ success: true, data: { message: 'Logged out' } });
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { authController } from '../controllers/authController.js';
|
import { authController } from '../controllers/authController.js';
|
||||||
import { auth } from '../middlewares/auth.js';
|
import { auth } from '../middlewares/auth.js';
|
||||||
|
import { uploadAvatar } from '../utils/multer.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -9,5 +10,6 @@ router.post('/login', authController.login);
|
|||||||
router.post('/refresh', authController.refresh);
|
router.post('/refresh', authController.refresh);
|
||||||
router.post('/logout', authController.logout);
|
router.post('/logout', authController.logout);
|
||||||
router.get('/me', auth, authController.me);
|
router.get('/me', auth, authController.me);
|
||||||
|
router.put('/profile', auth, uploadAvatar.single('avatar'), authController.updateProfile);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -168,3 +168,30 @@ export const getSessionUser = async (userId) => {
|
|||||||
|
|
||||||
return user;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import multer from 'multer';
|
|||||||
const coversDir = path.join(process.cwd(), 'uploads', 'covers');
|
const coversDir = path.join(process.cwd(), 'uploads', 'covers');
|
||||||
const audioDir = path.join(process.cwd(), 'uploads', 'audio');
|
const audioDir = path.join(process.cwd(), 'uploads', 'audio');
|
||||||
const playlistCoversDir = path.join(process.cwd(), 'uploads', 'playlists');
|
const playlistCoversDir = path.join(process.cwd(), 'uploads', 'playlists');
|
||||||
|
const avatarsDir = path.join(process.cwd(), 'uploads', 'avatars');
|
||||||
|
|
||||||
if (!fs.existsSync(coversDir)) {
|
if (!fs.existsSync(coversDir)) {
|
||||||
fs.mkdirSync(coversDir, { recursive: true });
|
fs.mkdirSync(coversDir, { recursive: true });
|
||||||
@@ -18,6 +19,10 @@ if (!fs.existsSync(playlistCoversDir)) {
|
|||||||
fs.mkdirSync(playlistCoversDir, { recursive: true });
|
fs.mkdirSync(playlistCoversDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(avatarsDir)) {
|
||||||
|
fs.mkdirSync(avatarsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
if (file.fieldname === 'cover') {
|
if (file.fieldname === 'cover') {
|
||||||
@@ -32,6 +37,10 @@ const storage = multer.diskStorage({
|
|||||||
return cb(null, playlistCoversDir);
|
return cb(null, playlistCoversDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file.fieldname === 'avatar') {
|
||||||
|
return cb(null, avatarsDir);
|
||||||
|
}
|
||||||
|
|
||||||
return cb(new Error('Invalid upload field'));
|
return cb(new Error('Invalid upload field'));
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
@@ -77,6 +86,13 @@ const imageFilter = (req, file, cb) => {
|
|||||||
|
|
||||||
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({
|
export const uploadTrackAssets = multer({
|
||||||
storage,
|
storage,
|
||||||
|
|||||||
Reference in New Issue
Block a user