Compare commits

...

78 Commits

Author SHA1 Message Date
a47b8a482c Merge pull request #151 from cupcakearmy/150-type-button
150 type button
2024-09-20 20:24:26 +02:00
847fc9677d bump version 2024-09-20 18:16:01 +00:00
6979be0c4a bump pnpm version 2024-09-20 18:15:56 +00:00
f61d3ece8b add type button to element 2024-09-20 18:15:38 +00:00
14d3e9eb03 add raycast links 2024-09-03 15:57:29 +02:00
7c6ba654f6 correct version bump 2024-09-02 10:28:08 +02:00
cb20224317 version bumps 2024-09-02 10:27:26 +02:00
085b1c20df Merge pull request #148 from cupcakearmy/move-shared-package-into-cli
Move shared package into cli
2024-09-02 10:26:35 +02:00
4b1f939281 remove useless dependecies and use cli instead of package 2024-09-02 10:20:03 +02:00
7b919f2a53 move shared package to cli 2024-09-02 10:19:35 +02:00
868b49c1c3 add guide 2024-08-27 15:11:18 +02:00
6b5eea34a1 Merge pull request #147 from cupcakearmy/actix-2-axum
Actix 2 axum
2024-08-27 15:06:29 +02:00
d70bee14af version bump 2024-08-27 14:52:17 +02:00
4960260076 also disable icon in text 2024-08-27 14:49:08 +02:00
3247a0cfca skip expiration in safari 2024-08-27 14:35:39 +02:00
9527a499ed skip size 2024-08-27 13:26:04 +02:00
b5590bb5ef add retry 2024-08-27 11:45:43 +02:00
7691dc80f8 use pnpm command 2024-08-27 10:56:21 +02:00
f8d8fa05b0 new message 2024-08-27 10:55:59 +02:00
7aae690850 cleanup imports 2024-08-27 10:55:51 +02:00
e15d9eb537 upload artifacts 2024-08-27 00:42:21 +02:00
7fe70a6f3b revert 2024-08-27 00:38:56 +02:00
d86ef79abd use container 2024-08-27 00:36:48 +02:00
23446a4c74 Merge branch 'main' into actix-2-axum 2024-08-27 00:32:28 +02:00
ee92928d1f pipeline 2024-08-27 00:32:17 +02:00
b60a079bbe Merge pull request #140 from elfakyn/main
Fix typos in english localization
2024-08-27 00:23:28 +02:00
ba474dff31 more robust config 2024-08-27 00:22:41 +02:00
3cb002ee33 Merge branch 'main' into main 2024-08-27 00:10:44 +02:00
a10d8735dd cleanup tests 2024-08-27 00:09:51 +02:00
dfa2401eea aria check 2024-08-25 22:22:45 +02:00
ea58d89f98 cleanup 2024-08-25 22:22:13 +02:00
eaca1a981d cleanup 2024-08-25 22:21:45 +02:00
199755d18e timeout 2024-08-25 22:20:46 +02:00
724d0709d3 .env.dev 2024-08-25 22:20:29 +02:00
bd5acca97a use axum body limit 2024-08-25 22:19:41 +02:00
a0a99cd3cc cleanup status variable 2024-08-25 22:18:31 +02:00
c3794fa2b6 port 3000 2024-08-23 14:27:59 +02:00
f9962c76c1 enable to big 2024-08-23 14:27:52 +02:00
c2b81bc04d refactor to use axum 2024-08-23 14:27:47 +02:00
a45f6a3772 use 3000 port 2024-08-23 14:27:17 +02:00
2006be0434 Merge pull request #145 from cupcakearmy/better-programmatic-access
better programmatic access
2024-08-23 11:19:57 +02:00
ca72e94e3c update node and playwright 2024-08-23 11:02:30 +02:00
dbcb3870aa fix tests 2024-08-23 11:01:57 +02:00
3ea176cc1f add build cli 2024-08-23 09:59:06 +02:00
145f9ef18f right package stuff 2024-08-22 20:21:05 +02:00
784c54236b better programmatic access 2024-08-22 20:01:14 +02:00
5648c76f78 Merge pull request #144 from cupcakearmy/update-rust
update rust
2024-08-22 19:57:39 +02:00
7761c795df update rust 2024-08-22 18:42:44 +02:00
4aadeb492a maintenance 2024-08-22 18:40:56 +02:00
0d9f3fe9c7 Merge pull request #130 from DDd-Devops/add-redis-tls-feature
Add redis tls feature 'rediss://'
2024-08-22 18:30:58 +02:00
Arya ';--bscriptalert(":)")/script
8ccfdd6e2e fix typos in english localization 2024-07-04 18:00:06 -04:00
f790438104 add french blog post 2024-06-16 22:34:44 +02:00
5936f4588c Merge pull request #133 from DDd-Devops/improve-french-translations
improve french translations wording
2024-06-16 22:31:34 +02:00
Thomas Chrétien
d3c04f8fda improve french translations wording 2024-06-14 14:26:02 +00:00
Matthieu Guegan
f8c17487bd Support dynamically-linked and/or native musl targets
See https://github.com/rust-lang/rust/pull/40113#issuecomment-323193341
2024-05-16 09:55:04 +02:00
Matthieu Guegan
ed3e5f48a0 Fix wrong type due to updated lib
See https://github.com/redis-rs/redis-rs/pull/589
2024-05-16 09:47:01 +02:00
Matthieu Guegan
e08c9d1871 Bump redis crate to 0.25.2
This will enable TLS feature
2024-05-16 09:45:26 +02:00
6d2150b0b6 version bump 2024-05-04 16:06:40 +02:00
3a68693be1 Merge pull request #128 from cbin/main
change locales
2024-04-26 13:45:17 +02:00
Oleg Salnikov
a612eec220 Add files via upload 2024-04-24 18:31:01 +03:00
98d3b0d394 Merge pull request #127 from zocimek/main
fix key for home.new_note_notice
2024-04-22 09:38:49 +02:00
Łukasz Pospiech
6aed2e2756 fix key for home.new_note_notice 2024-04-18 12:39:00 +02:00
6bb527198a Merge pull request #123 from zocimek/patch-1
Create polish translation
2024-04-10 16:57:18 +02:00
Łukasz Pospiech
7050389316 Create polish translation 2024-04-05 14:00:58 +02:00
0725a0c6f7 Merge pull request #122 from cupcakearmy/dependabot/npm_and_yarn/npm_and_yarn-security-group-e93d6eacd9
Bump the npm_and_yarn group across 1 directory with 1 update
2024-04-05 09:37:44 +02:00
dependabot[bot]
c8efcc04fc Bump the npm_and_yarn group across 1 directory with 1 update
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 5.1.4 to 5.1.7
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.1.7/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 17:38:45 +00:00
19bf155653 Merge pull request #120 from MairusuPawa/patch-1
Update fr.json
2024-03-24 20:57:00 +01:00
MairusuPawa
9a4e84db62 Update fr.json
Original translation confuses "crypter" and "chiffrer".
2024-03-23 16:19:29 +01:00
32cd3843a7 version bump 2024-03-21 15:55:21 +01:00
9b48d39c82 Merge pull request #119 from compasspathways/main
Misc improvements including `ALLOW_FILES` and `NEW_NOTE_NOTICE`
2024-03-19 16:04:04 +01:00
Tamás Kádár
239e950f8e NEW_NOTE_NOTICE -> THEME_NEW_NOTE_NOTICE 2024-03-19 12:06:46 +00:00
Tamás Kádár
b00846ce9d clicking the logo after the creation of the note should do a reset 2024-03-15 14:30:55 +00:00
Tamás Kádár
e70f06f99f NEW_NOTE_NOTICE feature flag 2024-03-15 14:26:33 +00:00
Tamás Kádár
4781882c72 fix extra space in /about 2024-03-15 14:19:30 +00:00
Tamás Kádár
549dfb55db update @types/node 2024-03-15 14:16:02 +00:00
Tamás Kádár
2653a4bccf ALLOW_FILES flag 2024-03-15 14:14:17 +00:00
Tamás Kádár
7213e6c690 Minor Dockerfile fixes 2024-03-15 14:11:34 +00:00
Tamás Kádár
8a5f667ff6 Fix file-saver CommonJS error 2024-03-15 13:53:12 +00:00
78 changed files with 4772 additions and 3950 deletions

View File

View File

@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Node # Node
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
cache: 'pnpm' cache: 'pnpm'
@@ -27,13 +27,16 @@ jobs:
- name: Prepare - name: Prepare
run: | run: |
pnpm install --frozen-lockfile pnpm install
pnpm exec playwright install --with-deps pnpm exec playwright install --with-deps
pnpm run test:prepare pnpm run test:prepare
- name: Run your tests - name: Run your tests
run: pnpm test run: pnpm test
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with: with:
name: test-results name: playwright-report
path: test-results path: test-results/
retention-days: 7

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ target
# Testing # Testing
test-results test-results
tmp

2
.nvmrc
View File

@@ -1 +1 @@
v20.11.1 v22.7.0

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
# FRONTEND # FRONTEND
FROM node:20-alpine as client FROM node:22-alpine as client
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN corepack enable
@@ -11,17 +11,17 @@ RUN pnpm run build
# BACKEND # BACKEND
FROM rust:1.76-alpine as backend FROM rust:1.80-alpine as backend
WORKDIR /tmp WORKDIR /tmp
RUN apk add libc-dev openssl-dev alpine-sdk RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
COPY ./packages/backend ./ COPY ./packages/backend ./
RUN cargo build --release RUN RUSTFLAGS="-Ctarget-feature=-crt-static" cargo build --release
# RUNNER # RUNNER
FROM alpine FROM alpine:3.19
WORKDIR /app WORKDIR /app
RUN apk add --no-cache curl RUN apk add --no-cache curl libgcc
COPY --from=backend /tmp/target/release/cryptgeon . COPY --from=backend /tmp/target/release/cryptgeon .
COPY --from=client /tmp/packages/frontend/build ./frontend COPY --from=client /tmp/packages/frontend/build ./frontend
ENV FRONTEND_PATH="./frontend" ENV FRONTEND_PATH="./frontend"

View File

@@ -12,9 +12,10 @@
<br/><br/> <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="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 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/> <br/><br/>
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md) EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
## About? ## About?
@@ -39,6 +40,12 @@ npx cryptgeon send text "This is a secret note"
For more documentation about the CLI see the [readme](./packages/cli/README.md). 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 ## Features
- send text or files - send text or files
@@ -62,19 +69,21 @@ of the notes even if it tried to.
## Environment Variables ## Environment Variables
| Variable | Default | Description | | Variable | Default | Description |
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) | | `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. | | `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
| `MAX_VIEWS` | `100` | Maximal number of views. | | `MAX_VIEWS` | `100` | Maximal number of views. |
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. | | `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_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
| `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_. | | `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
| `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` | | `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_. |
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable | | `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_TEXT` | `""` | Custom text for replacing the description below the logo | | `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
| `THEME_PAGE_TITLE` | `""` | Custom text the page title | | `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable | | `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`. |
## Deployment ## Deployment
@@ -139,12 +148,18 @@ 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) - 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) - 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 ## Development
**Requirements** **Requirements**
- `pnpm`: `>=6` - `pnpm`: `>=9`
- `node`: `>=18` - `node`: `>=22`
- `rust`: edition `2021` - `rust`: edition `2021`
**Install** **Install**
@@ -172,7 +187,7 @@ Running `pnpm run dev` in the root folder will start the following things:
- client - client
- cli - cli
You can see the app under [localhost:1234](http://localhost:1234). You can see the app under [localhost:3000](http://localhost:3000).
> There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json) > There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json)

View File

@@ -59,19 +59,19 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
## Variables de entorno ## Variables de entorno
| Variable | Default | Descripción | | 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) | | `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. | | `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_VIEWS` | `100` | Número máximo de vistas. |
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. | | `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. | | `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_. | | `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` | | `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_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_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título | | `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. | | `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
## Despliegue ## Despliegue
@@ -169,7 +169,7 @@ Ejecutando `pnpm run dev` en la carpeta raíz iniciará lo siguiente:
- client - client
- cli - cli
Puedes ver la app en [localhost:1234](http://localhost:1234). Puedes ver la app en [localhost:3000](http://localhost:3000).
> Existe una colección de Postman con algunas peticiones de ejemplo [disponible en el repo](./Cryptgeon.postman_collection.json) > Existe una colección de Postman con algunas peticiones de ejemplo [disponible en el repo](./Cryptgeon.postman_collection.json)

View File

@@ -14,7 +14,7 @@
<a href=""><img src="./.github/lokalise.png" height="50"> <a href=""><img src="./.github/lokalise.png" height="50">
<br/> <br/>
[EN](README.md) | 简体中文 | [ES](README_ES.md) [EN](README.md) | 简体中文 | [ES](README_ES.md)
## 关于本项目 ## 关于本项目
@@ -158,7 +158,7 @@ pnpm run dev
- 无热重载的 rust 后端 - 无热重载的 rust 后端
- 可热重载的客户端 - 可热重载的客户端
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234). 你可以通过 3000 端口进入该应用,即 [localhost:3000](http://localhost:3000).
## 测试 ## 测试

View File

@@ -11,9 +11,6 @@
}, },
{ {
"path": "packages/cli" "path": "packages/cli"
},
{
"path": "packages/shared"
} }
], ],
"settings": { "settings": {

View File

@@ -1,8 +1,6 @@
# DEV Compose file. # DEV Compose file.
# For a production file see: README.md # For a production file see: README.md
version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
@@ -11,12 +9,12 @@ services:
app: app:
build: . build: .
env_file: .dev.env env_file: .env.dev
depends_on: depends_on:
- redis - redis
restart: unless-stopped restart: unless-stopped
ports: ports:
- 1234:8000 - 3000:8000
healthcheck: healthcheck:
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/'] test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/']

View File

@@ -1,5 +1,3 @@
version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
@@ -16,7 +14,7 @@ services:
SIZE_LIMIT: 4 MiB SIZE_LIMIT: 4 MiB
ports: ports:
- 80:8000 - 80:8000
# Optional health checks # Optional health checks
# healthcheck: # healthcheck:
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"] # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]

View File

@@ -1,21 +1,21 @@
{ {
"scripts": { "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:packages": "pnpm --parallel run dev",
"dev": "run-p dev:*", "dev": "run-p dev:*",
"docker:up": "docker compose -f docker-compose.dev.yaml up", "docker:up": "docker compose -f docker-compose.dev.yaml up",
"docker:build": "docker compose -f docker-compose.dev.yaml build", "docker:build": "docker compose -f docker-compose.dev.yaml build",
"test": "playwright test --project chrome firefox safari", "test": "playwright test --project=chrome --project=firefox --project=safari",
"test:local": "playwright test --project chrome", "test:local": "playwright test --project=chrome",
"test:server": "run-s docker:up", "test:server": "run-s docker:up",
"test:prepare": "run-p build docker:build", "test:prepare": "run-p build docker:build",
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build" "build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.42.1", "@playwright/test": "^1.46.1",
"@types/node": "^20.11.24", "@types/node": "^22.5.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"shelljs": "^0.8.5" "shelljs": "^0.8.5"
}, },
"packageManager": "pnpm@8.15.4" "packageManager": "pnpm@9.11.0"
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,27 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "2.5.1" version = "2.8.2"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021" edition = "2021"
rust-version = "1.76" rust-version = "1.80"
[[bin]] [[bin]]
name = "cryptgeon" name = "cryptgeon"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
actix-web = "4" # Core
actix-files = "0.6" axum = "0.7.5"
serde = { version = "1.0", features = ["derive"] } 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
serde_json = "1" serde_json = "1"
lazy_static = "1" lazy_static = "1"
ring = "0.16" ring = "0.16"
bs62 = "0.1" bs62 = "0.1"
byte-unit = "4" byte-unit = "4"
dotenv = "0.15" dotenv = "0.15"
mime = "0.3"
env_logger = "0.9"
log = "0.4"
redis = "0.23"

View File

@@ -4,7 +4,7 @@
"scripts": { "scripts": {
"dev": "cargo watch -x 'run --bin cryptgeon'", "dev": "cargo watch -x 'run --bin cryptgeon'",
"build": "cargo build --release", "build": "cargo build --release",
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:1234 cargo run", "test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:3000 cargo run",
"test:prepare": "cargo build" "test:prepare": "cargo build"
} }
} }

View File

@@ -1,14 +0,0 @@
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()),
);
}

View File

@@ -1,17 +0,0 @@
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)?)
}

View File

@@ -14,26 +14,30 @@ lazy_static! {
// CONFIG // CONFIG
lazy_static! { lazy_static! {
pub static ref LIMIT: usize = pub static ref LIMIT: usize =
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string())) Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
.unwrap() .unwrap()
.get_bytes() as usize; .get_bytes() as usize;
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS") pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
.unwrap_or("100".to_string()) .unwrap_or("100".to_string())
.parse() .parse()
.unwrap(); .unwrap();
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION") pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
.unwrap_or("360".to_string()) // 6 hours in minutes .unwrap_or("360".to_string()) // 6 hours in minutes
.parse() .parse()
.unwrap(); .unwrap();
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED") pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
.unwrap_or("true".to_string()) .unwrap_or("true".to_string())
.parse() .parse()
.unwrap(); .unwrap();
pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH") pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH")
.unwrap_or("32".to_string()) .unwrap_or("32".to_string())
.parse() .parse()
.unwrap(); .unwrap();
pub static ref ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
.unwrap_or("true".to_string())
.parse()
.unwrap();
} }
// THEME // THEME
@@ -54,4 +58,8 @@ lazy_static! {
.unwrap_or("".to_string()) .unwrap_or("".to_string())
.parse() .parse()
.unwrap(); .unwrap();
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
.unwrap_or("true".to_string())
.parse()
.unwrap();
} }

View File

@@ -1,3 +1,10 @@
mod routes; use crate::store;
use axum::http::StatusCode;
pub use routes::*; pub async fn report_health() -> (StatusCode,) {
if store::can_reach_redis() {
return (StatusCode::OK,);
} else {
return (StatusCode::SERVICE_UNAVAILABLE,);
}
}

View File

@@ -1,16 +0,0 @@
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)
}

View File

@@ -1,44 +1,67 @@
use actix_web::{ use axum::{
middleware::{self, Logger}, extract::{DefaultBodyLimit, Request},
web::{self}, routing::{delete, get, post},
App, HttpServer, Router, ServiceExt,
}; };
use dotenv::dotenv; use dotenv::dotenv;
use log::error; use tower::Layer;
use tower_http::{
compression::CompressionLayer,
normalize_path::NormalizePathLayer,
services::{ServeDir, ServeFile},
};
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
mod api;
mod client;
mod config; mod config;
mod health; mod health;
mod note; mod note;
mod size;
mod status; mod status;
mod store; mod store;
#[actix_web::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() {
dotenv().ok(); dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
if !store::can_reach_redis() { if !store::can_reach_redis() {
error!("cannot reach redis"); println!("cannot reach redis");
panic!("canont reach redis"); panic!("canont reach redis");
} }
return HttpServer::new(|| { let notes_routes = Router::new()
App::new() .route("/", post(note::create))
.wrap(Logger::new("\"%r\" %s %b %T")) .route("/:id", delete(note::delete))
.wrap(middleware::Compress::default()) .route("/:id", get(note::preview));
.wrap(middleware::DefaultHeaders::default()) let health_routes = Router::new().route("/live", get(health::report_health));
.configure(size::init) let status_routes = Router::new().route("/status", get(status::get_status));
.configure(api::init) let api_routes = Router::new()
.configure(client::init) .nest("/notes", notes_routes)
.default_service(web::to(client::index)) .nest("/", health_routes)
}) .nest("/", status_routes);
.bind(config::LISTEN_ADDR.to_string())?
.run() let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
.await; 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)
.layer(DefaultBodyLimit::max(*config::LIMIT))
.layer(
CompressionLayer::new()
.br(true)
.deflate(true)
.gzip(true)
.zstd(true),
);
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();
} }

View File

@@ -12,12 +12,12 @@ pub struct Note {
pub expiration: Option<u32>, pub expiration: Option<u32>,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize)]
pub struct NoteInfo { pub struct NoteInfo {
pub meta: String, pub meta: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize)]
pub struct NotePublic { pub struct NotePublic {
pub meta: String, pub meta: String,
pub contents: String, pub contents: String,

View File

@@ -1,11 +1,18 @@
use actix_web::{delete, get, post, web, HttpResponse, Responder, Scope}; use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::SystemTime; use std::time::SystemTime;
use crate::config; use crate::config;
use crate::note::{generate_id, Note, NoteInfo, NotePublic}; use crate::note::{generate_id, Note, NoteInfo};
use crate::store; use crate::store;
use super::NotePublic;
pub fn now() -> u32 { pub fn now() -> u32 {
SystemTime::now() SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
@@ -13,20 +20,18 @@ pub fn now() -> u32 {
.as_secs() as u32 .as_secs() as u32
} }
#[derive(Serialize, Deserialize)] #[derive(Deserialize)]
struct NotePath { pub struct OneNoteParams {
id: String, id: String,
} }
#[get("/{id}")] pub async fn preview(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
async fn one(path: web::Path<NotePath>) -> impl Responder { let note = store::get(&id);
let p = path.into_inner();
let note = store::get(&p.id);
match note { match note {
Ok(Some(n)) => HttpResponse::Ok().json(NoteInfo { meta: n.meta }), Ok(Some(n)) => (StatusCode::OK, Json(NoteInfo { meta: n.meta })).into_response(),
Ok(None) => HttpResponse::NotFound().finish(), Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
@@ -35,13 +40,16 @@ struct CreateResponse {
id: String, id: String,
} }
#[post("/")] pub async fn create(Json(mut n): Json<Note>) -> Response {
async fn create(note: web::Json<Note>) -> impl Responder { // let mut n = note.into_inner();
let mut n = note.into_inner();
let id = generate_id(); let id = generate_id();
let bad_req = HttpResponse::BadRequest().finish(); // let bad_req = HttpResponse::BadRequest().finish();
if n.views == None && n.expiration == None { if n.views == None && n.expiration == None {
return bad_req; return (
StatusCode::BAD_REQUEST,
"At least views or expiration must be set",
)
.into_response();
} }
if !*config::ALLOW_ADVANCED { if !*config::ALLOW_ADVANCED {
n.views = Some(1); n.views = Some(1);
@@ -50,7 +58,7 @@ async fn create(note: web::Json<Note>) -> impl Responder {
match n.views { match n.views {
Some(v) => { Some(v) => {
if v > *config::MAX_VIEWS || v < 1 { if v > *config::MAX_VIEWS || v < 1 {
return bad_req; return (StatusCode::BAD_REQUEST, "Invalid views").into_response();
} }
n.expiration = None; // views overrides expiration n.expiration = None; // views overrides expiration
} }
@@ -58,8 +66,8 @@ async fn create(note: web::Json<Note>) -> impl Responder {
} }
match n.expiration { match n.expiration {
Some(e) => { Some(e) => {
if e > *config::MAX_EXPIRATION { if e > *config::MAX_EXPIRATION || e < 1 {
return bad_req; return (StatusCode::BAD_REQUEST, "Invalid expiration").into_response();
} }
let expiration = now() + (e * 60); let expiration = now() + (e * 60);
n.expiration = Some(expiration); n.expiration = Some(expiration);
@@ -67,38 +75,40 @@ async fn create(note: web::Json<Note>) -> impl Responder {
_ => {} _ => {}
} }
match store::set(&id.clone(), &n.clone()) { match store::set(&id.clone(), &n.clone()) {
Ok(_) => return HttpResponse::Ok().json(CreateResponse { id: id }), Ok(_) => (StatusCode::OK, Json(CreateResponse { id })).into_response(),
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
#[delete("/{id}")] pub async fn delete(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
async fn delete(path: web::Path<NotePath>) -> impl Responder { let note = store::get(&id);
let p = path.into_inner();
let note = store::get(&p.id);
match note { match note {
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), // Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
Ok(None) => return HttpResponse::NotFound().finish(), // Ok(None) => return HttpResponse::NotFound().finish(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Ok(Some(note)) => { Ok(Some(note)) => {
let mut changed = note.clone(); let mut changed = note.clone();
if changed.views == None && changed.expiration == None { if changed.views == None && changed.expiration == None {
return HttpResponse::BadRequest().finish(); return (StatusCode::BAD_REQUEST).into_response();
} }
match changed.views { match changed.views {
Some(v) => { Some(v) => {
changed.views = Some(v - 1); changed.views = Some(v - 1);
let id = p.id.clone(); let id = id.clone();
if v <= 1 { if v <= 1 {
match store::del(&id) { match store::del(&id) {
Err(e) => { Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string()) return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response();
} }
_ => {} _ => {}
} }
} else { } else {
match store::set(&id, &changed.clone()) { match store::set(&id, &changed.clone()) {
Err(e) => { Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string()) return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response();
} }
_ => {} _ => {}
} }
@@ -111,33 +121,26 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
match changed.expiration { match changed.expiration {
Some(e) => { Some(e) => {
if e < n { if e < n {
match store::del(&p.id.clone()) { match store::del(&id.clone()) {
Ok(_) => return HttpResponse::BadRequest().finish(), Ok(_) => return (StatusCode::BAD_REQUEST).into_response(),
Err(e) => { Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string()) return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response()
} }
} }
} }
} }
_ => {} _ => {}
} }
return HttpResponse::Ok().json(NotePublic {
contents: changed.contents, return (
meta: changed.meta, StatusCode::OK,
}); Json(NotePublic {
contents: changed.contents,
meta: changed.meta,
}),
)
.into_response();
} }
} }
} }
#[derive(Serialize, Deserialize)]
struct Status {
version: String,
max_size: usize,
}
pub fn init() -> Scope {
web::scope("/notes")
.service(one)
.service(create)
.service(delete)
}

View File

@@ -1,12 +0,0 @@
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);
}

View File

@@ -1,5 +1,39 @@
mod model; use crate::config;
mod routes; use axum::{http::StatusCode, Json};
use serde::Serialize;
pub use model::*; #[derive(Serialize)]
pub use routes::*; 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,
// 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,
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))
}

View File

@@ -1,17 +0,0 @@
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,
// Theme
pub theme_image: String,
pub theme_text: String,
pub theme_page_title: String,
pub theme_favicon: String,
}

View File

@@ -1,23 +0,0 @@
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,
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)
}

View File

@@ -36,7 +36,7 @@ pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
match note.expiration { match note.expiration {
Some(e) => { Some(e) => {
let seconds = e - now(); let seconds = e - now();
conn.expire(id, seconds as usize) conn.expire(id, seconds as i64)
.map_err(|_| "Unable to set expiration on notion")? .map_err(|_| "Unable to set expiration on notion")?
} }
None => {} None => {}

15
packages/cli/build.js Normal file
View File

@@ -0,0 +1,15 @@
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,
})

View File

@@ -1,6 +1,6 @@
{ {
"name": "cryptgeon", "name": "cryptgeon",
"version": "2.5.1", "version": "2.8.2",
"homepage": "https://github.com/cupcakearmy/cryptgeon", "homepage": "https://github.com/cupcakearmy/cryptgeon",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -9,7 +9,11 @@
}, },
"type": "module", "type": "module",
"exports": { "exports": {
".": "./dist/index.mjs" ".": "./dist/index.js",
"./shared": {
"import": "./dist/shared/shared.js",
"types": "./dist/shared/shared.d.ts"
}
}, },
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"bin": { "bin": {
@@ -20,22 +24,21 @@
], ],
"scripts": { "scripts": {
"bin": "run-s build package", "bin": "run-s build package",
"build": "rm -rf dist && tsc && ./scripts/build.js", "build": "tsc && node build.js",
"dev": "./scripts/build.js --watch", "dev": "node build.js --watch",
"prepublishOnly": "run-s build" "prepublishOnly": "run-s build"
}, },
"devDependencies": { "devDependencies": {
"@commander-js/extra-typings": "^12.0.1", "@commander-js/extra-typings": "^12.1.0",
"@cryptgeon/shared": "workspace:*",
"@types/inquirer": "^9.0.7", "@types/inquirer": "^9.0.7",
"@types/mime": "^3.0.4", "@types/mime": "^4.0.0",
"@types/node": "^20.11.24", "@types/node": "^20.11.24",
"commander": "^12.0.0", "commander": "^12.1.0",
"esbuild": "^0.20.1",
"inquirer": "^9.2.15", "inquirer": "^9.2.15",
"mime": "^4.0.1", "mime": "^4.0.1",
"occulto": "^2.0.3", "occulto": "^2.0.6",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"tsup": "^8.2.4",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"engines": { "engines": {

View File

@@ -1,34 +0,0 @@
#!/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)
}

View File

@@ -1,14 +1,15 @@
import { Adapters, get, info, setBase } from '@cryptgeon/shared'
import inquirer from 'inquirer' import inquirer from 'inquirer'
import { access, constants, writeFile } from 'node:fs/promises' import { access, constants, writeFile } from 'node:fs/promises'
import { basename, resolve } from 'node:path' import { basename, resolve } from 'node:path'
import { AES, Hex } from 'occulto' import { AES, Hex } from 'occulto'
import pretty from 'pretty-bytes' 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) { export async function download(url: URL, all: boolean, suggestedPassword?: string) {
setBase(url.origin) API.setOptions({ server: url.origin })
const id = url.pathname.split('/')[2] const id = url.pathname.split('/')[2]
const preview = await info(id).catch(() => { const preview = await API.info(id).catch(() => {
throw new Error('Note does not exist or is expired') throw new Error('Note does not exist or is expired')
}) })
@@ -33,7 +34,7 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin
} }
const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password) const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password)
const note = await get(id) const note = await API.get(id)
const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password') const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password')
switch (note.meta.type) { switch (note.meta.type) {

View File

@@ -1,9 +1,10 @@
import { readFile, stat } from 'node:fs/promises' import { readFile, stat } from 'node:fs/promises'
import { basename } from 'node:path' import { basename } from 'node:path'
import { Adapters, BASE, create, FileDTO, Note, NoteMeta } from '@cryptgeon/shared'
import mime from 'mime' import mime from 'mime'
import { AES, Hex } from 'occulto' 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 } export type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
@@ -38,8 +39,8 @@ export async function upload(input: string | string[], options: UploadOptions):
// Create the actual note and upload it. // Create the actual note and upload it.
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } } const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
const result = await create(note) const result = await API.create(note)
let url = `${BASE}/note/${result.id}` let url = `${API.getOptions().server}/note/${result.id}`
if (!derived) url += `#${Hex.encode(key)}` if (!derived) url += `#${Hex.encode(key)}`
return url return url
} }

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env node #!/usr/bin/env node
import { Argument, Option, program } from '@commander-js/extra-typings' import { Argument, Option, program } from '@commander-js/extra-typings'
import { setBase, status } from '@cryptgeon/shared'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { download } from './download.js' import { download } from './actions/download.js'
import { parseFile, parseNumber } from './parsers.js' import { upload } from './actions/upload.js'
import { getStdin } from './stdin.js' import { API } from './shared/api.js'
import { upload } from './upload.js' import { parseFile, parseNumber } from './utils/parsers.js'
import { checkConstrains, exit } from './utils.js' import { getStdin } from './utils/stdin.js'
import { checkConstrains, exit } from './utils/utils.js'
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org' const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer) 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') .description('show information about the server')
.addOption(server) .addOption(server)
.action(async (options) => { .action(async (options) => {
setBase(options.server) API.setOptions({ server: options.server })
const response = await status() const response = await API.status()
const formatted = { const formatted = {
...response, ...response,
max_size: prettyBytes(response.max_size), max_size: prettyBytes(response.max_size),
@@ -54,7 +54,7 @@ send
.addOption(minutes) .addOption(minutes)
.addOption(password) .addOption(password)
.action(async (files, options) => { .action(async (files, options) => {
setBase(options.server!) API.setOptions({ server: options.server })
await checkConstrains(options) await checkConstrains(options)
options.password ||= await getStdin() options.password ||= await getStdin()
try { try {
@@ -72,7 +72,7 @@ send
.addOption(minutes) .addOption(minutes)
.addOption(password) .addOption(password)
.action(async (text, options) => { .action(async (text, options) => {
setBase(options.server!) API.setOptions({ server: options.server })
await checkConstrains(options) await checkConstrains(options)
options.password ||= await getStdin() options.password ||= await getStdin()
try { try {

View File

@@ -1,4 +1,4 @@
export * from '@cryptgeon/shared' export * from './actions/download.js'
export * from './download.js' export * from './actions/upload.js'
export * from './upload.js' export * from './shared/adapters.js'
export * from './utils.js' export * from './shared/api.js'

View File

@@ -23,6 +23,10 @@ export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
contents: string contents: string
} }
type ClientOptions = {
server: string
}
type CallOptions = { type CallOptions = {
url: string url: string
method: string method: string
@@ -31,14 +35,21 @@ type CallOptions = {
export class PayloadToLargeError extends Error {} export class PayloadToLargeError extends Error {}
export let BASE = '' export let client: ClientOptions = {
server: '',
export function setBase(url: string) {
BASE = url
} }
export async function call(options: CallOptions) { function setOptions(options: Partial<ClientOptions>) {
const response = await fetch(BASE + '/api/' + options.url, { 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, {
method: options.method, method: options.method,
body: options.body === undefined ? undefined : JSON.stringify(options.body), body: options.body === undefined ? undefined : JSON.stringify(options.body),
mode: 'cors', mode: 'cors',
@@ -54,7 +65,7 @@ export async function call(options: CallOptions) {
return response.json() return response.json()
} }
export async function create(note: Note) { async function create(note: Note) {
const { meta, ...rest } = note const { meta, ...rest } = note
const body: NoteCreate = { const body: NoteCreate = {
...rest, ...rest,
@@ -68,7 +79,7 @@ export async function create(note: Note) {
return data as { id: string } return data as { id: string }
} }
export async function get(id: string): Promise<NotePublic> { async function get(id: string): Promise<NotePublic> {
const data = await call({ const data = await call({
url: `notes/${id}`, url: `notes/${id}`,
method: 'delete', method: 'delete',
@@ -82,7 +93,7 @@ export async function get(id: string): Promise<NotePublic> {
return note return note
} }
export async function info(id: string): Promise<NoteInfo> { async function info(id: string): Promise<NoteInfo> {
const data = await call({ const data = await call({
url: `notes/${id}`, url: `notes/${id}`,
method: 'get', method: 'get',
@@ -101,16 +112,27 @@ export type Status = {
max_views: number max_views: number
max_expiration: number max_expiration: number
allow_advanced: boolean allow_advanced: boolean
allow_files: boolean
theme_image: string theme_image: string
theme_text: string theme_text: string
theme_favicon: string theme_favicon: string
theme_page_title: string theme_page_title: string
theme_new_note_notice: boolean
} }
export async function status() { async function status() {
const data = await call({ const data = await call({
url: 'status/', url: 'status/',
method: 'get', method: 'get',
}) })
return data as Status return data as Status
} }
export const API = {
setOptions,
getOptions,
create,
get,
info,
status,
}

View File

@@ -21,7 +21,7 @@ export function parseURL(value: string, _: URL): URL {
} }
export function parseNumber(value: string, _: number): number { export function parseNumber(value: string, _: number): number {
const n = parseInt(value, 10) const n = Number.parseInt(value, 10)
if (isNaN(n)) throw new InvalidOptionArgumentError('invalid number') if (Number.isNaN(n)) throw new InvalidOptionArgumentError('invalid number')
return n return n
} }

View File

@@ -18,6 +18,7 @@ export function getStdin(timeout: number = 10): Promise<string> {
resolve('') resolve('')
}, timeout) }, timeout)
process.stdin.on('error', reject)
process.stdin.on('data', dataHandler) process.stdin.on('data', dataHandler)
process.stdin.on('end', endHandler) process.stdin.on('end', endHandler)
}) })

View File

@@ -1,5 +1,5 @@
import { status } from '@cryptgeon/shared'
import { exit as exitNode } from 'node:process' import { exit as exitNode } from 'node:process'
import { API } from '../shared/api.js'
export function exit(message: string) { export function exit(message: string) {
console.error(message) 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) exit('cannot set view and minutes constrains simultaneously')
if (!views && !minutes) constrains.views = 1 if (!views && !minutes) constrains.views = 1
const response = await status() const response = await API.status()
if (views && views > response.max_views) if (views && views > response.max_views)
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`) exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
if (minutes && minutes > response.max_expiration) if (minutes && minutes > response.max_expiration)

View File

@@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"target": "es2022", "target": "es2022",
"module": "es2022", "module": "es2022",
"moduleResolution": "node", "moduleResolution": "Bundler",
"declaration": true, "declaration": true,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"strict": true, "strict": true,

View File

@@ -25,7 +25,7 @@
"new_note": "new note", "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.)", "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": { "errors": {
"note_to_big": "could not create note. note is to big", "note_to_big": "could not create note. note is too big",
"note_error": "could not create note. please try again.", "note_error": "could not create note. please try again.",
"max": "max: {n}", "max": "max: {n}",
"empty_content": "note is empty." "empty_content": "note is empty."
@@ -44,7 +44,7 @@
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.", "decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
"unsupported_type": "unsupported note type." "unsupported_type": "unsupported note type."
}, },
"explanation": "click below to show and delete the note if the counter has reached it's limit", "explanation": "click below to show and delete the note if the counter has reached its limit",
"show_note": "show note", "show_note": "show note",
"warning_will_not_see_again": "you will <b>not</b> get the chance to see the note again.", "warning_will_not_see_again": "you will <b>not</b> get the chance to see the note again.",
"download_all": "download all", "download_all": "download all",

View File

@@ -12,20 +12,20 @@
"share_link": "partager le lien", "share_link": "partager le lien",
"copy_clipboard": "copier dans le presse-papiers", "copy_clipboard": "copier dans le presse-papiers",
"copied_to_clipboard": "copié dans le presse-papiers", "copied_to_clipboard": "copié dans le presse-papiers",
"encrypting": "cryptage", "encrypting": "chiffrer",
"decrypting": "déchiffrer", "decrypting": "déchiffrer",
"uploading": "téléchargement", "uploading": "téléversement",
"downloading": "téléchargement", "downloading": "téléchargement",
"qr_code": "code qr", "qr_code": "code qr",
"password": "mot de passe" "password": "mot de passe"
}, },
"home": { "home": {
"intro": "Envoyez facilement des notes ou des fichiers <i>entièrement cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.", "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}.", "explanation": "la note expirera et sera détruite après {type}.",
"new_note": "nouvelle note", "new_note": "nouvelle note",
"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 />(vous serez probablement bien, soyez juste averti.)", "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.)",
"errors": { "errors": {
"note_to_big": "Impossible de créer une note. La note est trop grande", "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.", "note_error": "n'a pas pu créer de note. Veuillez réessayer.",
"max": "max: {n}", "max": "max: {n}",
"empty_content": "La note est vide." "empty_content": "La note est vide."
@@ -45,9 +45,9 @@
"unsupported_type": "type de note non supporté." "unsupported_type": "type de note non supporté."
}, },
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.", "explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
"show_note": "note de présentation", "show_note": "afficher la note",
"warning_will_not_see_again": "vous <b>n'aurez pas</b> la chance de revoir la note.", "warning_will_not_see_again": "vous <b>n'aurez pas</b> la chance de revoir la note.",
"download_all": "télécharger tout", "download_all": "tout télécharger",
"links_found": "liens trouvés à lintérieur de la note :" "links_found": "liens trouvés à lintérieur de la note :"
}, },
"file_upload": { "file_upload": {

View File

@@ -0,0 +1,58 @@
{
"common": {
"note": "notatka",
"file": "plik",
"advanced": "zaawansowane",
"create": "utwórz",
"loading": "ładowanie",
"mode": "tryb",
"views": "{n, plural, =0 {wyświetleń} =1 {1 wyświetlenie} other {# wyświetleń}}",
"minutes": "{n, plural, =0 {minut} =1 {1 minuta} other {# minuty}}",
"max": "maks.",
"share_link": "link udostępniania",
"copy_clipboard": "kopiuj do schowka",
"copied_to_clipboard": "skopiowano do schowka",
"encrypting": "szyfrowanie",
"decrypting": "odszyfrowywanie",
"uploading": "wysyłanie",
"downloading": "pobieranie",
"qr_code": "kod QR",
"password": "hasło"
},
"home": {
"intro": "Łatwo wysyłaj <i>w pełni zaszyfrowane</i>, bezpieczne notatki lub pliki jednym kliknięciem. Po prostu utwórz notatkę i udostępnij link.",
"explanation": "notatka wygaśnie i zostanie zniszczona po {type}.",
"new_note": "nowa notatka",
"new_note_notice": "<b>dostępność:</b><br />nie ma gwarancji, że notatka będzie przechowywana, ponieważ wszystko jest przechowywane w pamięci RAM, jeśli się zapełni, najstarsze notatki zostaną usunięte.<br />(prawdopodobnie nic się nie stanie, ale warto ostrzec.)",
"errors": {
"note_to_big": "nie można utworzyć notatki. notatka jest za duża",
"note_error": "nie można utworzyć notatki. spróbuj ponownie.",
"max": "maks .: {n}",
"empty_content": "notatka jest pusta."
},
"messages": {
"note_created": "notatka utworzona."
},
"advanced": {
"explanation": "Domyślnie dla każdej notatki używane jest bezpiecznie wygenerowane hasło. Możesz jednak wybrać własne hasło, które nie jest uwzględnione w linku.",
"custom_password": "własne hasło"
}
},
"show": {
"errors": {
"not_found": "notatka nie została znaleziona lub została już usunięta.",
"decryption_failed": "błędne hasło. nie można odszyfrować. prawdopodobnie uszkodzony link. notatka została zniszczona.",
"unsupported_type": "nieobsługiwany typ notatki."
},
"explanation": "kliknij poniżej, aby wyświetlić i usunąć notatkę, jeśli licznik osiągnie swój limit",
"show_note": "pokaż notatkę",
"warning_will_not_see_again": "<b>nie będziesz mieć</b> możliwości ponownego zobaczenia notatki.",
"download_all": "pobierz wszystko",
"links_found": "linki znalezione w notatce:"
},
"file_upload": {
"selected_files": "Wybrane pliki",
"no_files_selected": "Nie wybrano plików",
"clear": "Wyczyść"
}
}

View File

@@ -1,58 +1,58 @@
{ {
"common": { "common": {
"note": "заметка", "note": "заметка",
"file": "файл", "file": "файл",
"advanced": "расширенные", "advanced": "расширенные",
"create": "создать", "create": "создать",
"loading": "загрузка", "loading": "загрузка",
"mode": "режим", "mode": "режим",
"views": "{n, plural, =0 {просмотры} =1 {1 просмотр} other {# просмотры}}", "views": "{n, plural, =0 {просмотры} =1 {1 просмотр} other {# просмотры}}",
"minutes": "{n, plural, =0 {минут} =1 {1 минута} other {# минуты}}", "minutes": "{n, plural, =0 {минут} =1 {1 минута} other {# минуты}}",
"max": "макс", "max": "макс",
"share_link": "поделиться ссылкой", "share_link": "поделиться ссылкой",
"copy_clipboard": "скопировать в буфер обмена", "copy_clipboard": "скопировать в буфер обмена",
"copied_to_clipboard": "скопировано в буфер обмена", "copied_to_clipboard": "скопировано в буфер обмена",
"encrypting": "шифрование", "encrypting": "шифрование",
"decrypting": "расшифровка", "decrypting": "расшифровка",
"uploading": "загрузка", "uploading": "загрузка",
"downloading": "скачивание", "downloading": "скачивание",
"qr_code": "qr код", "qr_code": "qr код",
"password": "пароль" "password": "пароль"
}, },
"home": { "home": {
"intro": "Легко отправляйте <i>полностью зашифрованные</i> защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.", "intro": "Легко отправляйте <i>полностью зашифрованные</i> защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.",
"explanation": "заметка истечет и будет уничтожена после {type}.", "explanation": "заметка истечет и будет уничтожена после {type}.",
"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.)", "new_note_notice": "<b>доступность:</b><br />сохранение заметки не гарантируется, поскольку все хранится в оперативной памяти; если она заполнится, самые старые заметки будут удалены.<br />( вероятно, все будет в порядке, просто будьте осторожны.)",
"errors": { "errors": {
"note_to_big": "нельзя создать новую заметку. заметка слишком большая", "note_to_big": "нельзя создать новую заметку. заметка слишком большая",
"note_error": "нельзя создать новую заметку. пожалйста попробуйте позднее.", "note_error": "нельзя создать новую заметку. пожалйста попробуйте позднее.",
"max": "макс: {n}", "max": "макс: {n}",
"empty_content": "пустая заметка." "empty_content": "пустая заметка."
}, },
"messages": { "messages": {
"note_created": "заметка создана." "note_created": "заметка создана."
}, },
"advanced": { "advanced": {
"explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.", "explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.",
"custom_password": "пользовательский пароль" "custom_password": "пользовательский пароль"
} }
}, },
"show": { "show": {
"errors": { "errors": {
"not_found": "заметка не найдена или была удалена.", "not_found": "заметка не найдена или была удалена.",
"decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. записка уничтожена.", "decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. записка уничтожена.",
"unsupported_type": "неподдерживаемый тип заметки." "unsupported_type": "неподдерживаемый тип заметки."
}, },
"explanation": "щелкните ниже, чтобы показать и удалить примечание, если счетчик достиг предела", "explanation": "щелкните ниже, чтобы показать и удалить примечание, если счетчик достиг предела",
"show_note": "показать заметку", "show_note": "показать заметку",
"warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.", "warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.",
"download_all": "скачать всё", "download_all": "скачать всё",
"links_found": "ссылки внутри заметки:" "links_found": "ссылки внутри заметки:"
}, },
"file_upload": { "file_upload": {
"selected_files": "Выбранные файлы", "selected_files": "Выбранные файлы",
"no_files_selected": "Файлы не выбраны", "no_files_selected": "Файлы не выбраны",
"clear": "Сброс" "clear": "Сброс"
} }
} }

View File

@@ -17,7 +17,6 @@
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.5.2", "@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/file-saver": "^2.0.7",
"@zerodevx/svelte-toast": "^0.9.5", "@zerodevx/svelte-toast": "^0.9.5",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
@@ -26,14 +25,12 @@
"svelte-intl-precompile": "^0.12.3", "svelte-intl-precompile": "^0.12.3",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.1.4" "vite": "^5.1.7"
}, },
"dependencies": { "dependencies": {
"@cryptgeon/shared": "workspace:*", "cryptgeon": "workspace:*",
"@fontsource/fira-mono": "^5.0.8", "@fontsource/fira-mono": "^5.0.8",
"copy-to-clipboard": "^3.3.3", "occulto": "^2.0.6",
"file-saver": "^2.0.5",
"occulto": "^2.0.3",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"qrious": "^4.0.2" "qrious": "^4.0.2"
} }

View File

@@ -1,8 +1,8 @@
import { status as getStatus, type Status } from '@cryptgeon/shared' import { API, type Status } from 'cryptgeon/shared'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
export const status = writable<null | Status>(null) export const status = writable<null | Status>(null)
export async function init() { export async function init() {
status.set(await getStatus()) status.set(await API.status())
} }

View File

@@ -1,11 +1,11 @@
import { toast, type SvelteToastOptions } from '@zerodevx/svelte-toast' import { toast } from '@zerodevx/svelte-toast'
export enum NotifyType { export enum NotifyType {
Success = 'success', Success = 'success',
Error = 'error', Error = 'error',
} }
const themeMapping: Record<NotifyType, SvelteToastOptions['theme']> = { const themeMapping: Record<NotifyType, Record<string, string>> = {
[NotifyType.Success]: { [NotifyType.Success]: {
'--toastBackground': 'var(--ui-clr-primary)', '--toastBackground': 'var(--ui-clr-primary)',
'--toastBarBackground': 'var(--ui-clr-primary-alt)', '--toastBarBackground': 'var(--ui-clr-primary-alt)',
@@ -17,7 +17,7 @@ const themeMapping: Record<NotifyType, SvelteToastOptions['theme']> = {
} }
function notifyFN(message: string, type: NotifyType = NotifyType.Success) { function notifyFN(message: string, type: NotifyType = NotifyType.Success) {
const options: SvelteToastOptions = { const options = {
duration: 5_000, duration: 5_000,
theme: { theme: {
...themeMapping[type], ...themeMapping[type],

View File

@@ -4,7 +4,7 @@
import { status } from '$lib/stores/status' import { status } from '$lib/stores/status'
import Switch from '$lib/ui/Switch.svelte' import Switch from '$lib/ui/Switch.svelte'
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import type { Note } from '@cryptgeon/shared' import type { Note } from 'cryptgeon/shared'
export let note: Note export let note: Note
export let timeExpiration = false export let timeExpiration = false

View File

@@ -3,7 +3,7 @@
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import MaxSize from '$lib/ui/MaxSize.svelte' import MaxSize from '$lib/ui/MaxSize.svelte'
import type { FileDTO } from '@cryptgeon/shared' import type { FileDTO } from 'cryptgeon/shared'
export let label: string = '' export let label: string = ''
export let files: FileDTO[] = [] export let files: FileDTO[] = []

View File

@@ -18,19 +18,19 @@
export let icon: keyof typeof map export let icon: keyof typeof map
</script> </script>
<div on:click {...$$restProps}> <button type="button" on:click {...$$restProps}>
{#if map[icon]} {#if map[icon]}
<svelte:component this={map[icon]} /> <svelte:component this={map[icon]} />
{/if} {/if}
</div> </button>
<style> <style>
div { button {
display: inline-block; display: inline-block;
contain: strict; contain: strict;
box-sizing: content-box; box-sizing: content-box;
} }
div > :global(svg) { button > :global(svg) {
display: block; display: block;
fill: currentColor; fill: currentColor;
} }

View File

@@ -7,7 +7,7 @@
<script lang="ts"> <script lang="ts">
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import { status } from '$lib/stores/status'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import Canvas from './Canvas.svelte' import Canvas from './Canvas.svelte'
@@ -35,9 +35,11 @@
<Canvas value={url} /> <Canvas value={url} />
</div> </div>
<p> {#if $status?.theme_new_note_notice}
{@html $t('home.new_note_notice')} <p>
</p> {@html $t('home.new_note_notice')}
</p>
{/if}
<br /> <br />
<Button on:click={reset}>{$t('home.new_note')}</Button> <Button on:click={reset}>{$t('home.new_note')}</Button>

View File

@@ -1,15 +1,26 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any } 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>
<script lang="ts"> <script lang="ts">
import { saveAs } from 'file-saver'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import { copy } from '$lib/utils' import { copy } from '$lib/utils'
import type { FileDTO, NotePublic } from '@cryptgeon/shared' import type { FileDTO, NotePublic } from 'cryptgeon/shared'
export let note: DecryptedNote export let note: DecryptedNote

View File

@@ -39,13 +39,23 @@
<input bind:value {...$$restProps} class:valid={valid === true} /> <input bind:value {...$$restProps} class:valid={valid === true} />
<div class="icons"> <div class="icons">
{#if isPassword} {#if isPassword}
<Icon class="icon" icon={hidden ? 'eye' : 'eye-off'} on:click={toggle} /> <Icon
disabled={$$restProps.disabled}
class="icon"
icon={hidden ? 'eye' : 'eye-off'}
on:click={toggle}
/>
{/if} {/if}
{#if random} {#if random}
<Icon class="icon" icon="dice" on:click={randomFN} /> <Icon disabled={$$restProps.disabled} class="icon" icon="dice" on:click={randomFN} />
{/if} {/if}
{#if copy} {#if copy}
<Icon class="icon" icon="copy" on:click={() => copyFN(value.toString())} /> <Icon
disabled={$$restProps.disabled}
class="icon"
icon="copy"
on:click={() => copyFN(value.toString())}
/>
{/if} {/if}
</div> </div>
</label> </label>

View File

@@ -1,11 +1,10 @@
import copyToClipboard from 'copy-to-clipboard'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { notify } from './toast' import { notify } from './toast'
export function copy(value: string) { export function copy(value: string) {
copyToClipboard(value) window.navigator.clipboard.writeText(value)
const msg = get(t)('common.copied_to_clipboard') const msg = get(t)('common.copied_to_clipboard')
notify.success(msg) notify.success(msg)
} }

View File

@@ -13,8 +13,7 @@
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte' import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
import Switch from '$lib/ui/Switch.svelte' import Switch from '$lib/ui/Switch.svelte'
import TextArea from '$lib/ui/TextArea.svelte' import TextArea from '$lib/ui/TextArea.svelte'
import type { FileDTO, Note } from '@cryptgeon/shared' import { Adapters, API, PayloadToLargeError, type FileDTO, type Note } from 'cryptgeon/shared'
import { Adapters, PayloadToLargeError, create } from '@cryptgeon/shared'
let note: Note = { let note: Note = {
contents: '', contents: '',
@@ -77,7 +76,7 @@
else data.views = parseInt(note.views as any) else data.views = parseInt(note.views as any)
loading = $t('common.uploading') loading = $t('common.uploading')
const response = await create(data) const response = await API.create(data)
result = { result = {
id: response.id, id: response.id,
password: customPassword ? undefined : Hex.encode(key), password: customPassword ? undefined : Hex.encode(key),
@@ -118,12 +117,14 @@
{/if} {/if}
<div class="bottom"> <div class="bottom">
<Switch {#if $status?.allow_files}
data-testid="switch-file" <Switch
class="file" data-testid="switch-file"
label={$t('common.file')} class="file"
bind:value={isFile} label={$t('common.file')}
/> bind:value={isFile}
/>
{/if}
{#if $status?.allow_advanced} {#if $status?.allow_advanced}
<Switch <Switch
data-testid="switch-advanced" data-testid="switch-advanced"

View File

@@ -1,9 +1,13 @@
<script lang="ts"> <script lang="ts">
import { status } from '$lib/stores/status' import { status } from '$lib/stores/status'
function reset() {
window.location.reload()
}
</script> </script>
<header> <header>
<a href="/"> <a on:click={reset} href="/">
{#if $status?.theme_image} {#if $status?.theme_image}
<img alt="logo" src={$status.theme_image} /> <img alt="logo" src={$status.theme_image} />
{:else} {:else}

View File

@@ -40,8 +40,8 @@
<br /> <br />
you are welcomed to check & audit the you are welcomed to check & audit the
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer"> <a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
source code source code</a
</a>. >.
</span> </span>
</AboutParagraph> </AboutParagraph>

View File

@@ -7,7 +7,7 @@
import Loader from '$lib/ui/Loader.svelte' import Loader from '$lib/ui/Loader.svelte'
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte' import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import { Adapters, get, info, type NoteMeta } from '@cryptgeon/shared' import { Adapters, API, type NoteMeta } from 'cryptgeon/shared'
import type { PageData } from './$types' import type { PageData } from './$types'
export let data: PageData export let data: PageData
@@ -28,7 +28,7 @@
try { try {
loading = $t('common.loading') loading = $t('common.loading')
password = window.location.hash.slice(1) password = window.location.hash.slice(1)
const note = await info(id) const note = await API.info(id)
meta = note.meta meta = note.meta
exists = true exists = true
} catch { } catch {
@@ -51,7 +51,7 @@
// Load note // Load note
error = null error = null
loading = $t('common.downloading') loading = $t('common.downloading')
const data = await get(id) const data = await API.get(id)
loading = $t('common.decrypting') loading = $t('common.decrypting')
const derived = meta?.derivation && (await AES.derive(password!, meta.derivation)) const derived = meta?.derivation && (await AES.derive(password!, meta.derivation))
const key = derived ? derived[0] : Hex.decode(password!) const key = derived ? derived[0] : Hex.decode(password!)

View File

@@ -12,5 +12,5 @@ const server = http.createServer(function (req, res) {
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001' const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 }) proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 })
}) })
server.listen(1234) server.listen(3000)
console.log('Proxy on http://localhost:1234') console.log('Proxy on http://localhost:3000')

View File

@@ -1,22 +0,0 @@
{
"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"
}
}

View File

@@ -1,12 +0,0 @@
{
"compilerOptions": {
"incremental": true,
"composite": true,
"target": "es2022",
"module": "es2022",
"rootDir": "./src",
"moduleResolution": "node",
"outDir": "./dist",
"strict": true
}
}

View File

@@ -3,17 +3,19 @@ import { devices, type PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
use: { use: {
video: 'retain-on-failure', video: 'retain-on-failure',
baseURL: 'http://localhost:1234', baseURL: 'http://localhost:3000',
actionTimeout: 60_000, actionTimeout: 30_000,
}, },
outputDir: './test-results', outputDir: './test-results',
testDir: './test', testDir: './test',
timeout: 60_000, timeout: 30_000,
fullyParallel: true,
retries: 2,
webServer: { webServer: {
command: 'docker compose -f docker-compose.dev.yaml up', command: 'pnpm run docker:up',
port: 1234, port: 3000,
reuseExistingServer: true, reuseExistingServer: true,
}, },
@@ -21,10 +23,6 @@ const config: PlaywrightTestConfig = {
{ name: 'chrome', use: { ...devices['Desktop Chrome'] } }, { name: 'chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'safari', use: { ...devices['Desktop Safari'] } }, { 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/] },
], ],
} }

4781
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { basename } from 'node:path' import { basename } from 'node:path'
import { Files, getFileChecksum, rm, tmpFile } from '../../files' import { rm } from 'node:fs/promises'
import { Files, getFileChecksum, tmpFile } from '../../files'
import { CLI, getLinkFromCLI } from '../../utils' import { CLI, getLinkFromCLI } from '../../utils'
test.describe('file @cli', () => { test.describe('file @cli', () => {
test('simple', async ({ page }) => { test('simple', async () => {
const file = await tmpFile(Files.Image) const file = await tmpFile(Files.Image)
const checksum = await getFileChecksum(file) const checksum = await getFileChecksum(file)
const note = await CLI('send', 'file', file) const note = await CLI('send', 'file', file)
@@ -17,7 +18,7 @@ test.describe('file @cli', () => {
test.expect(checksum).toBe(c) test.expect(checksum).toBe(c)
}) })
test('simple with password', async ({ page }) => { test('simple with password', async () => {
const file = await tmpFile(Files.Image) const file = await tmpFile(Files.Image)
const password = 'password' const password = 'password'
const checksum = await getFileChecksum(file) const checksum = await getFileChecksum(file)

View File

@@ -2,7 +2,7 @@ import { test } from '@playwright/test'
import { CLI, getLinkFromCLI } from '../../utils' import { CLI, getLinkFromCLI } from '../../utils'
test.describe('text @cli', () => { test.describe('text @cli', () => {
test('simple', async ({ page }) => { test('simple', async () => {
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 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 note = await CLI('send', 'text', text)
const link = getLinkFromCLI(note.stdout) const link = getLinkFromCLI(note.stdout)
@@ -11,7 +11,7 @@ test.describe('text @cli', () => {
test.expect(retrieved.stdout.trim()).toBe(text) test.expect(retrieved.stdout.trim()).toBe(text)
}) })
test('simple with password', async ({ page }) => { test('simple with password', async () => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying.` const text = `Endless prejudice endless play derive joy eternal-return selfish burying.`
const password = 'password' const password = 'password'
const note = await CLI('send', 'text', text, '--password', password) const note = await CLI('send', 'text', text, '--password', password)

View File

@@ -1,7 +1,8 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { CLI, checkLinkForDownload, checkLinkForText, createNote, getLinkFromCLI } from '../../utils' import { rm } from 'node:fs/promises'
import { Files, getFileChecksum, rm, tmpFile } from '../../files' import { basename } from 'node:path'
import { basename } from 'path' import { Files, getFileChecksum, tmpFile } from '../../files'
import { CLI, checkLinkForDownload, createNoteSuccessfully, 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 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' const password = 'password'
@@ -30,7 +31,7 @@ test.describe('text @cross', () => {
test('web to cli', async ({ page }) => { test('web to cli', async ({ page }) => {
const files = [Files.Image] const files = [Files.Image]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files }) const link = await createNoteSuccessfully(page, { files })
const filename = basename(files[0]) const filename = basename(files[0])
await CLI('open', link, '--all') await CLI('open', link, '--all')
@@ -42,7 +43,7 @@ test.describe('text @cross', () => {
test('web to cli with password', async ({ page }) => { test('web to cli with password', async ({ page }) => {
const files = [Files.Image] const files = [Files.Image]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files, password }) const link = await createNoteSuccessfully(page, { files, password })
const filename = basename(files[0]) const filename = basename(files[0])
await CLI('open', link, '--all', '--password', password) await CLI('open', link, '--all', '--password', password)

View File

@@ -1,5 +1,5 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { CLI, checkLinkForText, createNote, getLinkFromCLI } from '../../utils' import { CLI, checkLinkForText, createNoteSuccessfully, 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 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' const password = 'password'
@@ -13,7 +13,7 @@ test.describe('text @cross', () => {
}) })
test('web to cli', async ({ page }) => { test('web to cli', async ({ page }) => {
const link = await createNote(page, { text }) const link = await createNoteSuccessfully(page, { text })
const retrieved = await CLI('open', link) const retrieved = await CLI('open', link)
test.expect(retrieved.stdout.trim()).toBe(text) test.expect(retrieved.stdout.trim()).toBe(text)
}) })
@@ -25,7 +25,7 @@ test.describe('text @cross', () => {
}) })
test('web to cli with password', async ({ page }) => { test('web to cli with password', async ({ page }) => {
const link = await createNote(page, { text, password }) const link = await createNoteSuccessfully(page, { text, password })
const retrieved = await CLI('open', link, '--password', password) const retrieved = await CLI('open', link, '--password', password)
test.expect(retrieved.stdout.trim()).toBe(text) test.expect(retrieved.stdout.trim()).toBe(text)
}) })

View File

@@ -1,10 +1,5 @@
import { createHash } from 'crypto' import { createHash } from 'node:crypto'
import { cp as cpFN, rm as rmFN } from 'fs' import { cp, readFile } from 'node:fs/promises'
import { readFile } from 'fs/promises'
import { promisify } from 'util'
export const cp = promisify(cpFN)
export const rm = promisify(rmFN)
export const Files = { export const Files = {
PDF: 'test/assets/AES.pdf', PDF: 'test/assets/AES.pdf',

View File

@@ -6,17 +6,22 @@ import { getFileChecksum } from './files'
const exec = promisify(execFile) const exec = promisify(execFile)
type CreatePage = { type CreatePage = {
text?: string
files?: string[]
views?: number views?: number
expiration?: number expiration?: number
error?: string error?: string
password?: string password?: string
} } & (
export async function createNote(page: Page, options: CreatePage): Promise<string> { | {
text: string
}
| {
files: string[]
}
)
async function createNote(page: Page, options: CreatePage): Promise<void> {
await page.goto('/') await page.goto('/')
if (options.text) { if ('text' in options) {
await page.getByTestId('text-field').fill(options.text) await page.getByTestId('text-field').fill(options.text)
} else if (options.files) { } else if (options.files) {
await page.getByTestId('switch-file').click() await page.getByTestId('switch-file').click()
@@ -31,7 +36,8 @@ export async function createNote(page: Page, options: CreatePage): Promise<strin
if (options.views || options.expiration || options.password) await page.getByTestId('switch-advanced').click() if (options.views || options.expiration || options.password) await page.getByTestId('switch-advanced').click()
if (options.views) { if (options.views) {
await page.getByTestId('field-views').fill(options.views.toString()) await page.getByTestId('field-views').fill(options.views.toString())
} else if (options.expiration) { }
if (options.expiration) {
await page.getByTestId('switch-advanced-toggle').click() await page.getByTestId('switch-advanced-toggle').click()
await page.getByTestId('field-expiration').fill(options.expiration.toString()) await page.getByTestId('field-expiration').fill(options.expiration.toString())
} }
@@ -41,15 +47,18 @@ export async function createNote(page: Page, options: CreatePage): Promise<strin
} }
await page.locator('button:has-text("create")').click() await page.locator('button:has-text("create")').click()
}
if (options.error) { export async function createNoteSuccessfully(page: Page, options: CreatePage): Promise<string> {
await expect(page.locator('.error-text')).toContainText(options.error, { timeout: 60_000 }) await createNote(page, options)
}
// Return share link
return await page.getByTestId('share-link').inputValue() 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 = { type CheckLinkBase = {
link: string link: string
text: string text: string
@@ -69,7 +78,7 @@ export async function checkLinkForDownload(page: Page, options: CheckLinkBase &
const path = await download.path() const path = await download.path()
if (!path) throw new Error('Download failed') if (!path) throw new Error('Download failed')
const cs = await getFileChecksum(path) const cs = await getFileChecksum(path)
await expect(cs).toBe(options.checksum) expect(cs).toBe(options.checksum)
} }
export async function checkLinkForText(page: Page, options: CheckLinkBase) { export async function checkLinkForText(page: Page, options: CheckLinkBase) {
@@ -78,7 +87,7 @@ export async function checkLinkForText(page: Page, options: CheckLinkBase) {
if (options.password) await page.getByTestId('show-note-password').fill(options.password) if (options.password) await page.getByTestId('show-note-password').fill(options.password)
await page.getByTestId('show-note-button').click() await page.getByTestId('show-note-button').click()
const text = await page.getByTestId('result').locator('.note').innerText() const text = await page.getByTestId('result').locator('.note').innerText()
await expect(text).toContain(options.text) expect(text).toContain(options.text)
} }
export async function checkLinkDoesNotExist(page: Page, link: string) { export async function checkLinkDoesNotExist(page: Page, link: string) {
@@ -91,7 +100,7 @@ export async function CLI(...args: string[]) {
return await exec('./packages/cli/dist/cli.cjs', args, { return await exec('./packages/cli/dist/cli.cjs', args, {
env: { env: {
...process.env, ...process.env,
CRYPTGEON_SERVER: 'http://localhost:1234', CRYPTGEON_SERVER: 'http://localhost:3000',
}, },
}) })
} }

View File

@@ -1,12 +1,12 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { Files, getFileChecksum } from '../../files' import { Files, getFileChecksum } from '../../files'
import { checkLinkForDownload, createNote } from '../../utils' import { checkLinkForDownload, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('multiple', async ({ page }) => { test('multiple', async ({ page }) => {
const files = [Files.PDF, Files.Image] const files = [Files.PDF, Files.Image]
const checksums = await Promise.all(files.map(getFileChecksum)) const checksums = await Promise.all(files.map(getFileChecksum))
const link = await createNote(page, { files, views: 2 }) const link = await createNoteSuccessfully(page, { files, views: 2 })
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum: checksums[1] }) await checkLinkForDownload(page, { link, text: 'image.jpg', checksum: checksums[1] })
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum: checksums[0] }) await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum: checksums[0] })
}) })

View File

@@ -1,11 +1,11 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { Files, getFileChecksum } from '../../files' import { Files, getFileChecksum } from '../../files'
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote } from '../../utils' import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('simple pdf', async ({ page }) => { test('simple pdf', async ({ page }) => {
const files = [Files.PDF] const files = [Files.PDF]
const link = await createNote(page, { files }) const link = await createNoteSuccessfully(page, { files })
await checkLinkForText(page, { link, text: 'AES.pdf' }) await checkLinkForText(page, { link, text: 'AES.pdf' })
await checkLinkDoesNotExist(page, link) await checkLinkDoesNotExist(page, link)
}) })
@@ -13,21 +13,21 @@ test.describe('@web', () => {
test('pdf content', async ({ page }) => { test('pdf content', async ({ page }) => {
const files = [Files.PDF] const files = [Files.PDF]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files }) const link = await createNoteSuccessfully(page, { files })
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum }) await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum })
}) })
test('image content', async ({ page }) => { test('image content', async ({ page }) => {
const files = [Files.Image] const files = [Files.Image]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files }) const link = await createNoteSuccessfully(page, { files })
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum }) await checkLinkForDownload(page, { link, text: 'image.jpg', checksum })
}) })
test('simple pdf with password', async ({ page }) => { test('simple pdf with password', async ({ page }) => {
const files = [Files.PDF] const files = [Files.PDF]
const password = 'password' const password = 'password'
const link = await createNote(page, { files, password }) const link = await createNoteSuccessfully(page, { files, password })
await checkLinkForText(page, { link, text: 'AES.pdf', password }) await checkLinkForText(page, { link, text: 'AES.pdf', password })
await checkLinkDoesNotExist(page, link) await checkLinkDoesNotExist(page, link)
}) })

View File

@@ -1,10 +1,10 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { createNote } from '../../utils'
import { Files } from '../../files' import { Files } from '../../files'
import { createNoteError } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test.skip('to big zip', async ({ page }) => { test.skip('to big zip', async ({ page }) => {
const files = [Files.Zip] const files = [Files.Zip]
const link = await createNote(page, { files, error: 'note is to big' }) await createNoteError(page, { files }, 'could not create note. note is too big')
}) })
}) })

View File

@@ -1,13 +1,14 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils' import { checkLinkDoesNotExist, checkLinkForText, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('1 minute', async ({ page }) => { test('1 minute', async ({ page, browserName }) => {
test.skip(browserName === 'webkit')
const text = `Virtues value ascetic revaluation sea dead strong burying.` const text = `Virtues value ascetic revaluation sea dead strong burying.`
const minutes = 1 const minutes = 1
const timeout = minutes * 60_000 const timeout = minutes * 60_000
test.setTimeout(timeout * 2) test.setTimeout(timeout * 2)
const link = await createNote(page, { text, expiration: minutes }) const link = await createNoteSuccessfully(page, { text, expiration: minutes })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await page.waitForTimeout(timeout) await page.waitForTimeout(timeout)

View File

@@ -1,17 +1,17 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { checkLinkForText, createNote } from '../../utils' import { checkLinkForText, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('simple', async ({ page }) => { 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 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 createNote(page, { text }) const link = await createNoteSuccessfully(page, { text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
}) })
test('simple with password', async ({ page }) => { test('simple with password', async ({ page }) => {
const text = 'Foo bar' const text = 'Foo bar'
const password = '123' const password = '123'
const shareLink = await createNote(page, { text, password }) const shareLink = await createNoteSuccessfully(page, { text, password })
await checkLinkForText(page, { link: shareLink, text, password }) await checkLinkForText(page, { link: shareLink, text, password })
}) })
}) })

View File

@@ -1,17 +1,17 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils' import { checkLinkDoesNotExist, checkLinkForText, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('only shown once', async ({ page }) => { 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 text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
const link = await createNote(page, { text }) const link = await createNoteSuccessfully(page, { text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await checkLinkDoesNotExist(page, link) await checkLinkDoesNotExist(page, link)
}) })
test('view 3 times', async ({ page }) => { test('view 3 times', async ({ page }) => {
const text = `Justice holiest overcome fearful strong ultimate holiest christianity.` const text = `Justice holiest overcome fearful strong ultimate holiest christianity.`
const link = await createNote(page, { text, views: 3 }) const link = await createNoteSuccessfully(page, { text, views: 3 })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })