gadget/gadget-code/frontend/src/pages/Settings.tsx
Rob Colbert 896aff1b02 Fix User Settings persona display issue
- Switch frontend sign-in to /api/v1/auth/sign-in endpoint (includes persona)
- Add updateUser() to App context for proper state management
- Fix Settings.tsx save flow to use updateUser() instead of broken localStorage merge
- Remove unused web AuthController (gadget-code/src/controllers/auth.ts)
- Fix UserApiControllerV1 to return flat user object instead of double-wrapped
- Remove SessionType enum and references (dead code)
- Add proper server sign-out call before clearing local state

Resolves issue where User Settings view didn't display persona text even though it existed in the database.
2026-05-12 01:15:00 -04:00

253 lines
9.7 KiB
TypeScript

import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { userApi } from "../lib/api";
import { useAppContext } from "../App";
const PERSONA_MAX = 500;
export default function Settings() {
const { user, updateUser, setStatusMessage } = useAppContext();
const navigate = useNavigate();
const [displayName, setDisplayName] = useState(user?.displayName ?? "");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [persona, setPersona] = useState(user?.persona ?? "");
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [saving, setSaving] = useState(false);
const personaRemaining = PERSONA_MAX - persona.length;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setSuccess("");
if (displayName.length < 3 || displayName.length > 30) {
setError("Display Name must be between 3-30 characters.");
return;
}
const changingPassword =
currentPassword || newPassword || confirmPassword;
if (changingPassword) {
if (!currentPassword) {
setError("Current password is required to change your password.");
return;
}
if (!newPassword) {
setError("New password is required.");
return;
}
if (newPassword.length < 8) {
setError("New password must be at least 8 characters.");
return;
}
if (newPassword !== confirmPassword) {
setError("New passwords do not match.");
return;
}
}
if (persona.length > PERSONA_MAX) {
setError(`Persona must be ${PERSONA_MAX} characters or fewer.`);
return;
}
setSaving(true);
try {
const body: Record<string, string | undefined> = {
displayName,
persona,
};
if (changingPassword) {
body.currentPassword = currentPassword;
body.password = newPassword;
}
const updatedUser = await userApi.updateSettings(body);
setSuccess("Settings saved successfully.");
setStatusMessage("Settings updated.");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
// Update React context + localStorage in one place
updateUser(updatedUser);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save settings.");
} finally {
setSaving(false);
}
};
return (
<div className="flex-1 flex bg-bg-primary overflow-hidden">
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-xl mx-auto">
<div className="border-2 border-border-default p-6 rounded bg-bg-secondary">
<div className="font-mono text-text-secondary text-sm">
<div className="mb-1 text-text-muted">// USER SETTINGS</div>
<div className="mb-6 text-text-primary">ACCOUNT CONFIGURATION</div>
<div className="mb-6 p-3 border border-border-subtle rounded bg-bg-tertiary">
<div className="text-text-muted text-xs">
Signed in as <span className="text-text-primary">{user?.email}</span>
</div>
<div className="text-text-muted text-xs mt-1">
Email address cannot be changed. Contact your administrator.
</div>
</div>
{error && (
<div className="mb-4 p-3 border border-red-800 rounded bg-red-950/50">
<div className="text-red-400 font-mono text-sm">
<span className="text-text-muted">// ERROR: </span>
{error}
</div>
</div>
)}
{success && (
<div className="mb-4 p-3 border border-green-800 rounded bg-green-950/50">
<div className="text-green-400 font-mono text-sm">
<span className="text-text-muted">// SUCCESS: </span>
{success}
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label
htmlFor="displayName"
className="block text-sm text-text-muted mb-1 font-mono"
>
display_name
</label>
<input
id="displayName"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight"
minLength={3}
maxLength={30}
required
/>
</div>
<div>
<div className="text-xs text-text-muted mb-3 font-mono tracking-wider">
--- PASSWORD ---
</div>
<div className="space-y-3">
<div>
<label
htmlFor="currentPassword"
className="block text-sm text-text-muted mb-1 font-mono"
>
current_password
</label>
<input
id="currentPassword"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight"
autoComplete="current-password"
/>
</div>
<div>
<label
htmlFor="newPassword"
className="block text-sm text-text-muted mb-1 font-mono"
>
new_password
</label>
<input
id="newPassword"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight"
autoComplete="new-password"
/>
</div>
<div>
<label
htmlFor="confirmPassword"
className="block text-sm text-text-muted mb-1 font-mono"
>
confirm_new_password
</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight"
autoComplete="new-password"
/>
</div>
</div>
</div>
<div>
<div className="text-xs text-text-muted mb-3 font-mono tracking-wider">
--- PERSONA ---
</div>
<label
htmlFor="persona"
className="block text-sm text-text-muted mb-1 font-mono"
>
Describe yourself for your Agent
</label>
<textarea
id="persona"
value={persona}
onChange={(e) => setPersona(e.target.value)}
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight resize-y min-h-[120px]"
maxLength={PERSONA_MAX}
placeholder="e.g. I'm a frontend engineer focused on React. I need extra help with accessibility and testing..."
/>
<div
className={`text-right text-xs mt-1 font-mono ${
personaRemaining < 50
? "text-red-400"
: "text-text-muted"
}`}
>
{personaRemaining}/{PERSONA_MAX} characters remaining
</div>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => navigate(-1)}
className="flex-1 px-4 py-2 text-center border border-border-highlight text-text-primary hover:bg-bg-tertiary rounded transition-colors font-mono text-sm"
>
CANCEL
</button>
<button
type="submit"
disabled={saving}
className="flex-1 px-4 py-2 bg-brand hover:bg-red-700 text-white rounded transition-colors disabled:opacity-50 font-mono text-sm"
>
{saving ? "SAVING..." : "SAVE SETTINGS"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
);
}