diff --git a/admin/.eslintrc.cjs b/admin/.eslintrc.cjs
new file mode 100644
index 000000000..d6c953795
--- /dev/null
+++ b/admin/.eslintrc.cjs
@@ -0,0 +1,18 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+}
diff --git a/admin/.gitignore b/admin/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/admin/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/admin/README.md b/admin/README.md
new file mode 100644
index 000000000..0d6babedd
--- /dev/null
+++ b/admin/README.md
@@ -0,0 +1,30 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
+
+- Configure the top-level `parserOptions` property like this:
+
+```js
+export default {
+ // other rules...
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ project: ['./tsconfig.json', './tsconfig.node.json'],
+ tsconfigRootDir: __dirname,
+ },
+}
+```
+
+- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
+- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
+- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
diff --git a/admin/index.html b/admin/index.html
new file mode 100644
index 000000000..daff73e73
--- /dev/null
+++ b/admin/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
+
diff --git a/admin/package.json b/admin/package.json
new file mode 100644
index 000000000..ceb203f84
--- /dev/null
+++ b/admin/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "admin",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.0.5",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.22.3",
+ "zustand": "^4.5.2"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.56",
+ "@types/react-dom": "^18.2.19",
+ "@typescript-eslint/eslint-plugin": "^7.0.2",
+ "@typescript-eslint/parser": "^7.0.2",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "eslint": "^8.56.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "socket.io-client": "^4.7.4",
+ "typescript": "^5.2.2",
+ "vite": "^5.1.4",
+ "vite-plugin-svgr": "^4.2.0"
+ }
+}
diff --git a/admin/public/fond.jpg b/admin/public/fond.jpg
new file mode 100644
index 000000000..81357c7bb
Binary files /dev/null and b/admin/public/fond.jpg differ
diff --git a/admin/src/App.css b/admin/src/App.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/admin/src/App.tsx b/admin/src/App.tsx
new file mode 100644
index 000000000..89ab3c389
--- /dev/null
+++ b/admin/src/App.tsx
@@ -0,0 +1,80 @@
+import {useEffect} from 'react'
+import './App.css'
+import {connect} from 'socket.io-client'
+import {isJSONClean} from './utils/utils.ts'
+import {NavLink, Outlet} from "react-router-dom";
+import {useStore} from "./store/store.ts";
+import {LoadingScreen} from "./utils/LoadingScreen.tsx";
+
+export const App = ()=> {
+ const setSettings = useStore(state => state.setSettings);
+
+ useEffect(() => {
+ useStore.getState().setShowLoading(true);
+ const settingSocket = connect('http://localhost:9001/settings', {
+ transports: ['websocket'],
+ });
+
+ const pluginsSocket = connect('http://localhost:9001/pluginfw/installer', {
+ transports: ['websocket'],
+ })
+
+ pluginsSocket.on('connect', () => {
+ useStore.getState().setPluginsSocket(pluginsSocket);
+ });
+
+
+ settingSocket.on('connect', () => {
+ useStore.getState().setSettingsSocket(settingSocket);
+ settingSocket.emit('load');
+ console.log('connected');
+ });
+ settingSocket.on('disconnect', (reason) => {
+ // The settingSocket.io client will automatically try to reconnect for all reasons other than "io
+ // server disconnect".
+ if (reason === 'io server disconnect') settingSocket.connect();
+ });
+
+ settingSocket.on('settings', (settings) => {
+ /* Check whether the settings.json is authorized to be viewed */
+ if (settings.results === 'NOT_ALLOWED') {
+ console.log('Not allowed to view settings.json')
+ return;
+ }
+
+ /* Check to make sure the JSON is clean before proceeding */
+ if (isJSONClean(settings.results)) {
+ setSettings(settings.results);
+ } else {
+ alert('Invalid JSON');
+ }
+ useStore.getState().setShowLoading(false);
+ });
+
+ settingSocket.on('saveprogress', (status)=>{
+ console.log(status)
+ })
+
+ return () => {
+ settingSocket.disconnect();
+ pluginsSocket.disconnect()
+ }
+ }, []);
+
+ return
+
+
+
Etherpad
+
+ - Home
+ - Einstellungen
+ - Hilfe
+
+
+
+
+
+
+}
+
+export default App
diff --git a/admin/src/assets/react.svg b/admin/src/assets/react.svg
new file mode 100644
index 000000000..6c87de9bb
--- /dev/null
+++ b/admin/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/admin/src/index.css b/admin/src/index.css
new file mode 100644
index 000000000..c084f1405
--- /dev/null
+++ b/admin/src/index.css
@@ -0,0 +1,359 @@
+:root {
+ --etherpad-color: #0f775b;
+}
+
+
+
+html, body, #root {
+ box-sizing: border-box;
+ height: 100%;
+
+}
+
+*, *:before, *:after {
+ box-sizing: inherit;
+}
+
+body {
+ overflow: hidden;
+}
+
+body {
+ margin: 0;
+ color: #333;
+ font: 14px helvetica, sans-serif;
+ background: #eee;
+}
+
+div.menu {
+ height: 100%;
+ padding: 15px;
+ width: 220px;
+ border-right: 1px solid #ccc;
+ position: fixed;
+}
+
+div.menu ul {
+ padding: 0;
+}
+
+div.menu li {
+ list-style: none;
+ margin-left: 3px;
+ line-height: 3;
+ border-top: 1px solid #ccc;
+}
+
+div.menu li:last-child {
+ border-bottom: 1px solid #ccc;
+}
+
+div.innerwrapper {
+ padding: 15px;
+ padding-left: 265px;
+}
+
+div.innerwrapper-err {
+ padding: 15px;
+ padding-left: 265px;
+ display: none;
+}
+
+#wrapper {
+ background: none repeat scroll 0px 0px #FFFFFF;
+ box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2);
+ margin: auto;
+ max-width: 1150px;
+ min-height: 101%;/*always display a scrollbar*/
+
+}
+
+h1 {
+ font-size: 29px;
+}
+
+h2 {
+ font-size: 24px;
+}
+
+.separator {
+ margin: 10px 0;
+ height: 1px;
+ background: #aaa;
+ background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
+ background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
+ background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
+ background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
+}
+
+form {
+ margin-bottom: 0;
+}
+
+#inner {
+ width: 300px;
+ margin: 0 auto;
+}
+
+input {
+ font-weight: bold;
+ font-size: 15px;
+}
+
+
+.sort {
+ cursor: pointer;
+}
+.sort:after {
+ content: '▲▼'
+}
+.sort.up:after {
+ content:'▲'
+}
+.sort.down:after {
+ content:'▼'
+}
+
+table {
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ border-spacing: 0;
+ width: 100%;
+ margin: 20px 0;
+ position:relative; /* Allows us to position the loading indicator relative to the table */
+}
+
+table thead tr {
+ background: #eee;
+}
+
+td, th {
+ padding: 5px;
+}
+
+.template {
+ display: none;
+}
+
+#installed-plugins td>div {
+ position: relative;/* Allows us to position the loading indicator relative to this row */
+ display: inline-block; /*make this fill the whole cell*/
+ width:100%;
+}
+
+.messages {
+ height: 5em;
+}
+.messages * {
+ display: none;
+ text-align: center;
+}
+.messages .fetching {
+ display: block;
+}
+
+.progress {
+ position: absolute;
+ top: 0; left: 0; bottom:0; right:0;
+ padding: auto;
+
+ background: rgb(255,255,255);
+ display: none;
+}
+
+#search-progress.progress {
+ padding-top: 20%;
+ background: rgba(255,255,255,0.3);
+}
+
+.progress * {
+ display: block;
+ margin: 0 auto;
+ text-align: center;
+ color: #666;
+}
+
+.settings {
+ outline: none;
+ width: 100%;
+ min-height: 80vh;
+ resize: none;
+}
+
+#response {
+ display: inline;
+}
+
+a:link, a:visited, a:hover, a:focus {
+ color: #333333;
+ text-decoration: none;
+}
+
+a:focus, a:hover {
+ text-decoration: underline;
+}
+
+.installed-results a:link,
+.search-results a:link,
+.installed-results a:visited,
+.search-results a:visited,
+.installed-results a:hover,
+.search-results a:hover,
+.installed-results a:focus,
+.search-results a:focus {
+ text-decoration: underline;
+}
+
+.installed-results a:focus,
+.search-results a:focus,
+.installed-results a:hover,
+.search-results a:hover {
+ text-decoration: none;
+}
+
+pre {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+@media (max-width: 800px) {
+ div.innerwrapper {
+ padding: 0 15px 15px 15px;
+ }
+
+ div.menu {
+ padding: 1px 15px 0 15px;
+ position: static;
+ height: auto;
+ border-right: none;
+ width: auto;
+ }
+
+ table {
+ border: none;
+ }
+
+ table, thead, tbody, td, tr {
+ display: block;
+ }
+
+ thead tr {
+ display: none;
+ }
+
+ tr {
+ border: 1px solid #ccc;
+ margin-bottom: 5px;
+ border-radius: 3px;
+ }
+
+ td {
+ border: none;
+ border-bottom: 1px solid #eee;
+ position: relative;
+ padding-left: 50%;
+ white-space: normal;
+ text-align: left;
+ }
+
+ td.name {
+ word-wrap: break-word;
+ }
+
+ td:before {
+ position: absolute;
+ top: 6px;
+ left: 6px;
+ text-align: left;
+ padding-right: 10px;
+ white-space: nowrap;
+ font-weight: bold;
+ content: attr(data-label);
+ }
+
+ td:last-child {
+ border-bottom: none;
+ }
+
+ table input[type="button"] {
+ float: none;
+ }
+}
+
+
+.settings-button-bar {
+ margin-top: 10px;
+ display: flex;
+ gap: 10px;
+}
+
+.login-background {
+ background-image: url("/fond.jpg");
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ background-color: #f0f0f0;
+}
+
+
+.login-textinput {
+ width: 100%;
+ padding: 10px;
+ background-color: #fffacc;
+ border-radius: 5px;
+ border: 1px solid #ccc;
+ margin-bottom: 10px;
+}
+
+.login-box {
+ width: 20%;
+ padding: 20px;
+ border-radius: 5px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ background-color: #fff;
+}
+
+.login-inner-box{
+ position: relative;
+ padding: 20px;
+}
+
+.login-title {
+ color: var(--etherpad-color);
+ font-size: 2em;
+}
+
+.login-button {
+ padding: 10px;
+ background-color: var(--etherpad-color);
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ width: 100%;
+ height: 40px;
+}
+
+.dialog-overlay {
+ position: fixed;
+ inset: 0;
+ background-color: white;
+ z-index: 100;
+}
+
+
+.dialog-content {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ padding: 20px;
+ z-index: 101;
+}
+
+.dialog-title {
+ color: var(--etherpad-color);
+ font-size: 2em;
+ margin-bottom: 20px;
+}
diff --git a/admin/src/main.tsx b/admin/src/main.tsx
new file mode 100644
index 000000000..9a9dc25a0
--- /dev/null
+++ b/admin/src/main.tsx
@@ -0,0 +1,28 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.tsx'
+import './index.css'
+import {createBrowserRouter, createRoutesFromElements, Route, RouterProvider} from "react-router-dom";
+import {HomePage} from "./pages/HomePage.tsx";
+import {SettingsPage} from "./pages/SettingsPage.tsx";
+import {LoginScreen} from "./pages/LoginScreen.tsx";
+import {HelpPage} from "./pages/HelpPage.tsx";
+
+const router = createBrowserRouter(createRoutesFromElements(
+ <>}>
+ }/>
+ }/>
+ }/>
+ }/>
+
+ }/>
+ >
+))
+
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+ ,
+)
diff --git a/admin/src/pages/HelpPage.tsx b/admin/src/pages/HelpPage.tsx
new file mode 100644
index 000000000..10e21277d
--- /dev/null
+++ b/admin/src/pages/HelpPage.tsx
@@ -0,0 +1,5 @@
+export const HelpPage = () => {
+ return
+
Help Page
+
+}
diff --git a/admin/src/pages/HomePage.tsx b/admin/src/pages/HomePage.tsx
new file mode 100644
index 000000000..a8de85d77
--- /dev/null
+++ b/admin/src/pages/HomePage.tsx
@@ -0,0 +1,48 @@
+import {useStore} from "../store/store.ts";
+import {useEffect, useState} from "react";
+import {PluginDef} from "./Plugin.ts";
+
+export const HomePage = () => {
+ const pluginsSocket = useStore(state=>state.pluginsSocket)
+ const [limit, setLimit] = useState(20)
+ const [offset, setOffset] = useState(0)
+ const [plugins,setPlugins] = useState([])
+
+ useEffect(() => {
+ pluginsSocket?.emit('search', {
+ searchTerm: '',
+ offset: offset,
+ limit: limit,
+ sortBy: 'name',
+ sortDir: 'asc'
+ })
+ setOffset(offset+limit)
+
+ pluginsSocket!.on('results:search', (data) => {
+ setPlugins(data.results)
+ })
+ }, []);
+
+ return
+
Home Page
+
+
+
+ Name |
+ Description |
+ Action |
+
+
+
+ {plugins.map((plugin, index) => {
+ return
+ {plugin.name} |
+ {plugin.description} |
+ test |
+
+ })}
+
+
+
+
+}
diff --git a/admin/src/pages/LoginScreen.tsx b/admin/src/pages/LoginScreen.tsx
new file mode 100644
index 000000000..2c7dc62b3
--- /dev/null
+++ b/admin/src/pages/LoginScreen.tsx
@@ -0,0 +1,33 @@
+import {useState} from "react";
+
+export const LoginScreen = ()=>{
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+
+ const login = ()=>{
+ fetch('/api/auth', {
+ method: 'GET',
+ headers:{
+ Authorization: `Basic ${btoa(`${username}:${password}`)}`
+ }
+ }).then(r=>{
+ console.log(r.status)
+ }).catch(e=>{
+ console.error(e)
+ })
+ }
+
+ return
+}
diff --git a/admin/src/pages/Plugin.ts b/admin/src/pages/Plugin.ts
new file mode 100644
index 000000000..7cdb4a096
--- /dev/null
+++ b/admin/src/pages/Plugin.ts
@@ -0,0 +1,7 @@
+export type PluginDef = {
+ name: string,
+ description: string,
+ version: string,
+ time: string,
+ official: boolean,
+}
diff --git a/admin/src/pages/SettingsPage.tsx b/admin/src/pages/SettingsPage.tsx
new file mode 100644
index 000000000..00da85079
--- /dev/null
+++ b/admin/src/pages/SettingsPage.tsx
@@ -0,0 +1,27 @@
+import {useStore} from "../store/store.ts";
+import {isJSONClean} from "../utils/utils.ts";
+
+export const SettingsPage = ()=>{
+ const settingsSocket = useStore(state=>state.settingsSocket)
+
+ const settings = useStore(state=>state.settings)
+ return
+
Derzeitige Konfiguration
+
+}
diff --git a/admin/src/store/store.ts b/admin/src/store/store.ts
new file mode 100644
index 000000000..12b4629be
--- /dev/null
+++ b/admin/src/store/store.ts
@@ -0,0 +1,25 @@
+import {create} from "zustand";
+import {Socket} from "socket.io-client";
+
+type StoreState = {
+ settings: string|undefined,
+ setSettings: (settings: string) => void,
+ settingsSocket: Socket|undefined,
+ setSettingsSocket: (socket: Socket) => void,
+ showLoading: boolean,
+ setShowLoading: (show: boolean) => void,
+ setPluginsSocket: (socket: Socket) => void
+ pluginsSocket: Socket|undefined
+}
+
+
+export const useStore = create()((set) => ({
+ settings: undefined,
+ setSettings: (settings: string) => set({settings}),
+ settingsSocket: undefined,
+ setSettingsSocket: (socket: Socket) => set({settingsSocket: socket}),
+ showLoading: false,
+ setShowLoading: (show: boolean) => set({showLoading: show}),
+ pluginsSocket: undefined,
+ setPluginsSocket: (socket: Socket) => set({pluginsSocket: socket})
+}));
diff --git a/admin/src/utils/LoadingScreen.tsx b/admin/src/utils/LoadingScreen.tsx
new file mode 100644
index 000000000..a234dfc38
--- /dev/null
+++ b/admin/src/utils/LoadingScreen.tsx
@@ -0,0 +1,19 @@
+import {useStore} from "../store/store.ts";
+import * as Dialog from '@radix-ui/react-dialog';
+import ReactComponent from './brand.svg?react';
+export const LoadingScreen = ()=>{
+ const showLoading = useStore(state => state.showLoading)
+
+ return
+
+
+
+
+
+
+}
diff --git a/admin/src/utils/brand.svg b/admin/src/utils/brand.svg
new file mode 100644
index 000000000..2acd73c77
--- /dev/null
+++ b/admin/src/utils/brand.svg
@@ -0,0 +1,65 @@
+
+
diff --git a/admin/src/utils/utils.ts b/admin/src/utils/utils.ts
new file mode 100644
index 000000000..2e8f52a05
--- /dev/null
+++ b/admin/src/utils/utils.ts
@@ -0,0 +1,64 @@
+const minify = (json: string)=>{
+
+ let tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
+ in_string = false,
+ in_multiline_comment = false,
+ in_singleline_comment = false,
+ tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
+ ;
+
+ tokenizer.lastIndex = 0;
+
+ while (tmp = tokenizer.exec(json)) {
+ lc = RegExp.leftContext;
+ rc = RegExp.rightContext;
+ if (!in_multiline_comment && !in_singleline_comment) {
+ tmp2 = lc.substring(from);
+ if (!in_string) {
+ tmp2 = tmp2.replace(/(\n|\r|\s)*/g,"");
+ }
+ new_str[ns++] = tmp2;
+ }
+ from = tokenizer.lastIndex;
+
+ if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
+ tmp2 = lc.match(/(\\)*$/);
+ if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string
+ in_string = !in_string;
+ }
+ from--; // include " character in next catch
+ rc = json.substring(from);
+ }
+ else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) {
+ in_multiline_comment = true;
+ }
+ else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) {
+ in_multiline_comment = false;
+ }
+ else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) {
+ in_singleline_comment = true;
+ }
+ else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) {
+ in_singleline_comment = false;
+ }
+ else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) {
+ new_str[ns++] = tmp[0];
+ }
+ }
+ new_str[ns++] = rc;
+ return new_str.join("");
+}
+
+
+
+
+export const isJSONClean = (data: string) => {
+ let cleanSettings = minify(data);
+ // this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
+ cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
+ try {
+ return typeof JSON.parse(cleanSettings) === 'object';
+ } catch (e) {
+ return false; // the JSON failed to be parsed
+ }
+};
diff --git a/admin/src/vite-env.d.ts b/admin/src/vite-env.d.ts
new file mode 100644
index 000000000..b1f45c786
--- /dev/null
+++ b/admin/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/admin/tsconfig.json b/admin/tsconfig.json
new file mode 100644
index 000000000..a7fc6fbf2
--- /dev/null
+++ b/admin/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/admin/tsconfig.node.json b/admin/tsconfig.node.json
new file mode 100644
index 000000000..97ede7ee6
--- /dev/null
+++ b/admin/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/admin/vite.config.ts b/admin/vite.config.ts
new file mode 100644
index 000000000..db7235e3a
--- /dev/null
+++ b/admin/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+import svgr from 'vite-plugin-svgr'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react(), svgr()],
+ server:{
+ proxy: {
+ '/socket.io/': {
+ target: 'http://localhost:9001',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, '')
+ },
+ '/api/auth': {
+ target: 'http://localhost:9001',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/admin-prox/, '/admin/')
+ }
+ }
+ }
+})
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ced0baca4..3d3e285e0 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,2 +1,3 @@
packages:
- - src
\ No newline at end of file
+ - src
+ - admin
diff --git a/src/node/hooks/express/adminsettings.ts b/src/node/hooks/express/adminsettings.ts
index 900bfd479..1b3d42644 100644
--- a/src/node/hooks/express/adminsettings.ts
+++ b/src/node/hooks/express/adminsettings.ts
@@ -18,9 +18,12 @@ exports.expressCreateServer = (hookName:string, {app}:any) => {
exports.socketio = (hookName:string, {io}:any) => {
io.of('/settings').on('connection', (socket: any ) => {
+ console.log('Admin connected to /admin/settings')
// @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
+ console.log('isAdmin', isAdmin)
if (!isAdmin) return;
+ console.log('Admin authenticated')
socket.on('load', async (query:string):Promise => {
let data;
@@ -38,6 +41,7 @@ exports.socketio = (hookName:string, {io}:any) => {
});
socket.on('saveSettings', async (newSettings:string) => {
+ console.log('Admin request to save settings through a socket on /admin/settings');
await fsp.writeFile(settings.settingsFilename, newSettings);
socket.emit('saveprogress', 'saved');
});
diff --git a/src/node/hooks/express/socketio.ts b/src/node/hooks/express/socketio.ts
index 961e2cfd3..e64a16847 100644
--- a/src/node/hooks/express/socketio.ts
+++ b/src/node/hooks/express/socketio.ts
@@ -76,10 +76,12 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
})
- io.on('connection', (socket:any) => {
+
+ const handleConnection = (socket:Socket) => {
sockets.add(socket);
socketsEvents.emit('updated');
// https://socket.io/docs/v3/faq/index.html
+ // @ts-ignore
const session = socket.request.session;
session.connections++;
session.save();
@@ -87,15 +89,9 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
sockets.delete(socket);
socketsEvents.emit('updated');
});
- });
+ }
- io.use(socketSessionMiddleware(args));
-
- // Temporary workaround so all clients go through middleware and handle connection
- io.of('/pluginfw/installer').use(socketSessionMiddleware(args))
- io.of('/settings').use(socketSessionMiddleware(args))
-
- io.use((socket:any, next:Function) => {
+ const renewSession = (socket:any, next:Function) => {
socket.conn.on('packet', (packet:string) => {
// Tell express-session that the session is still active. The session store can use these
// touch events to defer automatic session cleanup, and if express-session is configured with
@@ -106,7 +102,24 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
if (socket.request.session != null) socket.request.session.touch();
});
next();
- });
+ }
+
+
+ io.on('connection', handleConnection);
+
+ io.use(socketSessionMiddleware(args));
+
+ // Temporary workaround so all clients go through middleware and handle connection
+ io.of('/pluginfw/installer')
+ .on('connection',handleConnection)
+ .use(socketSessionMiddleware(args))
+ .use(renewSession)
+ io.of('/settings')
+ .on('connection',handleConnection)
+ .use(socketSessionMiddleware(args))
+ .use(renewSession)
+
+ io.use(renewSession);
// var socketIOLogger = log4js.getLogger("socket.io");
// Debug logging now has to be set at an environment level, this is stupid.