diff --git a/.nvmrc b/.nvmrc index 805b5a4e..8e03fd37 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.9.0 +v22.15 diff --git a/.updateTdev b/.updateTdev index d359a57f..2574e55e 100644 --- a/.updateTdev +++ b/.updateTdev @@ -1 +1 @@ -CURRENT_COMMIT=5103a900773b33dca171a591128d82a758724ac5 \ No newline at end of file +CURRENT_COMMIT=439e0c4d9568d740a8577e58774106100f0522c7 \ No newline at end of file diff --git a/dev/demoExamFile.json b/dev/demoExamFile.json index f778f4f4..8438bc72 100644 --- a/dev/demoExamFile.json +++ b/dev/demoExamFile.json @@ -5,9 +5,7 @@ "data": { "type": "string", "documentRootId": "178c93a6-fa60-4ca8-97ed-f6b9344043e9", - "transformers": [ - "trim" - ], + "transformers": ["trim"], "solutions": [ { "value": "/Users/[^/]+/Desktop/demo.mp4", "regex": true }, { "value": "C://Users/[^/]+/Desktop/demo.mp4", "regex": true } @@ -19,14 +17,7 @@ "points": 3, "data": { "type": "true-false", - "answers": [ - true, - true, - false, - true, - false, - false - ], + "answers": [true, true, false, true, false, false], "grading": { "correct": 0.5, "incorrect": -0.25, @@ -47,12 +38,7 @@ "points": 2, "data": { "type": "multiple-choice", - "answers": [ - true, - true, - false, - false - ], + "answers": [true, true, false, false], "grading": { "error": -1, "negativePoints": false @@ -66,4 +52,4 @@ "type": "string" } } -} \ No newline at end of file +} diff --git a/docs/tdev/app-architecture/api-deploy/index.mdx b/docs/tdev/app-architecture/api-deploy/index.mdx index 5f88a8ec..65d8b31a 100644 --- a/docs/tdev/app-architecture/api-deploy/index.mdx +++ b/docs/tdev/app-architecture/api-deploy/index.mdx @@ -5,7 +5,7 @@ page_id: 560cfd4d-4464-42ff-a121-a3d116ea1994 import Steps from '@tdev-components/Steps' import { Val, TemplateCode, DynamicInput } from '@tdev-components/DynamicValues'; import _ from 'es-toolkit/compat'; -import { generateRandomBase64 } from './secureToken'; +import { generateRandomBase64 } from '../helpers/secureToken'; # API Aufsetzen @@ -16,7 +16,6 @@ import { generateRandomBase64 } from './secureToken'; _.camelCase((page.dynamicValues.get('app') || 'inf-teaching-api'))} /> - generateRandomBase64()} /> diff --git a/docs/tdev/app-architecture/dokku/index.mdx b/docs/tdev/app-architecture/dokku/index.mdx index 35cc9c8e..585c6d3c 100644 --- a/docs/tdev/app-architecture/dokku/index.mdx +++ b/docs/tdev/app-architecture/dokku/index.mdx @@ -4,6 +4,7 @@ page_id: 323ff390-40d6-4bd5-ac6c-7a05f3515526 import Steps from '@tdev-components/Steps' import { Val, TemplateCode, DynamicInput } from '@tdev-components/DynamicValues'; +import { generateRandomBase64 } from '../helpers/secureToken'; # Dokku Deploy @@ -18,8 +19,8 @@ Durch das Hosten auf einem eigenen Server kann mit bspw. `http-auth` der Zugriff 1. Eine neue App ____ auf dem Server erstellen - - + + generateRandomBase64()} /> @@ -32,9 +33,8 @@ Durch das Hosten auf einem eigenen Server kann mit bspw. `http-auth` der Zugriff # remove default global domain dokku domains:remove {{app}} {{app}}.gbsl.website - dokku config:set --no-restart {{app}} API_URI={{API_URI}} dokku config:set --no-restart {{app}} APP_URL=https://{{domain}} - dokku config:set --no-restart {{app}} BACKEND_URL={{BACKEND_URL}} + dokku config:set --no-restart {{app}} BETTER_AUTH_URL={{BETTER_AUTH_URL}} dokku config:set --no-restart {{app}} CLIENT_ID={{CLIENT_ID}} dokku config:set --no-restart {{app}} TENANT_ID={{TENANT_ID}} ``` @@ -42,9 +42,8 @@ Durch das Hosten auf einem eigenen Server kann mit bspw. `http-auth` der Zugriff :::details[Alternativ: direkt über `/home/dokku/{{app}}/ENV`] ```bash title="/home/dokku/{{app}}/ENV" - API_URI="{{API_URI}}" APP_URL="https://{{domain}}" - BACKEND_URL="{{BACKEND_URL}}" + BETTER_AUTH_URL="{{BETTER_AUTH_URL}}" CLIENT_ID="{{CLIENT_ID}}" TENANT_ID="{{TENANT_ID}}" NGINX_ROOT="build" diff --git a/docs/tdev/app-architecture/api-deploy/secureToken.ts b/docs/tdev/app-architecture/helpers/secureToken.ts similarity index 100% rename from docs/tdev/app-architecture/api-deploy/secureToken.ts rename to docs/tdev/app-architecture/helpers/secureToken.ts diff --git a/docs/tdev/app-architecture/remark-plugins/code-as-attribute/index.mdx b/docs/tdev/app-architecture/remark-plugins/code-as-attribute/index.mdx new file mode 100644 index 00000000..403915db --- /dev/null +++ b/docs/tdev/app-architecture/remark-plugins/code-as-attribute/index.mdx @@ -0,0 +1,149 @@ +--- +page_id: a923c804-819a-4683-b284-001c285626bc +tags: [remark] +--- + +# Code as Attribute + +Dieses Remark-Plugin ermöglicht es, Codeblöcke als Attribute an spezifizierte MDX-Komponenten zu übergeben. Dies ist besonders nützlich, wenn Codeblöcke als MD-Codeblock formatiert werden sollen, aber der Code als Inhalt an eine Komponente übergeben werden muss. + +Wird bspw. eingesetzt für: +- [SVG-Editor](../../../gallery/persistable-documents/svg-editor/index.mdx) +- [Netpbm-Editor](../../../gallery/persistable-documents/netpbm-editor/index.mdx) +- [HTML-Editor](../../../gallery/persistable-documents/html-editor/index.mdx) + +:::flex{minWidth=250px} +#### Vorher +````md +# Remark Code as Attribute + + +```html name="index.html" +

Foo Bar

+``` +
+```` +::br +#### Nachher +```md + +``` +::: + +## Optionen + +### `codeAttributesName` + +Wird `codeAttributesName` angegeben, so werden die Attribute `lang` und `meta` des Codeblocks zusätzlich als Objekt mit diesem Namen übergeben. + +```ts title="Komponenten-Konfiguration" +{ + name: 'HtmlIDE', + // highlight-start + attributeName: 'code', + codeAttributesName: 'codeAttributes' + // highlight-end +} +``` + +:::flex{minWidth=250px} +#### Vorher +````md +# Remark Code as Attribute + + +```html name=index.html +

Foo Bar

+``` +
+```` +::br +#### Nachher +```md + +``` +::: + +### `processMultiple` +Ist `processMultiple` gesetzt, so werden alle aufeinanderfolgenden Codeblöcke als Array von Objekten an die Komponente übergeben, wobei der CodeBlock im Feld `code` zu finden ist. Jedes Objekt enthält den Code und ggf. die Attribute `lang` und `meta`. + +```ts title="Komponenten-Konfiguration" +{ + name: 'HtmlIDE', + // highlight-start + attributeName: 'files', + processMultiple: true + // highlight-end +} +``` + +:::flex{minWidth=250px} +#### Vorher +````md +# Remark Code as Attribute + + +```html name=foo.html +

Foo

+``` +```html name=bar.html +

Bar

+``` +
+```` +::br +#### Nachher +```mdx +Foo\n", lang: "html", meta: "name=foo.html" }, + { code: "\n

Bar

\n", lang: "html", meta: "name=bar.html" } + ]} +/> +``` +::: + + +## Installation + +:::info[Code] +- `src/plugins/remark-code-as-attribute` +::: + +:::info[`docusaurus.config.ts`] +```ts +import codeAsAttributePlugin from './src/plugins/remark-code-as-attribute/plugin'; + +const REMARK_PLUGINS = [ + /* ... */ + [ + [ + codeAsAttributePlugin, + { + components: [ + { + name: 'HtmlEditor', // Name der React-Komponente + attributeName: 'code' // der Code wird als Attribut `code` übergeben + }, + { + name: 'HtmlIDE', + attributeName: 'files', + processMultiple: true // der Code wird als Array von MultiCode-Objekten übergeben + }, + { + name: 'TemplateCode', + attributeName: 'code', + codeAttributesName: 'codeAttributes' // es werden zusätzlich `lang` und `meta` übergeben + } + /*...*/ + ], + } + ] + ] +]; +``` +::: \ No newline at end of file diff --git a/docs/tdev/gallery/display/browser-window/index.mdx b/docs/tdev/gallery/display/browser-window/index.mdx new file mode 100644 index 00000000..34670182 --- /dev/null +++ b/docs/tdev/gallery/display/browser-window/index.mdx @@ -0,0 +1,84 @@ +--- +page_id: 250b7597-b43c-4923-968b-f10fd099fc10 +--- +import BrowserWindow from '@tdev-components/BrowserWindow'; + +# Browser Window + +```tsx +import BrowserWindow from '@tdev-components/BrowserWindow'; + + + Inhalte... + +``` + + + Inhalte... + + +## Optionen + +### `url` + +```tsx + + Auf der Seite [example.com](https://example.com) findet man... + +``` + + + Auf der Seite [example.com](https://example.com) findet man... + + +### `href` + +Die Adresse wird als Link dargestellt. + +```tsx + + Auf der Seite example.com findet man... + + + Inhalte... + +``` + + + Auf der Seite example.com findet man... + + + Inhalte... + + + +### `minHeight` und `maxHeight` + +```tsx + + Inhalte... + + +
+ Vieeele Inhalte... +
+
+``` + + + Inhalte... + + +
+ Vieeele Inhalte... +
+
+ +### Weitere Attribute + +`style` +: CSS-Stil für das gesamte Browser-Fenster +`bodyStyle` +: CSS-Stil für den Inhaltsbereich +`className` +: Zusätzliche CSS-Klasse für das gesamte Browser-Fenster \ No newline at end of file diff --git a/docs/tdev/gallery/markdown/index.mdx b/docs/tdev/gallery/markdown/index.mdx index 585e3ab1..36baa89f 100644 --- a/docs/tdev/gallery/markdown/index.mdx +++ b/docs/tdev/gallery/markdown/index.mdx @@ -3,6 +3,7 @@ page_id: 49c5bf94-f670-4aa1-bfe8-5fcfa6371a15 sidebar_position: 20 --- + # Markdown Remark und seine Plugins helfen dabei, Markdown- und MDX-Dateien zu verarbeiten und anzupassen, indem sie individuelle Funktionen hinzufügen, wie etwa die Umwandlung spezieller Syntax oder die Einbindung zusätzlicher Inhalte. diff --git a/docs/tdev/gallery/markdown/mermaid/index.mdx b/docs/tdev/gallery/markdown/mermaid/index.mdx new file mode 100644 index 00000000..90af1549 --- /dev/null +++ b/docs/tdev/gallery/markdown/mermaid/index.mdx @@ -0,0 +1,225 @@ +--- +page_id: 105787ee-e4f3-4d3a-aeee-4183e914615a +tags: [markdown, visualization, mermaid] +--- + +import BrowserWindow from '@tdev-components/BrowserWindow'; + +# Mermaid + +[Docusaurus](https://docusaurus.io/docs/api/themes/@docusaurus/theme-mermaid) unterstützt [Mermaid](https://docs.mermaidchart.com/mermaid-oss/intro/index.html) Diagramme in Markdown Dateien. + + +[Live Editor](https://mermaid.live/) + +## Beispiele +Beispiele von [https://docs.mermaidchart.com](https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html) + +### Graph +````md +```mermaid +graph LR + A[Square Rect] -- Link text --> B((Circle)) + A --> C(Round Rect) + B --> D{Rhombus} + C --> D +``` +```` + +```mermaid +graph LR + A[Square Rect] -- Link text --> B((Circle)) + A --> C(Round Rect) + B --> D{Rhombus} + C --> D +``` + + +### Pie Chart +````md +```mermaid +pie title NETFLIX + "Time spent looking for movie" : 90 + "Time spent watching it" : 10 +``` +```` + + +```mermaid +pie title NETFLIX + "Time spent looking for movie" : 90 + "Time spent watching it" : 10 +``` + + + +### Sequence Diagram +````md +```mermaid +sequenceDiagram + loop Daily query + Alice->>Bob: Hello Bob, how are you? + alt is sick + Bob->>Alice: Not so good :( + else is well + Bob->>Alice: Feeling fresh like a daisy + end + + opt Extra response + Bob->>Alice: Thanks for asking + end + end +``` +oder + +```mermaid +sequenceDiagram + actor Alice + actor Bob + Alice->>Alice: private Farbe mischen + Bob->>Bob: private Farbe mischen + Alice->>Bob: Gemeinsame Farbe + Alice->>Bob: privat + gemeinsam + Bob->>Alice: privat + gemeinsam + Bob->>Bob: erhalten + privat + Alice->>Alice: erhalten + privat +``` +```` + + +:::flex{flexBasis=220px} +```mermaid +sequenceDiagram + loop Daily query + Alice->>Bob: Hello Bob, how are you? + alt is sick + Bob->>Alice: Not so good :( + else is well + Bob->>Alice: Feeling fresh like a daisy + end + + opt Extra response + Bob->>Alice: Thanks for asking + end + end +``` + +::br +```mermaid +sequenceDiagram + actor Alice + actor Bob + Alice->>Alice: private Farbe mischen + Bob->>Bob: private Farbe mischen + Alice->>Bob: Gemeinsame Farbe + Alice->>Bob: privat + gemeinsam + Bob->>Alice: privat + gemeinsam + Bob->>Bob: erhalten + privat + Alice->>Alice: erhalten + privat +``` +::: + + +### Git Graph +````md +```mermaid +gitGraph: + commit "Ashish" + branch newbranch + checkout newbranch + commit id:"1111" + commit tag:"test" + checkout main + commit type: HIGHLIGHT + commit + merge newbranch + commit + branch b2 + commit +``` +```` + + +```mermaid +gitGraph: + commit "Ashish" + branch newbranch + checkout newbranch + commit id:"1111" + commit tag:"test" + checkout main + commit type: HIGHLIGHT + commit + merge newbranch + commit + branch b2 + commit +``` + + +````md + +### State Diagram + +```mermaid +--- +title: Simple sample +--- +stateDiagram-v2 + [*] --> Still + Still --> [*] + + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*] +``` +```` + + +```mermaid +--- +title: Simple sample +--- +stateDiagram-v2 + [*] --> Still + Still --> [*] + + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*] +``` + + +### Gantt Chart + +````md + +```mermaid +gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + section Section + A task :a1, 2014-01-01, 30d + Another task :after a1, 20d + section Another + Task in Another :2014-01-12, 12d + another task :24d +``` +```` + + + +```mermaid +gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + section Section + A task :a1, 2014-01-01, 30d + Another task :after a1, 20d + section Another + Task in Another :2014-01-12, 12d + another task :24d +``` + diff --git a/docs/tdev/gallery/markdown/remark-deflist/index.mdx b/docs/tdev/gallery/markdown/remark-deflist/index.mdx index 4918fee4..fd8cc49e 100644 --- a/docs/tdev/gallery/markdown/remark-deflist/index.mdx +++ b/docs/tdev/gallery/markdown/remark-deflist/index.mdx @@ -1,5 +1,5 @@ --- -page_id: 9a0e84c3-778f-43e6-948c-79ea7f70a48f +page_id: ebe2c231-95d4-4c9d-aab7-2d90138601c2 tags: [remark] --- import BrowserWindow from '@tdev-components/BrowserWindow'; diff --git a/docs/tdev/gallery/markdown/remark-media/circuitverse.mdx b/docs/tdev/gallery/markdown/remark-media/circuitverse.mdx index 336a9d15..7367caec 100644 --- a/docs/tdev/gallery/markdown/remark-media/circuitverse.mdx +++ b/docs/tdev/gallery/markdown/remark-media/circuitverse.mdx @@ -1,5 +1,5 @@ --- -page_id: 41c6a9c0-61fd-4c97-9375-936f552de079 +page_id: f6f7a1a7-faf2-4f95-8901-de6df01788f2 --- import BrowserWindow from '@tdev-components/BrowserWindow'; @@ -35,4 +35,4 @@ css Eigenschaften ::circuitverse[https://circuitverse.org/simulator/embed/rothe-inverter]{width=300px height=200px border="2px solid red"} - \ No newline at end of file + diff --git a/docs/tdev/gallery/markdown/remark-media/codepen.mdx b/docs/tdev/gallery/markdown/remark-media/codepen.mdx index 9a437c20..24a004b0 100644 --- a/docs/tdev/gallery/markdown/remark-media/codepen.mdx +++ b/docs/tdev/gallery/markdown/remark-media/codepen.mdx @@ -1,5 +1,5 @@ --- -page_id: 41c6a9c0-61fd-4c97-9375-936f552de079 +page_id: f9d808b6-4f46-48f3-8274-0e84c908944d --- import BrowserWindow from '@tdev-components/BrowserWindow'; diff --git a/docs/tdev/gallery/markdown/remark-media/index.mdx b/docs/tdev/gallery/markdown/remark-media/index.mdx index 56b6b567..8acfc05f 100644 --- a/docs/tdev/gallery/markdown/remark-media/index.mdx +++ b/docs/tdev/gallery/markdown/remark-media/index.mdx @@ -1,5 +1,5 @@ --- -page_id: 117c8fba-9e80-4400-944d-050129aa9fe5 +page_id: 627b08df-b0a2-4a5f-9d8c-e346c416d5f6 --- import DocCardList from '@theme/DocCardList'; @@ -29,4 +29,4 @@ const REMARK_PLUGINS = [ mediaPlugin, ]; ``` -::: \ No newline at end of file +::: diff --git a/docs/tdev/gallery/markdown/remark-media/learningapps.mdx b/docs/tdev/gallery/markdown/remark-media/learningapps.mdx index 95cf660b..04a72d89 100644 --- a/docs/tdev/gallery/markdown/remark-media/learningapps.mdx +++ b/docs/tdev/gallery/markdown/remark-media/learningapps.mdx @@ -1,5 +1,5 @@ --- -page_id: 41c6a9c0-61fd-4c97-9375-936f552de079 +page_id: 36af2cdd-3a39-4aaa-87ed-ae533f3cd350 --- import BrowserWindow from '@tdev-components/BrowserWindow'; diff --git a/docs/tdev/gallery/markdown/remark-media/video.mdx b/docs/tdev/gallery/markdown/remark-media/video.mdx index 84c90133..fb6477c7 100644 --- a/docs/tdev/gallery/markdown/remark-media/video.mdx +++ b/docs/tdev/gallery/markdown/remark-media/video.mdx @@ -1,5 +1,5 @@ --- -page_id: 41c6a9c0-61fd-4c97-9375-936f552de079 +page_id: f580154f-e240-446c-bf69-491bb9e3eadb --- import BrowserWindow from '@tdev-components/BrowserWindow'; diff --git a/docs/tdev/gallery/markdown/remark-media/youtube.mdx b/docs/tdev/gallery/markdown/remark-media/youtube.mdx index b964a5b5..4f2771e4 100644 --- a/docs/tdev/gallery/markdown/remark-media/youtube.mdx +++ b/docs/tdev/gallery/markdown/remark-media/youtube.mdx @@ -1,5 +1,5 @@ --- -page_id: 9492f5bd-6e5d-403d-ad01-a9e1ba83d2e7 +page_id: ed7243ee-2c42-4191-9ffe-2f4ac2899420 --- import BrowserWindow from '@tdev-components/BrowserWindow'; @@ -32,4 +32,4 @@ Fügt ein YouTube-Video ein. ::youtube[https://www.youtube.com/embed/QPZ0pIK_wsc?si=fP8L8fYQ-TYgYwUe]{height=200px width=300px} - \ No newline at end of file + diff --git a/docs/tdev/gallery/persistable-documents/code-block/index.mdx b/docs/tdev/gallery/persistable-documents/code-block/index.mdx index 399ac387..3eb5d6a5 100644 --- a/docs/tdev/gallery/persistable-documents/code-block/index.mdx +++ b/docs/tdev/gallery/persistable-documents/code-block/index.mdx @@ -611,7 +611,7 @@ Die neuen Models `Script` und `ScriptVersion` müssen unter `src/api/document.ts "rc-slider": "^11.1.5", "react-ace": "^12.0.0", "react-diff-viewer-continued": "^3.4.0", - "react-draggable": "^4.4.6", + "react-draggable": "^4.5.0", "svg-parser": "^2.0.4" }, "devDependencies": { diff --git a/docs/tdev/gallery/persistable-documents/html-editor/index.mdx b/docs/tdev/gallery/persistable-documents/html-editor/index.mdx index 1bbea997..ca9df883 100644 --- a/docs/tdev/gallery/persistable-documents/html-editor/index.mdx +++ b/docs/tdev/gallery/persistable-documents/html-editor/index.mdx @@ -6,10 +6,12 @@ tags: --- import HtmlEditor from '@tdev-components/documents/CodeEditor/HtmlEditor'; +import HtmlIDE from '@tdev-components/documents/CodeEditor/HtmlEditor/HtmlIDE'; import BrowserWindow from '@tdev-components/BrowserWindow'; # HTML Editor + Der SVG Editor ist eine Komponente, welche auf dem `CodeBlock` aufbaut - es muss also nichts zusätzlich installiert werden. ````md @@ -88,4 +90,82 @@ Für Übungen, in welchen "lokale" Medien referenziert werden sollen, kann der H ```
- \ No newline at end of file + + +## HTML IDE + +Der HTML IDE ist eine Komponente, welche auf dem `CodeBlock` und dem `HtmlEditor` aufbaut - es muss also nichts zusätzlich installiert werden. Sie ermöglicht das Editieren von mehreren Dateien in einem Dateibaum, wobei auch relative Verlinkungen möglich sind. + +````tsx + + ```html path=/index.html id=a877cf87-8505-4309-8129-c3a60ed831ad +

Foo Bar

+ Zu SubPage + ``` + ```html path=/dir/sub.html id=c1d27537-aaba-493d-8da5-15c45d5b05c4 +

SubPage

+ Zurück zur Startseite + ``` +
+```` + + + + ```html path=/index.html id=a877cf87-8505-4309-8129-c3a60ed831ad +

Foo Bar

+ Zu SubPage + ``` + ```html path=/dir/sub.html id=c1d27537-aaba-493d-8da5-15c45d5b05c4 +

SubPage

+ Zurück zur Startseite + ``` +
+
+ + +````tsx +import HtmlIDE from '@tdev-components/documents/CodeEditor/HtmlEditor/HtmlIDE'; + +```html path=/index.html id=c80067ae-1cd2-4117-b44d-4605fc847d50 +

Foo Bar

+ + +Zu SubPage +``` +```html path=/dir/sub.html id=edc961ef-f629-46a3-8f87-c49460e8b32b +

SubPage

+ + + +Zurück zur Startseite +``` +
+```` + + + + ```html path=/index.html id=c80067ae-1cd2-4117-b44d-4605fc847d50 +

Foo Bar

+ + + Zu SubPage + ``` + ```html path=/dir/sub.html id=edc961ef-f629-46a3-8f87-c49460e8b32b +

SubPage

+ + + + Zurück zur Startseite + ``` +
+
diff --git a/docusaurus.config.ts b/docusaurus.config.ts index a097be37..490ce513 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -22,6 +22,7 @@ import path from 'path'; import { recommendedBeforeDefaultRemarkPlugins, recommendedRehypePlugins, recommendedRemarkPlugins } from './src/siteConfig/markdownPluginConfigs'; import { remarkPdfPluginConfig } from '@tdev/remark-pdf'; import { excalidrawPluginConfig } from '@tdev/excalidoc'; +import { EditThisPageOption, ShowEditThisPage } from '@tdev/siteConfig/siteConfig'; const siteConfig = getSiteConfig(); @@ -64,14 +65,11 @@ const config: Config = applyTransformers({ projectName: PROJECT_NAME, // Usually your repo name. onBrokenLinks: siteConfig.onBrokenLinks ?? 'throw', - onBrokenMarkdownLinks: siteConfig.onBrokenMarkdownLinks ?? 'warn', customFields: { /** 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 @@ -81,16 +79,13 @@ 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, - PERSONAL_SPACE_DOC_ROOT_ID: siteConfig.personalSpaceDocRootId || '2686fc4e-10e7-4288-bf41-e6175e489b8e' + PERSONAL_SPACE_DOC_ROOT_ID: siteConfig.personalSpaceDocRootId || '2686fc4e-10e7-4288-bf41-e6175e489b8e', + showEditThisPage: siteConfig.showEditThisPage ?? 'always' satisfies ShowEditThisPage, + showEditThisPageOptions: siteConfig.showEditThisPageOptions ?? ['github', 'github-dev', 'cms'] satisfies EditThisPageOption[], + editThisPageCmsUrl: siteConfig.editThisPageCmsUrl ?? '/cms/', }, future: { v4: true, @@ -188,7 +183,11 @@ const config: Config = applyTransformers({ } return result; }, - mermaid: true + mermaid: true, + hooks: { + onBrokenMarkdownLinks: siteConfig.onBrokenMarkdownLinks ?? 'warn', + }, + ...siteConfig.markdown }, presets: [ [ @@ -310,6 +309,7 @@ const config: Config = applyTransformers({ ] ], themes: [ + '@docusaurus/theme-mermaid', [ themeCodeEditor, { @@ -317,7 +317,8 @@ const config: Config = applyTransformers({ brythonStdlibSrc: 'https://cdn.jsdelivr.net/npm/brython@3.13.2/brython_stdlib.js', libDir: '/bry-libs/' } - ] + ], + ...(siteConfig.themes || []) ], stylesheets: [ { diff --git a/package.json b/package.json index 2cb4e6f5..45f57d00 100644 --- a/package.json +++ b/package.json @@ -28,13 +28,12 @@ "@azure/msal-browser": "^v3.28.0", "@azure/msal-react": "^2.2.0", "@botom/quill-resize-module": "^2.0.1", - "@docusaurus/core": "^3.8.1", - "@docusaurus/faster": "^3.8.1", - "@docusaurus/preset-classic": "^3.8.1", - "@docusaurus/theme-classic": "^3.8.1", - "@docusaurus/theme-common": "^3.8.1", - "@docusaurus/theme-mermaid": "^3.5.2", - "@docusaurus/theme-search-algolia": "^3.7.0", + "@docusaurus/core": "^3.9.1", + "@docusaurus/faster": "^3.9.1", + "@docusaurus/preset-classic": "^3.9.1", + "@docusaurus/theme-classic": "^3.9.1", + "@docusaurus/theme-common": "^3.9.1", + "@docusaurus/theme-mermaid": "^3.9.1", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", @@ -52,6 +51,7 @@ "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", "@mdxeditor/editor": "^3.37.0", + "@mermaid-js/layout-elk": "^0.2.0", "@octokit/rest": "^21.1.0", "@radial-color-picker/react-color-picker": "^4.0.1", "@sentry/react": "^9.12.0", @@ -63,6 +63,7 @@ "axios": "^1.9.0", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-decorators-legacy": "^1.3.5", + "better-auth": "^1.3.26", "browser-image-compression": "^2.0.2", "clsx": "^2.1.1", "dotenv": "^16.5.0", @@ -82,19 +83,19 @@ "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.1.0", "micromatch": "^4.0.8", - "mobx": "^6.13.7", - "mobx-react-lite": "^4.1.0", - "mobx-utils": "^6.1.0", + "mobx": "^6.15.0", + "mobx-react-lite": "^4.1.1", + "mobx-utils": "^6.1.1", "moment": "^2.30.1", "prism-react-renderer": "^2.1.0", "qrcode.react": "^4.2.0", "quill": "^2.0.3", "rc-slider": "^11.1.8", - "react": "^19.1.0", + "react": "^19.2.0", "react-ace": "^14.0.1", "react-diff-viewer-continued": "^3.4.0", - "react-dom": "^19.1.0", - "react-draggable": "^4.4.6", + "react-dom": "^19.2.0", + "react-draggable": "^4.5.0", "react-katex": "^3.0.1", "react-quilljs": "^2.0.5", "react-select": "^5.10.1", @@ -108,15 +109,15 @@ "svg-parser": "^2.0.4", "ts-node": "^10.9.2", "typed.js": "^2.1.0", - "uuid": "^11.1.0", + "uuid": "^13.0.0", "yaml": "^2.3.4" }, "devDependencies": { "@babel/plugin-proposal-decorators": "^7.24.7", - "@docusaurus/module-type-aliases": "^3.8.1", - "@docusaurus/plugin-rsdoctor": "^3.8.1", - "@docusaurus/tsconfig": "^3.8.1", - "@docusaurus/types": "^3.8.1", + "@docusaurus/module-type-aliases": "^3.9.1", + "@docusaurus/plugin-rsdoctor": "^3.9.1", + "@docusaurus/tsconfig": "^3.9.1", + "@docusaurus/types": "^3.9.1", "@types/chokidar": "^2.1.3", "@types/exceljs": "^1.3.0", "@types/fs-extra": "^11.0.4", @@ -131,7 +132,7 @@ "docusaurus-plugin-sass": "^0.2.6", "prettier": "^3.3.2", "remark": "^15.0.1", - "sass": "^1.87.0", + "sass": "^1.93.2", "ts-jest": "^29.1.1", "typescript": "^5.7.2", "vfile": "^6.0.3", diff --git a/packages/tdev/excalidoc/index.ts b/packages/tdev/excalidoc/index.ts index ea45bd1a..86643bc7 100644 --- a/packages/tdev/excalidoc/index.ts +++ b/packages/tdev/excalidoc/index.ts @@ -1,7 +1,7 @@ -import type { PluginConfig } from '@docusaurus/types'; import path from 'path'; +import type { PluginModule } from '@docusaurus/types'; -export const excalidrawPluginConfig: PluginConfig = () => { +export const excalidrawPluginConfig = (() => { return { name: 'excalidraw-config', configureWebpack(config, isServer, { currentBundler }) { @@ -34,4 +34,4 @@ export const excalidrawPluginConfig: PluginConfig = () => { }; } }; -}; +}) satisfies PluginModule; diff --git a/packages/tdev/remark-pdf/index.ts b/packages/tdev/remark-pdf/index.ts index 2d419d26..6e8b511e 100644 --- a/packages/tdev/remark-pdf/index.ts +++ b/packages/tdev/remark-pdf/index.ts @@ -14,7 +14,7 @@ const getCopyPlugin = (currentBundler: CurrentBundler): typeof CopyWebpackPlugin return CopyWebpackPlugin; }; -export const remarkPdfPluginConfig: PluginConfig = () => { +export const remarkPdfPluginConfig = (() => { return { name: 'pdfjs-copy-dependencies', configureWebpack(config, isServer, { currentBundler }) { @@ -38,4 +38,4 @@ export const remarkPdfPluginConfig: PluginConfig = () => { }; } }; -}; +}) satisfies PluginConfig; diff --git a/src/api/OfflineApi/Adapter/MemoryDb.ts b/src/api/OfflineApi/Adapter/MemoryDb.ts index 5861790e..18845fb7 100644 --- a/src/api/OfflineApi/Adapter/MemoryDb.ts +++ b/src/api/OfflineApi/Adapter/MemoryDb.ts @@ -11,9 +11,9 @@ class MemoryDbAdapter implements DbAdapter { async getAll(storeName: string): Promise { if (!this.db[storeName]) { - return []; + return Promise.resolve([]); } - return Object.values(this.db[storeName]) as T[]; + return Promise.resolve(Object.values(this.db[storeName]) as T[]); } async byDocumentRootId( @@ -22,9 +22,9 @@ class MemoryDbAdapter implements DbAdapter { if (!documentRootId) { return Promise.resolve([]); } - return this.filter('documents', (doc) => doc.documentRootId === documentRootId) as Promise< - Document[] - >; + return Promise.resolve( + this.filter('documents', (doc) => doc.documentRootId === documentRootId) as Promise[]> + ); } async put(storeName: string, item: T & { id: string }): Promise { @@ -32,21 +32,24 @@ class MemoryDbAdapter implements DbAdapter { this.db[storeName] = {}; } this.db[storeName][item.id] = item; + return Promise.resolve(); } async delete(storeName: string, id: string): Promise { if (this.db[storeName] && this.db[storeName][id]) { delete this.db[storeName][id]; } + return Promise.resolve(); } async filter(storeName: string, filterFn: (item: T) => boolean): Promise { const allItems = await this.getAll(storeName); - return allItems.filter(filterFn); + return Promise.resolve(allItems.filter(filterFn)); } async destroyDb(): Promise { this.db = {}; console.log('MemoryDbAdapter: Database destroyed'); + return Promise.resolve(); } } diff --git a/src/api/OfflineApi/index.ts b/src/api/OfflineApi/index.ts index 5944f00a..63904e0a 100644 --- a/src/api/OfflineApi/index.ts +++ b/src/api/OfflineApi/index.ts @@ -18,6 +18,7 @@ const LOG_REQUESTS = false; let OfflineUser: User = { id: 'c23c0238-4aeb-457f-9a2c-3d2d5d8931c0', email: 'offline.user@tdev.ch', + name: 'Offline User', firstName: 'Offline', lastName: 'User', role: process.env.NODE_ENV === 'production' ? Role.STUDENT : Role.ADMIN, @@ -29,6 +30,7 @@ const DB_NAME = `${siteConfig.organizationName ?? 'gbsl'}-${siteConfig.projectNa const DOCUMENTS_STORE = 'documents'; const STUDENT_GROUPS_STORE = 'studentGroups'; const PERMISSIONS_STORE = 'permissions'; +export const getOfflineUser = () => ({ ...OfflineUser }); const resolveResponse = (data: T, statusCode: number = 200, statusText: string = ''): AxiosPromise => { return Promise.resolve({ @@ -90,6 +92,7 @@ export default class OfflineApi { this.dbAdapter = new MemoryDbAdapter(); } } else { + console.log('setup memory adapter'); this.dbAdapter = new MemoryDbAdapter(); } if (LOG_REQUESTS) { @@ -236,8 +239,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); @@ -259,7 +260,6 @@ export default class OfflineApi { } if (query.has('rids')) { const rids = query.getAll('rids'); - console.log('rids', rids); const allDocuments = await this.dbAdapter.getAll>(DOCUMENTS_STORE); diff --git a/src/api/admin.ts b/src/api/admin.ts index f65a7fd4..6c4a7f4b 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -22,3 +22,15 @@ export function createAllowedAction( export function allowedActions(signal: AbortSignal): AxiosPromise { return api.get(`/admin/allowedActions`, { signal }); } + +export function linkUserPassword( + userId: string, + userPW: string, + signal: AbortSignal +): AxiosPromise { + return api.post(`/admin/users/${userId}/linkUserPassword`, { pw: userPW }, { signal }); +} + +export function revokeUserPassword(userId: string, signal: AbortSignal): AxiosPromise { + return api.post(`/admin/users/${userId}/revokeUserPassword`, { signal }); +} diff --git a/src/api/base.ts b/src/api/base.ts index 35526868..de2555a0 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/api/user.ts b/src/api/user.ts index e3d17fc5..95dac96d 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,10 +1,12 @@ +import { mdiEmailLock, mdiGithub, mdiMicrosoft } from '@mdi/js'; import api from './base'; import { AxiosPromise } from 'axios'; +import { IfmColors } from '@tdev-components/shared/Colors'; export enum Role { - STUDENT = 'STUDENT', - TEACHER = 'TEACHER', - ADMIN = 'ADMIN' + STUDENT = 'student', + TEACHER = 'teacher', + ADMIN = 'admin' } export const RoleNames: { [key in Role]: string } = { @@ -13,18 +15,47 @@ export const RoleNames: { [key in Role]: string } = { [Role.ADMIN]: 'Admin' }; +export const RoleColors: { [key in Role]: string } = { + [Role.STUDENT]: 'blue', + [Role.TEACHER]: 'green', + [Role.ADMIN]: 'red' +}; + export const RoleAccessLevel: { [key in Role]: number } = { [Role.STUDENT]: 0, [Role.TEACHER]: 1, [Role.ADMIN]: 2 }; +export enum AuthProvider { + MICROSOFT = 'microsoft', + CREDENTIAL = 'credential', + GITHUB = 'github' +} + +export const AuthProviderIcons = { + [AuthProvider.MICROSOFT]: mdiMicrosoft, + [AuthProvider.CREDENTIAL]: mdiEmailLock, + [AuthProvider.GITHUB]: mdiGithub +}; + +export const AuthProviderColor = { + [AuthProvider.MICROSOFT]: IfmColors.blue, + [AuthProvider.CREDENTIAL]: IfmColors.info, + [AuthProvider.GITHUB]: IfmColors.black +}; + export type User = { id: string; email: string; + name: string; firstName: string; lastName: string; role: Role; + authProviders?: AuthProvider[]; + banned?: boolean; + banReason?: string; + banExpires?: Date; createdAt: string; updatedAt: string; }; diff --git a/src/auth-client.ts b/src/auth-client.ts new file mode 100644 index 00000000..d6259d51 --- /dev/null +++ b/src/auth-client.ts @@ -0,0 +1,29 @@ +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 { + BACKEND_URL: string; +} +export const { BACKEND_URL } = 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: [ + adminClient(), + oneTimeTokenClient(), + inferAdditionalFields({ + user: { + firstName: { + type: 'string' + }, + lastName: { + type: 'string' + } + } + }) + ] +}); diff --git a/src/authConfig.ts b/src/authConfig.ts deleted file mode 100644 index 03e03b58..00000000 --- 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/Admin/ActionRequest/NavReloadRequest.tsx b/src/components/Admin/ActionRequest/NavReloadRequest.tsx index fd588004..5f054300 100644 --- a/src/components/Admin/ActionRequest/NavReloadRequest.tsx +++ b/src/components/Admin/ActionRequest/NavReloadRequest.tsx @@ -9,6 +9,7 @@ import { mdiLoading, mdiReloadAlert, mdiWebRefresh } from '@mdi/js'; interface Props { roomIds?: string[]; userIds?: string[]; + slim?: boolean; } const NavReloadRequest = observer((props: Props) => { @@ -30,9 +31,9 @@ const NavReloadRequest = observer((props: Props) => { return ( { spin={isLoading} onConfirm={() => { setIsLoading(true); - socketStore - .requestNavigation(props.roomIds || [], props.userIds || [], { type: 'nav-reload' }) - .finally(() => { - setIsLoading(false); - }); + socketStore.requestReload(props.roomIds || [], props.userIds || []).finally(() => { + setIsLoading(false); + }); }} /> ); diff --git a/src/components/Admin/AdminPanel/index.tsx b/src/components/Admin/AdminPanel/index.tsx index 0c6de1d8..3e2cef3d 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'); @@ -30,6 +31,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 00000000..506a825e --- /dev/null +++ b/src/components/Admin/CreateUser/index.tsx @@ -0,0 +1,112 @@ +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'; +import { mdiAccountPlus, mdiCheck } from '@mdi/js'; +import Alert from '@tdev-components/shared/Alert'; + +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(''); + 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)} + 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; + }} + /> + + ); +}); + +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 00000000..843b1813 --- /dev/null +++ b/src/components/Admin/CreateUser/styles.module.scss @@ -0,0 +1,5 @@ +.card { + max-width: 30em; + margin-left: auto; + margin-right: auto; +} diff --git a/src/components/Admin/EditUser/index.tsx b/src/components/Admin/EditUser/index.tsx new file mode 100644 index 00000000..41819501 --- /dev/null +++ b/src/components/Admin/EditUser/index.tsx @@ -0,0 +1,303 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { Role, RoleAccessLevel, RoleNames, User as UserProps } from '@tdev-api/user'; +import Card from '@tdev-components/shared/Card'; +import TextInput from '@tdev-components/shared/TextInput'; +import Button from '@tdev-components/shared/Button'; +import { authClient } from '@tdev/auth-client'; +import { useStore } from '@tdev-hooks/useStore'; +import { Confirm } from '@tdev-components/shared/Button/Confirm'; +import { mdiAccountCancel, mdiAccountCheck, mdiLink, mdiLinkOff, mdiLoading, mdiTrashCan } from '@mdi/js'; +import { SIZE_XS } from '@tdev-components/shared/iconSizes'; +import { action } from 'mobx'; +import Loader from '@tdev-components/Loader'; +import type User from '@tdev-models/User'; +import Alert from '@tdev-components/shared/Alert'; + +interface Props { + user: User; + close: () => void; +} + +type SpinState = + | 'deleting' + | 'linking' + | 'unlinking' + | 'change-pw' + | 'block-user' + | 'unblock-user' + | 'update-user'; + +const SPIN_TEXT = { + deleting: 'Löschen...', + linking: 'Verknüpfen...', + unlinking: 'Verknüpfung aufheben...', + 'change-pw': 'Passwort ändern...', + 'block-user': 'User blockieren...', + 'unblock-user': 'Blockierung aufheben...', + 'update-user': 'Speichern...' +}; + +const pwValidator = (pw: string) => (pw.length > 7 ? null : 'Passwort muss min. 8 Zeichen haben'); + +const EditUser = observer((props: Props) => { + const { user } = props; + const userStore = useStore('userStore'); + const adminStore = useStore('adminStore'); + const [spinState, setSpinState] = React.useState(null); + const [password, setPassword] = React.useState(''); + const [pwState, setPwState] = React.useState<'error' | 'success' | null>(null); + + const defaultName = React.useRef(`${user.firstName} ${user.lastName}`); + const hasDefaultName = React.useRef(user.name === defaultName.current); + const [name, setName] = React.useState(user.name); + const [firstName, setFirstName] = React.useState(user.firstName); + const [lastName, setLastName] = React.useState(user.lastName); + React.useEffect(() => { + if (!pwState) { + return; + } + const timeout = setTimeout(() => { + setPwState(null); + }, 5000); + return () => clearTimeout(timeout); + }, [pwState]); + return ( + + + ))} + +
+

User Blockieren

+ Verhindert das Einloggen des Users. + {user.banned ? ( + { + setSpinState('unblock-user'); + authClient.admin.unbanUser({ userId: user.id }).finally(() => { + setSpinState(null); + }); + }} + size={SIZE_XS} + /> + ) : ( + { + setSpinState('block-user'); + authClient.admin.banUser({ userId: user.id }).finally(() => { + setSpinState(null); + }); + }} + size={SIZE_XS} + /> + )} +
+
+ Eigenschaften}> + + + + + +

Account

+ + Ein Mail-Passwort Authentifizierungs hinterlegen. Nützlich um sich bspw. auf + Deploy-Previews anzumelden oder um jemandem temporät Zugriff auf den Account zu + geben. + + Das permanente Hinterlegen eines Passworts stellt ein Sicherheitsrisiko dar, + da bspw. keine 2FA nötig ist. + + + + } + > +
+
+ +
+ {user.hasEmailPasswordAuth ? ( +
+ {user.hasEmailPasswordAuth && ( + { + setSpinState('unlinking'); + adminStore.revokeUserPassword(user.id).finally(() => { + setSpinState(null); + }); + }} + disabled={!!spinState} + size={SIZE_XS} + confirmText="Wirklich entfernen?" + /> + )} + {pwState === 'error' && Passwort konnte nicht gesetzt werden.} + {pwState === 'success' && Passwort erfolgreich gesetzt.} +
+ { + setSpinState('deleting'); + authClient.admin.removeUser({ userId: user.id }).then( + action((res) => { + if (res.data?.success) { + userStore.removeFromStore(user.id); + props.close(); + } + }) + ); + }} + color="red" + confirmText="Wirklich löschen?" + disabled={!userStore.current?.isAdmin || user.id === userStore.current?.id} + /> + {!!spinState && } + + ); +}); + +export default EditUser; diff --git a/src/components/Admin/EditUser/styles.module.scss b/src/components/Admin/EditUser/styles.module.scss new file mode 100644 index 00000000..e7a04a49 --- /dev/null +++ b/src/components/Admin/EditUser/styles.module.scss @@ -0,0 +1,18 @@ +.editUser { + position: relative; + .body { + max-height: 80vh; + overflow-y: auto; + .delete { + justify-content: flex-end; + } + :global(.card) { + margin-bottom: 0.5em; + } + .password { + display: flex; + gap: 0.5em; + align-items: flex-end; + } + } +} diff --git a/src/components/Admin/UserTable/User.tsx b/src/components/Admin/UserTable/User.tsx index bcd6742f..ef55e552 100644 --- a/src/components/Admin/UserTable/User.tsx +++ b/src/components/Admin/UserTable/User.tsx @@ -6,9 +6,19 @@ import { observer } from 'mobx-react-lite'; import { default as UserModel } from '@tdev-models/User'; import CopyBadge from '@tdev-components/shared/CopyBadge'; import { formatDateTime } from '@tdev-models/helpers/date'; -import { Role, RoleAccessLevel, RoleNames } from '@tdev-api/user'; +import { AuthProviderColor, AuthProviderIcons, RoleColors, RoleNames } from '@tdev-api/user'; import { useStore } from '@tdev-hooks/useStore'; import LiveStatusIndicator from '@tdev-components/LiveStatusIndicator'; +import Icon from '@mdi/react'; +import { mdiAccountCancel, mdiAccountEdit, mdiCloudQuestion } from '@mdi/js'; +import { SIZE_S, SIZE_XS } from '@tdev-components/shared/iconSizes'; +import Button from '@tdev-components/shared/Button'; +import Popup from 'reactjs-popup'; +import EditUser from '../EditUser'; +import { PopupActions } from 'reactjs-popup/dist/types'; +import Badge from '@tdev-components/shared/Badge'; +import NavReloadRequest from '../ActionRequest/NavReloadRequest'; +import { IfmColors } from '@tdev-components/shared/Colors'; interface Props { user: UserModel; @@ -18,6 +28,7 @@ const UserTableRow = observer((props: Props) => { const { user } = props; const userStore = useStore('userStore'); const { current } = userStore; + const ref = React.useRef(null); if (!current) { return null; } @@ -29,38 +40,47 @@ const UserTableRow = observer((props: Props) => { {user.connectedClients > 0 && ( {user.connectedClients} )} + {user.banned && } {user.email} -
- {Object.values(Role).map((role, idx) => ( - - ))} + {RoleNames[user.role]} + + +
+ +
{user.firstName} {user.lastName} + + {user.authProviders.map((u, idx) => ( + + ))} + {formatDateTime(user.createdAt)} {formatDateTime(user.updatedAt)} - + {user.studentGroups.map((group, idx) => ( {group.name} @@ -68,7 +88,11 @@ const UserTableRow = observer((props: Props) => { ))} - + ); diff --git a/src/components/Admin/UserTable/index.tsx b/src/components/Admin/UserTable/index.tsx index dcd5d8dc..e3bf0288 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' @@ -120,6 +121,7 @@ const UserTable = observer((props: Props) => { onClick={() => setSortColumn('accessLevel')} /> +
); -} +}; +export default MdiSelector; diff --git a/src/components/Navbar/AccountSwitcher/index.tsx b/src/components/Navbar/AccountSwitcher/index.tsx index 4f16b7d6..ae7f6e93 100644 --- a/src/components/Navbar/AccountSwitcher/index.tsx +++ b/src/components/Navbar/AccountSwitcher/index.tsx @@ -20,6 +20,7 @@ interface SwitchToUserButtonProps { const SwitchToUserButton = observer(({ user, isInCurrentClass }: SwitchToUserButtonProps) => { const userStore = useStore('userStore'); + const pageStore = useStore('pageStore'); return (
@@ -29,7 +30,14 @@ const SwitchToUserButton = observer(({ user, isInCurrentClass }: SwitchToUserBut className={clsx(styles.userButton)} iconSide="left" active={userStore.viewedUserId === user.id} - color={isInCurrentClass ? 'primary' : 'secondary'} + color={ + userStore.current?.id !== user.id && + pageStore.current?.userIdsWithoutEditingState?.includes(user.id) + ? 'grey' + : isInCurrentClass + ? 'primary' + : 'secondary' + } title={`Inhalte anzeigen für ${user.firstName} ${user.lastName}`} onClick={() => userStore.switchUser(user.id)} > @@ -66,11 +74,11 @@ const AccountSwitcher = observer(() => { )} +