feat(admin): add add-music api
This commit is contained in:
@@ -17,6 +17,7 @@ app.use(helmet());
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
||||
app.use('/uploads', express.static('uploads'));
|
||||
app.use('/api', routes);
|
||||
|
||||
app.use((req, res) => {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import Track from '../models/Track.js';
|
||||
|
||||
export const adminController = {
|
||||
async addTrack(req, res, next) {
|
||||
try {
|
||||
const { title, artist, album, genre } = req.body;
|
||||
|
||||
if (!title || !artist || !album || !genre) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: 'All fields are required' });
|
||||
}
|
||||
|
||||
const coverUrl = req.file ? `/uploads/covers/${req.file.filename}` : '';
|
||||
|
||||
const track = await Track.create({
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
genre,
|
||||
coverUrl,
|
||||
createdBy: req.userId,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: track });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import User from '../models/User.js';
|
||||
|
||||
export const requireAdmin = async (req, res, next) => {
|
||||
try {
|
||||
if (!req.userId) {
|
||||
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const user = await User.findById(req.userId).select('role').lean();
|
||||
if (!user || user.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, message: 'Forbidden' });
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const trackSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 120,
|
||||
},
|
||||
artist: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 120,
|
||||
},
|
||||
album: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 120,
|
||||
},
|
||||
genre: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 60,
|
||||
},
|
||||
coverUrl: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
createdBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default mongoose.model('Track', trackSchema);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Router } from 'express';
|
||||
import { adminController } from '../controllers/adminController.js';
|
||||
import { auth } from '../middlewares/auth.js';
|
||||
import { requireAdmin } from '../middlewares/roles.js';
|
||||
import { uploadCover } from '../utils/multer.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/tracks',
|
||||
auth,
|
||||
requireAdmin,
|
||||
uploadCover.single('cover'),
|
||||
adminController.addTrack
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import authRoutes from './auths.js';
|
||||
import adminRoutes from './admins.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -14,5 +15,6 @@ router.get('/health', (req, res) => {
|
||||
});
|
||||
|
||||
router.use('/auth', authRoutes);
|
||||
router.use('/admin', adminRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'dotenv/config';
|
||||
import app from './app.js';
|
||||
import { connectDb } from './config/db.js';
|
||||
import { ensureAdminUser } from './services/adminBootstrap.js';
|
||||
|
||||
const port = Number(process.env.PORT) || 3000;
|
||||
|
||||
const startServer = async () => {
|
||||
try {
|
||||
await connectDb();
|
||||
await ensureAdminUser();
|
||||
app.listen(port, () => {
|
||||
process.stdout.write(`Server listening on port ${port}\n`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import User from '../models/User.js';
|
||||
|
||||
const normalize = (value) => value.trim().toLowerCase();
|
||||
|
||||
export const ensureAdminUser = async () => {
|
||||
const username = process.env.ADMIN_USERNAME;
|
||||
const email = process.env.ADMIN_EMAIL;
|
||||
const password = process.env.ADMIN_PASSWORD;
|
||||
|
||||
if (!username || !email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedUsername = normalize(username);
|
||||
const normalizedEmail = normalize(email);
|
||||
|
||||
const existing = await User.findOne({
|
||||
$or: [{ username: normalizedUsername }, { email: normalizedEmail }],
|
||||
}).lean();
|
||||
|
||||
if (existing) {
|
||||
if (existing.role !== 'admin') {
|
||||
await User.updateOne({ _id: existing._id }, { role: 'admin' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
await User.create({
|
||||
name: 'Admin',
|
||||
username: normalizedUsername,
|
||||
email: normalizedEmail,
|
||||
passwordHash,
|
||||
role: 'admin',
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import multer from 'multer';
|
||||
|
||||
const coversDir = path.join(process.cwd(), 'uploads', 'covers');
|
||||
|
||||
if (!fs.existsSync(coversDir)) {
|
||||
fs.mkdirSync(coversDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, coversDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const safeName = file.originalname.replace(/\s+/g, '-');
|
||||
cb(null, `${Date.now()}-${safeName}`);
|
||||
},
|
||||
});
|
||||
|
||||
const imageFilter = (req, file, cb) => {
|
||||
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
if (!allowed.includes(file.mimetype)) {
|
||||
return cb(new Error('Only JPG, PNG, or WEBP images are allowed'));
|
||||
}
|
||||
|
||||
return cb(null, true);
|
||||
};
|
||||
|
||||
export const uploadCover = multer({
|
||||
storage,
|
||||
fileFilter: imageFilter,
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
});
|
||||
Reference in New Issue
Block a user