correct keys, and keys separated correctly
This commit is contained in:
parent
7e1f9454d4
commit
0424a29008
@ -49,10 +49,7 @@ const DEFAULT_FORM_VALUES: UpdateSettingsInput = {
|
||||
basicAuthPassword: "",
|
||||
ukvisajobsEmail: "",
|
||||
ukvisajobsPassword: "",
|
||||
ukvisajobsHeadless: null,
|
||||
webhookSecret: "",
|
||||
notionApiKey: "",
|
||||
notionDatabaseId: "",
|
||||
}
|
||||
|
||||
const NULL_SETTINGS_PAYLOAD: UpdateSettingsInput = {
|
||||
@ -80,10 +77,7 @@ const NULL_SETTINGS_PAYLOAD: UpdateSettingsInput = {
|
||||
basicAuthPassword: null,
|
||||
ukvisajobsEmail: null,
|
||||
ukvisajobsPassword: null,
|
||||
ukvisajobsHeadless: null,
|
||||
webhookSecret: null,
|
||||
notionApiKey: null,
|
||||
notionDatabaseId: null,
|
||||
}
|
||||
|
||||
const mapSettingsToForm = (data: AppSettings): UpdateSettingsInput => ({
|
||||
@ -111,10 +105,7 @@ const mapSettingsToForm = (data: AppSettings): UpdateSettingsInput => ({
|
||||
basicAuthPassword: "",
|
||||
ukvisajobsEmail: data.ukvisajobsEmail ?? "",
|
||||
ukvisajobsPassword: "",
|
||||
ukvisajobsHeadless: data.ukvisajobsHeadless,
|
||||
webhookSecret: "",
|
||||
notionApiKey: "",
|
||||
notionDatabaseId: data.notionDatabaseId ?? "",
|
||||
})
|
||||
|
||||
const normalizeString = (value: string | null | undefined) => {
|
||||
@ -214,8 +205,6 @@ const getDerivedSettings = (settings: AppSettings | null) => {
|
||||
rxresumeEmail: settings?.rxresumeEmail ?? "",
|
||||
ukvisajobsEmail: settings?.ukvisajobsEmail ?? "",
|
||||
basicAuthUser: settings?.basicAuthUser ?? "",
|
||||
notionDatabaseId: settings?.notionDatabaseId ?? "",
|
||||
ukvisajobsHeadless: settings?.ukvisajobsHeadless ?? true,
|
||||
},
|
||||
private: {
|
||||
openrouterApiKeyHint: settings?.openrouterApiKeyHint ?? null,
|
||||
@ -223,10 +212,11 @@ const getDerivedSettings = (settings: AppSettings | null) => {
|
||||
ukvisajobsPasswordHint: settings?.ukvisajobsPasswordHint ?? null,
|
||||
basicAuthPasswordHint: settings?.basicAuthPasswordHint ?? null,
|
||||
webhookSecretHint: settings?.webhookSecretHint ?? null,
|
||||
notionApiKeyHint: settings?.notionApiKeyHint ?? null,
|
||||
},
|
||||
basicAuthActive: settings?.basicAuthActive ?? false,
|
||||
},
|
||||
defaultResumeProjects: settings?.defaultResumeProjects ?? null,
|
||||
|
||||
profileProjects,
|
||||
maxProjectsTotal: profileProjects.length,
|
||||
}
|
||||
@ -316,14 +306,6 @@ export const SettingsPage: React.FC = () => {
|
||||
envPayload.basicAuthUser = normalizeString(data.basicAuthUser)
|
||||
}
|
||||
|
||||
if (dirtyFields.notionDatabaseId) {
|
||||
envPayload.notionDatabaseId = normalizeString(data.notionDatabaseId)
|
||||
}
|
||||
|
||||
if (dirtyFields.ukvisajobsHeadless) {
|
||||
envPayload.ukvisajobsHeadless = data.ukvisajobsHeadless ?? null
|
||||
}
|
||||
|
||||
if (dirtyFields.openrouterApiKey) {
|
||||
const value = normalizePrivateInput(data.openrouterApiKey)
|
||||
if (value !== undefined) envPayload.openrouterApiKey = value
|
||||
@ -349,11 +331,6 @@ export const SettingsPage: React.FC = () => {
|
||||
if (value !== undefined) envPayload.webhookSecret = value
|
||||
}
|
||||
|
||||
if (dirtyFields.notionApiKey) {
|
||||
const value = normalizePrivateInput(data.notionApiKey)
|
||||
if (value !== undefined) envPayload.notionApiKey = value
|
||||
}
|
||||
|
||||
const payload: UpdateSettingsInput = {
|
||||
model: normalizeString(data.model),
|
||||
modelScorer: normalizeString(data.modelScorer),
|
||||
|
||||
@ -11,14 +11,11 @@ const EnvironmentSettingsHarness = () => {
|
||||
rxresumeEmail: "resume@example.com",
|
||||
ukvisajobsEmail: "visa@example.com",
|
||||
basicAuthUser: "admin",
|
||||
notionDatabaseId: "db-123",
|
||||
ukvisajobsHeadless: false,
|
||||
openrouterApiKey: "",
|
||||
rxresumePassword: "",
|
||||
ukvisajobsPassword: "",
|
||||
basicAuthPassword: "",
|
||||
webhookSecret: "",
|
||||
notionApiKey: "",
|
||||
}
|
||||
})
|
||||
|
||||
@ -31,8 +28,6 @@ const EnvironmentSettingsHarness = () => {
|
||||
rxresumeEmail: "resume@example.com",
|
||||
ukvisajobsEmail: "visa@example.com",
|
||||
basicAuthUser: "admin",
|
||||
notionDatabaseId: "db-123",
|
||||
ukvisajobsHeadless: false,
|
||||
},
|
||||
private: {
|
||||
openrouterApiKeyHint: "sk-1",
|
||||
@ -40,8 +35,8 @@ const EnvironmentSettingsHarness = () => {
|
||||
ukvisajobsPasswordHint: "pass",
|
||||
basicAuthPasswordHint: "abcd",
|
||||
webhookSecretHint: "sec-",
|
||||
notionApiKeyHint: "not-",
|
||||
},
|
||||
basicAuthActive: true,
|
||||
}}
|
||||
isLoading={false}
|
||||
isSaving={false}
|
||||
@ -52,22 +47,25 @@ const EnvironmentSettingsHarness = () => {
|
||||
}
|
||||
|
||||
describe("EnvironmentSettingsSection", () => {
|
||||
it("renders readable values and masks private secrets with hints", () => {
|
||||
it("renders values grouped logically and masks private secrets with hints", () => {
|
||||
render(<EnvironmentSettingsHarness />)
|
||||
|
||||
expect(screen.getByDisplayValue("resume@example.com")).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue("visa@example.com")).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue("admin")).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue("db-123")).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByText("sk-1********")).toBeInTheDocument()
|
||||
expect(screen.getByText("pass********")).toBeInTheDocument()
|
||||
expect(screen.getByText("abcd********")).toBeInTheDocument()
|
||||
expect(screen.getByText("sec-********")).toBeInTheDocument()
|
||||
expect(screen.getByText("not-********")).toBeInTheDocument()
|
||||
expect(screen.getByText("Not set")).toBeInTheDocument()
|
||||
|
||||
const headlessToggle = screen.getByLabelText("UKVisaJobs headless mode")
|
||||
expect(headlessToggle).not.toBeChecked()
|
||||
// Basic Auth
|
||||
expect(screen.getByLabelText("Enable basic authentication")).toBeChecked()
|
||||
expect(screen.getByDisplayValue("admin")).toBeInTheDocument()
|
||||
|
||||
// Sections
|
||||
expect(screen.getByText("External Services")).toBeInTheDocument()
|
||||
expect(screen.getByText("Service Accounts")).toBeInTheDocument()
|
||||
expect(screen.getByText("Security")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from "react"
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useFormContext, Controller } from "react-hook-form"
|
||||
|
||||
import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||
@ -22,109 +22,27 @@ export const EnvironmentSettingsSection: React.FC<EnvironmentSettingsSectionProp
|
||||
isSaving,
|
||||
}) => {
|
||||
const { register, control, formState: { errors } } = useFormContext<UpdateSettingsInput>()
|
||||
const { readable, private: privateValues } = values
|
||||
const { readable, private: privateValues, basicAuthActive } = values
|
||||
|
||||
const [isBasicAuthEnabled, setIsBasicAuthEnabled] = useState(basicAuthActive)
|
||||
|
||||
useEffect(() => {
|
||||
setIsBasicAuthEnabled(basicAuthActive)
|
||||
}, [basicAuthActive])
|
||||
|
||||
return (
|
||||
<AccordionItem value="environment" className="border rounded-lg px-4">
|
||||
<AccordionTrigger className="hover:no-underline py-4">
|
||||
<span className="text-base font-semibold">Environment</span>
|
||||
<span className="text-base font-semibold">Environment & Accounts</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
{/* External Services */}
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">Readable values</div>
|
||||
<div className="text-sm font-bold uppercase tracking-wider text-muted-foreground">External Services</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">RxResume email</div>
|
||||
<Input
|
||||
{...register("rxresumeEmail")}
|
||||
placeholder="you@example.com"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.rxresumeEmail && <p className="text-xs text-destructive">{errors.rxresumeEmail.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Used for RxResume PDF automation.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">UKVisaJobs email</div>
|
||||
<Input
|
||||
{...register("ukvisajobsEmail")}
|
||||
placeholder="you@example.com"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.ukvisajobsEmail && <p className="text-xs text-destructive">{errors.ukvisajobsEmail.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Used for refreshing UKVisaJobs sessions.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Basic auth user</div>
|
||||
<Input
|
||||
{...register("basicAuthUser")}
|
||||
placeholder="username"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.basicAuthUser && <p className="text-xs text-destructive">{errors.basicAuthUser.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Pair with a password to require auth on writes.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Notion database ID</div>
|
||||
<Input
|
||||
{...register("notionDatabaseId")}
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.notionDatabaseId && <p className="text-xs text-destructive">{errors.notionDatabaseId.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Destination database for applied job entries.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Controller
|
||||
name="ukvisajobsHeadless"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id="ukvisajobsHeadless"
|
||||
checked={field.value ?? readable.ukvisajobsHeadless}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked === "indeterminate" ? null : checked === true)
|
||||
}}
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
htmlFor="ukvisajobsHeadless"
|
||||
className="text-sm font-medium leading-none cursor-pointer"
|
||||
>
|
||||
UKVisaJobs headless mode
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Disable to show the browser while authenticating.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">Private values</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">OpenRouter API key</div>
|
||||
<div className="text-sm font-medium">OpenRouter API key</div>
|
||||
<Input
|
||||
{...register("openrouterApiKey")}
|
||||
type="password"
|
||||
@ -138,49 +56,7 @@ export const EnvironmentSettingsSection: React.FC<EnvironmentSettingsSectionProp
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">RxResume password</div>
|
||||
<Input
|
||||
{...register("rxresumePassword")}
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.rxresumePassword && <p className="text-xs text-destructive">{errors.rxresumePassword.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Current: <span className="font-mono">{formatSecretHint(privateValues.rxresumePasswordHint)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">UKVisaJobs password</div>
|
||||
<Input
|
||||
{...register("ukvisajobsPassword")}
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.ukvisajobsPassword && <p className="text-xs text-destructive">{errors.ukvisajobsPassword.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Current: <span className="font-mono">{formatSecretHint(privateValues.ukvisajobsPasswordHint)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Basic auth password</div>
|
||||
<Input
|
||||
{...register("basicAuthPassword")}
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.basicAuthPassword && <p className="text-xs text-destructive">{errors.basicAuthPassword.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Current: <span className="font-mono">{formatSecretHint(privateValues.basicAuthPasswordHint)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Webhook secret</div>
|
||||
<div className="text-sm font-medium">Webhook secret</div>
|
||||
<Input
|
||||
{...register("webhookSecret")}
|
||||
type="password"
|
||||
@ -192,27 +68,128 @@ export const EnvironmentSettingsSection: React.FC<EnvironmentSettingsSectionProp
|
||||
Current: <span className="font-mono">{formatSecretHint(privateValues.webhookSecretHint)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Notion API key</div>
|
||||
<Input
|
||||
{...register("notionApiKey")}
|
||||
type="password"
|
||||
placeholder="Enter new key"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.notionApiKey && <p className="text-xs text-destructive">{errors.notionApiKey.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Current: <span className="font-mono">{formatSecretHint(privateValues.notionApiKeyHint)}</span>
|
||||
<Separator />
|
||||
|
||||
{/* Service Accounts */}
|
||||
<div className="space-y-6">
|
||||
<div className="text-sm font-bold uppercase tracking-wider text-muted-foreground">Service Accounts</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-semibold">RxResume</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Email</div>
|
||||
<Input
|
||||
{...register("rxresumeEmail")}
|
||||
placeholder="you@example.com"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.rxresumeEmail && <p className="text-xs text-destructive">{errors.rxresumeEmail.message}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Password</div>
|
||||
<Input
|
||||
{...register("rxresumePassword")}
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.rxresumePassword && <p className="text-xs text-destructive">{errors.rxresumePassword.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Current: <span className="font-mono">{formatSecretHint(privateValues.rxresumePasswordHint)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Private values are write-only. Enter a new value to replace the stored secret.
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-semibold">UKVisaJobs</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Email</div>
|
||||
<Input
|
||||
{...register("ukvisajobsEmail")}
|
||||
placeholder="you@example.com"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.ukvisajobsEmail && <p className="text-xs text-destructive">{errors.ukvisajobsEmail.message}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Password</div>
|
||||
<Input
|
||||
{...register("ukvisajobsPassword")}
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.ukvisajobsPassword && <p className="text-xs text-destructive">{errors.ukvisajobsPassword.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Current: <span className="font-mono">{formatSecretHint(privateValues.ukvisajobsPasswordHint)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Security */}
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-bold uppercase tracking-wider text-muted-foreground">Security</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="enableBasicAuth"
|
||||
checked={isBasicAuthEnabled}
|
||||
onCheckedChange={(checked) => setIsBasicAuthEnabled(checked === true)}
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
htmlFor="enableBasicAuth"
|
||||
className="text-sm font-medium leading-none cursor-pointer"
|
||||
>
|
||||
Enable basic authentication
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Require a username and password for write operations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isBasicAuthEnabled && (
|
||||
<div className="grid gap-4 md:grid-cols-2 pt-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Username</div>
|
||||
<Input
|
||||
{...register("basicAuthUser")}
|
||||
placeholder="username"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.basicAuthUser && <p className="text-xs text-destructive">{errors.basicAuthUser.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">Password</div>
|
||||
<Input
|
||||
{...register("basicAuthPassword")}
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{errors.basicAuthPassword && <p className="text-xs text-destructive">{errors.basicAuthPassword.message}</p>}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Current: <span className="font-mono">{formatSecretHint(privateValues.basicAuthPasswordHint)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,6 @@ export type EnvSettingsValues = {
|
||||
ukvisajobsPasswordHint: string | null
|
||||
basicAuthPasswordHint: string | null
|
||||
webhookSecretHint: string | null
|
||||
notionApiKeyHint: string | null
|
||||
}
|
||||
basicAuthActive: boolean
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ describe.sequential('Settings API routes', () => {
|
||||
env: {
|
||||
OPENROUTER_API_KEY: 'secret-key',
|
||||
RXRESUME_EMAIL: 'resume@example.com',
|
||||
UKVISAJOBS_HEADLESS: 'false',
|
||||
},
|
||||
}));
|
||||
});
|
||||
@ -30,7 +29,7 @@ describe.sequential('Settings API routes', () => {
|
||||
expect(Array.isArray(body.data.searchTerms)).toBe(true);
|
||||
expect(body.data.rxresumeEmail).toBe('resume@example.com');
|
||||
expect(body.data.openrouterApiKeyHint).toBe('secr');
|
||||
expect(body.data.ukvisajobsHeadless).toBe(false);
|
||||
expect(body.data.basicAuthActive).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid settings updates and persists overrides', async () => {
|
||||
@ -48,7 +47,6 @@ describe.sequential('Settings API routes', () => {
|
||||
searchTerms: ['engineer'],
|
||||
rxresumeEmail: 'updated@example.com',
|
||||
openrouterApiKey: 'updated-secret',
|
||||
ukvisajobsHeadless: true,
|
||||
}),
|
||||
});
|
||||
const patchBody = await patchRes.json();
|
||||
@ -57,6 +55,5 @@ describe.sequential('Settings API routes', () => {
|
||||
expect(patchBody.data.overrideSearchTerms).toEqual(['engineer']);
|
||||
expect(patchBody.data.rxresumeEmail).toBe('updated@example.com');
|
||||
expect(patchBody.data.openrouterApiKeyHint).toBe('upda');
|
||||
expect(patchBody.data.ukvisajobsHeadless).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -307,31 +307,12 @@ settingsRouter.patch('/', async (req: Request, res: Response) => {
|
||||
applyEnvValue('UKVISAJOBS_PASSWORD', value);
|
||||
}
|
||||
|
||||
if ('ukvisajobsHeadless' in input) {
|
||||
const value = input.ukvisajobsHeadless ?? null;
|
||||
const serialized = serializeEnvBoolean(value);
|
||||
await settingsRepo.setSetting('ukvisajobsHeadless', serialized);
|
||||
applyEnvValue('UKVISAJOBS_HEADLESS', serialized);
|
||||
}
|
||||
|
||||
if ('webhookSecret' in input) {
|
||||
const value = normalizeEnvInput(input.webhookSecret);
|
||||
await settingsRepo.setSetting('webhookSecret', value);
|
||||
applyEnvValue('WEBHOOK_SECRET', value);
|
||||
}
|
||||
|
||||
if ('notionApiKey' in input) {
|
||||
const value = normalizeEnvInput(input.notionApiKey);
|
||||
await settingsRepo.setSetting('notionApiKey', value);
|
||||
applyEnvValue('NOTION_API_KEY', value);
|
||||
}
|
||||
|
||||
if ('notionDatabaseId' in input) {
|
||||
const value = normalizeEnvInput(input.notionDatabaseId);
|
||||
await settingsRepo.setSetting('notionDatabaseId', value);
|
||||
applyEnvValue('NOTION_DATABASE_ID', value);
|
||||
}
|
||||
|
||||
const overrideModel = await settingsRepo.getSetting('model');
|
||||
const defaultModel = process.env.MODEL || 'openai/gpt-4o-mini';
|
||||
const model = overrideModel || defaultModel;
|
||||
|
||||
@ -31,10 +31,7 @@ export type SettingKey = 'model'
|
||||
| 'basicAuthPassword'
|
||||
| 'ukvisajobsEmail'
|
||||
| 'ukvisajobsPassword'
|
||||
| 'ukvisajobsHeadless'
|
||||
| 'webhookSecret'
|
||||
| 'notionApiKey'
|
||||
| 'notionDatabaseId'
|
||||
|
||||
export async function getSetting(key: SettingKey): Promise<string | null> {
|
||||
const [row] = await db.select().from(settings).where(eq(settings.key, key))
|
||||
|
||||
@ -7,12 +7,9 @@ const readableStringConfig: { settingKey: SettingKey, envKey: string }[] = [
|
||||
{ settingKey: 'rxresumeEmail', envKey: 'RXRESUME_EMAIL' },
|
||||
{ settingKey: 'ukvisajobsEmail', envKey: 'UKVISAJOBS_EMAIL' },
|
||||
{ settingKey: 'basicAuthUser', envKey: 'BASIC_AUTH_USER' },
|
||||
{ settingKey: 'notionDatabaseId', envKey: 'NOTION_DATABASE_ID' },
|
||||
];
|
||||
|
||||
const readableBooleanConfig: { settingKey: SettingKey, envKey: string, defaultValue: boolean }[] = [
|
||||
{ settingKey: 'ukvisajobsHeadless', envKey: 'UKVISAJOBS_HEADLESS', defaultValue: true },
|
||||
];
|
||||
const readableBooleanConfig: { settingKey: SettingKey, envKey: string, defaultValue: boolean }[] = [];
|
||||
|
||||
const privateStringConfig: { settingKey: SettingKey, envKey: string, hintKey: string }[] = [
|
||||
{ settingKey: 'openrouterApiKey', envKey: 'OPENROUTER_API_KEY', hintKey: 'openrouterApiKeyHint' },
|
||||
@ -20,7 +17,6 @@ const privateStringConfig: { settingKey: SettingKey, envKey: string, hintKey: st
|
||||
{ settingKey: 'ukvisajobsPassword', envKey: 'UKVISAJOBS_PASSWORD', hintKey: 'ukvisajobsPasswordHint' },
|
||||
{ settingKey: 'basicAuthPassword', envKey: 'BASIC_AUTH_PASSWORD', hintKey: 'basicAuthPasswordHint' },
|
||||
{ settingKey: 'webhookSecret', envKey: 'WEBHOOK_SECRET', hintKey: 'webhookSecretHint' },
|
||||
{ settingKey: 'notionApiKey', envKey: 'NOTION_API_KEY', hintKey: 'notionApiKeyHint' },
|
||||
];
|
||||
|
||||
export function normalizeEnvInput(value: string | null | undefined): string | null {
|
||||
@ -96,9 +92,13 @@ export async function getEnvSettingsData(): Promise<Record<string, string | bool
|
||||
privateValues[hintKey] = rawValue ? rawValue.slice(0, 4) : null;
|
||||
}
|
||||
|
||||
const basicAuthUser = (await settingsRepo.getSetting('basicAuthUser')) ?? process.env.BASIC_AUTH_USER;
|
||||
const basicAuthPassword = (await settingsRepo.getSetting('basicAuthPassword')) ?? process.env.BASIC_AUTH_PASSWORD;
|
||||
|
||||
return {
|
||||
...readableValues,
|
||||
...privateValues,
|
||||
basicAuthActive: Boolean(basicAuthUser && basicAuthPassword),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -31,10 +31,7 @@ export const updateSettingsSchema = z.object({
|
||||
basicAuthPassword: z.string().trim().max(2000).nullable().optional(),
|
||||
ukvisajobsEmail: z.string().trim().max(200).nullable().optional(),
|
||||
ukvisajobsPassword: z.string().trim().max(2000).nullable().optional(),
|
||||
ukvisajobsHeadless: z.boolean().nullable().optional(),
|
||||
webhookSecret: z.string().trim().max(2000).nullable().optional(),
|
||||
notionApiKey: z.string().trim().max(2000).nullable().optional(),
|
||||
notionDatabaseId: z.string().trim().max(200).nullable().optional(),
|
||||
});
|
||||
|
||||
export type UpdateSettingsInput = z.infer<typeof updateSettingsSchema>;
|
||||
|
||||
@ -390,8 +390,6 @@ export interface AppSettings {
|
||||
basicAuthPasswordHint: string | null;
|
||||
ukvisajobsEmail: string | null;
|
||||
ukvisajobsPasswordHint: string | null;
|
||||
ukvisajobsHeadless: boolean;
|
||||
webhookSecretHint: string | null;
|
||||
notionApiKeyHint: string | null;
|
||||
notionDatabaseId: string | null;
|
||||
basicAuthActive: boolean;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user