useCreateUserDialog.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. "use client";
  2. import React from "react";
  3. import { adminCreateUser, ApiClientError } from "@/lib/frontend/apiClient";
  4. import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
  5. import {
  6. notifySuccess,
  7. notifyError,
  8. notifyApiError,
  9. } from "@/lib/frontend/ui/toast";
  10. import {
  11. getPasswordPolicyHintLinesDe,
  12. reasonsToHintLinesDe,
  13. buildWeakPasswordMessageDe,
  14. } from "@/lib/frontend/profile/passwordPolicyUi";
  15. import { normalizeBranchIdDraft } from "@/components/admin/users/usersUi";
  16. import {
  17. CREATE_ROLE_OPTIONS,
  18. EMAIL_RE,
  19. BRANCH_RE,
  20. normalizeUsername,
  21. normalizeEmail,
  22. } from "@/components/admin/users/create-user/createUserUtils";
  23. const DEFAULT_FORM = Object.freeze({
  24. username: "",
  25. email: "",
  26. role: "branch",
  27. branchId: "",
  28. initialPassword: "",
  29. });
  30. function cloneDefaultForm() {
  31. return { ...DEFAULT_FORM };
  32. }
  33. function validateClient(form) {
  34. const username = normalizeUsername(form?.username);
  35. const email = normalizeEmail(form?.email);
  36. const role = String(form?.role || "").trim();
  37. const branchId =
  38. role === "branch" ? normalizeBranchIdDraft(form?.branchId) : "";
  39. const initialPassword = String(form?.initialPassword || "");
  40. if (!username) return { title: "Benutzername fehlt.", description: null };
  41. if (!email) return { title: "E-Mail fehlt.", description: null };
  42. if (!EMAIL_RE.test(email)) {
  43. return {
  44. title: "E-Mail ist ungültig.",
  45. description: "Bitte prüfen Sie die Eingabe.",
  46. };
  47. }
  48. const isKnownRole = CREATE_ROLE_OPTIONS.some((x) => x.value === role);
  49. if (!role || !isKnownRole)
  50. return { title: "Rolle fehlt.", description: null };
  51. if (role === "branch") {
  52. if (!branchId) {
  53. return {
  54. title: "Niederlassung fehlt.",
  55. description: "Für Niederlassungs-User ist eine NL erforderlich.",
  56. };
  57. }
  58. if (!BRANCH_RE.test(branchId)) {
  59. return {
  60. title: "Niederlassung ist ungültig.",
  61. description: "Format: NL01, NL02, ...",
  62. };
  63. }
  64. }
  65. if (!initialPassword.trim()) {
  66. return { title: "Initiales Passwort fehlt.", description: null };
  67. }
  68. return null;
  69. }
  70. function extractDuplicateFields(details) {
  71. if (!details || typeof details !== "object") return [];
  72. // Accept both shapes:
  73. // - { field: "username" }
  74. // - { fields: ["username","email"] }
  75. const one =
  76. typeof details.field === "string" && details.field.trim()
  77. ? details.field.trim()
  78. : null;
  79. const many = Array.isArray(details.fields)
  80. ? details.fields.map((x) => String(x))
  81. : null;
  82. const list = many ?? (one ? [one] : []);
  83. return Array.from(new Set(list.map(String)));
  84. }
  85. function mapDuplicateFieldsToGermanMessage(fields) {
  86. const hasUsername = fields.includes("username");
  87. const hasEmail = fields.includes("email");
  88. if (!hasUsername && !hasEmail) return null;
  89. if (hasUsername && hasEmail) {
  90. return {
  91. title: "Benutzername und E-Mail existieren bereits.",
  92. description: "Bitte wählen Sie andere Werte und versuchen Sie es erneut.",
  93. };
  94. }
  95. if (hasUsername) {
  96. return {
  97. title: "Benutzername existiert bereits.",
  98. description: "Bitte wählen Sie einen anderen Benutzernamen.",
  99. };
  100. }
  101. return {
  102. title: "E-Mail existiert bereits.",
  103. description: "Bitte wählen Sie eine andere E-Mail-Adresse.",
  104. };
  105. }
  106. export function useCreateUserDialog({ disabled = false, onCreated } = {}) {
  107. const [open, setOpen] = React.useState(false);
  108. const [isSubmitting, setIsSubmitting] = React.useState(false);
  109. const [form, setForm] = React.useState(() => cloneDefaultForm());
  110. const [error, setError] = React.useState(null);
  111. const policyLines = React.useMemo(() => getPasswordPolicyHintLinesDe(), []);
  112. const effectiveDisabled = Boolean(disabled || isSubmitting);
  113. const setPatch = React.useCallback((patch) => {
  114. setForm((prev) => ({ ...prev, ...(patch || {}) }));
  115. }, []);
  116. const resetForm = React.useCallback(() => {
  117. setForm(cloneDefaultForm());
  118. setError(null);
  119. }, []);
  120. const redirectToLoginExpired = React.useCallback(() => {
  121. const next =
  122. typeof window !== "undefined"
  123. ? `${window.location.pathname}${window.location.search}`
  124. : "/admin/users";
  125. window.location.replace(
  126. buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next }),
  127. );
  128. }, []);
  129. const handleOpenChange = React.useCallback(
  130. (nextOpen) => {
  131. setOpen(nextOpen);
  132. if (!nextOpen) resetForm();
  133. },
  134. [resetForm],
  135. );
  136. const handleSubmit = React.useCallback(
  137. async (e) => {
  138. e?.preventDefault?.();
  139. if (effectiveDisabled) return;
  140. setError(null);
  141. const clientErr = validateClient(form);
  142. if (clientErr) {
  143. setError(clientErr);
  144. notifyError({
  145. title: clientErr.title,
  146. description: clientErr.description,
  147. });
  148. return;
  149. }
  150. setIsSubmitting(true);
  151. try {
  152. const username = normalizeUsername(form.username);
  153. const email = normalizeEmail(form.email);
  154. const role = String(form.role || "").trim();
  155. const branchId =
  156. role === "branch" ? normalizeBranchIdDraft(form.branchId) : null;
  157. const initialPassword = String(form.initialPassword || "");
  158. await adminCreateUser({
  159. username,
  160. email,
  161. role,
  162. branchId,
  163. initialPassword,
  164. });
  165. notifySuccess({
  166. title: "Benutzer angelegt",
  167. description: `Benutzer "${username}" wurde erstellt.`,
  168. });
  169. setOpen(false);
  170. resetForm();
  171. if (typeof onCreated === "function") onCreated();
  172. } catch (err) {
  173. if (err instanceof ApiClientError) {
  174. if (err.code === "AUTH_UNAUTHENTICATED") {
  175. notifyApiError(err);
  176. redirectToLoginExpired();
  177. return;
  178. }
  179. if (err.code === "VALIDATION_WEAK_PASSWORD") {
  180. const reasons = err.details?.reasons;
  181. const minLength = err.details?.minLength;
  182. const hints = reasonsToHintLinesDe({ reasons, minLength });
  183. const description = buildWeakPasswordMessageDe({
  184. reasons,
  185. minLength,
  186. });
  187. setError({
  188. title: "Passwort ist zu schwach.",
  189. description,
  190. hints,
  191. });
  192. notifyError({ title: "Passwort ist zu schwach.", description });
  193. return;
  194. }
  195. // Precise duplicate feedback (username/email)
  196. if (err.code === "VALIDATION_INVALID_FIELD") {
  197. const fields = extractDuplicateFields(err.details);
  198. const mapped = mapDuplicateFieldsToGermanMessage(fields);
  199. if (mapped) {
  200. setError(mapped);
  201. notifyError(mapped);
  202. return;
  203. }
  204. }
  205. setError({
  206. title: "Benutzer konnte nicht angelegt werden.",
  207. description:
  208. "Bitte prüfen Sie die Eingaben und versuchen Sie es erneut.",
  209. });
  210. notifyApiError(err, {
  211. fallbackTitle: "Benutzer konnte nicht angelegt werden.",
  212. fallbackDescription:
  213. "Bitte prüfen Sie die Eingaben und versuchen Sie es erneut.",
  214. });
  215. return;
  216. }
  217. setError({
  218. title: "Benutzer konnte nicht angelegt werden.",
  219. description: "Bitte versuchen Sie es erneut.",
  220. });
  221. notifyError({
  222. title: "Benutzer konnte nicht angelegt werden.",
  223. description: "Bitte versuchen Sie es erneut.",
  224. });
  225. } finally {
  226. setIsSubmitting(false);
  227. }
  228. },
  229. [effectiveDisabled, form, onCreated, redirectToLoginExpired, resetForm],
  230. );
  231. return {
  232. open,
  233. setOpen,
  234. form,
  235. setPatch,
  236. error,
  237. policyLines,
  238. isSubmitting,
  239. effectiveDisabled,
  240. handleSubmit,
  241. handleOpenChange,
  242. };
  243. }