Rewritten to use drizzle instead of prisma
commit c0894548dac5133bd89da5b68684443748fa2559 Author: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Fri Nov 7 18:38:30 2025 +0100 Update config.ts commit 5a4f1159d2123ada0f698a10011c24720bf6ea6f Author: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Fri Nov 7 15:58:13 2025 +0100 first drizzle rewrite
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import prisma, { nowIso } from "../db";
|
||||
import { logAuditEvent } from "../audit";
|
||||
import db, { nowIso, toIso } from "../db";
|
||||
import { applyCaddyConfig } from "../caddy";
|
||||
import { logAuditEvent } from "../audit";
|
||||
import { accessListEntries, accessLists } from "../db/schema";
|
||||
import { asc, eq, inArray } from "drizzle-orm";
|
||||
|
||||
export type AccessListEntry = {
|
||||
id: number;
|
||||
@@ -25,94 +27,101 @@ export type AccessListInput = {
|
||||
users?: { username: string; password: string }[];
|
||||
};
|
||||
|
||||
function toAccessList(
|
||||
row: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
},
|
||||
entries: {
|
||||
id: number;
|
||||
username: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}[]
|
||||
): AccessList {
|
||||
type AccessListRow = typeof accessLists.$inferSelect;
|
||||
type AccessListEntryRow = typeof accessListEntries.$inferSelect;
|
||||
|
||||
function buildEntry(row: AccessListEntryRow): AccessListEntry {
|
||||
return {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
created_at: toIso(row.createdAt)!,
|
||||
updated_at: toIso(row.updatedAt)!
|
||||
};
|
||||
}
|
||||
|
||||
function toAccessList(row: AccessListRow, entries: AccessListEntryRow[]): AccessList {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
entries: entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
username: entry.username,
|
||||
created_at: entry.createdAt.toISOString(),
|
||||
updated_at: entry.updatedAt.toISOString()
|
||||
})),
|
||||
created_at: row.createdAt.toISOString(),
|
||||
updated_at: row.updatedAt.toISOString()
|
||||
entries: entries
|
||||
.slice()
|
||||
.sort((a, b) => a.username.localeCompare(b.username))
|
||||
.map(buildEntry),
|
||||
created_at: toIso(row.createdAt)!,
|
||||
updated_at: toIso(row.updatedAt)!
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAccessLists(): Promise<AccessList[]> {
|
||||
const lists = await prisma.accessList.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
include: {
|
||||
entries: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
},
|
||||
orderBy: { username: "asc" }
|
||||
}
|
||||
}
|
||||
const lists = await db.query.accessLists.findMany({
|
||||
orderBy: (table) => asc(table.name)
|
||||
});
|
||||
return lists.map((list: typeof lists[0]) => toAccessList(list, list.entries));
|
||||
|
||||
if (lists.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const listIds = lists.map((list) => list.id);
|
||||
const entries = await db
|
||||
.select()
|
||||
.from(accessListEntries)
|
||||
.where(inArray(accessListEntries.accessListId, listIds));
|
||||
|
||||
const entriesByList = new Map<number, AccessListEntryRow[]>();
|
||||
for (const entry of entries) {
|
||||
const bucket = entriesByList.get(entry.accessListId) ?? [];
|
||||
bucket.push(entry);
|
||||
entriesByList.set(entry.accessListId, bucket);
|
||||
}
|
||||
|
||||
return lists.map((list) => toAccessList(list, entriesByList.get(list.id) ?? []));
|
||||
}
|
||||
|
||||
export async function getAccessList(id: number): Promise<AccessList | null> {
|
||||
const list = await prisma.accessList.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
entries: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
},
|
||||
orderBy: { username: "asc" }
|
||||
}
|
||||
}
|
||||
const list = await db.query.accessLists.findFirst({
|
||||
where: (table, operators) => operators.eq(table.id, id)
|
||||
});
|
||||
return list ? toAccessList(list, list.entries) : null;
|
||||
if (!list) {
|
||||
return null;
|
||||
}
|
||||
const entries = await db
|
||||
.select()
|
||||
.from(accessListEntries)
|
||||
.where(eq(accessListEntries.accessListId, id))
|
||||
.orderBy(asc(accessListEntries.username));
|
||||
return toAccessList(list, entries);
|
||||
}
|
||||
|
||||
export async function createAccessList(input: AccessListInput, actorUserId: number) {
|
||||
const now = new Date(nowIso());
|
||||
const now = nowIso();
|
||||
|
||||
const accessList = await prisma.accessList.create({
|
||||
data: {
|
||||
const [accessList] = await db
|
||||
.insert(accessLists)
|
||||
.values({
|
||||
name: input.name.trim(),
|
||||
description: input.description ?? null,
|
||||
createdBy: actorUserId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
entries: input.users
|
||||
? {
|
||||
create: input.users.map((account) => ({
|
||||
username: account.username,
|
||||
passwordHash: bcrypt.hashSync(account.password, 10),
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}))
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
});
|
||||
updatedAt: now
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!accessList) {
|
||||
throw new Error("Failed to create access list");
|
||||
}
|
||||
|
||||
if (input.users && input.users.length > 0) {
|
||||
await db.insert(accessListEntries).values(
|
||||
input.users.map((account) => ({
|
||||
accessListId: accessList.id,
|
||||
username: account.username,
|
||||
passwordHash: bcrypt.hashSync(account.password, 10),
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
@@ -136,15 +145,15 @@ export async function updateAccessList(
|
||||
throw new Error("Access list not found");
|
||||
}
|
||||
|
||||
const now = new Date(nowIso());
|
||||
await prisma.accessList.update({
|
||||
where: { id },
|
||||
data: {
|
||||
const now = nowIso();
|
||||
await db
|
||||
.update(accessLists)
|
||||
.set({
|
||||
name: input.name ?? existing.name,
|
||||
description: input.description ?? existing.description,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.where(eq(accessLists.id, id));
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
@@ -163,23 +172,23 @@ export async function addAccessListEntry(
|
||||
entry: { username: string; password: string },
|
||||
actorUserId: number
|
||||
) {
|
||||
const list = await prisma.accessList.findUnique({
|
||||
where: { id: accessListId }
|
||||
const list = await db.query.accessLists.findFirst({
|
||||
where: (table, operators) => operators.eq(table.id, accessListId)
|
||||
});
|
||||
if (!list) {
|
||||
throw new Error("Access list not found");
|
||||
}
|
||||
const now = new Date(nowIso());
|
||||
|
||||
const now = nowIso();
|
||||
const hash = bcrypt.hashSync(entry.password, 10);
|
||||
await prisma.accessListEntry.create({
|
||||
data: {
|
||||
accessListId,
|
||||
username: entry.username,
|
||||
passwordHash: hash,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
await db.insert(accessListEntries).values({
|
||||
accessListId,
|
||||
username: entry.username,
|
||||
passwordHash: hash,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
});
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "create",
|
||||
@@ -192,15 +201,15 @@ export async function addAccessListEntry(
|
||||
}
|
||||
|
||||
export async function removeAccessListEntry(accessListId: number, entryId: number, actorUserId: number) {
|
||||
const list = await prisma.accessList.findUnique({
|
||||
where: { id: accessListId }
|
||||
const list = await db.query.accessLists.findFirst({
|
||||
where: (table, operators) => operators.eq(table.id, accessListId)
|
||||
});
|
||||
if (!list) {
|
||||
throw new Error("Access list not found");
|
||||
}
|
||||
await prisma.accessListEntry.delete({
|
||||
where: { id: entryId }
|
||||
});
|
||||
|
||||
await db.delete(accessListEntries).where(eq(accessListEntries.id, entryId));
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "delete",
|
||||
@@ -213,15 +222,15 @@ export async function removeAccessListEntry(accessListId: number, entryId: numbe
|
||||
}
|
||||
|
||||
export async function deleteAccessList(id: number, actorUserId: number) {
|
||||
const existing = await prisma.accessList.findUnique({
|
||||
where: { id }
|
||||
const existing = await db.query.accessLists.findFirst({
|
||||
where: (table, operators) => operators.eq(table.id, id)
|
||||
});
|
||||
if (!existing) {
|
||||
throw new Error("Access list not found");
|
||||
}
|
||||
await prisma.accessList.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
await db.delete(accessLists).where(eq(accessLists.id, id));
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "delete",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import prisma from "../db";
|
||||
import db, { toIso } from "../db";
|
||||
import { auditEvents } from "../db/schema";
|
||||
import { desc } from "drizzle-orm";
|
||||
|
||||
export type AuditEvent = {
|
||||
id: number;
|
||||
@@ -11,18 +13,19 @@ export type AuditEvent = {
|
||||
};
|
||||
|
||||
export async function listAuditEvents(limit = 100): Promise<AuditEvent[]> {
|
||||
const events = await prisma.auditEvent.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit
|
||||
});
|
||||
const events = await db
|
||||
.select()
|
||||
.from(auditEvents)
|
||||
.orderBy(desc(auditEvents.createdAt))
|
||||
.limit(limit);
|
||||
|
||||
return events.map((event: typeof events[0]) => ({
|
||||
return events.map((event) => ({
|
||||
id: event.id,
|
||||
user_id: event.userId,
|
||||
action: event.action,
|
||||
entity_type: event.entityType,
|
||||
entity_id: event.entityId,
|
||||
summary: event.summary,
|
||||
created_at: event.createdAt.toISOString()
|
||||
created_at: toIso(event.createdAt)!
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import prisma, { nowIso } from "../db";
|
||||
import db, { nowIso, toIso } from "../db";
|
||||
import { logAuditEvent } from "../audit";
|
||||
import { applyCaddyConfig } from "../caddy";
|
||||
import { certificates } from "../db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
export type CertificateType = "managed" | "imported";
|
||||
|
||||
@@ -27,18 +29,9 @@ export type CertificateInput = {
|
||||
private_key_pem?: string | null;
|
||||
};
|
||||
|
||||
function parseCertificate(row: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
domainNames: string;
|
||||
autoRenew: boolean;
|
||||
providerOptions: string | null;
|
||||
certificatePem: string | null;
|
||||
privateKeyPem: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): Certificate {
|
||||
type CertificateRow = typeof certificates.$inferSelect;
|
||||
|
||||
function parseCertificate(row: CertificateRow): Certificate {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -48,21 +41,19 @@ function parseCertificate(row: {
|
||||
provider_options: row.providerOptions ? JSON.parse(row.providerOptions) : null,
|
||||
certificate_pem: row.certificatePem,
|
||||
private_key_pem: row.privateKeyPem,
|
||||
created_at: row.createdAt.toISOString(),
|
||||
updated_at: row.updatedAt.toISOString()
|
||||
created_at: toIso(row.createdAt)!,
|
||||
updated_at: toIso(row.updatedAt)!
|
||||
};
|
||||
}
|
||||
|
||||
export async function listCertificates(): Promise<Certificate[]> {
|
||||
const certificates = await prisma.certificate.findMany({
|
||||
orderBy: { createdAt: "desc" }
|
||||
});
|
||||
return certificates.map(parseCertificate);
|
||||
const rows = await db.select().from(certificates).orderBy(desc(certificates.createdAt));
|
||||
return rows.map(parseCertificate);
|
||||
}
|
||||
|
||||
export async function getCertificate(id: number): Promise<Certificate | null> {
|
||||
const cert = await prisma.certificate.findUnique({
|
||||
where: { id }
|
||||
const cert = await db.query.certificates.findFirst({
|
||||
where: (table, { eq }) => eq(table.id, id)
|
||||
});
|
||||
return cert ? parseCertificate(cert) : null;
|
||||
}
|
||||
@@ -80,9 +71,10 @@ function validateCertificateInput(input: CertificateInput) {
|
||||
|
||||
export async function createCertificate(input: CertificateInput, actorUserId: number) {
|
||||
validateCertificateInput(input);
|
||||
const now = new Date(nowIso());
|
||||
const record = await prisma.certificate.create({
|
||||
data: {
|
||||
const now = nowIso();
|
||||
const [record] = await db
|
||||
.insert(certificates)
|
||||
.values({
|
||||
name: input.name.trim(),
|
||||
type: input.type,
|
||||
domainNames: JSON.stringify(
|
||||
@@ -95,8 +87,13 @@ export async function createCertificate(input: CertificateInput, actorUserId: nu
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: actorUserId
|
||||
}
|
||||
});
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!record) {
|
||||
throw new Error("Failed to create certificate");
|
||||
}
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "create",
|
||||
@@ -126,10 +123,10 @@ export async function updateCertificate(id: number, input: Partial<CertificateIn
|
||||
|
||||
validateCertificateInput(merged);
|
||||
|
||||
const now = new Date(nowIso());
|
||||
await prisma.certificate.update({
|
||||
where: { id },
|
||||
data: {
|
||||
const now = nowIso();
|
||||
await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
name: merged.name.trim(),
|
||||
type: merged.type,
|
||||
domainNames: JSON.stringify(Array.from(new Set(merged.domain_names))),
|
||||
@@ -138,8 +135,8 @@ export async function updateCertificate(id: number, input: Partial<CertificateIn
|
||||
certificatePem: merged.certificate_pem ?? null,
|
||||
privateKeyPem: merged.private_key_pem ?? null,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.where(eq(certificates.id, id));
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
@@ -158,9 +155,7 @@ export async function deleteCertificate(id: number, actorUserId: number) {
|
||||
throw new Error("Certificate not found");
|
||||
}
|
||||
|
||||
await prisma.certificate.delete({
|
||||
where: { id }
|
||||
});
|
||||
await db.delete(certificates).where(eq(certificates.id, id));
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "delete",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import prisma, { nowIso } from "../db";
|
||||
import db, { nowIso, toIso } from "../db";
|
||||
import { logAuditEvent } from "../audit";
|
||||
import { applyCaddyConfig } from "../caddy";
|
||||
import { deadHosts } from "../db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
export type DeadHost = {
|
||||
id: number;
|
||||
@@ -21,16 +23,9 @@ export type DeadHostInput = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
function parse(row: {
|
||||
id: number;
|
||||
name: string;
|
||||
domains: string;
|
||||
statusCode: number;
|
||||
responseBody: string | null;
|
||||
enabled: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): DeadHost {
|
||||
type DeadHostRow = typeof deadHosts.$inferSelect;
|
||||
|
||||
function parse(row: DeadHostRow): DeadHost {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -38,21 +33,19 @@ function parse(row: {
|
||||
status_code: row.statusCode,
|
||||
response_body: row.responseBody,
|
||||
enabled: row.enabled,
|
||||
created_at: row.createdAt.toISOString(),
|
||||
updated_at: row.updatedAt.toISOString()
|
||||
created_at: toIso(row.createdAt)!,
|
||||
updated_at: toIso(row.updatedAt)!
|
||||
};
|
||||
}
|
||||
|
||||
export async function listDeadHosts(): Promise<DeadHost[]> {
|
||||
const hosts = await prisma.deadHost.findMany({
|
||||
orderBy: { createdAt: "desc" }
|
||||
});
|
||||
const hosts = await db.select().from(deadHosts).orderBy(desc(deadHosts.createdAt));
|
||||
return hosts.map(parse);
|
||||
}
|
||||
|
||||
export async function getDeadHost(id: number): Promise<DeadHost | null> {
|
||||
const host = await prisma.deadHost.findUnique({
|
||||
where: { id }
|
||||
const host = await db.query.deadHosts.findFirst({
|
||||
where: (table, { eq }) => eq(table.id, id)
|
||||
});
|
||||
return host ? parse(host) : null;
|
||||
}
|
||||
@@ -62,9 +55,10 @@ export async function createDeadHost(input: DeadHostInput, actorUserId: number)
|
||||
throw new Error("At least one domain is required");
|
||||
}
|
||||
|
||||
const now = new Date(nowIso());
|
||||
const record = await prisma.deadHost.create({
|
||||
data: {
|
||||
const now = nowIso();
|
||||
const [record] = await db
|
||||
.insert(deadHosts)
|
||||
.values({
|
||||
name: input.name.trim(),
|
||||
domains: JSON.stringify(Array.from(new Set(input.domains.map((d) => d.trim().toLowerCase())))),
|
||||
statusCode: input.status_code ?? 503,
|
||||
@@ -73,8 +67,12 @@ export async function createDeadHost(input: DeadHostInput, actorUserId: number)
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: actorUserId
|
||||
}
|
||||
});
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!record) {
|
||||
throw new Error("Failed to create dead host");
|
||||
}
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "create",
|
||||
@@ -91,18 +89,18 @@ export async function updateDeadHost(id: number, input: Partial<DeadHostInput>,
|
||||
if (!existing) {
|
||||
throw new Error("Dead host not found");
|
||||
}
|
||||
const now = new Date(nowIso());
|
||||
await prisma.deadHost.update({
|
||||
where: { id },
|
||||
data: {
|
||||
const now = nowIso();
|
||||
await db
|
||||
.update(deadHosts)
|
||||
.set({
|
||||
name: input.name ?? existing.name,
|
||||
domains: JSON.stringify(input.domains ? Array.from(new Set(input.domains)) : existing.domains),
|
||||
statusCode: input.status_code ?? existing.status_code,
|
||||
responseBody: input.response_body ?? existing.response_body,
|
||||
enabled: input.enabled ?? existing.enabled,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.where(eq(deadHosts.id, id));
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "update",
|
||||
@@ -119,9 +117,7 @@ export async function deleteDeadHost(id: number, actorUserId: number) {
|
||||
if (!existing) {
|
||||
throw new Error("Dead host not found");
|
||||
}
|
||||
await prisma.deadHost.delete({
|
||||
where: { id }
|
||||
});
|
||||
await db.delete(deadHosts).where(eq(deadHosts.id, id));
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "delete",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import prisma, { nowIso } from "../db";
|
||||
import db, { nowIso, toIso } from "../db";
|
||||
import { applyCaddyConfig } from "../caddy";
|
||||
import { logAuditEvent } from "../audit";
|
||||
import { proxyHosts } from "../db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
const DEFAULT_AUTHENTIK_HEADERS = [
|
||||
"X-Authentik-Username",
|
||||
@@ -94,25 +96,7 @@ export type ProxyHostInput = {
|
||||
authentik?: ProxyHostAuthentikInput | null;
|
||||
};
|
||||
|
||||
type ProxyHostRow = {
|
||||
id: number;
|
||||
name: string;
|
||||
domains: string;
|
||||
upstreams: string;
|
||||
certificateId: number | null;
|
||||
accessListId: number | null;
|
||||
ownerUserId: number | null;
|
||||
sslForced: boolean;
|
||||
hstsEnabled: boolean;
|
||||
hstsSubdomains: boolean;
|
||||
allowWebsocket: boolean;
|
||||
preserveHostHeader: boolean;
|
||||
meta: string | null;
|
||||
skipHttpsHostnameValidation: boolean;
|
||||
enabled: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
type ProxyHostRow = typeof proxyHosts.$inferSelect;
|
||||
|
||||
function normalizeMetaValue(value: string | null | undefined) {
|
||||
if (!value) {
|
||||
@@ -394,8 +378,8 @@ function parseProxyHost(row: ProxyHostRow): ProxyHost {
|
||||
preserve_host_header: row.preserveHostHeader,
|
||||
skip_https_hostname_validation: row.skipHttpsHostnameValidation,
|
||||
enabled: row.enabled,
|
||||
created_at: row.createdAt.toISOString(),
|
||||
updated_at: row.updatedAt.toISOString(),
|
||||
created_at: toIso(row.createdAt)!,
|
||||
updated_at: toIso(row.updatedAt)!,
|
||||
custom_reverse_proxy_json: meta.custom_reverse_proxy_json ?? null,
|
||||
custom_pre_handlers_json: meta.custom_pre_handlers_json ?? null,
|
||||
authentik: hydrateAuthentik(meta.authentik)
|
||||
@@ -403,9 +387,7 @@ function parseProxyHost(row: ProxyHostRow): ProxyHost {
|
||||
}
|
||||
|
||||
export async function listProxyHosts(): Promise<ProxyHost[]> {
|
||||
const hosts = await prisma.proxyHost.findMany({
|
||||
orderBy: { createdAt: "desc" }
|
||||
});
|
||||
const hosts = await db.select().from(proxyHosts).orderBy(desc(proxyHosts.createdAt));
|
||||
return hosts.map(parseProxyHost);
|
||||
}
|
||||
|
||||
@@ -417,10 +399,11 @@ export async function createProxyHost(input: ProxyHostInput, actorUserId: number
|
||||
throw new Error("At least one upstream must be specified");
|
||||
}
|
||||
|
||||
const now = new Date(nowIso());
|
||||
const now = nowIso();
|
||||
const meta = buildMeta({}, input);
|
||||
const record = await prisma.proxyHost.create({
|
||||
data: {
|
||||
const [record] = await db
|
||||
.insert(proxyHosts)
|
||||
.values({
|
||||
name: input.name.trim(),
|
||||
domains: JSON.stringify(Array.from(new Set(input.domains.map((d) => d.trim().toLowerCase())))),
|
||||
upstreams: JSON.stringify(Array.from(new Set(input.upstreams.map((u) => u.trim())))),
|
||||
@@ -437,8 +420,12 @@ export async function createProxyHost(input: ProxyHostInput, actorUserId: number
|
||||
enabled: input.enabled ?? true,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!record) {
|
||||
throw new Error("Failed to create proxy host");
|
||||
}
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
@@ -454,8 +441,8 @@ export async function createProxyHost(input: ProxyHostInput, actorUserId: number
|
||||
}
|
||||
|
||||
export async function getProxyHost(id: number): Promise<ProxyHost | null> {
|
||||
const host = await prisma.proxyHost.findUnique({
|
||||
where: { id }
|
||||
const host = await db.query.proxyHosts.findFirst({
|
||||
where: (table, { eq }) => eq(table.id, id)
|
||||
});
|
||||
return host ? parseProxyHost(host) : null;
|
||||
}
|
||||
@@ -475,10 +462,10 @@ export async function updateProxyHost(id: number, input: Partial<ProxyHostInput>
|
||||
};
|
||||
const meta = buildMeta(existingMeta, input);
|
||||
|
||||
const now = new Date(nowIso());
|
||||
await prisma.proxyHost.update({
|
||||
where: { id },
|
||||
data: {
|
||||
const now = nowIso();
|
||||
await db
|
||||
.update(proxyHosts)
|
||||
.set({
|
||||
name: input.name ?? existing.name,
|
||||
domains,
|
||||
upstreams,
|
||||
@@ -493,8 +480,8 @@ export async function updateProxyHost(id: number, input: Partial<ProxyHostInput>
|
||||
skipHttpsHostnameValidation: input.skip_https_hostname_validation ?? existing.skip_https_hostname_validation,
|
||||
enabled: input.enabled ?? existing.enabled,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.where(eq(proxyHosts.id, id));
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
@@ -515,9 +502,7 @@ export async function deleteProxyHost(id: number, actorUserId: number) {
|
||||
throw new Error("Proxy host not found");
|
||||
}
|
||||
|
||||
await prisma.proxyHost.delete({
|
||||
where: { id }
|
||||
});
|
||||
await db.delete(proxyHosts).where(eq(proxyHosts.id, id));
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
action: "delete",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import prisma, { nowIso } from "../db";
|
||||
import db, { nowIso, toIso } from "../db";
|
||||
import { logAuditEvent } from "../audit";
|
||||
import { applyCaddyConfig } from "../caddy";
|
||||
import { redirectHosts } from "../db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
export type RedirectHost = {
|
||||
id: number;
|
||||
@@ -23,17 +25,9 @@ export type RedirectHostInput = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
function parseDbRecord(record: {
|
||||
id: number;
|
||||
name: string;
|
||||
domains: string;
|
||||
destination: string;
|
||||
statusCode: number;
|
||||
preserveQuery: boolean;
|
||||
enabled: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): RedirectHost {
|
||||
type RedirectHostRow = typeof redirectHosts.$inferSelect;
|
||||
|
||||
function parseDbRecord(record: RedirectHostRow): RedirectHost {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
@@ -42,21 +36,19 @@ function parseDbRecord(record: {
|
||||
status_code: record.statusCode,
|
||||
preserve_query: record.preserveQuery,
|
||||
enabled: record.enabled,
|
||||
created_at: record.createdAt.toISOString(),
|
||||
updated_at: record.updatedAt.toISOString()
|
||||
created_at: toIso(record.createdAt)!,
|
||||
updated_at: toIso(record.updatedAt)!
|
||||
};
|
||||
}
|
||||
|
||||
export async function listRedirectHosts(): Promise<RedirectHost[]> {
|
||||
const records = await prisma.redirectHost.findMany({
|
||||
orderBy: { createdAt: "desc" }
|
||||
});
|
||||
const records = await db.select().from(redirectHosts).orderBy(desc(redirectHosts.createdAt));
|
||||
return records.map(parseDbRecord);
|
||||
}
|
||||
|
||||
export async function getRedirectHost(id: number): Promise<RedirectHost | null> {
|
||||
const record = await prisma.redirectHost.findUnique({
|
||||
where: { id }
|
||||
const record = await db.query.redirectHosts.findFirst({
|
||||
where: (table, { eq }) => eq(table.id, id)
|
||||
});
|
||||
return record ? parseDbRecord(record) : null;
|
||||
}
|
||||
@@ -66,9 +58,10 @@ export async function createRedirectHost(input: RedirectHostInput, actorUserId:
|
||||
throw new Error("At least one domain is required");
|
||||
}
|
||||
|
||||
const now = new Date(nowIso());
|
||||
const record = await prisma.redirectHost.create({
|
||||
data: {
|
||||
const now = nowIso();
|
||||
const [record] = await db
|
||||
.insert(redirectHosts)
|
||||
.values({
|
||||
name: input.name.trim(),
|
||||
domains: JSON.stringify(Array.from(new Set(input.domains.map((d) => d.trim().toLowerCase())))),
|
||||
destination: input.destination.trim(),
|
||||
@@ -78,8 +71,12 @@ export async function createRedirectHost(input: RedirectHostInput, actorUserId:
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: actorUserId
|
||||
}
|
||||
});
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!record) {
|
||||
throw new Error("Failed to create redirect host");
|
||||
}
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
@@ -98,10 +95,10 @@ export async function updateRedirectHost(id: number, input: Partial<RedirectHost
|
||||
throw new Error("Redirect host not found");
|
||||
}
|
||||
|
||||
const now = new Date(nowIso());
|
||||
await prisma.redirectHost.update({
|
||||
where: { id },
|
||||
data: {
|
||||
const now = nowIso();
|
||||
await db
|
||||
.update(redirectHosts)
|
||||
.set({
|
||||
name: input.name ?? existing.name,
|
||||
domains: input.domains ? JSON.stringify(Array.from(new Set(input.domains))) : JSON.stringify(existing.domains),
|
||||
destination: input.destination ?? existing.destination,
|
||||
@@ -109,8 +106,8 @@ export async function updateRedirectHost(id: number, input: Partial<RedirectHost
|
||||
preserveQuery: input.preserve_query ?? existing.preserve_query,
|
||||
enabled: input.enabled ?? existing.enabled,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.where(eq(redirectHosts.id, id));
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
@@ -129,9 +126,7 @@ export async function deleteRedirectHost(id: number, actorUserId: number) {
|
||||
throw new Error("Redirect host not found");
|
||||
}
|
||||
|
||||
await prisma.redirectHost.delete({
|
||||
where: { id }
|
||||
});
|
||||
await db.delete(redirectHosts).where(eq(redirectHosts.id, id));
|
||||
|
||||
logAuditEvent({
|
||||
userId: actorUserId,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import prisma, { nowIso } from "../db";
|
||||
import db, { nowIso, toIso } from "../db";
|
||||
import { users } from "../db/schema";
|
||||
import { and, asc, count, eq } from "drizzle-orm";
|
||||
|
||||
export type User = {
|
||||
id: number;
|
||||
@@ -14,19 +16,9 @@ export type User = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
function parseDbUser(user: {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string | null;
|
||||
passwordHash: string | null;
|
||||
role: string;
|
||||
provider: string;
|
||||
subject: string;
|
||||
avatarUrl: string | null;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): User {
|
||||
type DbUser = typeof users.$inferSelect;
|
||||
|
||||
function parseDbUser(user: DbUser): User {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
@@ -37,38 +29,34 @@ function parseDbUser(user: {
|
||||
subject: user.subject,
|
||||
avatar_url: user.avatarUrl,
|
||||
status: user.status,
|
||||
created_at: user.createdAt.toISOString(),
|
||||
updated_at: user.updatedAt.toISOString()
|
||||
created_at: toIso(user.createdAt)!,
|
||||
updated_at: toIso(user.updatedAt)!
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserById(userId: number): Promise<User | null> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId }
|
||||
const user = await db.query.users.findFirst({
|
||||
where: (table, { eq }) => eq(table.id, userId)
|
||||
});
|
||||
return user ? parseDbUser(user) : null;
|
||||
}
|
||||
|
||||
export async function getUserCount(): Promise<number> {
|
||||
return await prisma.user.count();
|
||||
const result = await db.select({ value: count() }).from(users);
|
||||
return result[0]?.value ?? 0;
|
||||
}
|
||||
|
||||
export async function findUserByProviderSubject(provider: string, subject: string): Promise<User | null> {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
provider,
|
||||
subject
|
||||
}
|
||||
const user = await db.query.users.findFirst({
|
||||
where: (table, operators) => and(operators.eq(table.provider, provider), operators.eq(table.subject, subject))
|
||||
});
|
||||
return user ? parseDbUser(user) : null;
|
||||
}
|
||||
|
||||
export async function findUserByEmail(email: string): Promise<User | null> {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: normalizedEmail
|
||||
}
|
||||
const user = await db.query.users.findFirst({
|
||||
where: (table, { eq }) => eq(table.email, normalizedEmail)
|
||||
});
|
||||
return user ? parseDbUser(user) : null;
|
||||
}
|
||||
@@ -82,12 +70,13 @@ export async function createUser(data: {
|
||||
avatar_url?: string | null;
|
||||
passwordHash?: string | null;
|
||||
}): Promise<User> {
|
||||
const now = new Date(nowIso());
|
||||
const now = nowIso();
|
||||
const role = data.role ?? "user";
|
||||
const email = data.email.trim().toLowerCase();
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
const [user] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
email,
|
||||
name: data.name ?? null,
|
||||
passwordHash: data.passwordHash ?? null,
|
||||
@@ -98,8 +87,8 @@ export async function createUser(data: {
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.returning();
|
||||
|
||||
return parseDbUser(user);
|
||||
}
|
||||
@@ -110,45 +99,46 @@ export async function updateUserProfile(userId: number, data: { email?: string;
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date(nowIso());
|
||||
const user = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
const now = nowIso();
|
||||
const [updated] = await db
|
||||
.update(users)
|
||||
.set({
|
||||
email: data.email ?? current.email,
|
||||
name: data.name ?? current.name,
|
||||
avatarUrl: data.avatar_url ?? current.avatar_url,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.where(eq(users.id, userId))
|
||||
.returning();
|
||||
|
||||
return parseDbUser(user);
|
||||
return updated ? parseDbUser(updated) : null;
|
||||
}
|
||||
|
||||
export async function updateUserPassword(userId: number, passwordHash: string): Promise<void> {
|
||||
const now = new Date(nowIso());
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
const now = nowIso();
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
passwordHash,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.where(eq(users.id, userId));
|
||||
}
|
||||
|
||||
export async function listUsers(): Promise<User[]> {
|
||||
const users = await prisma.user.findMany({
|
||||
orderBy: { createdAt: "asc" }
|
||||
const rows = await db.query.users.findMany({
|
||||
orderBy: (table, { asc }) => asc(table.createdAt)
|
||||
});
|
||||
return users.map(parseDbUser);
|
||||
return rows.map(parseDbUser);
|
||||
}
|
||||
|
||||
export async function promoteToAdmin(userId: number): Promise<void> {
|
||||
const now = new Date(nowIso());
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
const now = nowIso();
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
role: "admin",
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
})
|
||||
.where(eq(users.id, userId));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user