251004 frontend backend

This commit is contained in:
admin
2025-10-04 10:56:56 +07:00
parent 10150583cc
commit a70ad11035
1186 changed files with 700 additions and 272 deletions

View File

@@ -2,6 +2,7 @@
import fs from "node:fs";
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser"; // added
import sql from "./db/index.js";
import healthRouter from "./routes/health.js";
@@ -75,6 +76,10 @@ app.use(
exposedHeaders: ["Content-Disposition", "Content-Length"],
})
);
// parse cookies สำหรับ access_token / refresh_token
app.use(cookieParser()); // added
app.options(
"*",
cors({

View File

@@ -1,9 +1,4 @@
// FILE: src/middleware/auth.js
// Authentication & Authorization middleware
// - JWT-based authentication
// - Role & Permission enrichment
// - RBAC (Role-Based Access Control) helpers
// - Requires User, Role, Permission, UserRole, RolePermission models
// FILE: backend/src/middleware/auth.js
import jwt from "jsonwebtoken";
import { config } from "../config.js";
@@ -20,10 +15,17 @@ export function signRefreshToken(payload) {
});
}
export function extractToken(req) {
// ให้คุกกี้มาก่อน แล้วค่อย Bearer (รองรับทั้งสองทาง)
const cookieTok = req.cookies?.access_token || null;
if (cookieTok) return cookieTok;
const hdr = req.headers.authorization || "";
return hdr.startsWith("Bearer ") ? hdr.slice(7) : null;
}
export function requireAuth(req, res, next) {
if (req.path === "/health") return next(); // อนุญาต health เสมอ
const hdr = req.headers.authorization || "";
const token = hdr.startsWith("Bearer ") ? hdr.slice(7) : null;
const token = extractToken(req);
if (!token) return res.status(401).json({ error: "Missing token" });
try {
@@ -33,6 +35,15 @@ export function requireAuth(req, res, next) {
return res.status(401).json({ error: "Invalid/Expired token" });
}
}
// ใช้กับเส้นทางที่ login แล้วจะ enrich ต่อได้ แต่ไม่บังคับ
export function optionalAuth(req, _res, next) {
const token = extractToken(req);
if (!token) return next();
try {
req.user = jwt.verify(token, config.JWT.SECRET);
} catch {}
next();
}
export async function enrichRoles(req, _res, next) {
if (!req.user?.user_id) return next();

View File

@@ -0,0 +1,279 @@
// FILE: backend/src/routes/auth.js (ESM) — Bearer only, refresh via header/body, forgot/reset password
import { Router } from "express";
import jwt from "jsonwebtoken";
import sql from "../db/index.js";
import { cookieOpts } from "../utils/cookie.js";
import bcrypt from "bcryptjs";
import { requireAuth } from "../middleware/auth.js";
import crypto from "node:crypto";
const r = Router();
/* =========================
* CONFIG & HELPERS
* ========================= */
// ใช้ค่าเดียวกับ middleware authJwt()
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret";
const REFRESH_SECRET = process.env.REFRESH_SECRET || "dev-refresh-secret";
const ACCESS_TTL = process.env.ACCESS_TTL || "30m";
const REFRESH_TTL = process.env.REFRESH_TTL || "30d";
// อายุของ reset token (นาที)
const RESET_TTL_MIN = Number(process.env.RESET_TTL_MIN || 30);
function signAccessToken(user) {
return jwt.sign(
{ user_id: user.user_id, username: user.username },
JWT_SECRET,
{ expiresIn: ACCESS_TTL, issuer: "dms-backend" }
);
}
function signRefreshToken(user) {
return jwt.sign(
{ user_id: user.user_id, username: user.username, t: "refresh" },
REFRESH_SECRET,
{ expiresIn: REFRESH_TTL, issuer: "dms-backend" }
);
}
function getBearer(req) {
const h = req.headers.authorization || "";
if (!h.startsWith("Bearer ")) return null;
const token = h.slice(7).trim();
return token || null;
}
async function findUserByUsername(username) {
const [rows] = await sql.query(
`SELECT user_id, username, email, first_name, last_name, password_hash
FROM users WHERE username=? LIMIT 1`,
[username]
);
return rows?.[0] || null;
}
async function findUserByEmail(email) {
const [rows] = await sql.query(
`SELECT user_id, username, email, first_name, last_name, password_hash
FROM users WHERE email=? LIMIT 1`,
[email]
);
return rows?.[0] || null;
}
/* =========================
* POST /api/auth/login
* - รับ username/password
* - ตรวจ bcrypt แล้วออก token+refresh_token (JSON)
* ========================= */
r.post("/login", async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ error: "USERNAME_PASSWORD_REQUIRED" });
}
const user = await findUserByUsername(username);
if (!user) return res.status(401).json({ error: "INVALID_CREDENTIALS" });
const ok = await bcrypt.compare(password, user.password_hash || "");
if (!ok) return res.status(401).json({ error: "INVALID_CREDENTIALS" });
const token = signAccessToken(user);
const refresh_token = signRefreshToken(user);
// set httpOnly cookies (ยังคงส่ง token ใน body กลับเช่นเดิม)
res.cookie(
"access_token",
token,
cookieOpts(parseInt(process.env.ACCESS_TTL_MS || "900000", 10))
);
res.cookie(
"refresh_token",
refresh_token,
cookieOpts(parseInt(process.env.REFRESH_TTL_MS || "604800000", 10))
);
return res.json({
token,
refresh_token,
user: {
user_id: user.user_id,
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
},
});
});
/* =========================
* GET /api/auth/me (cookie or bearer)
* ========================= */
r.get("/me", requireAuth, async (req, res) => {
return res.json({
ok: true,
user: { user_id: req.user.user_id, username: req.user.username },
});
});
+(
/* =========================
* POST /api/auth/refresh
* - รองรับ refresh token จาก:
* 1) Authorization: Bearer <refresh_token>
* 2) req.body.refresh_token
* - ออก token ใหม่ + refresh ใหม่ (rotation)
* ========================= */
r.post("/refresh", async (req, res) => {
const fromHeader = getBearer(req);
const fromBody = (req.body || {}).refresh_token;
const refreshToken = fromHeader || fromBody;
if (!refreshToken) {
return res.status(400).json({ error: "REFRESH_TOKEN_REQUIRED" });
}
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET, {
issuer: "dms-backend",
});
if (payload.t !== "refresh") throw new Error("bad token type");
const [[user]] = await sql.query(
`SELECT user_id, username, email, first_name, last_name
FROM users WHERE user_id=? LIMIT 1`,
[payload.user_id]
);
if (!user) return res.status(401).json({ error: "USER_NOT_FOUND" });
// rotation
const token = signAccessToken(user);
const new_refresh = signRefreshToken(user);
// rotate cookies
res.cookie(
"access_token",
token,
cookieOpts(parseInt(process.env.ACCESS_TTL_MS || "900000", 10))
);
res.cookie(
"refresh_token",
new_refresh,
cookieOpts(parseInt(process.env.REFRESH_TTL_MS || "604800000", 10))
);
return res.json({
token,
refresh_token: new_refresh,
user: {
user_id: user.user_id,
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
},
});
} catch {
return res.status(401).json({ error: "INVALID_REFRESH_TOKEN" });
}
})
);
/* =========================
* POST /api/auth/forgot-password
* - รับ username หรือ email อย่างใดอย่างหนึ่ง
* - สร้าง reset token แบบสุ่ม, เก็บ hash ใน DB พร้อมหมดอายุ
* - ส่งเสมอ {ok:true} เพื่อลด user enumeration
* - การ “ส่งอีเมล/ลิงก์รีเซ็ต” ให้ทำนอกระบบนี้ (เช่น n8n)
* ========================= */
r.post("/forgot-password", async (req, res) => {
const { username, email } = req.body || {};
// หา user จาก username หรือ email (ถ้ามีทั้งสอง จะให้ username มาก่อน)
let user = null;
if (username) user = await findUserByUsername(username);
if (!user && email) user = await findUserByEmail(email);
// สร้างโทเคน “เหมือนจริง” เสมอ (แต่ถ้าไม่เจอ user ก็ไม่บอก)
if (user) {
const raw = crypto.randomBytes(32).toString("hex"); // โทเคนดิบ (ส่งทางอีเมล)
const hash = crypto.createHash("sha256").update(raw).digest("hex"); // เก็บใน DB
const expires = new Date(Date.now() + RESET_TTL_MIN * 60 * 1000);
// ทำ invalid เก่า ๆ ของ user นี้ (optional)
await sql.query(
`UPDATE password_resets SET used_at=NOW()
WHERE user_id=? AND used_at IS NULL AND expires_at < NOW()`,
[user.user_id]
);
// บันทึก token ใหม่
await sql.query(
`INSERT INTO password_resets (user_id, token_hash, expires_at)
VALUES (?,?,?)`,
[user.user_id, hash, expires]
);
// TODO: ส่ง “raw token” ไปช่องทางปลอดภัย (เช่น n8n ส่งอีเมล)
// ตัวอย่างลิงก์ที่ frontend จะใช้:
// https://<frontend-domain>/reset-password?token=<raw>
// คุณสามารถต่อ webhook ไป n8n ได้ที่นี่ถ้าต้องการ
}
// ไม่บอกว่าเจอหรือไม่เจอ user
return res.json({ ok: true });
});
/* =========================
* POST /api/auth/reset-password
* - รับ token (จากลิงก์ในอีเมล) + new_password
* - ตรวจ token_hash กับ DB, หมดอายุหรือถูกใช้ไปแล้วหรือยัง
* - เปลี่ยนรหัสผ่าน/ปิดใช้ token
* ========================= */
r.post("/reset-password", async (req, res) => {
const { token, new_password } = req.body || {};
if (!token || !new_password) {
return res.status(400).json({ error: "TOKEN_AND_NEW_PASSWORD_REQUIRED" });
}
const token_hash = crypto.createHash("sha256").update(token).digest("hex");
const [[row]] = await sql.query(
`SELECT id, user_id, expires_at, used_at
FROM password_resets
WHERE token_hash=? LIMIT 1`,
[token_hash]
);
if (!row) return res.status(400).json({ error: "INVALID_TOKEN" });
if (row.used_at) return res.status(400).json({ error: "TOKEN_ALREADY_USED" });
if (new Date(row.expires_at).getTime() < Date.now()) {
return res.status(400).json({ error: "TOKEN_EXPIRED" });
}
// เปลี่ยนรหัสผ่าน
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(new_password, salt);
await sql.query(`UPDATE users SET password_hash=? WHERE user_id=?`, [
hash,
row.user_id,
]);
// ปิดใช้ token นี้
await sql.query(`UPDATE password_resets SET used_at=NOW() WHERE id=?`, [
row.id,
]);
return res.json({ ok: true });
});
/* =========================
* POST /api/auth/logout — stateless
* - frontend ลบ token เอง
* ========================= */
r.post("/logout", (_req, res) => {
res.clearCookie("access_token", { path: "/" });
res.clearCookie("refresh_token", { path: "/" });
return res.json({ ok: true });
});
export default r;
// หมายเหตุ: คุณอาจเพิ่ม /register, /forgot-password, /reset-password ตามต้องการ
// แต่ในโปรเจกต์นี้จะให้แอดมินสร้างบัญชีผู้ใช้ผ่าน /api/users แทน

View File

@@ -1,251 +1,137 @@
// FILE: src/routes/auth.js (ESM) — Bearer only, refresh via header/body, forgot/reset password
// 03.2 เพิ่ม auth.js และ lookup.js ให้สอดคล้อง RBAC/permission_code
// ตาม src/config/permissions.js) และอ่าน scope จาก DB เสมอ
/*สมมติว่ามีตาราง password_resets สำหรับเก็บโทเคนรีเซ็ต:
password_resets(
id BIGINT PK, user_id BIGINT, token_hash CHAR(64),
expires_at DATETIME, used_at DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
INDEX(token_hash), INDEX(user_id), INDEX(expires_at)
*/
// backend/src/routes/auth.js
import { Router } from "express";
import jwt from "jsonwebtoken";
import sql from "../db/index.js";
import bcrypt from "bcryptjs";
import crypto from "node:crypto";
import jwt from "jsonwebtoken";
import { config } from "../config.js";
import { User } from "../db/sequelize.js";
import {
signAccessToken,
signRefreshToken,
requireAuth,
} from "../middleware/auth.js";
const r = Router();
/* =========================
* CONFIG & HELPERS
* ========================= */
// ใช้ค่าเดียวกับ middleware authJwt()
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret";
const REFRESH_SECRET = process.env.REFRESH_SECRET || "dev-refresh-secret";
const ACCESS_TTL = process.env.ACCESS_TTL || "30m";
const REFRESH_TTL = process.env.REFRESH_TTL || "30d";
// อายุของ reset token (นาที)
const RESET_TTL_MIN = Number(process.env.RESET_TTL_MIN || 30);
function signAccessToken(user) {
return jwt.sign(
{ user_id: user.user_id, username: user.username },
JWT_SECRET,
{ expiresIn: ACCESS_TTL, issuer: "dms-backend" }
);
// cookie options — ทำในไฟล์นี้เลย (ไม่เพิ่ม utils ใหม่)
function cookieOpts(maxAgeMs) {
const isProd = process.env.NODE_ENV === "production";
const opts = {
httpOnly: true,
secure: true, // หลัง Nginx/HTTPS
sameSite: "none", // ส่งข้าม subdomain ได้
path: "/",
maxAge: maxAgeMs,
};
if (config.COOKIE_DOMAIN) opts.domain = config.COOKIE_DOMAIN; // เช่น .np-dms.work
if (!isProd && process.env.ALLOW_INSECURE_COOKIE === "1") {
opts.secure = false;
opts.sameSite = "lax";
}
return opts;
}
function signRefreshToken(user) {
return jwt.sign(
{ user_id: user.user_id, username: user.username, t: "refresh" },
REFRESH_SECRET,
{ expiresIn: REFRESH_TTL, issuer: "dms-backend" }
);
}
// helper TTL จาก config เดิม
const ACCESS_TTL_MS = (() => {
// รับทั้งรูปแบบ "15m" (เช่น EXPIRES_IN) หรือ milliseconds
// ถ้าเป็นเลขอยู่แล้ว (ms) ก็ใช้เลย
if (/^\d+$/.test(String(config.JWT.EXPIRES_IN)))
return Number(config.JWT.EXPIRES_IN);
// แปลงรูปแบบเช่น "15m" เป็น ms แบบง่าย ๆ
const s = String(config.JWT.EXPIRES_IN || "15m");
const n = parseInt(s, 10);
if (s.endsWith("h")) return n * 60 * 60 * 1000;
if (s.endsWith("m")) return n * 60 * 1000;
if (s.endsWith("s")) return n * 1000;
return 15 * 60 * 1000;
})();
const REFRESH_TTL_MS = (() => {
if (/^\d+$/.test(String(config.JWT.REFRESH_EXPIRES_IN)))
return Number(config.JWT.REFRESH_EXPIRES_IN);
const s = String(config.JWT.REFRESH_EXPIRES_IN || "7d");
const n = parseInt(s, 10);
if (s.endsWith("d")) return n * 24 * 60 * 60 * 1000;
if (s.endsWith("h")) return n * 60 * 60 * 1000;
if (s.endsWith("m")) return n * 60 * 1000;
if (s.endsWith("s")) return n * 1000;
return 7 * 24 * 60 * 60 * 1000;
})();
function getBearer(req) {
const h = req.headers.authorization || "";
if (!h.startsWith("Bearer ")) return null;
const token = h.slice(7).trim();
return token || null;
}
async function findUserByUsername(username) {
const [rows] = await sql.query(
`SELECT user_id, username, email, first_name, last_name, password_hash
FROM users WHERE username=? LIMIT 1`,
[username]
);
return rows?.[0] || null;
}
async function findUserByEmail(email) {
const [rows] = await sql.query(
`SELECT user_id, username, email, first_name, last_name, password_hash
FROM users WHERE email=? LIMIT 1`,
[email]
);
return rows?.[0] || null;
}
/* =========================
* POST /api/auth/login
* - รับ username/password
* - ตรวจ bcrypt แล้วออก token+refresh_token (JSON)
* ========================= */
// == POST /api/auth/login ==
r.post("/login", async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) {
if (!username || !password)
return res.status(400).json({ error: "USERNAME_PASSWORD_REQUIRED" });
}
const user = await findUserByUsername(username);
const user = await User.findOne({ where: { username }, raw: true });
if (!user) return res.status(401).json({ error: "INVALID_CREDENTIALS" });
const ok = await bcrypt.compare(password, user.password_hash || "");
if (!ok) return res.status(401).json({ error: "INVALID_CREDENTIALS" });
const token = signAccessToken(user);
const refresh_token = signRefreshToken(user);
// NOTE: สิทธิ์จริง ๆ ให้ดึงจากตาราง role/permission ของคุณ
const permissions = []; // ใส่เปล่าไว้ก่อน (คุณมี enrichRoles ที่อื่นอยู่แล้ว)
const payload = {
user_id: user.user_id,
username: user.username,
permissions,
};
const access = signAccessToken(payload);
const refresh = signRefreshToken({ user_id: user.user_id });
// ตั้งคุกกี้ (และยังส่ง token ใน body ได้ถ้าคุณใช้อยู่)
res.cookie("access_token", access, cookieOpts(ACCESS_TTL_MS));
res.cookie("refresh_token", refresh, cookieOpts(REFRESH_TTL_MS));
return res.json({
token,
refresh_token,
ok: true,
token: access,
refresh_token: refresh,
user: { user_id: user.user_id, username: user.username, email: user.email },
});
});
// == GET /api/auth/me ==
r.get("/me", requireAuth, async (req, res) => {
// enrich เพิ่มจากฐานได้ตามต้องการ; ตอนนี้เอาเบื้องต้นจาก token
return res.json({
ok: true,
user: {
user_id: user.user_id,
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
user_id: req.user.user_id,
username: req.user.username,
permissions: req.user.permissions || [],
},
});
});
/* =========================
* POST /api/auth/refresh
* - รองรับ refresh token จาก:
* 1) Authorization: Bearer <refresh_token>
* 2) req.body.refresh_token
* - ออก token ใหม่ + refresh ใหม่ (rotation)
* ========================= */
// == POST /api/auth/refresh ==
r.post("/refresh", async (req, res) => {
const fromHeader = getBearer(req);
const fromBody = (req.body || {}).refresh_token;
const refreshToken = fromHeader || fromBody;
if (!refreshToken) {
return res.status(400).json({ error: "REFRESH_TOKEN_REQUIRED" });
}
// รับจากคุกกี้ก่อน แล้วค่อย Authorization
const bearer = req.headers.authorization?.startsWith("Bearer ")
? req.headers.authorization.slice(7)
: null;
const rt = req.cookies?.refresh_token || bearer;
if (!rt) return res.status(401).json({ error: "Unauthenticated" });
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET, {
issuer: "dms-backend",
});
if (payload.t !== "refresh") throw new Error("bad token type");
const [[user]] = await sql.query(
`SELECT user_id, username, email, first_name, last_name
FROM users WHERE user_id=? LIMIT 1`,
[payload.user_id]
);
if (!user) return res.status(401).json({ error: "USER_NOT_FOUND" });
// rotation
const token = signAccessToken(user);
const new_refresh = signRefreshToken(user);
return res.json({
token,
refresh_token: new_refresh,
user: {
user_id: user.user_id,
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
},
});
// verify refresh โดยตรงด้วย config.JWT.REFRESH_SECRET (คงสไตล์เดิม)
const p = jwt.verify(rt, config.JWT.REFRESH_SECRET, { clockTolerance: 10 });
// โหลดสิทธิ์ล่าสุดจากฐานได้ ถ้าต้องการ; ตอนนี้ใส่ [] ไว้ก่อน
const permissions = [];
const access = signAccessToken({ user_id: p.user_id, permissions });
res.cookie("access_token", access, cookieOpts(ACCESS_TTL_MS));
return res.json({ ok: true, token: access });
} catch {
return res.status(401).json({ error: "INVALID_REFRESH_TOKEN" });
return res.status(401).json({ error: "Unauthenticated" });
}
});
/* =========================
* POST /api/auth/forgot-password
* - รับ username หรือ email อย่างใดอย่างหนึ่ง
* - สร้าง reset token แบบสุ่ม, เก็บ hash ใน DB พร้อมหมดอายุ
* - ส่งเสมอ {ok:true} เพื่อลด user enumeration
* - การ “ส่งอีเมล/ลิงก์รีเซ็ต” ให้ทำนอกระบบนี้ (เช่น n8n)
* ========================= */
r.post("/forgot-password", async (req, res) => {
const { username, email } = req.body || {};
// หา user จาก username หรือ email (ถ้ามีทั้งสอง จะให้ username มาก่อน)
let user = null;
if (username) user = await findUserByUsername(username);
if (!user && email) user = await findUserByEmail(email);
// สร้างโทเคน “เหมือนจริง” เสมอ (แต่ถ้าไม่เจอ user ก็ไม่บอก)
if (user) {
const raw = crypto.randomBytes(32).toString("hex"); // โทเคนดิบ (ส่งทางอีเมล)
const hash = crypto.createHash("sha256").update(raw).digest("hex"); // เก็บใน DB
const expires = new Date(Date.now() + RESET_TTL_MIN * 60 * 1000);
// ทำ invalid เก่า ๆ ของ user นี้ (optional)
await sql.query(
`UPDATE password_resets SET used_at=NOW()
WHERE user_id=? AND used_at IS NULL AND expires_at < NOW()`,
[user.user_id]
);
// บันทึก token ใหม่
await sql.query(
`INSERT INTO password_resets (user_id, token_hash, expires_at)
VALUES (?,?,?)`,
[user.user_id, hash, expires]
);
// TODO: ส่ง “raw token” ไปช่องทางปลอดภัย (เช่น n8n ส่งอีเมล)
// ตัวอย่างลิงก์ที่ frontend จะใช้:
// https://<frontend-domain>/reset-password?token=<raw>
// คุณสามารถต่อ webhook ไป n8n ได้ที่นี่ถ้าต้องการ
}
// ไม่บอกว่าเจอหรือไม่เจอ user
return res.json({ ok: true });
});
/* =========================
* POST /api/auth/reset-password
* - รับ token (จากลิงก์ในอีเมล) + new_password
* - ตรวจ token_hash กับ DB, หมดอายุหรือถูกใช้ไปแล้วหรือยัง
* - เปลี่ยนรหัสผ่าน/ปิดใช้ token
* ========================= */
r.post("/reset-password", async (req, res) => {
const { token, new_password } = req.body || {};
if (!token || !new_password) {
return res.status(400).json({ error: "TOKEN_AND_NEW_PASSWORD_REQUIRED" });
}
const token_hash = crypto.createHash("sha256").update(token).digest("hex");
const [[row]] = await sql.query(
`SELECT id, user_id, expires_at, used_at
FROM password_resets
WHERE token_hash=? LIMIT 1`,
[token_hash]
);
if (!row) return res.status(400).json({ error: "INVALID_TOKEN" });
if (row.used_at) return res.status(400).json({ error: "TOKEN_ALREADY_USED" });
if (new Date(row.expires_at).getTime() < Date.now()) {
return res.status(400).json({ error: "TOKEN_EXPIRED" });
}
// เปลี่ยนรหัสผ่าน
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(new_password, salt);
await sql.query(`UPDATE users SET password_hash=? WHERE user_id=?`, [
hash,
row.user_id,
]);
// ปิดใช้ token นี้
await sql.query(`UPDATE password_resets SET used_at=NOW() WHERE id=?`, [
row.id,
]);
return res.json({ ok: true });
});
/* =========================
* POST /api/auth/logout — stateless
* - frontend ลบ token เอง
* ========================= */
// == POST /api/auth/logout ==
r.post("/logout", (_req, res) => {
res.clearCookie("access_token", { path: "/" });
res.clearCookie("refresh_token", { path: "/" });
return res.json({ ok: true });
});
export default r;
// หมายเหตุ: คุณอาจเพิ่ม /register, /forgot-password, /reset-password ตามต้องการ
// แต่ในโปรเจกต์นี้จะให้แอดมินสร้างบัญชีผู้ใช้ผ่าน /api/users แทน

View File

@@ -0,0 +1,17 @@
// FILE: backend/src/utils/cookie.js
export const cookieOpts = (maxAgeMs) => {
const isProd = process.env.NODE_ENV === "production";
const opts = {
httpOnly: true,
secure: true, // หลัง Nginx/HTTPS
sameSite: "none", // ส่งข้าม subdomain ได้
path: "/",
maxAge: maxAgeMs,
};
if (process.env.COOKIE_DOMAIN) opts.domain = process.env.COOKIE_DOMAIN; // เช่น .np-dms.work
if (!isProd && process.env.ALLOW_INSECURE_COOKIE === "1") {
opts.secure = false;
opts.sameSite = "lax";
}
return opts;
};

31
backend/src/utils/jwt.js Normal file
View File

@@ -0,0 +1,31 @@
// FILE: backend/src/utils/jwt.js
import jwt from "jsonwebtoken";
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || "dev_access_secret";
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || "dev_refresh_secret";
export const ACCESS_TTL_MS = parseInt(
process.env.ACCESS_TTL_MS || `${15 * 60 * 1000}`,
10
); // 15 นาที
export const REFRESH_TTL_MS = parseInt(
process.env.REFRESH_TTL_MS || `${7 * 24 * 60 * 60 * 1000}`,
10
); // 7 วัน
export function signAccessToken(payload) {
return jwt.sign(payload, ACCESS_SECRET, {
expiresIn: Math.floor(ACCESS_TTL_MS / 1000),
});
}
export function signRefreshToken(payload) {
return jwt.sign(payload, REFRESH_SECRET, {
expiresIn: Math.floor(REFRESH_TTL_MS / 1000),
});
}
export function verifyAccessToken(token) {
return jwt.verify(token, ACCESS_SECRET, { clockTolerance: 10 }); // เผื่อเวลา QNAP คลาด
}
export function verifyRefreshToken(token) {
return jwt.verify(token, REFRESH_SECRET, { clockTolerance: 10 });
}