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) => {
|
server.post("/admin/customers/:customerId/invite-portal-user", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const currentUser = await requireAdmin(req, reply);
|
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>) => {
|
const updateUser = async (id: string, body: Record<string, any>) => {
|
||||||
return await $api(`/api/admin/users/${id}`, {
|
return await $api(`/api/admin/users/${id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@@ -106,6 +113,7 @@ export const useAdmin = () => {
|
|||||||
return {
|
return {
|
||||||
getOverview,
|
getOverview,
|
||||||
createUser,
|
createUser,
|
||||||
|
createUserForProfile,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserAccess,
|
updateUserAccess,
|
||||||
createTenant,
|
createTenant,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const admin = useAdmin()
|
||||||
const { $api } = useNuxtApp()
|
const { $api } = useNuxtApp()
|
||||||
|
|
||||||
const id = route.params.id as string
|
const id = route.params.id as string
|
||||||
@@ -10,11 +12,21 @@ const branches = ref<any[]>([])
|
|||||||
const teams = ref<any[]>([])
|
const teams = ref<any[]>([])
|
||||||
const pending = ref(true)
|
const pending = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
const creatingLinkedUser = ref(false)
|
||||||
|
const createLinkedUserModalOpen = ref(false)
|
||||||
|
const createdLinkedUserPassword = ref("")
|
||||||
|
const createLinkedUserForm = reactive({
|
||||||
|
email: "",
|
||||||
|
})
|
||||||
const selectMenuUi = {
|
const selectMenuUi = {
|
||||||
base: 'w-full',
|
base: 'w-full',
|
||||||
content: 'min-w-[min(32rem,90vw)] w-max max-w-[90vw]'
|
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() {
|
async function fetchBranches() {
|
||||||
try {
|
try {
|
||||||
branches.value = await useEntities("branches").select()
|
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 = [
|
const weekdays = [
|
||||||
{ key: '1', label: 'Montag' },
|
{ key: '1', label: 'Montag' },
|
||||||
{ key: '2', label: 'Dienstag' },
|
{ key: '2', label: 'Dienstag' },
|
||||||
@@ -243,14 +303,25 @@ onMounted(async () => {
|
|||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<UDashboardToolbar>
|
<UDashboardToolbar>
|
||||||
<template #right>
|
<template #right>
|
||||||
<UButton
|
<div class="flex items-center gap-2">
|
||||||
icon="i-mdi-content-save"
|
<UButton
|
||||||
color="primary"
|
v-if="canCreateLinkedUser"
|
||||||
:loading="saving"
|
icon="i-heroicons-user-plus"
|
||||||
@click="saveProfile"
|
color="neutral"
|
||||||
>
|
variant="outline"
|
||||||
Speichern
|
@click="openCreateLinkedUserModal"
|
||||||
</UButton>
|
>
|
||||||
|
Benutzer anlegen
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
icon="i-mdi-content-save"
|
||||||
|
color="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveProfile"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardToolbar>
|
</UDashboardToolbar>
|
||||||
|
|
||||||
@@ -263,6 +334,9 @@ onMounted(async () => {
|
|||||||
<h2 class="text-xl font-semibold text-gray-900">{{ profile.full_name }}</h2>
|
<h2 class="text-xl font-semibold text-gray-900">{{ profile.full_name }}</h2>
|
||||||
<p class="text-sm text-gray-500">{{ profile.employee_number || '–' }}</p>
|
<p class="text-sm text-gray-500">{{ profile.employee_number || '–' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<UBadge :color="linkedUserStatusColor" variant="subtle">
|
||||||
|
{{ linkedUserStatusLabel }}
|
||||||
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<USeparator label="Persönliche Daten" />
|
<USeparator label="Persönliche Daten" />
|
||||||
@@ -478,5 +552,46 @@ onMounted(async () => {
|
|||||||
</UCard>
|
</UCard>
|
||||||
|
|
||||||
<USkeleton v-if="pending" height="300px" />
|
<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>
|
</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>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user