mirror of
https://github.com/UA-Fediland/synapse-admin.git
synced 2024-10-29 19:04:52 +00:00
Compare commits
33 commits
7deb9bcf7e
...
b112689b8c
Author | SHA1 | Date | |
---|---|---|---|
|
b112689b8c | ||
|
ac3b40b188 | ||
|
a490b7bc85 | ||
|
e094047388 | ||
|
5d1e43611c | ||
|
630e809e78 | ||
|
264e0b5ec6 | ||
|
1837733e07 | ||
|
77be88402f | ||
|
f4ea63c8f4 | ||
|
2e2085cdfe | ||
|
4b1277f653 | ||
|
ef3836313c | ||
|
428cb56fdf | ||
|
c6f9dbec18 | ||
|
531d8f2d7f | ||
|
0986d95de2 | ||
|
5f1dfc95c7 | ||
|
e666c9c7bd | ||
|
441f7749a2 | ||
|
028babc885 | ||
|
400e9aa416 | ||
|
6d9abe85b0 | ||
|
df1fbbc16b | ||
|
6bdeadcc3e | ||
|
881760c8d8 | ||
|
03c4955ef7 | ||
|
c9cb9aa9e0 | ||
|
25020c2d5b | ||
|
1acffdb618 | ||
|
0b4f3a60c0 | ||
|
33d29e01b1 | ||
|
a2e47cb793 |
55 changed files with 11303 additions and 10880 deletions
|
@ -1,10 +1,13 @@
|
||||||
# Exclude a bunch of stuff which can make the build context a larger than it needs to be
|
# Exclude a bunch of stuff which can make the build context a larger than it needs to be
|
||||||
tests/
|
tests/
|
||||||
build/
|
build/
|
||||||
|
dist/
|
||||||
lib/
|
lib/
|
||||||
node_modules/
|
node_modules/
|
||||||
electron_app/
|
electron_app/
|
||||||
karma-reports/
|
karma-reports/
|
||||||
|
.pnp.cjs
|
||||||
|
.pnp.loader.mjs
|
||||||
.idea/
|
.idea/
|
||||||
.tmp/
|
.tmp/
|
||||||
config.json*
|
config.json*
|
||||||
|
|
5
.env
5
.env
|
@ -1,5 +0,0 @@
|
||||||
# This setting allows to fix the homeserver.
|
|
||||||
# If you set this setting, the user will not be able to select
|
|
||||||
# the server and have to use synapse-admin with this server.
|
|
||||||
|
|
||||||
#REACT_APP_SERVER=https://yourmatrixserver.example.com
|
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
yarn*.cjs binary
|
46
.github/workflows/docker-release.yml
vendored
46
.github/workflows/docker-release.yml
vendored
|
@ -1,4 +1,5 @@
|
||||||
name: Create docker image(s) and push to docker hub
|
name: Create docker image(s) and push to docker hub and ghcr.io
|
||||||
|
# see https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub-and-github-packages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -13,39 +14,50 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
|
name: Push Docker image to multiple registries
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Calculate docker image tag
|
|
||||||
id: set-tag
|
- name: Login to GHCR
|
||||||
run: |
|
uses: docker/login-action@v3
|
||||||
case "${GITHUB_REF}" in
|
with:
|
||||||
refs/heads/master|refs/heads/main)
|
registry: ghcr.io
|
||||||
tag=latest
|
username: ${{ github.repository_owner }}
|
||||||
;;
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
refs/tags/*)
|
|
||||||
tag=${GITHUB_REF#refs/tags/}
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
;;
|
id: meta
|
||||||
*)
|
uses: docker/metadata-action@v5
|
||||||
tag=${GITHUB_SHA}
|
with:
|
||||||
;;
|
images: |
|
||||||
esac
|
awesometechnologies/synapse-admin
|
||||||
echo "::set-output name=tag::$tag"
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
- name: Build and Push Tag
|
- name: Build and Push Tag
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
11
.github/workflows/edge_ghpage.yml
vendored
11
.github/workflows/edge_ghpage.yml
vendored
|
@ -11,16 +11,19 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout 🛎️
|
- name: Checkout 🛎️
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 100
|
||||||
|
fetch-tags: true
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "20"
|
||||||
- name: Install and Build 🔧
|
- name: Install and Build 🔧
|
||||||
run: |
|
run: |
|
||||||
yarn install --immutable
|
yarn install --immutable
|
||||||
yarn build
|
yarn build --base=/synapse-admin
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.5.0
|
uses: JamesIves/github-pages-deploy-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
branch: gh-pages
|
branch: gh-pages
|
||||||
folder: build
|
folder: dist
|
||||||
|
|
7
.github/workflows/github-release.yml
vendored
7
.github/workflows/github-release.yml
vendored
|
@ -16,15 +16,14 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "20"
|
||||||
- run: yarn install --immutable
|
- run: yarn install --immutable
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- run: |
|
- run: |
|
||||||
version=`git describe --dirty --tags || echo unknown`
|
version=`git describe --dirty --tags || echo unknown`
|
||||||
mkdir -p dist
|
cp -r dist synapse-admin-$version
|
||||||
cp -r build synapse-admin-$version
|
|
||||||
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
||||||
- uses: softprops/action-gh-release@3198ee18f814cdf787321b4a32a26ddbf37acc52
|
- uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564
|
||||||
with:
|
with:
|
||||||
files: dist/*.tar.gz
|
files: dist/*.tar.gz
|
||||||
env:
|
env:
|
||||||
|
|
51
.github/workflows/test-docker-image.yml
vendored
51
.github/workflows/test-docker-image.yml
vendored
|
@ -1,51 +0,0 @@
|
||||||
name: Test docker image creation
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
# Sequence of patterns matched against refs/heads
|
|
||||||
# prettier-ignore
|
|
||||||
branches:
|
|
||||||
# Push events on branch fix_docker_cd
|
|
||||||
- fix_docker_cd
|
|
||||||
# Sequence of patterns matched against refs/tags
|
|
||||||
tags:
|
|
||||||
- '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Calculate docker image tag
|
|
||||||
id: set-tag
|
|
||||||
run: |
|
|
||||||
case "${GITHUB_REF}" in
|
|
||||||
refs/heads/master|refs/heads/main)
|
|
||||||
tag=latest
|
|
||||||
;;
|
|
||||||
refs/tags/*)
|
|
||||||
tag=${GITHUB_REF#refs/tags/}
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
tag=${GITHUB_SHA}
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
echo "::set-output name=tag::$tag"
|
|
||||||
- name: Build and Push Tag
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: false
|
|
||||||
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
208
.gitignore
vendored
208
.gitignore
vendored
|
@ -1,23 +1,193 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Created by https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node,yarn,react,visualstudiocode
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### react ###
|
||||||
|
.DS_*
|
||||||
|
**/*.backup.*
|
||||||
|
**/*.back.*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
*.sublime*
|
||||||
|
|
||||||
|
psd
|
||||||
|
thumb
|
||||||
|
sketch
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
### yarn ###
|
||||||
|
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
|
||||||
|
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# if you are NOT using Zero-installs, then:
|
||||||
|
# comment the following lines
|
||||||
|
!.yarn/cache
|
||||||
|
|
||||||
|
# and uncomment the following lines
|
||||||
|
# .pnp.*
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode
|
||||||
|
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.yarn
|
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"arcanis.vscode-zipfs",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.yarn": true,
|
||||||
|
"**/.pnp.*": true
|
||||||
|
},
|
||||||
|
"eslint.nodePath": ".yarn/sdks",
|
||||||
|
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
|
||||||
|
"typescript.tsdk": ".yarn/sdks/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
|
}
|
BIN
.yarn/releases/yarn-4.1.1.cjs
vendored
Executable file
BIN
.yarn/releases/yarn-4.1.1.cjs
vendored
Executable file
Binary file not shown.
20
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
20
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require eslint/bin/eslint.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real eslint/bin/eslint.js your application uses
|
||||||
|
module.exports = absRequire(`eslint/bin/eslint.js`);
|
20
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
20
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require eslint
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real eslint your application uses
|
||||||
|
module.exports = absRequire(`eslint`);
|
20
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
20
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require eslint/use-at-your-own-risk
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real eslint/use-at-your-own-risk your application uses
|
||||||
|
module.exports = absRequire(`eslint/use-at-your-own-risk`);
|
14
.yarn/sdks/eslint/package.json
vendored
Normal file
14
.yarn/sdks/eslint/package.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "eslint",
|
||||||
|
"version": "8.57.0-sdk",
|
||||||
|
"main": "./lib/api.js",
|
||||||
|
"type": "commonjs",
|
||||||
|
"bin": {
|
||||||
|
"eslint": "./bin/eslint.js"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": "./lib/api.js",
|
||||||
|
"./use-at-your-own-risk": "./lib/unsupported-api.js"
|
||||||
|
}
|
||||||
|
}
|
5
.yarn/sdks/integrations.yml
vendored
Normal file
5
.yarn/sdks/integrations.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# This file is automatically generated by @yarnpkg/sdks.
|
||||||
|
# Manual changes might be lost!
|
||||||
|
|
||||||
|
integrations:
|
||||||
|
- vscode
|
20
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
20
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require prettier/bin/prettier.cjs
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real prettier/bin/prettier.cjs your application uses
|
||||||
|
module.exports = absRequire(`prettier/bin/prettier.cjs`);
|
20
.yarn/sdks/prettier/index.cjs
vendored
Normal file
20
.yarn/sdks/prettier/index.cjs
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require prettier
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real prettier your application uses
|
||||||
|
module.exports = absRequire(`prettier`);
|
7
.yarn/sdks/prettier/package.json
vendored
Normal file
7
.yarn/sdks/prettier/package.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "prettier",
|
||||||
|
"version": "3.2.5-sdk",
|
||||||
|
"main": "./index.cjs",
|
||||||
|
"type": "commonjs",
|
||||||
|
"bin": "./bin/prettier.cjs"
|
||||||
|
}
|
20
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
20
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/bin/tsc
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/bin/tsc your application uses
|
||||||
|
module.exports = absRequire(`typescript/bin/tsc`);
|
20
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
20
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/bin/tsserver
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/bin/tsserver your application uses
|
||||||
|
module.exports = absRequire(`typescript/bin/tsserver`);
|
20
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
20
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsc.js your application uses
|
||||||
|
module.exports = absRequire(`typescript/lib/tsc.js`);
|
225
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
225
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const moduleWrapper = tsserver => {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
return tsserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {isAbsolute} = require(`path`);
|
||||||
|
const pnpApi = require(`pnpapi`);
|
||||||
|
|
||||||
|
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||||
|
const isPortal = str => str.startsWith("portal:/");
|
||||||
|
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||||
|
|
||||||
|
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||||
|
return `${locator.name}@${locator.reference}`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||||
|
// doesn't understand. This layer makes sure to remove the protocol
|
||||||
|
// before forwarding it to TS, and to add it back on all returned paths.
|
||||||
|
|
||||||
|
function toEditorPath(str) {
|
||||||
|
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||||
|
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||||
|
// We also take the opportunity to turn virtual paths into physical ones;
|
||||||
|
// this makes it much easier to work with workspaces that list peer
|
||||||
|
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||||
|
// file instances instead of the real ones.
|
||||||
|
//
|
||||||
|
// We only do this to modules owned by the the dependency tree roots.
|
||||||
|
// This avoids breaking the resolution when jumping inside a vendor
|
||||||
|
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||||
|
// errors on react).
|
||||||
|
//
|
||||||
|
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||||
|
if (resolved) {
|
||||||
|
const locator = pnpApi.findPackageLocator(resolved);
|
||||||
|
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||||
|
str = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str = normalize(str);
|
||||||
|
|
||||||
|
if (str.match(/\.zip\//)) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||||
|
// VSCode only adds it automatically for supported schemes,
|
||||||
|
// so we have to do it manually for the `zip` scheme.
|
||||||
|
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||||
|
//
|
||||||
|
// 2021-10-08: VSCode changed the format in 1.61.
|
||||||
|
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-04-06: VSCode changed the format in 1.66.
|
||||||
|
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-05-06: VSCode changed the format in 1.68
|
||||||
|
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
case `vscode <1.61`: {
|
||||||
|
str = `^zip:${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.66`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.68`: {
|
||||||
|
str = `^/zip${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// To make "go to definition" work,
|
||||||
|
// We have to resolve the actual file system path from virtual path
|
||||||
|
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = resolve(`zipfile:${str}`);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||||
|
// We have to resolve the actual file system path from virtual path,
|
||||||
|
// everything else is up to neovim
|
||||||
|
case `neovim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = `zipfile://${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
str = `zip:${str}`;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromEditorPath(str) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||||
|
// So in order to convert it back, we use .* to match all the thing
|
||||||
|
// before `zipfile:`
|
||||||
|
return process.platform === `win32`
|
||||||
|
? str.replace(/^.*zipfile:\//, ``)
|
||||||
|
: str.replace(/^.*zipfile:/, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `neovim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||||
|
return str.replace(/^zipfile:\/\//, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`:
|
||||||
|
default: {
|
||||||
|
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force enable 'allowLocalPluginLoads'
|
||||||
|
// TypeScript tries to resolve plugins using a path relative to itself
|
||||||
|
// which doesn't work when using the global cache
|
||||||
|
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||||
|
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||||
|
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||||
|
// https://github.com/microsoft/vscode/issues/45856
|
||||||
|
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||||
|
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||||
|
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||||
|
this.projectService.allowLocalPluginLoads = true;
|
||||||
|
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// And here is the point where we hijack the VSCode <-> TS communications
|
||||||
|
// by adding ourselves in the middle. We locate everything that looks
|
||||||
|
// like an absolute path of ours and normalize it.
|
||||||
|
|
||||||
|
const Session = tsserver.server.Session;
|
||||||
|
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||||
|
let hostInfo = `unknown`;
|
||||||
|
|
||||||
|
Object.assign(Session.prototype, {
|
||||||
|
onMessage(/** @type {string | object} */ message) {
|
||||||
|
const isStringMessage = typeof message === 'string';
|
||||||
|
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedMessage != null &&
|
||||||
|
typeof parsedMessage === `object` &&
|
||||||
|
parsedMessage.arguments &&
|
||||||
|
typeof parsedMessage.arguments.hostInfo === `string`
|
||||||
|
) {
|
||||||
|
hostInfo = parsedMessage.arguments.hostInfo;
|
||||||
|
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||||
|
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||||
|
// The RegExp from https://semver.org/ but without the caret at the start
|
||||||
|
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
|
) ?? []).map(Number)
|
||||||
|
|
||||||
|
if (major === 1) {
|
||||||
|
if (minor < 61) {
|
||||||
|
hostInfo += ` <1.61`;
|
||||||
|
} else if (minor < 66) {
|
||||||
|
hostInfo += ` <1.66`;
|
||||||
|
} else if (minor < 68) {
|
||||||
|
hostInfo += ` <1.68`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||||
|
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return originalOnMessage.call(
|
||||||
|
this,
|
||||||
|
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
send(/** @type {any} */ msg) {
|
||||||
|
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||||
|
return typeof value === `string` ? toEditorPath(value) : value;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tsserver;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||||
|
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
225
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
225
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const moduleWrapper = tsserver => {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
return tsserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {isAbsolute} = require(`path`);
|
||||||
|
const pnpApi = require(`pnpapi`);
|
||||||
|
|
||||||
|
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||||
|
const isPortal = str => str.startsWith("portal:/");
|
||||||
|
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||||
|
|
||||||
|
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||||
|
return `${locator.name}@${locator.reference}`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||||
|
// doesn't understand. This layer makes sure to remove the protocol
|
||||||
|
// before forwarding it to TS, and to add it back on all returned paths.
|
||||||
|
|
||||||
|
function toEditorPath(str) {
|
||||||
|
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||||
|
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||||
|
// We also take the opportunity to turn virtual paths into physical ones;
|
||||||
|
// this makes it much easier to work with workspaces that list peer
|
||||||
|
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||||
|
// file instances instead of the real ones.
|
||||||
|
//
|
||||||
|
// We only do this to modules owned by the the dependency tree roots.
|
||||||
|
// This avoids breaking the resolution when jumping inside a vendor
|
||||||
|
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||||
|
// errors on react).
|
||||||
|
//
|
||||||
|
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||||
|
if (resolved) {
|
||||||
|
const locator = pnpApi.findPackageLocator(resolved);
|
||||||
|
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||||
|
str = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str = normalize(str);
|
||||||
|
|
||||||
|
if (str.match(/\.zip\//)) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||||
|
// VSCode only adds it automatically for supported schemes,
|
||||||
|
// so we have to do it manually for the `zip` scheme.
|
||||||
|
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||||
|
//
|
||||||
|
// 2021-10-08: VSCode changed the format in 1.61.
|
||||||
|
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-04-06: VSCode changed the format in 1.66.
|
||||||
|
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-05-06: VSCode changed the format in 1.68
|
||||||
|
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
case `vscode <1.61`: {
|
||||||
|
str = `^zip:${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.66`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.68`: {
|
||||||
|
str = `^/zip${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// To make "go to definition" work,
|
||||||
|
// We have to resolve the actual file system path from virtual path
|
||||||
|
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = resolve(`zipfile:${str}`);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||||
|
// We have to resolve the actual file system path from virtual path,
|
||||||
|
// everything else is up to neovim
|
||||||
|
case `neovim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = `zipfile://${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
str = `zip:${str}`;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromEditorPath(str) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||||
|
// So in order to convert it back, we use .* to match all the thing
|
||||||
|
// before `zipfile:`
|
||||||
|
return process.platform === `win32`
|
||||||
|
? str.replace(/^.*zipfile:\//, ``)
|
||||||
|
: str.replace(/^.*zipfile:/, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `neovim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||||
|
return str.replace(/^zipfile:\/\//, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`:
|
||||||
|
default: {
|
||||||
|
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force enable 'allowLocalPluginLoads'
|
||||||
|
// TypeScript tries to resolve plugins using a path relative to itself
|
||||||
|
// which doesn't work when using the global cache
|
||||||
|
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||||
|
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||||
|
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||||
|
// https://github.com/microsoft/vscode/issues/45856
|
||||||
|
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||||
|
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||||
|
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||||
|
this.projectService.allowLocalPluginLoads = true;
|
||||||
|
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// And here is the point where we hijack the VSCode <-> TS communications
|
||||||
|
// by adding ourselves in the middle. We locate everything that looks
|
||||||
|
// like an absolute path of ours and normalize it.
|
||||||
|
|
||||||
|
const Session = tsserver.server.Session;
|
||||||
|
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||||
|
let hostInfo = `unknown`;
|
||||||
|
|
||||||
|
Object.assign(Session.prototype, {
|
||||||
|
onMessage(/** @type {string | object} */ message) {
|
||||||
|
const isStringMessage = typeof message === 'string';
|
||||||
|
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedMessage != null &&
|
||||||
|
typeof parsedMessage === `object` &&
|
||||||
|
parsedMessage.arguments &&
|
||||||
|
typeof parsedMessage.arguments.hostInfo === `string`
|
||||||
|
) {
|
||||||
|
hostInfo = parsedMessage.arguments.hostInfo;
|
||||||
|
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||||
|
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||||
|
// The RegExp from https://semver.org/ but without the caret at the start
|
||||||
|
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
|
) ?? []).map(Number)
|
||||||
|
|
||||||
|
if (major === 1) {
|
||||||
|
if (minor < 61) {
|
||||||
|
hostInfo += ` <1.61`;
|
||||||
|
} else if (minor < 66) {
|
||||||
|
hostInfo += ` <1.66`;
|
||||||
|
} else if (minor < 68) {
|
||||||
|
hostInfo += ` <1.68`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||||
|
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return originalOnMessage.call(
|
||||||
|
this,
|
||||||
|
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
send(/** @type {any} */ msg) {
|
||||||
|
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||||
|
return typeof value === `string` ? toEditorPath(value) : value;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tsserver;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||||
|
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
20
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
20
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript your application uses
|
||||||
|
module.exports = absRequire(`typescript`);
|
10
.yarn/sdks/typescript/package.json
vendored
Normal file
10
.yarn/sdks/typescript/package.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "typescript",
|
||||||
|
"version": "5.4.5-sdk",
|
||||||
|
"main": "./lib/typescript.js",
|
||||||
|
"type": "commonjs",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "./bin/tsc",
|
||||||
|
"tsserver": "./bin/tsserver"
|
||||||
|
}
|
||||||
|
}
|
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
21
Dockerfile
21
Dockerfile
|
@ -1,19 +1,26 @@
|
||||||
# Builder
|
# Builder
|
||||||
FROM node:lts as builder
|
FROM node:lts as builder
|
||||||
|
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
||||||
ARG REACT_APP_SERVER
|
# Base path for synapse admin
|
||||||
|
ARG BASE_PATH=./
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src
|
# Copy .yarn directory to the working directory (must be on a separate line!)
|
||||||
RUN yarn --network-timeout=300000 install --immutable
|
# Use https://docs.docker.com/engine/reference/builder/#copy---parents when available
|
||||||
RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
COPY .yarn .yarn
|
||||||
|
COPY package.json .yarnrc.yml yarn.lock ./
|
||||||
|
|
||||||
|
# Disable telemetry and install packages
|
||||||
|
RUN yarn config set enableTelemetry 0 && yarn install --immutable --network-timeout=300000
|
||||||
|
|
||||||
|
COPY . /src
|
||||||
|
RUN yarn build --base=$BASE_PATH
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginx:alpine
|
FROM nginx:stable-alpine
|
||||||
|
|
||||||
COPY --from=builder /src/build /app
|
COPY --from=builder /src/dist /app
|
||||||
|
|
||||||
RUN rm -rf /usr/share/nginx/html \
|
RUN rm -rf /usr/share/nginx/html \
|
||||||
&& ln -s /app /usr/share/nginx/html
|
&& ln -s /app /usr/share/nginx/html
|
||||||
|
|
86
README.md
86
README.md
|
@ -64,11 +64,6 @@ You have three options:
|
||||||
- download dependencies: `yarn install`
|
- download dependencies: `yarn install`
|
||||||
- start web server: `yarn start`
|
- start web server: `yarn start`
|
||||||
|
|
||||||
You can fix the homeserver, so that the user can no longer define it himself.
|
|
||||||
Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver.example.com yarn start`)
|
|
||||||
or by editing it in the [.env](.env) file. See also the
|
|
||||||
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
|
|
||||||
|
|
||||||
#### Steps for 3)
|
#### Steps for 3)
|
||||||
|
|
||||||
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
|
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
|
||||||
|
@ -76,19 +71,16 @@ or by editing it in the [.env](.env) file. See also the
|
||||||
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
|
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
synapse-admin:
|
synapse-admin:
|
||||||
container_name: synapse-admin
|
container_name: synapse-admin
|
||||||
hostname: synapse-admin
|
hostname: synapse-admin
|
||||||
build:
|
build:
|
||||||
context: https://github.com/Awesome-Technologies/synapse-admin.git
|
context: https://github.com/Awesome-Technologies/synapse-admin.git
|
||||||
# args:
|
args:
|
||||||
|
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||||
# # see #266, PUBLIC_URL must be without surrounding quotation marks
|
# - BASE_PATH="/synapse-admin"
|
||||||
# - PUBLIC_URL=/synapse-admin
|
|
||||||
# - REACT_APP_SERVER="https://matrix.example.com"
|
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@ -96,11 +88,83 @@ or by editing it in the [.env](.env) file. See also the
|
||||||
|
|
||||||
- browse to http://localhost:8080
|
- browse to http://localhost:8080
|
||||||
|
|
||||||
|
### Restricting available homeserver
|
||||||
|
|
||||||
|
You can restrict the homeserver(s), so that the user can no longer define it himself.
|
||||||
|
|
||||||
|
Edit `config.json` to restrict either to a single homeserver:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"restrictBaseUrl": "https://your-matrixs-erver.example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or to a list of homeservers:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"restrictBaseUrl": [
|
||||||
|
"https://your-first-matrix-server.example.com",
|
||||||
|
"https://your-second-matrix-server.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `config.json` can be injected into a Docker container using a bind mount.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
services:
|
||||||
|
synapse-admin:
|
||||||
|
...
|
||||||
|
volumes:
|
||||||
|
./config.json:/app/config.json:ro
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Serving Synapse-Admin on a different path
|
||||||
|
|
||||||
|
The path prefix where synapse-admin is served can only be changed during the build step.
|
||||||
|
|
||||||
|
If you downloaded the source code, use `yarn build --base=/my-prefix` to set a path prefix.
|
||||||
|
|
||||||
|
If you want to build your own Docker container, use the `BASE_PATH` argument.
|
||||||
|
|
||||||
|
We do not support directly changing the path where Synapse-Admin is served in the pre-built Docker container. Instead please use a reverse proxy if you need to move Synapse-Admin to a different base path. If you want to serve multiple applications with different paths on the same domain, you need a reverse proxy anyway.
|
||||||
|
|
||||||
|
Example for Traefik:
|
||||||
|
|
||||||
|
`docker-compose.yml`
|
||||||
|
|
||||||
|
```yml
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:mimolette
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
|
||||||
|
synapse-admin:
|
||||||
|
image: awesometechnologies/synapse-admin:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.synapse-admin.rule=Host(`example.com`)&&PathPrefix(`/admin`)"
|
||||||
|
- "traefik.http.routers.synapse-admin.middlewares=admin,admin_path"
|
||||||
|
- "traefik.http.middlewares.admin.redirectregex.regex=^(.*)/admin/?"
|
||||||
|
- "traefik.http.middlewares.admin.redirectregex.replacement=$${1}/admin/"
|
||||||
|
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
||||||
|
```
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
![Screenshots](./screenshots.jpg)
|
![Screenshots](./screenshots.jpg)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
|
||||||
- Use `yarn test` to run all style, lint and unit tests
|
- Use `yarn test` to run all style, lint and unit tests
|
||||||
- Use `yarn fix` to fix the coding style
|
- Use `yarn fix` to fix the coding style
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
synapse-admin:
|
synapse-admin:
|
||||||
container_name: synapse-admin
|
container_name: synapse-admin
|
||||||
|
@ -13,14 +11,11 @@ services:
|
||||||
# context: https://github.com/Awesome-Technologies/synapse-admin.git
|
# context: https://github.com/Awesome-Technologies/synapse-admin.git
|
||||||
|
|
||||||
# args:
|
# args:
|
||||||
|
# - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||||
# if you're building on an architecture other than amd64, make sure
|
# if you're building on an architecture other than amd64, make sure
|
||||||
# to define a maximum ram for node. otherwise the build will fail.
|
# to define a maximum ram for node. otherwise the build will fail.
|
||||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||||
# default is .
|
# - BASE_PATH="/synapse-admin"
|
||||||
# - PUBLIC_URL=/synapse-admin
|
|
||||||
# You can use a fixed homeserver, so that the user can no longer
|
|
||||||
# define it himself
|
|
||||||
# - REACT_APP_SERVER="https://matrix.example.com"
|
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
132
index.html
Normal file
132
index.html
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Synapse-Admin"
|
||||||
|
/>
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="./manifest.json" />
|
||||||
|
<link rel="shortcut icon" href="./favicon.ico" />
|
||||||
|
<title>Synapse-Admin</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Spinner from https://projects.lukehaas.me/css-loaders/ */
|
||||||
|
|
||||||
|
.loader,
|
||||||
|
.loader:before,
|
||||||
|
.loader:after {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
color: #283593;
|
||||||
|
font-size: 11px;
|
||||||
|
text-indent: -99999em;
|
||||||
|
margin: 55px auto;
|
||||||
|
position: relative;
|
||||||
|
width: 10em;
|
||||||
|
height: 10em;
|
||||||
|
box-shadow: inset 0 0 0 1em;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
-ms-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader:before,
|
||||||
|
.loader:after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader:before {
|
||||||
|
width: 5.2em;
|
||||||
|
height: 10.2em;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 10.2em 0 0 10.2em;
|
||||||
|
top: -0.1em;
|
||||||
|
left: -0.1em;
|
||||||
|
-webkit-transform-origin: 5.2em 5.1em;
|
||||||
|
transform-origin: 5.2em 5.1em;
|
||||||
|
-webkit-animation: load2 2s infinite ease 1.5s;
|
||||||
|
animation: load2 2s infinite ease 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader:after {
|
||||||
|
width: 5.2em;
|
||||||
|
height: 10.2em;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 0 10.2em 10.2em 0;
|
||||||
|
top: -0.1em;
|
||||||
|
left: 5.1em;
|
||||||
|
-webkit-transform-origin: 0px 5.1em;
|
||||||
|
transform-origin: 0px 5.1em;
|
||||||
|
-webkit-animation: load2 2s infinite ease;
|
||||||
|
animation: load2 2s infinite ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes load2 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes load2 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root">
|
||||||
|
<div class="loader-container">
|
||||||
|
<div class="loader">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
|
<footer
|
||||||
|
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||||
|
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
|
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
|
||||||
|
Synapse-Admin <b><span id="version"></span></b> by Awesome Technologies Innovationslabor GmbH
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
<script>document.getElementById("version").textContent = __SYNAPSE_ADMIN_VERSION__</script>
|
||||||
|
</html>
|
70
package.json
70
package.json
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "synapse-admin",
|
"name": "synapse-admin",
|
||||||
"version": "0.9.2",
|
"version": "0.10.1",
|
||||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||||
|
"type": "module",
|
||||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
|
@ -9,48 +10,87 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
},
|
},
|
||||||
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.24.4",
|
||||||
|
"@babel/preset-env": "^7.24.4",
|
||||||
|
"@babel/preset-react": "^7.24.1",
|
||||||
|
"@testing-library/dom": "^10.0.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^15.0.2",
|
"@testing-library/react": "^15.0.2",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"babel-jest": "^29.7.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "^3.2.5"
|
"prettier": "^3.2.5",
|
||||||
|
"react-test-renderer": "^18.2.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vite-plugin-version-mark": "^0.0.13"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.4.1",
|
||||||
|
"@emotion/styled": "^11.3.0",
|
||||||
|
"@haleos/ra-language-german": "^1.0.0",
|
||||||
|
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||||
"@mui/icons-material": "^5.15.15",
|
"@mui/icons-material": "^5.15.15",
|
||||||
"@mui/material": "^5.15.15",
|
"@mui/material": "^5.15.15",
|
||||||
"@mui/styles": "^5.15.15",
|
"history": "^5.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"ra-language-chinese": "^2.0.10",
|
"query-string": "^7.1.1",
|
||||||
"ra-language-french": "^4.16.15",
|
"ra-core": "^4.16.15",
|
||||||
"ra-language-german": "^3.13.4",
|
"ra-i18n-polyglot": "^4.16.15",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-english": "^4.16.15",
|
||||||
"ra-language-farsi": "^4.2.0",
|
"ra-language-farsi": "^4.2.0",
|
||||||
|
"ra-language-french": "^4.16.15",
|
||||||
|
"ra-language-italian": "^3.13.1",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-admin": "^4.16.15",
|
"react-admin": "^4.16.15",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-scripts": "^5.0.1"
|
"react-hook-form": "^7.43.9",
|
||||||
|
"react-is": "^18.2.0",
|
||||||
|
"react-query": "^3.32.1",
|
||||||
|
"react-router": "^6.1.0",
|
||||||
|
"react-router-dom": "^6.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
|
"start": "vite serve",
|
||||||
"build": "REACT_APP_VERSION=$(git describe --tags) react-scripts build",
|
"build": "vite build",
|
||||||
"fix:other": "yarn prettier --write",
|
"fix:other": "yarn prettier --write",
|
||||||
"fix:code": "yarn test:lint --fix",
|
"fix:code": "yarn test:lint --fix",
|
||||||
"fix": "yarn fix:code && yarn fix:other",
|
"fix": "yarn fix:code && yarn fix:other",
|
||||||
"prettier": "prettier --ignore-path .gitignore \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
|
"prettier": "prettier \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
|
||||||
"test:code": "react-scripts test",
|
"test:code": "jest",
|
||||||
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
|
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
|
||||||
"test:style": "yarn prettier --list-different",
|
"test:style": "yarn prettier --check",
|
||||||
"test": "yarn test:style && yarn test:lint && yarn test:code",
|
"test": "yarn test:style && yarn test:lint && yarn test:code"
|
||||||
"eject": "react-scripts eject"
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
[
|
||||||
|
"@babel/preset-react",
|
||||||
|
{
|
||||||
|
"runtime": "automatic"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
},
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"<rootDir>/src/setupTests.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
|
|
1
public/config.json
Normal file
1
public/config.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,49 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Synapse-Admin"
|
|
||||||
/>
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>Synapse-Admin</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
<footer
|
|
||||||
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
|
||||||
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
|
||||||
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
|
|
||||||
Synapse-Admin <b>(%REACT_APP_VERSION%)</b> by Awesome Technologies Innovationslabor GmbH
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
14
src/App.jsx
14
src/App.jsx
|
@ -6,6 +6,7 @@ import {
|
||||||
resolveBrowserLocale,
|
resolveBrowserLocale,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||||
|
import merge from "lodash/merge";
|
||||||
import authProvider from "./synapse/authProvider";
|
import authProvider from "./synapse/authProvider";
|
||||||
import dataProvider from "./synapse/dataProvider";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
import users from "./components/users";
|
import users from "./components/users";
|
||||||
|
@ -33,8 +34,17 @@ const messages = {
|
||||||
zh: chineseMessages,
|
zh: chineseMessages,
|
||||||
};
|
};
|
||||||
const i18nProvider = polyglotI18nProvider(
|
const i18nProvider = polyglotI18nProvider(
|
||||||
locale => (messages[locale] ? messages[locale] : messages.en),
|
locale =>
|
||||||
resolveBrowserLocale()
|
messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en,
|
||||||
|
resolveBrowserLocale(),
|
||||||
|
[
|
||||||
|
{ locale: "en", name: "English" },
|
||||||
|
{ locale: "de", name: "Deutsch" },
|
||||||
|
{ locale: "fr", name: "Français" },
|
||||||
|
{ locale: "it", name: "Italiano" },
|
||||||
|
{ locale: "fa", name: "Persian(فارسی)" },
|
||||||
|
{ locale: "zh", name: "简体中文" },
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
|
|
5
src/AppContext.jsx
Normal file
5
src/AppContext.jsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
export const AppContext = createContext({});
|
||||||
|
|
||||||
|
export const useAppContext = () => useContext(AppContext);
|
|
@ -15,6 +15,7 @@ import {
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
import { MXCField } from "./media";
|
||||||
import PageviewIcon from "@mui/icons-material/Pageview";
|
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||||
import ReportIcon from "@mui/icons-material/Warning";
|
import ReportIcon from "@mui/icons-material/Warning";
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
@ -89,6 +90,8 @@ export const ReportShow = props => {
|
||||||
<TextField source="event_json.type" />
|
<TextField source="event_json.type" />
|
||||||
<TextField source="event_json.content.msgtype" />
|
<TextField source="event_json.content.msgtype" />
|
||||||
<TextField source="event_json.content.body" />
|
<TextField source="event_json.content.body" />
|
||||||
|
<TextField source="event_json.content.info.mimetype" />
|
||||||
|
<MXCField source="event_json.content.url" />
|
||||||
<TextField source="event_json.content.format" />
|
<TextField source="event_json.content.format" />
|
||||||
<TextField source="event_json.content.formatted_body" />
|
<TextField source="event_json.content.formatted_body" />
|
||||||
<TextField source="event_json.content.algorithm" />
|
<TextField source="event_json.content.algorithm" />
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
useTranslate,
|
useTranslate,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
useLocales,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
|
@ -21,13 +22,15 @@ import {
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
TextField,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
|
||||||
|
import { useAppContext } from "../AppContext";
|
||||||
import {
|
import {
|
||||||
getServerVersion,
|
getServerVersion,
|
||||||
|
getSupportedFeatures,
|
||||||
getSupportedLoginFlows,
|
getSupportedLoginFlows,
|
||||||
getWellKnownUrl,
|
getWellKnownUrl,
|
||||||
isValidBaseUrl,
|
isValidBaseUrl,
|
||||||
|
@ -37,7 +40,7 @@ import {
|
||||||
const FormBox = styled(Box)(({ theme }) => ({
|
const FormBox = styled(Box)(({ theme }) => ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
minHeight: "calc(100vh - 1em)",
|
minHeight: "calc(100vh - 1rem)",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
background: "url(./images/floating-cogs.svg)",
|
background: "url(./images/floating-cogs.svg)",
|
||||||
|
@ -46,12 +49,12 @@ const FormBox = styled(Box)(({ theme }) => ({
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
|
|
||||||
[`& .card`]: {
|
[`& .card`]: {
|
||||||
minWidth: "30em",
|
width: "30rem",
|
||||||
marginTop: "6em",
|
marginTop: "6rem",
|
||||||
marginBottom: "6em",
|
marginBottom: "6rem",
|
||||||
},
|
},
|
||||||
[`& .avatar`]: {
|
[`& .avatar`]: {
|
||||||
margin: "1em",
|
margin: "1rem",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
|
@ -60,36 +63,49 @@ const FormBox = styled(Box)(({ theme }) => ({
|
||||||
},
|
},
|
||||||
[`& .hint`]: {
|
[`& .hint`]: {
|
||||||
marginTop: "1em",
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
color: theme.palette.grey[600],
|
color: theme.palette.grey[600],
|
||||||
},
|
},
|
||||||
[`& .form`]: {
|
[`& .form`]: {
|
||||||
padding: "0 1em 1em 1em",
|
padding: "0 1rem 1rem 1rem",
|
||||||
},
|
},
|
||||||
[`& .input`]: {
|
[`& .select`]: {
|
||||||
marginTop: "1em",
|
marginBottom: "2rem",
|
||||||
},
|
},
|
||||||
[`& .actions`]: {
|
[`& .actions`]: {
|
||||||
padding: "0 1em 1em 1em",
|
padding: "0 1rem 1rem 1rem",
|
||||||
},
|
},
|
||||||
[`& .serverVersion`]: {
|
[`& .serverVersion`]: {
|
||||||
color: theme.palette.grey[500],
|
color: theme.palette.grey[500],
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
marginBottom: "1em",
|
marginLeft: "0.5rem",
|
||||||
marginLeft: "0.5em",
|
},
|
||||||
|
[`& .matrixVersions`]: {
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
marginLeft: "0.5rem",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
const { restrictBaseUrl } = useAppContext();
|
||||||
|
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
|
||||||
|
const allowMultipleBaseUrls = Array.isArray(restrictBaseUrl);
|
||||||
|
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||||
const [locale, setLocale] = useLocaleState();
|
const [locale, setLocale] = useLocaleState();
|
||||||
|
const locales = useLocales();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const base_url = localStorage.getItem("base_url");
|
const base_url = allowSingleBaseUrl
|
||||||
const cfg_base_url = process.env.REACT_APP_SERVER;
|
? restrictBaseUrl
|
||||||
|
: localStorage.getItem("base_url");
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||||
|
|
||||||
|
@ -127,20 +143,6 @@ const LoginPage = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderInput = ({
|
|
||||||
meta: { touched, error } = {},
|
|
||||||
input: { ...inputProps },
|
|
||||||
...props
|
|
||||||
}) => (
|
|
||||||
<TextField
|
|
||||||
error={!!(touched && error)}
|
|
||||||
helperText={touched && error}
|
|
||||||
{...inputProps}
|
|
||||||
{...props}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const validateBaseUrl = value => {
|
const validateBaseUrl = value => {
|
||||||
if (!value.match(/^(http|https):\/\//)) {
|
if (!value.match(/^(http|https):\/\//)) {
|
||||||
return translate("synapseadmin.auth.protocol_error");
|
return translate("synapseadmin.auth.protocol_error");
|
||||||
|
@ -179,17 +181,27 @@ const LoginPage = () => {
|
||||||
const UserData = ({ formData }) => {
|
const UserData = ({ formData }) => {
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
const [serverVersion, setServerVersion] = useState("");
|
const [serverVersion, setServerVersion] = useState("");
|
||||||
|
const [matrixVersions, setMatrixVersions] = useState("");
|
||||||
|
|
||||||
const handleUsernameChange = _ => {
|
const handleUsernameChange = _ => {
|
||||||
if (formData.base_url || cfg_base_url) return;
|
if (formData.base_url || allowSingleBaseUrl) return;
|
||||||
// check if username is a full qualified userId then set base_url accordingly
|
// check if username is a full qualified userId then set base_url accordingly
|
||||||
const domain = splitMxid(formData.username)?.domain;
|
const domain = splitMxid(formData.username)?.domain;
|
||||||
if (domain) {
|
if (domain) {
|
||||||
getWellKnownUrl(domain).then(url => form.setValue("base_url", url));
|
getWellKnownUrl(domain).then(url => {
|
||||||
|
if (
|
||||||
|
allowAnyBaseUrl ||
|
||||||
|
(allowMultipleBaseUrls && restrictBaseUrl.includes(url))
|
||||||
|
)
|
||||||
|
form.setValue("base_url", url);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (formData.base_url === "" && allowMultipleBaseUrls) {
|
||||||
|
form.setValue("base_url", restrictBaseUrl[0]);
|
||||||
|
}
|
||||||
if (!isValidBaseUrl(formData.base_url)) return;
|
if (!isValidBaseUrl(formData.base_url)) return;
|
||||||
|
|
||||||
getServerVersion(formData.base_url)
|
getServerVersion(formData.base_url)
|
||||||
|
@ -200,6 +212,14 @@ const LoginPage = () => {
|
||||||
)
|
)
|
||||||
.catch(() => setServerVersion(""));
|
.catch(() => setServerVersion(""));
|
||||||
|
|
||||||
|
getSupportedFeatures(formData.base_url)
|
||||||
|
.then(features =>
|
||||||
|
setMatrixVersions(
|
||||||
|
`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch(() => setMatrixVersions(""));
|
||||||
|
|
||||||
// Set SSO Url
|
// Set SSO Url
|
||||||
getSupportedLoginFlows(formData.base_url)
|
getSupportedLoginFlows(formData.base_url)
|
||||||
.then(loginFlows => {
|
.then(loginFlows => {
|
||||||
|
@ -211,7 +231,7 @@ const LoginPage = () => {
|
||||||
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
||||||
})
|
})
|
||||||
.catch(() => setSSOBaseUrl(""));
|
.catch(() => setSSOBaseUrl(""));
|
||||||
}, [formData.base_url]);
|
}, [formData.base_url, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -219,49 +239,56 @@ const LoginPage = () => {
|
||||||
<TextInput
|
<TextInput
|
||||||
autoFocus
|
autoFocus
|
||||||
name="username"
|
name="username"
|
||||||
component={renderInput}
|
|
||||||
label="ra.auth.username"
|
label="ra.auth.username"
|
||||||
|
autoComplete="username"
|
||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
onBlur={handleUsernameChange}
|
onBlur={handleUsernameChange}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
className="input"
|
|
||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
name="password"
|
name="password"
|
||||||
component={renderInput}
|
|
||||||
label="ra.auth.password"
|
label="ra.auth.password"
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
className="input"
|
|
||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
name="base_url"
|
name="base_url"
|
||||||
component={renderInput}
|
|
||||||
label="synapseadmin.auth.base_url"
|
label="synapseadmin.auth.base_url"
|
||||||
disabled={cfg_base_url || loading}
|
select={allowMultipleBaseUrls}
|
||||||
resettable
|
autoComplete="url"
|
||||||
|
disabled={loading}
|
||||||
|
readOnly={allowSingleBaseUrl}
|
||||||
|
resettable={allowAnyBaseUrl}
|
||||||
fullWidth
|
fullWidth
|
||||||
className="input"
|
|
||||||
validate={[required(), validateBaseUrl]}
|
validate={[required(), validateBaseUrl]}
|
||||||
/>
|
>
|
||||||
|
{allowMultipleBaseUrls &&
|
||||||
|
restrictBaseUrl.map(url => (
|
||||||
|
<MenuItem key={url} value={url}>
|
||||||
|
{url}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextInput>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography className="serverVersion">{serverVersion}</Typography>
|
<Typography className="serverVersion">{serverVersion}</Typography>
|
||||||
|
<Typography className="matrixVersions">{matrixVersions}</Typography>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
defaultValues={{ base_url: cfg_base_url || base_url }}
|
defaultValues={{ base_url: base_url }}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
mode="onTouched"
|
mode="onTouched"
|
||||||
>
|
>
|
||||||
|
@ -280,19 +307,16 @@ const LoginPage = () => {
|
||||||
<Box className="form">
|
<Box className="form">
|
||||||
<Select
|
<Select
|
||||||
value={locale}
|
value={locale}
|
||||||
onChange={e => {
|
onChange={e => setLocale(e.target.value)}
|
||||||
setLocale(e.target.value);
|
|
||||||
}}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="input"
|
className="select"
|
||||||
>
|
>
|
||||||
<MenuItem value="de">Deutsch</MenuItem>
|
{locales.map(l => (
|
||||||
<MenuItem value="en">English</MenuItem>
|
<MenuItem key={l.locale} value={l.locale}>
|
||||||
<MenuItem value="fr">Français</MenuItem>
|
{l.name}
|
||||||
<MenuItem value="it">Italiano</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem value="zh">简体中文</MenuItem>
|
))}
|
||||||
<MenuItem value="fa">Persian(فارسی)</MenuItem>
|
|
||||||
</Select>
|
</Select>
|
||||||
<FormDataConsumer>
|
<FormDataConsumer>
|
||||||
{formDataProps => <UserData {...formDataProps} />}
|
{formDataProps => <UserData {...formDataProps} />}
|
||||||
|
|
|
@ -1,14 +1,71 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { AdminContext } from "react-admin";
|
import { AdminContext } from "react-admin";
|
||||||
import LoginPage from "./LoginPage";
|
import LoginPage from "./LoginPage";
|
||||||
|
import { AppContext } from "../AppContext";
|
||||||
|
|
||||||
describe("LoginForm", () => {
|
describe("LoginForm", () => {
|
||||||
it("renders", () => {
|
it("renders with no restriction to homeserver", () => {
|
||||||
render(
|
render(
|
||||||
<AdminContext>
|
<AdminContext>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</AdminContext>
|
</AdminContext>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
screen.getByText("synapseadmin.auth.welcome");
|
||||||
|
screen.getByRole("combobox", { name: "" });
|
||||||
|
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||||
|
screen.getByText("ra.auth.password");
|
||||||
|
const baseUrlInput = screen.getByRole("textbox", {
|
||||||
|
name: "synapseadmin.auth.base_url",
|
||||||
|
});
|
||||||
|
expect(baseUrlInput.className.split(" ")).not.toContain("Mui-readOnly");
|
||||||
|
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with single restricted homeserver", () => {
|
||||||
|
render(
|
||||||
|
<AppContext.Provider
|
||||||
|
value={{ restrictBaseUrl: "https://matrix.example.com" }}
|
||||||
|
>
|
||||||
|
<AdminContext>
|
||||||
|
<LoginPage />
|
||||||
|
</AdminContext>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByText("synapseadmin.auth.welcome");
|
||||||
|
screen.getByRole("combobox", { name: "" });
|
||||||
|
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||||
|
screen.getByText("ra.auth.password");
|
||||||
|
const baseUrlInput = screen.getByRole("textbox", {
|
||||||
|
name: "synapseadmin.auth.base_url",
|
||||||
|
});
|
||||||
|
expect(baseUrlInput.className.split(" ")).toContain("Mui-readOnly");
|
||||||
|
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with multiple restricted homeservers", async () => {
|
||||||
|
render(
|
||||||
|
<AppContext.Provider
|
||||||
|
value={{
|
||||||
|
restrictBaseUrl: [
|
||||||
|
"https://matrix.example.com",
|
||||||
|
"https://matrix.example.org",
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AdminContext>
|
||||||
|
<LoginPage />
|
||||||
|
</AdminContext>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByText("synapseadmin.auth.welcome");
|
||||||
|
screen.getByRole("combobox", { name: "" });
|
||||||
|
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||||
|
screen.getByText("ra.auth.password");
|
||||||
|
screen.getByRole("combobox", { name: "synapseadmin.auth.base_url" });
|
||||||
|
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import get from "lodash/get";
|
||||||
import {
|
import {
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
Button,
|
Button,
|
||||||
|
@ -14,10 +15,12 @@ import {
|
||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import BlockIcon from "@mui/icons-material/Block";
|
import BlockIcon from "@mui/icons-material/Block";
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
|
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
|
@ -27,7 +30,9 @@ import {
|
||||||
import IconCancel from "@mui/icons-material/Cancel";
|
import IconCancel from "@mui/icons-material/Cancel";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
|
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
||||||
import { alpha, useTheme } from "@mui/material/styles";
|
import { alpha, useTheme } from "@mui/material/styles";
|
||||||
|
import { getMediaUrl } from "../synapse/synapse";
|
||||||
|
|
||||||
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
@ -333,3 +338,49 @@ export const QuarantineMediaButton = props => {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ViewMediaButton = ({ media_id, label }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const url = getMediaUrl(media_id);
|
||||||
|
return (
|
||||||
|
<Box style={{ whiteSpace: "pre" }}>
|
||||||
|
<Tooltip title={translate("resources.users_media.action.open")}>
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
style={{ minWidth: 0, paddingLeft: 0, paddingRight: 0 }}
|
||||||
|
>
|
||||||
|
<FileOpenIcon />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{label}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MediaIDField = ({ source }) => {
|
||||||
|
const homeserver = localStorage.getItem("home_server");
|
||||||
|
const record = useRecordContext();
|
||||||
|
if (!record) return null;
|
||||||
|
|
||||||
|
const src = get(record, source)?.toString();
|
||||||
|
if (!src) return null;
|
||||||
|
|
||||||
|
return <ViewMediaButton media_id={`${homeserver}/${src}`} label={src} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MXCField = ({ source }) => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
if (!record) return null;
|
||||||
|
|
||||||
|
const src = get(record, source)?.toString();
|
||||||
|
if (!src) return null;
|
||||||
|
|
||||||
|
const media_id = src.replace("mxc://", "");
|
||||||
|
|
||||||
|
return <ViewMediaButton media_id={media_id} label={src} />;
|
||||||
|
};
|
||||||
|
|
|
@ -51,7 +51,11 @@ import { Link } from "react-router-dom";
|
||||||
import AvatarField from "./AvatarField";
|
import AvatarField from "./AvatarField";
|
||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
||||||
import { DeviceRemoveButton } from "./devices";
|
import { DeviceRemoveButton } from "./devices";
|
||||||
import { ProtectMediaButton, QuarantineMediaButton } from "./media";
|
import {
|
||||||
|
MediaIDField,
|
||||||
|
ProtectMediaButton,
|
||||||
|
QuarantineMediaButton,
|
||||||
|
} from "./media";
|
||||||
|
|
||||||
const choices_medium = [
|
const choices_medium = [
|
||||||
{ id: "email", name: "resources.users.email" },
|
{ id: "email", name: "resources.users.email" },
|
||||||
|
@ -449,13 +453,13 @@ export const UserEdit = props => {
|
||||||
sort={{ field: "created_ts", order: "DESC" }}
|
sort={{ field: "created_ts", order: "DESC" }}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
|
<MediaIDField source="media_id" />
|
||||||
<DateField source="created_ts" showTime options={date_format} />
|
<DateField source="created_ts" showTime options={date_format} />
|
||||||
<DateField
|
<DateField
|
||||||
source="last_access_ts"
|
source="last_access_ts"
|
||||||
showTime
|
showTime
|
||||||
options={date_format}
|
options={date_format}
|
||||||
/>
|
/>
|
||||||
<TextField source="media_id" />
|
|
||||||
<NumberField source="media_length" />
|
<NumberField source="media_length" />
|
||||||
<TextField source="media_type" />
|
<TextField source="media_type" />
|
||||||
<TextField source="upload_name" />
|
<TextField source="upload_name" />
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import germanMessages from "ra-language-german";
|
import { formalGermanMessages } from "@haleos/ra-language-german";
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
...germanMessages,
|
...formalGermanMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "Heimserver URL",
|
base_url: "Heimserver URL",
|
||||||
welcome: "Willkommen bei Synapse-admin",
|
welcome: "Willkommen bei Synapse-admin",
|
||||||
server_version: "Synapse Version",
|
server_version: "Synapse Version",
|
||||||
|
supports_specs: "unterstützt Matrix-Specs",
|
||||||
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
|
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
|
||||||
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
|
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
|
||||||
url_error: "Keine gültige Matrix Server URL",
|
url_error: "Keine gültige Matrix Server URL",
|
||||||
|
@ -207,6 +208,9 @@ const de = {
|
||||||
format: "Nachrichtenformat",
|
format: "Nachrichtenformat",
|
||||||
formatted_body: "Formatierter Nachrichteninhalt",
|
formatted_body: "Formatierter Nachrichteninhalt",
|
||||||
algorithm: "Verschlüsselungsalgorithmus",
|
algorithm: "Verschlüsselungsalgorithmus",
|
||||||
|
info: {
|
||||||
|
mimetype: "Typ",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -255,6 +259,9 @@ const de = {
|
||||||
created_ts: "Erstellt",
|
created_ts: "Erstellt",
|
||||||
last_access_ts: "Letzter Zugriff",
|
last_access_ts: "Letzter Zugriff",
|
||||||
},
|
},
|
||||||
|
action: {
|
||||||
|
open: "Mediendatei in neuem Fenster öffnen",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
delete_media: {
|
delete_media: {
|
||||||
name: "Medien",
|
name: "Medien",
|
||||||
|
@ -388,37 +395,5 @@ const de = {
|
||||||
helper: { length: "Länge des Tokens, wenn kein Token vorgegeben wird." },
|
helper: { length: "Länge des Tokens, wenn kein Token vorgegeben wird." },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ra: {
|
|
||||||
...germanMessages.ra,
|
|
||||||
action: {
|
|
||||||
...germanMessages.ra.action,
|
|
||||||
unselect: "Abwählen",
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
...germanMessages.ra.auth,
|
|
||||||
auth_check_error: "Anmeldung fehlgeschlagen",
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
...germanMessages.ra.input,
|
|
||||||
password: {
|
|
||||||
...germanMessages.ra.input.password,
|
|
||||||
toggle_hidden: "Anzeigen",
|
|
||||||
toggle_visible: "Verstecken",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
notification: {
|
|
||||||
...germanMessages.ra.notification,
|
|
||||||
logged_out: "Abgemeldet",
|
|
||||||
},
|
|
||||||
page: {
|
|
||||||
...germanMessages.ra.page,
|
|
||||||
empty: "Keine Einträge vorhanden",
|
|
||||||
invite: "",
|
|
||||||
},
|
|
||||||
navigation: {
|
|
||||||
...germanMessages.ra.navigation,
|
|
||||||
skip_nav: "Zum Inhalt springen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
export default de;
|
export default de;
|
||||||
|
|
|
@ -7,6 +7,7 @@ const en = {
|
||||||
base_url: "Homeserver URL",
|
base_url: "Homeserver URL",
|
||||||
welcome: "Welcome to Synapse-admin",
|
welcome: "Welcome to Synapse-admin",
|
||||||
server_version: "Synapse version",
|
server_version: "Synapse version",
|
||||||
|
supports_specs: "supports Matrix specs",
|
||||||
username_error: "Please enter fully qualified user ID: '@user:domain'",
|
username_error: "Please enter fully qualified user ID: '@user:domain'",
|
||||||
protocol_error: "URL has to start with 'http://' or 'https://'",
|
protocol_error: "URL has to start with 'http://' or 'https://'",
|
||||||
url_error: "Not a valid Matrix server URL",
|
url_error: "Not a valid Matrix server URL",
|
||||||
|
@ -204,6 +205,10 @@ const en = {
|
||||||
format: "format",
|
format: "format",
|
||||||
formatted_body: "formatted content",
|
formatted_body: "formatted content",
|
||||||
algorithm: "algorithm",
|
algorithm: "algorithm",
|
||||||
|
url: "URL",
|
||||||
|
info: {
|
||||||
|
mimetype: "Type",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -252,6 +257,9 @@ const en = {
|
||||||
created_ts: "Created",
|
created_ts: "Created",
|
||||||
last_access_ts: "Last access",
|
last_access_ts: "Last access",
|
||||||
},
|
},
|
||||||
|
action: {
|
||||||
|
open: "Open media file in new window",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
delete_media: {
|
delete_media: {
|
||||||
name: "Media",
|
name: "Media",
|
||||||
|
@ -371,7 +379,6 @@ const en = {
|
||||||
},
|
},
|
||||||
action: { reconnect: "Reconnect" },
|
action: { reconnect: "Reconnect" },
|
||||||
},
|
},
|
||||||
},
|
|
||||||
registration_tokens: {
|
registration_tokens: {
|
||||||
name: "Registration tokens",
|
name: "Registration tokens",
|
||||||
fields: {
|
fields: {
|
||||||
|
@ -385,5 +392,6 @@ const en = {
|
||||||
},
|
},
|
||||||
helper: { length: "Length of the token if no token is given." },
|
helper: { length: "Length of the token if no token is given." },
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default en;
|
export default en;
|
||||||
|
|
|
@ -364,7 +364,6 @@ const fa = {
|
||||||
},
|
},
|
||||||
action: { reconnect: "دوباره وصل شوید" },
|
action: { reconnect: "دوباره وصل شوید" },
|
||||||
},
|
},
|
||||||
},
|
|
||||||
registration_tokens: {
|
registration_tokens: {
|
||||||
name: "توکن های ثبت نام",
|
name: "توکن های ثبت نام",
|
||||||
fields: {
|
fields: {
|
||||||
|
@ -378,5 +377,6 @@ const fa = {
|
||||||
},
|
},
|
||||||
helper: { length: "طول توکن در صورت عدم ارائه توکن." },
|
helper: { length: "طول توکن در صورت عدم ارائه توکن." },
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default fa;
|
export default fa;
|
||||||
|
|
|
@ -356,7 +356,6 @@ const fr = {
|
||||||
send_failure: "Une erreur s'est produite",
|
send_failure: "Une erreur s'est produite",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
registration_tokens: {
|
registration_tokens: {
|
||||||
name: "Jetons d'inscription",
|
name: "Jetons d'inscription",
|
||||||
fields: {
|
fields: {
|
||||||
|
@ -373,5 +372,6 @@ const fr = {
|
||||||
"Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
|
"Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default fr;
|
export default fr;
|
||||||
|
|
|
@ -367,7 +367,6 @@ const it = {
|
||||||
},
|
},
|
||||||
action: { reconnect: "Riconnetti" },
|
action: { reconnect: "Riconnetti" },
|
||||||
},
|
},
|
||||||
},
|
|
||||||
registration_tokens: {
|
registration_tokens: {
|
||||||
name: "Token di registrazione",
|
name: "Token di registrazione",
|
||||||
fields: {
|
fields: {
|
||||||
|
@ -381,5 +380,6 @@ const it = {
|
||||||
},
|
},
|
||||||
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default it;
|
export default it;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import chineseMessages from "ra-language-chinese";
|
import chineseMessages from "@haxqer/ra-language-chinese";
|
||||||
|
|
||||||
const zh = {
|
const zh = {
|
||||||
...chineseMessages,
|
...chineseMessages,
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
import { AppContext } from "./AppContext";
|
||||||
|
|
||||||
|
fetch("config.json")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(props =>
|
||||||
createRoot(document.getElementById("root")).render(
|
createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<AppContext.Provider value={props}>
|
||||||
<App />
|
<App />
|
||||||
|
</AppContext.Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,10 +2,7 @@ import { fetchUtils } from "react-admin";
|
||||||
|
|
||||||
const authProvider = {
|
const authProvider = {
|
||||||
// called when the user attempts to log in
|
// called when the user attempts to log in
|
||||||
login: ({ base_url, username, password, loginToken }) => {
|
login: async ({ base_url, username, password, loginToken }) => {
|
||||||
// force homeserver for protection in case the form is manipulated
|
|
||||||
base_url = process.env.REACT_APP_SERVER || base_url;
|
|
||||||
|
|
||||||
console.log("login ");
|
console.log("login ");
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -38,15 +35,14 @@ const authProvider = {
|
||||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||||
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||||
|
|
||||||
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
|
const { json } = await fetchUtils.fetchJson(login_api_url, options);
|
||||||
localStorage.setItem("home_server", json.home_server);
|
localStorage.setItem("home_server", json.home_server);
|
||||||
localStorage.setItem("user_id", json.user_id);
|
localStorage.setItem("user_id", json.user_id);
|
||||||
localStorage.setItem("access_token", json.access_token);
|
localStorage.setItem("access_token", json.access_token);
|
||||||
localStorage.setItem("device_id", json.device_id);
|
localStorage.setItem("device_id", json.device_id);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// called when the user clicks on the logout button
|
// called when the user clicks on the logout button
|
||||||
logout: () => {
|
logout: async () => {
|
||||||
console.log("logout");
|
console.log("logout");
|
||||||
|
|
||||||
const logout_api_url =
|
const logout_api_url =
|
||||||
|
@ -62,11 +58,9 @@ const authProvider = {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof access_token === "string") {
|
if (typeof access_token === "string") {
|
||||||
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
|
await fetchUtils.fetchJson(logout_api_url, options);
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
|
||||||
},
|
},
|
||||||
// called when the API returns an error
|
// called when the API returns an error
|
||||||
checkError: ({ status }) => {
|
checkError: ({ status }) => {
|
||||||
|
|
135
src/synapse/authProvider.test.js
Normal file
135
src/synapse/authProvider.test.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import authProvider from "./authProvider";
|
||||||
|
|
||||||
|
describe("authProvider", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.resetMocks();
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("login", () => {
|
||||||
|
it("should successfully login with username and password", async () => {
|
||||||
|
fetch.once(
|
||||||
|
JSON.stringify({
|
||||||
|
home_server: "example.com",
|
||||||
|
user_id: "@user:example.com",
|
||||||
|
access_token: "foobar",
|
||||||
|
device_id: "some_device",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const ret = await authProvider.login({
|
||||||
|
base_url: "http://example.com",
|
||||||
|
username: "@user:example.com",
|
||||||
|
password: "secret",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ret).toBe(undefined);
|
||||||
|
expect(fetch).toBeCalledWith(
|
||||||
|
"http://example.com/_matrix/client/r0/login",
|
||||||
|
{
|
||||||
|
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: ["application/json"],
|
||||||
|
"Content-Type": ["application/json"],
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
|
||||||
|
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
||||||
|
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
||||||
|
expect(localStorage.getItem("device_id")).toEqual("some_device");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully login with token", async () => {
|
||||||
|
fetch.once(
|
||||||
|
JSON.stringify({
|
||||||
|
home_server: "example.com",
|
||||||
|
user_id: "@user:example.com",
|
||||||
|
access_token: "foobar",
|
||||||
|
device_id: "some_device",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const ret = await authProvider.login({
|
||||||
|
base_url: "https://example.com/",
|
||||||
|
loginToken: "login_token",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ret).toBe(undefined);
|
||||||
|
expect(fetch).toBeCalledWith(
|
||||||
|
"https://example.com/_matrix/client/r0/login",
|
||||||
|
{
|
||||||
|
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: ["application/json"],
|
||||||
|
"Content-Type": ["application/json"],
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
|
||||||
|
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
||||||
|
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
||||||
|
expect(localStorage.getItem("device_id")).toEqual("some_device");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("logout", () => {
|
||||||
|
it("should remove the access_token from localStorage", async () => {
|
||||||
|
localStorage.setItem("base_url", "example.com");
|
||||||
|
localStorage.setItem("access_token", "foo");
|
||||||
|
fetch.mockResponse(JSON.stringify({}));
|
||||||
|
|
||||||
|
await authProvider.logout();
|
||||||
|
|
||||||
|
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: ["application/json"],
|
||||||
|
Authorization: ["Bearer foo"],
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
user: { authenticated: true, token: "Bearer foo" },
|
||||||
|
});
|
||||||
|
expect(localStorage.getItem("access_token")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkError", () => {
|
||||||
|
it("should resolve if error.status is not 401 or 403", async () => {
|
||||||
|
await expect(
|
||||||
|
authProvider.checkError({ status: 200 })
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject if error.status is 401", async () => {
|
||||||
|
await expect(
|
||||||
|
authProvider.checkError({ status: 401 })
|
||||||
|
).rejects.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject if error.status is 403", async () => {
|
||||||
|
await expect(
|
||||||
|
authProvider.checkError({ status: 403 })
|
||||||
|
).rejects.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkAuth", () => {
|
||||||
|
it("should reject when not logged in", async () => {
|
||||||
|
await expect(authProvider.checkAuth({})).rejects.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve when logged in", async () => {
|
||||||
|
localStorage.setItem("access_token", "foobar");
|
||||||
|
|
||||||
|
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPermissions", () => {
|
||||||
|
it("should do nothing", async () => {
|
||||||
|
await expect(authProvider.getPermissions()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -348,7 +348,7 @@ function getSearchOrder(order) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataProvider = {
|
const dataProvider = {
|
||||||
getList: (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const {
|
const {
|
||||||
user_id,
|
user_id,
|
||||||
|
@ -383,13 +383,14 @@ const dataProvider = {
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const url = `${endpoint_url}?${stringify(query)}`;
|
const url = `${endpoint_url}?${stringify(query)}`;
|
||||||
|
|
||||||
return jsonClient(url).then(({ json }) => ({
|
const { json } = await jsonClient(url);
|
||||||
|
return {
|
||||||
data: json[res.data].map(res.map),
|
data: json[res.data].map(res.map),
|
||||||
total: res.total(json, from, perPage),
|
total: res.total(json, from, perPage),
|
||||||
}));
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getOne: (resource, params) => {
|
getOne: async (resource, params) => {
|
||||||
console.log("getOne " + resource);
|
console.log("getOne " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
@ -397,14 +398,13 @@ const dataProvider = {
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`).then(
|
const { json } = await jsonClient(
|
||||||
({ json }) => ({
|
`${endpoint_url}/${encodeURIComponent(params.id)}`
|
||||||
data: res.map(json),
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
return { data: res.map(json) };
|
||||||
},
|
},
|
||||||
|
|
||||||
getMany: (resource, params) => {
|
getMany: async (resource, params) => {
|
||||||
console.log("getMany " + resource);
|
console.log("getMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
@ -412,17 +412,18 @@ const dataProvider = {
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map(id =>
|
params.ids.map(id =>
|
||||||
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
|
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
|
||||||
)
|
)
|
||||||
).then(responses => ({
|
);
|
||||||
|
return {
|
||||||
data: responses.map(({ json }) => res.map(json)),
|
data: responses.map(({ json }) => res.map(json)),
|
||||||
total: responses.length,
|
total: responses.length,
|
||||||
}));
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getManyReference: (resource, params) => {
|
getManyReference: async (resource, params) => {
|
||||||
console.log("getManyReference " + resource);
|
console.log("getManyReference " + resource);
|
||||||
const { page, perPage } = params.pagination;
|
const { page, perPage } = params.pagination;
|
||||||
const { field, order } = params.sort;
|
const { field, order } = params.sort;
|
||||||
|
@ -442,13 +443,14 @@ const dataProvider = {
|
||||||
const ref = res["reference"](params.id);
|
const ref = res["reference"](params.id);
|
||||||
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
|
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
|
||||||
|
|
||||||
return jsonClient(endpoint_url).then(({ headers, json }) => ({
|
const { json } = await jsonClient(endpoint_url);
|
||||||
|
return {
|
||||||
data: json[res.data].map(res.map),
|
data: json[res.data].map(res.map),
|
||||||
total: res.total(json, from, perPage),
|
total: res.total(json, from, perPage),
|
||||||
}));
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
update: (resource, params) => {
|
update: async (resource, params) => {
|
||||||
console.log("update " + resource);
|
console.log("update " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
@ -456,15 +458,17 @@ const dataProvider = {
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
|
const { json } = await jsonClient(
|
||||||
|
`${endpoint_url}/${encodeURIComponent(params.id)}`,
|
||||||
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
}).then(({ json }) => ({
|
}
|
||||||
data: res.map(json),
|
);
|
||||||
}));
|
return { data: res.map(json) };
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMany: (resource, params) => {
|
updateMany: async (resource, params) => {
|
||||||
console.log("updateMany " + resource);
|
console.log("updateMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
@ -472,7 +476,7 @@ const dataProvider = {
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map(
|
params.ids.map(
|
||||||
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
|
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
|
||||||
{
|
{
|
||||||
|
@ -480,12 +484,11 @@ const dataProvider = {
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
).then(responses => ({
|
);
|
||||||
data: responses.map(({ json }) => json),
|
return { data: responses.map(({ json }) => json) };
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
create: (resource, params) => {
|
create: async (resource, params) => {
|
||||||
console.log("create " + resource);
|
console.log("create " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
@ -495,15 +498,14 @@ const dataProvider = {
|
||||||
|
|
||||||
const create = res["create"](params.data);
|
const create = res["create"](params.data);
|
||||||
const endpoint_url = homeserver + create.endpoint;
|
const endpoint_url = homeserver + create.endpoint;
|
||||||
return jsonClient(endpoint_url, {
|
const { json } = await jsonClient(endpoint_url, {
|
||||||
method: create.method,
|
method: create.method,
|
||||||
body: JSON.stringify(create.body, filterNullValues),
|
body: JSON.stringify(create.body, filterNullValues),
|
||||||
}).then(({ json }) => ({
|
});
|
||||||
data: res.map(json),
|
return { data: res.map(json) };
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createMany: (resource, params) => {
|
createMany: async (resource, params) => {
|
||||||
console.log("createMany " + resource);
|
console.log("createMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
@ -511,7 +513,7 @@ const dataProvider = {
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
if (!("create" in res)) return Promise.reject();
|
if (!("create" in res)) return Promise.reject();
|
||||||
|
|
||||||
return Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map(id => {
|
params.ids.map(id => {
|
||||||
params.data.id = id;
|
params.data.id = id;
|
||||||
const cre = res["create"](params.data);
|
const cre = res["create"](params.data);
|
||||||
|
@ -521,12 +523,11 @@ const dataProvider = {
|
||||||
body: JSON.stringify(cre.body, filterNullValues),
|
body: JSON.stringify(cre.body, filterNullValues),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
).then(responses => ({
|
);
|
||||||
data: responses.map(({ json }) => json),
|
return { data: responses.map(({ json }) => json) };
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: (resource, params) => {
|
delete: async (resource, params) => {
|
||||||
console.log("delete " + resource);
|
console.log("delete " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
@ -536,24 +537,22 @@ const dataProvider = {
|
||||||
if ("delete" in res) {
|
if ("delete" in res) {
|
||||||
const del = res["delete"](params);
|
const del = res["delete"](params);
|
||||||
const endpoint_url = homeserver + del.endpoint;
|
const endpoint_url = homeserver + del.endpoint;
|
||||||
return jsonClient(endpoint_url, {
|
const { json } = await jsonClient(endpoint_url, {
|
||||||
method: "method" in del ? del.method : "DELETE",
|
method: "method" in del ? del.method : "DELETE",
|
||||||
body: "body" in del ? JSON.stringify(del.body) : null,
|
body: "body" in del ? JSON.stringify(del.body) : null,
|
||||||
}).then(({ json }) => ({
|
});
|
||||||
data: json,
|
return { data: json };
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return jsonClient(`${endpoint_url}/${params.id}`, {
|
const { json } = await jsonClient(`${endpoint_url}/${params.id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify(params.previousData, filterNullValues),
|
body: JSON.stringify(params.previousData, filterNullValues),
|
||||||
}).then(({ json }) => ({
|
});
|
||||||
data: json,
|
return { data: json };
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMany: (resource, params) => {
|
deleteMany: async (resource, params) => {
|
||||||
console.log("deleteMany " + resource);
|
console.log("deleteMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
@ -561,7 +560,7 @@ const dataProvider = {
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
if ("delete" in res) {
|
if ("delete" in res) {
|
||||||
return Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map(id => {
|
params.ids.map(id => {
|
||||||
const del = res["delete"]({ ...params, id: id });
|
const del = res["delete"]({ ...params, id: id });
|
||||||
const endpoint_url = homeserver + del.endpoint;
|
const endpoint_url = homeserver + del.endpoint;
|
||||||
|
@ -570,21 +569,21 @@ const dataProvider = {
|
||||||
body: "body" in del ? JSON.stringify(del.body) : null,
|
body: "body" in del ? JSON.stringify(del.body) : null,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
).then(responses => ({
|
);
|
||||||
|
return {
|
||||||
data: responses.map(({ json }) => json),
|
data: responses.map(({ json }) => json),
|
||||||
}));
|
};
|
||||||
} else {
|
} else {
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map(id =>
|
params.ids.map(id =>
|
||||||
jsonClient(`${endpoint_url}/${id}`, {
|
jsonClient(`${endpoint_url}/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).then(responses => ({
|
);
|
||||||
data: responses.map(({ json }) => json),
|
return { data: responses.map(({ json }) => json) };
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,6 +36,13 @@ export const getServerVersion = async baseUrl => {
|
||||||
return response.json.server_version;
|
return response.json.server_version;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Get supported Matrix features */
|
||||||
|
export const getSupportedFeatures = async baseUrl => {
|
||||||
|
const versionUrl = `${baseUrl}/_matrix/client/versions`;
|
||||||
|
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
|
||||||
|
return response.json;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get supported login flows
|
* Get supported login flows
|
||||||
* @param baseUrl the base URL of the homeserver
|
* @param baseUrl the base URL of the homeserver
|
||||||
|
@ -46,3 +53,8 @@ export const getSupportedLoginFlows = async baseUrl => {
|
||||||
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
|
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
|
||||||
return response.json.flows;
|
return response.json.flows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMediaUrl = media_id => {
|
||||||
|
const baseUrl = localStorage.getItem("base_url");
|
||||||
|
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
|
||||||
|
};
|
||||||
|
|
15
vite.config.js
Normal file
15
vite.config.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { vitePluginVersionMark } from "vite-plugin-version-mark";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
vitePluginVersionMark({
|
||||||
|
command: "git describe --tags",
|
||||||
|
ifMeta: true,
|
||||||
|
ifLog: true,
|
||||||
|
ifGlobal: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
Loading…
Reference in a new issue