feat(auth): support schema-qualified SQL stores
This commit is contained in:
1 parent
a85cbf240d
commit
681679483a
5 files changed
+48
-4
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/auth",
|
||||
"version": "0.8.19",
|
||||
"version": "0.8.20",
|
||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -8,6 +8,7 @@ export {
|
||||
export type { AuthStore } from "./store.ts";
|
||||
export { MemoryAuthStore } from "./stores/memory.ts";
|
||||
export { SqlAuthStore } from "./stores/sql.ts";
|
||||
export type { SqlAuthStoreOptions } from "./stores/sql.ts";
|
||||
export {
|
||||
authSession,
|
||||
requireAuth,
|
||||
|
||||
@@ -34,10 +34,31 @@ function placeholders(sql: string, dialect: string): string {
|
||||
return sql.replace(/\?/g, () => `$${++index}`);
|
||||
}
|
||||
|
||||
export interface SqlAuthStoreOptions {
|
||||
/** PostgreSQL schema owning authentication tables. */
|
||||
schema?: string;
|
||||
/** Defaults to wrn_auth_; use an empty prefix for e.g. sso.users. */
|
||||
tablePrefix?: string;
|
||||
}
|
||||
|
||||
export class SqlAuthStore implements AuthStore {
|
||||
constructor(private readonly db: Db) {}
|
||||
constructor(
|
||||
private readonly db: Db,
|
||||
private readonly options: SqlAuthStoreOptions = {},
|
||||
) {
|
||||
if (options.schema && !/^[a-z_][a-z0-9_]*$/i.test(options.schema))
|
||||
throw new Error("Invalid auth schema identifier");
|
||||
if (options.tablePrefix && !/^[a-z_][a-z0-9_]*$/i.test(options.tablePrefix))
|
||||
throw new Error("Invalid auth table prefix");
|
||||
if (options.schema && db.driver.dialect !== "postgres")
|
||||
throw new Error("Auth schema requires PostgreSQL");
|
||||
}
|
||||
private sql(value: string): string {
|
||||
return placeholders(value, this.db.driver.dialect);
|
||||
const qualified = value.replace(/\bwrn_auth_([a-z_]+)\b/g, (_match, table: string) => {
|
||||
const name = (this.options.tablePrefix ?? "wrn_auth_") + table;
|
||||
return this.options.schema ? '"' + this.options.schema + '"."' + name + '"' : name;
|
||||
});
|
||||
return placeholders(qualified, this.db.driver.dialect);
|
||||
}
|
||||
private async one(sql: string, params: unknown[] = []): Promise<Row | undefined> {
|
||||
return (await this.db.one(this.sql(sql), params)) ?? undefined;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { Db } from "@wrnexus/db";
|
||||
import { SqlAuthStore } from "../src/stores/sql.ts";
|
||||
|
||||
test("auth store qualifies tables without changing parameter values", async () => {
|
||||
let query = "";
|
||||
let values: unknown[] = [];
|
||||
const db = {
|
||||
driver: { dialect: "postgres" },
|
||||
one: async (sql: string, params: unknown[]) => {
|
||||
query = sql;
|
||||
values = params;
|
||||
return undefined;
|
||||
},
|
||||
} as unknown as Db;
|
||||
const store = new SqlAuthStore(db, { schema: "sso", tablePrefix: "" });
|
||||
await store.findUserById("wrn_auth_users");
|
||||
expect(query).toContain('FROM "sso"."users"');
|
||||
expect(query).toContain("$1");
|
||||
expect(values).toEqual(["wrn_auth_users"]);
|
||||
expect(() => new SqlAuthStore(db, { schema: 'sso";DROP TABLE users' })).toThrow();
|
||||
});
|
||||
Reference in new issue
Block a user