| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- import { Database } from "bun:sqlite";
- const authDB = new Database("./data/auth.sqlite");
- authDB.run(`create table if not exists verifications (
- email text not null primary key,
- token text not null,
- code integer not null,
- timestamp datetime default current_timestamp
- );`)
- authDB.run(`create table if not exists users (
- email text not null primary key,
- name text,
- timestamp datetime default current_timestamp
- );`)
- authDB.run(`create table if not exists sessions (
- id text not null primary key,
- secretHash text not null,
- email text not null,
- timestamp datetime default current_timestamp
- );`)
- import nodemailer from "nodemailer"
- const transporter = nodemailer.createTransport({
- host: process.env.AUTH_EMAIL_SMTP_HOSTNAME,
- port: process.env.AUTH_EMAIL_SMTP_PORT,
- secure: true, // Use true for port 465, false for port 587
- auth: {
- user: process.env.AUTH_EMAIL_SMTP_USERNAME,
- pass: process.env.AUTH_EMAIL_SMTP_PASSWORD,
- },
- });
- export default class Auth {
- static addVerification(email) {
- const token = generateSecureRandomString()
- const code = generateSecureCode()
- authDB.prepare(`
- INSERT INTO verifications (email, token, code)
- VALUES (?, ?, ?)
- ON CONFLICT(email) DO UPDATE SET
- token = excluded.token,
- code = excluded.code;
- `).run(email, token, code)
- return { email, token, code }
- }
- static async sendVerification(email, subject, text, html) {
- return await transporter.sendMail({
- from: '"auth — meteostanica" <auth@meteostanica.com>',
- to: email,
- subject,
- text, // Plain-text version of the message
- html, // HTML version of the message
- });
- }
- static getVerification(token) {
- const statement = authDB.prepare(`
- SELECT *,
- ( CASE
- WHEN (strftime('%s', 'now') - strftime('%s', timestamp)) > $seconds THEN 0
- ELSE 1
- END
- ) AS valid
- FROM verifications
- WHERE token = $token;
- `)
- const result = statement.get({
- $seconds: process.env.EMAIL_VERIFICATION_TIMEOUT,
- $token: token
- });
- return result;
- }
- static removeVerification(token) {
- authDB.prepare(`
- DELETE
- FROM verifications
- WHERE token = ?;
- `).run(token)
- }
- static addUser(email) {
- const statement = authDB.prepare("INSERT INTO users (email) VALUES (?);")
- statement.run(
- email
- );
- return { email };
- }
- static getUser(email) {
- const statement = authDB.prepare("select * from users where email = ?;")
- const result = statement.get(
- email
- );
- return result;
- }
- static async createSession(email) {
- const id = generateSecureRandomString();
- const secret = generateSecureRandomString();
- const secretHash = await hashSecret(secret);
- const token = id + "." + secret;
- const statement = authDB.prepare("INSERT INTO sessions (id, secretHash, email) VALUES (?, ?, ?);")
- statement.run(
- id,
- secretHash,
- email
- );
- return { id, secretHash, token, email };
- }
- static async getSession(token) {
- if (!token) return null
- const [id, secret] = token.split(".")
- const statement = authDB.prepare(`
- SELECT *,
- ( CASE
- WHEN (strftime('%s', 'now') - strftime('%s', timestamp)) > $seconds THEN 0
- ELSE 1
- END
- ) AS valid
- FROM sessions
- WHERE id = $id AND secretHash = $secretHash;
- `)
- const result = statement.get({
- $seconds: process.env.SESSION_TIMEOUT,
- $id: id,
- $secretHash: await hashSecret(secret)
- });
- return result
- }
- static removeSession(id) {
- authDB.prepare(`
- DELETE
- FROM sessions
- WHERE id = $id;
- `).run(id)
- }
- }
- function generateSecureCode() {
- // Generate a random 32-bit unsigned integer
- const array = new Uint32Array(1);
- crypto.getRandomValues(array);
- // Use modulo to get a value within 1,000,000
- // Then pad with leading zeros if you want 000000-999999
- // Or adjust the math if you strictly want 100,000-999,999
- const number = array[0] % 1000000;
- return number.toString().padStart(6, '0');
- }
- function generateSecureRandomString() {
- // Human readable alphabet (a-z, 0-9 without l, o, 0, 1 to avoid confusion)
- const alphabet = "abcdefghijkmnpqrstuvwxyz23456789";
- // Generate 24 bytes = 192 bits of entropy.
- // We're only going to use 5 bits per byte so the total entropy will be 192 * 5 / 8 = 120 bits
- const bytes = new Uint8Array(24);
- crypto.getRandomValues(bytes);
- let id = "";
- for (let i = 0; i < bytes.length; i++) {
- // >> 3 "removes" the right-most 3 bits of the byte
- id += alphabet[bytes[i] >> 3];
- }
- return id;
- }
- async function hashSecret(secret) {
- const secretBytes = new TextEncoder().encode(secret);
- const secretHashBuffer = await crypto.subtle.digest("SHA-256", secretBytes);
- return new Uint8Array(secretHashBuffer);
- }
|