174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useState, type FormEvent } from "react";
|
|
import { ActionButton } from "@/components/ActionButton";
|
|
import { apiFetch, errorMessage } from "@/lib/client-api";
|
|
import type { Account, Role } from "@/lib/types";
|
|
|
|
const ROLES: { value: Role; label: string }[] = [
|
|
{ value: "viewer", label: "leitura" },
|
|
{ value: "admin", label: "admin" },
|
|
{ value: "owner", label: "owner" },
|
|
];
|
|
|
|
export type UserOption = { id: number; name: string };
|
|
|
|
function UserSelect({
|
|
users,
|
|
value,
|
|
onChange,
|
|
label,
|
|
}: {
|
|
users: UserOption[];
|
|
value: number | null;
|
|
onChange: (id: number | null) => void;
|
|
label: string;
|
|
}) {
|
|
return (
|
|
<select value={value ?? ""} onChange={(e) => onChange(e.target.value ? Number(e.target.value) : null)} aria-label={label}>
|
|
<option value="">Sem usuário ligado</option>
|
|
{users.map((u) => (
|
|
<option key={u.id} value={u.id}>
|
|
{u.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
);
|
|
}
|
|
|
|
export function CreateAccountForm({ users }: { users: UserOption[] }) {
|
|
const router = useRouter();
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [role, setRole] = useState<Role>("admin");
|
|
const [userId, setUserId] = useState<number | null>(null);
|
|
const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function submit(e: FormEvent) {
|
|
e.preventDefault();
|
|
setBusy(true);
|
|
setMessage(null);
|
|
try {
|
|
await apiFetch("/accounts", { method: "POST", body: { username, password, role, user_id: userId } });
|
|
setUsername("");
|
|
setUserId(null);
|
|
setPassword("");
|
|
setMessage({ ok: true, text: "Conta criada." });
|
|
router.refresh();
|
|
} catch (err) {
|
|
setMessage({ ok: false, text: errorMessage(err) });
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form className="inline-form" onSubmit={submit} autoComplete="off">
|
|
<label className="field">
|
|
<span>Usuário</span>
|
|
<input value={username} onChange={(e) => setUsername(e.target.value)} pattern="[a-zA-Z0-9._-]{3,50}" required />
|
|
</label>
|
|
<label className="field">
|
|
<span>Senha (mínimo 12 caracteres)</span>
|
|
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} minLength={12} autoComplete="new-password" required />
|
|
</label>
|
|
<label className="field">
|
|
<span>Papel</span>
|
|
<select value={role} onChange={(e) => setRole(e.target.value as Role)}>
|
|
{ROLES.map((r) => (
|
|
<option key={r.value} value={r.value}>
|
|
{r.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="field">
|
|
<span>Usuário do Telegram (leitura só vê este)</span>
|
|
<UserSelect users={users} value={userId} onChange={setUserId} label="Usuário do Telegram" />
|
|
</label>
|
|
<button className="btn primary" type="submit" disabled={busy}>
|
|
Criar
|
|
</button>
|
|
{message && <p className={message.ok ? "ok-text" : "error-text"}>{message.text}</p>}
|
|
</form>
|
|
);
|
|
}
|
|
|
|
export function AccountActions({ account, isSelf, users }: { account: Account; isSelf: boolean; users: UserOption[] }) {
|
|
const router = useRouter();
|
|
const [error, setError] = useState("");
|
|
|
|
async function changeRole(role: Role) {
|
|
setError("");
|
|
try {
|
|
await apiFetch(`/accounts/${account.id}`, { method: "PATCH", body: { role } });
|
|
router.refresh();
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
}
|
|
}
|
|
|
|
async function changeUser(userId: number | null) {
|
|
setError("");
|
|
try {
|
|
await apiFetch(`/accounts/${account.id}`, { method: "PATCH", body: { user_id: userId } });
|
|
router.refresh();
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
}
|
|
}
|
|
|
|
async function resetPassword() {
|
|
const password = window.prompt(`Nova senha para ${account.username} (mínimo 12 caracteres):`);
|
|
if (!password) return;
|
|
setError("");
|
|
try {
|
|
await apiFetch(`/accounts/${account.id}/reset-password`, { method: "POST", body: { password } });
|
|
window.alert("Senha redefinida. As sessões abertas dessa conta foram encerradas.");
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="actions">
|
|
<select value={account.role} onChange={(e) => changeRole(e.target.value as Role)} aria-label="Papel">
|
|
{ROLES.map((r) => (
|
|
<option key={r.value} value={r.value}>
|
|
{r.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<UserSelect users={users} value={account.user_id} onChange={changeUser} label="Usuário do Telegram" />
|
|
<button type="button" className="btn secondary" onClick={resetPassword}>
|
|
Redefinir senha
|
|
</button>
|
|
{!isSelf &&
|
|
(account.disabled ? (
|
|
<ActionButton path={`/accounts/${account.id}`} method="PATCH" body={{ disabled: false }} label="Reativar" />
|
|
) : (
|
|
<ActionButton
|
|
path={`/accounts/${account.id}`}
|
|
method="PATCH"
|
|
body={{ disabled: true }}
|
|
label="Desativar"
|
|
variant="danger"
|
|
confirm={`Desativar a conta ${account.username}? As sessões abertas serão encerradas.`}
|
|
/>
|
|
))}
|
|
{!isSelf && (
|
|
<ActionButton
|
|
path={`/accounts/${account.id}`}
|
|
method="DELETE"
|
|
label="Excluir"
|
|
variant="danger"
|
|
confirm={`Excluir a conta ${account.username} de vez? Não dá para desfazer; a auditoria mantém o nome da conta.`}
|
|
/>
|
|
)}
|
|
{error && <span className="error-text">{error}</span>}
|
|
</div>
|
|
);
|
|
}
|