auth.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import crypto from 'node:crypto';
  2. const COOKIE_NAME = 'ppai_session';
  3. const DEFAULT_MAX_AGE = 12;
  4. const DEFAULT_REFRESH_THRESHOLD = 2;
  5. const getConfig = () => {
  6. return {
  7. username: process.env.PPAI_AUTH_USER || 'admin',
  8. password: process.env.PPAI_AUTH_PASSWORD || 'admin123',
  9. secret: process.env.PPAI_AUTH_SECRET || 'ppai-dev-secret',
  10. maxAge: Number(process.env.PPAI_AUTH_MAX_AGE || DEFAULT_MAX_AGE) * 60 * 60 * 1000,
  11. refreshThreshold: Number(process.env.PPAI_AUTH_REFRESH_THRESHOLD || DEFAULT_REFRESH_THRESHOLD) * 60 * 60 * 1000,
  12. };
  13. };
  14. const base64UrlEncode = (value) => Buffer.from(value).toString('base64url');
  15. const base64UrlDecode = (value) => Buffer.from(value, 'base64url').toString();
  16. const sign = (payload, secret) => {
  17. return crypto.createHmac('sha256', secret).update(payload).digest('base64url');
  18. };
  19. export const safeEqual = (a = '', b = '') => {
  20. const aBuffer = Buffer.from(a);
  21. const bBuffer = Buffer.from(b);
  22. if (aBuffer.length !== bBuffer.length) {
  23. return false;
  24. }
  25. return crypto.timingSafeEqual(aBuffer, bBuffer);
  26. };
  27. export const cookieOptions = () => {
  28. const { maxAge } = getConfig();
  29. return {
  30. httpOnly: true,
  31. sameSite: 'lax',
  32. secure: process.env.NODE_ENV === 'production',
  33. maxAge,
  34. path: '/',
  35. };
  36. };
  37. export const clearCookieOptions = () => {
  38. const { maxAge, ...options } = cookieOptions();
  39. return options;
  40. };
  41. export const createSession = (username) => {
  42. const { secret, maxAge } = getConfig();
  43. const payload = base64UrlEncode(JSON.stringify({
  44. username,
  45. exp: Date.now() + maxAge,
  46. }));
  47. const signature = sign(payload, secret);
  48. return `${payload}.${signature}`;
  49. };
  50. export const refreshSessionIfNeeded = (res, session) => {
  51. if (!session?.username || !session?.exp) {
  52. return false;
  53. }
  54. const { refreshThreshold } = getConfig();
  55. const remainingTime = session.exp - Date.now();
  56. if (remainingTime > refreshThreshold) {
  57. return false;
  58. }
  59. res.cookie(authCookieName, createSession(session.username), cookieOptions());
  60. return true;
  61. };
  62. export const verifySession = (token) => {
  63. if (!token || typeof token !== 'string') {
  64. return null;
  65. }
  66. const [payload, signature] = token.split('.');
  67. if (!payload || !signature) {
  68. return null;
  69. }
  70. const { secret } = getConfig();
  71. const expectedSignature = sign(payload, secret);
  72. if (!safeEqual(signature, expectedSignature)) {
  73. return null;
  74. }
  75. try {
  76. const session = JSON.parse(base64UrlDecode(payload));
  77. if (!session?.username || !session?.exp || Date.now() > session.exp) {
  78. return null;
  79. }
  80. return { username: session.username, exp: session.exp };
  81. }
  82. catch {
  83. return null;
  84. }
  85. };
  86. export const validateCredentials = (username, password) => {
  87. const config = getConfig();
  88. return safeEqual(username, config.username) && safeEqual(password, config.password);
  89. };
  90. export const authCookieName = COOKIE_NAME;