@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.
- package/dist/auth/authApi.js +2 -5
- package/dist/i18n/authTranslations.js +59 -3
- package/dist/pages/AccountPage.js +8 -13
- package/dist/utils/authService.js +11 -10
- package/package.json +1 -1
- package/src/auth/authApi.jsx +2 -5
- package/src/i18n/authTranslations.js +62 -4
- package/src/pages/AccountPage.jsx +8 -14
- package/src/utils/authService.js +23 -17
package/dist/auth/authApi.js
CHANGED
|
@@ -388,12 +388,9 @@ export async function updateUserRole(userId, newRole) {
|
|
|
388
388
|
*/
|
|
389
389
|
export async function updateUserSupportStatus(userId, isSupportAgent) {
|
|
390
390
|
try {
|
|
391
|
-
//
|
|
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
|
-
|
|
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.
|
|
959
|
+
"Common.YES": {
|
|
960
960
|
"de": "Ja",
|
|
961
961
|
"fr": "Oui",
|
|
962
962
|
"en": "Yes"
|
|
963
963
|
},
|
|
964
|
-
"Common.
|
|
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
|
-
//
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - Permissions:
|
|
21
|
-
* -
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
16
|
-
const
|
|
17
|
-
//
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
package/src/auth/authApi.jsx
CHANGED
|
@@ -402,12 +402,9 @@ export async function updateUserRole(userId, newRole) {
|
|
|
402
402
|
*/
|
|
403
403
|
export async function updateUserSupportStatus(userId, isSupportAgent) {
|
|
404
404
|
try {
|
|
405
|
-
//
|
|
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
|
-
|
|
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.
|
|
1006
|
+
"Common.YES": {
|
|
1007
1007
|
"de": "Ja",
|
|
1008
1008
|
"fr": "Oui",
|
|
1009
1009
|
"en": "Yes"
|
|
1010
1010
|
},
|
|
1011
|
-
"Common.
|
|
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
|
-
//
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* - Permissions:
|
|
29
|
-
* -
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
package/src/utils/authService.js
CHANGED
|
@@ -25,28 +25,34 @@ function resolveWebAuthnOptions(options) {
|
|
|
25
25
|
|
|
26
26
|
export async function registerPasskey(name = 'Passkey') {
|
|
27
27
|
ensureWebAuthnSupport();
|
|
28
|
-
|
|
29
|
-
// 1.
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
}
|