|
|
@@ -0,0 +1,93 @@
|
|
|
+import crypto from 'node:crypto';
|
|
|
+
|
|
|
+const COOKIE_NAME = 'ppai_session';
|
|
|
+const DEFAULT_MAX_AGE = 12;
|
|
|
+
|
|
|
+const getConfig = () => ({
|
|
|
+ username: process.env.PPAI_AUTH_USER || 'admin',
|
|
|
+ password: process.env.PPAI_AUTH_PASSWORD || 'admin123',
|
|
|
+ secret: process.env.PPAI_AUTH_SECRET || 'ppai-dev-secret',
|
|
|
+ maxAge: Number(process.env.PPAI_AUTH_MAX_AGE || DEFAULT_MAX_AGE) * 60 * 60 * 1000,
|
|
|
+});
|
|
|
+
|
|
|
+const base64UrlEncode = (value) => Buffer.from(value).toString('base64url');
|
|
|
+const base64UrlDecode = (value) => Buffer.from(value, 'base64url').toString();
|
|
|
+
|
|
|
+const sign = (payload, secret) => {
|
|
|
+ return crypto.createHmac('sha256', secret).update(payload).digest('base64url');
|
|
|
+};
|
|
|
+
|
|
|
+const safeEqual = (a = '', b = '') => {
|
|
|
+ const aBuffer = Buffer.from(a);
|
|
|
+ const bBuffer = Buffer.from(b);
|
|
|
+
|
|
|
+ if (aBuffer.length !== bBuffer.length) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return crypto.timingSafeEqual(aBuffer, bBuffer);
|
|
|
+};
|
|
|
+
|
|
|
+export const cookieOptions = () => {
|
|
|
+ const { maxAge } = getConfig();
|
|
|
+
|
|
|
+ return {
|
|
|
+ httpOnly: true,
|
|
|
+ sameSite: 'lax',
|
|
|
+ secure: process.env.NODE_ENV === 'production',
|
|
|
+ maxAge,
|
|
|
+ path: '/',
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+export const clearCookieOptions = () => ({
|
|
|
+ ...cookieOptions(),
|
|
|
+ maxAge: 0,
|
|
|
+});
|
|
|
+
|
|
|
+export const createSession = (username) => {
|
|
|
+ const { secret, maxAge } = getConfig();
|
|
|
+ const payload = base64UrlEncode(JSON.stringify({
|
|
|
+ username,
|
|
|
+ exp: Date.now() + maxAge,
|
|
|
+ }));
|
|
|
+ const signature = sign(payload, secret);
|
|
|
+
|
|
|
+ return `${payload}.${signature}`;
|
|
|
+};
|
|
|
+
|
|
|
+export const verifySession = (token) => {
|
|
|
+ if (!token || typeof token !== 'string') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const [payload, signature] = token.split('.');
|
|
|
+ if (!payload || !signature) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { secret } = getConfig();
|
|
|
+ const expectedSignature = sign(payload, secret);
|
|
|
+ if (!safeEqual(signature, expectedSignature)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const session = JSON.parse(base64UrlDecode(payload));
|
|
|
+ if (!session?.username || !session?.exp || Date.now() > session.exp) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return { username: session.username };
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+export const validateCredentials = (username, password) => {
|
|
|
+ const config = getConfig();
|
|
|
+
|
|
|
+ return safeEqual(username, config.username) && safeEqual(password, config.password);
|
|
|
+};
|
|
|
+
|
|
|
+export const authCookieName = COOKIE_NAME;
|