auth.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { Elysia } from 'elysia'
  2. import { Eta } from "eta"
  3. const eta = new Eta({ views: "./templates" })
  4. import ratelimits from '../../utils/ratelimits'
  5. import normalizeEmail from '../../utils/normalizeEmail';
  6. import Auth from '../../utils/auth';
  7. import validateTurnstile from '../../utils/validateTurnstile';
  8. import Meteostanice from '../../utils/meteostanice';
  9. export default (langName, lang) => new Elysia({ prefix: "/auth" })
  10. .get("/", async ({ cookie, redirect, query, set }) => {
  11. const token = cookie.session.value
  12. const session = await Auth.getSession(token)
  13. if (session?.valid) {
  14. return redirect(`/${langName === "sk" ? `` : `${langName}/`}panel`)
  15. }
  16. const error = query?.error
  17. const duration = query?.duration
  18. set.headers['content-type'] = 'text/html; charset=utf8'
  19. return eta.render(`${langName}/auth/index`, { siteKey: process.env.TURNSTILE_SITE_KEY, lang, error, errorDetails: { duration } })
  20. })
  21. .post("/", async ({ request, server, body, redirect }) => {
  22. const clientIP = request.headers.get('x-forwarded-for') ?? server.requestIP(request).address
  23. const email = body?.email
  24. if (!normalizeEmail(email)) return redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=invalidEmail`)
  25. const hourlyEmailRatelimit = ratelimits("authEmailHourly", email, process.env.AUTH_EMAIL_HOURLY_RATELIMIT, 3600)
  26. if (hourlyEmailRatelimit.status) return Response.redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=ratelimits.email&duration=${hourlyEmailRatelimit.duration}`)
  27. const hourlyIPRatelimit = ratelimits("authIPHourly", clientIP, process.env.AUTH_IP_HOURLY_RATELIMIT, 3600)
  28. if (hourlyIPRatelimit.status) return Response.redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=ratelimits.ip&duration=${hourlyIPRatelimit.duration}`)
  29. const dailyIPRatelimit = ratelimits("authIPDaily", clientIP, process.env.AUTH_IP_DAILY_RATELIMIT, 86400)
  30. if (dailyIPRatelimit.status) return Response.redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=ratelimits.ip&duration=${dailyIPRatelimit.duration}`)
  31. const turnstileResponse = body?.["cf-turnstile-response"]
  32. if (!turnstileResponse) return redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=turnstile.noToken`)
  33. const turnstileValid = await validateTurnstile(turnstileResponse, clientIP)
  34. if (!turnstileValid.success) {
  35. let errorMessage = `turnstile.unavailable`;
  36. if (turnstileValid["error-codes"]?.includes("invalid-input-response"))
  37. errorMessage = `turnstile.invalidResponse`
  38. if (turnstileValid["error-codes"]?.includes("timeout-or-duplicate"))
  39. errorMessage = `turnstile.keyUsedOrExpired`
  40. return redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=${errorMessage}`)
  41. }
  42. const verification = Auth.addVerification(email)
  43. const emailLink = `${process.env.BASE_URL}/${langName === "sk" ? `` : `${langName}/`}auth/verify?token=${verification.token}&code=${verification.code}`
  44. await Auth.sendVerification(email, lang.emails.auth.subject(), lang.emails.auth.text({ code: verification.code, link: emailLink }), eta.render(`${langName}/email/auth`, { code: verification.code, link: emailLink }))
  45. return redirect(`/${langName === "sk" ? `` : `${langName}/`}auth/verify?token=${verification.token}`)
  46. })
  47. .get("/verify", async ({ query, redirect, set, cookie }) => {
  48. const token = query?.token
  49. const code = Number.parseInt(query?.code)
  50. if (!token) {
  51. return redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=noVerificationToken`)
  52. }
  53. const verification = Auth.getVerification(token)
  54. if (!verification?.valid) {
  55. return redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=verificationTokenUsedOrExpired`)
  56. }
  57. if (!code) {
  58. set.headers['content-type'] = 'text/html; charset=utf8'
  59. return eta.render(`${langName}/auth/verify`, { token })
  60. }
  61. if (code !== verification.code) {
  62. const codeRatelimit = ratelimits("authCodeHourly", token, process.env.AUTH_CODE_HOURLY_RATELIMIT, 3600)
  63. if (codeRatelimit.status) return eta.render(`${langName}/auth/verify`, { token, lang, error: `ratelimits.code` })
  64. set.headers['content-type'] = 'text/html; charset=utf8'
  65. return eta.render(`${langName}/auth/verify`, { token, lang, error: `invalidVerificationCode` })
  66. }
  67. Auth.removeVerification(token)
  68. let user = Auth.getUser(verification.email)
  69. if (!user) {
  70. user = Auth.addUser(verification.email)
  71. }
  72. const session = await Auth.createSession(verification.email)
  73. cookie.session.set({
  74. value: session.token,
  75. expires: new Date(2147483647 * 1000),
  76. maxAge: new Date(2147483647 * 1000),
  77. domain: process.env.BASE_URL.replace(/^https?:\/\/(www\.)?|:\d+/gi, ''),
  78. httpOnly: true
  79. })
  80. return redirect(`/${langName === "sk" ? `` : `${langName}/`}panel`)
  81. })
  82. .get("/logout", async ({ cookie, redirect }) => {
  83. const token = cookie.session.value
  84. const session = await Auth.getSession(token)
  85. if (session?.valid) {
  86. Auth.removeSession(session.id)
  87. delete cookie.session
  88. }
  89. return redirect(`/${langName === "sk" ? `` : langName}`)
  90. })
  91. .get("/delete", async ({ cookie, redirect }) => {
  92. const token = cookie.session.value
  93. const session = await Auth.getSession(token)
  94. if (!session?.valid) {
  95. return redirect(`/${langName === "sk" ? `` : `${langName}/`}auth?error=loginNeeded`)
  96. }
  97. delete cookie.session
  98. Auth.deleteUser(session.email)
  99. return redirect(`/${langName === "sk" ? `` : langName}`)
  100. })