Benutzeranlage direkt aus Mitarbeiterprofil ermöglichen

This commit is contained in:
2026-04-29 16:23:35 +02:00
parent d5aed2140e
commit 2d26cedaa3
3 changed files with 241 additions and 8 deletions

View File

@@ -451,6 +451,116 @@ export default async function adminRoutes(server: FastifyInstance) {
}
});
// -------------------------------------------------------------
// POST /admin/profiles/:profileId/create-user
// -------------------------------------------------------------
server.post("/admin/profiles/:profileId/create-user", async (req, reply) => {
try {
const currentUser = await requireAdmin(req, reply);
if (!currentUser) return;
const { profileId } = req.params as { profileId: string };
const body = req.body as { email?: string };
const email = body.email?.trim().toLowerCase();
if (!email) {
return reply.code(400).send({ error: "email required" });
}
const [profile] = await server.db
.select({
id: authProfiles.id,
tenant_id: authProfiles.tenant_id,
user_id: authProfiles.user_id,
first_name: authProfiles.first_name,
last_name: authProfiles.last_name,
email: authProfiles.email,
})
.from(authProfiles)
.where(eq(authProfiles.id, profileId))
.limit(1);
if (!profile) {
return reply.code(404).send({ error: "Profile not found" });
}
if (profile.user_id) {
return reply.code(409).send({ error: "Profile already linked to a user" });
}
const existingUsers = await server.db
.select({ id: authUsers.id })
.from(authUsers)
.where(eq(authUsers.email, email))
.limit(1);
if (existingUsers.length) {
return reply.code(409).send({ error: "User with this email already exists" });
}
const initialPassword = generateRandomPassword(14);
const passwordHash = await hashPassword(initialPassword);
const result = await server.db.transaction(async (tx) => {
const [createdUser] = await tx
.insert(authUsers)
.values({
email,
passwordHash,
is_admin: false,
multiTenant: true,
must_change_password: true,
updatedAt: new Date(),
})
.returning({
id: authUsers.id,
email: authUsers.email,
must_change_password: authUsers.must_change_password,
is_admin: authUsers.is_admin,
multiTenant: authUsers.multiTenant,
created_at: authUsers.created_at,
});
await tx
.insert(authTenantUsers)
.values({
tenant_id: profile.tenant_id,
user_id: createdUser.id,
created_by: currentUser.id,
});
const [updatedProfile] = await tx
.update(authProfiles)
.set({
user_id: createdUser.id,
email,
})
.where(eq(authProfiles.id, profile.id))
.returning({
id: authProfiles.id,
tenant_id: authProfiles.tenant_id,
user_id: authProfiles.user_id,
first_name: authProfiles.first_name,
last_name: authProfiles.last_name,
email: authProfiles.email,
});
return {
user: createdUser,
profile: updatedProfile,
};
});
return {
...result,
initialPassword,
};
} catch (err) {
console.error("ERROR /admin/profiles/:profileId/create-user:", err);
return reply.code(500).send({ error: "Internal Server Error" });
}
});
server.post("/admin/customers/:customerId/invite-portal-user", async (req, reply) => {
try {
const currentUser = await requireAdmin(req, reply);

View File

@@ -69,6 +69,13 @@ export const useAdmin = () => {
})
}
const createUserForProfile = async (profileId: string, body: Record<string, any>) => {
return await $api(`/api/admin/profiles/${profileId}/create-user`, {
method: "POST",
body,
})
}
const updateUser = async (id: string, body: Record<string, any>) => {
return await $api(`/api/admin/users/${id}`, {
method: "PUT",
@@ -106,6 +113,7 @@ export const useAdmin = () => {
return {
getOverview,
createUser,
createUserForProfile,
updateUser,
updateUserAccess,
createTenant,

View File

@@ -2,6 +2,8 @@
const route = useRoute()
const toast = useToast()
const auth = useAuthStore()
const admin = useAdmin()
const { $api } = useNuxtApp()
const id = route.params.id as string
@@ -10,11 +12,21 @@ const branches = ref<any[]>([])
const teams = ref<any[]>([])
const pending = ref(true)
const saving = ref(false)
const creatingLinkedUser = ref(false)
const createLinkedUserModalOpen = ref(false)
const createdLinkedUserPassword = ref("")
const createLinkedUserForm = reactive({
email: "",
})
const selectMenuUi = {
base: 'w-full',
content: 'min-w-[min(32rem,90vw)] w-max max-w-[90vw]'
}
const canCreateLinkedUser = computed(() => Boolean(auth.user?.is_admin && profile.value && !profile.value.user_id))
const linkedUserStatusLabel = computed(() => profile.value?.user_id ? "Benutzer verknüpft" : "Kein Benutzer verknüpft")
const linkedUserStatusColor = computed(() => profile.value?.user_id ? "green" : "orange")
async function fetchBranches() {
try {
branches.value = await useEntities("branches").select()
@@ -135,6 +147,54 @@ async function saveProfile() {
}
}
function openCreateLinkedUserModal() {
if (!profile.value) return
createLinkedUserForm.email = profile.value.email || ""
createLinkedUserModalOpen.value = true
}
async function createLinkedUser() {
if (!profile.value || creatingLinkedUser.value) return
const email = createLinkedUserForm.email.trim().toLowerCase()
if (!email) {
toast.add({
title: 'E-Mail fehlt',
description: 'Bitte eine E-Mail-Adresse für den neuen Benutzer angeben.',
color: 'orange'
})
return
}
creatingLinkedUser.value = true
try {
const response = await admin.createUserForProfile(profile.value.id, { email })
createdLinkedUserPassword.value = response?.initialPassword || ""
createLinkedUserModalOpen.value = false
createLinkedUserForm.email = ""
toast.add({
title: 'Benutzer angelegt',
description: createdLinkedUserPassword.value ? `Initialpasswort: ${createdLinkedUserPassword.value}` : undefined,
color: 'green'
})
await fetchProfile()
} catch (err: any) {
console.error('[createLinkedUser]', err)
toast.add({
title: 'Benutzer konnte nicht angelegt werden',
description: err?.data?.error || err?.message || 'Unbekannter Fehler',
color: 'red'
})
} finally {
creatingLinkedUser.value = false
}
}
const weekdays = [
{ key: '1', label: 'Montag' },
{ key: '2', label: 'Dienstag' },
@@ -243,14 +303,25 @@ onMounted(async () => {
<!-- Toolbar -->
<UDashboardToolbar>
<template #right>
<UButton
icon="i-mdi-content-save"
color="primary"
:loading="saving"
@click="saveProfile"
>
Speichern
</UButton>
<div class="flex items-center gap-2">
<UButton
v-if="canCreateLinkedUser"
icon="i-heroicons-user-plus"
color="neutral"
variant="outline"
@click="openCreateLinkedUserModal"
>
Benutzer anlegen
</UButton>
<UButton
icon="i-mdi-content-save"
color="primary"
:loading="saving"
@click="saveProfile"
>
Speichern
</UButton>
</div>
</template>
</UDashboardToolbar>
@@ -263,6 +334,9 @@ onMounted(async () => {
<h2 class="text-xl font-semibold text-gray-900">{{ profile.full_name }}</h2>
<p class="text-sm text-gray-500">{{ profile.employee_number || '' }}</p>
</div>
<UBadge :color="linkedUserStatusColor" variant="subtle">
{{ linkedUserStatusLabel }}
</UBadge>
</div>
<USeparator label="Persönliche Daten" />
@@ -478,5 +552,46 @@ onMounted(async () => {
</UCard>
<USkeleton v-if="pending" height="300px" />
<div v-if="createdLinkedUserPassword" class="mt-4">
<UAlert
title="Initialpasswort für den neuen Benutzer"
:description="createdLinkedUserPassword"
color="amber"
variant="soft"
close-button
@close="createdLinkedUserPassword = ''"
/>
</div>
</UDashboardPanelContent>
<UModal v-model:open="createLinkedUserModalOpen">
<template #content>
<UCard>
<template #header>
<div>
<div class="text-lg font-semibold">Benutzer zum Profil anlegen</div>
<p class="mt-1 text-sm text-gray-500">
Es wird automatisch ein zufälliges Initialpasswort erzeugt und der neue Benutzer direkt mit diesem Mitarbeiterprofil verknüpft.
</p>
</div>
</template>
<UForm :state="createLinkedUserForm" class="space-y-4" @submit.prevent="createLinkedUser">
<UFormField label="E-Mail">
<UInput v-model="createLinkedUserForm.email" type="email" autocomplete="email" />
</UFormField>
<div class="flex justify-end gap-3 pt-2">
<UButton color="gray" variant="soft" @click="createLinkedUserModalOpen = false">
Abbrechen
</UButton>
<UButton type="submit" color="primary" :loading="creatingLinkedUser">
Benutzer anlegen
</UButton>
</div>
</UForm>
</UCard>
</template>
</UModal>
</template>