From da814ad6ad5c2cdad3c3bd61fbcc6a572f29ac5f Mon Sep 17 00:00:00 2001 From: bh0fer Date: Mon, 6 Oct 2025 10:05:01 +0200 Subject: [PATCH 01/29] poc of better-auth --- package.json | 3 +- src/api/OfflineApi/index.ts | 2 - src/api/base.ts | 102 +----- src/auth-client.ts | 18 + src/authConfig.ts | 95 ------ src/components/HomepageFeatures/index.tsx | 11 +- .../Navbar/LoginProfileButton/index.tsx | 6 +- src/pages/login.tsx | 20 +- src/pages/user/index.tsx | 53 +-- src/stores/AuthStore.ts | 63 ++++ src/stores/SessionStore.ts | 112 +------ src/stores/SocketDataStore.ts | 94 ++---- src/stores/rootStore.ts | 10 +- src/theme/Root.tsx | 217 +++--------- yarn.lock | 316 +++++++++++++++++- 15 files changed, 503 insertions(+), 619 deletions(-) create mode 100644 src/auth-client.ts delete mode 100644 src/authConfig.ts create mode 100644 src/stores/AuthStore.ts diff --git a/package.json b/package.json index 26afa9f1f..2a0a06301 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,6 @@ "updateTdev": "ts-node updateSync/updateTdev.ts" }, "dependencies": { - "@azure/msal-browser": "^v3.28.0", - "@azure/msal-react": "^2.2.0", "@docusaurus/core": "^3.8.1", "@docusaurus/faster": "^3.8.1", "@docusaurus/preset-classic": "^3.8.1", @@ -49,6 +47,7 @@ "@sentry/webpack-plugin": "^3.3.1", "ace-builds": "^1.41.0", "axios": "^1.9.0", + "better-auth": "^1.3.26", "browser-image-compression": "^2.0.2", "clsx": "^2.1.1", "docusaurus-plugin-sass": "^0.2.6", diff --git a/src/api/OfflineApi/index.ts b/src/api/OfflineApi/index.ts index 5944f00a3..acfdf806f 100644 --- a/src/api/OfflineApi/index.ts +++ b/src/api/OfflineApi/index.ts @@ -236,8 +236,6 @@ export default class OfflineApi { return resolveResponse([] as unknown as T); case 'allowedActions': return resolveResponse([] as unknown as T); - case 'checklogin': - return resolveResponse({ user: OfflineUser } as unknown as T, 200, 'ok'); case 'documents': if (id) { const document = await this.dbAdapter.get>(DOCUMENTS_STORE, id); diff --git a/src/api/base.ts b/src/api/base.ts index 355268682..de2555a09 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -1,11 +1,8 @@ -import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; -import { BACKEND_URL, apiConfig } from '../authConfig'; -import { InteractionRequiredAuthError } from '@azure/msal-browser'; -import { msalInstance } from '../theme/Root'; +import axios, { AxiosInstance } from 'axios'; import siteConfig from '@generated/docusaurus.config'; import OfflineApi from './OfflineApi'; -const { NO_AUTH, OFFLINE_API } = siteConfig.customFields as { - NO_AUTH?: boolean; +const { BACKEND_URL, OFFLINE_API } = siteConfig.customFields as { + BACKEND_URL: string; OFFLINE_API?: boolean | 'memory' | 'indexedDB'; }; @@ -25,97 +22,4 @@ const api: AxiosInstance & { mode?: 'indexedDB' | 'memory'; destroyDb?: () => Pr headers: {} }); -export const setupDefaultAxios = () => { - /** clear all current interceptors and set them up... */ - api.interceptors.request.clear(); - api.interceptors.request.use( - async (config: InternalAxiosRequestConfig) => { - if (config.headers['Authorization']) { - delete config.headers['Authorization']; - } - return config; - }, - (error) => { - Promise.reject(error); - } - ); -}; - -export const setupMsalAxios = () => { - /** clear all current interceptors and set them up... */ - api.interceptors.request.clear(); - api.interceptors.request.use( - async (config: InternalAxiosRequestConfig) => { - if (process.env.NODE_ENV !== 'production' && NO_AUTH) { - return config; - } - // This will only return a non-null value if you have logic somewhere else that calls the setActiveAccount API - // --> @see src/theme/Root.tsx - const activeAccount = msalInstance.getActiveAccount(); - if (activeAccount) { - try { - const accessTokenResponse = await msalInstance.acquireTokenSilent({ - scopes: apiConfig.scopes, - account: activeAccount - }); - const accessToken = accessTokenResponse.accessToken; - if (config.headers && accessToken) { - config.headers['Authorization'] = 'Bearer ' + accessToken; - } - } catch (e) { - delete config.headers['Authorization']; - if (e instanceof InteractionRequiredAuthError) { - // If there are no cached tokens, or the cached tokens are expired, then the user will need to interact - // with the page to get a new token. - console.log('User interaction required to get a new token.', e); - // hacky way to get the user to log in again - only happens on firefox when - // the default "no 3dparty cookies" setting is active - const msalKeys = Object.keys(localStorage).filter((k) => k.startsWith('msal.')); - msalKeys.forEach((k) => localStorage.removeItem(k)); - // proceed with the login - await msalInstance.acquireTokenRedirect({ - scopes: apiConfig.scopes, - account: activeAccount - }); - } - } - } else { - /* - * User is not signed in. Throw error or wait for user to login. - * Do not attempt to log a user in outside of the context of MsalProvider - */ - if (config.headers['Authorization']) { - delete config.headers['Authorization']; - } - } - return config; - }, - (error) => { - Promise.reject(error); - } - ); -}; - -export const setupNoAuthAxios = (userEmail: string) => { - if (process.env.NODE_ENV === 'production') { - return; - } - /** clear all current interceptors and set them up... */ - api.interceptors.request.clear(); - api.interceptors.request.use( - async (config: InternalAxiosRequestConfig) => { - config.headers['Authorization'] = JSON.stringify({ - email: userEmail - }); - return config; - }, - (error) => { - Promise.reject(error); - } - ); -}; -export const checkLogin = (signal: AbortSignal) => { - return api.get('/checklogin', { signal }); -}; - export default api; diff --git a/src/auth-client.ts b/src/auth-client.ts new file mode 100644 index 000000000..2ba780995 --- /dev/null +++ b/src/auth-client.ts @@ -0,0 +1,18 @@ +import { createAuthClient } from 'better-auth/react'; +import { oneTimeTokenClient } from 'better-auth/client/plugins'; +import siteConfig from '@generated/docusaurus.config'; +interface AuthFields { + APP_URL: string; + BACKEND_URL: string; + CLIENT_ID: string; + TENANT_ID: string; + API_URI: string; +} +export const { BACKEND_URL, CLIENT_ID, APP_URL, TENANT_ID, API_URI } = + siteConfig.customFields as any as AuthFields; + +export const authClient = createAuthClient({ + /** The base URL of the server (optional if you're using the same domain) */ + baseURL: BACKEND_URL, + plugins: [oneTimeTokenClient()] +}); diff --git a/src/authConfig.ts b/src/authConfig.ts deleted file mode 100644 index 03e03b58b..000000000 --- a/src/authConfig.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Configuration, LogLevel, type RedirectRequest } from '@azure/msal-browser'; -import siteConfig from '@generated/docusaurus.config'; - -export interface CustomFields { - APP_URL: string; - BACKEND_URL: string; - CLIENT_ID: string; - TENANT_ID: string; - API_URI: string; -} - -/** The Domain Name of this app */ -export const { BACKEND_URL, CLIENT_ID, APP_URL, TENANT_ID, API_URI } = - siteConfig.customFields as any as CustomFields; - -/** - * Configuration object to be passed to MSAL instance on creation. - * For a full list of MSAL.js configuration parameters, visit: - * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md - */ -//cloudDiscoveryMetadata: '{"tenant_discovery_endpoint":"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration","api-version":"1.1", -//"metadata":[{"preferred_network":"login.microsoftonline.com","preferred_cache":"login.windows.net","aliases":["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"]},{"preferred_network":"login.partner.microsoftonline.cn","preferred_cache":"login.partner.microsoftonline.cn","aliases":["login.partner.microsoftonline.cn","login.chinacloudapi.cn"]},{"preferred_network":"login.microsoftonline.de","preferred_cache":"login.microsoftonline.de","aliases":["login.microsoftonline.de"]},{"preferred_network":"login.microsoftonline.us","preferred_cache":"login.microsoftonline.us","aliases":["login.microsoftonline.us","login.usgovcloudapi.net"]},{"preferred_network":"login-us.microsoftonline.com","preferred_cache":"login-us.microsoftonline.com","aliases":["login-us.microsoftonline.com"]}]}' - -export const msalConfig: Configuration = { - auth: { - clientId: CLIENT_ID || 'nope', - authority: `https://login.microsoftonline.com/${TENANT_ID}`, - redirectUri: `${APP_URL}/user`, - postLogoutRedirectUri: APP_URL - }, - cache: { - cacheLocation: 'localStorage', // This configures where your cache will be stored - storeAuthStateInCookie: false // Set this to "true" if you are having issues on IE11 or Edge - }, - system: { - loggerOptions: { - loggerCallback: (level, message, containsPii) => { - if (containsPii) { - return; - } - switch (level) { - case LogLevel.Error: - console.error(message); - return; - case LogLevel.Info: - if (process.env.NODE_ENV !== 'debug') { - return; - } - console.info(message); - return; - case LogLevel.Verbose: - if (process.env.NODE_ENV !== 'debug') { - return; - } - console.debug(message); - return; - case LogLevel.Warning: - console.warn(message); - return; - } - } - } - } -}; - -export const scopes = [`${API_URI}/access_as_user`]; - -// Add here the endpoints and scopes for the web API you would like to use. -export const apiConfig = { - uri: API_URI, - scopes: scopes -}; -/** - * Scopes you add here will be prompted for user consent during sign-in. - * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. - * For more information about OIDC scopes, visit: - * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes - */ -export const loginRequest = { - scopes: scopes -}; - -/** - * Scopes you add here will be used to request a token from Azure AD to be used for accessing a protected resource. - * To learn more about how to work with scopes and resources, see: - * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md - */ -export const tokenRequest: RedirectRequest = { - scopes: [...apiConfig.scopes] -}; diff --git a/src/components/HomepageFeatures/index.tsx b/src/components/HomepageFeatures/index.tsx index 035b3024f..c9803c2c2 100644 --- a/src/components/HomepageFeatures/index.tsx +++ b/src/components/HomepageFeatures/index.tsx @@ -3,12 +3,17 @@ import styles from './styles.module.scss'; import { useStore } from '@tdev-hooks/useStore'; import { observer } from 'mobx-react-lite'; import DefinitionList from '@tdev-components/DefinitionList'; -import { BACKEND_URL } from '@tdev/authConfig'; import Icon from '@mdi/react'; import { mdiCheckCircle, mdiCloseCircle, mdiConnection } from '@mdi/js'; import Button from '@tdev-components/shared/Button'; import { useIsLive } from '@tdev-hooks/useIsLive'; import Card from '@tdev-components/shared/Card'; +import siteConfig from '@generated/docusaurus.config'; +const { BACKEND_URL, NO_AUTH, OFFLINE_API } = siteConfig.customFields as { + BACKEND_URL: string; + NO_AUTH?: boolean; + OFFLINE_API?: boolean | 'memory' | 'indexedDB'; +}; const HomepageFeatures = observer(() => { const socketStore = useStore('socketStore'); @@ -45,6 +50,10 @@ const HomepageFeatures = observer(() => { )} +
Offline API
+
{OFFLINE_API || '-'}
+
No Auth
+
{NO_AUTH ? 'Ja' : 'Nein'}
Connection
- {NO_AUTH && ( - <> -
Test-User wechseln
-
-
- user.email)} - onChange={(username) => setTestUser(username)} - value={(sessionStore.account as any)?.username} - disabled={userStore.users.length <= 1} - placeholder={ - TEST_USER || 'DEFAULT_TEST_USER nicht definiert in .env' - } - /> - {(sessionStore.account as any)?.username !== - TEST_USER?.toLowerCase() && ( -
-
- - )}
Ausloggen
+ + + + ); +} diff --git a/src/pages/signIn/styles.module.scss b/src/pages/signIn/styles.module.scss new file mode 100644 index 000000000..2893b105e --- /dev/null +++ b/src/pages/signIn/styles.module.scss @@ -0,0 +1,8 @@ +.form { + display: flex; + gap: 10px; + flex-wrap: wrap; + > input { + flex-basis: 20em; + } +} diff --git a/src/pages/signUp/index.tsx b/src/pages/signUp/index.tsx new file mode 100644 index 000000000..9a9ddbe48 --- /dev/null +++ b/src/pages/signUp/index.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Layout from '@theme/Layout'; +import Heading from '@theme/Heading'; + +import styles from './index.module.css'; +import { useStore } from '../../hooks/useStore'; +import { authClient } from '@site/src/auth-client'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import { Redirect } from '@docusaurus/router'; + +export default function SignUp(): React.ReactNode { + const { siteConfig } = useDocusaurusContext(); + const [email, setEmail] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [firstName, setFirstName] = React.useState(''); + const [lastName, setLastName] = React.useState(''); + const authStore = useStore('authStore'); + const { data: session } = authClient.useSession(); + const userPage = useBaseUrl('/user'); + + if (session?.user) { + return ; + } + + return ( + + setFirstName(e.target.value)} + /> + setLastName(e.target.value)} + /> + setEmail(e.target.value)} + /> + ) => setPassword(e.target.value)} + /> + {email && password && firstName && lastName && ( + + )} + + ); +} diff --git a/src/stores/AuthStore.ts b/src/stores/AuthStore.ts index 95c21ead4..485c6eb1d 100644 --- a/src/stores/AuthStore.ts +++ b/src/stores/AuthStore.ts @@ -10,12 +10,14 @@ export class AuthStore { } @action - signUp(email: string, password: string, name: string) { + signUp(email: string, password: string, firstName: string, lastName: string) { return authClient.signUp.email( { - email, // user email address - password, // user password -> min 8 characters by default - name // user display name + email, + password, + firstName, + lastName, + name: `${firstName} ${lastName}` }, { onRequest: (ctx) => { From 7fea5aaa9491c3a2056e5ee91a7a3a6b494be378 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sun, 12 Oct 2025 18:03:29 +0200 Subject: [PATCH 13/29] setup create user --- src/auth-client.ts | 3 + src/components/Admin/AdminPanel/index.tsx | 4 ++ src/components/Admin/CreateUser/index.tsx | 53 ++++++++++++++ .../Admin/CreateUser/styles.module.scss | 3 + src/components/Admin/UserTable/User.tsx | 3 + src/components/Admin/UserTable/index.tsx | 1 + src/pages/login.module.scss | 5 +- src/pages/login.tsx | 5 +- src/pages/signIn/index.tsx | 22 +++--- src/pages/signUp/index.tsx | 69 ------------------- src/stores/AuthStore.ts | 29 +++----- 11 files changed, 89 insertions(+), 108 deletions(-) create mode 100644 src/components/Admin/CreateUser/index.tsx create mode 100644 src/components/Admin/CreateUser/styles.module.scss delete mode 100644 src/pages/signUp/index.tsx diff --git a/src/auth-client.ts b/src/auth-client.ts index 462c28984..d6259d513 100644 --- a/src/auth-client.ts +++ b/src/auth-client.ts @@ -1,5 +1,7 @@ import { inferAdditionalFields } from 'better-auth/client/plugins'; import { createAuthClient } from 'better-auth/react'; + +import { adminClient } from 'better-auth/client/plugins'; import { oneTimeTokenClient } from 'better-auth/client/plugins'; import siteConfig from '@generated/docusaurus.config'; interface AuthFields { @@ -11,6 +13,7 @@ export const authClient = createAuthClient({ /** The base URL of the server (optional if you're using the same domain) */ baseURL: BACKEND_URL, plugins: [ + adminClient(), oneTimeTokenClient(), inferAdditionalFields({ user: { diff --git a/src/components/Admin/AdminPanel/index.tsx b/src/components/Admin/AdminPanel/index.tsx index 0c6de1d84..af348c076 100644 --- a/src/components/Admin/AdminPanel/index.tsx +++ b/src/components/Admin/AdminPanel/index.tsx @@ -8,6 +8,7 @@ import TabItem from '@theme/TabItem'; import StudentGroupPanel from '@tdev-components/Admin/StudentGroupPanel'; import UserTable from '@tdev-components/Admin/UserTable'; import AllowedActions from '../AllowedActions'; +import CreateUser from '../CreateUser'; const AdminPanel = observer(() => { const userStore = useStore('userStore'); @@ -33,6 +34,9 @@ const AdminPanel = observer(() => { + + + ); diff --git a/src/components/Admin/CreateUser/index.tsx b/src/components/Admin/CreateUser/index.tsx new file mode 100644 index 000000000..7a0d48179 --- /dev/null +++ b/src/components/Admin/CreateUser/index.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import Button from '@tdev-components/shared/Button'; +import TextInput from '@tdev-components/shared/TextInput'; +import Card from '@tdev-components/shared/Card'; + +interface Props {} + +const CreateUser = observer((props: Props) => { + const authStore = useStore('authStore'); + const [email, setEmail] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [firstName, setFirstName] = React.useState(''); + const [lastName, setLastName] = React.useState(''); + + return ( + + + + } + classNames={{ + card: clsx(styles.card) + }} + > + setFirstName(val)} /> + setLastName(val)} /> + setEmail(val)} /> + setPassword(val)} + /> + + ); +}); + +export default CreateUser; diff --git a/src/components/Admin/CreateUser/styles.module.scss b/src/components/Admin/CreateUser/styles.module.scss new file mode 100644 index 000000000..1a5613e84 --- /dev/null +++ b/src/components/Admin/CreateUser/styles.module.scss @@ -0,0 +1,3 @@ +.card { + max-width: 20em; +} diff --git a/src/components/Admin/UserTable/User.tsx b/src/components/Admin/UserTable/User.tsx index bcd6742fc..fca824ea2 100644 --- a/src/components/Admin/UserTable/User.tsx +++ b/src/components/Admin/UserTable/User.tsx @@ -9,6 +9,9 @@ import { formatDateTime } from '@tdev-models/helpers/date'; import { Role, RoleAccessLevel, RoleNames } from '@tdev-api/user'; import { useStore } from '@tdev-hooks/useStore'; import LiveStatusIndicator from '@tdev-components/LiveStatusIndicator'; +import Icon from '@mdi/react'; +import { mdiLink } from '@mdi/js'; +import { SIZE_S } from '@tdev-components/shared/iconSizes'; interface Props { user: UserModel; diff --git a/src/components/Admin/UserTable/index.tsx b/src/components/Admin/UserTable/index.tsx index dcd5d8dc4..2e5bf4e69 100644 --- a/src/components/Admin/UserTable/index.tsx +++ b/src/components/Admin/UserTable/index.tsx @@ -16,6 +16,7 @@ type SortColumn = | 'accessLevel' | 'firstName' | 'lastName' + | 'linkedAccounts' | 'createdAt' | 'updatedAt' | 'groups' diff --git a/src/pages/login.module.scss b/src/pages/login.module.scss index 4bd340a3b..7cece7081 100644 --- a/src/pages/login.module.scss +++ b/src/pages/login.module.scss @@ -1,8 +1,7 @@ .loginPage { display: flex; - align-items: center; - justify-content: space-between; - flex-direction: column; + justify-content: center; + gap: 1em; margin: 3em; } .heroBanner { diff --git a/src/pages/login.tsx b/src/pages/login.tsx index dccc8bb1d..aacb87820 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -10,7 +10,8 @@ import siteConfig from '@generated/docusaurus.config'; import Translate from '@docusaurus/Translate'; import { authClient } from '@tdev/auth-client'; import Button from '@tdev-components/shared/Button'; -import { mdiGithub, mdiMicrosoft } from '@mdi/js'; +import { mdiEmail, mdiGithub, mdiMicrosoft } from '@mdi/js'; +import useBaseUrl from '@docusaurus/useBaseUrl'; const { NO_AUTH, APP_URL } = siteConfig.customFields as { NO_AUTH?: boolean; APP_URL?: string }; function HomepageHeader() { @@ -27,6 +28,7 @@ function HomepageHeader() { const LoginPage = observer(() => { const { data: session } = authClient.useSession(); + const signInPage = useBaseUrl('/signIn'); if (session?.user || NO_AUTH) { return ; } @@ -59,6 +61,7 @@ const LoginPage = observer(() => { iconSide="left" color="black" /> + diff --git a/src/pages/signUp/index.tsx b/src/pages/signUp/index.tsx deleted file mode 100644 index 9a9ddbe48..000000000 --- a/src/pages/signUp/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import clsx from 'clsx'; -import Link from '@docusaurus/Link'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import Layout from '@theme/Layout'; -import Heading from '@theme/Heading'; - -import styles from './index.module.css'; -import { useStore } from '../../hooks/useStore'; -import { authClient } from '@site/src/auth-client'; -import useBaseUrl from '@docusaurus/useBaseUrl'; -import { Redirect } from '@docusaurus/router'; - -export default function SignUp(): React.ReactNode { - const { siteConfig } = useDocusaurusContext(); - const [email, setEmail] = React.useState(''); - const [password, setPassword] = React.useState(''); - const [firstName, setFirstName] = React.useState(''); - const [lastName, setLastName] = React.useState(''); - const authStore = useStore('authStore'); - const { data: session } = authClient.useSession(); - const userPage = useBaseUrl('/user'); - - if (session?.user) { - return ; - } - - return ( - - setFirstName(e.target.value)} - /> - setLastName(e.target.value)} - /> - setEmail(e.target.value)} - /> - ) => setPassword(e.target.value)} - /> - {email && password && firstName && lastName && ( - - )} - - ); -} diff --git a/src/stores/AuthStore.ts b/src/stores/AuthStore.ts index 485c6eb1d..7a6288c51 100644 --- a/src/stores/AuthStore.ts +++ b/src/stores/AuthStore.ts @@ -10,29 +10,16 @@ export class AuthStore { } @action - signUp(email: string, password: string, firstName: string, lastName: string) { - return authClient.signUp.email( - { - email, - password, + createUser(email: string, password: string, firstName: string, lastName: string) { + return authClient.admin.createUser({ + email, + password, + name: `${firstName} ${lastName}`, + data: { firstName, - lastName, - name: `${firstName} ${lastName}` - }, - { - onRequest: (ctx) => { - console.log('sign up request started', ctx); - }, - onSuccess: (ctx) => { - console.log('sign up successful', ctx); - //redirect to the dashboard or sign in page - }, - onError: (ctx) => { - // display the error message - console.log('sign up failed', ctx.error.message); - } + lastName } - ); + }); } @action From 6114f3bae0631c0bd3a7a9de511ccc449947e3f6 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Mon, 13 Oct 2025 10:33:56 +0200 Subject: [PATCH 14/29] add basic error handling --- src/components/Admin/AdminPanel/index.tsx | 6 +- src/components/Admin/CreateUser/index.tsx | 75 +++++++++++++++++-- .../Admin/CreateUser/styles.module.scss | 4 +- src/components/shared/TextInput/index.tsx | 20 ++++- .../shared/TextInput/styles.module.scss | 10 ++- 5 files changed, 101 insertions(+), 14 deletions(-) diff --git a/src/components/Admin/AdminPanel/index.tsx b/src/components/Admin/AdminPanel/index.tsx index af348c076..3e2cef3d8 100644 --- a/src/components/Admin/AdminPanel/index.tsx +++ b/src/components/Admin/AdminPanel/index.tsx @@ -31,12 +31,12 @@ const AdminPanel = observer(() => { + + + - - - ); diff --git a/src/components/Admin/CreateUser/index.tsx b/src/components/Admin/CreateUser/index.tsx index 7a0d48179..506a825e0 100644 --- a/src/components/Admin/CreateUser/index.tsx +++ b/src/components/Admin/CreateUser/index.tsx @@ -6,6 +6,8 @@ import { useStore } from '@tdev-hooks/useStore'; import Button from '@tdev-components/shared/Button'; import TextInput from '@tdev-components/shared/TextInput'; import Card from '@tdev-components/shared/Card'; +import { mdiAccountPlus, mdiCheck } from '@mdi/js'; +import Alert from '@tdev-components/shared/Alert'; interface Props {} @@ -15,36 +17,93 @@ const CreateUser = observer((props: Props) => { const [password, setPassword] = React.useState(''); const [firstName, setFirstName] = React.useState(''); const [lastName, setLastName] = React.useState(''); + const [created, setCreated] = React.useState(false); + const [error, setError] = React.useState(''); return ( Neue Benutzer:in erfassen} footer={ <> + {created && ( + + )} + {error && {error}} } classNames={{ card: clsx(styles.card) }} > - setFirstName(val)} /> - setLastName(val)} /> - setEmail(val)} /> + setFirstName(val)} + readOnly={created} + /> + setLastName(val)} + readOnly={created} + /> + setEmail(val)} + readOnly={created} + /> setPassword(val)} + readOnly={created} + validator={(t) => { + if (t.length < 8) { + return 'Mindestens 8 Zeichen'; + } + return null; + }} /> ); diff --git a/src/components/Admin/CreateUser/styles.module.scss b/src/components/Admin/CreateUser/styles.module.scss index 1a5613e84..843b18133 100644 --- a/src/components/Admin/CreateUser/styles.module.scss +++ b/src/components/Admin/CreateUser/styles.module.scss @@ -1,3 +1,5 @@ .card { - max-width: 20em; + max-width: 30em; + margin-left: auto; + margin-right: auto; } diff --git a/src/components/shared/TextInput/index.tsx b/src/components/shared/TextInput/index.tsx index af77db03f..bb8cd7f64 100644 --- a/src/components/shared/TextInput/index.tsx +++ b/src/components/shared/TextInput/index.tsx @@ -10,6 +10,7 @@ interface Props { onChange: (text: string) => void; onEnter?: () => void; onEscape?: () => void; + validator?: (text: string) => string | null; className?: string; labelClassName?: string; value?: string; @@ -29,10 +30,14 @@ const TextInput = observer((props: Props) => { const _id = React.useId(); const id = props.id || _id; const [text, setText] = React.useState(props.defaultValue || ''); + const validator = React.useCallback(props.validator ?? ((text: string) => null), [props.validator]); return ( <> {props.label && ( -
)} {sessionStore.apiMode !== 'api' && ( diff --git a/src/stores/SocketDataStore.ts b/src/stores/SocketDataStore.ts index a472bea34..b658f2518 100644 --- a/src/stores/SocketDataStore.ts +++ b/src/stores/SocketDataStore.ts @@ -128,7 +128,11 @@ export class SocketDataStore extends iStore<'ping'> { @action _connect(socket: TypedSocket) { this._socketConfig(socket); + const winSock: { tdevSockets?: Socket[] } = window as any; + winSock.tdevSockets = (winSock.tdevSockets || []).filter((s: Socket) => s.connected || s.active); + winSock.tdevSockets.forEach((s: Socket) => s.disconnect()); socket.connect(); + winSock.tdevSockets.push(socket); } _socketConfig(socket: TypedSocket) { @@ -349,6 +353,11 @@ export class SocketDataStore extends iStore<'ping'> { }); } + @action + requestReload(roomIds: string[], userIds: string[]) { + return this.requestNavigation(roomIds || [], userIds || [], { type: 'nav-reload' }); + } + @action cleanup() { this.disconnect(); diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx index 0879c5426..3380817df 100644 --- a/src/theme/Root.tsx +++ b/src/theme/Root.tsx @@ -9,9 +9,7 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { useHistory } from '@docusaurus/router'; import LoggedOutOverlay from '@tdev-components/LoggedOutOverlay'; import { authClient } from '@tdev/auth-client'; -const { NO_AUTH, OFFLINE_API, TEST_USER, SENTRY_DSN } = siteConfig.customFields as { - TEST_USER?: string; - NO_AUTH?: boolean; +const { OFFLINE_API, SENTRY_DSN } = siteConfig.customFields as { SENTRY_DSN?: string; OFFLINE_API?: boolean | 'memory' | 'indexedDB'; }; @@ -84,31 +82,6 @@ const Sentry = observer(() => { }); function Root({ children }: { children: React.ReactNode }) { - React.useEffect(() => { - if (!rootStore) { - return; - } - if (window) { - if ((window as any).store && (window as any).store !== rootStore) { - try { - (window as any).store.cleanup(); - } catch (e) { - console.error('Failed to cleanup the store', e); - } - } - (window as any).store = rootStore; - } - return () => { - /** - * TODO: cleanup the store - * - remove all listeners - * - clear all data - * - disconnect all sockets - */ - // rootStore?.cleanup(); - }; - }, [rootStore]); - const { siteConfig } = useDocusaurusContext(); React.useEffect(() => { /** @@ -117,34 +90,6 @@ function Root({ children }: { children: React.ReactNode }) { (window as any).store = rootStore; }, [rootStore]); - React.useEffect(() => { - // let timeoutId: ReturnType; - const handleVisibilityChange = () => { - if (document.hidden) { - /** - * eventuall we could disconnect the socket - * or at least indicate to admins that the user has left the page (e.g. for exams) - */ - // rootStore.socketStore.disconnect(); - } else { - /** - * make sure to reconnect the socket when the user returns to the page - * The delay is added to avoid reconnecting too quickly - */ - // timeoutId = setTimeout(() => { - // rootStore.socketStore.reconnect(); - // }, 3000); - } - }; - - document.addEventListener('visibilitychange', handleVisibilityChange); - - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - // clearTimeout(timeoutId); - }; - }, [rootStore]); - return ( <> @@ -170,3 +115,26 @@ function Root({ children }: { children: React.ReactNode }) { } export default Root; + +// React.useEffect(() => { +// // let timeoutId: ReturnType; +// const handleVisibilityChange = () => { +// if (document.hidden) { +// /** +// * The Browser-Window is now hidden +// * we could indicate to admins that the user has left the page +// * (e.g. for exams) +// */ +// } else { +// /** +// * The Browser-Window is now visible again +// */ +// } +// }; + +// document.addEventListener('visibilitychange', handleVisibilityChange); + +// return () => { +// document.removeEventListener('visibilitychange', handleVisibilityChange); +// }; +// }, [rootStore]); From 2b0c0352411fc9780b8426d09abfb8fa96a8c6b7 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Tue, 14 Oct 2025 00:06:01 +0200 Subject: [PATCH 21/29] ensure propper user feedback --- .env.example | 4 -- .github/workflows/deploy.yml | 3 -- README.md | 4 -- docusaurus.config.ts | 8 --- src/components/Admin/EditUser/index.tsx | 70 ++++++++++++++++--------- src/components/Admin/UserTable/User.tsx | 4 +- src/models/User.ts | 1 - src/stores/UserStore.ts | 1 + 8 files changed, 48 insertions(+), 47 deletions(-) diff --git a/.env.example b/.env.example index 415063ee7..6063dc176 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,3 @@ -CLIENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -TENANT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" APP_URL="http://localhost:3000" BACKEND_URL="http://localhost:3002" -API_URI="api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/access_as_user" -STUDENT_USERNAME_PATTERN="@edu" DEFAULT_TEST_USER="adam.admin@gbsl.ch" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 433e5004e..24fd3ea03 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,9 +21,6 @@ jobs: cache: yarn - name: Install dependencies and build website env: - CLIENT_ID: ${{ secrets.CLIENT_ID }} - TENANT_ID: ${{ secrets.TENANT_ID }} - API_URI: ${{ secrets.API_URI }} APP_URL: ${{ secrets.APP_URL }} BACKEND_URL: ${{ secrets.BACKEND_URL }} GH_OAUTH_CLIENT_ID: ${{ vars.GH_OAUTH_CLIENT_ID}} diff --git a/README.md b/README.md index bb3f920c2..e22889856 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,6 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati | :------------------------- | :------------- | :---------------------------------- | :------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `APP_URL` | Production | `http://localhost:3000` | | Domain of the hosted app | | `BACKEND_URL` | Production | `http://localhost:3002` | | Url of the API Endpoint | -| `CLIENT_ID` | Production | | | Azure ID: Client ID | -| `TENANT_ID` | Production | | | Azure AD: Tenant Id | -| `API_URI` | Production | | | Azure AD: API Url | -| `STUDENT_USERNAME_PATTERN` | Production | | `@edu` | Users with usernames matching this RegExp pattern are displayed as students (regardless of admin status). If unset, all non-admin users are displayed as students. | | `DEFAULT_TEST_USER` | Development | | `admin.bar@bazz.ch` | To log in offline. Email of the user to be selected by default. Must correspond to a user email found in the API's database.\* | | `OFFLINE_API` | Dev/Production | `memory` | `true` | `memory` | `indexedDB` | In case the project shall be fully functional, but API persistent data is not needed (e.g. when run in Github Codespace), set this option to true (=`memory`). | | `SENTRY_DSN` | Production | | | Sentry DSN for error tracking | diff --git a/docusaurus.config.ts b/docusaurus.config.ts index b0c4cbe1e..324491ad2 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -71,8 +71,6 @@ const config: Config = applyTransformers({ /** Use test user in local dev: set DEFAULT_TEST_USER to the default test users email adress*/ TEST_USER: DEFAULT_TEST_USER, OFFLINE_API: OFFLINE_API, - /** User.ts#isStudent returns `true` for users matching this pattern. If unset, it returns `true` for all non-admin users. */ - STUDENT_USERNAME_PATTERN: process.env.STUDENT_USERNAME_PATTERN, NO_AUTH: (process.env.NODE_ENV !== 'production' || OFFLINE_API) && !!DEFAULT_TEST_USER, /** The Domain Name where the api is running */ APP_URL: process.env.NETLIFY @@ -82,12 +80,6 @@ const config: Config = applyTransformers({ : process.env.APP_URL || 'http://localhost:3000', /** The Domain Name of this app */ BACKEND_URL: process.env.BACKEND_URL || 'http://localhost:3002', - /** The application id generated in https://portal.azure.com */ - CLIENT_ID: process.env.CLIENT_ID, - /** Tenant / Verzeichnis-ID (Mandant) */ - TENANT_ID: process.env.TENANT_ID, - /** The application id uri generated in https://portal.azure.com */ - API_URI: process.env.API_URI, GIT_COMMIT_SHA: GIT_COMMIT_SHA, SENTRY_DSN: process.env.SENTRY_DSN, GH_OAUTH_CLIENT_ID: GH_OAUTH_CLIENT_ID, diff --git a/src/components/Admin/EditUser/index.tsx b/src/components/Admin/EditUser/index.tsx index 3b651b6b1..9608c5949 100644 --- a/src/components/Admin/EditUser/index.tsx +++ b/src/components/Admin/EditUser/index.tsx @@ -21,11 +21,15 @@ interface Props { close: () => void; } +type SpinState = 'deleting' | 'linking' | 'unlinking' | 'change-pw' | 'block-user' | 'unblock-user'; + const SPIN_TEXT = { deleting: 'Löschen...', linking: 'Verknüpfen...', unlinking: 'Verknüpfung aufheben...', - 'change-pw': 'Passwort ändern...' + 'change-pw': 'Passwort ändern...', + 'block-user': 'User blockieren...', + 'unblock-user': 'Blockierung aufheben...' }; const pwValidator = (pw: string) => (pw.length > 7 ? null : 'Passwort muss min. 8 Zeichen haben'); @@ -34,9 +38,7 @@ const EditUser = observer((props: Props) => { const { user } = props; const userStore = useStore('userStore'); const adminStore = useStore('adminStore'); - const [spinState, setSpinState] = React.useState< - null | 'deleting' | 'linking' | 'unlinking' | 'change-pw' - >(null); + const [spinState, setSpinState] = React.useState(null); const [password, setPassword] = React.useState(''); const [pwState, setPwState] = React.useState<'error' | 'success' | null>(null); @@ -65,7 +67,7 @@ const EditUser = observer((props: Props) => { props.close(); }} color="black" - text="Abbrechen" + text="Schliessen" disabled={!!spinState} />