mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-28 01:48:51 +05:00
Authorization, favorites and players have been moved to the API server
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
.env.local
|
||||
node_modules
|
||||
1048
package-lock.json
generated
1048
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,12 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongodb": "^6.5.0",
|
||||
"nodemailer": "^6.9.9",
|
||||
"uuid": "^9.0.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"vercel": "^39.3.0"
|
||||
},
|
||||
|
||||
28
src/db.js
Normal file
28
src/db.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
const uri = process.env.MONGODB_URI || process.env.mongodb_uri || process.env.MONGO_URI;
|
||||
|
||||
if (!uri) {
|
||||
throw new Error('MONGODB_URI environment variable is not set');
|
||||
}
|
||||
|
||||
let client;
|
||||
let clientPromise;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!global._mongoClientPromise) {
|
||||
client = new MongoClient(uri);
|
||||
global._mongoClientPromise = client.connect();
|
||||
}
|
||||
clientPromise = global._mongoClientPromise;
|
||||
} else {
|
||||
client = new MongoClient(uri);
|
||||
clientPromise = client.connect();
|
||||
}
|
||||
|
||||
async function getDb() {
|
||||
const _client = await clientPromise;
|
||||
return _client.db();
|
||||
}
|
||||
|
||||
module.exports = { getDb };
|
||||
27
src/index.js
27
src/index.js
@@ -33,6 +33,7 @@ const swaggerOptions = {
|
||||
description: process.env.NODE_ENV === 'production' ? 'Production server' : 'Development server'
|
||||
}
|
||||
],
|
||||
security: [{ bearerAuth: [] }],
|
||||
tags: [
|
||||
{
|
||||
name: 'movies',
|
||||
@@ -45,9 +46,28 @@ const swaggerOptions = {
|
||||
{
|
||||
name: 'health',
|
||||
description: 'Проверка работоспособности API'
|
||||
},
|
||||
{
|
||||
name: 'auth',
|
||||
description: 'Операции авторизации'
|
||||
},
|
||||
{
|
||||
name: 'favorites',
|
||||
description: 'Операции с избранным'
|
||||
},
|
||||
{
|
||||
name: 'players',
|
||||
description: 'Плееры Alloha и Lumex'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
Movie: {
|
||||
type: 'object',
|
||||
@@ -226,11 +246,18 @@ const moviesRouter = require('./routes/movies');
|
||||
const tvRouter = require('./routes/tv');
|
||||
const imagesRouter = require('./routes/images');
|
||||
const categoriesRouter = require('./routes/categories');
|
||||
const favoritesRouter = require('./routes/favorites');
|
||||
const playersRouter = require('./routes/players');
|
||||
require('./utils/cleanup');
|
||||
const authRouter = require('./routes/auth');
|
||||
|
||||
app.use('/movies', moviesRouter);
|
||||
app.use('/tv', tvRouter);
|
||||
app.use('/images', imagesRouter);
|
||||
app.use('/categories', categoriesRouter);
|
||||
app.use('/favorites', favoritesRouter);
|
||||
app.use('/players', playersRouter);
|
||||
app.use('/auth', authRouter);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
|
||||
35
src/middleware/auth.js
Normal file
35
src/middleware/auth.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
/**
|
||||
* Express middleware to protect routes with JWT authentication.
|
||||
* Attaches the decoded token to req.user on success.
|
||||
*/
|
||||
function authRequired(req, res, next) {
|
||||
try {
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({ error: 'Authorization header missing' });
|
||||
}
|
||||
|
||||
const parts = authHeader.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return res.status(401).json({ error: 'Invalid Authorization header format' });
|
||||
}
|
||||
|
||||
const token = parts[1];
|
||||
const secret = process.env.JWT_SECRET || process.env.jwt_secret;
|
||||
if (!secret) {
|
||||
console.error('JWT_SECRET not set');
|
||||
return res.status(500).json({ error: 'Server configuration error' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, secret);
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error('JWT auth error:', err);
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = authRequired;
|
||||
209
src/routes/auth.js
Normal file
209
src/routes/auth.js
Normal 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
152
src/routes/favorites.js
Normal 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
110
src/routes/players.js
Normal 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;
|
||||
54
src/utils/adblock.js
Normal file
54
src/utils/adblock.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
// Lazy-loaded in-memory set of ad domains
|
||||
let adDomains = null;
|
||||
|
||||
function loadAdDomains() {
|
||||
if (adDomains) return adDomains;
|
||||
adDomains = new Set();
|
||||
try {
|
||||
const listPath = path.join(__dirname, '..', '..', 'easylist.txt');
|
||||
const data = fs.readFileSync(listPath, 'utf8');
|
||||
const lines = data.split('\n');
|
||||
const domainRegex = /^\|\|([^\/^]+)\^/; // matches ||domain.com^
|
||||
for (const line of lines) {
|
||||
const m = domainRegex.exec(line.trim());
|
||||
if (m) {
|
||||
adDomains.add(m[1].replace(/^www\./, ''));
|
||||
}
|
||||
}
|
||||
console.log(`Adblock: loaded ${adDomains.size} domains from easylist.txt`);
|
||||
} catch (e) {
|
||||
console.error('Adblock: failed to load easylist.txt', e);
|
||||
adDomains = new Set();
|
||||
}
|
||||
return adDomains;
|
||||
}
|
||||
|
||||
function cleanHtml(html) {
|
||||
const domains = loadAdDomains();
|
||||
const $ = cheerio.load(html);
|
||||
const removed = [];
|
||||
$('script[src], iframe[src], img[src], link[href]').each((_, el) => {
|
||||
const attr = $(el).attr('src') || $(el).attr('href');
|
||||
if (!attr) return;
|
||||
try {
|
||||
const host = new URL(attr, 'https://dummy-base/').hostname.replace(/^www\./, '');
|
||||
if (domains.has(host)) {
|
||||
removed.push(host);
|
||||
$(el).remove();
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore invalid URLs
|
||||
}
|
||||
});
|
||||
if (removed.length) {
|
||||
const unique = [...new Set(removed)];
|
||||
console.log(`Adblock removed resources from: ${unique.join(', ')}`);
|
||||
}
|
||||
return $.html();
|
||||
}
|
||||
|
||||
module.exports = { cleanHtml };
|
||||
23
src/utils/cleanup.js
Normal file
23
src/utils/cleanup.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const { getDb } = require('../db');
|
||||
|
||||
// Delete unverified users older than 7 days
|
||||
async function deleteStaleUsers() {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
const result = await db.collection('users').deleteMany({ verified: false, createdAt: { $lt: weekAgo } });
|
||||
if (result.deletedCount) {
|
||||
console.log(`Cleanup: removed ${result.deletedCount} stale unverified users`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Cleanup error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// run once at startup and then every 24h
|
||||
(async () => {
|
||||
await deleteStaleUsers();
|
||||
setInterval(deleteStaleUsers, 24 * 60 * 60 * 1000);
|
||||
})();
|
||||
|
||||
module.exports = { deleteStaleUsers };
|
||||
45
src/utils/mailer.js
Normal file
45
src/utils/mailer.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: process.env.GMAIL_USER || process.env.gmail_user,
|
||||
pass: process.env.GMAIL_APP_PASSWORD || process.env.gmail_app_password
|
||||
}
|
||||
});
|
||||
|
||||
async function sendVerificationEmail(to, code) {
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: process.env.GMAIL_USER || process.env.gmail_user,
|
||||
to,
|
||||
subject: 'Подтверждение регистрации Neo Movies',
|
||||
html: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h1 style="color: #2196f3;">Neo Movies</h1>
|
||||
<p>Здравствуйте!</p>
|
||||
<p>Для завершения регистрации введите этот код:</p>
|
||||
<div style="
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
letter-spacing: 4px;
|
||||
margin: 20px 0;
|
||||
">
|
||||
${code}
|
||||
</div>
|
||||
<p>Код действителен в течение 10 минут.</p>
|
||||
<p>Если вы не регистрировались на нашем сайте, просто проигнорируйте это письмо.</p>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error sending verification email:', err);
|
||||
return { error: 'Failed to send email' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { sendVerificationEmail };
|
||||
Reference in New Issue
Block a user