first commit
This commit is contained in:
14
src/services/adminLock.service.js
Normal file
14
src/services/adminLock.service.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
export const isEnvAdmin = (user) => {
|
||||
if (!env.adminRoleLock) return false;
|
||||
if (!user) return false;
|
||||
return (
|
||||
Boolean(env.adminEmail) &&
|
||||
Boolean(env.adminUsername) &&
|
||||
user.email === env.adminEmail &&
|
||||
user.username === env.adminUsername
|
||||
);
|
||||
};
|
||||
|
||||
export const getEffectiveRole = (user) => (isEnvAdmin(user) ? 'admin' : 'user');
|
||||
90
src/services/auth.service.js
Normal file
90
src/services/auth.service.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { supabase } from '../config/supabase.js';
|
||||
import { comparePassword, hashPassword } from '../utils/crypto.js';
|
||||
import { createSession } from './session.service.js';
|
||||
import { getEffectiveRole } from './adminLock.service.js';
|
||||
|
||||
const USERS_TABLE = 'users';
|
||||
|
||||
const buildError = (status, code, message, details) => {
|
||||
const err = new Error(message);
|
||||
err.status = status;
|
||||
err.code = code;
|
||||
if (details) err.details = details;
|
||||
return err;
|
||||
};
|
||||
|
||||
const normalizeUser = (record) => {
|
||||
if (!record) return null;
|
||||
return {
|
||||
id: record.id,
|
||||
email: record.email,
|
||||
name: record.name,
|
||||
username: record.username,
|
||||
role: record.role || 'user'
|
||||
};
|
||||
};
|
||||
|
||||
const fetchExistingUser = async (email, username) => {
|
||||
const { data, error } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.select('id,email,username')
|
||||
.or(`email.eq.${email},username.eq.${username}`);
|
||||
if (error) throw buildError(500, 'SUPABASE_ERROR', 'Kullanıcı kontrolü başarısız', error.message);
|
||||
return data;
|
||||
};
|
||||
|
||||
const fetchByEmailOrUsername = async (emailOrUsername) => {
|
||||
const { data, error } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.select('id,email,name,username,password_hash,role')
|
||||
.or(`email.eq.${emailOrUsername},username.eq.${emailOrUsername}`)
|
||||
.single();
|
||||
if (error) {
|
||||
if (error.code === 'PGRST116') {
|
||||
// kayıt bulunamadı
|
||||
return null;
|
||||
}
|
||||
throw buildError(500, 'SUPABASE_ERROR', 'Kullanıcı sorgusu başarısız', error.message);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const registerUser = async ({ email, name, username, password, userAgent, ip }) => {
|
||||
const existing = await fetchExistingUser(email, username);
|
||||
if (existing && existing.length > 0) {
|
||||
throw buildError(400, 'USER_EXISTS', 'Email veya kullanıcı adı zaten kullanılıyor');
|
||||
}
|
||||
|
||||
const password_hash = await hashPassword(password);
|
||||
const { data, error } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.insert([{ email, name, username, password_hash, role: 'user' }])
|
||||
.select('id,email,name,username,role')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw buildError(500, 'SUPABASE_ERROR', 'Kullanıcı oluşturulamadı', error.message);
|
||||
}
|
||||
|
||||
const user = normalizeUser(data);
|
||||
const { token, cookieOptions, jti, roleEffective } = await createSession(user, { userAgent, ip });
|
||||
|
||||
return { user: { ...user, roleEffective }, token, cookieOptions, jti };
|
||||
};
|
||||
|
||||
export const loginUser = async ({ emailOrUsername, password, userAgent, ip }) => {
|
||||
const record = await fetchByEmailOrUsername(emailOrUsername);
|
||||
if (!record) {
|
||||
throw buildError(401, 'INVALID_CREDENTIALS', 'Kullanıcı bulunamadı veya parola hatalı');
|
||||
}
|
||||
|
||||
const passwordOk = await comparePassword(password, record.password_hash);
|
||||
if (!passwordOk) {
|
||||
throw buildError(401, 'INVALID_CREDENTIALS', 'Kullanıcı bulunamadı veya parola hatalı');
|
||||
}
|
||||
|
||||
const user = normalizeUser(record);
|
||||
const roleEffective = getEffectiveRole(user);
|
||||
const { token, cookieOptions, jti } = await createSession(user, { userAgent, ip });
|
||||
return { user: { ...user, roleEffective }, token, cookieOptions, jti };
|
||||
};
|
||||
39
src/services/mediaData.service.js
Normal file
39
src/services/mediaData.service.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { supabase } from '../config/supabase.js';
|
||||
|
||||
const TABLE = 'media_data';
|
||||
|
||||
const buildError = (status, code, message, details) => {
|
||||
const err = new Error(message);
|
||||
err.status = status;
|
||||
err.code = code;
|
||||
if (details) err.details = details;
|
||||
return err;
|
||||
};
|
||||
|
||||
export const saveMediaData = async ({
|
||||
media_id,
|
||||
media_name,
|
||||
year,
|
||||
type,
|
||||
thumbnail_url,
|
||||
info,
|
||||
genre,
|
||||
media_provider
|
||||
}) => {
|
||||
const payload = {
|
||||
media_id,
|
||||
media_name,
|
||||
year,
|
||||
type,
|
||||
thumbnail_url,
|
||||
info,
|
||||
genre,
|
||||
media_provider
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from(TABLE).insert([payload]).select('*').single();
|
||||
if (error) {
|
||||
throw buildError(500, 'SUPABASE_ERROR', 'Media kaydedilemedi', error.message);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
69
src/services/meta.service.js
Normal file
69
src/services/meta.service.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { scraperNetflix, scraperPrime } from 'metascraper';
|
||||
import net from 'net';
|
||||
|
||||
const ALLOWED_HOSTS = ['netflix.com', 'www.netflix.com', 'primevideo.com', 'www.primevideo.com'];
|
||||
const MAX_URL_LENGTH = 2048;
|
||||
|
||||
const buildError = (status, code, message) => {
|
||||
const err = new Error(message);
|
||||
err.status = status;
|
||||
err.code = code;
|
||||
return err;
|
||||
};
|
||||
|
||||
const getHost = (urlString) => {
|
||||
if (!urlString || urlString.length > MAX_URL_LENGTH) return null;
|
||||
try {
|
||||
const parsed = new URL(urlString);
|
||||
return parsed.hostname;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isIpHost = (hostname) => Boolean(net.isIP(hostname));
|
||||
|
||||
const isLocalhost = (hostname) =>
|
||||
['localhost', '127.0.0.1', '::1'].includes(hostname.toLowerCase());
|
||||
|
||||
const isAllowedHost = (hostname) => ALLOWED_HOSTS.includes(hostname.toLowerCase());
|
||||
|
||||
const isAllowedProtocol = (urlString) => {
|
||||
try {
|
||||
const parsed = new URL(urlString);
|
||||
return ['https:', 'http:'].includes(parsed.protocol);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const scrapeMedia = async (url) => {
|
||||
const hostname = getHost(url);
|
||||
if (!hostname) {
|
||||
throw buildError(400, 'INVALID_URL', 'Geçersiz URL');
|
||||
}
|
||||
|
||||
if (!isAllowedProtocol(url)) {
|
||||
throw buildError(400, 'INVALID_PROTOCOL', 'Sadece http/https desteklenir');
|
||||
}
|
||||
|
||||
if (isIpHost(hostname) || isLocalhost(hostname)) {
|
||||
throw buildError(400, 'UNSUPPORTED_DOMAIN', 'Desteklenmeyen domain');
|
||||
}
|
||||
|
||||
if (!isAllowedHost(hostname)) {
|
||||
throw buildError(400, 'UNSUPPORTED_DOMAIN', 'Desteklenmeyen domain');
|
||||
}
|
||||
|
||||
if (hostname.endsWith('netflix.com')) {
|
||||
const data = await scraperNetflix(url);
|
||||
return { provider: 'netflix', data };
|
||||
}
|
||||
|
||||
if (hostname.endsWith('primevideo.com')) {
|
||||
const data = await scraperPrime(url);
|
||||
return { provider: 'primevideo', data };
|
||||
}
|
||||
|
||||
throw buildError(400, 'UNSUPPORTED_DOMAIN', 'Desteklenmeyen domain');
|
||||
};
|
||||
74
src/services/session.service.js
Normal file
74
src/services/session.service.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import crypto from 'crypto';
|
||||
import { redis } from '../config/redis.js';
|
||||
import { env } from '../config/env.js';
|
||||
import { signToken, expiresInToMs } from '../utils/crypto.js';
|
||||
import { getEffectiveRole } from './adminLock.service.js';
|
||||
|
||||
const SESSION_PREFIX = 'session:';
|
||||
|
||||
const sessionKey = (jti) => `${SESSION_PREFIX}${jti}`;
|
||||
|
||||
export const createSession = async (user, { userAgent, ip } = {}) => {
|
||||
const jti = crypto.randomUUID();
|
||||
const issuedAt = Date.now();
|
||||
const maxAgeMs = expiresInToMs(env.jwtExpiresIn);
|
||||
const expiresAt = maxAgeMs ? issuedAt + maxAgeMs : undefined;
|
||||
const roleEffective = getEffectiveRole(user);
|
||||
|
||||
const token = signToken(
|
||||
{
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
roleEffective
|
||||
},
|
||||
{ jwtid: jti }
|
||||
);
|
||||
|
||||
const sessionData = {
|
||||
userId: user.id,
|
||||
issuedAt,
|
||||
expiresAt,
|
||||
userAgent,
|
||||
ip
|
||||
};
|
||||
|
||||
const ttlSeconds = maxAgeMs ? Math.ceil(maxAgeMs / 1000) : undefined;
|
||||
if (ttlSeconds) {
|
||||
await redis.set(sessionKey(jti), JSON.stringify(sessionData), 'EX', ttlSeconds);
|
||||
} else {
|
||||
await redis.set(sessionKey(jti), JSON.stringify(sessionData));
|
||||
}
|
||||
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: env.cookieSecure || env.nodeEnv === 'production',
|
||||
...(maxAgeMs ? { maxAge: maxAgeMs } : {})
|
||||
};
|
||||
|
||||
return { token, cookieOptions, jti, roleEffective, sessionData };
|
||||
};
|
||||
|
||||
export const getSession = async (jti) => {
|
||||
if (!jti) return null;
|
||||
const raw = await redis.get(sessionKey(jti));
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeSession = async (jti) => {
|
||||
if (!jti) return;
|
||||
await redis.del(sessionKey(jti));
|
||||
};
|
||||
|
||||
export const refreshSession = async (jti) => {
|
||||
const maxAgeMs = expiresInToMs(env.jwtExpiresIn);
|
||||
if (!maxAgeMs || !jti) return;
|
||||
const ttlSeconds = Math.ceil(maxAgeMs / 1000);
|
||||
await redis.expire(sessionKey(jti), ttlSeconds);
|
||||
};
|
||||
Reference in New Issue
Block a user