- 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.
253 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
}
|