This commit is contained in:
relikd
2023-10-02 23:39:20 +02:00
commit 8629b01da3
47 changed files with 1412 additions and 0 deletions

68
frontend/README.md Normal file
View 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.

After

Width:  |  Height:  |  Size: 6.6 KiB

View 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

View 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

View 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

View 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>

View File

@@ -0,0 +1,4 @@
{
"name": "Ausweis",
"display": "fullscreen"
}

View 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);

View 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;
}

View 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')
}
}

View 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);
}

View 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
}

View 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
}