Benutzeranlage direkt aus Mitarbeiterprofil ermöglichen
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user