feat: add better auth sqlite runtime and server-side session flow

This commit is contained in:
Poyraz
2026-07-10 22:07:37 +03:00
parent 3504a02229
commit 57aab2932e
41 changed files with 4907 additions and 143 deletions
+207
View File
@@ -0,0 +1,207 @@
import "server-only";
import { count, eq } from "drizzle-orm";
import { getSqliteConnection } from "@/server/db/client";
import { appProfiles, appSetupState, authAuditEvents } from "@/server/db/schema";
import type { AuthAuditEventType } from "@/server/auth/types";
import { getDefaultDisplayName, normalizeAuthEmail } from "@/server/auth/validation";
const FIRST_FREELANCER_SETUP_KEY = "first_freelancer";
const SETUP_LOCK_TTL_MS = 10 * 60 * 1000;
export type FirstFreelancerSetupState = {
available: boolean;
locked: boolean;
errorMessage?: string;
};
export async function getFirstFreelancerSetupState(): Promise<FirstFreelancerSetupState> {
try {
return readFirstFreelancerSetupState();
} catch (error) {
return {
available: false,
locked: false,
errorMessage:
error instanceof Error
? error.message
: "İlk kurulum durumu okunamadı.",
};
}
}
export function readFirstFreelancerSetupState(): FirstFreelancerSetupState {
const { db } = getSqliteConnection();
const [{ value: freelancerCount }] = db
.select({ value: count() })
.from(appProfiles)
.where(eq(appProfiles.role, "freelancer"))
.all();
if (freelancerCount > 0) {
return { available: false, locked: false };
}
const [setupState] = db
.select()
.from(appSetupState)
.where(eq(appSetupState.key, FIRST_FREELANCER_SETUP_KEY))
.limit(1)
.all();
if (!setupState) {
return { available: true, locked: false };
}
if (setupState.status === "completed") {
return { available: false, locked: false };
}
const lockedAt = setupState.lockedAt?.getTime() ?? 0;
const isStale = Date.now() - lockedAt > SETUP_LOCK_TTL_MS;
return {
available: isStale,
locked: !isStale,
errorMessage: isStale ? undefined : "İlk kurulum şu anda başka bir istek tarafından işleniyor.",
};
}
export async function reserveFirstFreelancerSetup(email: string): Promise<boolean> {
const normalizedEmail = normalizeAuthEmail(email);
const { db } = getSqliteConnection();
return db.transaction((tx) => {
const [{ value: freelancerCount }] = tx
.select({ value: count() })
.from(appProfiles)
.where(eq(appProfiles.role, "freelancer"))
.all();
if (freelancerCount > 0) {
return false;
}
const [setupState] = tx
.select()
.from(appSetupState)
.where(eq(appSetupState.key, FIRST_FREELANCER_SETUP_KEY))
.limit(1)
.all();
const now = new Date();
if (setupState?.status === "completed") {
return false;
}
if (setupState?.status === "pending") {
const lockedAt = setupState.lockedAt?.getTime() ?? 0;
if (Date.now() - lockedAt <= SETUP_LOCK_TTL_MS) {
return false;
}
}
tx.insert(appSetupState)
.values({
key: FIRST_FREELANCER_SETUP_KEY,
status: "pending",
lockedBy: normalizedEmail,
lockedAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: appSetupState.key,
set: {
status: "pending",
lockedBy: normalizedEmail,
lockedAt: now,
updatedAt: now,
},
})
.run();
tx.insert(authAuditEvents)
.values({
type: "setup_started",
email: normalizedEmail,
metadata: { source: "better_auth_user_create" },
})
.run();
return true;
});
}
export async function completeFirstFreelancerSetup(user: {
id: string;
email: string;
name?: string | null;
}): Promise<void> {
const normalizedEmail = normalizeAuthEmail(user.email);
const now = new Date();
const { db } = getSqliteConnection();
db.transaction((tx) => {
tx.insert(appProfiles)
.values({
authUserId: user.id,
email: normalizedEmail,
displayName: user.name || getDefaultDisplayName(normalizedEmail),
role: "freelancer",
disabled: false,
createdAt: now,
updatedAt: now,
})
.onConflictDoNothing()
.run();
tx.insert(appSetupState)
.values({
key: FIRST_FREELANCER_SETUP_KEY,
status: "completed",
lockedBy: normalizedEmail,
lockedAt: now,
completedAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: appSetupState.key,
set: {
status: "completed",
lockedBy: normalizedEmail,
completedAt: now,
updatedAt: now,
},
})
.run();
tx.insert(authAuditEvents)
.values({
type: "setup_completed",
authUserId: user.id,
email: normalizedEmail,
metadata: { role: "freelancer" },
})
.run();
});
}
export async function recordAuthAuditEvent(input: {
type: AuthAuditEventType;
authUserId?: string | null;
email?: string | null;
metadata?: Record<string, unknown> | null;
}): Promise<void> {
const { db } = getSqliteConnection();
db.insert(authAuditEvents)
.values({
type: input.type,
authUserId: input.authUserId ?? null,
email: input.email ? normalizeAuthEmail(input.email) : null,
metadata: input.metadata ?? null,
})
.run();
}