feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
import { Fragment, useState, type FormEvent } from 'react';
|
|
|
|
|
import { extractApiError } from '@/api/client';
|
|
|
|
|
import type { Role, User } from '@/api/types';
|
|
|
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
|
|
|
import {
|
|
|
|
|
useCreateUser,
|
|
|
|
|
useDeleteUser,
|
|
|
|
|
usePatchUser,
|
|
|
|
|
useUsersList,
|
|
|
|
|
} from '@/hooks/useUsers';
|
|
|
|
|
import { useToast } from '@/hooks/useToast';
|
|
|
|
|
import { FormField, Select, TextInput } from '@/components/FormField';
|
|
|
|
|
import { LoadingState } from '@/components/LoadingState';
|
|
|
|
|
import { ErrorState } from '@/components/ErrorState';
|
|
|
|
|
import { EmptyState } from '@/components/EmptyState';
|
|
|
|
|
|
|
|
|
|
const ROLE_OPTIONS: { value: Role; label: string }[] = [
|
|
|
|
|
{ value: 'admin', label: 'Admin' },
|
|
|
|
|
{ value: 'redteam', label: 'Red Team' },
|
|
|
|
|
{ value: 'soc', label: 'SOC' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
interface CreateFormState {
|
|
|
|
|
username: string;
|
|
|
|
|
password: string;
|
|
|
|
|
role: Role;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EMPTY_CREATE: CreateFormState = { username: '', password: '', role: 'redteam' };
|
|
|
|
|
|
|
|
|
|
export function UsersAdminPage(): JSX.Element {
|
|
|
|
|
const { user: currentUser } = useAuth();
|
|
|
|
|
const { push } = useToast();
|
|
|
|
|
const list = useUsersList();
|
|
|
|
|
const createMutation = useCreateUser();
|
|
|
|
|
const patchMutation = usePatchUser();
|
|
|
|
|
const deleteMutation = useDeleteUser();
|
|
|
|
|
|
|
|
|
|
const [createForm, setCreateForm] = useState<CreateFormState>(EMPTY_CREATE);
|
|
|
|
|
const [createError, setCreateError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// Per-row password reset state. Only one row open at a time.
|
|
|
|
|
const [resetOpen, setResetOpen] = useState<number | null>(null);
|
|
|
|
|
const [resetPassword, setResetPassword] = useState('');
|
|
|
|
|
|
|
|
|
|
const onCreate = async (e: FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setCreateError(null);
|
|
|
|
|
if (createForm.password.length < 8) {
|
|
|
|
|
setCreateError('Password must be at least 8 characters');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
await createMutation.mutateAsync(createForm);
|
|
|
|
|
setCreateForm(EMPTY_CREATE);
|
|
|
|
|
push('User created', 'success');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setCreateError(extractApiError(err, 'Could not create user'));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onRoleChange = async (u: User, role: Role) => {
|
|
|
|
|
if (u.role === role) return;
|
|
|
|
|
try {
|
|
|
|
|
await patchMutation.mutateAsync({ id: u.id, input: { role } });
|
|
|
|
|
push(`Role updated for ${u.username}`, 'success');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
push(extractApiError(err, 'Could not update role'), 'error');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onResetPassword = async (u: User, e: FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (resetPassword.length < 8) {
|
|
|
|
|
push('Password must be at least 8 characters', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
await patchMutation.mutateAsync({ id: u.id, input: { password: resetPassword } });
|
|
|
|
|
push(`Password reset for ${u.username}`, 'success');
|
|
|
|
|
setResetOpen(null);
|
|
|
|
|
setResetPassword('');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
push(extractApiError(err, 'Could not reset password'), 'error');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onDelete = async (u: User) => {
|
|
|
|
|
if (currentUser?.id === u.id) {
|
|
|
|
|
push('You cannot delete your own account', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!window.confirm(`Delete user "${u.username}"?`)) return;
|
|
|
|
|
try {
|
|
|
|
|
await deleteMutation.mutateAsync(u.id);
|
|
|
|
|
push('User deleted', 'success');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
push(extractApiError(err, 'Could not delete user'), 'error');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-xl">
|
|
|
|
|
<header>
|
2026-06-09 18:44:47 +02:00
|
|
|
<h1 className="text-[32px] font-medium leading-none">User accounts</h1>
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
<p className="text-charcoal text-[16px] mt-sm">
|
|
|
|
|
Manage local accounts. Admins can create new red team or SOC analysts.
|
|
|
|
|
</p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<section className="card-product flex flex-col gap-md">
|
|
|
|
|
<h2 className="text-[20px] font-medium">Create account</h2>
|
2026-05-27 20:19:16 +02:00
|
|
|
{/*
|
2026-05-27 20:28:32 +02:00
|
|
|
Option A structural fix (AC-17.3): labels / inputs / hints in 3 explicit grid rows
|
|
|
|
|
so the browser can never misalign them by collapsing different-height cells.
|
|
|
|
|
grid-rows-[auto_auto_auto] ensures row 1 = labels, row 2 = inputs, row 3 = hints.
|
2026-05-27 20:19:16 +02:00
|
|
|
*/}
|
2026-05-27 20:28:32 +02:00
|
|
|
<form
|
|
|
|
|
onSubmit={onCreate}
|
|
|
|
|
className="grid grid-cols-1 md:grid-cols-4 md:grid-rows-[auto_auto_auto] gap-x-md gap-y-xs"
|
|
|
|
|
>
|
|
|
|
|
{/* Row 1 — labels */}
|
|
|
|
|
<label htmlFor="new-username" className="text-[14px] font-medium text-ink">
|
|
|
|
|
Username <span className="text-bloom-deep">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label htmlFor="new-password" className="text-[14px] font-medium text-ink">
|
|
|
|
|
Password <span className="text-bloom-deep">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label htmlFor="new-role" className="text-[14px] font-medium text-ink">
|
|
|
|
|
Role <span className="text-bloom-deep">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div aria-hidden="true" />
|
|
|
|
|
|
|
|
|
|
{/* Row 2 — inputs + button (all same height = h-11) */}
|
|
|
|
|
<TextInput
|
|
|
|
|
id="new-username"
|
|
|
|
|
value={createForm.username}
|
|
|
|
|
onChange={(e) => setCreateForm({ ...createForm, username: e.target.value })}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
<TextInput
|
|
|
|
|
id="new-password"
|
|
|
|
|
type="password"
|
|
|
|
|
value={createForm.password}
|
|
|
|
|
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
|
|
|
|
required
|
|
|
|
|
minLength={8}
|
|
|
|
|
/>
|
|
|
|
|
<Select
|
|
|
|
|
id="new-role"
|
|
|
|
|
value={createForm.role}
|
|
|
|
|
onChange={(e) => setCreateForm({ ...createForm, role: e.target.value as Role })}
|
|
|
|
|
options={ROLE_OPTIONS}
|
|
|
|
|
/>
|
|
|
|
|
<button type="submit" className="btn-primary w-full" disabled={createMutation.isPending}>
|
|
|
|
|
{createMutation.isPending ? 'Creating…' : 'Create'}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* Row 3 — hints */}
|
|
|
|
|
<div aria-hidden="true" />
|
|
|
|
|
<span className="text-[12px] text-graphite">≥ 8 characters</span>
|
|
|
|
|
<div aria-hidden="true" />
|
|
|
|
|
<div aria-hidden="true" />
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
</form>
|
|
|
|
|
{createError ? (
|
|
|
|
|
<div role="alert" className="text-[14px] text-bloom-deep">
|
|
|
|
|
{createError}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="flex flex-col gap-md">
|
|
|
|
|
<h2 className="text-[20px] font-medium">All accounts</h2>
|
|
|
|
|
|
|
|
|
|
{list.isLoading ? <LoadingState label="Loading users…" /> : null}
|
|
|
|
|
{list.isError ? (
|
|
|
|
|
<ErrorState
|
|
|
|
|
message={extractApiError(list.error, 'Could not load users')}
|
|
|
|
|
onRetry={() => list.refetch()}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
{!list.isLoading && !list.isError && list.data && list.data.length === 0 ? (
|
|
|
|
|
<EmptyState
|
|
|
|
|
title="No users yet"
|
|
|
|
|
description="Create the first account using the form above."
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{!list.isLoading && !list.isError && list.data && list.data.length > 0 ? (
|
|
|
|
|
<div className="card-product overflow-hidden p-0">
|
|
|
|
|
<table className="w-full text-left">
|
|
|
|
|
<thead className="bg-cloud border-b border-hairline">
|
|
|
|
|
<tr className="text-[12px] uppercase tracking-[0.5px] text-graphite">
|
|
|
|
|
<th className="px-xl py-md">Username</th>
|
|
|
|
|
<th className="px-xl py-md">Role</th>
|
|
|
|
|
<th className="px-xl py-md">Created</th>
|
|
|
|
|
<th className="px-xl py-md text-right">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{list.data.map((u) => {
|
|
|
|
|
const isSelf = currentUser?.id === u.id;
|
|
|
|
|
return (
|
|
|
|
|
// Fragment must carry the key — `<>` cannot, which broke
|
|
|
|
|
// per-row reconciliation (reset-password state leaked across rows).
|
|
|
|
|
<Fragment key={u.id}>
|
|
|
|
|
<tr className="border-b border-hairline last:border-0">
|
2026-06-09 18:44:47 +02:00
|
|
|
<td className="px-xl py-md font-mono font-medium text-ink">
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
{u.username}
|
|
|
|
|
{isSelf ? (
|
|
|
|
|
<span className="ml-sm text-[12px] text-graphite uppercase tracking-[0.5px]">
|
|
|
|
|
(you)
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-xl py-md">
|
|
|
|
|
<Select
|
|
|
|
|
value={u.role}
|
|
|
|
|
onChange={(e) => onRoleChange(u, e.target.value as Role)}
|
|
|
|
|
options={ROLE_OPTIONS}
|
|
|
|
|
aria-label={`Change role for ${u.username}`}
|
|
|
|
|
disabled={patchMutation.isPending}
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
2026-06-09 18:44:47 +02:00
|
|
|
<td className="px-xl py-md text-charcoal font-mono">{u.created_at}</td>
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
<td className="px-xl py-md text-right">
|
|
|
|
|
<div className="inline-flex gap-sm">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-text-link"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setResetOpen(resetOpen === u.id ? null : u.id);
|
|
|
|
|
setResetPassword('');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Reset password
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-text-link text-bloom-deep disabled:text-steel"
|
|
|
|
|
disabled={isSelf || deleteMutation.isPending}
|
|
|
|
|
onClick={() => onDelete(u)}
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{resetOpen === u.id ? (
|
|
|
|
|
<tr className="border-b border-hairline last:border-0 bg-cloud">
|
|
|
|
|
<td colSpan={4} className="px-xl py-md">
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={(e) => onResetPassword(u, e)}
|
|
|
|
|
className="flex items-end gap-md"
|
|
|
|
|
>
|
|
|
|
|
<FormField
|
|
|
|
|
label={`New password for ${u.username}`}
|
|
|
|
|
htmlFor={`reset-${u.id}`}
|
|
|
|
|
hint="≥ 8 characters"
|
|
|
|
|
>
|
|
|
|
|
<TextInput
|
|
|
|
|
id={`reset-${u.id}`}
|
|
|
|
|
type="password"
|
|
|
|
|
value={resetPassword}
|
|
|
|
|
onChange={(e) => setResetPassword(e.target.value)}
|
|
|
|
|
minLength={8}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
<button type="submit" className="btn-primary">
|
|
|
|
|
Save password
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-outline-ink"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setResetOpen(null);
|
|
|
|
|
setResetPassword('');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
) : null}
|
|
|
|
|
</Fragment>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|