Initial
This commit is contained in:
68
frontend/README.md
Normal file
68
frontend/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Webserver config
|
||||
|
||||
There are two ways to host this app:
|
||||
|
||||
- `nginx-split-path.conf`: backend and frontend are served at different URLs
|
||||
- `nginx-same-path.conf`: backend and frontend use the same base URL
|
||||
|
||||
Both examples use `/srv/http/my-domain/` as the base folder, make sure it exists.
|
||||
The server will create files at these locations:
|
||||
|
||||
|
||||
```sh
|
||||
/srv/http/my-domain/ # base folder, make sure it exists
|
||||
/srv/http/my-domain/backend_data/ # holds the encrypted json files
|
||||
/srv/http/my-domain/backend_static/ # holds static js files (mostly django admin)
|
||||
/srv/http/my-domain/frontend/ # place your frontend html here (e.g. content of example-html)
|
||||
/srv/http/my-domain/root/ # root-level files ("/") (only split-path)
|
||||
```
|
||||
|
||||
|
||||
## Split path
|
||||
|
||||
Since both parts are served separately, you can host the server on two different machines (or two different subpaths). The frontend is a purely static server. You can use a CDN if you want.
|
||||
|
||||
If you serve the backend on a subpath (not root "/"), you need to pass that subpath to the env file. E.g., `URL_SUBPATH=my/sub/path`.
|
||||
|
||||
If you use two different servers, you have to transfer the encryted json files to the frontend server somehow (e.g., with rsync or a tiny REST API). Or use the integrated API `api/json/<org>/<uuid>` (disabled by default). Though the other way (pushing data from backend to frontend) is more favorable. That way you will not expose the backend server to the public.
|
||||
|
||||
|
||||
## Same path
|
||||
|
||||
With this config, both – frontend and backend – are served from the root of the domain ("/"). You can still serve them from two different servers, but its easy to guess the url and discover the other server.
|
||||
|
||||
If you use two different servers, you will need to declare the exception explicitly (namely, `edit`, `upload`, `static`, and `api`). If the backend URLs change, you will need to update the config too. But in the given example, nginx will fallback to the backend server whever a file cannot be found.
|
||||
|
||||
**Note:** you can not have files in your frontend which are named like any of the backend URLs. For example, if you create a static `upload` or `edit` folder in your frontend code, it would probably break the administration backend (precedence).
|
||||
|
||||
|
||||
## Comparison
|
||||
|
||||
Assuming we configured the split-path config to use `frontend="card"` and `backend="hidden-service"` the URLs would be:
|
||||
|
||||
|
||||
| | Split path | Same path |
|
||||
|--------|------------------------------|---------------|
|
||||
|Frontend| /card/#org/id/pw | /#org/id/pw |
|
||||
|Backend | /hidden-service/edit/ | /edit/ |
|
||||
|Static | /hidden-service/static/ | /static/ |
|
||||
|Data | /card/data/org/id | /data/org/id/ |
|
||||
|API | /hidden-service/api/json/... | /api/json/... |
|
||||
|Root | /other-service/ | – |
|
||||
|
||||
|
||||
## Security considerations
|
||||
|
||||
A dynamic server (like Django) is always a security risk. You should limit public access wherever possible. For example:
|
||||
|
||||
- If you are the only person managing member cards, you can run the backend in a local-only environment and just sync the changes with rsync to a static frontend server.
|
||||
|
||||
- If you run the backend as a public server, you can try to limit the access. For example, by allowing only known IP Adresses (again, only if there are few people managing the cards and/or the connection location is fixed, e.g., business network).
|
||||
|
||||
- If your backend is public, at least do not use common URL paths. For example, `/admin/` is easy to guess and most crawlers will try these locations.
|
||||
|
||||
- Both configs allow you to run the backend and frontend on separate servers. This separates the sensitive data (Django app with unencrypted raw data) from the publicly accessible data (encrypted json).
|
||||
|
||||
- If you need to communicate between both servers, try to push the data from backend to frontend instead of the other way around. This way your frontend stays static and an attacker will not discover the backend server just by analyzing the web traffic. (attention: you may still expose it through Certificate Transparency Logs)
|
||||
|
||||
- Needless to say, communication between servers must be authenticated. Or else someone can just create new member cards arbitrarily.
|
||||
Binary file not shown.
BIN
frontend/example-html/img/bg.png
Normal file
BIN
frontend/example-html/img/bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
18
frontend/example-html/img/header.svg
Normal file
18
frontend/example-html/img/header.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg viewBox="0 0 657.3 53.6" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m3.8 53.1c-1 0-1.8-.2-2.6-.6-.8-.5-1.2-1.1-1.2-2v-47.5c0-.8.4-1.4 1.1-1.9.7-.4 1.5-.6 2.3-.6h25.3c.9 0 1.5.4 1.9 1.1s.6 1.5.6 2.2c0 .9-.2 1.7-.6 2.4s-1 1-1.8 1h-21v16.5h11.2c.8 0 1.4.3 1.8.9s.6 1.3.6 2.2c0 .7-.2 1.3-.6 2-.4.6-1 1-1.9 1h-11.3v20.8c0 .9-.4 1.5-1.2 1.9-.7.4-1.6.6-2.6.6z"/>
|
||||
<path d="m30.2 53.1c-.7 0-1.4-.1-2.2-.4s-1.4-.7-1.9-1.1c-.5-.5-.7-1-.7-1.6 0-.1 0-.4.1-.6l14.4-46.8c.3-.9.9-1.5 1.8-1.9s1.9-.6 3-.6 2.1.2 3.1.6c.9.4 1.5 1.1 1.8 1.9l14.3 46.7c.1.3.1.5.1.6 0 .6-.3 1.1-.8 1.6s-1.1.9-1.9 1.2c-.7.3-1.5.4-2.2.4-.6 0-1.2-.1-1.6-.4-.5-.2-.8-.6-1-1.2l-3-10.2h-17.7l-2.9 10.2c-.1.6-.5 1-1 1.2s-1 .4-1.7.4zm7.3-18h14.4l-7.2-24.7z"/>
|
||||
<path d="m73.1 53.1c-1 0-1.8-.2-2.4-.7s-.9-1.1-.9-1.7v-47.7c0-.8.4-1.4 1.1-1.9.7-.4 1.5-.6 2.3-.6h13.1c2.6 0 5 .3 7.2 1s4 1.9 5.3 3.7 2 4.4 2 7.7c0 3-.5 5.5-1.6 7.5s-2.6 3.4-4.5 4.2c1.5.6 2.8 1.3 3.9 2.3 1.1.9 2 2.2 2.6 3.7.6 1.6.9 3.6.9 6v1.4c0 3.7-.6 6.6-1.9 8.9-1.2 2.2-3 3.8-5.1 4.8-2.2 1-4.6 1.5-7.3 1.5h-14.7zm4.2-31.6h8.6c2.4 0 4.3-.6 5.4-1.9 1.2-1.2 1.8-3.1 1.8-5.5 0-1.6-.3-3-.9-4s-1.5-1.8-2.6-2.2c-1.1-.5-2.4-.7-3.9-.7h-8.5v14.3zm0 24.8h9c2.6 0 4.6-.7 6-2.1s2.1-3.7 2.1-6.8v-1.2c0-3.2-.7-5.4-2.1-6.7s-3.4-1.9-6-1.9h-9z"/>
|
||||
<path d="m127.3 53.1c-.8 0-1.6-.2-2.3-.6s-1.1-1.1-1.1-1.9v-47.7c0-.9.4-1.5 1.2-1.9s1.7-.6 2.6-.6c1 0 1.8.2 2.6.6s1.2 1 1.2 1.9v43.4h18.7c.8 0 1.3.3 1.7 1s.6 1.5.6 2.3-.2 1.6-.6 2.3-1 1.1-1.7 1.1h-22.9z"/>
|
||||
<path d="m159 53.1c-.7 0-1.4-.1-2.2-.4s-1.4-.7-1.9-1.1c-.5-.5-.7-1-.7-1.6 0-.1 0-.4.1-.6l14.3-46.7c.3-.9.9-1.5 1.8-1.9s1.9-.6 3-.6 2.1.2 3.1.6c.9.4 1.5 1.1 1.8 1.9l14.3 46.7c.1.3.1.5.1.6 0 .6-.3 1.1-.8 1.6s-1.1.9-1.9 1.2c-.7.3-1.5.4-2.2.4-.6 0-1.2-.1-1.6-.4-.5-.2-.8-.6-1-1.2l-3-10.2h-17.9l-2.9 10.2c-.1.6-.5 1-1 1.2-.2.1-.7.3-1.4.3zm7.3-18h14.4l-7.2-24.7z"/>
|
||||
<path d="m201.9 53.1c-1 0-1.8-.2-2.4-.7s-.9-1.1-.9-1.7v-47.7c0-.8.4-1.4 1.1-1.9.7-.4 1.5-.6 2.3-.6h13c2.6 0 5 .3 7.2 1s4 1.9 5.3 3.7 2 4.4 2 7.7c0 3-.5 5.5-1.6 7.5s-2.6 3.4-4.5 4.2c1.5.6 2.8 1.3 3.9 2.3 1.1.9 2 2.2 2.6 3.7.6 1.6.9 3.6.9 6v1.4c0 3.7-.6 6.6-1.9 8.9-1.2 2.2-3 3.8-5.1 4.8-2.2 1-4.6 1.5-7.3 1.5h-14.6zm4.2-31.6h8.6c2.4 0 4.3-.6 5.4-1.9 1.2-1.2 1.8-3.1 1.8-5.5 0-1.6-.3-3-.9-4s-1.5-1.8-2.6-2.2c-1.1-.5-2.4-.7-3.9-.7h-8.5v14.3zm0 24.8h9c2.6 0 4.6-.7 6-2.1s2.1-3.7 2.1-6.8v-1.2c0-3.2-.7-5.4-2.1-6.7s-3.4-1.9-6-1.9h-9z"/>
|
||||
<path d="m256.5 53.1c-1 0-1.8-.2-2.6-.6s-1.2-1.1-1.2-1.9v-46.4c0-1.4.4-2.4 1.2-3s1.7-.8 2.6-.8 1.7.1 2.4.4 1.3.7 1.9 1.3 1.2 1.5 1.8 2.6l10.1 19.1 10.2-19.1c.6-1.1 1.2-2 1.8-2.6s1.2-1.1 1.8-1.3c.7-.3 1.5-.4 2.4-.4 1 0 1.9.3 2.7.8.8.6 1.2 1.5 1.2 3v46.3c0 .8-.4 1.4-1.2 1.9s-1.7.6-2.6.6c-1 0-1.8-.2-2.6-.6s-1.2-1.1-1.2-1.9v-34.5l-9.6 17.9c-.4.6-.8 1.1-1.3 1.3-.5.3-1 .4-1.5.4-.4 0-.9-.1-1.4-.4-.5-.2-.9-.7-1.2-1.4l-9.6-18.3v35c0 .8-.4 1.4-1.2 1.9s-1.9.7-2.9.7z"/>
|
||||
<path d="m303.1 53.1c-.7 0-1.4-.1-2.2-.4s-1.4-.7-1.9-1.1c-.5-.5-.7-1-.7-1.6 0-.1 0-.4.1-.6l14.3-46.7c.3-.9.9-1.5 1.8-1.9s1.9-.6 3-.6 2.1.2 3.1.6c.9.4 1.5 1.1 1.8 1.9l14.3 46.7c.1.3.1.5.1.6 0 .6-.3 1.1-.8 1.6s-1.1.9-1.9 1.2c-.7.3-1.5.4-2.2.4-.6 0-1.2-.1-1.6-.4-.5-.2-.8-.6-1-1.2l-3-10.2h-17.9l-2.9 10.2c-.1.6-.5 1-1 1.2-.2.1-.8.3-1.4.3zm7.3-18h14.4l-7.2-24.7z"/>
|
||||
<path d="m346.4 53.1c-1 0-1.8-.2-2.6-.6s-1.2-1.1-1.2-1.9v-47.7c0-.9.4-1.5 1.2-1.9s1.7-.6 2.6-.6c1 0 1.8.2 2.6.6s1.2 1 1.2 1.9v21.9l17.6-23.3c.5-.7 1.2-1.1 2.2-1.1.6 0 1.3.2 2 .6s1.3.9 1.7 1.5c.5.6.7 1.2.7 1.9 0 .2 0 .4-.1.7s-.2.6-.4.8l-13.8 17.3 16.4 25c.3.4.4.9.4 1.4 0 .6-.2 1.2-.6 1.8s-1 1-1.7 1.4-1.4.5-2.1.5c-.5 0-1-.1-1.5-.3s-.9-.6-1.2-1.1l-14.7-22.2-4.7 5.9v15c0 .8-.4 1.4-1.2 1.9s-1.9.6-2.8.6z"/>
|
||||
<path d="m385 53.1c-.8 0-1.6-.2-2.3-.6s-1.1-1.1-1.1-1.9v-47.6c0-.8.4-1.4 1.1-1.9s1.5-.6 2.3-.6h25.6c.9 0 1.6.4 1.9 1.1.4.7.6 1.5.6 2.2 0 .9-.2 1.7-.6 2.4s-1.1 1-1.9 1h-21.4v16.5h11.1c.8 0 1.4.3 1.9.9.4.6.6 1.3.6 2.2 0 .7-.2 1.3-.6 2-.4.6-1 1-1.9 1h-11.1v16.6h21.4c.8 0 1.4.3 1.9 1 .4.7.6 1.5.6 2.4 0 .8-.2 1.5-.6 2.2s-1 1.1-1.9 1.1z"/>
|
||||
<path d="m447.5 53.3c-.6 0-1.2-.2-1.7-.6s-.9-1-1.3-1.6l-10.5-20.6h-7.2v20.1c0 .8-.4 1.4-1.2 1.9s-1.7.6-2.6.6c-1 0-1.8-.2-2.6-.6s-1.2-1.1-1.2-1.9v-47.7c0-.7.2-1.2.7-1.7s1.1-.7 1.9-.7h13.7c3 0 5.8.4 8.3 1.3s4.5 2.4 6 4.5 2.2 5.1 2.2 8.8c0 2.9-.4 5.3-1.3 7.2s-2.1 3.4-3.6 4.6c-1.5 1.1-3.2 2-5.1 2.5l10.1 18.9c.1.1.2.3.2.5s.1.4.1.5c0 .6-.2 1.2-.7 1.8s-1.1 1.1-1.8 1.5c-.9.5-1.6.7-2.4.7zm-20.7-29h8.6c2.7 0 4.8-.6 6.4-1.9s2.4-3.5 2.4-6.6-.8-5.3-2.4-6.6-3.8-1.9-6.4-1.9h-8.6z"/>
|
||||
<path d="m486.6 53.6c-3.1 0-5.9-.5-8.3-1.4s-4.3-2-5.6-3.3-2-2.6-2-3.8c0-.6.2-1.2.5-1.8s.7-1.2 1.2-1.6c.5-.5 1-.7 1.6-.7.7 0 1.3.3 1.9.8.6.6 1.3 1.2 2.2 2s1.9 1.4 3.2 2 3 .9 5 .9c1.7 0 3.3-.3 4.6-.8 1.4-.6 2.4-1.4 3.2-2.5s1.2-2.6 1.2-4.3c0-1.8-.4-3.3-1.3-4.5-.8-1.2-2-2.2-3.4-3s-2.9-1.6-4.6-2.2c-1.6-.7-3.3-1.4-5-2.2s-3.2-1.7-4.6-2.8-2.5-2.4-3.3-4.1-1.3-3.7-1.3-6.2c0-2.6.5-4.8 1.5-6.6s2.3-3.2 3.9-4.2c1.6-1.1 3.4-1.8 5.4-2.3s3.9-.7 5.8-.7c1.2 0 2.5.1 4 .3s2.9.5 4.3.9 2.5.9 3.5 1.6c.9.7 1.4 1.5 1.4 2.4 0 .5-.1 1-.4 1.7s-.6 1.2-1 1.7c-.5.5-1 .7-1.8.7-.6 0-1.3-.2-2.1-.7s-1.8-.9-3-1.4-2.8-.7-4.9-.7c-1.7 0-3.3.2-4.6.7-1.4.5-2.4 1.2-3.2 2.1s-1.2 2.2-1.2 3.7.4 2.8 1.3 3.8c.8 1 2 1.8 3.3 2.5 1.4.7 2.9 1.4 4.6 2s3.3 1.4 5 2.2 3.2 1.9 4.6 3.1 2.5 2.7 3.4 4.6c.8 1.8 1.3 4.1 1.3 6.9 0 3.5-.7 6.3-2.1 8.6s-3.4 4-5.8 5.1c-2.4 1-5.2 1.5-8.4 1.5z"/>
|
||||
<path d="m514 53.1c-1 0-1.8-.2-2.6-.6s-1.2-1-1.2-1.9v-47.7c0-.7.3-1.2.8-1.7.6-.5 1.3-.7 2.1-.7h14c2.9 0 5.6.5 8 1.5s4.3 2.7 5.7 5 2.1 5.4 2.1 9.3v.6c0 3.8-.7 6.9-2.2 9.3-1.4 2.3-3.4 4-5.8 5.1s-5.2 1.6-8.2 1.6h-8.9v17.8c0 .9-.4 1.5-1.2 1.9-.8.3-1.6.5-2.6.5zm3.8-26.4h8.9c2.7 0 4.8-.8 6.3-2.3s2.3-3.9 2.3-7.1v-.9c0-3.2-.8-5.5-2.3-7s-3.6-2.3-6.3-2.3h-8.9z"/>
|
||||
<path d="m547.3 53.1c-.7 0-1.4-.1-2.2-.4s-1.4-.7-1.9-1.1c-.5-.5-.7-1-.7-1.6 0-.1 0-.4.1-.6l14.4-46.8c.3-.9.9-1.5 1.8-1.9s1.9-.6 3-.6 2.1.2 3.1.6c.9.4 1.5 1.1 1.8 1.9l14.3 46.6c.1.3.1.5.1.6 0 .6-.3 1.1-.8 1.6s-1.1.9-1.9 1.2c-.7.3-1.5.4-2.2.4-.6 0-1.2-.1-1.6-.4-.5-.2-.8-.6-1-1.2l-3-10.2h-17.9l-2.7 10.3c-.1.6-.5 1-1 1.2s-1 .4-1.7.4zm7.3-18h14.4l-7.2-24.7z"/>
|
||||
<path d="m602.2 53.5c-2.9 0-5.5-.5-8-1.6-2.4-1.1-4.4-2.8-5.8-5.1-1.4-2.4-2.2-5.5-2.2-9.4v-21c0-3.9.7-7 2.2-9.4 1.4-2.4 3.4-4.1 5.8-5.1 2.4-1.1 5.1-1.6 8-1.6 3.3 0 6.2.6 8.6 1.7s4.3 2.7 5.7 4.6 2.1 4.2 2.1 6.7c0 1.6-.3 2.7-.9 3.2s-1.6.8-2.9.8c-1.2 0-2.1-.2-2.7-.6-.7-.4-1-1-1.1-1.9 0-.6-.2-1.4-.4-2.2-.2-.9-.6-1.7-1.1-2.6-.5-.7-1.4-1.4-2.5-2s-2.6-.8-4.5-.8c-2.8 0-4.9.8-6.4 2.3s-2.2 3.9-2.2 7v21c0 3.2.8 5.5 2.3 7s3.7 2.3 6.6 2.3c1.8 0 3.3-.3 4.4-.8 1.1-.6 1.9-1.2 2.4-2.1.5-.8.9-1.7 1.1-2.7.2-.9.3-1.8.4-2.6 0-.9.4-1.6 1.1-1.9.7-.4 1.6-.6 2.6-.6 1.3 0 2.3.3 3 .8.6.5.9 1.6.9 3.2 0 2.6-.7 4.9-2.1 6.9s-3.3 3.6-5.7 4.8c-2.5 1.1-5.4 1.7-8.7 1.7z"/>
|
||||
<path d="m629.1 53.1c-.8 0-1.6-.2-2.3-.6s-1.1-1.1-1.1-1.9v-47.6c0-.8.4-1.4 1.1-1.9s1.5-.6 2.3-.6h25.6c.9 0 1.6.4 1.9 1.1.4.7.6 1.5.6 2.2 0 .9-.2 1.7-.6 2.4s-1.1 1-1.9 1h-21.4v16.5h11.1c.8 0 1.4.3 1.9.9.4.6.6 1.3.6 2.2 0 .7-.2 1.3-.6 2-.4.6-1 1-1.9 1h-11.1v16.6h21.4c.8 0 1.4.3 1.9 1 .4.7.6 1.5.6 2.4 0 .8-.2 1.5-.6 2.2s-1 1.1-1.9 1.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
5
frontend/example-html/img/logo.svg
Normal file
5
frontend/example-html/img/logo.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 547 546.8" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#0e9a48" d="m208.1 485.8v-65.2c2.3-.9 4.6-2 6.8-3.3 31.1-18.3 34.4-70.4 7.3-116.3s-74.2-68.2-105.3-49.9-34.4 70.4-7.3 116.3c4.4 7.4 9.3 14.2 14.5 20.3v48.4l-67-38.3v-240.1l-23.5-15.5c-21.4 39-33.6 83.7-33.6 131.3 0 147.9 117.4 268.3 264.1 273.2v-28.2z"/>
|
||||
<path fill="#cb2026" d="m130.3 104.2 41 24.3c-.8 6.7-.8 14.1-.8 16.7.4 36.1 49.2 63.5 102.5 63s91.2-32.8 90.9-64.6c-.3-36.1-35-64.3-88.3-65.9-18-.5-35.4 3.1-43.3 4.6l-29.9-19.1 71-40.5 209.4 121.7 24-13.2c-23.1-38-55.8-70.9-97.1-94.6-128-73.9-290.9-32.3-368.3 92.5l23.8 15z"/>
|
||||
<path fill="#35469d" d="m490 236.5-44.5 23.3c-1.6-.3-11.5-9.5-13.7-10.8-31.5-17.6-77.9 5.7-103.9 52.2-25.9 46.5-23.7 99.8 7.8 117.3 31.5 17.6 78.5-3.5 104.4-50 4.2-7.5 9.8-24.9 12.4-32.6l37.9-21.9-.2 83.3-208.9 122 .1 27.5c44.4-1.2 89.2-13.4 130.3-37.4 127.6-74.8 172.3-236.9 102.3-366l-24 13.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 914 B |
1
frontend/example-html/img/no-img.svg
Normal file
1
frontend/example-html/img/no-img.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 70 90" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h70v90h-70z" fill="#ddd"/><g fill="#888"><path d="m66.2 73.1c0 3.6-1 6.6-3 9.1-2.1 2.5-4.5 3.8-7.3 3.8h-41.8c-2.9 0-5.4-1.2-7.3-3.8s-3-5.5-3-9.1c0-2.8.2-5.4.4-7.9s.8-4.9 1.5-7.4 1.7-4.6 2.9-6.4 2.7-3.2 4.6-4.4 4.1-1.7 6.6-1.7c4.3 4.2 9.4 6.2 15.3 6.2s11-2.1 15.3-6.2c2.5 0 4.6.5 6.6 1.7s3.4 2.6 4.6 4.4 2.1 3.9 2.9 6.4c.7 2.5 1.2 5 1.5 7.4s.2 5.1.2 7.9z"/><circle cx="35.1" cy="29.9" r="18.7"/></g></svg>
|
||||
|
After Width: | Height: | Size: 477 B |
35
frontend/example-html/index.html
Normal file
35
frontend/example-html/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="initial-scale=1.0,maximum-scale=1.0" />
|
||||
<title>Member card</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="script.js"></script>
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<div id="card">
|
||||
<div id="msg" data-load="Loading ...">Loading ...</div>
|
||||
<div id="pass" class="hidden">
|
||||
<div class="spin"></div>
|
||||
<header>
|
||||
<img src="img/header.svg" height="16" style="margin: 4px">
|
||||
<img src="img/logo.svg" height="60" style="float: right">
|
||||
</header>
|
||||
<main>
|
||||
<img id="img" src="img/no-img.svg">
|
||||
<div>
|
||||
<div><span id="name">Name</span></div>
|
||||
<div>ID: <span id="member_id">42</span></div>
|
||||
<div>Valid: <span id="valid">on 1/1/1970</span></div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Member card valid <span id="valid">on 1/1/1970</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<script>onResize()</script>
|
||||
</body>
|
||||
</html>
|
||||
4
frontend/example-html/manifest.json
Normal file
4
frontend/example-html/manifest.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Ausweis",
|
||||
"display": "fullscreen"
|
||||
}
|
||||
124
frontend/example-html/script.js
Normal file
124
frontend/example-html/script.js
Normal file
@@ -0,0 +1,124 @@
|
||||
async function decrypt(ciphertext, password) {
|
||||
const pwUtf8 = new TextEncoder().encode(password);
|
||||
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8);
|
||||
const ctUtf8 = new Uint8Array(ciphertext);
|
||||
const iv = ctUtf8.slice(0, 12);
|
||||
const alg = { name: 'AES-GCM', iv: iv, additionalData: iv };
|
||||
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']);
|
||||
try {
|
||||
const buf = await crypto.subtle.decrypt(alg, key, ctUtf8.slice(12));
|
||||
return new TextDecoder().decode(buf);
|
||||
} catch (e) {
|
||||
throw new Error('Decrypt failed')
|
||||
}
|
||||
}
|
||||
|
||||
function setError(text) {
|
||||
document.getElementById('msg').innerText = text;
|
||||
}
|
||||
|
||||
function setFields(elem, fields) {
|
||||
for (const [k, v] of Object.entries(fields)) {
|
||||
for (const el of elem.querySelectorAll('#' + k)) {
|
||||
el.innerText = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function apply(data, password) {
|
||||
const json = JSON.parse(await decrypt(data, password));
|
||||
const now = new Date().getTime();
|
||||
const vA = new Date(json['valid_since'] + 'T00:00:00');
|
||||
if (vA.getTime() > now) {
|
||||
return setError('Member card not valid yet');
|
||||
}
|
||||
const vZ = new Date(json['valid_until'] + 'T23:59:59');
|
||||
if (vZ.getTime() < now) {
|
||||
return setError('Member card expired');
|
||||
}
|
||||
const pass = document.getElementById('pass');
|
||||
const imgElem = pass.querySelector('#img');
|
||||
if (imgElem) {
|
||||
if (json['img']) {
|
||||
imgElem.src = 'data:image;base64,' + json['img'];
|
||||
} else {
|
||||
imgElem.src = 'img/no-img.svg';
|
||||
}
|
||||
}
|
||||
setFields(pass, {
|
||||
name: json['name'],
|
||||
member_id: json['id'],
|
||||
org_name: json['org'],
|
||||
valid: 'on ' + new Date().toLocaleDateString('en'),
|
||||
});
|
||||
setFields(pass, json['data']); // may overwrite previous fields
|
||||
|
||||
const title = 'Member card for ' + json['name'] + ' – ' + json['org'];
|
||||
document.head.querySelector('title').innerText = title;
|
||||
document.getElementById('msg').classList.add('hidden');
|
||||
pass.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function onLoad() {
|
||||
// reset previous download
|
||||
document.getElementById('pass').classList.add('hidden');
|
||||
const msg = document.getElementById('msg');
|
||||
msg.innerText = msg.dataset.load;
|
||||
msg.classList.remove('hidden');
|
||||
// download new data
|
||||
const [org, uuid, secret] = location.hash.slice(1).split('/');
|
||||
if (!org || !uuid || !secret) {
|
||||
return setError('Invalid URL');
|
||||
}
|
||||
const res = await fetch('./data/' + org + '/' + uuid);
|
||||
if (!res.ok) {
|
||||
return setError('Error loading\n\n' + res.status + ' ' + res.statusText);
|
||||
}
|
||||
try {
|
||||
const data = await res.arrayBuffer();
|
||||
await apply(data, secret);
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// load and parse data
|
||||
window.onload = onLoad;
|
||||
// force reload if hash params change
|
||||
window.addEventListener('hashchange', onLoad, true);
|
||||
|
||||
// -------------
|
||||
// scale-up card
|
||||
// -------------
|
||||
|
||||
function onResize() {
|
||||
const card = document.getElementById('card');
|
||||
const sw = window.innerWidth / card.offsetWidth;
|
||||
const sh = window.innerHeight / card.offsetHeight;
|
||||
card.style.scale = Math.min(Math.min(sw, sh) * 0.97, 2);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', onResize, true);
|
||||
window.addEventListener('orientationchange', onResize, true);
|
||||
screen?.orientation?.addEventListener('change', onResize, true);
|
||||
|
||||
// -----------------
|
||||
// check for updates
|
||||
// -----------------
|
||||
|
||||
lastUpdate = new Date().getTime();
|
||||
|
||||
function needsUpdate() {
|
||||
// reload page if older than 15min
|
||||
const now = new Date().getTime();
|
||||
if (now - lastUpdate > 900_000) {
|
||||
lastUpdate = now;
|
||||
onLoad();
|
||||
}
|
||||
}
|
||||
// setInterval(needsUpdate, 1000);
|
||||
window.addEventListener('focus', needsUpdate, true);
|
||||
window.addEventListener('pageshow', needsUpdate, true);
|
||||
window.addEventListener('visibilitychange', function () {
|
||||
!document.hidden && document.visibilityState !== 'hidden' && needsUpdate();
|
||||
}, true);
|
||||
125
frontend/example-html/style.css
Normal file
125
frontend/example-html/style.css
Normal file
@@ -0,0 +1,125 @@
|
||||
:root {
|
||||
--w: 85.6mm;
|
||||
--h: 54mm;
|
||||
--r: 3.18mm;
|
||||
--iw: 2.5cm;
|
||||
--ih: 3.2cm;
|
||||
--ratio: 2.5 / 3.2;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #666;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#msg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#card {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
width: var(--w);
|
||||
height: var(--h);
|
||||
top: calc(50% - var(--h)/2);
|
||||
left: calc(50% - var(--w)/2);
|
||||
border-radius: var(--r);
|
||||
box-shadow: 0 0 4px;
|
||||
}
|
||||
|
||||
#pass {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
border-radius: var(--r);
|
||||
font-size: 3mm;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 1mm;
|
||||
border-radius: var(--r) var(--r) 0 0;
|
||||
max-height: 6mm;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 1mm 3mm;
|
||||
border-radius: 0 0 var(--r) var(--r);
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
main>div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2mm;
|
||||
text-shadow: 1px 0 #fff, 0 1px #fff, -1px 0 #fff, 0 -1px #fff,
|
||||
1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff;
|
||||
}
|
||||
main>div>div {
|
||||
display: flex;
|
||||
gap: 1mm;
|
||||
}
|
||||
|
||||
#img {
|
||||
max-width: var(--iw);
|
||||
max-height: var(--ih);
|
||||
object-fit: cover;
|
||||
border-radius: 1mm;
|
||||
margin: 0 2mm;
|
||||
aspect-ratio: var(--ratio);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.spin {
|
||||
position: absolute;
|
||||
bottom: 1.2mm;
|
||||
right: 1.5mm;
|
||||
width: 2mm;
|
||||
height: 2mm;
|
||||
border: .5mm solid transparent;
|
||||
border-top-color: #fff;
|
||||
border-bottom-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 2s steps(5) infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: unset;
|
||||
}
|
||||
#card {
|
||||
top: unset;
|
||||
left: unset;
|
||||
scale: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Customize appearance */
|
||||
|
||||
#pass {
|
||||
background: url('img/bg.png') 25mm 6mm/60mm no-repeat;
|
||||
}
|
||||
footer {
|
||||
color: #fff;
|
||||
background: #0E9A48;
|
||||
}
|
||||
13
frontend/js-encryption/decrypt.js
Normal file
13
frontend/js-encryption/decrypt.js
Normal file
@@ -0,0 +1,13 @@
|
||||
async function decrypt(ciphertext, password) {
|
||||
const pwUtf8 = new TextEncoder().encode(password);
|
||||
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8);
|
||||
const ctUtf8 = new Uint8Array(Array.from(atob(ciphertext)).map(x => x.charCodeAt(0)));
|
||||
const alg = { name: 'AES-GCM', iv: ctUtf8.slice(0,12) };
|
||||
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']);
|
||||
try {
|
||||
const buf = await crypto.subtle.decrypt(alg, key, ctUtf8.slice(12));
|
||||
return new TextDecoder().decode(buf);
|
||||
} catch (e) {
|
||||
throw new Error('Decrypt failed')
|
||||
}
|
||||
}
|
||||
11
frontend/js-encryption/encrypt.js
Normal file
11
frontend/js-encryption/encrypt.js
Normal file
@@ -0,0 +1,11 @@
|
||||
async function encrypt(plaintext, password) {
|
||||
const pwUtf8 = new TextEncoder().encode(password);
|
||||
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const alg = { name: 'AES-GCM', iv: iv };
|
||||
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']);
|
||||
const ptUint8 = new TextEncoder().encode(plaintext);
|
||||
const buf = await crypto.subtle.encrypt(alg, key, ptUint8);
|
||||
const ctStr = Array.from(new Uint8Array(buf)).map(b => String.fromCharCode(b)).join('');
|
||||
return btoa(String.fromCharCode(...iv) + ctStr);
|
||||
}
|
||||
49
frontend/nginx-same-path.conf
Normal file
49
frontend/nginx-same-path.conf
Normal file
@@ -0,0 +1,49 @@
|
||||
upstream ausweis { server 127.0.0.1:8099; }
|
||||
server {
|
||||
server_name MYDOMAIN;
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
server {
|
||||
server_name MYDOMAIN;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
access_log /var/log/nginx/ausweis.access.log;
|
||||
error_log /var/log/nginx/ausweis.error.log warn;
|
||||
|
||||
# make sure everything under / is in a sub-folder
|
||||
root /srv/http/my-domain/frontend/;
|
||||
|
||||
add_header Permissions-Policy "interest-cohort=()";
|
||||
add_header Cache-Control must-revalidate;
|
||||
expires 300;
|
||||
|
||||
location /data/ {
|
||||
alias /srv/http/my-domain/backend_data/;
|
||||
try_files $uri =404; # disable index, prevent attacks on finding a valid slug
|
||||
expires 30;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /srv/http/my-domain/backend_static/;
|
||||
try_files $uri =404; # disable index
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ @app_server;
|
||||
}
|
||||
|
||||
location @app_server {
|
||||
proxy_pass http://ausweis;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
ssl_certificate fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key privkey.pem; # managed by Certbot
|
||||
}
|
||||
50
frontend/nginx-split-path.conf
Normal file
50
frontend/nginx-split-path.conf
Normal file
@@ -0,0 +1,50 @@
|
||||
upstream ausweis { server 127.0.0.1:8099; }
|
||||
server {
|
||||
server_name MYDOMAIN;
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
server {
|
||||
server_name MYDOMAIN;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
access_log /var/log/nginx/ausweis.access.log;
|
||||
error_log /var/log/nginx/ausweis.error.log warn;
|
||||
|
||||
# make sure everything under / is in a sub-folder
|
||||
root /srv/http/my-domain/root/;
|
||||
|
||||
add_header Permissions-Policy "interest-cohort=()";
|
||||
add_header Cache-Control must-revalidate;
|
||||
expires 300;
|
||||
|
||||
location /frontend/data/ {
|
||||
alias /srv/http/my-domain/backend_data/;
|
||||
try_files $uri =404; # disable index, prevent attacks on finding a valid slug
|
||||
expires 30;
|
||||
}
|
||||
|
||||
location /frontend {
|
||||
alias /srv/http/my-domain/frontend/;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location /backend/static/ {
|
||||
alias /srv/http/my-domain/backend_static/;
|
||||
try_files $uri =404; # disable index
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location /backend/ {
|
||||
proxy_pass http://ausweis;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
ssl_certificate fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key privkey.pem; # managed by Certbot
|
||||
}
|
||||
Reference in New Issue
Block a user