From 2d26cedaa330f2fc2d3d117bb31c22362c4b23ac Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Wed, 29 Apr 2026 16:23:35 +0200 Subject: [PATCH] =?UTF-8?q?Benutzeranlage=20direkt=20aus=20Mitarbeiterprof?= =?UTF-8?q?il=20erm=C3=B6glichen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/admin.ts | 110 +++++++++++++++++++++ frontend/composables/useAdmin.ts | 8 ++ frontend/pages/staff/profiles/[id].vue | 131 +++++++++++++++++++++++-- 3 files changed, 241 insertions(+), 8 deletions(-) diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 4e61207..0b05fc6 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -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); diff --git a/frontend/composables/useAdmin.ts b/frontend/composables/useAdmin.ts index 1ecbc03..b48ab4c 100644 --- a/frontend/composables/useAdmin.ts +++ b/frontend/composables/useAdmin.ts @@ -69,6 +69,13 @@ export const useAdmin = () => { }) } + const createUserForProfile = async (profileId: string, body: Record) => { + return await $api(`/api/admin/profiles/${profileId}/create-user`, { + method: "POST", + body, + }) + } + const updateUser = async (id: string, body: Record) => { return await $api(`/api/admin/users/${id}`, { method: "PUT", @@ -106,6 +113,7 @@ export const useAdmin = () => { return { getOverview, createUser, + createUserForProfile, updateUser, updateUserAccess, createTenant, diff --git a/frontend/pages/staff/profiles/[id].vue b/frontend/pages/staff/profiles/[id].vue index 2df2432..7286ea3 100644 --- a/frontend/pages/staff/profiles/[id].vue +++ b/frontend/pages/staff/profiles/[id].vue @@ -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([]) const teams = ref([]) 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 () => { @@ -263,6 +334,9 @@ onMounted(async () => {

{{ profile.full_name }}

{{ profile.employee_number || '–' }}

+ + {{ linkedUserStatusLabel }} + @@ -478,5 +552,46 @@ onMounted(async () => { + +
+ +
+ + + +