auth.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { Database } from "bun:sqlite";
  2. const authDB = new Database("./data/auth.sqlite");
  3. authDB.run(`create table if not exists verifications (
  4. email text not null primary key,
  5. token text not null,
  6. code integer not null,
  7. timestamp datetime default current_timestamp
  8. );`)
  9. authDB.run(`create table if not exists users (
  10. email text not null primary key,
  11. name text,
  12. timestamp datetime default current_timestamp
  13. );`)
  14. authDB.run(`create table if not exists sessions (
  15. id text not null primary key,
  16. secretHash text not null,
  17. email text not null,
  18. timestamp datetime default current_timestamp
  19. );`)
  20. import nodemailer from "nodemailer"
  21. const transporter = nodemailer.createTransport({
  22. host: process.env.AUTH_EMAIL_SMTP_HOSTNAME,
  23. port: process.env.AUTH_EMAIL_SMTP_PORT,
  24. secure: true, // Use true for port 465, false for port 587
  25. auth: {
  26. user: process.env.AUTH_EMAIL_SMTP_USERNAME,
  27. pass: process.env.AUTH_EMAIL_SMTP_PASSWORD,
  28. },
  29. });
  30. export default class Auth {
  31. static addVerification(email) {
  32. const token = generateSecureRandomString()
  33. const code = generateSecureCode()
  34. authDB.prepare(`
  35. INSERT INTO verifications (email, token, code)
  36. VALUES (?, ?, ?)
  37. ON CONFLICT(email) DO UPDATE SET
  38. token = excluded.token,
  39. code = excluded.code;
  40. `).run(email, token, code)
  41. return { email, token, code }
  42. }
  43. static async sendVerification(email, subject, text, html) {
  44. return await transporter.sendMail({
  45. from: '"auth — meteostanica" <auth@meteostanica.com>',
  46. to: email,
  47. subject,
  48. text, // Plain-text version of the message
  49. html, // HTML version of the message
  50. });
  51. }
  52. static getVerification(token) {
  53. const statement = authDB.prepare(`
  54. SELECT *,
  55. ( CASE
  56. WHEN (strftime('%s', 'now') - strftime('%s', timestamp)) > $seconds THEN 0
  57. ELSE 1
  58. END
  59. ) AS valid
  60. FROM verifications
  61. WHERE token = $token;
  62. `)
  63. const result = statement.get({
  64. $seconds: process.env.EMAIL_VERIFICATION_TIMEOUT,
  65. $token: token
  66. });
  67. return result;
  68. }
  69. static removeVerification(token) {
  70. authDB.prepare(`
  71. DELETE
  72. FROM verifications
  73. WHERE token = ?;
  74. `).run(token)
  75. }
  76. static addUser(email) {
  77. const statement = authDB.prepare("INSERT INTO users (email) VALUES (?);")
  78. statement.run(
  79. email
  80. );
  81. return { email };
  82. }
  83. static getUser(email) {
  84. const statement = authDB.prepare("select * from users where email = ?;")
  85. const result = statement.get(
  86. email
  87. );
  88. return result;
  89. }
  90. static async createSession(email) {
  91. const id = generateSecureRandomString();
  92. const secret = generateSecureRandomString();
  93. const secretHash = await hashSecret(secret);
  94. const token = id + "." + secret;
  95. const statement = authDB.prepare("INSERT INTO sessions (id, secretHash, email) VALUES (?, ?, ?);")
  96. statement.run(
  97. id,
  98. secretHash,
  99. email
  100. );
  101. return { id, secretHash, token, email };
  102. }
  103. static async getSession(token) {
  104. if (!token) return null
  105. const [id, secret] = token.split(".")
  106. const statement = authDB.prepare(`
  107. SELECT *,
  108. ( CASE
  109. WHEN (strftime('%s', 'now') - strftime('%s', timestamp)) > $seconds THEN 0
  110. ELSE 1
  111. END
  112. ) AS valid
  113. FROM sessions
  114. WHERE id = $id AND secretHash = $secretHash;
  115. `)
  116. const result = statement.get({
  117. $seconds: process.env.SESSION_TIMEOUT,
  118. $id: id,
  119. $secretHash: await hashSecret(secret)
  120. });
  121. return result
  122. }
  123. static removeSession(id) {
  124. authDB.prepare(`
  125. DELETE
  126. FROM sessions
  127. WHERE id = $id;
  128. `).run(id)
  129. }
  130. }
  131. function generateSecureCode() {
  132. // Generate a random 32-bit unsigned integer
  133. const array = new Uint32Array(1);
  134. crypto.getRandomValues(array);
  135. // Use modulo to get a value within 1,000,000
  136. // Then pad with leading zeros if you want 000000-999999
  137. // Or adjust the math if you strictly want 100,000-999,999
  138. const number = array[0] % 1000000;
  139. return number.toString().padStart(6, '0');
  140. }
  141. function generateSecureRandomString() {
  142. // Human readable alphabet (a-z, 0-9 without l, o, 0, 1 to avoid confusion)
  143. const alphabet = "abcdefghijkmnpqrstuvwxyz23456789";
  144. // Generate 24 bytes = 192 bits of entropy.
  145. // We're only going to use 5 bits per byte so the total entropy will be 192 * 5 / 8 = 120 bits
  146. const bytes = new Uint8Array(24);
  147. crypto.getRandomValues(bytes);
  148. let id = "";
  149. for (let i = 0; i < bytes.length; i++) {
  150. // >> 3 "removes" the right-most 3 bits of the byte
  151. id += alphabet[bytes[i] >> 3];
  152. }
  153. return id;
  154. }
  155. async function hashSecret(secret) {
  156. const secretBytes = new TextEncoder().encode(secret);
  157. const secretHashBuffer = await crypto.subtle.digest("SHA-256", secretBytes);
  158. return new Uint8Array(secretHashBuffer);
  159. }