Authorization, favorites and players have been moved to the API server

This commit is contained in:
2025-07-07 18:08:42 +03:00
parent 6bf00451fa
commit 02dedbb8f7
12 changed files with 1735 additions and 5 deletions

209
src/routes/auth.js Normal file
View File

@@ -0,0 +1,209 @@
const { Router } = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const { getDb } = require('../db');
const { sendVerificationEmail } = require('../utils/mailer');
/**
* @swagger
* tags:
* name: auth
* description: Операции авторизации
*/
const router = Router();
// Helper to generate 6-digit code
function generateCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// Register
/**
* @swagger
* /auth/register:
* post:
* tags: [auth]
* summary: Регистрация пользователя
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* password:
* type: string
* name:
* type: string
* responses:
* 200:
* description: OK
*/
router.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
const db = await getDb();
const existing = await db.collection('users').findOne({ email });
if (existing) return res.status(400).json({ error: 'Email already registered' });
const hashed = await bcrypt.hash(password, 12);
const code = generateCode();
const codeExpires = new Date(Date.now() + 10 * 60 * 1000);
await db.collection('users').insertOne({
email,
password: hashed,
name: name || email,
verified: false,
verificationCode: code,
verificationExpires: codeExpires,
isAdmin: false,
adminVerified: false,
createdAt: new Date()
});
await sendVerificationEmail(email, code);
res.json({ success: true, message: 'Registered. Check email for code.' });
} catch (err) {
console.error('Register error:', err);
res.status(500).json({ error: 'Registration failed' });
}
});
// Verify email
/**
* @swagger
* /auth/verify:
* post:
* tags: [auth]
* summary: Подтверждение email
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* code:
* type: string
* responses:
* 200:
* description: OK
*/
router.post('/verify', async (req, res) => {
try {
const { email, code } = req.body;
const db = await getDb();
const user = await db.collection('users').findOne({ email });
if (!user) return res.status(400).json({ error: 'User not found' });
if (user.verified) return res.json({ success: true, message: 'Already verified' });
if (user.verificationCode !== code || user.verificationExpires < new Date()) {
return res.status(400).json({ error: 'Invalid or expired code' });
}
await db.collection('users').updateOne({ email }, { $set: { verified: true }, $unset: { verificationCode: '', verificationExpires: '' } });
res.json({ success: true });
} catch (err) {
console.error('Verify error:', err);
res.status(500).json({ error: 'Verification failed' });
}
});
// Resend code
/**
* @swagger
* /auth/resend-code:
* post:
* tags: [auth]
* summary: Повторная отправка кода подтверждения
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* responses:
* 200:
* description: OK
*/
router.post('/resend-code', async (req, res) => {
try {
const { email } = req.body;
const db = await getDb();
const user = await db.collection('users').findOne({ email });
if (!user) return res.status(400).json({ error: 'User not found' });
const code = generateCode();
const codeExpires = new Date(Date.now() + 10 * 60 * 1000);
await db.collection('users').updateOne({ email }, { $set: { verificationCode: code, verificationExpires: codeExpires } });
await sendVerificationEmail(email, code);
res.json({ success: true });
} catch (err) {
console.error('Resend code error:', err);
res.status(500).json({ error: 'Failed to resend code' });
}
});
// Login
/**
* @swagger
* /auth/login:
* post:
* tags: [auth]
* summary: Логин пользователя
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* password:
* type: string
*
* responses:
* 200:
* description: JWT token
*/
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const db = await getDb();
const user = await db.collection('users').findOne({ email });
if (!user) return res.status(400).json({ error: 'User not found' });
if (!user.verified) {
return res.status(403).json({ error: 'Account not activated. Please verify your email.' });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) return res.status(400).json({ error: 'Invalid password' });
const payload = {
id: user._id.toString(),
email: user.email,
name: user.name || '',
verified: user.verified,
isAdmin: user.isAdmin,
adminVerified: user.adminVerified
};
const secret = process.env.JWT_SECRET || process.env.jwt_secret;
const token = jwt.sign(payload, secret, { expiresIn: '7d', jwtid: uuidv4() });
res.json({ token, user: { name: user.name || '', email: user.email } });
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ error: 'Login failed' });
}
});
module.exports = router;

152
src/routes/favorites.js Normal file
View File

@@ -0,0 +1,152 @@
const { Router } = require('express');
const { getDb } = require('../db');
const authRequired = require('../middleware/auth');
/**
* @swagger
* tags:
* name: favorites
* description: Операции с избранным
*/
const router = Router();
// Apply auth middleware to all favorites routes
router.use(authRequired);
/**
* @swagger
* /favorites:
* get:
* tags: [favorites]
* summary: Получить список избранного пользователя
* security:
* - bearerAuth: []
* responses:
* 200:
* description: OK
*/
router.get('/', async (req, res) => {
try {
const db = await getDb();
const userId = req.user.email || req.user.id;
const items = await db
.collection('favorites')
.find({ userId })
.toArray();
res.json(items);
} catch (err) {
console.error('Get favorites error:', err);
res.status(500).json({ error: 'Failed to fetch favorites' });
}
});
/**
* @swagger
* /favorites/check/{mediaId}:
* get:
* tags: [favorites]
* summary: Проверить, находится ли элемент в избранном
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: mediaId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get('/check/:mediaId', async (req, res) => {
try {
const { mediaId } = req.params;
const db = await getDb();
const exists = await db
.collection('favorites')
.findOne({ userId: req.user.email || req.user.id, mediaId });
res.json({ exists: !!exists });
} catch (err) {
console.error('Check favorite error:', err);
res.status(500).json({ error: 'Failed to check favorite' });
}
});
/**
* @swagger
* /favorites/{mediaId}:
* post:
* tags: [favorites]
* summary: Добавить элемент в избранное
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: mediaId
* required: true
* schema:
* type: string
* - in: query
* name: mediaType
* required: true
* schema:
* type: string
* enum: [movie, tv]
* responses:
* 200:
* description: OK
*/
router.post('/:mediaId', async (req, res) => {
try {
const { mediaId } = req.params;
const { mediaType } = req.query;
if (!mediaType) return res.status(400).json({ error: 'mediaType required' });
const db = await getDb();
await db.collection('favorites').insertOne({
userId: req.user.email || req.user.id,
mediaId,
mediaType,
createdAt: new Date()
});
res.json({ success: true });
} catch (err) {
if (err.code === 11000) {
return res.status(409).json({ error: 'Already in favorites' });
}
console.error('Add favorite error:', err);
res.status(500).json({ error: 'Failed to add favorite' });
}
});
/**
* @swagger
* /favorites/{mediaId}:
* delete:
* tags: [favorites]
* summary: Удалить элемент из избранного
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: mediaId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.delete('/:mediaId', async (req, res) => {
try {
const { mediaId } = req.params;
const db = await getDb();
await db.collection('favorites').deleteOne({ userId: req.user.email || req.user.id, mediaId });
res.json({ success: true });
} catch (err) {
console.error('Delete favorite error:', err);
res.status(500).json({ error: 'Failed to delete favorite' });
}
});
module.exports = router;

110
src/routes/players.js Normal file
View File

@@ -0,0 +1,110 @@
const { Router } = require('express');
const fetch = require('node-fetch');
const router = Router();
/**
* @swagger
* tags:
* name: players
* description: Плееры Alloha и Lumex
*/
/**
* @swagger
* /players/alloha:
* get:
* tags: [players]
* summary: Получить iframe от Alloha по IMDb ID или TMDB ID
* parameters:
* - in: query
* name: imdb_id
* schema:
* type: string
* description: IMDb ID (например tt0111161)
* - in: query
* name: tmdb_id
* schema:
* type: string
* description: TMDB ID (числовой)
* responses:
* 200:
* description: OK
*/
router.get('/alloha', async (req, res) => {
try {
const { imdb_id: imdbId, tmdb_id: tmdbId } = req.query;
if (!imdbId && !tmdbId) {
return res.status(400).json({ error: 'imdb_id or tmdb_id query param is required' });
}
const token = process.env.ALLOHA_TOKEN;
if (!token) {
return res.status(500).json({ error: 'Server misconfiguration: ALLOHA_TOKEN missing' });
}
const idParam = imdbId ? `imdb=${encodeURIComponent(imdbId)}` : `tmdb=${encodeURIComponent(tmdbId)}`;
const apiUrl = `https://api.alloha.tv/?token=${token}&${idParam}`;
const apiRes = await fetch(apiUrl);
if (!apiRes.ok) {
console.error('Alloha response error', apiRes.status);
return res.status(apiRes.status).json({ error: 'Failed to fetch from Alloha' });
}
const json = await apiRes.json();
if (json.status !== 'success' || !json.data?.iframe) {
return res.status(404).json({ error: 'Video not found' });
}
let iframeCode = json.data.iframe;
// If Alloha returns just a URL, wrap it in an iframe
if (!iframeCode.includes('<')) {
iframeCode = `<iframe src="${iframeCode}" allowfullscreen style="border:none;width:100%;height:100%"></iframe>`;
}
// If iframe markup already provided
const htmlDoc = `<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Alloha Player</title><style>html,body{margin:0;height:100%;}</style></head><body>${iframeCode}</body></html>`;
res.set('Content-Type', 'text/html');
return res.send(htmlDoc);
} catch (e) {
console.error('Alloha route error:', e);
res.status(500).json({ error: 'Internal Server Error' });
}
});
/**
* @swagger
* /players/lumex:
* get:
* tags: [players]
* summary: Получить URL плеера Lumex
* parameters:
* - in: query
* name: imdb_id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get('/lumex', (req, res) => {
try {
const { imdb_id: imdbId } = req.query;
if (!imdbId) return res.status(400).json({ error: 'imdb_id required' });
const baseUrl = process.env.LUMEX_URL || process.env.NEXT_PUBLIC_LUMEX_URL;
if (!baseUrl) return res.status(500).json({ error: 'Server misconfiguration: LUMEX_URL missing' });
const url = `${baseUrl}?imdb_id=${encodeURIComponent(imdbId)}`;
const iframe = `<iframe src=\"${url}\" allowfullscreen loading=\"lazy\" style=\"border:none;width:100%;height:100%;\"></iframe>`;
const htmlDoc = `<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Lumex Player</title><style>html,body{margin:0;height:100%;}</style></head><body>${iframe}</body></html>`;
res.set('Content-Type', 'text/html');
res.send(htmlDoc);
} catch (e) {
console.error('Lumex route error:', e);
res.status(500).json({ error: 'Internal Server Error' });
}
});
module.exports = router;