correct keys, and keys separated correctly

This commit is contained in:
DaKheera47
2026-01-22 12:04:20 +00:00
parent 7e1f9454d4
commit 0424a29008
10 changed files with 148 additions and 226 deletions
+2 -25
View File
@@ -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
}