import express from "express"; import fs from "fs"; import path from "path"; import jwt from "jsonwebtoken"; import crypto from "crypto"; const DEFAULT_ACCESS_TTL = process.env.JWT_TTL || "15m"; const DEFAULT_REFRESH_TTL = process.env.JWT_REFRESH_TTL || "30d"; const ITERATIONS = 120000; const KEY_LEN = 64; const DIGEST = "sha512"; function ensureDir(target) { const dir = path.dirname(target); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } function readJsonSafe(filePath, fallback = null) { if (!fs.existsSync(filePath)) return fallback; try { return JSON.parse(fs.readFileSync(filePath, "utf-8")); } catch (err) { console.warn(`⚠️ JSON okunamadı (${filePath}): ${err.message}`); return fallback; } } function writeJsonSafe(filePath, data) { try { ensureDir(filePath); fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); } catch (err) { console.warn(`⚠️ JSON yazılamadı (${filePath}): ${err.message}`); } } function buildPasswordHash(password, salt = crypto.randomBytes(16).toString("hex")) { const hash = crypto .pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST) .toString("hex"); return { hash, salt, iterations: ITERATIONS, keylen: KEY_LEN, digest: DIGEST }; } function verifyPassword(password, user) { if (!user?.password) return false; const { salt, iterations, keylen, digest, hash } = user.password; const candidate = crypto .pbkdf2Sync(password, salt, iterations, keylen, digest) .toString("hex"); return crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(hash)); } function loadSecret(secretPath) { if (process.env.JWT_SECRET) return process.env.JWT_SECRET; if (secretPath && fs.existsSync(secretPath)) { try { return fs.readFileSync(secretPath, "utf-8").trim(); } catch (err) { console.warn(`⚠️ JWT secret okunamadı (${secretPath}): ${err.message}`); } } const generated = crypto.randomBytes(48).toString("hex"); if (secretPath) { try { ensureDir(secretPath); fs.writeFileSync(secretPath, generated, "utf-8"); console.log("🔑 Yeni JWT secret oluşturuldu (diskte saklandı)."); } catch (err) { console.warn(`⚠️ JWT secret yazılamadı (${secretPath}): ${err.message}`); } } return generated; } function loadUsers(usersPath, defaultUser) { let users = readJsonSafe(usersPath, []); if (!Array.isArray(users)) users = []; if (defaultUser?.username && defaultUser?.password) { const exists = users.some((u) => u.username === defaultUser.username); if (!exists) { const password = buildPasswordHash(defaultUser.password); users.push({ username: defaultUser.username, role: defaultUser.role || "admin", password }); writeJsonSafe(usersPath, users); console.log(`👤 Varsayılan kullanıcı eklendi: ${defaultUser.username}`); } } return users; } export function createAuth({ usersPath, secretPath }) { const secret = loadSecret(secretPath); const users = loadUsers(usersPath, { username: process.env.USERNAME || "admin", password: process.env.PASSWORD || "dupe", role: "admin" }); function signToken(payload, opts = {}) { const jwtOpts = { issuer: "dupe", audience: "dupe-clients", expiresIn: opts.expiresIn || DEFAULT_ACCESS_TTL }; // subject zaten payload.sub içinde ise tekrar opsiyonlara eklemeyelim if (!payload.sub && payload.username) { jwtOpts.subject = payload.username; } return jwt.sign(payload, secret, jwtOpts); } function verifyToken(token, expectedType = "access") { try { const decoded = jwt.verify(token, secret, { issuer: "dupe", audience: "dupe-clients" }); if (expectedType && decoded.type !== expectedType) return null; return decoded; } catch (err) { return null; } } function issueTokens(user) { const base = { sub: user.username, role: user.role || "user" }; const accessToken = signToken({ ...base, type: "access" }, { expiresIn: DEFAULT_ACCESS_TTL }); const refreshToken = signToken({ ...base, type: "refresh" }, { expiresIn: DEFAULT_REFRESH_TTL }); return { accessToken, refreshToken }; } function requireAuth(req, res, next) { const header = req.headers.authorization || ""; const bearer = header.startsWith("Bearer ") ? header.slice(7) : null; const token = bearer || req.query.token; if (!token) return res.status(401).json({ error: "Unauthorized" }); const decoded = verifyToken(token, req.path.startsWith("/media/") ? null : "access"); if (!decoded) return res.status(401).json({ error: "Unauthorized" }); req.user = decoded; next(); } function requireRole(role) { return (req, res, next) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const roles = Array.isArray(role) ? role : [role]; if (!roles.includes(req.user.role)) { return res.status(403).json({ error: "Forbidden" }); } next(); }; } function issueMediaToken(targetPath, ttlSeconds = 3600) { const expiresIn = Math.min(Math.max(Number(ttlSeconds) || 3600, 60), 72 * 3600); return signToken( { type: "media", path: targetPath || "*", role: "media" }, { expiresIn } ); } function verifyMediaToken(token, requestedPath) { const decoded = verifyToken(token, "media"); if (!decoded) return null; if (decoded.path && decoded.path !== "*" && requestedPath) { if (!requestedPath.startsWith(decoded.path)) return null; } return decoded; } const router = express.Router(); router.post("/api/login", (req, res) => { const { username, password } = req.body || {}; if (!username || !password) { return res.status(400).json({ error: "username ve password gerekli" }); } const user = users.find((u) => u.username === username); if (!user || !verifyPassword(password, user)) { return res.status(401).json({ error: "Invalid credentials" }); } const { accessToken, refreshToken } = issueTokens(user); res.json({ accessToken, refreshToken, user: { username: user.username, role: user.role || "user" } }); }); router.post("/api/token/refresh", (req, res) => { const { refreshToken } = req.body || {}; if (!refreshToken) { return res.status(400).json({ error: "refreshToken gerekli" }); } const decoded = verifyToken(refreshToken, "refresh"); if (!decoded) return res.status(401).json({ error: "Unauthorized" }); const accessToken = signToken( { sub: decoded.sub, role: decoded.role, type: "access" }, { expiresIn: DEFAULT_ACCESS_TTL } ); res.json({ accessToken }); }); router.get("/api/auth/profile", requireAuth, (req, res) => { res.json({ user: { username: req.user.sub, role: req.user.role } }); }); return { router, requireAuth, requireRole, issueMediaToken, verifyMediaToken, verifyToken }; }