feat: admin controls
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import type { Transaction } from "./auth-transaction";
|
||||
import { and, eq, isNotNull, ne, or, isNull, sql, like } from "drizzle-orm";
|
||||
import { accounts, users, verifications } from "../../drizzle/schema";
|
||||
|
||||
const ADMIN_LOCK_ID = 7_261_009;
|
||||
|
||||
// Deletion, banning and demotion must all hold this lock until commit.
|
||||
export const lockAdministration = async (tx: Transaction): Promise<void> => {
|
||||
await tx.execute(sql`select pg_advisory_xact_lock(${ADMIN_LOCK_ID})`);
|
||||
};
|
||||
|
||||
export const hasUsableAdministrator = async (tx: Transaction, excludedUserId?: string): Promise<boolean> => {
|
||||
const rows = await tx.select({ id: users.id })
|
||||
.from(users)
|
||||
.innerJoin(accounts, eq(accounts.userId, users.id))
|
||||
.where(and(
|
||||
eq(users.role, "admin"),
|
||||
excludedUserId === undefined ? undefined : ne(users.id, excludedUserId),
|
||||
or(eq(users.banned, false), isNull(users.banned)),
|
||||
eq(accounts.providerId, "credential"),
|
||||
isNotNull(accounts.password),
|
||||
ne(accounts.password, ""),
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
return rows.length > 0;
|
||||
};
|
||||
|
||||
// All credential, recovery-link and provider-config writes use the user row
|
||||
// as their common lock, including when no credential or recovery row exists.
|
||||
export const lockAccount = async (tx: Transaction, userId: string): Promise<boolean> => {
|
||||
const rows = await tx.select({ id: users.id }).from(users)
|
||||
.where(eq(users.id, userId)).for("update");
|
||||
|
||||
return rows.length > 0;
|
||||
};
|
||||
|
||||
export const revokeTwoFactorTrust = async (tx: Transaction, userId: string): Promise<void> => {
|
||||
await tx.delete(verifications).where(and(
|
||||
eq(verifications.value, userId),
|
||||
or(
|
||||
like(verifications.identifier, "trust-device-%"),
|
||||
like(verifications.identifier, "2fa-%"),
|
||||
),
|
||||
));
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import { db } from "./db";
|
||||
|
||||
export type Transaction = Parameters<Parameters<typeof db.transaction>[0]>[0];
|
||||
|
||||
const storage = new AsyncLocalStorage<Transaction>();
|
||||
|
||||
/** The transaction opened by authTransaction() around the running code, if any. */
|
||||
export const currentTransaction = (): Transaction | undefined => storage.getStore();
|
||||
|
||||
// What the one auth instance is given as its database. Every access goes to
|
||||
// the transaction opened by authTransaction() around the running code, or to
|
||||
// the pool when there is none, so no second auth instance is needed to make
|
||||
// Better Auth write on a transaction's connection.
|
||||
export const authDatabase = new Proxy(db, {
|
||||
get(_target, property) {
|
||||
const target = storage.getStore() ?? db;
|
||||
const value = Reflect.get(target, property, target);
|
||||
|
||||
return typeof value === "function" ? value.bind(target) : value;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Runs the callback in a transaction that everything the auth instance does
|
||||
* inside it joins: its endpoints, its hooks and its internal adapter all
|
||||
* commit or roll back together with the callback's own writes.
|
||||
*/
|
||||
export const authTransaction = <T>(callback: (tx: Transaction) => Promise<T>): Promise<T> =>
|
||||
db.transaction(tx => storage.run(tx, () => callback(tx)));
|
||||
Reference in New Issue
Block a user