user management keyed (267 -> 214)
Accounts, permissions and the create-user form. The confirm dialogs for removing an account and resetting a password were multi-line JSX the scanner cannot see, so they were converted on sight rather than from its list. Cancel comes from ui.button.cancel in all four places it appears here.
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { CreateUserForm } from './CreateUserForm';
|
||||
import { copyToClipboard } from 'helpers/clipboard';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
|
||||
type ManagedUser = {
|
||||
id: number;
|
||||
@@ -43,6 +44,7 @@ type UsersResponse = {
|
||||
const USERS_KEY = ['MANAGED_USERS'];
|
||||
|
||||
export const UsersSection = () => {
|
||||
const { t, tUi } = useTranslation();
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const [pendingId, setPendingId] = useState<number | null>(null);
|
||||
@@ -77,11 +79,7 @@ export const UsersSection = () => {
|
||||
* reasons to press this, and an empty answer means "leave authorized_keys alone" rather than "remove it".
|
||||
*/
|
||||
const provisionLinux = async (user: ManagedUser) => {
|
||||
const key = window.prompt(
|
||||
`Linux account for ${user.email}.\n\n` +
|
||||
`Paste an SSH public key to allow them to SSH in, or leave empty to keep the current one.`,
|
||||
'',
|
||||
);
|
||||
const key = window.prompt(t('settings.users.provisionPrompt', { email: user.email }), '');
|
||||
// Cancel is null; empty string is a deliberate "no change".
|
||||
if (key === null) return;
|
||||
|
||||
@@ -147,13 +145,13 @@ export const UsersSection = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading accounts…
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> {t('settings.users.loadingAccounts')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return <div className="text-sm text-destructive">Could not load the accounts.</div>;
|
||||
return <div className="text-sm text-destructive">{t('settings.users.accountsFailed')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -173,7 +171,12 @@ export const UsersSection = () => {
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate text-sm font-medium">{user.name || user.username || user.email}</span>
|
||||
{user.isOwner && <Crown className="h-3.5 w-3.5 shrink-0 text-duck-teal" aria-label="Server owner" />}
|
||||
{user.isOwner && (
|
||||
<Crown
|
||||
className="h-3.5 w-3.5 shrink-0 text-duck-teal"
|
||||
aria-label={t('settings.users.serverOwner')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
@@ -212,11 +215,13 @@ export const UsersSection = () => {
|
||||
size="icon"
|
||||
className={`shrink-0 ${user.osUser ? 'text-muted-foreground' : 'text-amber-500'}`}
|
||||
disabled={busy}
|
||||
aria-label={user.osUser ? `Repair ${user.email}'s Linux account` : `Create a Linux account`}
|
||||
aria-label={
|
||||
user.osUser ? t('settings.users.repairFor', { email: user.email }) : t('settings.users.createLinux')
|
||||
}
|
||||
title={
|
||||
user.osUser
|
||||
? `Linux account: ${user.osUser} — click to repair or replace their SSH key`
|
||||
: 'No Linux account — click to create one'
|
||||
? t('settings.users.linuxAccountFor', { osUser: user.osUser })
|
||||
: t('settings.users.noLinux')
|
||||
}
|
||||
onClick={() => void provisionLinux(user)}
|
||||
>
|
||||
@@ -231,11 +236,11 @@ export const UsersSection = () => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 text-muted-foreground"
|
||||
aria-label={`Copy ${user.email}'s SSH public key`}
|
||||
title="Copy their SSH public key (add it to their Gitea account)"
|
||||
aria-label={t('settings.users.copySshFor', { email: user.email })}
|
||||
title={t('settings.users.copySshTitle')}
|
||||
onClick={() => {
|
||||
void copyToClipboard(user.osSshPublicKey!);
|
||||
toast.success('Public key copied');
|
||||
toast.success(t('settings.users.publicKeyCopied'));
|
||||
}}
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
@@ -250,8 +255,8 @@ export const UsersSection = () => {
|
||||
size="icon"
|
||||
className="shrink-0 text-muted-foreground"
|
||||
disabled={busy}
|
||||
aria-label={`Reset ${user.email}'s password`}
|
||||
title="Generate a new password — shown once, and signs them out everywhere"
|
||||
aria-label={t('settings.users.resetPasswordFor', { email: user.email })}
|
||||
title={t('settings.users.resetPasswordTitle')}
|
||||
onClick={() => setConfirmReset(user)}
|
||||
>
|
||||
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <RotateCcw className="h-4 w-4" />}
|
||||
@@ -264,7 +269,7 @@ export const UsersSection = () => {
|
||||
className="shrink-0 text-muted-foreground hover:text-destructive"
|
||||
disabled={user.isOwner || busy}
|
||||
onClick={() => setConfirmDelete(user)}
|
||||
aria-label={`Remove ${user.email}`}
|
||||
aria-label={t('settings.users.removeFor', { email: user.email })}
|
||||
>
|
||||
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
||||
</Button>
|
||||
@@ -282,19 +287,18 @@ export const UsersSection = () => {
|
||||
<AlertDialog open={!!confirmDelete} onOpenChange={(open) => !open && setConfirmDelete(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove {confirmDelete?.email}?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This deletes the account and everything belonging to it — passkeys, dashboards, saved layouts, and
|
||||
anything an installed plugin keeps for them. It cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogTitle>
|
||||
{t('settings.users.removeTitle', { email: confirmDelete?.email ?? '' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>{t('settings.users.removeBody')}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{tUi('button.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={() => confirmDelete && void remove(confirmDelete)}
|
||||
>
|
||||
Remove account
|
||||
{t('settings.users.removeAccount')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -305,23 +309,16 @@ export const UsersSection = () => {
|
||||
<AlertDialog open={!!confirmReset} onOpenChange={(open) => !open && setConfirmReset(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reset the password for {confirmReset?.email}?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('settings.users.resetTitle', { email: confirmReset?.email ?? '' })}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
A new password is generated and shown to you once — it is not stored anywhere and cannot be looked up
|
||||
afterwards. Their existing password stops working immediately, and they are signed out everywhere.
|
||||
{confirmReset?.osUser ? (
|
||||
<>
|
||||
{' '}
|
||||
Their Linux account ({confirmReset.osUser}) is not affected: it has no password, and SSH keys are
|
||||
unchanged.
|
||||
</>
|
||||
) : null}
|
||||
{t('settings.users.resetBody')}
|
||||
{confirmReset?.osUser ? ` ${t('settings.users.resetLinuxNote', { osUser: confirmReset.osUser })}` : null}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{tUi('button.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => confirmReset && void resetPassword(confirmReset)}>
|
||||
Generate a new password
|
||||
{t('settings.users.generateNewPassword')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -345,11 +342,11 @@ export const UsersSection = () => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
aria-label="Copy the new password"
|
||||
aria-label={t('settings.users.copyNewPassword')}
|
||||
onClick={() => {
|
||||
if (!newPassword) return;
|
||||
void copyToClipboard(newPassword.password).then((ok) =>
|
||||
ok ? toast.success('Password copied') : toast.error('Could not copy — select it and copy by hand'),
|
||||
ok ? toast.success(t('settings.users.passwordCopied')) : toast.error(t('settings.users.copyFailed')),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -358,7 +355,7 @@ export const UsersSection = () => {
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={() => setNewPassword(null)}>Done</AlertDialogAction>
|
||||
<AlertDialogAction onClick={() => setNewPassword(null)}>{t('settings.users.done')}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
Reference in New Issue
Block a user