diff --git a/.dockerignore b/.dockerignore
index 7bbbfb779..1858fa026 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,3 +5,5 @@ docat/__pycache__
docat/upload
docat/.tox
web/node_modules
+web/build
+web/.env*
diff --git a/.github/workflows/docat.yml b/.github/workflows/docat.yml
index 14a1e9639..84b505d31 100644
--- a/.github/workflows/docat.yml
+++ b/.github/workflows/docat.yml
@@ -60,7 +60,7 @@ jobs:
- name: run test suite
working-directory: web
- run: yarn jest
+ run: yarn test
container-image:
runs-on: ubuntu-latest
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..c5220423b
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "none"
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 2584c488b..169e577c8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,17 +1,21 @@
# building frontend
-FROM node:16.14 as build-deps
+FROM node:16.14 as frontend
+WORKDIR /app/frontend
COPY web ./
+
# fix docker not following symlinks
COPY doc/getting-started.md ./src/assets/
+
RUN yarn install --frozen-lockfile
RUN yarn lint
-RUN yarn run test:unit
+
+# fix test not exiting by default
+ARG CI=true
+RUN yarn test
+
RUN yarn build
# setup Python
-# TODO(Fliiiix): FastApi is broken in Python 3.11
-# We need to wait for a fix:
-# https://github.com/tiangolo/fastapi/issues/5048
FROM python:3.11.0-alpine3.15 AS backend
# configure docker container
@@ -29,12 +33,9 @@ COPY /docat/pyproject.toml /docat/poetry.lock /app/
# Install the application
WORKDIR /app/docat
-RUN poetry install --no-root --no-ansi --no-dev
+RUN poetry install --no-root --no-ansi --only main
# production
-# TODO(Fliiiix): FastApi is broken in Python 3.11
-# We need to wait for a fix:
-# https://github.com/tiangolo/fastapi/issues/5048
FROM python:3.11.0-alpine3.15
# set up the system
@@ -46,7 +47,7 @@ RUN mkdir -p /var/docat/doc
# install the application
RUN mkdir -p /var/www/html
-COPY --from=build-deps /dist /var/www/html
+COPY --from=frontend /app/frontend/build /var/www/html
COPY docat /app/docat
WORKDIR /app/docat
diff --git a/docat/docat/app.py b/docat/docat/app.py
index 5f6b3f502..33ae23291 100644
--- a/docat/docat/app.py
+++ b/docat/docat/app.py
@@ -40,6 +40,7 @@
get_all_projects,
get_project_details,
index_all_projects,
+ is_forbidden_project_name,
remove_docs,
remove_file_index_from_db,
remove_version_from_version_index,
@@ -113,7 +114,12 @@ def get_project(project):
@app.get("/api/search", response_model=SearchResponse, status_code=status.HTTP_200_OK)
@app.get("/api/search/", response_model=SearchResponse, status_code=status.HTTP_200_OK)
def search(query: str):
- query = query.lower()
+ query = query.lower().strip()
+
+ # an empty string would match almost everything
+ if not query:
+ return SearchResponse(projects=[], versions=[], files=[])
+
found_projects: list[SearchResultProject] = []
found_versions: list[SearchResultVersion] = []
found_files: list[SearchResultFile] = []
@@ -312,6 +318,10 @@ def upload(
docat_api_key: Optional[str] = Header(None),
db: TinyDB = Depends(get_db),
):
+ if is_forbidden_project_name(project):
+ response.status_code = status.HTTP_400_BAD_REQUEST
+ return ApiResponse(message=f'Project name "{project}" is forbidden, as it conflicts with pages in docat web.')
+
project_base_path = DOCAT_UPLOAD_FOLDER / project
base_path = project_base_path / version
target_file = base_path / file.filename
@@ -391,6 +401,10 @@ def claim(project: str, db: TinyDB = Depends(get_db)):
@app.put("/api/{project}/rename/{new_project_name}", response_model=ApiResponse, status_code=status.HTTP_200_OK)
@app.put("/api/{project}/rename/{new_project_name}/", response_model=ApiResponse, status_code=status.HTTP_200_OK)
def rename(project: str, new_project_name: str, response: Response, docat_api_key: str = Header(None), db: TinyDB = Depends(get_db)):
+ if is_forbidden_project_name(new_project_name):
+ response.status_code = status.HTTP_400_BAD_REQUEST
+ return ApiResponse(message=f'New project name "{new_project_name}" is forbidden, as it conflicts with pages in docat web.')
+
project_base_path = DOCAT_UPLOAD_FOLDER / project
new_project_base_path = DOCAT_UPLOAD_FOLDER / new_project_name
diff --git a/docat/docat/utils.py b/docat/docat/utils.py
index 9c2cd0145..51c4191cd 100644
--- a/docat/docat/utils.py
+++ b/docat/docat/utils.py
@@ -99,6 +99,16 @@ def calculate_token(password, salt):
return hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000).hex()
+def is_forbidden_project_name(name: str) -> bool:
+ """
+ Checks if the given project name is forbidden.
+ The project name is forbidden if it conflicts with
+ a page on the docat website.
+ """
+ name = name.lower().strip()
+ return name in ["upload", "claim", "delete", "search", "help"]
+
+
def get_all_projects(upload_folder_path: Path) -> Projects:
"""
Returns all projects in the upload folder.
diff --git a/docat/tests/test_rename.py b/docat/tests/test_rename.py
index 4fbfb83d9..6a25d23aa 100644
--- a/docat/tests/test_rename.py
+++ b/docat/tests/test_rename.py
@@ -72,3 +72,26 @@ def test_rename_success(client_with_claimed_project):
assert len(claims_with_old_name) == 0
claims_with_new_name = table.search(Project.name == "new-project-name")
assert len(claims_with_new_name) == 1
+
+
+def test_rename_rejects_forbidden_project_name(client_with_claimed_project):
+ """
+ Names that conflict with pages in docat web are forbidden,
+ and renaming a project to such a name should fail.
+ """
+
+ create_response = client_with_claimed_project.post(
+ "/api/some-project/1.0.0", files={"file": ("index.html", io.BytesIO(b"
Hello World
"), "plain/text")}
+ )
+ assert create_response.status_code == 201
+
+ with patch("os.rename") as rename_mock:
+ for project_name in ["upload", "claim", "delete", "Search ", "help"]:
+
+ rename_response = client_with_claimed_project.put(f"/api/some-project/rename/{project_name}", headers={"Docat-Api-Key": "1234"})
+ assert rename_response.status_code == 400
+ assert rename_response.json() == {
+ "message": f'New project name "{project_name}" is forbidden, as it conflicts with pages in docat web.'
+ }
+
+ assert rename_mock.mock_calls == []
diff --git a/docat/tests/test_search.py b/docat/tests/test_search.py
index 811b3a417..ac844f20c 100644
--- a/docat/tests/test_search.py
+++ b/docat/tests/test_search.py
@@ -46,6 +46,25 @@ def test_search_project_by_name_negative(client_with_claimed_project):
assert search_response.json() == {"projects": [], "versions": [], "files": []}
+def test_search_ignores_empty_query(client_with_claimed_project):
+ """
+ Search should return an empty result if the query is empty.
+ """
+ create_project_response = client_with_claimed_project.post(
+ "/api/some-project/1.0.0",
+ files={"file": ("index.html", io.BytesIO(b"Hello World
"), "plain/text")},
+ )
+ assert create_project_response.status_code == 201
+
+ search_response = client_with_claimed_project.get("/api/search?query=%20")
+ assert search_response.status_code == 200
+ assert search_response.json() == {"projects": [], "versions": [], "files": []}
+
+ search_response = client_with_claimed_project.get("/api/search?query=&")
+ assert search_response.status_code == 200
+ assert search_response.json() == {"projects": [], "versions": [], "files": []}
+
+
def test_search_finds_tag(client_with_claimed_project):
"""
Search should find a tag by name. (Partial match)
diff --git a/docat/tests/test_upload.py b/docat/tests/test_upload.py
index 1d5746693..8542894e6 100644
--- a/docat/tests/test_upload.py
+++ b/docat/tests/test_upload.py
@@ -150,3 +150,20 @@ def test_fails_with_invalid_token(client_with_claimed_project):
assert response_data["message"] == "Docat-Api-Key token is not valid for some-project"
assert remove_mock.mock_calls == []
+
+
+def test_upload_rejects_forbidden_project_name(client_with_claimed_project):
+ """
+ Names that conflict with pages in docat web are forbidden,
+ and creating a project with such a name should fail.
+ """
+
+ with patch("docat.app.remove_docs") as remove_mock:
+ for project_name in ["upload", "claim", "delete", "Search ", "help"]:
+ response = client_with_claimed_project.post(
+ f"/api/{project_name}/1.0.0", files={"file": ("index.html", io.BytesIO(b"Hello World
"), "plain/text")}
+ )
+ assert response.status_code == 400
+ assert response.json() == {"message": f'Project name "{project_name}" is forbidden, as it conflicts with pages in docat web.'}
+
+ assert remove_mock.mock_calls == []
diff --git a/web/.eslintrc.json b/web/.eslintrc.json
new file mode 100644
index 000000000..1f9084a58
--- /dev/null
+++ b/web/.eslintrc.json
@@ -0,0 +1,45 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": [
+ "plugin:react/recommended",
+ "standard-with-typescript"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts",
+ "*.tsx"
+ ],
+ "extends": [
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@typescript-eslint/recommended-requiring-type-checking"
+ ],
+ "parserOptions": {
+ "project": [
+ "./tsconfig.json"
+ ]
+ },
+ "rules": {
+ "@typescript-eslint/space-before-function-paren": "off"
+ }
+ },
+ {
+ "files": [
+ "src/react-app-env.d.ts"
+ ],
+ "rules": {
+ "@typescript-eslint/triple-slash-reference": "off"
+ }
+ }
+ ],
+ "parserOptions": {
+ "ecmaVersion": "latest"
+ },
+ "plugins": [
+ "react"
+ ],
+ "rules": {}
+}
\ No newline at end of file
diff --git a/web/.gitignore b/web/.gitignore
index a0dddc6fb..900f50e0b 100644
--- a/web/.gitignore
+++ b/web/.gitignore
@@ -1,21 +1,29 @@
-.DS_Store
-node_modules
-/dist
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-# local env files
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.prettierrc
+.DS_Store
.env.local
-.env.*.local
+.env.development.local
+.env.test.local
+.env.production.local
-# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-# Editor directories and files
-.idea
-.vscode
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
+# local env files
+.env
+.env.local
+.env.*.local
diff --git a/web/README.md b/web/README.md
index d9ab52661..cf8add862 100644
--- a/web/README.md
+++ b/web/README.md
@@ -8,18 +8,16 @@ yarn install [--pure-lockfile]
### Compiles and hot-reloads for development
-Configure the backend connection by setting
-port and host in `.env.development.local`.
-Like this configuration for the host `127.0.0.1`
-and the port `1337`.
-
+Configure both the frontend port and the backend connection
+by setting them in `.env.development.local`.
```sh
-VUE_APP_BACKEND_PORT=1337
-VUE_APP_BACKEND_HOST=127.0.0.1
+PORT=8080
+BACKEND_HOST=127.0.0.1
+BACKEND_PORT=5000
```
```sh
-yarn serve
+yarn start
```
### Compiles and minifies for production
@@ -34,7 +32,13 @@ yarn build
yarn lint
```
-### Basic Header Theeming
+### Tests
+
+```sh
+yarn test
+```
+
+### Basic Header Theming
Not happy with the default Docat logo and header?
Just add your custom html header to the `/var/www/html/config.json` file.
@@ -43,21 +47,22 @@ Just add your custom html header to the `/var/www/html/config.json` file.
{ "headerHTML": "MyCompany
" }
```
-### Customize configuration
-
-See [Configuration Reference](https://cli.vuejs.org/config/).
-
## Development
-To mount the development `dist/` folder while working on the
-web frontend, you can mount the `dist/` folder as a docker volume:
-
```sh
sudo docker run \
--detach \
--volume /path/to/doc:/var/docat/doc/ \
- --volume /path/to/docat/web/dist:/var/www/html/ \
--publish 8000:80 \
docat
```
+
+## Errors
+
+If you get a 403 response when trying to read a version,
+try changing the permissions of your docs folder on your host.
+
+```sh
+sudo chmod 777 /path/to/doc -r
+```
\ No newline at end of file
diff --git a/web/babel.config.js b/web/babel.config.js
deleted file mode 100644
index e9558405f..000000000
--- a/web/babel.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- presets: [
- '@vue/cli-plugin-babel/preset'
- ]
-}
diff --git a/web/jest.config.js b/web/jest.config.js
deleted file mode 100644
index 0f9579148..000000000
--- a/web/jest.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- preset: '@vue/cli-plugin-unit-jest'
-}
diff --git a/web/package.json b/web/package.json
index 2922e9962..a77699eca 100644
--- a/web/package.json
+++ b/web/package.json
@@ -2,71 +2,63 @@
"name": "docat-web",
"version": "0.1.0",
"private": true,
- "scripts": {
- "serve": "vue-cli-service serve",
- "build": "vue-cli-service build",
- "test:unit": "vue-cli-service test:unit",
- "lint": "vue-cli-service lint"
- },
"dependencies": {
- "babel-runtime": "^6.26.0",
- "core-js": "^3.26.0",
- "html-loader": "^0.5.5",
- "node-sass": "^6.0.0",
- "sass-loader": "^10.0.5",
- "semver": "^7.3.7",
- "v-tooltip": "^2.1.3",
- "vue": "^2.6.10",
- "vue-markdown": "^2.2.4",
- "vuelidate": "^0.7.4"
+ "@emotion/react": "^11.10.4",
+ "@emotion/styled": "^11.10.4",
+ "@mui/icons-material": "^5.10.9",
+ "@mui/material": "^5.10.11",
+ "@testing-library/jest-dom": "^5.14.1",
+ "@testing-library/react": "^13.0.0",
+ "@testing-library/user-event": "^13.2.1",
+ "@types/jest": "^27.0.1",
+ "@types/lodash": "^4.14.191",
+ "@types/node": "^16.7.13",
+ "@types/react": "^18.0.0",
+ "@types/react-dom": "^18.0.0",
+ "eslint": "^8.0.1",
+ "http-proxy-middleware": "^2.0.6",
+ "lodash": "^4.17.21",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-markdown": "^8.0.3",
+ "react-router-dom": "^6.4.2",
+ "react-scripts": "5.0.1",
+ "react-tooltip": "^4.4.3",
+ "typescript": "*",
+ "web-vitals": "^2.1.0"
},
- "devDependencies": {
- "@vue/cli-plugin-babel": "^5.0.8",
- "@vue/cli-plugin-eslint": "^4.0.0",
- "@vue/cli-plugin-unit-jest": "~4.2.0",
- "@vue/cli-service": "^4.0.0",
- "@vue/test-utils": "^1.3.0",
- "babel-eslint": "^10.0.3",
- "eslint": "^5.16.0",
- "eslint-plugin-vue": "^6.2.2",
- "vue-material": "^1.0.0-beta-11",
- "vue-router": "^3.1.3",
- "vue-template-compiler": "^2.6.10"
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test --watchAll=false --testMatch **/src/**/*.test.ts",
+ "eject": "react-scripts eject",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx"
},
"eslintConfig": {
- "root": true,
- "env": {
- "node": true
- },
"extends": [
- "plugin:vue/essential",
- "eslint:recommended"
- ],
- "rules": {
- "no-console": "off"
- },
- "parserOptions": {
- "parser": "babel-eslint"
- },
- "overrides": [
- {
- "files": [
- "**/__tests__/*.{j,t}s?(x)",
- "**/tests/unit/**/*.spec.{j,t}s?(x)"
- ],
- "env": {
- "jest": true
- }
- }
+ "react-app",
+ "react-app/jest"
]
},
- "postcss": {
- "plugins": {
- "autoprefixer": {}
- }
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
},
- "browserslist": [
- "> 1%",
- "last 2 versions"
- ]
+ "proxy": "http://localhost:5000/",
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^5.0.0",
+ "eslint-config-standard-with-typescript": "^23.0.0",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-n": "^15.0.0",
+ "eslint-plugin-promise": "^6.0.0",
+ "eslint-plugin-react": "^7.31.11"
+ }
}
diff --git a/web/public/index.html b/web/public/index.html
index b244128a9..3029b21a2 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -4,13 +4,12 @@
-
-
+
-
+