67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
import { authId, matchingStep, type AuthConfig } from "./auth";
|
|
import { openSiteDatabase } from "./site-database";
|
|
|
|
function database() {
|
|
const db = openSiteDatabase();
|
|
db.exec(`PRAGMA busy_timeout=5000;
|
|
CREATE TABLE IF NOT EXISTS login_state (
|
|
id TEXT PRIMARY KEY, window_start INTEGER NOT NULL,
|
|
attempts INTEGER NOT NULL DEFAULT 0, last_step INTEGER NOT NULL DEFAULT -1
|
|
);`);
|
|
return db;
|
|
}
|
|
|
|
// SQLite's write transaction serializes attempts across workers and restarts.
|
|
export function consumeCode(
|
|
config: AuthConfig,
|
|
code: string,
|
|
now = Date.now(),
|
|
): "ok" | "invalid" | "limited" {
|
|
const db = database();
|
|
try {
|
|
db.exec("BEGIN IMMEDIATE");
|
|
const id = authId(config);
|
|
db.prepare(
|
|
"INSERT OR IGNORE INTO login_state(id,window_start) VALUES (?,?)",
|
|
).run(id, now);
|
|
const state = db
|
|
.prepare("SELECT * FROM login_state WHERE id=?")
|
|
.get(id) as {
|
|
window_start: number;
|
|
attempts: number;
|
|
last_step: number;
|
|
};
|
|
const expired = now - state.window_start >= 300000;
|
|
const attempts = expired ? 0 : state.attempts;
|
|
let result: "ok" | "invalid" | "limited" = "limited";
|
|
if (attempts < 20) {
|
|
db.prepare(
|
|
"UPDATE login_state SET attempts=?,window_start=? WHERE id=?",
|
|
).run(attempts + 1, expired ? now : state.window_start, id);
|
|
const step = matchingStep(config.secret, code, now);
|
|
result = step !== null && step > state.last_step ? "ok" : "invalid";
|
|
if (result === "ok")
|
|
db.prepare("UPDATE login_state SET last_step=? WHERE id=?").run(
|
|
step!,
|
|
id,
|
|
);
|
|
}
|
|
db.exec("COMMIT");
|
|
return result;
|
|
} catch (error) {
|
|
try {
|
|
db.exec("ROLLBACK");
|
|
} catch {
|
|
/* Transaction may not have started. */
|
|
}
|
|
throw error;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export function initializeStore() {
|
|
const db = database();
|
|
db.close();
|
|
}
|