backend: Mod
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
[/dms]
|
||||
max_log = 496206
|
||||
number = 3
|
||||
max_log = 498246
|
||||
number = 4
|
||||
finish = 1
|
||||
|
||||
3178
.qsync/meta/qmeta0
3178
.qsync/meta/qmeta0
File diff suppressed because it is too large
Load Diff
4871
.qsync/meta/qmeta1
4871
.qsync/meta/qmeta1
File diff suppressed because it is too large
Load Diff
5235
.qsync/meta/qmeta2
5235
.qsync/meta/qmeta2
File diff suppressed because it is too large
Load Diff
150
backend/src/db/sequelize.js
Normal file → Executable file
150
backend/src/db/sequelize.js
Normal file → Executable file
@@ -5,11 +5,7 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
import { config } from "../config.js";
|
||||
|
||||
export const sequelize = new Sequelize(
|
||||
config.DB.NAME,
|
||||
config.DB.USER,
|
||||
config.DB.PASS,
|
||||
{
|
||||
export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB.PASS, {
|
||||
host: config.DB.HOST,
|
||||
port: config.DB.PORT,
|
||||
dialect: "mariadb",
|
||||
@@ -17,79 +13,91 @@ export const sequelize = new Sequelize(
|
||||
dialectOptions: { timezone: "Z" },
|
||||
define: { freezeTableName: true, underscored: false, timestamps: false },
|
||||
pool: { max: 10, min: 0, idle: 10000 },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด ---
|
||||
export let User = null;
|
||||
export let Role = null;
|
||||
export let Permission = null;
|
||||
export let UserRole = null;
|
||||
export let RolePermission = null;
|
||||
export let Project = null; // <-- เพิ่มเข้ามา
|
||||
export let UserProjectRole = null; // <-- เพิ่มเข้ามา
|
||||
export let User, Role, Permission, Organization, Project, UserRole, RolePermission,
|
||||
UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,
|
||||
Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,
|
||||
Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory;
|
||||
|
||||
if (process.env.ENABLE_SEQUELIZE === "1") {
|
||||
// --- 2. โหลดโมเดลทั้งหมดแบบ on-demand ---
|
||||
const mdlUser = await import("./models/User.js").catch(() => null);
|
||||
const mdlRole = await import("./models/Role.js").catch(() => null);
|
||||
const mdlPerm = await import("./models/Permission.js").catch(() => null);
|
||||
const mdlUR = await import("./models/UserRole.js").catch(() => null);
|
||||
const mdlRP = await import("./models/RolePermission.js").catch(() => null);
|
||||
const mdlProj = await import("./models/Project.js").catch(() => null); // <-- เพิ่มเข้ามา
|
||||
const mdlUPR = await import("./models/UserProjectRole.js").catch(() => null); // <-- เพิ่มเข้ามา
|
||||
// --- 2. สร้าง Object ของ Models ทั้งหมดที่จะโหลด ---
|
||||
const modelsToLoad = {
|
||||
User: await import("./models/User.js").catch(() => null),
|
||||
Role: await import("./models/Role.js").catch(() => null),
|
||||
Permission: await import("./models/Permission.js").catch(() => null),
|
||||
Organization: await import("./models/Organization.js").catch(() => null),
|
||||
Project: await import("./models/Project.js").catch(() => null),
|
||||
UserRole: await import("./models/UserRole.js").catch(() => null),
|
||||
RolePermission: await import("./models/RolePermission.js").catch(() => null),
|
||||
UserProjectRole: await import("./models/UserProjectRole.js").catch(() => null),
|
||||
Correspondence: await import("./models/Correspondence.js").catch(() => null),
|
||||
CorrespondenceVersion: await import("./models/CorrespondenceVersion.js").catch(() => null),
|
||||
Document: await import("./models/Document.js").catch(() => null),
|
||||
CorrDocumentMap: await import("./models/CorrDocumentMap.js").catch(() => null),
|
||||
Drawing: await import("./models/Drawing.js").catch(() => null),
|
||||
DrawingRevision: await import("./models/DrawingRevision.js").catch(() => null),
|
||||
FileObject: await import("./models/FileObject.js").catch(() => null),
|
||||
RFA: await import("./models/RFA.js").catch(() => null),
|
||||
RFARevision: await import("./models/RFARevision.js").catch(() => null),
|
||||
RfaDrawingMap: await import("./models/RfaDrawingMap.js").catch(() => null),
|
||||
Transmittal: await import("./models/Transmittal.js").catch(() => null),
|
||||
TransmittalItem: await import("./models/TransmittalItem.js").catch(() => null),
|
||||
Volume: await import("./models/Volume.js").catch(() => null),
|
||||
ContractDwg: await import("./models/ContractDwg.js").catch(() => null),
|
||||
SubCategory: await import("./models/SubCategory.js").catch(() => null),
|
||||
};
|
||||
|
||||
// --- 3. Initialize Model ทั้งหมด ---
|
||||
if (mdlUser?.default) User = mdlUser.default(sequelize);
|
||||
if (mdlRole?.default) Role = mdlRole.default(sequelize);
|
||||
if (mdlPerm?.default) Permission = mdlPerm.default(sequelize);
|
||||
if (mdlUR?.default) UserRole = mdlUR.default(sequelize);
|
||||
if (mdlRP?.default) RolePermission = mdlRP.default(sequelize);
|
||||
if (mdlProj?.default) Project = mdlProj.default(sequelize); // <-- เพิ่มเข้ามา
|
||||
if (mdlUPR?.default) UserProjectRole = mdlUPR.default(sequelize); // <-- เพิ่มเข้ามา
|
||||
// --- 3. Initialize Model ทั้งหมด ---
|
||||
User = modelsToLoad.User?.default ? modelsToLoad.User.default(sequelize) : null;
|
||||
Role = modelsToLoad.Role?.default ? modelsToLoad.Role.default(sequelize) : null;
|
||||
Permission = modelsToLoad.Permission?.default ? modelsToLoad.Permission.default(sequelize) : null;
|
||||
Organization = modelsToLoad.Organization?.default ? modelsToLoad.Organization.default(sequelize) : null;
|
||||
Project = modelsToLoad.Project?.default ? modelsToLoad.Project.default(sequelize) : null;
|
||||
UserRole = modelsToLoad.UserRole?.default ? modelsToLoad.UserRole.default(sequelize) : null;
|
||||
RolePermission = modelsToLoad.RolePermission?.default ? modelsToLoad.RolePermission.default(sequelize) : null;
|
||||
UserProjectRole = modelsToLoad.UserProjectRole?.default ? modelsToLoad.UserProjectRole.default(sequelize) : null;
|
||||
Correspondence = modelsToLoad.Correspondence?.default ? modelsToLoad.Correspondence.default(sequelize) : null;
|
||||
CorrespondenceVersion = modelsToLoad.CorrespondenceVersion?.default ? modelsToLoad.CorrespondenceVersion.default(sequelize) : null;
|
||||
Document = modelsToLoad.Document?.default ? modelsToLoad.Document.default(sequelize) : null;
|
||||
CorrDocumentMap = modelsToLoad.CorrDocumentMap?.default ? modelsToLoad.CorrDocumentMap.default(sequelize) : null;
|
||||
Drawing = modelsToLoad.Drawing?.default ? modelsToLoad.Drawing.default(sequelize) : null;
|
||||
DrawingRevision = modelsToLoad.DrawingRevision?.default ? modelsToLoad.DrawingRevision.default(sequelize) : null;
|
||||
FileObject = modelsToLoad.FileObject?.default ? modelsToLoad.FileObject.default(sequelize) : null;
|
||||
RFA = modelsToLoad.RFA?.default ? modelsToLoad.RFA.default(sequelize) : null;
|
||||
RFARevision = modelsToLoad.RFARevision?.default ? modelsToLoad.RFARevision.default(sequelize) : null;
|
||||
RfaDrawingMap = modelsToLoad.RfaDrawingMap?.default ? modelsToLoad.RfaDrawingMap.default(sequelize) : null;
|
||||
Transmittal = modelsToLoad.Transmittal?.default ? modelsToLoad.Transmittal.default(sequelize) : null;
|
||||
TransmittalItem = modelsToLoad.TransmittalItem?.default ? modelsToLoad.TransmittalItem.default(sequelize) : null;
|
||||
Volume = modelsToLoad.Volume?.default ? modelsToLoad.Volume.default(sequelize) : null;
|
||||
ContractDwg = modelsToLoad.ContractDwg?.default ? modelsToLoad.ContractDwg.default(sequelize) : null;
|
||||
SubCategory = modelsToLoad.SubCategory?.default ? modelsToLoad.SubCategory.default(sequelize) : null;
|
||||
|
||||
// --- 4. สร้างความสัมพันธ์ (Associations) ทั้งหมด ---
|
||||
if (User && Role && Permission && UserRole && RolePermission) {
|
||||
// ความสัมพันธ์ระดับระบบ (System-level)
|
||||
User.belongsToMany(Role, { through: UserRole, foreignKey: "user_id" });
|
||||
Role.belongsToMany(User, { through: UserRole, foreignKey: "role_id" });
|
||||
Role.belongsToMany(Permission, {
|
||||
through: RolePermission,
|
||||
foreignKey: "role_id",
|
||||
});
|
||||
Permission.belongsToMany(Role, {
|
||||
through: RolePermission,
|
||||
foreignKey: "permission_id",
|
||||
});
|
||||
}
|
||||
|
||||
// ความสัมพันธ์ระดับโปรเจกต์ (Project-level) - ส่วนที่เพิ่มเข้ามา
|
||||
if (User && Project && Role && UserProjectRole) {
|
||||
// ทำให้ UserProjectRole เป็นตัวกลางเชื่อม 3 ตาราง
|
||||
UserProjectRole.belongsTo(User, { foreignKey: "user_id" });
|
||||
UserProjectRole.belongsTo(Project, { foreignKey: "project_id" });
|
||||
UserProjectRole.belongsTo(Role, { foreignKey: "role_id" });
|
||||
|
||||
// ทำให้สามารถ include ข้อมูลที่เกี่ยวข้องได้ง่ายขึ้น
|
||||
User.hasMany(UserProjectRole, { foreignKey: "user_id" });
|
||||
Project.hasMany(UserProjectRole, { foreignKey: "project_id" });
|
||||
Role.hasMany(UserProjectRole, { foreignKey: "role_id" });
|
||||
}
|
||||
// --- 4. สร้างความสัมพันธ์ (Associations) ---
|
||||
const loadedModels = { User, Role, Permission, Organization, Project, UserRole, RolePermission,
|
||||
UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,
|
||||
Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,
|
||||
Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory };
|
||||
|
||||
for (const modelName in loadedModels) {
|
||||
if (loadedModels[modelName] && loadedModels[modelName].associate) {
|
||||
loadedModels[modelName].associate(loadedModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ฟังก์ชันสำหรับทดสอบการเชื่อมต่อ Sequelize
|
||||
*/
|
||||
export async function dbReady() {
|
||||
if (process.env.ENABLE_SEQUELIZE !== "1") {
|
||||
console.log("Sequelize is disabled.");
|
||||
return Promise.resolve();
|
||||
}
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log("Sequelize connection has been established successfully.");
|
||||
} catch (error) {
|
||||
console.error("Unable to connect to the database via Sequelize:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
if (process.env.ENABLE_SEQUELIZE !== "1") {
|
||||
console.log("Sequelize is disabled.");
|
||||
return Promise.resolve();
|
||||
}
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log("Sequelize connection has been established successfully.");
|
||||
} catch (error) {
|
||||
console.error("Unable to connect to the database via Sequelize:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
61
backend/src/middleware/auth copy.js
Executable file
61
backend/src/middleware/auth copy.js
Executable file
@@ -0,0 +1,61 @@
|
||||
// FILE: backend/src/middleware/auth.js
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
import { config } from "../config.js";
|
||||
import { User, Role, UserRole } from "../db/sequelize.js";
|
||||
|
||||
export function signAccessToken(payload) {
|
||||
return jwt.sign(payload, config.JWT.SECRET, {
|
||||
expiresIn: config.JWT.EXPIRES_IN,
|
||||
});
|
||||
}
|
||||
export function signRefreshToken(payload) {
|
||||
return jwt.sign(payload, config.JWT.REFRESH_SECRET, {
|
||||
expiresIn: config.JWT.REFRESH_EXPIRES_IN,
|
||||
});
|
||||
}
|
||||
|
||||
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 token = extractToken(req);
|
||||
if (!token) return res.status(401).json({ error: "Missing token" });
|
||||
|
||||
try {
|
||||
req.user = jwt.verify(token, config.JWT.SECRET);
|
||||
next();
|
||||
} catch {
|
||||
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();
|
||||
const rows = await UserRole.findAll({
|
||||
where: { user_id: req.user.user_id },
|
||||
include: [{ model: Role }],
|
||||
}).catch(() => []);
|
||||
req.user.roles = rows.map((r) => r.role?.role_name).filter(Boolean);
|
||||
next();
|
||||
}
|
||||
|
||||
export function hasPerm(req, perm) {
|
||||
const set = new Set(req?.user?.permissions || []);
|
||||
return set.has(perm);
|
||||
}
|
||||
0
backend/src/middleware/auth.js
Normal file → Executable file
0
backend/src/middleware/auth.js
Normal file → Executable file
6
backend/src/middleware/index.js
Normal file → Executable file
6
backend/src/middleware/index.js
Normal file → Executable file
@@ -1,7 +1,7 @@
|
||||
// File: backend/src/middleware/index.js
|
||||
import * as authJwt from "./authJwt.js";
|
||||
import * as abac from "./abac.js";
|
||||
import * as auth from "./auth.js";
|
||||
import * as authJwt from "./authJwt.js";
|
||||
import * as errorHandler from "./errorHandler.js";
|
||||
import * as loadPrincipal from "./loadPrincipal.js";
|
||||
import * as permGuard from "./permGuard.js";
|
||||
@@ -12,9 +12,9 @@ import * as requirePerm from "./requirePerm.js";
|
||||
// Export ทุกอย่างออกมาเป็น named exports
|
||||
// เพื่อให้สามารถ import แบบ `import { authJwt, permGuard } from '../middleware';` ได้
|
||||
export {
|
||||
authJwt,
|
||||
abac,
|
||||
auth,
|
||||
authJwt,
|
||||
errorHandler,
|
||||
loadPrincipal,
|
||||
permGuard,
|
||||
@@ -25,9 +25,9 @@ export {
|
||||
|
||||
// (Optional) สร้าง default export สำหรับกรณีที่ต้องการ import ทั้งหมดใน object เดียว
|
||||
const middleware = {
|
||||
authJwt,
|
||||
abac,
|
||||
auth,
|
||||
authJwt,
|
||||
errorHandler,
|
||||
loadPrincipal,
|
||||
permGuard,
|
||||
|
||||
0
backend/src/middleware/loadPrincipal.js
Normal file → Executable file
0
backend/src/middleware/loadPrincipal.js
Normal file → Executable file
74
backend/src/routes/dashboard.js
Normal file → Executable file
74
backend/src/routes/dashboard.js
Normal file → Executable file
@@ -1,23 +1,63 @@
|
||||
import { Router } from "express";
|
||||
import { User } from "../db/index.js";
|
||||
import { authJwt } from "../middleware/index.js";
|
||||
// backend/src/routes/dashboard.js
|
||||
import { Router } from 'express';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
// 1. Import Middleware ที่ถูกต้อง
|
||||
import { authJwt } from '../middleware/authJwt.js';
|
||||
import { loadPrincipalMw } from '../middleware/loadPrincipal.js';
|
||||
|
||||
// 2. Import Sequelize Models จาก `sequelize.js` ไม่ใช่ `index.js`
|
||||
import { Correspondence, Document, RFA, User } from '../db/sequelize.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authJwt.verifyToken);
|
||||
// 3. ใช้ Middleware Chain ที่ถูกต้อง 100%
|
||||
router.use(authJwt(), loadPrincipalMw());
|
||||
|
||||
router.get("/users/summary", async (req, res, next) => {
|
||||
try {
|
||||
const totalUsers = await User.count();
|
||||
const activeUsers = await User.count({ where: { is_active: true } });
|
||||
res.json({
|
||||
total: totalUsers,
|
||||
active: activeUsers,
|
||||
inactive: totalUsers - activeUsers,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
// === API สำหรับ User Management Widget ===
|
||||
router.get('/users/summary', async (req, res, next) => {
|
||||
try {
|
||||
// ตรวจสอบว่า Model ถูกโหลดแล้วหรือยัง (จำเป็นสำหรับโหมด lazy-load)
|
||||
if (!User) {
|
||||
return res.status(503).json({ message: 'Database models not available. Is ENABLE_SEQUELIZE=1 set?' });
|
||||
}
|
||||
const totalUsers = await User.count();
|
||||
const activeUsers = await User.count({ where: { is_active: true } });
|
||||
|
||||
res.json({
|
||||
total: totalUsers,
|
||||
active: activeUsers,
|
||||
inactive: totalUsers - activeUsers,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
// === API อื่นๆ สำหรับ Dashboard ที่เราคุยกันไว้ก่อนหน้า ===
|
||||
router.get('/stats', async (req, res, next) => {
|
||||
try {
|
||||
if (!Document || !RFA) {
|
||||
return res.status(503).json({ message: 'Database models not available. Is ENABLE_SEQUELIZE=1 set?' });
|
||||
}
|
||||
|
||||
const sevenDaysAgo = new Date(new Date().setDate(new Date().getDate() - 7));
|
||||
|
||||
const totalDocuments = await Document.count();
|
||||
const newThisWeek = await Document.count({ where: { createdAt: { [Op.gte]: sevenDaysAgo } } });
|
||||
const pendingRfas = await RFA.count({ where: { status: 'pending' } }); // สมมติตาม status
|
||||
|
||||
res.json({
|
||||
totalDocuments,
|
||||
newThisWeek,
|
||||
pendingRfas
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
130
backend/src/routes/rbac_admin.js
Normal file → Executable file
130
backend/src/routes/rbac_admin.js
Normal file → Executable file
@@ -1,144 +1,88 @@
|
||||
// FILE: backend/src/routes/rbac_admin.js
|
||||
import { Router } from "express";
|
||||
import { Role, Permission, UserProjectRole, Project } from "../db/sequelize.js";
|
||||
import { authJwt, permGuard } from "../middleware/index.js";
|
||||
import { authJwt } from "../middleware/authJwt.js";
|
||||
import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// กำหนดให้ทุก route ในไฟล์นี้ต้องมีสิทธิ์ 'manage_rbac'
|
||||
router.use(authJwt.verifyToken, permGuard("manage_rbac"));
|
||||
// Middleware Chain ที่ถูกต้อง 100% ตามสถาปัตยกรรมของคุณ
|
||||
router.use(authJwt(), loadPrincipalMw());
|
||||
|
||||
// == ROLES Management ==
|
||||
router.get("/roles", async (req, res, next) => {
|
||||
router.get("/roles", requirePerm("roles.manage"), async (req, res, next) => {
|
||||
try {
|
||||
const roles = await Role.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
attributes: ["id", "name"],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
include: [{ model: Permission, attributes: ["id", "name"], through: { attributes: [] } }],
|
||||
order: [["name", "ASC"]],
|
||||
});
|
||||
res.json(roles);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
router.post("/roles", async (req, res, next) => {
|
||||
router.post("/roles", requirePerm("roles.manage"), async (req, res, next) => {
|
||||
const { name, description } = req.body;
|
||||
if (!name) return res.status(400).json({ message: "Role name is required." });
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
if (!name)
|
||||
return res.status(400).json({ message: "Role name is required." });
|
||||
const newRole = await Role.create({ name, description });
|
||||
res.status(201).json(newRole);
|
||||
} catch (error) {
|
||||
if (error.name === "SequelizeUniqueConstraintError") {
|
||||
return res
|
||||
.status(409)
|
||||
.json({ message: `Role '${name}' already exists.` });
|
||||
return res.status(409).json({ message: `Role '${name}' already exists.` });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/roles/:id/permissions", async (req, res, next) => {
|
||||
try {
|
||||
router.put("/roles/:id/permissions", requirePerm("roles.manage"), async (req, res, next) => {
|
||||
const { permissionIds } = req.body;
|
||||
if (!Array.isArray(permissionIds))
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "permissionIds must be an array." });
|
||||
|
||||
const role = await Role.findByPk(req.params.id);
|
||||
if (!role) return res.status(404).json({ message: "Role not found." });
|
||||
|
||||
await role.setPermissions(permissionIds);
|
||||
const updatedRole = await Role.findByPk(req.params.id, {
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
attributes: ["id", "name"],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
});
|
||||
res.json(updatedRole);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// == PERMISSIONS Management ==
|
||||
router.get("/permissions", async (req, res, next) => {
|
||||
try {
|
||||
const permissions = await Permission.findAll({ order: [["name", "ASC"]] });
|
||||
res.json(permissions);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
if (!Array.isArray(permissionIds)) return res.status(400).json({ message: "permissionIds must be an array." });
|
||||
try {
|
||||
const role = await Role.findByPk(req.params.id);
|
||||
if (!role) return res.status(404).json({ message: "Role not found." });
|
||||
await role.setPermissions(permissionIds);
|
||||
const updatedRole = await Role.findByPk(req.params.id, {
|
||||
include: [{ model: Permission, attributes: ['id', 'name'], through: { attributes: [] } }]
|
||||
});
|
||||
res.json(updatedRole);
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
// == USER-PROJECT-ROLES Management ==
|
||||
router.get("/user-project-roles", async (req, res, next) => {
|
||||
router.get("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
|
||||
const { userId } = req.query;
|
||||
if (!userId)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "userId query parameter is required." });
|
||||
if (!userId) return res.status(400).json({ message: "userId query parameter is required." });
|
||||
try {
|
||||
const assignments = await UserProjectRole.findAll({
|
||||
where: { user_id: userId },
|
||||
include: [
|
||||
{ model: Project, attributes: ["id", "name"] },
|
||||
{ model: Role, attributes: ["id", "name"] },
|
||||
],
|
||||
include: [ { model: Project, attributes: ["id", "name"] }, { model: Role, attributes: ["id", "name"] } ],
|
||||
});
|
||||
res.json(assignments);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
router.post("/user-project-roles", async (req, res, next) => {
|
||||
router.post("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
|
||||
const { userId, projectId, roleId } = req.body;
|
||||
if (!userId || !projectId || !roleId)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "userId, projectId, and roleId are required." });
|
||||
if (!userId || !projectId || !roleId) return res.status(400).json({ message: "userId, projectId, and roleId are required." });
|
||||
try {
|
||||
const [assignment, created] = await UserProjectRole.findOrCreate({
|
||||
where: { user_id: userId, project_id: projectId, role_id: roleId },
|
||||
defaults: { user_id: userId, project_id: projectId, role_id: roleId },
|
||||
});
|
||||
if (!created)
|
||||
return res
|
||||
.status(409)
|
||||
.json({ message: "This assignment already exists." });
|
||||
if (!created) return res.status(409).json({ message: "This assignment already exists." });
|
||||
res.status(201).json(assignment);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
router.delete("/user-project-roles", async (req, res, next) => {
|
||||
router.delete("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
|
||||
const { userId, projectId, roleId } = req.body;
|
||||
if (!userId || !projectId || !roleId)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "userId, projectId, and roleId are required." });
|
||||
if (!userId || !projectId || !roleId) return res.status(400).json({ message: "userId, projectId, and roleId are required." });
|
||||
try {
|
||||
const deletedCount = await UserProjectRole.destroy({
|
||||
where: { user_id: userId, project_id: projectId, role_id: roleId },
|
||||
});
|
||||
if (deletedCount === 0)
|
||||
return res.status(404).json({ message: "Assignment not found." });
|
||||
const deletedCount = await UserProjectRole.destroy({ where: { user_id: userId, project_id: projectId, role_id: roleId } });
|
||||
if (deletedCount === 0) return res.status(404).json({ message: 'Assignment not found.' });
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
205
backend/src/routes/users.js
Normal file → Executable file
205
backend/src/routes/users.js
Normal file → Executable file
@@ -1,136 +1,99 @@
|
||||
// File: backend/src/routes/users.js
|
||||
import { Router } from "express";
|
||||
import { User, Role } from "../db/sequelize.js";
|
||||
import { authJwt, permGuard } from "../middleware/index.js";
|
||||
import { hashPassword } from "../utils/passwords.js";
|
||||
import { Router } from 'express';
|
||||
import { User, Role } from '../db/sequelize.js';
|
||||
import { authJwt } from "../middleware/authJwt.js";
|
||||
import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { hashPassword } from '../utils/passwords.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Middleware สำหรับทุก route ในไฟล์นี้
|
||||
router.use(authJwt.verifyToken);
|
||||
// Middleware Chain ที่ถูกต้อง 100%
|
||||
router.use(authJwt(), loadPrincipalMw());
|
||||
|
||||
// GET /api/users - ดึงรายชื่อผู้ใช้ทั้งหมด
|
||||
router.get(
|
||||
"/",
|
||||
permGuard("manage_users"), // ตรวจสอบสิทธิ์
|
||||
async (req, res, next) => {
|
||||
// GET /api/users
|
||||
router.get('/', requirePerm('users.view'), async (req, res, next) => {
|
||||
try {
|
||||
const users = await User.findAll({
|
||||
attributes: { exclude: ["password_hash"] }, // **สำคัญมาก: ห้ามส่ง password hash ออกไป**
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
attributes: ["id", "name"],
|
||||
through: { attributes: [] }, // ไม่ต้องเอาข้อมูลจากตาราง join (UserRoles) มา
|
||||
},
|
||||
],
|
||||
order: [["username", "ASC"]],
|
||||
});
|
||||
res.json(users);
|
||||
const users = await User.findAll({
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }],
|
||||
order: [['username', 'ASC']]
|
||||
});
|
||||
res.json(users);
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
// POST /api/users
|
||||
router.post('/', requirePerm('users.manage'), async (req, res, next) => {
|
||||
const { username, email, password, first_name, last_name, is_active, roles } = req.body;
|
||||
if (!username || !email || !password) {
|
||||
return res.status(400).json({ message: 'Username, email, and password are required' });
|
||||
}
|
||||
try {
|
||||
const password_hash = await hashPassword(password);
|
||||
const newUser = await User.create({
|
||||
username, email, password_hash, first_name, last_name, is_active: is_active !== false,
|
||||
created_by: req.principal.user_id,
|
||||
updated_by: req.principal.user_id,
|
||||
org_id: req.principal.org_ids[0] || null,
|
||||
});
|
||||
|
||||
if (roles && roles.length > 0) {
|
||||
await newUser.setRoles(roles);
|
||||
}
|
||||
|
||||
const userWithRoles = await User.findByPk(newUser.id, {
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }]
|
||||
});
|
||||
res.status(201).json(userWithRoles);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||
return res.status(409).json({ message: 'Username or email already exists.' });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/users - สร้างผู้ใช้ใหม่
|
||||
router.post("/", permGuard("manage_users"), async (req, res, next) => {
|
||||
const { username, email, password, first_name, last_name, is_active, roles } =
|
||||
req.body;
|
||||
|
||||
if (!username || !email || !password) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "Username, email, and password are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const password_hash = await hashPassword(password);
|
||||
const newUser = await User.create({
|
||||
username,
|
||||
email,
|
||||
password_hash,
|
||||
first_name,
|
||||
last_name,
|
||||
is_active: is_active !== false,
|
||||
});
|
||||
|
||||
if (roles && roles.length > 0) {
|
||||
await newUser.setRoles(roles);
|
||||
}
|
||||
|
||||
const userWithRoles = await User.findByPk(newUser.id, {
|
||||
attributes: { exclude: ["password_hash"] },
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
attributes: ["id", "name"],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
res.status(201).json(userWithRoles);
|
||||
} catch (error) {
|
||||
if (error.name === "SequelizeUniqueConstraintError") {
|
||||
return res
|
||||
.status(409)
|
||||
.json({ message: "Username or email already exists." });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/users/:id - อัปเดตข้อมูลผู้ใช้
|
||||
router.put("/:id", permGuard("manage_users"), async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
const { email, first_name, last_name, is_active, roles } = req.body;
|
||||
// PUT /api/users/:id
|
||||
router.put('/:id', requirePerm('users.manage'), async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
const { email, first_name, last_name, is_active, roles } = req.body;
|
||||
try {
|
||||
const user = await User.findByPk(id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: 'User not found' });
|
||||
}
|
||||
user.email = email ?? user.email;
|
||||
user.first_name = first_name ?? user.first_name;
|
||||
user.last_name = last_name ?? user.last_name;
|
||||
user.is_active = is_active ?? user.is_active;
|
||||
user.updated_by = req.principal.user_id;
|
||||
await user.save();
|
||||
|
||||
try {
|
||||
const user = await User.findByPk(id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "User not found" });
|
||||
}
|
||||
|
||||
user.email = email ?? user.email;
|
||||
user.first_name = first_name ?? user.first_name;
|
||||
user.last_name = last_name ?? user.last_name;
|
||||
user.is_active = is_active ?? user.is_active;
|
||||
await user.save();
|
||||
|
||||
if (roles) {
|
||||
await user.setRoles(roles);
|
||||
}
|
||||
|
||||
const updatedUser = await User.findByPk(id, {
|
||||
attributes: { exclude: ["password_hash"] },
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
attributes: ["id", "name"],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
});
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
if (roles) {
|
||||
await user.setRoles(roles);
|
||||
}
|
||||
const updatedUser = await User.findByPk(id, {
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }]
|
||||
});
|
||||
res.json(updatedUser);
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
// DELETE /api/users/:id - ลบผู้ใช้ (Soft Delete)
|
||||
router.delete("/:id", permGuard("manage_users"), async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "User not found" });
|
||||
}
|
||||
user.is_active = false; // Soft Delete
|
||||
await user.save();
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
// DELETE /api/users/:id
|
||||
router.delete('/:id', requirePerm('users.manage'), async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: 'User not found' });
|
||||
}
|
||||
user.is_active = false;
|
||||
user.updated_by = req.principal.user_id;
|
||||
await user.save();
|
||||
res.status(204).send();
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
38
frontend/lib/auth copy.js
Executable file
38
frontend/lib/auth copy.js
Executable file
@@ -0,0 +1,38 @@
|
||||
// frontend/lib/auth.js
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const COOKIE_NAME = "access_token";
|
||||
|
||||
/**
|
||||
* Server-side session fetcher (ใช้ใน Server Components/Layouts)
|
||||
* - อ่านคุกกี้แบบ async: await cookies()
|
||||
* - ถ้าไม่มี token → return null
|
||||
* - ถ้ามี → เรียก /api/auth/me ที่ backend เพื่อตรวจสอบ
|
||||
*/
|
||||
export async function getSession() {
|
||||
// ✅ ต้อง await
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get(COOKIE_NAME)?.value;
|
||||
|
||||
if (!token) return null;
|
||||
|
||||
// เรียก backend ตรวจ session (ปรับ endpoint ให้ตรงของคุณ)
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/me`, {
|
||||
// ส่งต่อคุกกี้ไป backend (เลือกอย่างใดอย่างหนึ่ง)
|
||||
// วิธี A: ส่ง header Cookie โดยตรง
|
||||
headers: { Cookie: `${COOKIE_NAME}=${token}` },
|
||||
// วิธี B: ถ้า proxy ผ่าน nginx ในโดเมนเดียวกัน ใช้ credentials รวมคุกกี้อัตโนมัติได้
|
||||
// credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
const data = await res.json();
|
||||
// คาดหวังโครงสร้าง { user, permissions } จาก backend
|
||||
return {
|
||||
user: data.user,
|
||||
permissions: data.permissions || [],
|
||||
token,
|
||||
};
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
1
|
||||
/var/lib/postgresql/data
|
||||
1759373409
|
||||
1759595341
|
||||
5432
|
||||
/var/run/postgresql
|
||||
*
|
||||
|
||||
Binary file not shown.
@@ -1,99 +0,0 @@
|
||||
# ------------------------------------------------------------
|
||||
# git.np-dms.work
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
map $scheme $hsts_header {
|
||||
https "max-age=63072000; preload";
|
||||
}
|
||||
|
||||
server {
|
||||
set $forward_scheme http;
|
||||
set $server "gitea";
|
||||
set $port 3000;
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
|
||||
|
||||
server_name git.np-dms.work;
|
||||
|
||||
http2 on;
|
||||
|
||||
|
||||
# Let's Encrypt SSL
|
||||
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||
include conf.d/include/ssl-cache.conf;
|
||||
include conf.d/include/ssl-ciphers.conf;
|
||||
ssl_certificate /etc/letsencrypt/live/npm-10/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/npm-10/privkey.pem;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Force SSL
|
||||
include conf.d/include/force-ssl.conf;
|
||||
|
||||
|
||||
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
access_log /data/logs/proxy-host-5_access.log proxy;
|
||||
error_log /data/logs/proxy-host-5_error.log warn;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
send_timeout 300s;
|
||||
client_max_body_size 512m;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
location / {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
# Proxy!
|
||||
include conf.d/include/proxy.conf;
|
||||
}
|
||||
|
||||
|
||||
# Custom
|
||||
include /data/nginx/custom/server_proxy[.]conf;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# ------------------------------------------------------------
|
||||
# pma.np-dms.work
|
||||
# git.np-dms.work
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ map $scheme $hsts_header {
|
||||
|
||||
server {
|
||||
set $forward_scheme http;
|
||||
set $server "phpmyadmin";
|
||||
set $port 80;
|
||||
set $server "gitea";
|
||||
set $port 3000;
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
@@ -20,16 +20,17 @@ listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
|
||||
|
||||
server_name pma.np-dms.work;
|
||||
http2 off;
|
||||
server_name git.np-dms.work;
|
||||
|
||||
http2 on;
|
||||
|
||||
|
||||
# Let's Encrypt SSL
|
||||
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||
include conf.d/include/ssl-cache.conf;
|
||||
include conf.d/include/ssl-ciphers.conf;
|
||||
ssl_certificate /etc/letsencrypt/live/npm-11/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/npm-11/privkey.pem;
|
||||
ssl_certificate /etc/letsencrypt/live/npm-10/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/npm-10/privkey.pem;
|
||||
|
||||
|
||||
|
||||
@@ -45,28 +46,31 @@ http2 off;
|
||||
|
||||
|
||||
|
||||
# Force SSL
|
||||
include conf.d/include/force-ssl.conf;
|
||||
|
||||
|
||||
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
access_log /data/logs/proxy-host-6_access.log proxy;
|
||||
error_log /data/logs/proxy-host-6_error.log warn;
|
||||
access_log /data/logs/proxy-host-5_access.log proxy;
|
||||
error_log /data/logs/proxy-host-5_error.log warn;
|
||||
|
||||
client_max_body_size 128m;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
send_timeout 300s;
|
||||
client_max_body_size 512m;
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +84,8 @@ proxy_buffering off;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"6537407a672f376761784c234d79666a"; HMAC_secret |s:16:";_tF,Ps<jX+#Z=f>";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"34347471654523645a3b4a32556f6c2c"; HMAC_secret |s:16:"cxqQ@%S|9[H3Y6^%";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"263a746d69672f6a57305b5842603462"; HMAC_secret |s:16:".e@/Y$FkQ[hrnThJ";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"53754e49395b233f312a3e444e604c6c"; HMAC_secret |s:16:"'W%y<SKRJnU;}^!|";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"4156236d5e362168327e646d41463b24"; HMAC_secret |s:16:"0JzUDdkie;5^OD$x";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"397472635b712b6d2a40624244354f3f"; HMAC_secret |s:16:"MIW6@(E`(8hUZPvn";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"59692f53212c545c55273d71446d4a3a"; HMAC_secret |s:16:"-+x:]'T{Gq%(y'M)";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"4239422850276e464b7e426d556b5277"; HMAC_secret |s:16:"q]Ldh$];4JH,{.MX";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"24545877502f6d7d7967723b5e6c3a38"; HMAC_secret |s:16:"B%B#Rs(y)DGW?uv6";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"222b524d303848785047625636644350"; HMAC_secret |s:16:"r>gT.Yji&;>+(WBB";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"2f5e6c66684a7b414659303778513a25"; HMAC_secret |s:16:"32szCS_&Uq+0r$jk";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"637c736e2b30345b2f5e34516d2a5279"; HMAC_secret |s:16:"dwf#X!d2CIQn#30!";browser_access_time|a:0:{}encryption_key|s:32:"<22>S];W<>
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"32295c5e38423a7e2a4a237221505b3d"; HMAC_secret |s:16:"&:))STa2pQ/P6Z"h";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"2f472d2e70326a4f667d725e6e6c2e34"; HMAC_secret |s:16:"mAV2g;|NA4_6~ZyR";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"7057757d4e6d696d4e592a7b37584c55"; HMAC_secret |s:16:"A;qa<:i@!s2_vvR>";errors|a:0:{}
|
||||
@@ -1 +0,0 @@
|
||||
PMA_token |s:32:"34247c3c42743a5d50762a54336f2355"; HMAC_secret |s:16:"vj[cT;Q$S4x)t]Qi";errors|a:0:{}
|
||||
Reference in New Issue
Block a user