login issue

This commit is contained in:
2026-09-19 13:12:19 +05:30
parent dccb1beda5
commit ea3dbbeaf3
41 changed files with 2342 additions and 163 deletions

View File

@@ -1,5 +1,6 @@
'use client';
import {useState} from 'react';
import {proportional} from '@astryxdesign/core/Table';
import type {TableColumn} from '@astryxdesign/core/Table';
import {VStack, HStack} from '@astryxdesign/core/Layout';
@@ -11,7 +12,12 @@ import {PanelCard} from '@/shared/components/patterns/PanelCard';
import {ResponsiveTable} from '@/shared/components/patterns/ResponsiveTable';
import {SkeletonRows} from '@/shared/components/patterns/LoadingState';
import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel';
import {useTeam} from '@/features/team/hooks/useTeam';
import {Button} from '@astryxdesign/core/Button';
import {Banner} from '@astryxdesign/core/Banner';
import {useInvitations, useTeam} from '@/features/team/hooks/useTeam';
import {teamRepository} from '@/features/team/repositories/teamRepository';
import {AddMemberDialog} from '@/features/team/components/AddMemberDialog';
import {SecretOnce} from '@/shared/components/patterns/SecretOnce';
import type {TeamMember} from '@/features/team/types/team';
/**
@@ -26,18 +32,17 @@ import type {TeamMember} from '@/features/team/types/team';
* local state and were lost on refresh. A merchant could not tell any of it
* from the real thing.
*
* ── Why this screen is read-only ─────────────────────────────────────────
* Accounts are created by INVITATION, and that flow already exists in the
* platform's own web app: a manager mints a code, the holder redeems it and
* chooses their own password. Adding a second way to create an account here
* would be a second place a login can be born, on the one surface where that
* matters most. So this lists the team and does not mint anyone.
* ── Why the actions are here now ─────────────────────────────────────────
* This was read-only, because `PATCH /api/team/{id}`, the invitation endpoints
* and `POST /api/team/members` all answered 404 on the deployed platform when
* it was built. They do not any more, so the screen does what it always
* described: lists who has access and changes it.
*
* Role changes and deactivation are supported upstream by
* `PATCH /api/team/{id}` and are NOT wired here — see the report. Deactivating
* revokes every session that person holds immediately, so it wants a
* confirmation step and `409 last_owner` handling, neither of which belongs in
* a change whose purpose is removing invented data.
* Deactivating revokes every session that person holds IMMEDIATELY — it is not
* a flag that takes effect at their next sign-in. The platform also refuses to
* remove the last active owner, answering `409 last_owner`; that message is
* surfaced verbatim rather than flattened into "something went wrong", because
* it names a rule the merchant can act on.
*/
interface TeamRow extends Record<string, unknown> {
@@ -113,6 +118,36 @@ const COLUMNS: TableColumn<TeamRow>[] = [
export function TeamManagement() {
const team = useTeam();
const invitations = useInvitations();
const [adding, setAdding] = useState(false);
const [error, setError] = useState<string | null>(null);
const [reset, setReset] = useState<{label: string; value: string} | null>(
null,
);
function refresh() {
team.refetch();
invitations.refetch();
}
async function setActive(id: string, active: boolean) {
setError(null);
const res = await teamRepository.update(id, {active});
// `last_owner` is the one failure worth repeating word for word: it names a
// rule rather than a fault, and the merchant can act on it.
if (!res.ok) setError(res.message ?? 'Could not change that account.');
refresh();
}
async function resetPassword(id: string, email: string) {
setError(null);
const res = await teamRepository.resetPassword(id);
if (!res.ok || !res.data) {
setError(res.message ?? 'Could not reset that password.');
return;
}
setReset({label: `New password for ${email}`, value: res.data.password});
}
return (
<PanelCard
@@ -137,13 +172,100 @@ export function TeamManagement() {
summaryKeys={['role', 'status']}
columns={COLUMNS}
/>
{/* Says where accounts come from, because the button that used to
claim to create one has gone. Without this the screen reads as
missing a feature rather than pointing at the right place. */}
<Text size="xsm" color="secondary">
New members are added by invitation, and choose their own password
when they redeem it. Roles and access are managed on the platform.
</Text>
{error ? <Banner status="error" title={error} /> : null}
{reset ? (
<SecretOnce value={reset.value} label={reset.label} />
) : null}
<HStack gap={2} hAlign="between" vAlign="center">
<Text size="xsm" color="secondary">
An invitation lets somebody choose their own password; a created
login shows one once. Deactivating signs that person out at once.
</Text>
<Button size="sm" onClick={() => setAdding(true)} label="Add someone" />
</HStack>
{members.some((m) => !m.active) ? (
<VStack gap={2}>
<Text size="xsm" color="secondary">
Deactivated
</Text>
{members
.filter((m) => !m.active)
.map((m) => (
<HStack key={m.id} gap={2} vAlign="center" hAlign="between">
<Text size="sm">{m.name}</Text>
<Button
size="sm"
variant="secondary"
onClick={() => void setActive(m.id, true)}
label="Restore access"
/>
</HStack>
))}
</VStack>
) : null}
<VStack gap={2}>
{members
.filter((m) => m.active)
.map((m) => (
<HStack key={m.id} gap={2} vAlign="center" hAlign="between">
<Text size="sm" color="secondary">
{m.email}
</Text>
<HStack gap={2}>
<Button
size="sm"
variant="secondary"
onClick={() => void resetPassword(m.id, m.email)}
label="Reset password"
/>
<Button
size="sm"
variant="secondary"
onClick={() => void setActive(m.id, false)}
label="Deactivate"
/>
</HStack>
</HStack>
))}
</VStack>
{invitations.data && invitations.data.length > 0 ? (
<VStack gap={2}>
<Text size="xsm" color="secondary">
Invited, not yet joined
</Text>
{invitations.data.map((inv) => (
<HStack key={inv.id} gap={2} vAlign="center" hAlign="between">
<Text size="sm" color="secondary">
{inv.email} · {inv.role}
</Text>
<Button
size="sm"
variant="secondary"
onClick={() => {
void teamRepository
.revokeInvitation(inv.id)
.then(() => invitations.refetch());
}}
label="Withdraw"
/>
</HStack>
))}
</VStack>
) : null}
{adding ? (
<AddMemberDialog
onClose={() => setAdding(false)}
onDone={() => {
setAdding(false);
refresh();
}}
/>
) : null}
</VStack>
)}
</PanelCard>