feat(backend): migrate freelancer and portal runtimes

This commit is contained in:
poyrazavsever
2026-07-17 00:16:38 +03:00
parent 561af11b70
commit 678c0236db
41 changed files with 5293 additions and 2324 deletions
+132
View File
@@ -0,0 +1,132 @@
import "server-only";
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { getServerConfig } from "../config";
import { getSqliteConnection } from "../db/client";
import { userAiSettings, type AiProvider } from "../db/schema/settings";
import { requireOwnerScope, type DomainActor } from "../domain/actor";
import { DomainError } from "../domain/errors";
const inputSchema = z.object({
provider: z.enum(["gemini", "openai", "groq", "ollama"]),
apiKey: z.string().trim().max(4_096).optional(),
});
export type PublicAiSettings = {
provider: AiProvider;
hasApiKey: boolean;
};
export function getPublicAiSettings(actor: DomainActor): PublicAiSettings {
const scope = requireOwnerScope(actor);
const row = getSqliteConnection().db
.select()
.from(userAiSettings)
.where(eq(userAiSettings.ownerUserId, scope.ownerUserId))
.get();
return {
provider: row?.provider ?? "gemini",
hasApiKey: Boolean(row?.encryptedApiKey),
};
}
export function updateAiSettings(actor: DomainActor, input: unknown): PublicAiSettings {
const scope = requireOwnerScope(actor);
const parsed = inputSchema.safeParse(input);
if (!parsed.success) {
throw new DomainError("VALIDATION_ERROR", "Yapay zeka ayarları geçersiz.");
}
const { db } = getSqliteConnection();
const current = db
.select()
.from(userAiSettings)
.where(eq(userAiSettings.ownerUserId, scope.ownerUserId))
.get();
const encryptedApiKey = parsed.data.provider === "ollama"
? null
: parsed.data.apiKey
? encryptSecret(parsed.data.apiKey)
: current?.encryptedApiKey ?? null;
db.insert(userAiSettings)
.values({
ownerUserId: scope.ownerUserId,
provider: parsed.data.provider,
model: null,
encryptedApiKey,
})
.onConflictDoUpdate({
target: userAiSettings.ownerUserId,
set: {
provider: parsed.data.provider,
model: null,
encryptedApiKey,
updatedAt: sqlNow(),
},
})
.run();
return { provider: parsed.data.provider, hasApiKey: Boolean(encryptedApiKey) };
}
export function getAiRuntimeSettings(actor: DomainActor): {
provider: AiProvider;
model: string | null;
apiKey: string | null;
} {
const scope = requireOwnerScope(actor);
const row = getSqliteConnection().db
.select()
.from(userAiSettings)
.where(eq(userAiSettings.ownerUserId, scope.ownerUserId))
.get();
return {
provider: row?.provider ?? "gemini",
model: row?.model ?? null,
apiKey: row?.encryptedApiKey ? decryptSecret(row.encryptedApiKey) : null,
};
}
function encryptionKey(): Buffer {
const secret = getServerConfig().betterAuthSecret
?? "neta-development-only-ai-settings-secret";
return createHash("sha256").update(`neta:ai-settings:${secret}`).digest();
}
function encryptSecret(value: string): string {
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", encryptionKey(), iv);
const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return `v1.${iv.toString("base64url")}.${tag.toString("base64url")}.${ciphertext.toString("base64url")}`;
}
function decryptSecret(value: string): string {
const [version, ivValue, tagValue, ciphertextValue] = value.split(".");
if (version !== "v1" || !ivValue || !tagValue || !ciphertextValue) {
throw new DomainError("INVARIANT_VIOLATION", "AI secret formatı geçersiz.");
}
try {
const decipher = createDecipheriv(
"aes-256-gcm",
encryptionKey(),
Buffer.from(ivValue, "base64url"),
);
decipher.setAuthTag(Buffer.from(tagValue, "base64url"));
return Buffer.concat([
decipher.update(Buffer.from(ciphertextValue, "base64url")),
decipher.final(),
]).toString("utf8");
} catch {
throw new DomainError("INVARIANT_VIOLATION", "AI secret çözülemedi.");
}
}
function sqlNow(): string {
return new Date().toISOString();
}