@micha.bigler/ui-core-micha 1.4.38 → 1.4.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -388,12 +388,9 @@ export async function updateUserRole(userId, newRole) {
388
388
  */
389
389
  export async function updateUserSupportStatus(userId, isSupportAgent) {
390
390
  try {
391
- // Wir gehen davon aus, dass dein Serializer "profile" nested akzeptiert
392
- // (siehe BaseUserSerializer.update Logik im Backend)
391
+ // Backend expects flat field names, DRF maps via `source="profile.is_support_agent"`
393
392
  const payload = {
394
- profile: {
395
- is_support_agent: isSupportAgent,
396
- },
393
+ is_support_agent: isSupportAgent,
397
394
  };
398
395
  const res = await apiClient.patch(`${USERS_BASE}/${userId}/`, payload);
399
396
  return res.data;
@@ -956,14 +956,70 @@ export const authTranslations = {
956
956
  "fr": "Expirée",
957
957
  "en": "Expired"
958
958
  },
959
- "Common.Yes": {
959
+ "Common.YES": {
960
960
  "de": "Ja",
961
961
  "fr": "Oui",
962
962
  "en": "Yes"
963
963
  },
964
- "Common.No": {
964
+ "Common.NO": {
965
965
  "de": "Nein",
966
966
  "fr": "Non",
967
967
  "en": "No"
968
- }
968
+ },
969
+ "Account.TITLE": {
970
+ "de": "Konto & Verwaltung",
971
+ "fr": "Compte et administration",
972
+ "en": "Account & Administration"
973
+ },
974
+ "Account.PAGE_TITLE": {
975
+ "de": "Konto",
976
+ "fr": "Compte",
977
+ "en": "Account"
978
+ },
979
+ "Account.TAB_PROFILE": {
980
+ "de": "Profil",
981
+ "fr": "Profil",
982
+ "en": "Profile"
983
+ },
984
+ "Account.TAB_SECURITY": {
985
+ "de": "Sicherheit",
986
+ "fr": "Sécurité",
987
+ "en": "Security"
988
+ },
989
+ "Account.TAB_USERS": {
990
+ "de": "Benutzer",
991
+ "fr": "Utilisateurs",
992
+ "en": "Users"
993
+ },
994
+ "Account.TAB_INVITE": {
995
+ "de": "Einladen",
996
+ "fr": "Inviter",
997
+ "en": "Invite"
998
+ },
999
+ "Account.TAB_ACCESS_CODES": {
1000
+ "de": "Zugangscodes",
1001
+ "fr": "Codes d'accès",
1002
+ "en": "Access codes"
1003
+ },
1004
+ "Account.TAB_SUPPORT": {
1005
+ "de": "Support",
1006
+ "fr": "Support",
1007
+ "en": "Support"
1008
+ },
1009
+ "Account.ACCESS_CODES_HINT": {
1010
+ "de": "Verwalten Sie Zugangscodes für die Selbstregistrierung.",
1011
+ "fr": "Gérer les codes d'accès pour l'auto-inscription.",
1012
+ "en": "Manage access codes for self-registration."
1013
+ },
1014
+ // --- Missing Auth Keys ---
1015
+ "Auth.NOT_LOGGED_IN": {
1016
+ "de": "Benutzer nicht angemeldet.",
1017
+ "fr": "Utilisateur non connecté.",
1018
+ "en": "User not logged in."
1019
+ },
1020
+ "Auth.INVITE_BUTTON": {
1021
+ "de": "Einladung senden",
1022
+ "fr": "Envoyer l'invitation",
1023
+ "en": "Send invitation"
1024
+ },
969
1025
  };
@@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet';
4
4
  import { useSearchParams } from 'react-router-dom';
5
5
  import { Tabs, Tab, Box, Typography, Alert, CircularProgress } from '@mui/material';
6
6
  import { useTranslation } from 'react-i18next';
7
- // Interne Komponenten der Library
7
+ // Internal Library Components
8
8
  import { WidePage } from '../layout/PageLayout';
9
9
  import { ProfileComponent } from '../components/ProfileComponent';
10
10
  import { SecurityComponent } from '../components/SecurityComponent';
@@ -15,11 +15,10 @@ import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequest
15
15
  import { AuthContext } from '../auth/AuthContext';
16
16
  import { updateUserProfile } from '../auth/authApi';
17
17
  /**
18
- * Vollständige, selbst-konfigurierende Account-Seite.
19
- * * Architektur:
20
- * - Permissions: Werden aus user.ui_permissions gelesen (vom Backend).
21
- * - Rollen: Werden aus user.available_roles gelesen (vom Backend).
22
- * - Keine Props mehr nötig -> Plug & Play in der App.js.
18
+ * Complete, self-configuring Account Page.
19
+ * Architecture:
20
+ * - Permissions: Read from user.ui_permissions (from Backend).
21
+ * - Roles: Read from user.available_roles (from Backend).
23
22
  */
24
23
  export function AccountPage() {
25
24
  const { t } = useTranslation();
@@ -29,10 +28,8 @@ export function AccountPage() {
29
28
  const currentTab = searchParams.get('tab') || 'profile';
30
29
  const fromRecovery = searchParams.get('from') === 'recovery';
31
30
  const fromWeakLogin = searchParams.get('from') === 'weak_login';
32
- // 2. Daten & Permissions extrahieren (Single Source of Truth)
33
- // Backend liefert die Liste der Rollen (Keys), z.B. ['student', 'teacher']
31
+ // 2. Data & Permissions (Single Source of Truth)
34
32
  const activeRoles = (user === null || user === void 0 ? void 0 : user.available_roles) || [];
35
- // Backend liefert Permissions basierend auf settings.py
36
33
  const perms = (user === null || user === void 0 ? void 0 : user.ui_permissions) || {};
37
34
  const handleTabChange = (_event, newValue) => {
38
35
  setSearchParams({ tab: newValue });
@@ -41,9 +38,8 @@ export function AccountPage() {
41
38
  const updatedUser = await updateUserProfile(payload);
42
39
  login(updatedUser);
43
40
  };
44
- // 3. Dynamische Tabs bauen
41
+ // 3. Dynamic Tabs
45
42
  const tabs = useMemo(() => {
46
- // Wenn User noch nicht da ist, leere Liste (Loading State fängt das ab)
47
43
  if (!user)
48
44
  return [];
49
45
  const list = [
@@ -71,8 +67,7 @@ export function AccountPage() {
71
67
  if (!user) {
72
68
  return (_jsx(WidePage, { children: _jsx(Alert, { severity: "warning", children: t('Auth.NOT_LOGGED_IN', 'User not logged in.') }) }));
73
69
  }
74
- // 5. Sicherheits-Check: Ist der aktuelle Tab überhaupt erlaubt?
75
- // Verhindert Zugriff durch URL-Manipulation (z.B. ?tab=users eingeben ohne Admin-Rechte)
70
+ // 5. Security Check: Is the active tab allowed?
76
71
  const activeTabExists = tabs.some(t => t.value === currentTab);
77
72
  const safeTab = activeTabExists ? currentTab : 'profile';
78
73
  return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserInviteComponent, {}) })), safeTab === 'access' && (_jsxs(Box, { sx: { mt: 2 }, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) }))] }));
@@ -12,24 +12,25 @@ function resolveWebAuthnOptions(options) {
12
12
  }
13
13
  export async function registerPasskey(name = 'Passkey') {
14
14
  ensureWebAuthnSupport();
15
- // 1. Get Options from Server
16
- const creationOptions = await getPasskeyRegistrationOptions();
17
- // 2. Call Browser API
15
+ // 1. Options vom Server
16
+ const optionsEnvelope = await getPasskeyRegistrationOptions();
17
+ // optionsEnvelope ist bei dir: { publicKey: { ... } } oder { creation_options: { publicKey: {...} } }
18
+ // Extrahiere die eigentlichen publicKey-Options
19
+ const publicKeyJson = (optionsEnvelope.creation_options && optionsEnvelope.creation_options.publicKey) ||
20
+ optionsEnvelope.publicKey ||
21
+ optionsEnvelope;
18
22
  let credential;
19
23
  try {
20
- // FIX: Erst entpacken (JSON -> JSON ohne publicKey Wrapper)
21
- const optionsJson = resolveWebAuthnOptions(creationOptions);
22
- // Dann parsen (JSON -> ArrayBuffer)
23
- const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(creationOptions);
24
- credential = await navigator.credentials.create({ publicKey: publicKeyOptions });
24
+ const publicKey = window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
25
+ // Wichtig: create({ publicKey: ... })
26
+ credential = await navigator.credentials.create({ publicKey });
25
27
  }
26
28
  catch (err) {
27
29
  if (err.name === 'NotAllowedError') {
28
30
  throw normaliseApiError(new Error('Auth.PASSKEY_CANCELLED'), 'Auth.PASSKEY_CANCELLED');
29
31
  }
30
- throw err;
32
+ throw normaliseApiError(err, 'Auth.PASSKEY_CREATE_FAILED');
31
33
  }
32
- // 3. Send back to Server
33
34
  const credentialJson = serializeCredential(credential);
34
35
  return completePasskeyRegistration(credentialJson, name);
35
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "1.4.38",
3
+ "version": "1.4.40",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -402,12 +402,9 @@ export async function updateUserRole(userId, newRole) {
402
402
  */
403
403
  export async function updateUserSupportStatus(userId, isSupportAgent) {
404
404
  try {
405
- // Wir gehen davon aus, dass dein Serializer "profile" nested akzeptiert
406
- // (siehe BaseUserSerializer.update Logik im Backend)
405
+ // Backend expects flat field names, DRF maps via `source="profile.is_support_agent"`
407
406
  const payload = {
408
- profile: {
409
- is_support_agent: isSupportAgent,
410
- },
407
+ is_support_agent: isSupportAgent,
411
408
  };
412
409
  const res = await apiClient.patch(`${USERS_BASE}/${userId}/`, payload);
413
410
  return res.data;
@@ -1003,14 +1003,72 @@ export const authTranslations = {
1003
1003
  "fr": "Expirée",
1004
1004
  "en": "Expired"
1005
1005
  },
1006
- "Common.Yes": {
1006
+ "Common.YES": {
1007
1007
  "de": "Ja",
1008
1008
  "fr": "Oui",
1009
1009
  "en": "Yes"
1010
1010
  },
1011
- "Common.No": {
1011
+ "Common.NO": {
1012
1012
  "de": "Nein",
1013
1013
  "fr": "Non",
1014
1014
  "en": "No"
1015
- }
1016
- };
1015
+ },
1016
+ "Account.TITLE": {
1017
+ "de": "Konto & Verwaltung",
1018
+ "fr": "Compte et administration",
1019
+ "en": "Account & Administration"
1020
+ },
1021
+ "Account.PAGE_TITLE": {
1022
+ "de": "Konto",
1023
+ "fr": "Compte",
1024
+ "en": "Account"
1025
+ },
1026
+ "Account.TAB_PROFILE": {
1027
+ "de": "Profil",
1028
+ "fr": "Profil",
1029
+ "en": "Profile"
1030
+ },
1031
+ "Account.TAB_SECURITY": {
1032
+ "de": "Sicherheit",
1033
+ "fr": "Sécurité",
1034
+ "en": "Security"
1035
+ },
1036
+ "Account.TAB_USERS": {
1037
+ "de": "Benutzer",
1038
+ "fr": "Utilisateurs",
1039
+ "en": "Users"
1040
+ },
1041
+ "Account.TAB_INVITE": {
1042
+ "de": "Einladen",
1043
+ "fr": "Inviter",
1044
+ "en": "Invite"
1045
+ },
1046
+ "Account.TAB_ACCESS_CODES": {
1047
+ "de": "Zugangscodes",
1048
+ "fr": "Codes d'accès",
1049
+ "en": "Access codes"
1050
+ },
1051
+ "Account.TAB_SUPPORT": {
1052
+ "de": "Support",
1053
+ "fr": "Support",
1054
+ "en": "Support"
1055
+ },
1056
+ "Account.ACCESS_CODES_HINT": {
1057
+ "de": "Verwalten Sie Zugangscodes für die Selbstregistrierung.",
1058
+ "fr": "Gérer les codes d'accès pour l'auto-inscription.",
1059
+ "en": "Manage access codes for self-registration."
1060
+ },
1061
+
1062
+ // --- Missing Auth Keys ---
1063
+ "Auth.NOT_LOGGED_IN": {
1064
+ "de": "Benutzer nicht angemeldet.",
1065
+ "fr": "Utilisateur non connecté.",
1066
+ "en": "User not logged in."
1067
+ },
1068
+ "Auth.INVITE_BUTTON": {
1069
+ "de": "Einladung senden",
1070
+ "fr": "Envoyer l'invitation",
1071
+ "en": "Send invitation"
1072
+ },
1073
+
1074
+ }
@@ -11,7 +11,7 @@ import {
11
11
  } from '@mui/material';
12
12
  import { useTranslation } from 'react-i18next';
13
13
 
14
- // Interne Komponenten der Library
14
+ // Internal Library Components
15
15
  import { WidePage } from '../layout/PageLayout';
16
16
  import { ProfileComponent } from '../components/ProfileComponent';
17
17
  import { SecurityComponent } from '../components/SecurityComponent';
@@ -23,11 +23,10 @@ import { AuthContext } from '../auth/AuthContext';
23
23
  import { updateUserProfile } from '../auth/authApi';
24
24
 
25
25
  /**
26
- * Vollständige, selbst-konfigurierende Account-Seite.
27
- * * Architektur:
28
- * - Permissions: Werden aus user.ui_permissions gelesen (vom Backend).
29
- * - Rollen: Werden aus user.available_roles gelesen (vom Backend).
30
- * - Keine Props mehr nötig -> Plug & Play in der App.js.
26
+ * Complete, self-configuring Account Page.
27
+ * Architecture:
28
+ * - Permissions: Read from user.ui_permissions (from Backend).
29
+ * - Roles: Read from user.available_roles (from Backend).
31
30
  */
32
31
  export function AccountPage() {
33
32
  const { t } = useTranslation();
@@ -39,11 +38,8 @@ export function AccountPage() {
39
38
  const fromRecovery = searchParams.get('from') === 'recovery';
40
39
  const fromWeakLogin = searchParams.get('from') === 'weak_login';
41
40
 
42
- // 2. Daten & Permissions extrahieren (Single Source of Truth)
43
- // Backend liefert die Liste der Rollen (Keys), z.B. ['student', 'teacher']
41
+ // 2. Data & Permissions (Single Source of Truth)
44
42
  const activeRoles = user?.available_roles || [];
45
-
46
- // Backend liefert Permissions basierend auf settings.py
47
43
  const perms = user?.ui_permissions || {};
48
44
 
49
45
  const handleTabChange = (_event, newValue) => {
@@ -55,9 +51,8 @@ export function AccountPage() {
55
51
  login(updatedUser);
56
52
  };
57
53
 
58
- // 3. Dynamische Tabs bauen
54
+ // 3. Dynamic Tabs
59
55
  const tabs = useMemo(() => {
60
- // Wenn User noch nicht da ist, leere Liste (Loading State fängt das ab)
61
56
  if (!user) return [];
62
57
 
63
58
  const list = [
@@ -103,8 +98,7 @@ export function AccountPage() {
103
98
  );
104
99
  }
105
100
 
106
- // 5. Sicherheits-Check: Ist der aktuelle Tab überhaupt erlaubt?
107
- // Verhindert Zugriff durch URL-Manipulation (z.B. ?tab=users eingeben ohne Admin-Rechte)
101
+ // 5. Security Check: Is the active tab allowed?
108
102
  const activeTabExists = tabs.some(t => t.value === currentTab);
109
103
  const safeTab = activeTabExists ? currentTab : 'profile';
110
104
 
@@ -25,28 +25,34 @@ function resolveWebAuthnOptions(options) {
25
25
 
26
26
  export async function registerPasskey(name = 'Passkey') {
27
27
  ensureWebAuthnSupport();
28
-
29
- // 1. Get Options from Server
30
- const creationOptions = await getPasskeyRegistrationOptions();
31
-
32
- // 2. Call Browser API
28
+
29
+ // 1. Options vom Server
30
+ const optionsEnvelope = await getPasskeyRegistrationOptions();
31
+ // optionsEnvelope ist bei dir: { publicKey: { ... } } oder { creation_options: { publicKey: {...} } }
32
+
33
+ // Extrahiere die eigentlichen publicKey-Options
34
+ const publicKeyJson =
35
+ (optionsEnvelope.creation_options && optionsEnvelope.creation_options.publicKey) ||
36
+ optionsEnvelope.publicKey ||
37
+ optionsEnvelope;
38
+
33
39
  let credential;
34
40
  try {
35
- // FIX: Erst entpacken (JSON -> JSON ohne publicKey Wrapper)
36
- const optionsJson = resolveWebAuthnOptions(creationOptions);
37
-
38
- // Dann parsen (JSON -> ArrayBuffer)
39
- const publicKeyOptions =
40
- window.PublicKeyCredential.parseCreationOptionsFromJSON(creationOptions);
41
- credential = await navigator.credentials.create({ publicKey: publicKeyOptions });
41
+ const publicKey = window.PublicKeyCredential.parseCreationOptionsFromJSON(
42
+ publicKeyJson,
43
+ );
44
+ // Wichtig: create({ publicKey: ... })
45
+ credential = await navigator.credentials.create({ publicKey });
42
46
  } catch (err) {
43
- if (err.name === 'NotAllowedError') {
44
- throw normaliseApiError(new Error('Auth.PASSKEY_CANCELLED'), 'Auth.PASSKEY_CANCELLED');
45
- }
46
- throw err;
47
+ if (err.name === 'NotAllowedError') {
48
+ throw normaliseApiError(
49
+ new Error('Auth.PASSKEY_CANCELLED'),
50
+ 'Auth.PASSKEY_CANCELLED',
51
+ );
52
+ }
53
+ throw normaliseApiError(err, 'Auth.PASSKEY_CREATE_FAILED');
47
54
  }
48
55
 
49
- // 3. Send back to Server
50
56
  const credentialJson = serializeCredential(credential);
51
57
  return completePasskeyRegistration(credentialJson, name);
52
58
  }