Compare commits
No commits in common. "main" and "v2.6.1" have entirely different histories.
13
.github/workflows/test.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Node
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
@ -27,16 +27,13 @@ jobs:
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm exec playwright install --with-deps
|
||||
pnpm run test:prepare
|
||||
|
||||
- name: Run your tests
|
||||
run: pnpm test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-report
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
name: test-results
|
||||
path: test-results
|
||||
|
1
.gitignore
vendored
@ -8,4 +8,3 @@ target
|
||||
|
||||
# Testing
|
||||
test-results
|
||||
tmp
|
||||
|
@ -1,5 +1,5 @@
|
||||
# FRONTEND
|
||||
FROM node:22-alpine as client
|
||||
FROM node:20-alpine as client
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@ -11,17 +11,17 @@ RUN pnpm run build
|
||||
|
||||
|
||||
# BACKEND
|
||||
FROM rust:1.85-alpine as backend
|
||||
FROM rust:1.76-alpine as backend
|
||||
WORKDIR /tmp
|
||||
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
|
||||
COPY ./packages/backend ./
|
||||
RUN RUSTFLAGS="-Ctarget-feature=-crt-static" cargo build --release
|
||||
RUN cargo build --release
|
||||
|
||||
|
||||
# RUNNER
|
||||
FROM alpine:3.19
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache curl libgcc
|
||||
RUN apk add --no-cache curl
|
||||
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||
COPY --from=client /tmp/packages/frontend/build ./frontend
|
||||
ENV FRONTEND_PATH="./frontend"
|
||||
|
37
README.md
@ -12,10 +12,9 @@
|
||||
<br/><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" height="50" /></a>
|
||||
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||
<a title="Install cryptgeon Raycast Extension" href="https://www.raycast.com/cupcakearmy/cryptgeon"><img src="https://www.raycast.com/cupcakearmy/cryptgeon/install_button@2x.png?v=1.1" height="64" alt="" style="height: 64px;"></a>
|
||||
<br/><br/>
|
||||
|
||||
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
|
||||
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
|
||||
|
||||
## About?
|
||||
|
||||
@ -40,12 +39,6 @@ npx cryptgeon send text "This is a secret note"
|
||||
|
||||
For more documentation about the CLI see the [readme](./packages/cli/README.md).
|
||||
|
||||
### Raycast Extension
|
||||
|
||||
There is an [official Raycast extension](https://www.raycast.com/cupcakearmy/cryptgeon).
|
||||
|
||||
<a title="Install cryptgeon Raycast Extension" href="https://www.raycast.com/cupcakearmy/cryptgeon"><img src="https://www.raycast.com/cupcakearmy/cryptgeon/install_button@2x.png?v=1.1" height="64" alt="" style="height: 64px;"></a>
|
||||
|
||||
## Features
|
||||
|
||||
- send text or files
|
||||
@ -63,8 +56,6 @@ client side with the <code>key</code> and then sent to the server. data is store
|
||||
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents
|
||||
of the notes even if it tried to.
|
||||
|
||||
> View counts are guaranteed with one running instance of cryptgeon. Multiple instances connected to the same Redis instance can run into race conditions, where a note might be retrieved more than the view count allows.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
@ -79,15 +70,14 @@ of the notes even if it tried to.
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||
| `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
|
||||
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
||||
| `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. |
|
||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
||||
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
|
||||
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. |
|
||||
|
||||
## Deployment
|
||||
|
||||
> ℹ️ `https` is required otherwise browsers will not support the cryptographic functions.
|
||||
@ -106,14 +96,9 @@ version: '3.8'
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
# https://redis.io/docs/manual/eviction/
|
||||
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
@ -156,18 +141,12 @@ There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-sy
|
||||
- English by [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
|
||||
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
||||
|
||||
### Written Guides
|
||||
|
||||
- French by [zarevskaya](https://belginux.com/installer-cryptgeon-avec-docker/)
|
||||
- Italian by [@nicfab](https://notes.nicfab.eu/it/posts/cryptgeon/)
|
||||
- English by [@nicfab](https://notes.nicfab.eu/en/posts/cryptgeon/)
|
||||
|
||||
## Development
|
||||
|
||||
**Requirements**
|
||||
|
||||
- `pnpm`: `>=9`
|
||||
- `node`: `>=22`
|
||||
- `pnpm`: `>=6`
|
||||
- `node`: `>=18`
|
||||
- `rust`: edition `2021`
|
||||
|
||||
**Install**
|
||||
@ -195,7 +174,7 @@ Running `pnpm run dev` in the root folder will start the following things:
|
||||
- client
|
||||
- cli
|
||||
|
||||
You can see the app under [localhost:3000](http://localhost:3000).
|
||||
You can see the app under [localhost:1234](http://localhost:1234).
|
||||
|
||||
> There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json)
|
||||
|
||||
|
35
README_ES.md
@ -59,19 +59,19 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
| Variable | Default | Descripción |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| Variable | Default | Descripción |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Tamaño máximo. Valores aceptados según la [unidad byte](https://docs.rs/byte-unit/). <br> `512 MiB` es el máximo permitido. <br> El frontend mostrará ese número, incluyendo el ~35% de sobrecarga de codificación. |
|
||||
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
||||
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
||||
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
||||
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
|
||||
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
||||
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
||||
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
|
||||
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
||||
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
||||
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
||||
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
||||
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
|
||||
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
||||
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
||||
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
|
||||
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
||||
|
||||
## Despliegue
|
||||
|
||||
@ -91,14 +91,9 @@ version: '3.8'
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
# https://redis.io/docs/manual/eviction/
|
||||
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
@ -174,7 +169,7 @@ Ejecutando `pnpm run dev` en la carpeta raíz iniciará lo siguiente:
|
||||
- client
|
||||
- cli
|
||||
|
||||
Puedes ver la app en [localhost:3000](http://localhost:3000).
|
||||
Puedes ver la app en [localhost:1234](http://localhost:1234).
|
||||
|
||||
> Existe una colección de Postman con algunas peticiones de ejemplo [disponible en el repo](./Cryptgeon.postman_collection.json)
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||
<br/>
|
||||
|
||||
[EN](README.md) | 简体中文 | [ES](README_ES.md)
|
||||
[EN](README.md) | 简体中文 | [ES](README_ES.md)
|
||||
|
||||
## 关于本项目
|
||||
|
||||
@ -74,14 +74,6 @@ version: '3.8'
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
@ -116,14 +108,7 @@ networks:
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
restart: unless-stopped
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
@ -173,7 +158,7 @@ pnpm run dev
|
||||
- 无热重载的 rust 后端
|
||||
- 可热重载的客户端
|
||||
|
||||
你可以通过 3000 端口进入该应用,即 [localhost:3000](http://localhost:3000).
|
||||
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234).
|
||||
|
||||
## 测试
|
||||
|
||||
|
@ -11,6 +11,9 @@
|
||||
},
|
||||
{
|
||||
"path": "packages/cli"
|
||||
},
|
||||
{
|
||||
"path": "packages/shared"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
@ -1,28 +1,22 @@
|
||||
# DEV Compose file.
|
||||
# For a production file see: README.md
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
app:
|
||||
build: .
|
||||
env_file: .env.dev
|
||||
env_file: .dev.env
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 3000:8000
|
||||
- 1234:8000
|
||||
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/']
|
||||
|
@ -1,14 +1,11 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
# https://redis.io/docs/manual/eviction/
|
||||
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
@ -19,7 +16,7 @@ services:
|
||||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:8000
|
||||
|
||||
|
||||
# Optional health checks
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
|
||||
|
@ -3,20 +3,12 @@ version: '3.8'
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
|
||||
proxy:
|
||||
image: nginx:alpine
|
||||
depends_on:
|
||||
|
@ -109,14 +109,7 @@ networks:
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
restart: unless-stopped
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Assumptions:
|
||||
|
||||
- Traefik 2/3 installed.
|
||||
- Traefik 2 installed.
|
||||
- External proxy docker network `proxy`.
|
||||
- A certificate resolver `le`.
|
||||
- A https entrypoint `secure`.
|
||||
@ -18,14 +18,7 @@ networks:
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
restart: unless-stopped
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
@ -41,51 +34,3 @@ services:
|
||||
- traefik.http.routers.cryptgeon.entrypoints=secure
|
||||
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
||||
```
|
||||
|
||||
## With basic auth
|
||||
|
||||
Some times it's useful to hide the service behind auth. This is easily achieved with traefik middleware. Many reverse proxies support similar features, so while traefik is used in this example, other reverse proxies can do the same.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.0
|
||||
command:
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# This is required to stay in RAM only.
|
||||
command: redis-server --save "" --appendonly no
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||
# This prevents the creation of an anonymous volume.
|
||||
tmpfs:
|
||||
- /data
|
||||
|
||||
cryptgeon:
|
||||
image: cupcakearmy/cryptgeon
|
||||
depends_on:
|
||||
- redis
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.cryptgeon.rule=Host(`cryptgeon.localhost`)"
|
||||
- "traefik.http.routers.cryptgeon.entrypoints=web"
|
||||
- "traefik.http.routers.cryptgeon.middlewares=cryptgeon-auth"
|
||||
- "traefik.http.middlewares.cryptgeon-auth.basicauth.users=user:$$2y$$05$$juUw0zgc5ebvJ00MFPVVLujF6P.rcEMbGZ99Jfq6ZWEa1dgetacEq"
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
1. Open http://cryptgeon.localhost
|
||||
2. Log in with `user` and `secret`
|
||||
|
12
package.json
@ -1,21 +1,21 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev:docker": "docker compose -f docker-compose.dev.yaml up redis",
|
||||
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
|
||||
"dev:packages": "pnpm --parallel run dev",
|
||||
"dev": "run-p dev:*",
|
||||
"docker:up": "docker compose -f docker-compose.dev.yaml up",
|
||||
"docker:build": "docker compose -f docker-compose.dev.yaml build",
|
||||
"test": "playwright test --project=chrome --project=firefox --project=safari",
|
||||
"test:local": "playwright test --project=chrome",
|
||||
"test": "playwright test --project chrome firefox safari",
|
||||
"test:local": "playwright test --project chrome",
|
||||
"test:server": "run-s docker:up",
|
||||
"test:prepare": "run-p build docker:build",
|
||||
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@types/node": "^22.5.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/node": "^20.11.28",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"shelljs": "^0.8.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.3.0"
|
||||
"packageManager": "pnpm@8.15.4"
|
||||
}
|
||||
|
1815
packages/backend/Cargo.lock
generated
@ -1,27 +1,25 @@
|
||||
[package]
|
||||
name = "cryptgeon"
|
||||
version = "2.9.1"
|
||||
version = "2.6.1"
|
||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.76"
|
||||
|
||||
[[bin]]
|
||||
name = "cryptgeon"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
axum = "0.7.5"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
||||
tower = "0.5.0"
|
||||
tower-http = { version = "0.5.2", features = ["full"] }
|
||||
redis = { version = "0.25.2", features = ["tls-native-tls"] }
|
||||
|
||||
# Utility
|
||||
actix-web = "4"
|
||||
actix-files = "0.6"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
lazy_static = "1"
|
||||
ring = "0.17"
|
||||
ring = "0.16"
|
||||
bs62 = "0.1"
|
||||
byte-unit = "4"
|
||||
dotenv = "0.15"
|
||||
mime = "0.3"
|
||||
env_logger = "0.9"
|
||||
log = "0.4"
|
||||
redis = "0.23"
|
||||
|
@ -4,7 +4,7 @@
|
||||
"scripts": {
|
||||
"dev": "cargo watch -x 'run --bin cryptgeon'",
|
||||
"build": "cargo build --release",
|
||||
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:3000 cargo run",
|
||||
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:1234 cargo run",
|
||||
"test:prepare": "cargo build"
|
||||
}
|
||||
}
|
||||
|
14
packages/backend/src/api.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use actix_web::web;
|
||||
|
||||
use crate::health;
|
||||
use crate::note;
|
||||
use crate::status;
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/api")
|
||||
.service(note::init())
|
||||
.service(status::init())
|
||||
.service(health::init()),
|
||||
);
|
||||
}
|
17
packages/backend/src/client.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_web::{web, Result};
|
||||
|
||||
use crate::config;
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
Files::new("/", config::FRONTEND_PATH.to_string())
|
||||
.index_file("index.html")
|
||||
.use_etag(true),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn index() -> Result<NamedFile> {
|
||||
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
|
||||
Ok(NamedFile::open(index)?)
|
||||
}
|
@ -38,12 +38,8 @@ pub static ref ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref IMPRINT_URL: String = std::env::var("IMPRINT_URL")
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref IMPRINT_HTML: String = std::env::var("IMPRINT_HTML")
|
||||
.unwrap_or("".to_string())
|
||||
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
@ -66,8 +62,4 @@ lazy_static! {
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
use axum::{body::Body, extract::Request, http::HeaderValue, middleware::Next, response::Response};
|
||||
|
||||
const CUSTOM_HEADER_NAME: &str = "Content-Security-Policy";
|
||||
const CUSTOM_HEADER_VALUE: &str = "default-src 'self'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; object-src 'none'; base-uri 'self'; connect-src 'self' data:; font-src 'self'; frame-src 'self'; img-src 'self'; manifest-src 'self'; media-src 'self'; worker-src 'none';";
|
||||
|
||||
lazy_static! {
|
||||
static ref HEADER_VALUE: HeaderValue = HeaderValue::from_static(CUSTOM_HEADER_VALUE);
|
||||
}
|
||||
|
||||
pub async fn add_csp_header(request: Request<Body>, next: Next) -> Response {
|
||||
let mut response = next.run(request).await;
|
||||
response
|
||||
.headers_mut()
|
||||
.append(CUSTOM_HEADER_NAME, HEADER_VALUE.clone());
|
||||
response
|
||||
}
|
@ -1,10 +1,3 @@
|
||||
use crate::store;
|
||||
use axum::http::StatusCode;
|
||||
mod routes;
|
||||
|
||||
pub async fn report_health() -> (StatusCode,) {
|
||||
if store::can_reach_redis() {
|
||||
return (StatusCode::OK,);
|
||||
} else {
|
||||
return (StatusCode::SERVICE_UNAVAILABLE,);
|
||||
}
|
||||
}
|
||||
pub use routes::*;
|
||||
|
16
packages/backend/src/health/routes.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use actix_web::{get, web, HttpResponse, Responder, Scope};
|
||||
|
||||
use crate::store;
|
||||
|
||||
#[get("/")]
|
||||
async fn get_live() -> impl Responder {
|
||||
if store::can_reach_redis() {
|
||||
return HttpResponse::Ok();
|
||||
} else {
|
||||
return HttpResponse::ServiceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> Scope {
|
||||
web::scope("/live").service(get_live)
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SharedState {
|
||||
pub locks: LockMap,
|
||||
}
|
||||
|
||||
pub type LockMap = Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>;
|
@ -1,80 +1,44 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
extract::{DefaultBodyLimit, Request},
|
||||
routing::{delete, get, post},
|
||||
Router, ServiceExt,
|
||||
use actix_web::{
|
||||
middleware::{self, Logger},
|
||||
web::{self},
|
||||
App, HttpServer,
|
||||
};
|
||||
use dotenv::dotenv;
|
||||
use lock::SharedState;
|
||||
use tokio::sync::Mutex;
|
||||
use tower::Layer;
|
||||
use tower_http::{
|
||||
compression::CompressionLayer,
|
||||
normalize_path::NormalizePathLayer,
|
||||
services::{ServeDir, ServeFile},
|
||||
};
|
||||
use log::error;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
mod api;
|
||||
mod client;
|
||||
mod config;
|
||||
mod csp;
|
||||
mod health;
|
||||
mod lock;
|
||||
mod note;
|
||||
mod size;
|
||||
mod status;
|
||||
mod store;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv().ok();
|
||||
|
||||
let shared_state = SharedState {
|
||||
locks: Arc::new(Mutex::new(HashMap::new())),
|
||||
};
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
|
||||
|
||||
if !store::can_reach_redis() {
|
||||
println!("cannot reach redis");
|
||||
panic!("cannot reach redis");
|
||||
error!("cannot reach redis");
|
||||
panic!("canont reach redis");
|
||||
}
|
||||
|
||||
let notes_routes = Router::new()
|
||||
.route("/", post(note::create))
|
||||
.route("/:id", delete(note::delete))
|
||||
.route("/:id", get(note::preview));
|
||||
let health_routes = Router::new().route("/live", get(health::report_health));
|
||||
let status_routes = Router::new().route("/status", get(status::get_status));
|
||||
let api_routes = Router::new()
|
||||
.nest("/notes", notes_routes)
|
||||
.nest("/", health_routes)
|
||||
.nest("/", status_routes);
|
||||
|
||||
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
|
||||
let serve_dir =
|
||||
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
|
||||
let app = Router::new()
|
||||
.nest("/api", api_routes)
|
||||
.fallback_service(serve_dir)
|
||||
// Disabled for now, as svelte inlines scripts
|
||||
// .layer(middleware::from_fn(csp::add_csp_header))
|
||||
.layer(DefaultBodyLimit::max(*config::LIMIT))
|
||||
.layer(
|
||||
CompressionLayer::new()
|
||||
.br(true)
|
||||
.deflate(true)
|
||||
.gzip(true)
|
||||
.zstd(true),
|
||||
)
|
||||
.with_state(shared_state);
|
||||
|
||||
let app = NormalizePathLayer::trim_trailing_slash().layer(app);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(config::LISTEN_ADDR.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("listening on {}", listener.local_addr().unwrap());
|
||||
axum::serve(listener, ServiceExt::<Request>::into_make_service(app))
|
||||
.await
|
||||
.unwrap();
|
||||
return HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(Logger::new("\"%r\" %s %b %T"))
|
||||
.wrap(middleware::Compress::default())
|
||||
.wrap(middleware::DefaultHeaders::default())
|
||||
.configure(size::init)
|
||||
.configure(api::init)
|
||||
.configure(client::init)
|
||||
.default_service(web::to(client::index))
|
||||
})
|
||||
.bind(config::LISTEN_ADDR.to_string())?
|
||||
.run()
|
||||
.await;
|
||||
}
|
||||
|
@ -12,12 +12,12 @@ pub struct Note {
|
||||
pub expiration: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NoteInfo {
|
||||
pub meta: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NotePublic {
|
||||
pub meta: String,
|
||||
pub contents: String,
|
||||
|
@ -1,18 +1,10 @@
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use actix_web::{delete, get, post, web, HttpResponse, Responder, Scope};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
use tokio::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::note::{generate_id, Note, NoteInfo};
|
||||
use crate::config;
|
||||
use crate::note::{generate_id, Note, NoteInfo, NotePublic};
|
||||
use crate::store;
|
||||
use crate::{config, lock::SharedState};
|
||||
|
||||
use super::NotePublic;
|
||||
|
||||
pub fn now() -> u32 {
|
||||
SystemTime::now()
|
||||
@ -21,18 +13,20 @@ pub fn now() -> u32 {
|
||||
.as_secs() as u32
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct OneNoteParams {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct NotePath {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub async fn preview(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
|
||||
let note = store::get(&id);
|
||||
#[get("/{id}")]
|
||||
async fn one(path: web::Path<NotePath>) -> impl Responder {
|
||||
let p = path.into_inner();
|
||||
let note = store::get(&p.id);
|
||||
|
||||
match note {
|
||||
Ok(Some(n)) => (StatusCode::OK, Json(NoteInfo { meta: n.meta })).into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
Ok(Some(n)) => HttpResponse::Ok().json(NoteInfo { meta: n.meta }),
|
||||
Ok(None) => HttpResponse::NotFound().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,16 +35,13 @@ struct CreateResponse {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub async fn create(Json(mut n): Json<Note>) -> Response {
|
||||
// let mut n = note.into_inner();
|
||||
#[post("/")]
|
||||
async fn create(note: web::Json<Note>) -> impl Responder {
|
||||
let mut n = note.into_inner();
|
||||
let id = generate_id();
|
||||
// let bad_req = HttpResponse::BadRequest().finish();
|
||||
let bad_req = HttpResponse::BadRequest().finish();
|
||||
if n.views == None && n.expiration == None {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"At least views or expiration must be set",
|
||||
)
|
||||
.into_response();
|
||||
return bad_req;
|
||||
}
|
||||
if !*config::ALLOW_ADVANCED {
|
||||
n.views = Some(1);
|
||||
@ -59,7 +50,7 @@ pub async fn create(Json(mut n): Json<Note>) -> Response {
|
||||
match n.views {
|
||||
Some(v) => {
|
||||
if v > *config::MAX_VIEWS || v < 1 {
|
||||
return (StatusCode::BAD_REQUEST, "Invalid views").into_response();
|
||||
return bad_req;
|
||||
}
|
||||
n.expiration = None; // views overrides expiration
|
||||
}
|
||||
@ -67,8 +58,8 @@ pub async fn create(Json(mut n): Json<Note>) -> Response {
|
||||
}
|
||||
match n.expiration {
|
||||
Some(e) => {
|
||||
if e > *config::MAX_EXPIRATION || e < 1 {
|
||||
return (StatusCode::BAD_REQUEST, "Invalid expiration").into_response();
|
||||
if e > *config::MAX_EXPIRATION {
|
||||
return bad_req;
|
||||
}
|
||||
let expiration = now() + (e * 60);
|
||||
n.expiration = Some(expiration);
|
||||
@ -76,49 +67,38 @@ pub async fn create(Json(mut n): Json<Note>) -> Response {
|
||||
_ => {}
|
||||
}
|
||||
match store::set(&id.clone(), &n.clone()) {
|
||||
Ok(_) => (StatusCode::OK, Json(CreateResponse { id })).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
Ok(_) => return HttpResponse::Ok().json(CreateResponse { id: id }),
|
||||
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
Path(OneNoteParams { id }): Path<OneNoteParams>,
|
||||
state: axum::extract::State<SharedState>,
|
||||
) -> Response {
|
||||
let mut locks_map = state.locks.lock().await;
|
||||
let lock = locks_map
|
||||
.entry(id.clone())
|
||||
.or_insert_with(|| Arc::new(Mutex::new(())))
|
||||
.clone();
|
||||
drop(locks_map);
|
||||
let _guard = lock.lock().await;
|
||||
|
||||
let note = store::get(&id);
|
||||
#[delete("/{id}")]
|
||||
async fn delete(path: web::Path<NotePath>) -> impl Responder {
|
||||
let p = path.into_inner();
|
||||
let note = store::get(&p.id);
|
||||
match note {
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Ok(None) => return HttpResponse::NotFound().finish(),
|
||||
Ok(Some(note)) => {
|
||||
let mut changed = note.clone();
|
||||
if changed.views == None && changed.expiration == None {
|
||||
return (StatusCode::BAD_REQUEST).into_response();
|
||||
return HttpResponse::BadRequest().finish();
|
||||
}
|
||||
match changed.views {
|
||||
Some(v) => {
|
||||
changed.views = Some(v - 1);
|
||||
let id = id.clone();
|
||||
let id = p.id.clone();
|
||||
if v <= 1 {
|
||||
match store::del(&id) {
|
||||
Err(e) => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
.into_response();
|
||||
return HttpResponse::InternalServerError().body(e.to_string())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match store::set(&id, &changed.clone()) {
|
||||
Err(e) => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
.into_response();
|
||||
return HttpResponse::InternalServerError().body(e.to_string())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@ -131,26 +111,33 @@ pub async fn delete(
|
||||
match changed.expiration {
|
||||
Some(e) => {
|
||||
if e < n {
|
||||
match store::del(&id.clone()) {
|
||||
Ok(_) => return (StatusCode::BAD_REQUEST).into_response(),
|
||||
match store::del(&p.id.clone()) {
|
||||
Ok(_) => return HttpResponse::BadRequest().finish(),
|
||||
Err(e) => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
.into_response()
|
||||
return HttpResponse::InternalServerError().body(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(NotePublic {
|
||||
contents: changed.contents,
|
||||
meta: changed.meta,
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
return HttpResponse::Ok().json(NotePublic {
|
||||
contents: changed.contents,
|
||||
meta: changed.meta,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Status {
|
||||
version: String,
|
||||
max_size: usize,
|
||||
}
|
||||
|
||||
pub fn init() -> Scope {
|
||||
web::scope("/notes")
|
||||
.service(one)
|
||||
.service(create)
|
||||
.service(delete)
|
||||
}
|
||||
|
12
packages/backend/src/size.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use crate::config;
|
||||
use actix_web::web;
|
||||
use mime;
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
let json = web::JsonConfig::default().limit(*config::LIMIT);
|
||||
let plain = web::PayloadConfig::default()
|
||||
.limit(*config::LIMIT)
|
||||
.mimetype(mime::STAR_STAR);
|
||||
// cfg.app_data(plain);
|
||||
cfg.app_data(json).app_data(plain);
|
||||
}
|
@ -1,43 +1,5 @@
|
||||
use crate::config;
|
||||
use axum::{http::StatusCode, Json};
|
||||
use serde::Serialize;
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Status {
|
||||
// General
|
||||
pub version: String,
|
||||
// Config
|
||||
pub max_size: u32,
|
||||
pub max_views: u32,
|
||||
pub max_expiration: u32,
|
||||
pub allow_advanced: bool,
|
||||
pub allow_files: bool,
|
||||
pub imprint_url: String,
|
||||
pub imprint_html: String,
|
||||
// Theme
|
||||
pub theme_image: String,
|
||||
pub theme_text: String,
|
||||
pub theme_page_title: String,
|
||||
pub theme_favicon: String,
|
||||
pub theme_new_note_notice: bool,
|
||||
}
|
||||
|
||||
pub async fn get_status() -> (StatusCode, Json<Status>) {
|
||||
let status = Status {
|
||||
version: config::VERSION.to_string(),
|
||||
max_size: *config::LIMIT as u32,
|
||||
max_views: *config::MAX_VIEWS,
|
||||
max_expiration: *config::MAX_EXPIRATION,
|
||||
allow_advanced: *config::ALLOW_ADVANCED,
|
||||
allow_files: *config::ALLOW_FILES,
|
||||
imprint_url: config::IMPRINT_URL.to_string(),
|
||||
imprint_html: config::IMPRINT_HTML.to_string(),
|
||||
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
|
||||
theme_image: config::THEME_IMAGE.to_string(),
|
||||
theme_text: config::THEME_TEXT.to_string(),
|
||||
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
|
||||
theme_favicon: config::THEME_FAVICON.to_string(),
|
||||
};
|
||||
|
||||
(StatusCode::OK, Json(status))
|
||||
}
|
||||
pub use model::*;
|
||||
pub use routes::*;
|
||||
|
19
packages/backend/src/status/model.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Status {
|
||||
// General
|
||||
pub version: String,
|
||||
// Config
|
||||
pub max_size: u32,
|
||||
pub max_views: u32,
|
||||
pub max_expiration: u32,
|
||||
pub allow_advanced: bool,
|
||||
pub allow_files: bool,
|
||||
pub theme_new_note_notice: bool,
|
||||
// Theme
|
||||
pub theme_image: String,
|
||||
pub theme_text: String,
|
||||
pub theme_page_title: String,
|
||||
pub theme_favicon: String,
|
||||
}
|
25
packages/backend/src/status/routes.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use actix_web::{get, web, HttpResponse, Responder, Scope};
|
||||
|
||||
use crate::config;
|
||||
use crate::status::Status;
|
||||
|
||||
#[get("/")]
|
||||
async fn get_status() -> impl Responder {
|
||||
return HttpResponse::Ok().json(Status {
|
||||
version: config::VERSION.to_string(),
|
||||
max_size: *config::LIMIT as u32,
|
||||
max_views: *config::MAX_VIEWS,
|
||||
max_expiration: *config::MAX_EXPIRATION,
|
||||
allow_advanced: *config::ALLOW_ADVANCED,
|
||||
allow_files: *config::ALLOW_FILES,
|
||||
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
|
||||
theme_image: config::THEME_IMAGE.to_string(),
|
||||
theme_text: config::THEME_TEXT.to_string(),
|
||||
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
|
||||
theme_favicon: config::THEME_FAVICON.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn init() -> Scope {
|
||||
web::scope("/status").service(get_status)
|
||||
}
|
@ -36,7 +36,7 @@ pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
|
||||
match note.expiration {
|
||||
Some(e) => {
|
||||
let seconds = e - now();
|
||||
conn.expire(id, seconds as i64)
|
||||
conn.expire(id, seconds as usize)
|
||||
.map_err(|_| "Unable to set expiration on notion")?
|
||||
}
|
||||
None => {}
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { build } from 'tsup'
|
||||
import pkg from './package.json' with { type: 'json' }
|
||||
|
||||
const watch = process.argv.slice(2)[0] === '--watch'
|
||||
|
||||
await build({
|
||||
entry: ['src/index.ts', 'src/cli.ts', 'src/shared/shared.ts'],
|
||||
dts: true,
|
||||
minify: true,
|
||||
format: ['esm', 'cjs'],
|
||||
target: 'es2020',
|
||||
clean: true,
|
||||
define: { VERSION: `"${pkg.version}"` },
|
||||
watch,
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cryptgeon",
|
||||
"version": "2.9.1",
|
||||
"version": "2.6.1",
|
||||
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -9,11 +9,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./shared": {
|
||||
"import": "./dist/shared/shared.js",
|
||||
"types": "./dist/shared/shared.d.ts"
|
||||
}
|
||||
".": "./dist/index.mjs"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
@ -24,21 +20,22 @@
|
||||
],
|
||||
"scripts": {
|
||||
"bin": "run-s build package",
|
||||
"build": "tsc && node build.js",
|
||||
"dev": "node build.js --watch",
|
||||
"build": "rm -rf dist && tsc && ./scripts/build.js",
|
||||
"dev": "./scripts/build.js --watch",
|
||||
"prepublishOnly": "run-s build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commander-js/extra-typings": "^12.1.0",
|
||||
"@commander-js/extra-typings": "^12.0.1",
|
||||
"@cryptgeon/shared": "workspace:*",
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/mime": "^4.0.0",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node": "^20.11.24",
|
||||
"commander": "^12.1.0",
|
||||
"commander": "^12.0.0",
|
||||
"esbuild": "^0.20.1",
|
||||
"inquirer": "^9.2.15",
|
||||
"mime": "^4.0.1",
|
||||
"occulto": "^2.0.6",
|
||||
"occulto": "^2.0.3",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"tsup": "^8.2.4",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
|
34
packages/cli/scripts/build.js
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { build, context } from 'esbuild'
|
||||
import pkg from '../package.json' assert { type: 'json' }
|
||||
|
||||
const common = {
|
||||
bundle: true,
|
||||
minify: true,
|
||||
platform: 'node',
|
||||
define: { VERSION: `"${pkg.version}"` },
|
||||
}
|
||||
|
||||
const cliOptions = {
|
||||
...common,
|
||||
entryPoints: ['./src/cli.ts'],
|
||||
format: 'cjs',
|
||||
outfile: './dist/cli.cjs',
|
||||
}
|
||||
|
||||
const indexOptions = {
|
||||
...common,
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outfile: './dist/index.mjs',
|
||||
format: 'esm',
|
||||
}
|
||||
|
||||
const watch = process.argv.slice(2)[0] === '--watch'
|
||||
if (watch) {
|
||||
const ctx = await context(cliOptions)
|
||||
ctx.watch()
|
||||
} else {
|
||||
await build(cliOptions)
|
||||
await build(indexOptions)
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Argument, Option, program } from '@commander-js/extra-typings'
|
||||
import { setBase, status } from '@cryptgeon/shared'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { download } from './actions/download.js'
|
||||
import { upload } from './actions/upload.js'
|
||||
import { API } from './shared/api.js'
|
||||
import { parseFile, parseNumber } from './utils/parsers.js'
|
||||
import { getStdin } from './utils/stdin.js'
|
||||
import { checkConstrains, exit } from './utils/utils.js'
|
||||
import { download } from './download.js'
|
||||
import { parseFile, parseNumber } from './parsers.js'
|
||||
import { getStdin } from './stdin.js'
|
||||
import { upload } from './upload.js'
|
||||
import { checkConstrains, exit } from './utils.js'
|
||||
|
||||
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
|
||||
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer)
|
||||
@ -33,8 +33,8 @@ program
|
||||
.description('show information about the server')
|
||||
.addOption(server)
|
||||
.action(async (options) => {
|
||||
API.setOptions({ server: options.server })
|
||||
const response = await API.status()
|
||||
setBase(options.server)
|
||||
const response = await status()
|
||||
const formatted = {
|
||||
...response,
|
||||
max_size: prettyBytes(response.max_size),
|
||||
@ -54,7 +54,7 @@ send
|
||||
.addOption(minutes)
|
||||
.addOption(password)
|
||||
.action(async (files, options) => {
|
||||
API.setOptions({ server: options.server })
|
||||
setBase(options.server!)
|
||||
await checkConstrains(options)
|
||||
options.password ||= await getStdin()
|
||||
try {
|
||||
@ -72,7 +72,7 @@ send
|
||||
.addOption(minutes)
|
||||
.addOption(password)
|
||||
.action(async (text, options) => {
|
||||
API.setOptions({ server: options.server })
|
||||
setBase(options.server!)
|
||||
await checkConstrains(options)
|
||||
options.password ||= await getStdin()
|
||||
try {
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { Adapters, get, info, setBase } from '@cryptgeon/shared'
|
||||
import inquirer from 'inquirer'
|
||||
import { access, constants, writeFile } from 'node:fs/promises'
|
||||
import { basename, resolve } from 'node:path'
|
||||
import { AES, Hex } from 'occulto'
|
||||
import pretty from 'pretty-bytes'
|
||||
import { Adapters } from '../shared/adapters.js'
|
||||
import { API } from '../shared/api.js'
|
||||
|
||||
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
|
||||
API.setOptions({ server: url.origin })
|
||||
setBase(url.origin)
|
||||
const id = url.pathname.split('/')[2]
|
||||
const preview = await API.info(id).catch(() => {
|
||||
const preview = await info(id).catch(() => {
|
||||
throw new Error('Note does not exist or is expired')
|
||||
})
|
||||
|
||||
@ -34,7 +33,7 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin
|
||||
}
|
||||
|
||||
const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password)
|
||||
const note = await API.get(id)
|
||||
const note = await get(id)
|
||||
|
||||
const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password')
|
||||
switch (note.meta.type) {
|
@ -1,4 +1,4 @@
|
||||
export * from './actions/download.js'
|
||||
export * from './actions/upload.js'
|
||||
export * from './shared/adapters.js'
|
||||
export * from './shared/api.js'
|
||||
export * from '@cryptgeon/shared'
|
||||
export * from './download.js'
|
||||
export * from './upload.js'
|
||||
export * from './utils.js'
|
||||
|
@ -21,7 +21,7 @@ export function parseURL(value: string, _: URL): URL {
|
||||
}
|
||||
|
||||
export function parseNumber(value: string, _: number): number {
|
||||
const n = Number.parseInt(value, 10)
|
||||
if (Number.isNaN(n)) throw new InvalidOptionArgumentError('invalid number')
|
||||
const n = parseInt(value, 10)
|
||||
if (isNaN(n)) throw new InvalidOptionArgumentError('invalid number')
|
||||
return n
|
||||
}
|
@ -18,7 +18,6 @@ export function getStdin(timeout: number = 10): Promise<string> {
|
||||
resolve('')
|
||||
}, timeout)
|
||||
|
||||
process.stdin.on('error', reject)
|
||||
process.stdin.on('data', dataHandler)
|
||||
process.stdin.on('end', endHandler)
|
||||
})
|
@ -1,10 +1,9 @@
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { basename } from 'node:path'
|
||||
|
||||
import { Adapters, BASE, create, FileDTO, Note, NoteMeta } from '@cryptgeon/shared'
|
||||
import mime from 'mime'
|
||||
import { AES, Hex } from 'occulto'
|
||||
import { Adapters } from '../shared/adapters.js'
|
||||
import { API, FileDTO, Note, NoteMeta } from '../shared/api.js'
|
||||
|
||||
export type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
|
||||
|
||||
@ -39,8 +38,8 @@ export async function upload(input: string | string[], options: UploadOptions):
|
||||
|
||||
// Create the actual note and upload it.
|
||||
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
|
||||
const result = await API.create(note)
|
||||
let url = `${API.getOptions().server}/note/${result.id}`
|
||||
const result = await create(note)
|
||||
let url = `${BASE}/note/${result.id}`
|
||||
if (!derived) url += `#${Hex.encode(key)}`
|
||||
return url
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { status } from '@cryptgeon/shared'
|
||||
import { exit as exitNode } from 'node:process'
|
||||
import { API } from '../shared/api.js'
|
||||
|
||||
export function exit(message: string) {
|
||||
console.error(message)
|
||||
@ -11,7 +11,7 @@ export async function checkConstrains(constrains: { views?: number; minutes?: nu
|
||||
if (views && minutes) exit('cannot set view and minutes constrains simultaneously')
|
||||
if (!views && !minutes) constrains.views = 1
|
||||
|
||||
const response = await API.status()
|
||||
const response = await status()
|
||||
if (views && views > response.max_views)
|
||||
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
|
||||
if (minutes && minutes > response.max_expiration)
|
@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"strict": true,
|
||||
|
@ -25,7 +25,7 @@
|
||||
"new_note": "new note",
|
||||
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes will be removed.<br />(you probably will be fine, just be warned.)",
|
||||
"errors": {
|
||||
"note_to_big": "could not create note. note is too big",
|
||||
"note_to_big": "could not create note. note is to big",
|
||||
"note_error": "could not create note. please try again.",
|
||||
"max": "max: {n}",
|
||||
"empty_content": "note is empty."
|
||||
@ -44,7 +44,7 @@
|
||||
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
|
||||
"unsupported_type": "unsupported note type."
|
||||
},
|
||||
"explanation": "click below to show and delete the note if the counter has reached its limit",
|
||||
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
||||
"show_note": "show note",
|
||||
"warning_will_not_see_again": "you will <b>not</b> get the chance to see the note again.",
|
||||
"download_all": "download all",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"intro": "Envoyez facilement des notes ou des fichiers <i>entièrement chiffrés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.",
|
||||
"explanation": "la note expirera et sera détruite après {type}.",
|
||||
"new_note": "nouvelle note",
|
||||
"new_note_notice": "<b>disponibilité :</b><br />il n'est pas garanti que la note reste stockée car tout est conservé dans la mémoire vive; si elle se remplit, les notes les plus anciennes seront supprimées.<br />(tout ira probablement bien, soyez juste averti.)",
|
||||
"new_note_notice": "<b>disponibilité :</b><br />la note n'est pas garantie d'être stockée car tout est conservé dans la mémoire vive; si elle se remplit, les notes les plus anciennes seront supprimées.<br />(tout ira probablement bien, soyez juste averti.)",
|
||||
"errors": {
|
||||
"note_to_big": "Impossible de créer une note. La note est trop grande.",
|
||||
"note_error": "n'a pas pu créer de note. Veuillez réessayer.",
|
||||
@ -45,9 +45,9 @@
|
||||
"unsupported_type": "type de note non supporté."
|
||||
},
|
||||
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
||||
"show_note": "afficher la note",
|
||||
"show_note": "note de présentation",
|
||||
"warning_will_not_see_again": "vous <b>n'aurez pas</b> la chance de revoir la note.",
|
||||
"download_all": "tout télécharger",
|
||||
"download_all": "télécharger tout",
|
||||
"links_found": "liens trouvés à l’intérieur de la note :"
|
||||
},
|
||||
"file_upload": {
|
||||
|
@ -1,58 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"note": "筆記",
|
||||
"file": "檔案",
|
||||
"advanced": "進階",
|
||||
"create": "創建",
|
||||
"loading": "載入中",
|
||||
"mode": "模式",
|
||||
"views": "{n, plural, =0 {瀏覽次數} =1 {1 次瀏覽} other {# 次瀏覽}}",
|
||||
"minutes": "{n, plural, =0 {分鐘} =1 {1 分鐘} other {# 分鐘}}",
|
||||
"max": "最大",
|
||||
"share_link": "分享連結",
|
||||
"copy_clipboard": "複製到剪貼板",
|
||||
"copied_to_clipboard": "已複製到剪貼板",
|
||||
"encrypting": "加密中",
|
||||
"decrypting": "解密中",
|
||||
"uploading": "上傳中",
|
||||
"downloading": "下載中",
|
||||
"qr_code": "QR 碼",
|
||||
"password": "密碼"
|
||||
},
|
||||
"home": {
|
||||
"intro": "輕鬆地以一鍵傳送<i>完全加密</i>的安全筆記或檔案。只需創建筆記並分享連結。",
|
||||
"explanation": "筆記將在 {type} 後過期並被銷毀。",
|
||||
"new_note": "新筆記",
|
||||
"new_note_notice": "<b>可用性:</b><br />筆記不保證被儲存,因為所有內容都保留在 RAM 中,如果 RAM 填滿,最舊的筆記將被移除。<br />(您可能會沒事,只是提醒一下。)",
|
||||
"errors": {
|
||||
"note_to_big": "無法創建筆記。筆記過大",
|
||||
"note_error": "無法創建筆記。請再試一次。",
|
||||
"max": "最大值:{n}",
|
||||
"empty_content": "筆記內容為空。"
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "筆記已創建。"
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "預設情況下,每個筆記都會使用安全生成的密碼。您也可以選擇自己的密碼,該密碼不會包含在連結中。",
|
||||
"custom_password": "自定義密碼"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "筆記未找到或已被刪除。",
|
||||
"decryption_failed": "密碼錯誤。無法解密。可能是連結已損壞。筆記已被銷毀。",
|
||||
"unsupported_type": "不支持的筆記類型。"
|
||||
},
|
||||
"explanation": "如果計數器達到限制,請點擊下方以顯示並刪除筆記",
|
||||
"show_note": "顯示筆記",
|
||||
"warning_will_not_see_again": "您將<b>無法</b>再次查看筆記。",
|
||||
"download_all": "全部下載",
|
||||
"links_found": "在筆記中找到的連結:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "已選擇的檔案",
|
||||
"no_files_selected": "未選擇檔案",
|
||||
"clear": "重置"
|
||||
}
|
||||
}
|
@ -13,24 +13,27 @@
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@lokalise/node-api": "^13.2.1",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.17.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@zerodevx/svelte-toast": "^0.9.6",
|
||||
"adm-zip": "^0.5.16",
|
||||
"dotenv": "^16.4.7",
|
||||
"svelte": "^5.20.5",
|
||||
"svelte-check": "^4.1.4",
|
||||
"@lokalise/node-api": "^12.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@zerodevx/svelte-toast": "^0.9.5",
|
||||
"adm-zip": "^0.5.10",
|
||||
"dotenv": "^16.4.5",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.6",
|
||||
"svelte-intl-precompile": "^0.12.3",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.2.0"
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/fira-mono": "^5.1.1",
|
||||
"cryptgeon": "workspace:*",
|
||||
"occulto": "^2.0.6",
|
||||
"@cryptgeon/shared": "workspace:*",
|
||||
"@fontsource/fira-mono": "^5.0.8",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"occulto": "^2.0.3",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"qrious": "^4.0.2"
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
|
||||
><title>Contrast</title><path
|
||||
d="M256 32C132.29 32 32 132.29 32 256s100.29 224 224 224 224-100.29 224-224S379.71 32 256 32zM128.72 383.28A180 180 0 01256 76v360a178.82 178.82 0 01-127.28-52.72z"
|
||||
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 287 B |
@ -1,5 +1,3 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
|
||||
><title>Copy</title><path
|
||||
d="M456 480H136a24 24 0 01-24-24V128a16 16 0 0116-16h328a24 24 0 0124 24v320a24 24 0 01-24 24z"
|
||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 325 B |
@ -1,5 +1,3 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
|
||||
><title>Dice</title><path
|
||||
d="M48 366.92L240 480V284L48 170zM192 288c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zm-96 32c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zM272 284v196l192-113.08V170zm48 140c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm96 32c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm32 77.64zM256 32L64 144l192 112 192-112zm0 120c-13.25 0-24-7.16-24-16s10.75-16 24-16 24 7.16 24 16-10.75 16-24 16z"
|
||||
|
Before Width: | Height: | Size: 765 B After Width: | Height: | Size: 736 B |
@ -1,5 +1,3 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
|
||||
><title>Eye</title><circle cx="256" cy="256" r="64" /><path
|
||||
d="M394.82 141.18C351.1 111.2 304.31 96 255.76 96c-43.69 0-86.28 13-126.59 38.48C88.52 160.23 48.67 207 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416c49 0 95.85-15.07 139.3-44.79C433.31 345 469.71 299.82 496 256c-26.38-43.43-62.9-88.56-101.18-114.82zM256 352a96 96 0 1196-96 96.11 96.11 0 01-96 96z"
|
||||
|
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 483 B |
@ -1,5 +1,3 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
|
||||
><title>Eye Off</title><path
|
||||
d="M63.998 86.004l21.998-21.998L448 426.01l-21.998 21.998zM259.34 192.09l60.57 60.57a64.07 64.07 0 00-60.57-60.57zM252.66 319.91l-60.57-60.57a64.07 64.07 0 0060.57 60.57z"
|
||||
|
Before Width: | Height: | Size: 761 B After Width: | Height: | Size: 732 B |
@ -1,8 +1,8 @@
|
||||
import { API, type Status } from 'cryptgeon/shared'
|
||||
import { status as getStatus, type Status } from '@cryptgeon/shared'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export const status = writable<null | Status>(null)
|
||||
|
||||
export async function init() {
|
||||
status.set(await API.status())
|
||||
status.set(await getStatus())
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import { toast, type SvelteToastOptions } from '@zerodevx/svelte-toast'
|
||||
|
||||
export enum NotifyType {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
const themeMapping: Record<NotifyType, Record<string, string>> = {
|
||||
const themeMapping: Record<NotifyType, SvelteToastOptions['theme']> = {
|
||||
[NotifyType.Success]: {
|
||||
'--toastBackground': 'var(--ui-clr-primary)',
|
||||
'--toastBarBackground': 'var(--ui-clr-primary-alt)',
|
||||
@ -17,7 +17,7 @@ const themeMapping: Record<NotifyType, Record<string, string>> = {
|
||||
}
|
||||
|
||||
function notifyFN(message: string, type: NotifyType = NotifyType.Success) {
|
||||
const options = {
|
||||
const options: SvelteToastOptions = {
|
||||
duration: 5_000,
|
||||
theme: {
|
||||
...themeMapping[type],
|
||||
|
@ -1,17 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
let { title, children }: Props = $props()
|
||||
export let title: string
|
||||
</script>
|
||||
|
||||
<p>
|
||||
<b>▶ {title}</b>
|
||||
{@render children?.()}
|
||||
<slot />
|
||||
</p>
|
||||
|
||||
<style>
|
||||
|
@ -4,25 +4,15 @@
|
||||
import { status } from '$lib/stores/status'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import type { Note } from 'cryptgeon/shared'
|
||||
import type { Note } from '@cryptgeon/shared'
|
||||
|
||||
interface Props {
|
||||
note: Note
|
||||
timeExpiration?: boolean
|
||||
customPassword?: string | null
|
||||
}
|
||||
export let note: Note
|
||||
export let timeExpiration = false
|
||||
export let customPassword: string | null = null
|
||||
|
||||
let {
|
||||
note = $bindable(),
|
||||
timeExpiration = $bindable(false),
|
||||
customPassword = $bindable(null),
|
||||
}: Props = $props()
|
||||
let hasCustomPassword = false
|
||||
|
||||
let hasCustomPassword = $state(false)
|
||||
|
||||
$effect(() => {
|
||||
if (!hasCustomPassword) customPassword = null
|
||||
})
|
||||
$: if (!hasCustomPassword) customPassword = null
|
||||
</script>
|
||||
|
||||
<div class="flex col">
|
||||
|
@ -1,14 +1,4 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements'
|
||||
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let { children, ...rest }: HTMLButtonAttributes & Props = $props()
|
||||
</script>
|
||||
|
||||
<button {...rest}>{@render children?.()}</button>
|
||||
<button {...$$restProps} on:click><slot /></button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
|
@ -5,15 +5,11 @@
|
||||
|
||||
import { getCSSVariable } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
}
|
||||
export let value: string
|
||||
|
||||
let { value }: Props = $props()
|
||||
let canvas: HTMLCanvasElement
|
||||
|
||||
let canvas: HTMLCanvasElement | null = $state(null)
|
||||
|
||||
$effect(() => {
|
||||
$: {
|
||||
new QR({
|
||||
value,
|
||||
level: 'Q',
|
||||
@ -22,12 +18,12 @@
|
||||
foreground: getCSSVariable('--ui-text-0'),
|
||||
element: canvas,
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<small>{$t('common.qr_code')}</small>
|
||||
<div>
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
<canvas bind:this={canvas} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -3,15 +3,10 @@
|
||||
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||
import type { FileDTO } from 'cryptgeon/shared'
|
||||
import type { FileDTO } from '@cryptgeon/shared'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
files?: FileDTO[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
let { label = '', files = $bindable([]), ...rest }: Props = $props()
|
||||
export let label: string = ''
|
||||
export let files: FileDTO[] = []
|
||||
|
||||
async function fileToDTO(file: File): Promise<FileDTO> {
|
||||
return {
|
||||
@ -40,7 +35,7 @@
|
||||
<small>
|
||||
{label}
|
||||
</small>
|
||||
<input {...rest} type="file" onchange={onInput} multiple />
|
||||
<input {...$$restProps} type="file" on:change={onInput} multiple />
|
||||
<div class="box">
|
||||
{#if files.length}
|
||||
<div>
|
||||
@ -50,8 +45,8 @@
|
||||
{file.name}
|
||||
</div>
|
||||
{/each}
|
||||
<div class="spacer"></div>
|
||||
<Button onclick={clear}>{$t('file_upload.clear')}</Button>
|
||||
<div class="spacer" />
|
||||
<Button on:click={clear}>{$t('file_upload.clear')}</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
|
@ -1,10 +1,9 @@
|
||||
<script lang="ts" module>
|
||||
<script lang="ts" context="module">
|
||||
import IconContrast from '$lib/icons/IconContrast.svelte'
|
||||
import IconCopy from '$lib/icons/IconCopy.svelte'
|
||||
import IconDice from '$lib/icons/IconDice.svelte'
|
||||
import IconEye from '$lib/icons/IconEye.svelte'
|
||||
import IconEyeOff from '$lib/icons/IconEyeOff.svelte'
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements'
|
||||
|
||||
const map = {
|
||||
contrast: IconContrast,
|
||||
@ -16,27 +15,22 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
icon: keyof typeof map
|
||||
}
|
||||
|
||||
let { icon, ...rest }: HTMLButtonAttributes & Props = $props()
|
||||
export let icon: keyof typeof map
|
||||
</script>
|
||||
|
||||
<button type="button" {...rest}>
|
||||
<div on:click {...$$restProps}>
|
||||
{#if map[icon]}
|
||||
{@const SvelteComponent = map[icon]}
|
||||
<SvelteComponent />
|
||||
<svelte:component this={map[icon]} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
button {
|
||||
div {
|
||||
display: inline-block;
|
||||
contain: strict;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
button > :global(svg) {
|
||||
div > :global(svg) {
|
||||
display: block;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
Before Width: | Height: | Size: 813 B After Width: | Height: | Size: 784 B |
@ -1,4 +1,4 @@
|
||||
<script lang="ts" module>
|
||||
<script lang="ts" context="module">
|
||||
export type NoteResult = {
|
||||
id: string
|
||||
password?: string
|
||||
@ -12,13 +12,9 @@
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import Canvas from './Canvas.svelte'
|
||||
|
||||
interface Props {
|
||||
result: NoteResult
|
||||
}
|
||||
export let result: NoteResult
|
||||
|
||||
let { result }: Props = $props()
|
||||
|
||||
let url = $state(`${window.location.origin}/note/${result.id}`)
|
||||
let url = `${window.location.origin}/note/${result.id}`
|
||||
if (result.password) url += `#${result.password}`
|
||||
|
||||
function reset() {
|
||||
@ -45,7 +41,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
<br />
|
||||
<Button onclick={reset}>{$t('home.new_note')}</Button>
|
||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||
|
||||
<style>
|
||||
div {
|
||||
|
@ -1,35 +1,31 @@
|
||||
<script lang="ts" module>
|
||||
<script lang="ts" context="module">
|
||||
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
|
||||
|
||||
function saveAs(file: File) {
|
||||
const url = window.URL.createObjectURL(file)
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = url
|
||||
a.download = file.name
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
a.remove()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import pkg from 'file-saver'
|
||||
const { saveAs } = pkg
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import { copy } from '$lib/utils'
|
||||
import type { FileDTO, NotePublic } from 'cryptgeon/shared'
|
||||
import type { FileDTO, NotePublic } from '@cryptgeon/shared'
|
||||
|
||||
interface Props {
|
||||
note: DecryptedNote
|
||||
}
|
||||
|
||||
let { note }: Props = $props()
|
||||
export let note: DecryptedNote
|
||||
|
||||
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
|
||||
let files: FileDTO[] = $state([])
|
||||
let files: FileDTO[] = []
|
||||
|
||||
$: if (note.meta.type === 'file') {
|
||||
files = note.contents
|
||||
}
|
||||
|
||||
$: download = () => {
|
||||
for (const file of files) {
|
||||
downloadFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(file: FileDTO) {
|
||||
const f = new File([file.contents], file.name, {
|
||||
@ -38,17 +34,7 @@
|
||||
saveAs(f)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (note.meta.type === 'file') {
|
||||
files = note.contents
|
||||
}
|
||||
})
|
||||
let download = $derived(() => {
|
||||
for (const file of files) {
|
||||
downloadFile(file)
|
||||
}
|
||||
})
|
||||
let links = $derived(typeof note.contents === 'string' ? note.contents.match(RE_URL) : [])
|
||||
$: links = typeof note.contents === 'string' ? note.contents.match(RE_URL) : []
|
||||
</script>
|
||||
|
||||
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
|
||||
@ -57,7 +43,7 @@
|
||||
<div class="note">
|
||||
{note.contents}
|
||||
</div>
|
||||
<Button onclick={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
||||
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
||||
|
||||
{#if links && links.length}
|
||||
<div class="links">
|
||||
@ -74,13 +60,13 @@
|
||||
{:else}
|
||||
{#each files as file}
|
||||
<div class="note file">
|
||||
<button onclick={() => downloadFile(file)}>
|
||||
<button on:click={() => downloadFile(file)}>
|
||||
<b>↓ {file.name}</b>
|
||||
</button>
|
||||
<small> {file.type} - {prettyBytes(file.size)}</small>
|
||||
</div>
|
||||
{/each}
|
||||
<Button onclick={download}>{$t('show.download_all')}</Button>
|
||||
<Button on:click={download}>{$t('show.download_all')}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -1,18 +1,13 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label?: string
|
||||
value: boolean
|
||||
color?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
let { label = '', value = $bindable(), color = true, ...rest }: Props = $props()
|
||||
export let label: string = ''
|
||||
export let value: boolean
|
||||
export let color = true
|
||||
</script>
|
||||
|
||||
<label {...rest}>
|
||||
<label {...$$restProps}>
|
||||
<small>{label}</small>
|
||||
<input type="checkbox" bind:checked={value} />
|
||||
<span class:color class="slider"></span>
|
||||
<span class:color class="slider" />
|
||||
</label>
|
||||
|
||||
<style>
|
||||
|
@ -1,16 +1,11 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label?: string
|
||||
value: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
let { label = '', value = $bindable(), ...rest }: Props = $props()
|
||||
export let label: string = ''
|
||||
export let value: string
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<small>
|
||||
{label}
|
||||
</small>
|
||||
<textarea class="box" {...rest} bind:value></textarea>
|
||||
<textarea class="box" {...$$restProps} bind:value />
|
||||
</label>
|
||||
|
@ -2,34 +2,25 @@
|
||||
import Icon from '$lib/ui/Icon.svelte'
|
||||
import { copy as copyFN } from '$lib/utils'
|
||||
import { getRandomBytes, Hex } from 'occulto'
|
||||
import type { HTMLInputAttributes } from 'svelte/elements'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
value: any
|
||||
validate?: (value: any) => boolean | string
|
||||
copy?: boolean
|
||||
random?: boolean
|
||||
export let label: string = ''
|
||||
export let value: any
|
||||
export let validate: (value: any) => boolean | string = () => true
|
||||
export let copy: boolean = false
|
||||
export let random: boolean = false
|
||||
|
||||
const initialType = $$restProps.type
|
||||
const isPassword = initialType === 'password'
|
||||
let hidden = true
|
||||
|
||||
$: valid = validate(value)
|
||||
|
||||
$: if (isPassword) {
|
||||
value
|
||||
$$restProps.type = hidden ? initialType : 'text'
|
||||
}
|
||||
|
||||
let {
|
||||
label = '',
|
||||
value = $bindable(),
|
||||
validate = () => true,
|
||||
copy = false,
|
||||
random = false,
|
||||
...rest
|
||||
}: HTMLInputAttributes & Props = $props()
|
||||
|
||||
const initialType = $state(rest.type)
|
||||
const isPassword = initialType === 'password'
|
||||
let hidden = $state(true)
|
||||
|
||||
let valid = $derived(validate(value))
|
||||
let type = $derived(isPassword ? (hidden ? 'password' : 'text') : rest.type)
|
||||
|
||||
function toggle() {
|
||||
console.debug('toggle')
|
||||
hidden = !hidden
|
||||
}
|
||||
|
||||
@ -39,32 +30,22 @@
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<small class:disabled={rest.disabled}>
|
||||
<small class:disabled={$$restProps.disabled}>
|
||||
{label}
|
||||
{#if valid !== true}
|
||||
<span class="error-text">{valid}</span>
|
||||
{/if}
|
||||
</small>
|
||||
<input bind:value {...rest} {type} autocomplete="off" class:valid={valid === true} />
|
||||
<input bind:value {...$$restProps} class:valid={valid === true} />
|
||||
<div class="icons">
|
||||
{#if isPassword}
|
||||
<Icon
|
||||
disabled={rest.disabled}
|
||||
class="icon"
|
||||
icon={hidden ? 'eye' : 'eye-off'}
|
||||
onclick={toggle}
|
||||
/>
|
||||
<Icon class="icon" icon={hidden ? 'eye' : 'eye-off'} on:click={toggle} />
|
||||
{/if}
|
||||
{#if random}
|
||||
<Icon disabled={rest.disabled} class="icon" icon="dice" onclick={randomFN} />
|
||||
<Icon class="icon" icon="dice" on:click={randomFN} />
|
||||
{/if}
|
||||
{#if copy}
|
||||
<Icon
|
||||
disabled={rest.disabled}
|
||||
class="icon"
|
||||
icon="copy"
|
||||
onclick={() => copyFN(value.toString())}
|
||||
/>
|
||||
<Icon class="icon" icon="copy" on:click={() => copyFN(value.toString())} />
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
|
@ -1,21 +1,24 @@
|
||||
<script lang="ts" module>
|
||||
<script lang="ts" context="module">
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
const themes = ['dark', 'light', 'auto'] as const
|
||||
type Theme = (typeof themes)[number]
|
||||
enum Theme {
|
||||
Dark = 'dark',
|
||||
Light = 'light',
|
||||
Auto = 'auto',
|
||||
}
|
||||
|
||||
const NextTheme: Record<Theme, Theme> = {
|
||||
auto: 'light',
|
||||
light: 'dark',
|
||||
dark: 'auto',
|
||||
const NextTheme = {
|
||||
[Theme.Auto]: Theme.Light,
|
||||
[Theme.Light]: Theme.Dark,
|
||||
[Theme.Dark]: Theme.Auto,
|
||||
}
|
||||
|
||||
function init(): Theme {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = window.localStorage.getItem('theme') as Theme
|
||||
if (themes.includes(saved)) return saved
|
||||
if (Object.values(Theme).includes(saved)) return saved
|
||||
}
|
||||
return 'auto'
|
||||
return Theme.Auto
|
||||
}
|
||||
|
||||
export const theme = writable<Theme>(init())
|
||||
@ -37,7 +40,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={change}>
|
||||
<button on:click={change}>
|
||||
<Icon class="icon" icon="contrast" />
|
||||
{$theme}
|
||||
</button>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import { notify } from './toast'
|
||||
|
||||
export function copy(value: string) {
|
||||
window.navigator.clipboard.writeText(value)
|
||||
copyToClipboard(value)
|
||||
const msg = get(t)('common.copied_to_clipboard')
|
||||
notify.success(msg)
|
||||
}
|
||||
|
@ -13,31 +13,30 @@
|
||||
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextArea from '$lib/ui/TextArea.svelte'
|
||||
import { Adapters, API, PayloadToLargeError, type FileDTO, type Note } from 'cryptgeon/shared'
|
||||
import type { FileDTO, Note } from '@cryptgeon/shared'
|
||||
import { Adapters, PayloadToLargeError, create } from '@cryptgeon/shared'
|
||||
|
||||
let note: Note = $state({
|
||||
let note: Note = {
|
||||
contents: '',
|
||||
meta: { type: 'text' },
|
||||
views: 1,
|
||||
expiration: 60,
|
||||
})
|
||||
let files: FileDTO[] = $state([])
|
||||
let result: NoteResult | null = $state(null)
|
||||
let advanced = $state(false)
|
||||
let isFile = $state(false)
|
||||
let timeExpiration = $state(false)
|
||||
let customPassword: string | null = $state(null)
|
||||
let description = $state('')
|
||||
let loading: string | null = $state(null)
|
||||
}
|
||||
let files: FileDTO[]
|
||||
let result: NoteResult | null = null
|
||||
let advanced = false
|
||||
let isFile = false
|
||||
let timeExpiration = false
|
||||
let customPassword: string | null = null
|
||||
let description = ''
|
||||
let loading: string | null = null
|
||||
|
||||
$effect(() => {
|
||||
if (!advanced) {
|
||||
note.views = 1
|
||||
timeExpiration = false
|
||||
}
|
||||
})
|
||||
$: if (!advanced) {
|
||||
note.views = 1
|
||||
timeExpiration = false
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
$: {
|
||||
description = $t('home.explanation', {
|
||||
values: {
|
||||
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
||||
@ -45,22 +44,17 @@
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
note.meta.type = isFile ? 'file' : 'text'
|
||||
})
|
||||
$: note.meta.type = isFile ? 'file' : 'text'
|
||||
|
||||
$effect(() => {
|
||||
if (!isFile) {
|
||||
note.contents = ''
|
||||
}
|
||||
})
|
||||
$: if (!isFile) {
|
||||
note.contents = ''
|
||||
}
|
||||
|
||||
class EmptyContentError extends Error {}
|
||||
|
||||
async function submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
async function submit() {
|
||||
try {
|
||||
loading = $t('common.encrypting')
|
||||
|
||||
@ -83,7 +77,7 @@
|
||||
else data.views = parseInt(note.views as any)
|
||||
|
||||
loading = $t('common.uploading')
|
||||
const response = await API.create(data)
|
||||
const response = await create(data)
|
||||
result = {
|
||||
id: response.id,
|
||||
password: customPassword ? undefined : Hex.encode(key),
|
||||
@ -110,7 +104,7 @@
|
||||
<p>
|
||||
{@html $status?.theme_text || $t('home.intro')}
|
||||
</p>
|
||||
<form onsubmit={submit}>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<fieldset disabled={loading !== null}>
|
||||
{#if isFile}
|
||||
<FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
|
||||
@ -139,7 +133,7 @@
|
||||
bind:value={advanced}
|
||||
/>
|
||||
{/if}
|
||||
<div class="grow"></div>
|
||||
<div class="grow" />
|
||||
<div class="tr">
|
||||
<small>{$t('common.max')}: <MaxSize /> </small>
|
||||
<br />
|
||||
|
@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ThemeToggle from '$lib/ui/ThemeToggle.svelte'
|
||||
import { status } from '$lib/stores/status'
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
@ -8,11 +7,6 @@
|
||||
<nav>
|
||||
<a href="/">/home</a>
|
||||
<a href="/about">/about</a>
|
||||
{#if $status?.imprint_url}
|
||||
<a href={$status.imprint_url} target="_blank" rel="noopener noreferrer">/imprint</a>
|
||||
{:else if $status?.imprint_html}
|
||||
<a href="/imprint">/imprint</a>
|
||||
{/if}
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
||||
code
|
||||
</a>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<a onclick={reset} href="/">
|
||||
<a on:click={reset} href="/">
|
||||
{#if $status?.theme_image}
|
||||
<img alt="logo" src={$status.theme_image} />
|
||||
{:else}
|
||||
|
@ -8,11 +8,6 @@
|
||||
import { init as initStores, status } from '$lib/stores/status'
|
||||
import Footer from '$lib/views/Footer.svelte'
|
||||
import Header from '$lib/views/Header.svelte'
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let { children }: Props = $props()
|
||||
|
||||
onMount(() => {
|
||||
initStores()
|
||||
@ -27,7 +22,7 @@
|
||||
{#await waitLocale() then _}
|
||||
<main>
|
||||
<Header />
|
||||
{@render children?.()}
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<SvelteToast />
|
||||
|
@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { status } from '$lib/stores/status'
|
||||
|
||||
status.subscribe((config) => {
|
||||
if (config != null) {
|
||||
if (config.imprint_url) {
|
||||
window.location.href = config.imprint_url
|
||||
} else if (config.imprint_html == '') {
|
||||
goto('/about')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Imprint</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="content">
|
||||
{#if $status?.imprint_html}
|
||||
{@html $status.imprint_html}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
</style>
|
@ -7,32 +7,28 @@
|
||||
import Loader from '$lib/ui/Loader.svelte'
|
||||
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import { Adapters, API, type NoteMeta } from 'cryptgeon/shared'
|
||||
import { Adapters, get, info, type NoteMeta } from '@cryptgeon/shared'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
interface Props {
|
||||
data: PageData
|
||||
}
|
||||
|
||||
let { data }: Props = $props()
|
||||
export let data: PageData
|
||||
|
||||
let id = data.id
|
||||
let password: string | null = $state<string | null>(null)
|
||||
let note: DecryptedNote | null = $state(null)
|
||||
let exists = $state(false)
|
||||
let meta: NoteMeta | null = $state(null)
|
||||
let password: string | null = null
|
||||
let note: DecryptedNote | null = null
|
||||
let exists = false
|
||||
let meta: NoteMeta | null = null
|
||||
|
||||
let loading: string | null = $state(null)
|
||||
let error: string | null = $state(null)
|
||||
let loading: string | null = null
|
||||
let error: string | null = null
|
||||
|
||||
let valid = $derived(!!password?.length)
|
||||
$: valid = !!password?.length
|
||||
|
||||
onMount(async () => {
|
||||
// Check if note exists
|
||||
try {
|
||||
loading = $t('common.loading')
|
||||
password = window.location.hash.slice(1)
|
||||
const note = await API.info(id)
|
||||
const note = await info(id)
|
||||
meta = note.meta
|
||||
exists = true
|
||||
} catch {
|
||||
@ -45,8 +41,7 @@
|
||||
/**
|
||||
* Get the actual contents of the note and decrypt it.
|
||||
*/
|
||||
async function show(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
async function show() {
|
||||
try {
|
||||
if (!valid) {
|
||||
error = $t('show.errors.no_password')
|
||||
@ -56,7 +51,7 @@
|
||||
// Load note
|
||||
error = null
|
||||
loading = $t('common.downloading')
|
||||
const data = await API.get(id)
|
||||
const data = await get(id)
|
||||
loading = $t('common.decrypting')
|
||||
const derived = meta?.derivation && (await AES.derive(password!, meta.derivation))
|
||||
const key = derived ? derived[0] : Hex.decode(password!)
|
||||
@ -91,7 +86,7 @@
|
||||
{:else if note && !error}
|
||||
<ShowNote {note} />
|
||||
{:else}
|
||||
<form onsubmit={show}>
|
||||
<form on:submit|preventDefault={show}>
|
||||
<fieldset>
|
||||
<p>{$t('show.explanation')}</p>
|
||||
{#if meta?.derivation}
|
||||
|
@ -1,17 +1,12 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
||||
|
||||
const port = 3000
|
||||
const port = 8001
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8000',
|
||||
},
|
||||
},
|
||||
server: { port },
|
||||
preview: { port },
|
||||
plugins: [sveltekit(), precompileIntl('locales')],
|
||||
}
|
||||
|
12
packages/proxy/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@cryptgeon/proxy",
|
||||
"type": "module",
|
||||
"main": "./proxy.js",
|
||||
"scripts": {
|
||||
"dev": "node ."
|
||||
},
|
||||
"dependencies": {
|
||||
"http-proxy": "^1.18.1"
|
||||
}
|
||||
}
|
16
packages/proxy/proxy.js
Normal file
@ -0,0 +1,16 @@
|
||||
import http from 'http'
|
||||
import httpProxy from 'http-proxy'
|
||||
|
||||
const proxy = httpProxy.createProxyServer()
|
||||
proxy.on('error', function (err, req, res) {
|
||||
console.error(err)
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
||||
res.end('500 Internal Server Error')
|
||||
})
|
||||
|
||||
const server = http.createServer(function (req, res) {
|
||||
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
|
||||
proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 })
|
||||
})
|
||||
server.listen(1234)
|
||||
console.log('Proxy on http://localhost:1234')
|
22
packages/shared/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@cryptgeon/shared",
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsc -w",
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"occulto": "^2.0.3"
|
||||
}
|
||||
}
|
@ -23,10 +23,6 @@ export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
||||
contents: string
|
||||
}
|
||||
|
||||
type ClientOptions = {
|
||||
server: string
|
||||
}
|
||||
|
||||
type CallOptions = {
|
||||
url: string
|
||||
method: string
|
||||
@ -35,21 +31,14 @@ type CallOptions = {
|
||||
|
||||
export class PayloadToLargeError extends Error {}
|
||||
|
||||
export let client: ClientOptions = {
|
||||
server: '',
|
||||
export let BASE = ''
|
||||
|
||||
export function setBase(url: string) {
|
||||
BASE = url
|
||||
}
|
||||
|
||||
function setOptions(options: Partial<ClientOptions>) {
|
||||
client = { ...client, ...options }
|
||||
}
|
||||
|
||||
function getOptions(): ClientOptions {
|
||||
return client
|
||||
}
|
||||
|
||||
async function call(options: CallOptions) {
|
||||
const url = client.server + '/api/' + options.url
|
||||
const response = await fetch(url, {
|
||||
export async function call(options: CallOptions) {
|
||||
const response = await fetch(BASE + '/api/' + options.url, {
|
||||
method: options.method,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
mode: 'cors',
|
||||
@ -65,7 +54,7 @@ async function call(options: CallOptions) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function create(note: Note) {
|
||||
export async function create(note: Note) {
|
||||
const { meta, ...rest } = note
|
||||
const body: NoteCreate = {
|
||||
...rest,
|
||||
@ -79,7 +68,7 @@ async function create(note: Note) {
|
||||
return data as { id: string }
|
||||
}
|
||||
|
||||
async function get(id: string): Promise<NotePublic> {
|
||||
export async function get(id: string): Promise<NotePublic> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'delete',
|
||||
@ -93,7 +82,7 @@ async function get(id: string): Promise<NotePublic> {
|
||||
return note
|
||||
}
|
||||
|
||||
async function info(id: string): Promise<NoteInfo> {
|
||||
export async function info(id: string): Promise<NoteInfo> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'get',
|
||||
@ -112,29 +101,16 @@ export type Status = {
|
||||
max_views: number
|
||||
max_expiration: number
|
||||
allow_advanced: boolean
|
||||
allow_files: boolean
|
||||
imprint_url: string
|
||||
imprint_html: string
|
||||
theme_image: string
|
||||
theme_text: string
|
||||
theme_favicon: string
|
||||
theme_page_title: string
|
||||
theme_new_note_notice: boolean
|
||||
}
|
||||
|
||||
async function status() {
|
||||
export async function status() {
|
||||
const data = await call({
|
||||
url: 'status/',
|
||||
method: 'get',
|
||||
})
|
||||
return data as Status
|
||||
}
|
||||
|
||||
export const API = {
|
||||
setOptions,
|
||||
getOptions,
|
||||
create,
|
||||
get,
|
||||
info,
|
||||
status,
|
||||
}
|
12
packages/shared/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"composite": true,
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"strict": true
|
||||
}
|
||||
}
|
@ -3,19 +3,17 @@ import { devices, type PlaywrightTestConfig } from '@playwright/test'
|
||||
const config: PlaywrightTestConfig = {
|
||||
use: {
|
||||
video: 'retain-on-failure',
|
||||
baseURL: 'http://localhost:3000',
|
||||
actionTimeout: 30_000,
|
||||
baseURL: 'http://localhost:1234',
|
||||
actionTimeout: 60_000,
|
||||
},
|
||||
|
||||
outputDir: './test-results',
|
||||
testDir: './test',
|
||||
timeout: 30_000,
|
||||
fullyParallel: true,
|
||||
retries: 2,
|
||||
timeout: 60_000,
|
||||
|
||||
webServer: {
|
||||
command: 'pnpm run docker:up',
|
||||
port: 3000,
|
||||
command: 'docker compose -f docker-compose.dev.yaml up',
|
||||
port: 1234,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
|
||||
@ -23,6 +21,10 @@ const config: PlaywrightTestConfig = {
|
||||
{ name: 'chrome', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'safari', use: { ...devices['Desktop Safari'] } },
|
||||
|
||||
{ name: 'cli', use: { ...devices['Desktop Chrome'] }, grep: [/@cli/] },
|
||||
{ name: 'web', use: { ...devices['Desktop Chrome'] }, grep: [/@web/] },
|
||||
{ name: 'cross', use: { ...devices['Desktop Chrome'] }, grep: [/@cross/] },
|
||||
],
|
||||
}
|
||||
|
||||
|
5107
pnpm-lock.yaml
generated
@ -1,11 +1,10 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { basename } from 'node:path'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { Files, getFileChecksum, tmpFile } from '../../files'
|
||||
import { Files, getFileChecksum, rm, tmpFile } from '../../files'
|
||||
import { CLI, getLinkFromCLI } from '../../utils'
|
||||
|
||||
test.describe('file @cli', () => {
|
||||
test('simple', async () => {
|
||||
test('simple', async ({ page }) => {
|
||||
const file = await tmpFile(Files.Image)
|
||||
const checksum = await getFileChecksum(file)
|
||||
const note = await CLI('send', 'file', file)
|
||||
@ -18,7 +17,7 @@ test.describe('file @cli', () => {
|
||||
test.expect(checksum).toBe(c)
|
||||
})
|
||||
|
||||
test('simple with password', async () => {
|
||||
test('simple with password', async ({ page }) => {
|
||||
const file = await tmpFile(Files.Image)
|
||||
const password = 'password'
|
||||
const checksum = await getFileChecksum(file)
|
||||
|
@ -2,7 +2,7 @@ import { test } from '@playwright/test'
|
||||
import { CLI, getLinkFromCLI } from '../../utils'
|
||||
|
||||
test.describe('text @cli', () => {
|
||||
test('simple', async () => {
|
||||
test('simple', async ({ page }) => {
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||
const note = await CLI('send', 'text', text)
|
||||
const link = getLinkFromCLI(note.stdout)
|
||||
@ -11,7 +11,7 @@ test.describe('text @cli', () => {
|
||||
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||
})
|
||||
|
||||
test('simple with password', async () => {
|
||||
test('simple with password', async ({ page }) => {
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying.`
|
||||
const password = 'password'
|
||||
const note = await CLI('send', 'text', text, '--password', password)
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { basename } from 'node:path'
|
||||
import { Files, getFileChecksum, tmpFile } from '../../files'
|
||||
import { CLI, checkLinkForDownload, createNoteSuccessfully, getLinkFromCLI } from '../../utils'
|
||||
import { CLI, checkLinkForDownload, checkLinkForText, createNote, getLinkFromCLI } from '../../utils'
|
||||
import { Files, getFileChecksum, rm, tmpFile } from '../../files'
|
||||
import { basename } from 'path'
|
||||
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||
const password = 'password'
|
||||
@ -31,7 +30,7 @@ test.describe('text @cross', () => {
|
||||
test('web to cli', async ({ page }) => {
|
||||
const files = [Files.Image]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNoteSuccessfully(page, { files })
|
||||
const link = await createNote(page, { files })
|
||||
|
||||
const filename = basename(files[0])
|
||||
await CLI('open', link, '--all')
|
||||
@ -43,7 +42,7 @@ test.describe('text @cross', () => {
|
||||
test('web to cli with password', async ({ page }) => {
|
||||
const files = [Files.Image]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNoteSuccessfully(page, { files, password })
|
||||
const link = await createNote(page, { files, password })
|
||||
|
||||
const filename = basename(files[0])
|
||||
await CLI('open', link, '--all', '--password', password)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { CLI, checkLinkForText, createNoteSuccessfully, getLinkFromCLI } from '../../utils'
|
||||
import { CLI, checkLinkForText, createNote, getLinkFromCLI } from '../../utils'
|
||||
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||
const password = 'password'
|
||||
@ -13,7 +13,7 @@ test.describe('text @cross', () => {
|
||||
})
|
||||
|
||||
test('web to cli', async ({ page }) => {
|
||||
const link = await createNoteSuccessfully(page, { text })
|
||||
const link = await createNote(page, { text })
|
||||
const retrieved = await CLI('open', link)
|
||||
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||
})
|
||||
@ -25,7 +25,7 @@ test.describe('text @cross', () => {
|
||||
})
|
||||
|
||||
test('web to cli with password', async ({ page }) => {
|
||||
const link = await createNoteSuccessfully(page, { text, password })
|
||||
const link = await createNote(page, { text, password })
|
||||
const retrieved = await CLI('open', link, '--password', password)
|
||||
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||
})
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { cp, readFile } from 'node:fs/promises'
|
||||
import { createHash } from 'crypto'
|
||||
import { cp as cpFN, rm as rmFN } from 'fs'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { promisify } from 'util'
|
||||
|
||||
export const cp = promisify(cpFN)
|
||||
export const rm = promisify(rmFN)
|
||||
|
||||
export const Files = {
|
||||
PDF: 'test/assets/AES.pdf',
|
||||
|
@ -6,22 +6,17 @@ import { getFileChecksum } from './files'
|
||||
const exec = promisify(execFile)
|
||||
|
||||
type CreatePage = {
|
||||
text?: string
|
||||
files?: string[]
|
||||
views?: number
|
||||
expiration?: number
|
||||
error?: string
|
||||
password?: string
|
||||
} & (
|
||||
| {
|
||||
text: string
|
||||
}
|
||||
| {
|
||||
files: string[]
|
||||
}
|
||||
)
|
||||
async function createNote(page: Page, options: CreatePage): Promise<void> {
|
||||
}
|
||||
export async function createNote(page: Page, options: CreatePage): Promise<string> {
|
||||
await page.goto('/')
|
||||
|
||||
if ('text' in options) {
|
||||
if (options.text) {
|
||||
await page.getByTestId('text-field').fill(options.text)
|
||||
} else if (options.files) {
|
||||
await page.getByTestId('switch-file').click()
|
||||
@ -36,8 +31,7 @@ async function createNote(page: Page, options: CreatePage): Promise<void> {
|
||||
if (options.views || options.expiration || options.password) await page.getByTestId('switch-advanced').click()
|
||||
if (options.views) {
|
||||
await page.getByTestId('field-views').fill(options.views.toString())
|
||||
}
|
||||
if (options.expiration) {
|
||||
} else if (options.expiration) {
|
||||
await page.getByTestId('switch-advanced-toggle').click()
|
||||
await page.getByTestId('field-expiration').fill(options.expiration.toString())
|
||||
}
|
||||
@ -47,18 +41,15 @@ async function createNote(page: Page, options: CreatePage): Promise<void> {
|
||||
}
|
||||
|
||||
await page.locator('button:has-text("create")').click()
|
||||
}
|
||||
|
||||
export async function createNoteSuccessfully(page: Page, options: CreatePage): Promise<string> {
|
||||
await createNote(page, options)
|
||||
if (options.error) {
|
||||
await expect(page.locator('.error-text')).toContainText(options.error, { timeout: 60_000 })
|
||||
}
|
||||
|
||||
// Return share link
|
||||
return await page.getByTestId('share-link').inputValue()
|
||||
}
|
||||
|
||||
export async function createNoteError(page: Page, options: CreatePage, error: string): Promise<void> {
|
||||
await createNote(page, options)
|
||||
await expect(page.locator('._toastContainer')).toContainText(error)
|
||||
}
|
||||
|
||||
type CheckLinkBase = {
|
||||
link: string
|
||||
text: string
|
||||
@ -78,7 +69,7 @@ export async function checkLinkForDownload(page: Page, options: CheckLinkBase &
|
||||
const path = await download.path()
|
||||
if (!path) throw new Error('Download failed')
|
||||
const cs = await getFileChecksum(path)
|
||||
expect(cs).toBe(options.checksum)
|
||||
await expect(cs).toBe(options.checksum)
|
||||
}
|
||||
|
||||
export async function checkLinkForText(page: Page, options: CheckLinkBase) {
|
||||
@ -87,7 +78,7 @@ export async function checkLinkForText(page: Page, options: CheckLinkBase) {
|
||||
if (options.password) await page.getByTestId('show-note-password').fill(options.password)
|
||||
await page.getByTestId('show-note-button').click()
|
||||
const text = await page.getByTestId('result').locator('.note').innerText()
|
||||
expect(text).toContain(options.text)
|
||||
await expect(text).toContain(options.text)
|
||||
}
|
||||
|
||||
export async function checkLinkDoesNotExist(page: Page, link: string) {
|
||||
@ -100,7 +91,7 @@ export async function CLI(...args: string[]) {
|
||||
return await exec('./packages/cli/dist/cli.cjs', args, {
|
||||
env: {
|
||||
...process.env,
|
||||
CRYPTGEON_SERVER: 'http://localhost:3000',
|
||||
CRYPTGEON_SERVER: 'http://localhost:1234',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { Files, getFileChecksum } from '../../files'
|
||||
import { checkLinkForDownload, createNoteSuccessfully } from '../../utils'
|
||||
import { checkLinkForDownload, createNote } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('multiple', async ({ page }) => {
|
||||
const files = [Files.PDF, Files.Image]
|
||||
const checksums = await Promise.all(files.map(getFileChecksum))
|
||||
const link = await createNoteSuccessfully(page, { files, views: 2 })
|
||||
const link = await createNote(page, { files, views: 2 })
|
||||
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum: checksums[1] })
|
||||
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum: checksums[0] })
|
||||
})
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { Files, getFileChecksum } from '../../files'
|
||||
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNoteSuccessfully } from '../../utils'
|
||||
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('simple pdf', async ({ page }) => {
|
||||
const files = [Files.PDF]
|
||||
const link = await createNoteSuccessfully(page, { files })
|
||||
const link = await createNote(page, { files })
|
||||
await checkLinkForText(page, { link, text: 'AES.pdf' })
|
||||
await checkLinkDoesNotExist(page, link)
|
||||
})
|
||||
@ -13,21 +13,21 @@ test.describe('@web', () => {
|
||||
test('pdf content', async ({ page }) => {
|
||||
const files = [Files.PDF]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNoteSuccessfully(page, { files })
|
||||
const link = await createNote(page, { files })
|
||||
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum })
|
||||
})
|
||||
|
||||
test('image content', async ({ page }) => {
|
||||
const files = [Files.Image]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNoteSuccessfully(page, { files })
|
||||
const link = await createNote(page, { files })
|
||||
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum })
|
||||
})
|
||||
|
||||
test('simple pdf with password', async ({ page }) => {
|
||||
const files = [Files.PDF]
|
||||
const password = 'password'
|
||||
const link = await createNoteSuccessfully(page, { files, password })
|
||||
const link = await createNote(page, { files, password })
|
||||
await checkLinkForText(page, { link, text: 'AES.pdf', password })
|
||||
await checkLinkDoesNotExist(page, link)
|
||||
})
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { createNote } from '../../utils'
|
||||
import { Files } from '../../files'
|
||||
import { createNoteError } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test.skip('to big zip', async ({ page }) => {
|
||||
const files = [Files.Zip]
|
||||
await createNoteError(page, { files }, 'could not create note. note is too big')
|
||||
const link = await createNote(page, { files, error: 'note is to big' })
|
||||
})
|
||||
})
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNoteSuccessfully } from '../../utils'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('1 minute', async ({ page, browserName }) => {
|
||||
test.skip(browserName === 'webkit')
|
||||
test('1 minute', async ({ page }) => {
|
||||
const text = `Virtues value ascetic revaluation sea dead strong burying.`
|
||||
const minutes = 1
|
||||
const timeout = minutes * 60_000
|
||||
test.setTimeout(timeout * 2)
|
||||
const link = await createNoteSuccessfully(page, { text, expiration: minutes })
|
||||
const link = await createNote(page, { text, expiration: minutes })
|
||||
await checkLinkForText(page, { link, text })
|
||||
await checkLinkForText(page, { link, text })
|
||||
await page.waitForTimeout(timeout)
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkForText, createNoteSuccessfully } from '../../utils'
|
||||
import { checkLinkForText, createNote } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('simple', async ({ page }) => {
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of deceive play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||
const link = await createNoteSuccessfully(page, { text })
|
||||
const link = await createNote(page, { text })
|
||||
await checkLinkForText(page, { link, text })
|
||||
})
|
||||
|
||||
test('simple with password', async ({ page }) => {
|
||||
const text = 'Foo bar'
|
||||
const password = '123'
|
||||
const shareLink = await createNoteSuccessfully(page, { text, password })
|
||||
const shareLink = await createNote(page, { text, password })
|
||||
await checkLinkForText(page, { link: shareLink, text, password })
|
||||
})
|
||||
})
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNoteSuccessfully } from '../../utils'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('only shown once', async ({ page }) => {
|
||||
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
|
||||
const link = await createNoteSuccessfully(page, { text })
|
||||
const link = await createNote(page, { text })
|
||||
await checkLinkForText(page, { link, text })
|
||||
await checkLinkDoesNotExist(page, link)
|
||||
})
|
||||
|
||||
test('view 3 times', async ({ page }) => {
|
||||
const text = `Justice holiest overcome fearful strong ultimate holiest christianity.`
|
||||
const link = await createNoteSuccessfully(page, { text, views: 3 })
|
||||
const link = await createNote(page, { text, views: 3 })
|
||||
await checkLinkForText(page, { link, text })
|
||||
await checkLinkForText(page, { link, text })
|
||||
await checkLinkForText(page, { link, text })
|
||||
|