first commit

This commit is contained in:
2025-11-23 20:04:00 +03:00
commit 4c8c468acd
31 changed files with 1380 additions and 0 deletions

View 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');

View 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 };
};

View 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;
};

View 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');
};

View 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);
};