Files
dupe/server/modules/auth.js

222 lines
6.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
};
}