Files
cameleer-saas/ui/src/pages/tenant/SettingsPage.tsx

188 lines
6.5 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
import {
Alert,
Badge,
Button,
Card,
FormField,
Input,
Spinner,
useToast,
} from '@cameleer/design-system';
import { useTenantSettings, useChangeOwnPassword, useResetServerAdminPassword } from '../../api/tenant-hooks';
import { tierColor } from '../../utils/tier';
import styles from '../../styles/platform.module.css';
function statusColor(status: string): 'success' | 'error' | 'warning' | 'auto' {
switch (status?.toUpperCase()) {
case 'ACTIVE': return 'success';
case 'SUSPENDED': return 'warning';
case 'PROVISIONING': return 'auto';
case 'ERROR': return 'error';
default: return 'auto';
}
}
export function SettingsPage() {
const { data, isLoading, isError } = useTenantSettings();
const changePassword = useChangeOwnPassword();
const resetServerAdmin = useResetServerAdminPassword();
const { toast } = useToast();
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [serverAdminPw, setServerAdminPw] = useState('');
async function handleChangePassword(e: React.FormEvent) {
e.preventDefault();
if (newPassword.length < 8) {
toast({ title: 'Password must be at least 8 characters', variant: 'error' });
return;
}
if (newPassword !== confirmPassword) {
toast({ title: 'Passwords do not match', variant: 'error' });
return;
}
try {
await changePassword.mutateAsync(newPassword);
toast({ title: 'Password changed successfully', variant: 'success' });
setNewPassword('');
setConfirmPassword('');
} catch (err) {
toast({ title: 'Failed to change password', description: String(err), variant: 'error' });
}
}
if (isLoading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 64 }}>
<Spinner />
</div>
);
}
if (isError || !data) {
return (
<div style={{ padding: 24 }}>
<Alert variant="error" title="Failed to load settings">
Could not fetch settings. Please refresh.
</Alert>
</div>
);
}
return (
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20 }}>
<h1 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 600 }}>Settings</h1>
<Card title="Tenant Details">
<div className={styles.dividerList}>
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Name</span>
<span className={styles.kvValue}>{data.name}</span>
</div>
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Slug</span>
<span className={styles.kvValueMono}>{data.slug}</span>
</div>
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Tier</span>
<Badge label={data.tier} color={tierColor(data.tier)} />
</div>
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Status</span>
<Badge label={data.status} color={statusColor(data.status)} />
</div>
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Server Endpoint</span>
<span className={styles.kvValueMono}>{data.serverEndpoint ?? '—'}</span>
</div>
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Created</span>
<span className={styles.kvValue}>{new Date(data.createdAt).toLocaleDateString()}</span>
</div>
</div>
<p className={styles.description} style={{ marginTop: 16 }}>
To change your tier or other billing-related settings, please contact support.
</p>
</Card>
<Card title="Change Password">
<p className={styles.description} style={{ marginTop: 0 }}>
Update your login password. Minimum 8 characters.
</p>
<form onSubmit={handleChangePassword} style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12 }}>
<FormField label="New password" htmlFor="new-pw">
<Input
id="new-pw"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password"
required
minLength={8}
/>
</FormField>
<FormField label="Confirm password" htmlFor="confirm-pw">
<Input
id="confirm-pw"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password"
required
minLength={8}
/>
</FormField>
<div>
<Button type="submit" variant="primary" loading={changePassword.isPending}>
Change Password
</Button>
</div>
</form>
</Card>
<Card title="Server Admin Password">
<p className={styles.description} style={{ marginTop: 0 }}>
Reset the built-in admin password for your server dashboard (local login at <code>/login?local</code>).
</p>
<form
onSubmit={async (e) => {
e.preventDefault();
if (serverAdminPw.length < 8) {
toast({ title: 'Password must be at least 8 characters', variant: 'error' });
return;
}
try {
await resetServerAdmin.mutateAsync(serverAdminPw);
toast({ title: 'Server admin password reset successfully', variant: 'success' });
setServerAdminPw('');
} catch (err) {
toast({ title: 'Failed to reset server admin password', description: String(err), variant: 'error' });
}
}}
style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12 }}
>
<FormField label="New admin password" htmlFor="server-admin-pw">
<Input
id="server-admin-pw"
type="password"
value={serverAdminPw}
onChange={(e) => setServerAdminPw(e.target.value)}
placeholder="Enter new admin password"
required
minLength={8}
/>
</FormField>
<div>
<Button type="submit" variant="primary" loading={resetServerAdmin.isPending}>
Reset Admin Password
</Button>
</div>
</form>
</Card>
</div>
);
}