auth.js 2.8 KB

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