Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --omit=dev
COPY server.js .
COPY lib/ lib/
COPY middleware/ middleware/
COPY routes/ routes/
COPY public/ public/
EXPOSE 3000
CMD ["node", "server.js"]
34 changes: 34 additions & 0 deletions lib/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const path = require('path');
const fs = require('fs');

const DATA_DIR = path.join(__dirname, '..', 'data');
const SESSIONS_DIR = path.join(DATA_DIR, 'sessions');
const USERS_FILE = path.join(DATA_DIR, 'users.json');
const USERSTATE_FILE = path.join(DATA_DIR, 'userstate.json');
const PLAYLISTS_FILE = path.join(DATA_DIR, 'playlists.json');

for (const dir of [DATA_DIR, SESSIONS_DIR]) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
for (const file of [USERS_FILE, USERSTATE_FILE, PLAYLISTS_FILE]) {
if (!fs.existsSync(file)) {
fs.writeFileSync(file, file === USERS_FILE || file === PLAYLISTS_FILE ? '[]' : '{}');
}
}

function readJSON(file) {
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return file.endsWith('userstate.json') ? {} : []; }
}
function writeJSON(file, data) {
fs.writeFileSync(file, JSON.stringify(data, null, 2));
}

module.exports = {
DATA_DIR, SESSIONS_DIR, USERS_FILE, USERSTATE_FILE, PLAYLISTS_FILE,
getUsers: () => readJSON(USERS_FILE),
saveUsers: u => writeJSON(USERS_FILE, u),
getUserState: () => readJSON(USERSTATE_FILE),
saveUserState: s => writeJSON(USERSTATE_FILE, s),
getPlaylists: () => readJSON(PLAYLISTS_FILE),
savePlaylists: p => writeJSON(PLAYLISTS_FILE, p),
};
19 changes: 19 additions & 0 deletions lib/media.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const path = require('path');
const MEDIA_ROOT = path.resolve(process.env.MEDIA_ROOT || '/media');
const AUDIO_EXTS = new Set(['.mp3', '.flac', '.ogg', '.m4a', '.mp4', '.aac', '.wav', '.opus', '.wma', '.m4b', '.mp4a']);
const VIDEO_EXTS = new Set(['.mkv', '.webm', '.avi', '.mov', '.m4v', '.mpg', '.mpeg', '.wmv']);
const ARTWORK_NAMES = ['folder.jpg', 'folder.jpeg', 'folder.png', 'cover.jpg', 'cover.jpeg', 'cover.png', 'album.jpg', 'album.png'];

function safePath(relPath) {
const normalized = path.normalize(relPath || '');
const full = path.join(MEDIA_ROOT, normalized);
if (!full.startsWith(MEDIA_ROOT)) throw new Error('Path traversal denied');
return full;
}
function canAccess(user, relPath) {
if (user.role === 'admin') return true;
if (!relPath) return true;
return user.allowedPaths.some(p => relPath === p || relPath.startsWith(p + '/'));
}

module.exports = { MEDIA_ROOT, AUDIO_EXTS, VIDEO_EXTS, ARTWORK_NAMES, safePath, canAccess };
22 changes: 22 additions & 0 deletions middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { getUsers } = require('../lib/data.js');

function validUsername(u) { return typeof u === 'string' && /^[a-zA-Z0-9_-]{3,32}$/.test(u); }
function validPassword(p) { return typeof p === 'string' && p.length >= 8; }

function requireAuth(req, res, next) {
if (!req.session.userId) return res.status(401).json({ error: 'Not authenticated' });
const users = getUsers();
const user = users.find(u => u.id === req.session.userId);
if (!user) { req.session.destroy(() => {}); return res.status(401).json({ error: 'Not authenticated' }); }
req.user = user;
next();
}

function requireAdmin(req, res, next) {
requireAuth(req, res, () => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' });
next();
});
}

module.exports = { validUsername, validPassword, requireAuth, requireAdmin };
Loading
Loading