Add admin-managed user creation
linter / quality (pull_request) Successful in 1m14s
tests / ci (8.4) (pull_request) Successful in 1m37s

This commit is contained in:
2026-05-22 12:55:44 +00:00
parent 2bb74c20e1
commit d13e919310
17 changed files with 442 additions and 8 deletions
+155
View File
@@ -0,0 +1,155 @@
<script setup lang="ts">
import { Form, Head, Link } from '@inertiajs/vue3';
import UserManagementController from '@/actions/App/Http/Controllers/Admin/UserManagementController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import PasswordInput from '@/components/PasswordInput.vue';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import AppLayout from '@/layouts/AppLayout.vue';
import { create, index } from '@/routes/admin/users';
import type { BreadcrumbItem } from '@/types';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'User management',
href: index(),
},
{
title: 'Create user',
href: create(),
},
];
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Create user" />
<div class="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6 p-4">
<Heading
title="Create user"
description="Create a user account and optionally force a password change on the next login."
/>
<Card>
<CardHeader>
<CardTitle>User details</CardTitle>
<CardDescription>
Set an initial password that the user can replace after
their first sign-in.
</CardDescription>
</CardHeader>
<CardContent>
<Form
v-bind="UserManagementController.store.form()"
:transform="(data) => ({
...data,
must_change_password_on_next_login:
data.must_change_password_on_next_login === true ||
data.must_change_password_on_next_login === 'on',
})"
class="space-y-6"
v-slot="{ errors, processing }"
>
<div class="grid gap-2">
<Label for="name">Name</Label>
<Input
id="name"
name="name"
required
autofocus
autocomplete="name"
placeholder="Jane Admin"
/>
<InputError :message="errors.name" />
</div>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
id="email"
name="email"
type="email"
required
autocomplete="email"
placeholder="jane@example.com"
/>
<InputError :message="errors.email" />
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<PasswordInput
id="password"
name="password"
required
autocomplete="new-password"
placeholder="Temporary password"
/>
<InputError :message="errors.password" />
</div>
<div class="grid gap-2">
<Label for="password_confirmation"
>Confirm password</Label
>
<PasswordInput
id="password_confirmation"
name="password_confirmation"
required
autocomplete="new-password"
placeholder="Confirm password"
/>
</div>
<div class="grid gap-2">
<Label
for="must_change_password_on_next_login"
class="flex items-start gap-3 rounded-lg border px-4 py-3"
>
<input
id="must_change_password_on_next_login"
name="must_change_password_on_next_login"
type="checkbox"
checked
class="border-input text-primary focus:ring-ring mt-0.5 size-4 rounded"
/>
<span class="space-y-1">
<span class="block font-medium">
Must change password on next login
</span>
<span class="text-muted-foreground block text-sm">
Redirect the user to the security settings page until they save a new password.
</span>
</span>
</Label>
<InputError
:message="errors.must_change_password_on_next_login"
/>
</div>
<div class="flex items-center gap-3">
<Button type="submit" :disabled="processing">
Create user
</Button>
<Button as-child variant="outline">
<Link :href="index()">Cancel</Link>
</Button>
</div>
</Form>
</CardContent>
</Card>
</div>
</AppLayout>
</template>
+24 -7
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Form, Head } from '@inertiajs/vue3';
import { FolderPlus, Shield, Users } from 'lucide-vue-next';
import { Form, Head, Link } from '@inertiajs/vue3';
import { FolderPlus, KeyRound, Shield, UserPlus, Users } from 'lucide-vue-next';
import UserManagementController from '@/actions/App/Http/Controllers/Admin/UserManagementController';
import Heading from '@/components/Heading.vue';
import { Badge } from '@/components/ui/badge';
@@ -13,7 +13,7 @@ import {
CardTitle,
} from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { index as adminUsers } from '@/routes/admin/users';
import { create as createAdminUser, index as adminUsers } from '@/routes/admin/users';
import type { AdminUsersPageProps, BreadcrumbItem } from '@/types';
const props = defineProps<AdminUsersPageProps>();
@@ -40,10 +40,19 @@ const hasPermission = (actorId: number, permission: string) =>
description="Assign the global permissions that unlock platform-wide administration and project creation."
/>
<Badge variant="outline" class="gap-1.5 px-3 py-1">
<Users class="size-3.5" />
Admin-only area
</Badge>
<div class="flex items-center gap-3">
<Button as-child>
<Link :href="createAdminUser()">
<UserPlus class="size-4" />
User create
</Link>
</Button>
<Badge variant="outline" class="gap-1.5 px-3 py-1">
<Users class="size-3.5" />
Admin-only area
</Badge>
</div>
</div>
<div
@@ -108,6 +117,14 @@ const hasPermission = (actorId: number, permission: string) =>
</td>
<td class="px-4 py-4">
<div class="flex flex-wrap gap-2">
<Badge
v-if="user.must_change_password_on_next_login"
variant="outline"
class="gap-1.5"
>
<KeyRound class="size-3.5" />
Must change password
</Badge>
<Badge
v-if="
hasPermission(
+15
View File
@@ -19,13 +19,17 @@ import type { BreadcrumbItem } from '@/types';
type Props = {
canManageTwoFactor?: boolean;
mustChangePassword?: boolean;
requiresConfirmation?: boolean;
status?: string | null;
twoFactorEnabled?: boolean;
};
withDefaults(defineProps<Props>(), {
canManageTwoFactor: false,
mustChangePassword: false,
requiresConfirmation: false,
status: null,
twoFactorEnabled: false,
});
@@ -49,6 +53,17 @@ onUnmounted(() => clearTwoFactorAuthData());
<h1 class="sr-only">Security settings</h1>
<SettingsLayout>
<div
v-if="mustChangePassword || status"
class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100"
>
{{
mustChangePassword
? 'You must change your password before continuing anywhere else in the app.'
: status
}}
</div>
<div class="space-y-6">
<Heading
variant="small"