Compare commits

..

26 Commits

Author SHA1 Message Date
2def365cae quality of life improvemnts 2021-12-30 22:36:28 +01:00
c8b2539414 add version in about page 2021-12-22 15:20:30 +01:00
c8a25eb9bf release date 2021-12-22 14:57:58 +01:00
15bceb1715 add env 2021-12-22 14:56:33 +01:00
8acc4108ae Update README.md 2021-12-22 14:54:02 +01:00
0f708f53c0 changelog 2021-12-22 14:48:48 +01:00
8d03ad8e15 changelog 2021-12-22 14:47:43 +01:00
33829768eb time based fix 2021-12-22 14:46:06 +01:00
8cee6579e2 file upload 2021-12-22 13:10:08 +01:00
8eeb2a8de7 forgot missing packages 2021-12-21 10:35:02 +01:00
e4ce767444 add support for files 2021-12-21 00:15:04 +01:00
00fd514da5 feedback on to big error 2021-12-20 18:22:10 +01:00
ba38d2b819 proxy for cors 2021-12-20 18:14:59 +01:00
d0f83e6148 changelog 2021-12-20 17:54:17 +01:00
a040ad469e remove println 2021-12-20 17:54:09 +01:00
0c01866344 Merge pull request #15 from cupcakearmy/size-limit
Size limit
2021-12-20 17:45:05 +01:00
048c5198a2 Merge remote-tracking branch 'origin/main' into size-limit 2021-12-20 17:44:53 +01:00
f606916d97 Merge pull request #14 from cupcakearmy/pnpm
Pnpm
2021-12-20 17:43:11 +01:00
aea85c3b73 env docs 2021-12-20 17:42:35 +01:00
5f904b3971 cargo file 2021-12-20 17:42:30 +01:00
ac5d52a010 middleware to handle json payloads 2021-12-20 17:42:16 +01:00
8644a937d0 remove hardcoded limit 2021-12-20 17:42:08 +01:00
a0ebb97bc5 pnpm 2021-12-20 17:41:03 +01:00
19cd9b8507 examples on deployment 2021-12-16 13:54:15 +01:00
fe653e91c8 pnpm 2021-12-16 13:40:50 +01:00
a78ec72687 readme 2021-11-23 15:43:57 +01:00
35 changed files with 1175 additions and 534 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ node_modules
/.svelte /.svelte
/build /build
/functions /functions
.env

View File

@@ -5,114 +5,137 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.3.1] - 2021-12-30
### Added
- Short explanation in the home page.
### Changed
- Explanation in about & readme.
- Shorten server ids from 512 to 256bit.
## [1.3.0] - 2021-12-22
### Added
- Option to set a custom size limit.
- Options to share files.
### Changed
- Don't delete note if time is not expired yet
- Use pnpm instead of npm.
## [1.2.0] - 2021-11-11 ## [1.2.0] - 2021-11-11
### Changed ### Changed
- Switch to pnpm - Switch to pnpm.
### Security ### Security
- Dependencies updated - Dependencies updated.
## [1.1.1] - 2021-05-17 ## [1.1.1] - 2021-05-17
### Fixed ### Fixed
- Height on big displays - Height on big displays.
- About page - About page.
## [1.1.0] - 2021-05-16 ## [1.1.0] - 2021-05-16
### Security ### Security
- Using hash `#` instead of path - Using hash `#` instead of path.
## [1.0.11] - 2021-05-08 ## [1.0.11] - 2021-05-08
### Added ### Added
- loading text - loading text.
- description for created notes about availability - description for created notes about availability.
### Changed ### Changed
- iterations from 100 to 100k - iterations from 100 to 100k.
### Fixed ### Fixed
- time based view bug - time based view bug.
## [1.0.10] - 2021-05-08 ## [1.0.10] - 2021-05-08
### Fixed ### Fixed
- API endpoint was not reachable - API endpoint was not reachable.
## [1.0.9] - 2021-05-07 ## [1.0.9] - 2021-05-07
## Changed ## Changed
- Removed a dependency - Removed a dependency.
## [1.0.8] - 2021-05-05 ## [1.0.8] - 2021-05-05
### Added ### Added
- Manual theme override option - Manual theme override option.
### Fixed ### Fixed
- Removed Arm builds for now - Removed Arm builds for now.
- iOS style bugs - iOS style bugs.
## [1.0.7] - 2021-05-04 ## [1.0.7] - 2021-05-04
### Added ### Added
- Arm images - Arm images.
## [1.0.6] - 2021-05-04 ## [1.0.6] - 2021-05-04
### Added ### Added
- Always use encryption with random passwords included links - Always use encryption with random passwords included links.
## [1.0.5] - 2021-05-03 ## [1.0.5] - 2021-05-03
### Fixed ### Fixed
- Typos - Typos.
## [1.0.4] - 2021-05-02 ## [1.0.4] - 2021-05-02
### Added ### Added
- From scratch docker image - From scratch docker image.
## [1.0.3] - 2021-05-02 ## [1.0.3] - 2021-05-02
### Fixed ### Fixed
- Higher default text area - Higher default text area.
- Mobile touchups - Mobile touchups.
## [1.0.2] - 2021-05-02 ## [1.0.2] - 2021-05-02
### Fixed ### Fixed
- SVG Icons - SVG Icons.
## [1.0.1] - 2021-05-02 ## [1.0.1] - 2021-05-02
### Added ### Added
- Dark mode support - Dark mode support.
### Fixed ### Fixed
- Don't reload data on wrong password - Don't reload data on wrong password.
## [1.0.0] - 2021-05-02 ## [1.0.0] - 2021-05-02
Initial release Initial release.

512
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "1.2.0" version = "1.3.0"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2018" edition = "2018"
@@ -18,4 +18,7 @@ serde_json = "1"
lazy_static = "1" lazy_static = "1"
ring = "0.16" ring = "0.16"
bs62 = "0.1" bs62 = "0.1"
memcache = "0.15" memcache = "0.16"
byte-unit = "4"
dotenv = "0.15"
mime = "0.3"

View File

@@ -1,16 +1,21 @@
<p align="center"> <p align="center">
<img src="./design/Github.png"> <img src="./design/Github.png" alt="logo">
</p> </p>
![Docker pulls badge](https://img.shields.io/docker/pulls/cupcakearmy/cryptgeon) <a href="https://discord.gg/nuby6RnxZt">
![Docker image size badge](https://img.shields.io/docker/image-size/cupcakearmy/cryptgeon) <img alt="discord" src="https://img.shields.io/discord/252403122348097536?style=for-the-badge" />
![Latest version](https://img.shields.io/github/v/release/cupcakearmy/cryptgeon) <img alt="docker pulls" src="https://img.shields.io/docker/pulls/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Docker image size badge" src="https://img.shields.io/docker/image-size/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
</a>
<br/>
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<br/>
## About? ## About?
_cryptgeon_ is a secure, open source sharing note service inspired by [_PrivNote_](https://privnote.com) _cryptgeon_ is a secure, open source sharing note or file service inspired by [_PrivNote_](https://privnote.com)
## Demo ## Demo
@@ -19,22 +24,36 @@ Check out the demo and see for yourself https://cryptgeon.nicco.io.
## Features ## Features
- server cannot decrypt contents due to client side encryption - server cannot decrypt contents due to client side encryption
- view and time constraints - view or time constraints
- in memory, no persistence - in memory, no persistence
- obligatory dark mode support - obligatory dark mode support
## How does it work? ## How does it work?
each note has a 512bit generated <i>id</i> that is used to retrieve the note. data is stored in memory and never persisted to disk. each note has a generated <code>id (256bit)</code> and <code>key 256(bit)</code>. The
<code>id</code>
is used to save & retrieve the note. the note is then encrypted with aes in gcm mode on the
client side with the <code>key</code> and then sent to the server. data is stored in memory and
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents
of the notes even if it tried to.
## Screenshot ## Screenshot
![screenshot](./design/Screens.png) ![screenshot](./design/Screens.png)
## Environment Variables
| Variable | Default | Description |
| ------------ | ----------------- | --------------------------------------------------------------------------------------- |
| `MEMCACHE` | `memcached:11211` | Memcached URL to connect to. |
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/) |
## Deployment ## Deployment
`https` is required otherwise browsers will not support the cryptographic functions. `https` is required otherwise browsers will not support the cryptographic functions.
### Docker
Docker is the easiest way. There is the [official image here](https://hub.docker.com/r/cupcakearmy/cryptgeon). Docker is the easiest way. There is the [official image here](https://hub.docker.com/r/cupcakearmy/cryptgeon).
```yaml ```yaml
@@ -45,21 +64,64 @@ version: '3.7'
services: services:
memcached: memcached:
image: memcached:1-alpine image: memcached:1-alpine
entrypoint: memcached -m 128 # Limit to 128 MB Ram, customize at free will. entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
depends_on: depends_on:
- memcached - memcached
environment:
SIZE_LIMIT: 4M
ports: ports:
- 80:5000 - 80:5000
``` ```
### NGINX Proxy
See the [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx) folder. There an example with a simple proxy, and one with https. You need to specify the server names and certificates.
### Traefik 2
Assumptions:
- External proxy docker network `proxy`
- A certificate resolver `le`
- A https entrypoint `secure`
- Domain name `example.org`
```yaml
version: '3.8'
networks:
proxy:
external: true
services:
memcached:
image: memcached:1-alpine
restart: unless-stopped
entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
app:
image: cupcakearmy/cryptgeon:latest
restart: unless-stopped
depends_on:
- memcached
networks:
- default
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
- traefik.http.routers.cryptgeon.entrypoints=secure
- traefik.http.routers.cryptgeon.tls.certresolver=le
```
## Development ## Development
1. Clone 1. Clone
2. run `npm i` in the root and and client `client/` folders. 2. run `pnpm i` in the root and and client `client/` folders.
3. Run `npm run dev` to start development. 3. Run `pnpm run dev` to start development.
Running `npm run dev` in the root folder will start the following things Running `npm run dev` in the root folder will start the following things
@@ -67,7 +129,7 @@ Running `npm run dev` in the root folder will start the following things
- rust backend with hot reload - rust backend with hot reload
- client with hot reload - client with hot reload
You can see the app under [localhost:3000](http://localhost:3000). You can see the app under [localhost:1234](http://localhost:1234).
###### Attributions ###### Attributions

View File

@@ -1,4 +1,4 @@
├─ MIT: 43 ├─ MIT: 46
├─ MIT*: 2 ├─ MIT*: 2
├─ BSD-3-Clause: 2 ├─ BSD-3-Clause: 2
├─ ISC: 1 ├─ ISC: 1
1 ├─ MIT: 43 ├─ MIT: 46
2 ├─ MIT*: 2 ├─ MIT*: 2
3 ├─ BSD-3-Clause: 2 ├─ BSD-3-Clause: 2
4 ├─ ISC: 1 ├─ ISC: 1

View File

@@ -4,20 +4,22 @@
"dev": "svelte-kit dev", "dev": "svelte-kit dev",
"build": "svelte-kit build", "build": "svelte-kit build",
"preview": "svelte-kit preview", "preview": "svelte-kit preview",
"licenses": "npx license-checker --summary > licenses.csv" "licenses": "license-checker --summary > licenses.csv"
},
"devDependencies": {
"@sveltejs/adapter-static": "next",
"@sveltejs/kit": "next",
"svelte": "^3.44.1",
"svelte-preprocess": "^4.9.8",
"tslib": "^2.3.1",
"typescript": "^4.4.4",
"vite": "^2.6.14"
}, },
"type": "module", "type": "module",
"devDependencies": {
"@sveltejs/adapter-static": "^1.0.0-next.22",
"@sveltejs/kit": "^1.0.0-next.202",
"svelte": "^3.44.3",
"svelte-preprocess": "^4.10.1",
"tslib": "^2.3.1",
"typescript": "^4.5.4",
"vite": "^2.7.4"
},
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.0", "@fontsource/fira-mono": "^4.5.0",
"copy-to-clipboard": "^3.3.1" "copy-to-clipboard": "^3.3.1",
"file-saver": "^2.0.5",
"pretty-bytes": "^5.6.0"
} }
} }

239
client/pnpm-lock.yaml generated
View File

@@ -2,27 +2,31 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@fontsource/fira-mono': ^4.5.0 '@fontsource/fira-mono': ^4.5.0
'@sveltejs/adapter-static': next '@sveltejs/adapter-static': ^1.0.0-next.22
'@sveltejs/kit': next '@sveltejs/kit': ^1.0.0-next.202
copy-to-clipboard: ^3.3.1 copy-to-clipboard: ^3.3.1
svelte: ^3.44.1 file-saver: ^2.0.5
svelte-preprocess: ^4.9.8 pretty-bytes: ^5.6.0
svelte: ^3.44.3
svelte-preprocess: ^4.10.1
tslib: ^2.3.1 tslib: ^2.3.1
typescript: ^4.4.4 typescript: ^4.5.4
vite: ^2.6.14 vite: ^2.7.4
dependencies: dependencies:
'@fontsource/fira-mono': 4.5.0 '@fontsource/fira-mono': 4.5.0
copy-to-clipboard: 3.3.1 copy-to-clipboard: 3.3.1
file-saver: 2.0.5
pretty-bytes: 5.6.0
devDependencies: devDependencies:
'@sveltejs/adapter-static': 1.0.0-next.21 '@sveltejs/adapter-static': 1.0.0-next.22
'@sveltejs/kit': 1.0.0-next.195_svelte@3.44.1 '@sveltejs/kit': 1.0.0-next.202_svelte@3.44.3
svelte: 3.44.1 svelte: 3.44.3
svelte-preprocess: 4.9.8_svelte@3.44.1+typescript@4.4.4 svelte-preprocess: 4.10.1_svelte@3.44.3+typescript@4.5.4
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.4.4 typescript: 4.5.4
vite: 2.6.14 vite: 2.7.4
packages: packages:
@@ -30,30 +34,30 @@ packages:
resolution: {integrity: sha512-KE+d3wmgq/YKM0BqgUF7p2yeBNi805Nfof1lC1wJ7E9i2EWoC363sGdKG+MQBVm+ei3GYZu+Bo8Xha1w1pkB7g==} resolution: {integrity: sha512-KE+d3wmgq/YKM0BqgUF7p2yeBNi805Nfof1lC1wJ7E9i2EWoC363sGdKG+MQBVm+ei3GYZu+Bo8Xha1w1pkB7g==}
dev: false dev: false
/@rollup/pluginutils/4.1.1: /@rollup/pluginutils/4.1.2:
resolution: {integrity: sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==} resolution: {integrity: sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
dependencies: dependencies:
estree-walker: 2.0.2 estree-walker: 2.0.2
picomatch: 2.3.0 picomatch: 2.3.0
dev: true dev: true
/@sveltejs/adapter-static/1.0.0-next.21: /@sveltejs/adapter-static/1.0.0-next.22:
resolution: {integrity: sha512-B4+QoUVAaANKx+mHntG8SqF45zbj3Ct4Akg/cGauo6COyfKZRhO5OsMa+wPuT2TKJBZC4eEDK0p+p9nyQBkxKQ==} resolution: {integrity: sha512-Dc1V9Z72dA7caVwNxxzl9Jhcq4uN9N1udA2GKNTLMu3aWX3Cq+v6C2CddY9Aazr+F9h6J0vi9AienuH+ySRXzQ==}
dev: true dev: true
/@sveltejs/kit/1.0.0-next.195_svelte@3.44.1: /@sveltejs/kit/1.0.0-next.202_svelte@3.44.3:
resolution: {integrity: sha512-R2X4FgzXQhp63XOik6S1Flw91S2CEA7sTxdsnNFrq3O+bIN7pQhJhkm6zgH68MZANdDcq8oIiSRkxT4M3t1+jQ==} resolution: {integrity: sha512-rXmJ0FplkWvD1CaeCfejRYhOJYrlmeUm5Fkw7gIKDdWPQev5rqOhd9B9ZvRpq35oMqCAwaOfK+e5S6k+83feEQ==}
engines: {node: '>=14.13'} engines: {node: '>=14.13'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
svelte: ^3.44.0 svelte: ^3.44.0
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 1.0.0-next.30_svelte@3.44.1+vite@2.6.14 '@sveltejs/vite-plugin-svelte': 1.0.0-next.32_svelte@3.44.3+vite@2.7.4
cheap-watch: 1.0.4 cheap-watch: 1.0.4
sade: 1.7.4 sade: 1.7.4
svelte: 3.44.1 svelte: 3.44.3
vite: 2.6.14 vite: 2.7.4
transitivePeerDependencies: transitivePeerDependencies:
- diff-match-patch - diff-match-patch
- less - less
@@ -62,41 +66,41 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@sveltejs/vite-plugin-svelte/1.0.0-next.30_svelte@3.44.1+vite@2.6.14: /@sveltejs/vite-plugin-svelte/1.0.0-next.32_svelte@3.44.3+vite@2.7.4:
resolution: {integrity: sha512-YQqdMxjL1VgSFk4/+IY3yLwuRRapPafPiZTiaGEq1psbJYSNYUWx9F1zMm32GMsnogg3zn99mGJOqe3ld3HZSg==} resolution: {integrity: sha512-Lhf5BxVylosHIW6U2s6WDQA39ycd+bXivC8gHsXCJeLzxoHj7Pv7XAOk25xRSXT4wHg9DWFMBQh2DFU0DxHZ2g==}
engines: {node: ^14.13.1 || >= 16} engines: {node: ^14.13.1 || >= 16}
peerDependencies: peerDependencies:
diff-match-patch: ^1.0.5 diff-match-patch: ^1.0.5
svelte: ^3.44.0 svelte: ^3.44.0
vite: ^2.6.0 vite: ^2.7.0
peerDependenciesMeta: peerDependenciesMeta:
diff-match-patch: diff-match-patch:
optional: true optional: true
dependencies: dependencies:
'@rollup/pluginutils': 4.1.1 '@rollup/pluginutils': 4.1.2
debug: 4.3.2 debug: 4.3.3
kleur: 4.1.4 kleur: 4.1.4
magic-string: 0.25.7 magic-string: 0.25.7
require-relative: 0.8.7 require-relative: 0.8.7
svelte: 3.44.1 svelte: 3.44.3
svelte-hmr: 0.14.7_svelte@3.44.1 svelte-hmr: 0.14.7_svelte@3.44.3
vite: 2.6.14 vite: 2.7.4
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
/@types/node/16.11.7: /@types/node/17.0.1:
resolution: {integrity: sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==} resolution: {integrity: sha512-NXKvBVUzIbs6ylBwmOwHFkZS2EXCcjnqr8ZCRNaXBkHAf+3mn/rPcJxwrzuc6movh8fxQAsUUfYklJ/EG+hZqQ==}
dev: true dev: true
/@types/pug/2.0.5: /@types/pug/2.0.5:
resolution: {integrity: sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==} resolution: {integrity: sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==}
dev: true dev: true
/@types/sass/1.43.0: /@types/sass/1.43.1:
resolution: {integrity: sha512-DPSXNJ1rYLo88GyF9tuB4bsYGfpKI1a4+wOQmc+LI1SUoocm9QLRSpz0GxxuyjmJsYFIQo/dDlRSSpIXngff+w==} resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
dependencies: dependencies:
'@types/node': 16.11.7 '@types/node': 17.0.1
dev: true dev: true
/balanced-match/1.0.2: /balanced-match/1.0.2:
@@ -129,8 +133,8 @@ packages:
toggle-selection: 1.0.6 toggle-selection: 1.0.6
dev: false dev: false
/debug/4.3.2: /debug/4.3.3:
resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
peerDependencies: peerDependencies:
supports-color: '*' supports-color: '*'
@@ -150,170 +154,174 @@ packages:
resolution: {integrity: sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=} resolution: {integrity: sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=}
dev: true dev: true
/esbuild-android-arm64/0.13.13: /esbuild-android-arm64/0.13.15:
resolution: {integrity: sha512-T02aneWWguJrF082jZworjU6vm8f4UQ+IH2K3HREtlqoY9voiJUwHLRL6khRlsNLzVglqgqb7a3HfGx7hAADCQ==} resolution: {integrity: sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-darwin-64/0.13.13: /esbuild-darwin-64/0.13.15:
resolution: {integrity: sha512-wkaiGAsN/09X9kDlkxFfbbIgR78SNjMOfUhoel3CqKBDsi9uZhw7HBNHNxTzYUK8X8LAKFpbODgcRB3b/I8gHA==} resolution: {integrity: sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-darwin-arm64/0.13.13: /esbuild-darwin-arm64/0.13.15:
resolution: {integrity: sha512-b02/nNKGSV85Gw9pUCI5B48AYjk0vFggDeom0S6QMP/cEDtjSh1WVfoIFNAaLA0MHWfue8KBwoGVsN7rBshs4g==} resolution: {integrity: sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-freebsd-64/0.13.13: /esbuild-freebsd-64/0.13.15:
resolution: {integrity: sha512-ALgXYNYDzk9YPVk80A+G4vz2D22Gv4j4y25exDBGgqTcwrVQP8rf/rjwUjHoh9apP76oLbUZTmUmvCMuTI1V9A==} resolution: {integrity: sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-freebsd-arm64/0.13.13: /esbuild-freebsd-arm64/0.13.15:
resolution: {integrity: sha512-uFvkCpsZ1yqWQuonw5T1WZ4j59xP/PCvtu6I4pbLejhNo4nwjW6YalqnBvBSORq5/Ifo9S/wsIlVHzkzEwdtlw==} resolution: {integrity: sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==}
cpu: [arm64] cpu: [arm64]
os: [freebsd] os: [freebsd]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-linux-32/0.13.13: /esbuild-linux-32/0.13.15:
resolution: {integrity: sha512-yxR9BBwEPs9acVEwTrEE2JJNHYVuPQC9YGjRfbNqtyfK/vVBQYuw8JaeRFAvFs3pVJdQD0C2BNP4q9d62SCP4w==} resolution: {integrity: sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==}
cpu: [ia32] cpu: [ia32]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-linux-64/0.13.13: /esbuild-linux-64/0.13.15:
resolution: {integrity: sha512-kzhjlrlJ+6ESRB/n12WTGll94+y+HFeyoWsOrLo/Si0s0f+Vip4b8vlnG0GSiS6JTsWYAtGHReGczFOaETlKIw==} resolution: {integrity: sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-linux-arm/0.13.13: /esbuild-linux-arm/0.13.15:
resolution: {integrity: sha512-hXub4pcEds+U1TfvLp1maJ+GHRw7oizvzbGRdUvVDwtITtjq8qpHV5Q5hWNNn6Q+b3b2UxF03JcgnpzCw96nUQ==} resolution: {integrity: sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-linux-arm64/0.13.13: /esbuild-linux-arm64/0.13.15:
resolution: {integrity: sha512-KMrEfnVbmmJxT3vfTnPv/AiXpBFbbyExH13BsUGy1HZRPFMi5Gev5gk8kJIZCQSRfNR17aqq8sO5Crm2KpZkng==} resolution: {integrity: sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-linux-mips64le/0.13.13: /esbuild-linux-mips64le/0.13.15:
resolution: {integrity: sha512-cJT9O1LYljqnnqlHaS0hdG73t7hHzF3zcN0BPsjvBq+5Ad47VJun+/IG4inPhk8ta0aEDK6LdP+F9299xa483w==} resolution: {integrity: sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==}
cpu: [mips64el] cpu: [mips64el]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-linux-ppc64le/0.13.13: /esbuild-linux-ppc64le/0.13.15:
resolution: {integrity: sha512-+rghW8st6/7O6QJqAjVK3eXzKkZqYAw6LgHv7yTMiJ6ASnNvghSeOcIvXFep3W2oaJc35SgSPf21Ugh0o777qQ==} resolution: {integrity: sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-netbsd-64/0.13.13: /esbuild-netbsd-64/0.13.15:
resolution: {integrity: sha512-A/B7rwmzPdzF8c3mht5TukbnNwY5qMJqes09ou0RSzA5/jm7Jwl/8z853ofujTFOLhkNHUf002EAgokzSgEMpQ==} resolution: {integrity: sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==}
cpu: [x64] cpu: [x64]
os: [netbsd] os: [netbsd]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-openbsd-64/0.13.13: /esbuild-openbsd-64/0.13.15:
resolution: {integrity: sha512-szwtuRA4rXKT3BbwoGpsff6G7nGxdKgUbW9LQo6nm0TVCCjDNDC/LXxT994duIW8Tyq04xZzzZSW7x7ttDiw1w==} resolution: {integrity: sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==}
cpu: [x64] cpu: [x64]
os: [openbsd] os: [openbsd]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-sunos-64/0.13.13: /esbuild-sunos-64/0.13.15:
resolution: {integrity: sha512-ihyds9O48tVOYF48iaHYUK/boU5zRaLOXFS+OOL3ceD39AyHo46HVmsJLc7A2ez0AxNZCxuhu+P9OxfPfycTYQ==} resolution: {integrity: sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==}
cpu: [x64] cpu: [x64]
os: [sunos] os: [sunos]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-windows-32/0.13.13: /esbuild-windows-32/0.13.15:
resolution: {integrity: sha512-h2RTYwpG4ldGVJlbmORObmilzL8EECy8BFiF8trWE1ZPHLpECE9//J3Bi+W3eDUuv/TqUbiNpGrq4t/odbayUw==} resolution: {integrity: sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-windows-64/0.13.13: /esbuild-windows-64/0.13.15:
resolution: {integrity: sha512-oMrgjP4CjONvDHe7IZXHrMk3wX5Lof/IwFEIbwbhgbXGBaN2dke9PkViTiXC3zGJSGpMvATXVplEhlInJ0drHA==} resolution: {integrity: sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild-windows-arm64/0.13.13: /esbuild-windows-arm64/0.13.15:
resolution: {integrity: sha512-6fsDfTuTvltYB5k+QPah/x7LrI2+OLAJLE3bWLDiZI6E8wXMQU+wLqtEO/U/RvJgVY1loPs5eMpUBpVajczh1A==} resolution: {integrity: sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/esbuild/0.13.13: /esbuild/0.13.15:
resolution: {integrity: sha512-Z17A/R6D0b4s3MousytQ/5i7mTCbaF+Ua/yPfoe71vdTv4KBvVAvQ/6ytMngM2DwGJosl8WxaD75NOQl2QF26Q==} resolution: {integrity: sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
optionalDependencies: optionalDependencies:
esbuild-android-arm64: 0.13.13 esbuild-android-arm64: 0.13.15
esbuild-darwin-64: 0.13.13 esbuild-darwin-64: 0.13.15
esbuild-darwin-arm64: 0.13.13 esbuild-darwin-arm64: 0.13.15
esbuild-freebsd-64: 0.13.13 esbuild-freebsd-64: 0.13.15
esbuild-freebsd-arm64: 0.13.13 esbuild-freebsd-arm64: 0.13.15
esbuild-linux-32: 0.13.13 esbuild-linux-32: 0.13.15
esbuild-linux-64: 0.13.13 esbuild-linux-64: 0.13.15
esbuild-linux-arm: 0.13.13 esbuild-linux-arm: 0.13.15
esbuild-linux-arm64: 0.13.13 esbuild-linux-arm64: 0.13.15
esbuild-linux-mips64le: 0.13.13 esbuild-linux-mips64le: 0.13.15
esbuild-linux-ppc64le: 0.13.13 esbuild-linux-ppc64le: 0.13.15
esbuild-netbsd-64: 0.13.13 esbuild-netbsd-64: 0.13.15
esbuild-openbsd-64: 0.13.13 esbuild-openbsd-64: 0.13.15
esbuild-sunos-64: 0.13.13 esbuild-sunos-64: 0.13.15
esbuild-windows-32: 0.13.13 esbuild-windows-32: 0.13.15
esbuild-windows-64: 0.13.13 esbuild-windows-64: 0.13.15
esbuild-windows-arm64: 0.13.13 esbuild-windows-arm64: 0.13.15
dev: true dev: true
/estree-walker/2.0.2: /estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true dev: true
/file-saver/2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
dev: false
/fs.realpath/1.0.0: /fs.realpath/1.0.0:
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
dev: true dev: true
@@ -441,15 +449,20 @@ packages:
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
dev: true dev: true
/postcss/8.3.11: /postcss/8.4.5:
resolution: {integrity: sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==} resolution: {integrity: sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
dependencies: dependencies:
nanoid: 3.1.30 nanoid: 3.1.30
picocolors: 1.0.0 picocolors: 1.0.0
source-map-js: 0.6.2 source-map-js: 1.0.1
dev: true dev: true
/pretty-bytes/5.6.0:
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
engines: {node: '>=6'}
dev: false
/require-relative/0.8.7: /require-relative/0.8.7:
resolution: {integrity: sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=} resolution: {integrity: sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=}
dev: true dev: true
@@ -468,8 +481,8 @@ packages:
glob: 7.2.0 glob: 7.2.0
dev: true dev: true
/rollup/2.59.0: /rollup/2.61.1:
resolution: {integrity: sha512-l7s90JQhCQ6JyZjKgo7Lq1dKh2RxatOM+Jr6a9F7WbS9WgKbocyUSeLmZl8evAse7y96Ae98L2k1cBOwWD8nHw==} resolution: {integrity: sha512-BbTXlEvB8d+XFbK/7E5doIcRtxWPRiqr0eb5vQ0+2paMM04Ye4PZY5nHOQef2ix24l/L0SpLd5hwcH15QHPdvA==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
hasBin: true hasBin: true
optionalDependencies: optionalDependencies:
@@ -502,8 +515,8 @@ packages:
sourcemap-codec: 1.4.8 sourcemap-codec: 1.4.8
dev: true dev: true
/source-map-js/0.6.2: /source-map-js/1.0.1:
resolution: {integrity: sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==} resolution: {integrity: sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
@@ -518,16 +531,16 @@ packages:
min-indent: 1.0.1 min-indent: 1.0.1
dev: true dev: true
/svelte-hmr/0.14.7_svelte@3.44.1: /svelte-hmr/0.14.7_svelte@3.44.3:
resolution: {integrity: sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==} resolution: {integrity: sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==}
peerDependencies: peerDependencies:
svelte: '>=3.19.0' svelte: '>=3.19.0'
dependencies: dependencies:
svelte: 3.44.1 svelte: 3.44.3
dev: true dev: true
/svelte-preprocess/4.9.8_svelte@3.44.1+typescript@4.4.4: /svelte-preprocess/4.10.1_svelte@3.44.3+typescript@4.5.4:
resolution: {integrity: sha512-EQS/oRZzMtYdAprppZxY3HcysKh11w54MgA63ybtL+TAZ4hVqYOnhw41JVJjWN9dhPnNjjLzvbZ2tMhTsla1Og==} resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==}
engines: {node: '>= 9.11.2'} engines: {node: '>= 9.11.2'}
requiresBuild: true requiresBuild: true
peerDependencies: peerDependencies:
@@ -542,7 +555,7 @@ packages:
stylus: ^0.54.7 stylus: ^0.54.7
sugarss: ^2.0.0 sugarss: ^2.0.0
svelte: ^3.23.0 svelte: ^3.23.0
typescript: ^3.9.5 || ^4.0.0 typescript: ^4.5.2
peerDependenciesMeta: peerDependenciesMeta:
'@babel/core': '@babel/core':
optional: true optional: true
@@ -568,17 +581,17 @@ packages:
optional: true optional: true
dependencies: dependencies:
'@types/pug': 2.0.5 '@types/pug': 2.0.5
'@types/sass': 1.43.0 '@types/sass': 1.43.1
detect-indent: 6.1.0 detect-indent: 6.1.0
magic-string: 0.25.7 magic-string: 0.25.7
sorcery: 0.10.0 sorcery: 0.10.0
strip-indent: 3.0.0 strip-indent: 3.0.0
svelte: 3.44.1 svelte: 3.44.3
typescript: 4.4.4 typescript: 4.5.4
dev: true dev: true
/svelte/3.44.1: /svelte/3.44.3:
resolution: {integrity: sha512-4DrCEJoBvdR689efHNSxIQn2pnFwB7E7j2yLEJtHE/P8hxwZWIphCtJ8are7bjl/iVMlcEf5uh5pJ68IwR09vQ==} resolution: {integrity: sha512-aGgrNCip5PQFNfq9e9tmm7EYxWLVHoFsEsmKrtOeRD8dmoGDdyTQ+21xd7qgFd8MNdKGSYvg7F9dr+Tc0yDymg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
dev: true dev: true
@@ -590,14 +603,14 @@ packages:
resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==}
dev: true dev: true
/typescript/4.4.4: /typescript/4.5.4:
resolution: {integrity: sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==} resolution: {integrity: sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==}
engines: {node: '>=4.2.0'} engines: {node: '>=4.2.0'}
hasBin: true hasBin: true
dev: true dev: true
/vite/2.6.14: /vite/2.7.4:
resolution: {integrity: sha512-2HA9xGyi+EhY2MXo0+A2dRsqsAG3eFNEVIo12olkWhOmc8LfiM+eMdrXf+Ruje9gdXgvSqjLI9freec1RUM5EA==} resolution: {integrity: sha512-f+0426k9R/roz5mRNwJlQ+6UOnhCwIypJSbfgCmsVzVJe9jTTM5iRX2GWYUean+iqPBWaU/dYLryx9AoH2pmrw==}
engines: {node: '>=12.2.0'} engines: {node: '>=12.2.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -612,10 +625,10 @@ packages:
stylus: stylus:
optional: true optional: true
dependencies: dependencies:
esbuild: 0.13.13 esbuild: 0.13.15
postcss: 8.3.11 postcss: 8.4.5
resolve: 1.20.0 resolve: 1.20.0
rollup: 2.59.0 rollup: 2.61.1
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true

View File

@@ -97,3 +97,28 @@ fieldset {
padding: 0; padding: 0;
border: none; border: none;
} }
.box {
width: 100%;
min-height: min(calc(100vh - 30rem), 20rem);
margin: 0;
border: 2px solid var(--ui-bg-1);
resize: vertical;
outline: none;
padding: 0.5rem;
}
@media screen and (max-width: 30rem) {
.box {
min-height: calc(100vh - 25rem);
}
}
.box:hover,
.box:focus {
border-color: var(--ui-clr-primary);
}
.tr {
text-align: right;
}

View File

@@ -1,51 +1,74 @@
import { dev } from '$app/env' export type NoteMeta = { type: 'text' | 'file' }
export type Note = { export type Note = {
contents: string contents: string
meta: NoteMeta
views?: number views?: number
expiration?: number expiration?: number
} }
export type NoteInfo = {} export type NoteInfo = {}
export type NotePublic = Pick<Note, 'contents'> export type NotePublic = Pick<Note, 'contents' | 'meta'>
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
contents: string
}
type CallOptions = { type CallOptions = {
url: string url: string
method: string method: string
body?: any body?: any
} }
const base = dev ? 'http://localhost:5000/api/' : '/api/'
async function call(options: CallOptions) { export class PayloadToLargeError extends Error {}
return fetch(base + options.url, {
export async function call(options: CallOptions) {
const response = await fetch('/api/' + options.url, {
method: options.method, method: options.method,
body: options.body === undefined ? undefined : JSON.stringify(options.body), body: options.body === undefined ? undefined : JSON.stringify(options.body),
mode: 'cors', mode: 'cors',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}).then((r) => r.json()) })
if (!response.ok) {
if (response.status === 413) throw new PayloadToLargeError()
else throw new Error('API call failed')
}
return response.json()
} }
export async function create(note: Note) { export async function create(note: Note) {
const { meta, ...rest } = note
const body: NoteCreate = {
...rest,
meta: JSON.stringify(meta),
}
const data = await call({ const data = await call({
url: 'notes', url: 'notes/',
method: 'post', method: 'post',
body: note, body,
}) })
return data as { id: string } return data as { id: string }
} }
export async function get(id: string) { export async function get(id: string): Promise<NotePublic> {
const data = await call({ const data = await call({
url: `notes/${id}`, url: `notes/${id}`,
method: 'delete', method: 'delete',
}) })
return data as NotePublic const { contents, meta } = data
return {
contents,
meta: JSON.parse(meta) as NoteMeta,
}
} }
export async function info(id: string) { export async function info(id: string): Promise<NoteInfo> {
const data = await call({ const data = await call({
url: `notes/${id}`, url: `notes/${id}`,
method: 'get', method: 'get',
}) })
return data as NoteInfo return data
} }

13
client/src/lib/files.ts Normal file
View File

@@ -0,0 +1,13 @@
export class Files {
static toString(f: File | Blob): Promise<string> {
const reader = new window.FileReader()
reader.readAsDataURL(f)
return new Promise((resolve) => {
reader.onloadend = () => resolve(reader.result as string)
})
}
static fromString(s: string): Promise<Blob> {
return fetch(s).then((r) => r.blob())
}
}

View File

@@ -0,0 +1,17 @@
import { call } from '$lib/api'
import { writable } from 'svelte/store'
export type Status = {
version: string
max_size: number
}
export const status = writable<null | Status>(null)
export async function init() {
const data = await call({
url: 'status',
method: 'get',
})
status.set(data)
}

View File

@@ -0,0 +1,19 @@
<script lang="ts">
export let title: string
</script>
<p>
<b>{title}</b>
<slot />
</p>
<style>
b {
display: block;
margin-bottom: 0.25rem;
}
p > :global(span) {
padding-left: 1.25em;
}
</style>

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import type { FileDTO } from '$lib/api'
import { Files } from '$lib/files'
import { createEventDispatcher } from 'svelte'
import MaxSize from './MaxSize.svelte'
export let label: string = ''
let files: File[] = []
const dispatch = createEventDispatcher<{ file: string }>()
async function onInput(e: Event) {
const input = e.target as HTMLInputElement
if (input.files.length) {
files = Array.from(input.files)
const data: FileDTO[] = await Promise.all(
files.map(async (file) => ({
name: file.name,
type: file.type,
size: file.size,
contents: await Files.toString(file),
}))
)
console.debug(
'files',
data.map((d) => d.contents.length)
)
dispatch('file', JSON.stringify(data))
} else {
dispatch('file', '')
}
}
</script>
<label>
<small>
{label}
</small>
<input type="file" on:change={onInput} multiple />
<div class="box">
{#if files.length}
<div>
<b>Selected Files</b>
{#each files as file}
<div class="file">
{file.name}
</div>
{/each}
</div>
{:else}
<div>
<b>No Files Selected</b>
<br />
<small>max: <MaxSize /></small>
</div>
{/if}
</div>
</label>
<style>
input {
display: none;
}
.box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { status } from '$lib/stores/status'
import prettyBytes from 'pretty-bytes'
</script>
<span>
{#if $status !== null}
{prettyBytes($status.max_size, { binary: true })}
{:else}
loading...
{/if}
</span>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import type { FileDTO, NotePublic } from '$lib/api'
import { Files } from '$lib/files'
import copy from 'copy-to-clipboard'
import { saveAs } from 'file-saver'
import prettyBytes from 'pretty-bytes'
import Button from './Button.svelte'
export let note: NotePublic
let files: FileDTO[] = []
$: if (note.meta.type === 'file') {
files = JSON.parse(note.contents) as FileDTO[]
}
$: download = () => {
for (const file of files) {
downloadFile(file)
}
}
async function downloadFile(file: FileDTO) {
const f = new File([await Files.fromString(file.contents)], file.name, {
type: file.type,
})
saveAs(f)
}
</script>
<p class="error-text">you will <b>not</b> get the chance to see the note again.</p>
{#if note.meta.type === 'text'}
<div class="note" data-testid="note-result">
{note.contents}
</div>
<Button on:click={() => copy(note.contents)}>copy to clipboard</Button>
{:else}
{#each files as file}
<div class="note file" data-testid="note-result">
<b on:click={() => downloadFile(file)}> {file.name}</b>
<small> {file.type} {prettyBytes(file.size)}</small>
</div>
{/each}
<Button on:click={download}>download all</Button>
{/if}
<style>
.note {
width: 100%;
margin: 0;
padding: 0;
border: 2px solid var(--ui-bg-1);
outline: none;
padding: 0.5rem;
white-space: pre;
overflow: auto;
margin-bottom: 0.5rem;
}
.note b {
cursor: pointer;
}
.note.file {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -4,7 +4,7 @@
export let color = true export let color = true
</script> </script>
<div> <div {...$$restProps}>
<label class="switch"> <label class="switch">
<small>{label}</small> <small>{label}</small>
<input type="checkbox" bind:checked={value} /> <input type="checkbox" bind:checked={value} />

View File

@@ -7,28 +7,5 @@
<small> <small>
{label} {label}
</small> </small>
<textarea {...$$restProps} bind:value /> <textarea class="box" {...$$restProps} bind:value />
</label> </label>
<style>
textarea {
width: 100%;
min-height: min(calc(100vh - 30rem), 30rem);
margin: 0;
border: 2px solid var(--ui-bg-1);
resize: vertical;
outline: none;
padding: 0.5rem;
}
@media screen and (max-width: 30rem) {
textarea {
min-height: calc(100vh - 25rem);
}
}
textarea:hover,
textarea:focus {
border-color: var(--ui-clr-primary);
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
export enum Theme { enum Theme {
Dark = 'dark', Dark = 'dark',
Light = 'light', Light = 'light',
Auto = 'auto', Auto = 'auto',

View File

@@ -1,20 +1,23 @@
<script lang="ts"> <script lang="ts">
import type { Note } from '$lib/api' import { create, Note, PayloadToLargeError } from '$lib/api'
import { create } from '$lib/api' import { encrypt, getKeyFromString, getRandomBytes, Hex } from '$lib/crypto'
import { getKeyFromString, encrypt, Hex, getRandomBytes } from '$lib/crypto'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import FileUpload from '$lib/ui/FileUpload.svelte'
import MaxSize from '$lib/ui/MaxSize.svelte'
import Switch from '$lib/ui/Switch.svelte' import Switch from '$lib/ui/Switch.svelte'
import TextArea from '$lib/ui/TextArea.svelte' import TextArea from '$lib/ui/TextArea.svelte'
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import { blur } from 'svelte/transition'
let note: Note = { let note: Note = {
contents: '', contents: '',
meta: { type: 'text' },
views: 1, views: 1,
expiration: 60, expiration: 60,
} }
let result: { password: string; id: string } | null = null let result: { password: string; id: string } | null = null
let advanced = false let advanced = false
let file = false
let type = false let type = false
let message = '' let message = ''
let loading = false let loading = false
@@ -31,6 +34,12 @@
message = 'the note will expire and be destroyed after ' + fraction message = 'the note will expire and be destroyed after ' + fraction
} }
$: note.meta.type = file ? 'file' : 'text'
$: if (!file) {
note.contents = ''
}
async function submit() { async function submit() {
try { try {
error = null error = null
@@ -39,6 +48,7 @@
const key = await getKeyFromString(password) const key = await getKeyFromString(password)
const data: Note = { const data: Note = {
contents: await encrypt(note.contents, key), contents: await encrypt(note.contents, key),
meta: note.meta,
} }
// @ts-ignore // @ts-ignore
if (type) data.expiration = parseInt(note.expiration) if (type) data.expiration = parseInt(note.expiration)
@@ -51,8 +61,11 @@
id: response.id, id: response.id,
} }
} catch (e) { } catch (e) {
console.error(e) if (e instanceof PayloadToLargeError) {
error = 'could not create note.' error = 'could not create not. note is to big'
} else {
error = 'could not create note. please try again.'
}
} finally { } finally {
loading = false loading = false
} }
@@ -84,18 +97,32 @@
<br /> <br />
<Button on:click={reset}>new note</Button> <Button on:click={reset}>new note</Button>
{:else} {:else}
<p>
Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and
share the link.
</p>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<fieldset disabled={loading}> <fieldset disabled={loading}>
<TextArea {#if file}
label="note" <FileUpload label="file" on:file={(f) => (note.contents = f.detail)} />
bind:value={note.contents} {:else}
placeholder="..." <TextArea
data-testid="input-note" label="note"
/> bind:value={note.contents}
placeholder="..."
data-testid="input-note"
/>
{/if}
<div class="bottom"> <div class="bottom">
<Switch class="file" label="file" bind:value={file} />
<Switch label="advanced" bind:value={advanced} /> <Switch label="advanced" bind:value={advanced} />
<Button type="submit" data-testid="button-create">create</Button> <div class="grow" />
<div class="tr">
<small>max: <MaxSize /> </small>
<br />
<Button type="submit" data-testid="button-create">create</Button>
</div>
</div> </div>
{#if error} {#if error}
@@ -111,37 +138,30 @@
{/if} {/if}
</p> </p>
<div class="advanced" class:hidden={!advanced}> {#if advanced}
<br /> <div transition:blur={{ duration: 250 }}>
<div class="fields"> <br />
<TextInput <div class="fields">
type="number" <TextInput
label="views" type="number"
bind:value={note.views} label="views"
disabled={type} bind:value={note.views}
max={100} disabled={type}
/> max={100}
<div class="middle-switch"> />
<Switch label="mode" bind:value={type} color={false} /> <div class="middle-switch">
<Switch label="mode" bind:value={type} color={false} />
</div>
<TextInput
type="number"
label="minutes"
bind:value={note.expiration}
disabled={!type}
max={360}
/>
</div> </div>
<TextInput
type="number"
label="minutes"
bind:value={note.expiration}
disabled={!type}
max={360}
/>
</div> </div>
</div> {/if}
<style>
.fields {
display: flex;
}
.spacer {
width: 3rem;
}
</style>
</fieldset> </fieldset>
</form> </form>
{/if} {/if}
@@ -149,22 +169,27 @@
<style> <style>
.bottom { .bottom {
display: flex; display: flex;
justify-content: space-between;
align-items: flex-end; align-items: flex-end;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.bottom :global(.file) {
margin-right: 0.5rem;
}
.grow {
flex: 1;
}
.middle-switch { .middle-switch {
margin: 0 1rem; margin: 0 1rem;
} }
.advanced { .error-text {
max-height: 14em; margin-top: 0.5rem;
overflow: hidden;
transition: var(--ui-anim);
} }
.advanced.hidden { .fields {
max-height: 0; display: flex;
} }
</style> </style>

View File

@@ -1,8 +1,13 @@
<script lang="ts"> <script lang="ts">
import { init } from '$lib/stores/status'
import Footer from '$lib/views/Footer.svelte' import Footer from '$lib/views/Footer.svelte'
import Header from '$lib/views/Header.svelte' import Header from '$lib/views/Header.svelte'
import { onMount } from 'svelte'
import '../app.css' import '../app.css'
onMount(() => {
init()
})
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,5 +1,8 @@
<script context="module"> <script context="module">
import { browser, dev } from '$app/env' import { browser, dev } from '$app/env'
import { status } from '$lib/stores/status'
import AboutParagraph from '$lib/ui/AboutParagraph.svelte'
export const hydrate = dev export const hydrate = dev
export const router = browser export const router = browser
export const prerender = true export const prerender = true
@@ -13,45 +16,54 @@
<h1>About</h1> <h1>About</h1>
<p> <p>
<i>cryptgeon</i> is a secure, open source sharing note service inspired by <i>cryptgeon</i> is a secure, open source sharing note / file service inspired by
<a href="https://privnote.com"><i>PrivNote</i></a>. <a href="https://privnote.com"><i>PrivNote</i></a>.
</p> </p>
<p> <AboutParagraph title="how does it work?">
<b>▶ how does it work?</b> <span>
<br /> each note has a generated <code>id (256bit)</code> and <code>key 256(bit)</code>. The
each note has a 512bit generated <i>id</i> that is used to retrieve the note. the note is then encrypted <code>id</code>
with aes in gcm mode on the client side and then sent to the server. data is stored in memory and is used to save & retrieve the note. the note is then encrypted with aes in gcm mode on the client
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents side with the <code>key</code> and then sent to the server. data is stored in memory and never
of the notes even if it tried to. persisted to disk. the server never sees the encryption key and cannot decrypt the contents of
</p> the notes even if it tried to.
</span>
</AboutParagraph>
<b>▶ features</b> <AboutParagraph title="features">
<ul> <ul>
<li>server cannot decrypt contents due to client side encryption</li> <li>server cannot decrypt contents due to client side encryption</li>
<li>view and time constraints</li> <li>view and time constraints</li>
<li>in memory, no persistence</li> <li>in memory, no persistence</li>
</ul> </ul>
</AboutParagraph>
<p> <AboutParagraph title="tech stack">
<b>▶ tech stack</b> <span>
<br /> the backend is written in rust and the frontend is svelte and typescript.
the backend is written in rust and the frontend is svelte and typescript. <br />
<br /> you are welcomed to check & audit the
you are welcomed to check & audit the <a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">source code</a source code
>. </a>.
</p> </span>
</AboutParagraph>
<p> <AboutParagraph title="attribution">
<br /> <span>
<b>▶ attributions</b>
<br />
<small>
icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from
<a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
</small> </span>
</p> </AboutParagraph>
<AboutParagraph title="version">
<span>
{#if $status}
<code>v{$status.version}</code>
{/if}
</span>
</AboutParagraph>
</section> </section>
<style> <style>

View File

@@ -7,13 +7,12 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'
import copy from 'copy-to-clipboard'
import type { NotePublic } from '$lib/api' import type { NotePublic } from '$lib/api'
import { info, get } from '$lib/api' import { get, info } from '$lib/api'
import { decrypt, getKeyFromString } from '$lib/crypto' import { decrypt, getKeyFromString } from '$lib/crypto'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import ShowNote from '$lib/ui/ShowNote.svelte'
import { onMount } from 'svelte'
export let id: string export let id: string
@@ -29,7 +28,6 @@
loading = true loading = true
error = null error = null
password = window.location.hash.slice(1) password = window.location.hash.slice(1)
console.log(password)
await info(id) await info(id)
exists = true exists = true
} catch { } catch {
@@ -61,12 +59,7 @@
note was not found or was already deleted. note was not found or was already deleted.
</p> </p>
{:else if note && !error} {:else if note && !error}
<p class="error-text">you will not get the chance to see the note again.</p> <ShowNote {note} />
<div class="note" data-testid="note-result">
{note.contents}
</div>
<br />
<Button on:click={() => copy(note.contents)}>copy to clipboard</Button>
{:else} {:else}
<form on:submit|preventDefault={show}> <form on:submit|preventDefault={show}>
<fieldset> <fieldset>
@@ -86,16 +79,3 @@
{#if loading} {#if loading}
<p>loading...</p> <p>loading...</p>
{/if} {/if}
<style>
.note {
width: 100%;
margin: 0;
padding: 0;
border: 2px solid var(--ui-bg-1);
outline: none;
padding: 0.5rem;
white-space: pre;
overflow: auto;
}
</style>

View File

@@ -6,7 +6,8 @@ version: '3.7'
services: services:
memcached: memcached:
image: memcached:1-alpine image: memcached:1-alpine
entrypoint: memcached -m 128 restart: unless-stopped
entrypoint: memcached -m 128M -I 4M
ports: ports:
- 11211:11211 - 11211:11211
@@ -14,5 +15,7 @@ services:
build: . build: .
depends_on: depends_on:
- memcached - memcached
environment:
SIZE_LIMIT: 4M
ports: ports:
- 5000:5000 - 80:5000

View File

@@ -0,0 +1,22 @@
version: '3.8'
services:
memcached:
image: memcached:1-alpine
entrypoint: memcached -m 128 # Limit to 128 MB Ram, customize at free will.
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- memcached
proxy:
image: nginx:alpine
depends_on:
- app
volumes:
- ./nginx-plain.conf:/etc/nginx/conf.d/default.conf
# Or with tls
# - ./nginx-tls.conf:/etc/nginx/conf.d/default.conf
ports:
- 80:80

View File

@@ -0,0 +1,13 @@
server {
listen 80;
listen [::]:80;
server_name _;
location / {
proxy_pass http://app:5000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,29 @@
# You should change the server_name to something sensible.
# Also you need to specify the path to the ssl certificates.
server {
listen 80;
listen [::]:80;
server_name _;
# Enforce HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name _;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_trusted_certificate /path/to/fullchain.pem;
location / {
proxy_pass http://app:5000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -2,10 +2,16 @@
"scripts": { "scripts": {
"dev:docker": "docker-compose up memcached", "dev:docker": "docker-compose up memcached",
"dev:backend": "cargo watch -x 'run --bin cryptgeon'", "dev:backend": "cargo watch -x 'run --bin cryptgeon'",
"dev:front": "npm --prefix client run dev", "dev:front": "pnpm --prefix client run dev",
"dev:proxy": "node proxy.mjs",
"dev": "run-p dev:*" "dev": "run-p dev:*"
}, },
"devDependencies": { "devDependencies": {
"http-proxy": "^1.18.1",
"npm-run-all": "^4.1.5" "npm-run-all": "^4.1.5"
},
"dependencies": {
"file-saver": "^2.0.5",
"pretty-bytes": "^5.6.0"
} }
} }

46
pnpm-lock.yaml generated
View File

@@ -1,9 +1,17 @@
lockfileVersion: 5.3 lockfileVersion: 5.3
specifiers: specifiers:
file-saver: ^2.0.5
http-proxy: ^1.18.1
npm-run-all: ^4.1.5 npm-run-all: ^4.1.5
pretty-bytes: ^5.6.0
dependencies:
file-saver: 2.0.5
pretty-bytes: 5.6.0
devDependencies: devDependencies:
http-proxy: 1.18.1
npm-run-all: 4.1.5 npm-run-all: 4.1.5
packages: packages:
@@ -116,6 +124,24 @@ packages:
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
dev: true dev: true
/eventemitter3/4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: true
/file-saver/2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
dev: false
/follow-redirects/1.14.6:
resolution: {integrity: sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: true
/function-bind/1.1.1: /function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true dev: true
@@ -157,6 +183,17 @@ packages:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true dev: true
/http-proxy/1.18.1:
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
engines: {node: '>=8.0.0'}
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.14.6
requires-port: 1.0.0
transitivePeerDependencies:
- debug
dev: true
/is-arrayish/0.2.1: /is-arrayish/0.2.1:
resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=} resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=}
dev: true dev: true
@@ -330,6 +367,11 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/pretty-bytes/5.6.0:
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
engines: {node: '>=6'}
dev: false
/read-pkg/3.0.0: /read-pkg/3.0.0:
resolution: {integrity: sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=} resolution: {integrity: sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -339,6 +381,10 @@ packages:
path-type: 3.0.0 path-type: 3.0.0
dev: true dev: true
/requires-port/1.0.0:
resolution: {integrity: sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=}
dev: true
/resolve/1.20.0: /resolve/1.20.0:
resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==} resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==}
dependencies: dependencies:

19
proxy.mjs Normal file
View File

@@ -0,0 +1,19 @@
import http from 'http'
import httpProxy from 'http-proxy'
function start() {
try {
const proxy = httpProxy.createProxyServer({})
const server = http.createServer(function (req, res) {
const target = req.url.startsWith('/api/') ? 'http://localhost:5000' : 'http://localhost:3000'
proxy.web(req, res, { target })
})
server.listen(1234)
server.on('error', () => start())
} catch (e) {
start()
}
}
start()

View File

@@ -1,23 +1,27 @@
use actix_web::{middleware, web, App, HttpServer}; use actix_web::{middleware, web, App, HttpServer};
use dotenv::dotenv;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
mod client; mod client;
mod note; mod note;
mod size;
mod store; mod store;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
HttpServer::new(|| { dotenv().ok();
return HttpServer::new(|| {
App::new() App::new()
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(middleware::DefaultHeaders::default()) .wrap(middleware::DefaultHeaders::default())
.configure(size::init)
.configure(note::init) .configure(note::init)
.configure(client::init) .configure(client::init)
.default_service(web::resource("").route(web::get().to(client::fallback_fn))) .default_service(web::resource("").route(web::get().to(client::fallback_fn)))
}) })
.bind("0.0.0.0:5000")? .bind("0.0.0.0:5000")?
.run() .run()
.await .await;
} }

View File

@@ -4,9 +4,10 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct Note { pub struct Note {
pub meta: String,
pub contents: String, pub contents: String,
pub views: Option<u8>, pub views: Option<u8>,
pub expiration: Option<u64>, pub expiration: Option<u32>,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
@@ -14,11 +15,12 @@ pub struct NoteInfo {}
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct NotePublic { pub struct NotePublic {
pub meta: String,
pub contents: String, pub contents: String,
} }
pub fn generate_id() -> String { pub fn generate_id() -> String {
let mut id: [u8; 64] = [0; 64]; let mut id: [u8; 32] = [0; 32];
let sr = ring::rand::SystemRandom::new(); let sr = ring::rand::SystemRandom::new();
let _ = sr.fill(&mut id); let _ = sr.fill(&mut id);
return bs62::encode_data(&id); return bs62::encode_data(&id);

View File

@@ -3,13 +3,14 @@ use serde::{Deserialize, Serialize};
use std::time::SystemTime; use std::time::SystemTime;
use crate::note::{generate_id, Note, NoteInfo, NotePublic}; use crate::note::{generate_id, Note, NoteInfo, NotePublic};
use crate::size::LIMIT;
use crate::store; use crate::store;
fn now() -> u64 { pub fn now() -> u32 {
SystemTime::now() SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs() .as_secs() as u32
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -17,7 +18,7 @@ struct NotePath {
id: String, id: String,
} }
#[get("/notes/{id}")] #[get("/{id}")]
async fn one(path: web::Path<NotePath>) -> impl Responder { async fn one(path: web::Path<NotePath>) -> impl Responder {
let p = path.into_inner(); let p = path.into_inner();
let note = store::get(&p.id); let note = store::get(&p.id);
@@ -32,14 +33,11 @@ struct CreateResponse {
id: String, id: String,
} }
#[post("/notes")] #[post("/")]
async fn create(note: web::Json<Note>) -> impl Responder { async fn create(note: web::Json<Note>) -> impl Responder {
let mut n = note.into_inner(); let mut n = note.into_inner();
let id = generate_id(); let id = generate_id();
let bad_req = HttpResponse::BadRequest().finish(); let bad_req = HttpResponse::BadRequest().finish();
if n.contents.chars().count() > 8192 {
return bad_req;
}
if n.views == None && n.expiration == None { if n.views == None && n.expiration == None {
return bad_req; return bad_req;
} }
@@ -65,7 +63,7 @@ async fn create(note: web::Json<Note>) -> impl Responder {
return HttpResponse::Ok().json(CreateResponse { id: id }); return HttpResponse::Ok().json(CreateResponse { id: id });
} }
#[delete("/notes/{id}")] #[delete("/{id}")]
async fn delete(path: web::Path<NotePath>) -> impl Responder { async fn delete(path: web::Path<NotePath>) -> impl Responder {
let p = path.into_inner(); let p = path.into_inner();
let note = store::get(&p.id); let note = store::get(&p.id);
@@ -88,10 +86,12 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
} }
_ => {} _ => {}
} }
let n = now();
match changed.expiration { match changed.expiration {
Some(e) => { Some(e) => {
store::del(&p.id.clone()); if e < n {
if e < now() { store::del(&p.id.clone());
return HttpResponse::BadRequest().finish(); return HttpResponse::BadRequest().finish();
} }
} }
@@ -99,16 +99,39 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
} }
return HttpResponse::Ok().json(NotePublic { return HttpResponse::Ok().json(NotePublic {
contents: changed.contents, contents: changed.contents,
meta: changed.meta,
}); });
} }
} }
} }
#[derive(Serialize, Deserialize)]
struct Status {
version: String,
max_size: usize,
}
#[get("/status")]
async fn status() -> impl Responder {
println!("Limit: {}", *LIMIT);
return HttpResponse::Ok().json(Status {
version: option_env!("CARGO_PKG_VERSION")
.unwrap_or("Unknown")
.to_string(),
max_size: *LIMIT,
});
}
pub fn init(cfg: &mut web::ServiceConfig) { pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::scope("/api") web::scope("/api")
.service(create) .service(
.service(delete) web::scope("/notes")
.service(one), .service(one)
.service(create)
.service(delete)
.service(status),
)
.service(status),
); );
} }

20
src/size.rs Normal file
View File

@@ -0,0 +1,20 @@
use actix_web::web;
use byte_unit::Byte;
use mime;
lazy_static! {
pub static ref LIMIT: usize =
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
.unwrap()
.get_bytes() as usize;
}
pub fn init(cfg: &mut web::ServiceConfig) {
println!("Limit: {}", *LIMIT);
let json = web::JsonConfig::default().limit(*LIMIT);
let plain = web::PayloadConfig::default()
.limit(*LIMIT)
.mimetype(mime::STAR_STAR);
cfg.data(json);
cfg.data(plain);
}

View File

@@ -1,5 +1,6 @@
use memcache; use memcache;
use crate::note::now;
use crate::note::Note; use crate::note::Note;
lazy_static! { lazy_static! {
@@ -12,7 +13,11 @@ lazy_static! {
pub fn set(id: &String, note: &Note) { pub fn set(id: &String, note: &Note) {
let serialized = serde_json::to_string(&note.clone()).unwrap(); let serialized = serde_json::to_string(&note.clone()).unwrap();
CLIENT.set(id, serialized, 0).unwrap(); let expiration: u32 = match note.expiration {
Some(e) => e - now(),
None => 0,
};
CLIENT.set(id, serialized, expiration).unwrap();
} }
pub fn get(id: &String) -> Option<Note> { pub fn get(id: &String) -> Option<Note> {