GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。 NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは 別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
95 lines
2.6 KiB
JavaScript
95 lines
2.6 KiB
JavaScript
"use strict";
|
|
const crypto = require("node:crypto");
|
|
|
|
const SESSION_COOKIE_NAME = "lwauth_admin_session";
|
|
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
const TEMP_KEY_TTL_MS = 60 * 60 * 1000;
|
|
const KIND_TTL_MS = { session: SESSION_TTL_MS, temp: TEMP_KEY_TTL_MS };
|
|
|
|
function timingSafeEqualStrings(a, b) {
|
|
const bufA = Buffer.from(String(a));
|
|
const bufB = Buffer.from(String(b));
|
|
if (bufA.length !== bufB.length) {
|
|
crypto.timingSafeEqual(bufA, bufA);
|
|
return false;
|
|
}
|
|
return crypto.timingSafeEqual(bufA, bufB);
|
|
}
|
|
|
|
function verifyMasterKey(input, masterKey) {
|
|
if (!masterKey || !input) return false;
|
|
return timingSafeEqualStrings(input, masterKey);
|
|
}
|
|
|
|
function sign(payload, secret) {
|
|
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
}
|
|
|
|
function buildToken(kind, secret, now) {
|
|
const payload = `${kind}:${now}`;
|
|
return `${payload}.${sign(payload, secret)}`;
|
|
}
|
|
|
|
function createSessionToken(secret, now = Date.now()) {
|
|
return buildToken("session", secret, now);
|
|
}
|
|
|
|
function createTempKey(secret, now = Date.now()) {
|
|
return buildToken("temp", secret, now);
|
|
}
|
|
|
|
function verifySessionToken(token, secret, now = Date.now()) {
|
|
if (!token || typeof token !== "string") return false;
|
|
const parts = token.split(".");
|
|
if (parts.length !== 2) return false;
|
|
const [payload, signature] = parts;
|
|
if (!timingSafeEqualStrings(signature, sign(payload, secret))) return false;
|
|
const [kind, tsStr] = payload.split(":");
|
|
const ttl = KIND_TTL_MS[kind];
|
|
if (!ttl) return false;
|
|
const issuedAt = Number(tsStr);
|
|
if (!Number.isFinite(issuedAt)) return false;
|
|
return now - issuedAt < ttl;
|
|
}
|
|
|
|
function parseCookies(cookieHeader) {
|
|
const result = {};
|
|
if (!cookieHeader) return result;
|
|
cookieHeader.split(";").forEach((pair) => {
|
|
const idx = pair.indexOf("=");
|
|
if (idx === -1) return;
|
|
const key = pair.slice(0, idx).trim();
|
|
const value = pair.slice(idx + 1).trim();
|
|
if (key) result[key] = decodeURIComponent(value);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function buildSessionCookieHeader(token, maxAgeMs = SESSION_TTL_MS) {
|
|
return [
|
|
`${SESSION_COOKIE_NAME}=${encodeURIComponent(token)}`,
|
|
"HttpOnly",
|
|
"Secure",
|
|
"Path=/auth",
|
|
"SameSite=Strict",
|
|
`Max-Age=${Math.floor(maxAgeMs / 1000)}`,
|
|
].join("; ");
|
|
}
|
|
|
|
function buildLogoutCookieHeader() {
|
|
return [`${SESSION_COOKIE_NAME}=`, "HttpOnly", "Secure", "Path=/auth", "SameSite=Strict", "Max-Age=0"].join("; ");
|
|
}
|
|
|
|
module.exports = {
|
|
SESSION_COOKIE_NAME,
|
|
SESSION_TTL_MS,
|
|
TEMP_KEY_TTL_MS,
|
|
verifyMasterKey,
|
|
createSessionToken,
|
|
createTempKey,
|
|
verifySessionToken,
|
|
parseCookies,
|
|
buildSessionCookieHeader,
|
|
buildLogoutCookieHeader,
|
|
};
|