feat: auth & db
This commit is contained in:
parent
8e2ad2174f
commit
5e63ffb18c
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -9,8 +9,7 @@
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# database
|
# database
|
||||||
/prisma/db.sqlite
|
/data/
|
||||||
/prisma/db.sqlite-journal
|
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
|
|
10
drizzle.config.ts
Normal file
10
drizzle.config.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import type { Config } from "drizzle-kit";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
schema: "./src/server/db/schema.ts",
|
||||||
|
driver: "better-sqlite",
|
||||||
|
dbCredentials: {
|
||||||
|
url: env.DATABASE_PATH,
|
||||||
|
},
|
||||||
|
} satisfies Config;
|
BIN
exts/sqlite-uuidv7.so
Normal file
BIN
exts/sqlite-uuidv7.so
Normal file
Binary file not shown.
|
@ -12,7 +12,6 @@
|
||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.1.1",
|
|
||||||
"@prisma/migrate": "^5.4.1",
|
"@prisma/migrate": "^5.4.1",
|
||||||
"@t3-oss/env-nextjs": "^0.6.0",
|
"@t3-oss/env-nextjs": "^0.6.0",
|
||||||
"@tanstack/react-query": "^4.32.6",
|
"@tanstack/react-query": "^4.32.6",
|
||||||
|
@ -20,8 +19,10 @@
|
||||||
"@trpc/next": "^10.37.1",
|
"@trpc/next": "^10.37.1",
|
||||||
"@trpc/react-query": "^10.37.1",
|
"@trpc/react-query": "^10.37.1",
|
||||||
"@trpc/server": "^10.37.1",
|
"@trpc/server": "^10.37.1",
|
||||||
|
"better-sqlite3": "^9.0.0",
|
||||||
"bunyan": "^1.8.15",
|
"bunyan": "^1.8.15",
|
||||||
"bunyan-format": "^0.2.1",
|
"bunyan-format": "^0.2.1",
|
||||||
|
"drizzle-orm": "^0.28.6",
|
||||||
"next": "^13.4.19",
|
"next": "^13.4.19",
|
||||||
"next-auth": "^4.23.2",
|
"next-auth": "^4.23.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.6",
|
||||||
"@types/bunyan": "^1.8.9",
|
"@types/bunyan": "^1.8.9",
|
||||||
"@types/bunyan-format": "^0.2.6",
|
"@types/bunyan-format": "^0.2.6",
|
||||||
"@types/eslint": "^8.44.2",
|
"@types/eslint": "^8.44.2",
|
||||||
|
@ -40,12 +42,12 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||||
"@typescript-eslint/parser": "^6.3.0",
|
"@typescript-eslint/parser": "^6.3.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
|
"drizzle-kit": "^0.19.13",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-config-next": "^13.4.19",
|
"eslint-config-next": "^13.4.19",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.1",
|
"prettier-plugin-tailwindcss": "^0.5.1",
|
||||||
"prisma": "^5.1.1",
|
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
|
|
725
pnpm-lock.yaml
725
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,20 +0,0 @@
|
||||||
// This is your Prisma schema file,
|
|
||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
||||||
|
|
||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "sqlite"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Post {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
name String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@index([name])
|
|
||||||
}
|
|
15
src/env.mjs
15
src/env.mjs
|
@ -7,16 +7,13 @@ export const env = createEnv({
|
||||||
* isn't built with invalid env vars.
|
* isn't built with invalid env vars.
|
||||||
*/
|
*/
|
||||||
server: {
|
server: {
|
||||||
DATABASE_URL: z
|
DATABASE_PATH: z.string().default("./data/db.sqlite"),
|
||||||
.string()
|
|
||||||
.url()
|
|
||||||
.refine(
|
|
||||||
(str) => !str.includes("YOUR_MYSQL_URL_HERE"),
|
|
||||||
"You forgot to change the default URL"
|
|
||||||
),
|
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "test", "production"])
|
.enum(["development", "test", "production"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
|
|
||||||
|
SQLITE_UUIDV7_EXT_PATH: z.string().optional(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,9 +30,9 @@ export const env = createEnv({
|
||||||
* middlewares) or client-side so we need to destruct manually.
|
* middlewares) or client-side so we need to destruct manually.
|
||||||
*/
|
*/
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
DATABASE_URL: process.env.DATABASE_URL,
|
DATABASE_PATH: process.env.DATABASE_PATH,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
SQLITE_UUIDV7_EXT_PATH: process.env.SQLITE_UUIDV7_EXT_PATH,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
|
|
@ -4,9 +4,13 @@ const { version } = pacakge;
|
||||||
export async function register() {
|
export async function register() {
|
||||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
const logger = (await import("./server/utils/logger")).default;
|
const logger = (await import("./server/utils/logger")).default;
|
||||||
const { migrate } = await import("./server/db/migrate");
|
const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
|
||||||
|
const { db } = await import("./server/db");
|
||||||
|
|
||||||
logger.info(`🚀 Hostforge v${version} starting up`);
|
logger.child({ module: "database" }).info("⚙️ Migrating database");
|
||||||
await migrate();
|
migrate(db, { migrationsFolder: "./migrations" });
|
||||||
|
logger.child({ module: "database" }).info("✅ Database migrated");
|
||||||
|
|
||||||
|
logger.info(`🚀 Hostforge v${version} ready!`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
||||||
* need to use are documented accordingly near the end.
|
* need to use are documented accordingly near the end.
|
||||||
*/
|
*/
|
||||||
import { initTRPC } from "@trpc/server";
|
import { TRPCError, initTRPC } from "@trpc/server";
|
||||||
import { type NextRequest } from "next/server";
|
import { type NextRequest } from "next/server";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
|
import { Session } from "../auth/Session";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
|
@ -20,9 +21,9 @@ import { db } from "~/server/db";
|
||||||
*
|
*
|
||||||
* These allow you to access things when processing a request, like the database, the session, etc.
|
* These allow you to access things when processing a request, like the database, the session, etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface CreateContextOptions {
|
interface CreateContextOptions {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
|
session: Session | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,6 +39,7 @@ interface CreateContextOptions {
|
||||||
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
return {
|
return {
|
||||||
headers: opts.headers,
|
headers: opts.headers,
|
||||||
|
session: opts.session,
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -48,11 +50,20 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/context
|
* @see https://trpc.io/docs/context
|
||||||
*/
|
*/
|
||||||
export const createTRPCContext = (opts: { req: NextRequest }) => {
|
export const createTRPCContext = async (opts: { req: NextRequest }) => {
|
||||||
// Fetch stuff that depends on the request
|
// resolve session data
|
||||||
|
const sessionToken = opts.req.cookies.get("session")?.value;
|
||||||
|
|
||||||
|
const session: Session | null = sessionToken
|
||||||
|
? await Session.fetchFromTokenAndUpdate(sessionToken, {
|
||||||
|
ip: opts.req.ip,
|
||||||
|
ua: opts.req.headers.get("user-agent") ?? undefined,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
return createInnerTRPCContext({
|
||||||
headers: opts.req.headers,
|
headers: opts.req.headers,
|
||||||
|
session,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,8 +74,7 @@ export const createTRPCContext = (opts: { req: NextRequest }) => {
|
||||||
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
|
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
|
||||||
* errors on the backend.
|
* errors on the backend.
|
||||||
*/
|
*/
|
||||||
|
export const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
errorFormatter({ shape, error }) {
|
errorFormatter({ shape, error }) {
|
||||||
return {
|
return {
|
||||||
|
@ -100,3 +110,26 @@ export const createTRPCRouter = t.router;
|
||||||
* are logged in.
|
* are logged in.
|
||||||
*/
|
*/
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticated procedure
|
||||||
|
*
|
||||||
|
* This is the base piece you use to build new queries and mutations on your tRPC API. It guarantees
|
||||||
|
* that a user querying is authorized, and you can access user session data.
|
||||||
|
*/
|
||||||
|
export const authenticatedProcedure = t.procedure.use(
|
||||||
|
t.middleware(({ ctx, next }) => {
|
||||||
|
if (!ctx.session) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You must be logged in to perform this action.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
session: ctx.session,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
55
src/server/auth/Session.ts
Normal file
55
src/server/auth/Session.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { users, sessions } from "../db/schema";
|
||||||
|
|
||||||
|
export class Session {
|
||||||
|
/**
|
||||||
|
* Fetch a session from a session token.
|
||||||
|
* @param token The session cookie
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static async fetchFromToken(token: string) {
|
||||||
|
const [sessionData] = await db
|
||||||
|
.select()
|
||||||
|
.from(sessions)
|
||||||
|
.where(eq(sessions.token, token));
|
||||||
|
|
||||||
|
if (!sessionData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Session(sessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to fetchFromToken, but also updates the session's lastAccessed, lastIP, and lastUA fields.
|
||||||
|
* @param token The session cookie
|
||||||
|
* @param context The context of the request
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static async fetchFromTokenAndUpdate(
|
||||||
|
token: string,
|
||||||
|
context: {
|
||||||
|
ip?: string;
|
||||||
|
ua?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const [sessionData] = await db
|
||||||
|
.update(sessions)
|
||||||
|
.set({ lastAccessed: new Date(), lastIP: context.ip, lastUA: context.ua })
|
||||||
|
.where(eq(sessions.token, token))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!sessionData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Session(sessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new session instance from a user's session data.
|
||||||
|
* @param sessionData The user's session data.
|
||||||
|
*/
|
||||||
|
constructor(public readonly data: typeof sessions.$inferSelect) {}
|
||||||
|
}
|
|
@ -1,16 +1,18 @@
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
|
import SQLite3 from "better-sqlite3";
|
||||||
|
import { join } from "path";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
const sqlite = new SQLite3(env.DATABASE_PATH);
|
||||||
prisma: PrismaClient | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const db =
|
// enable WAL mode
|
||||||
globalForPrisma.prisma ??
|
sqlite.pragma("journal_mode = WAL");
|
||||||
new PrismaClient({
|
|
||||||
log:
|
|
||||||
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
|
// load uuidv7 extension
|
||||||
|
// built from https://github.com/craigpastro/sqlite-uuidv7
|
||||||
|
sqlite.loadExtension(
|
||||||
|
env.SQLITE_UUIDV7_EXT_PATH ??
|
||||||
|
join(__dirname, "../../../exts/sqlite-uuidv7.so"),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const db = drizzle(sqlite);
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
import { Migrate } from "@prisma/migrate/dist/Migrate";
|
|
||||||
import rootLogger from "../utils/logger";
|
|
||||||
import { ensureDatabaseExists } from "@prisma/migrate/dist/utils/ensureDatabaseExists";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs database migrations. Code stolen from diced/zipline:
|
|
||||||
* https://github.com/diced/zipline/blob/d112c3a509fcc4c3f36906dca5ad02a74a4f423e/src/server/util.ts#L9
|
|
||||||
*/
|
|
||||||
export async function migrate() {
|
|
||||||
const logger = rootLogger.child({ module: "db::migrate" });
|
|
||||||
|
|
||||||
logger.info("Running database migrations");
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.debug("establishing database connection");
|
|
||||||
const migrate = new Migrate("./prisma/schema.prisma");
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
"ensuring database exists, if not creating database - may error if no permissions",
|
|
||||||
);
|
|
||||||
await ensureDatabaseExists("apply", "./prisma/schema.prisma");
|
|
||||||
|
|
||||||
const diagnose = await migrate.diagnoseMigrationHistory({
|
|
||||||
optInToShadowDatabase: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (diagnose.history?.diagnostic === "databaseIsBehind") {
|
|
||||||
if (!diagnose.hasMigrationsTable) {
|
|
||||||
logger.debug("no migrations table found, attempting schema push");
|
|
||||||
try {
|
|
||||||
logger.debug("pushing schema");
|
|
||||||
const migration = await migrate.push({ force: false });
|
|
||||||
if (migration.unexecutable && migration.unexecutable.length > 0)
|
|
||||||
throw new Error(
|
|
||||||
"This database is not empty, schema push is not possible.",
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
migrate.stop();
|
|
||||||
logger.error("failed to push schema");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
logger.debug("finished pushing schema, marking migrations as applied");
|
|
||||||
for (const migration of diagnose.history.unappliedMigrationNames) {
|
|
||||||
await migrate.markMigrationApplied({ migrationId: migration });
|
|
||||||
}
|
|
||||||
migrate.stop();
|
|
||||||
logger.info("finished migrating database");
|
|
||||||
} else if (diagnose.hasMigrationsTable) {
|
|
||||||
logger.debug("database is behind, attempting to migrate");
|
|
||||||
try {
|
|
||||||
logger.debug("migrating database");
|
|
||||||
await migrate.applyMigrations();
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("failed to migrate database");
|
|
||||||
migrate.stop();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
migrate.stop();
|
|
||||||
logger.info("finished migrating database");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info("exiting migrations engine - database is up to date");
|
|
||||||
migrate.stop();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message.startsWith("P1001")) {
|
|
||||||
logger.error(
|
|
||||||
`Unable to connect to database \`${process.env.DATABASE_URL}\`, check your database connection`,
|
|
||||||
);
|
|
||||||
logger.debug(error);
|
|
||||||
} else {
|
|
||||||
logger.error("Failed to migrate database... exiting...");
|
|
||||||
logger.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.nextTick(() => process.exit(1));
|
|
||||||
}
|
|
||||||
}
|
|
7
src/server/db/prisma.d.ts
vendored
7
src/server/db/prisma.d.ts
vendored
|
@ -1,7 +0,0 @@
|
||||||
declare module "@prisma/migrate/dist/Migrate" {
|
|
||||||
export const Migrate: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@prisma/migrate/dist/utils/ensureDatabaseExists" {
|
|
||||||
export const ensureDatabaseExists: (...args: any[]) => Promise<void>;
|
|
||||||
}
|
|
77
src/server/db/schema.ts
Normal file
77
src/server/db/schema.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { sql, relations } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
text,
|
||||||
|
blob,
|
||||||
|
integer,
|
||||||
|
sqliteTable,
|
||||||
|
index,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
// util
|
||||||
|
const uuidv7 = sql`(uuid_generate_v7())`;
|
||||||
|
const now = sql`CURRENT_TIMESTAMP`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User table.
|
||||||
|
* Represents a global user.
|
||||||
|
*/
|
||||||
|
export const users = sqliteTable(
|
||||||
|
"users",
|
||||||
|
{
|
||||||
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
|
username: text("username").unique().notNull(),
|
||||||
|
password: blob("password"), // raw hash
|
||||||
|
|
||||||
|
// user configuration
|
||||||
|
mfaToken: blob("mfa_token"), // raw hash
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
usernameIdx: index("username_idx").on(table.username),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const userRelations = relations(users, ({ many }) => ({
|
||||||
|
sessions: many(sessions),
|
||||||
|
mfaRequestSessions: many(MFARequestSessions),
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Session table.
|
||||||
|
* Represents a user's session.
|
||||||
|
*/
|
||||||
|
export const sessions = sqliteTable("session", {
|
||||||
|
token: text("token").primaryKey(),
|
||||||
|
lastUA: text("last_useragent"),
|
||||||
|
lastIP: text("last_ip"),
|
||||||
|
// NOT IN MILLISECONDS!
|
||||||
|
lastAccessed: integer("last_accessed", { mode: "timestamp" }),
|
||||||
|
createdAt: integer("created_at").default(now),
|
||||||
|
userId: text("id"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sessionRelations = relations(sessions, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [sessions.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MFA Request Session.
|
||||||
|
* The intermediate between successful basic/oauth login and a full session for users with MFA enabled
|
||||||
|
*/
|
||||||
|
export const MFARequestSessions = sqliteTable("mfa_request_sessions", {
|
||||||
|
token: text("token").primaryKey(),
|
||||||
|
userId: text("user_id"),
|
||||||
|
createdAt: integer("created_at").default(now),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MFARequestSessionRelations = relations(
|
||||||
|
MFARequestSessions,
|
||||||
|
({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [MFARequestSessions.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
Loading…
Reference in a new issue