mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2025-09-04 00:20:39 +00:00
Compare commits
78 Commits
Author | SHA1 | Date | |
---|---|---|---|
7c6ba654f6 | |||
cb20224317 | |||
085b1c20df | |||
4b1f939281 | |||
7b919f2a53 | |||
868b49c1c3 | |||
6b5eea34a1 | |||
d70bee14af | |||
4960260076 | |||
3247a0cfca | |||
9527a499ed | |||
b5590bb5ef | |||
7691dc80f8 | |||
f8d8fa05b0 | |||
7aae690850 | |||
e15d9eb537 | |||
7fe70a6f3b | |||
d86ef79abd | |||
23446a4c74 | |||
ee92928d1f | |||
b60a079bbe | |||
ba474dff31 | |||
3cb002ee33 | |||
a10d8735dd | |||
dfa2401eea | |||
ea58d89f98 | |||
eaca1a981d | |||
199755d18e | |||
724d0709d3 | |||
bd5acca97a | |||
a0a99cd3cc | |||
c3794fa2b6 | |||
f9962c76c1 | |||
c2b81bc04d | |||
a45f6a3772 | |||
2006be0434 | |||
ca72e94e3c | |||
dbcb3870aa | |||
3ea176cc1f | |||
145f9ef18f | |||
784c54236b | |||
5648c76f78 | |||
7761c795df | |||
4aadeb492a | |||
0d9f3fe9c7 | |||
|
8ccfdd6e2e | ||
f790438104 | |||
5936f4588c | |||
|
d3c04f8fda | ||
|
f8c17487bd | ||
|
ed3e5f48a0 | ||
|
e08c9d1871 | ||
6d2150b0b6 | |||
3a68693be1 | |||
|
a612eec220 | ||
98d3b0d394 | |||
|
6aed2e2756 | ||
6bb527198a | |||
|
7050389316 | ||
0725a0c6f7 | |||
|
c8efcc04fc | ||
19bf155653 | |||
|
9a4e84db62 | ||
32cd3843a7 | |||
9b48d39c82 | |||
|
239e950f8e | ||
|
b00846ce9d | ||
|
e70f06f99f | ||
|
4781882c72 | ||
|
549dfb55db | ||
|
2653a4bccf | ||
|
7213e6c690 | ||
|
8a5f667ff6 | ||
fc3938701e | |||
23b4f81dac | |||
7c68620d8b | |||
eb76fe085a | |||
|
38540b33f2 |
13
.github/workflows/test.yaml
vendored
13
.github/workflows/test.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Node
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
@@ -27,13 +27,16 @@ jobs:
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm install
|
||||
pnpm exec playwright install --with-deps
|
||||
pnpm run test:prepare
|
||||
|
||||
- name: Run your tests
|
||||
run: pnpm test
|
||||
- uses: actions/upload-artifact@v3
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
name: playwright-report
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ target
|
||||
|
||||
# Testing
|
||||
test-results
|
||||
tmp
|
||||
|
File diff suppressed because it is too large
Load Diff
12
Dockerfile
12
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# FRONTEND
|
||||
FROM node:18-alpine as client
|
||||
FROM node:22-alpine as client
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -11,17 +11,17 @@ RUN pnpm run build
|
||||
|
||||
|
||||
# BACKEND
|
||||
FROM rust:1.76-alpine as backend
|
||||
FROM rust:1.80-alpine as backend
|
||||
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 ./
|
||||
RUN cargo build --release
|
||||
RUN RUSTFLAGS="-Ctarget-feature=-crt-static" cargo build --release
|
||||
|
||||
|
||||
# RUNNER
|
||||
FROM alpine
|
||||
FROM alpine:3.19
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache curl
|
||||
RUN apk add --no-cache curl libgcc
|
||||
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||
COPY --from=client /tmp/packages/frontend/build ./frontend
|
||||
ENV FRONTEND_PATH="./frontend"
|
||||
|
42
README.md
42
README.md
@@ -14,7 +14,7 @@
|
||||
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||
<br/><br/>
|
||||
|
||||
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
|
||||
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
|
||||
|
||||
## About?
|
||||
|
||||
@@ -62,19 +62,21 @@ of the notes even if it tried to.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `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_. |
|
||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||
| Variable | Default | Description |
|
||||
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||
| `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
|
||||
| `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. |
|
||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -139,12 +141,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)
|
||||
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
||||
|
||||
### Written Guides
|
||||
|
||||
- French by [zarevskaya](https://belginux.com/installer-cryptgeon-avec-docker/)
|
||||
- Italian by [@nicfab](https://notes.nicfab.eu/it/posts/cryptgeon/)
|
||||
- English by [@nicfab](https://notes.nicfab.eu/en/posts/cryptgeon/)
|
||||
|
||||
## Development
|
||||
|
||||
**Requirements**
|
||||
|
||||
- `pnpm`: `>=6`
|
||||
- `node`: `>=18`
|
||||
- `pnpm`: `>=9`
|
||||
- `node`: `>=22`
|
||||
- `rust`: edition `2021`
|
||||
|
||||
**Install**
|
||||
@@ -172,7 +180,7 @@ Running `pnpm run dev` in the root folder will start the following things:
|
||||
- client
|
||||
- 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)
|
||||
|
||||
|
26
README_ES.md
26
README_ES.md
@@ -59,19 +59,19 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
| Variable | Default | Descripción |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| Variable | Default | Descripción |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Tamaño máximo. Valores aceptados según la [unidad byte](https://docs.rs/byte-unit/). <br> `512 MiB` es el máximo permitido. <br> El frontend mostrará ese número, incluyendo el ~35% de sobrecarga de codificación. |
|
||||
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
||||
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
||||
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
||||
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
|
||||
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
||||
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
||||
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
|
||||
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
||||
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
||||
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
||||
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
||||
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
|
||||
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
||||
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
||||
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
|
||||
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
||||
|
||||
## Despliegue
|
||||
|
||||
@@ -169,7 +169,7 @@ Ejecutando `pnpm run dev` en la carpeta raíz iniciará lo siguiente:
|
||||
- client
|
||||
- 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)
|
||||
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||
<br/>
|
||||
|
||||
[EN](README.md) | 简体中文 | [ES](README_ES.md)
|
||||
[EN](README.md) | 简体中文 | [ES](README_ES.md)
|
||||
|
||||
## 关于本项目
|
||||
|
||||
@@ -158,7 +158,7 @@ pnpm run dev
|
||||
- 无热重载的 rust 后端
|
||||
- 可热重载的客户端
|
||||
|
||||
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234).
|
||||
你可以通过 3000 端口进入该应用,即 [localhost:3000](http://localhost:3000).
|
||||
|
||||
## 测试
|
||||
|
||||
|
@@ -11,9 +11,6 @@
|
||||
},
|
||||
{
|
||||
"path": "packages/cli"
|
||||
},
|
||||
{
|
||||
"path": "packages/shared"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
@@ -1,8 +1,6 @@
|
||||
# DEV Compose file.
|
||||
# For a production file see: README.md
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
@@ -11,12 +9,12 @@ services:
|
||||
|
||||
app:
|
||||
build: .
|
||||
env_file: .dev.env
|
||||
env_file: .env.dev
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 1234:8000
|
||||
- 3000:8000
|
||||
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/']
|
||||
|
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
@@ -16,7 +14,7 @@ services:
|
||||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:8000
|
||||
|
||||
|
||||
# Optional health checks
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
|
||||
|
12
package.json
12
package.json
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
|
||||
"dev:docker": "docker compose -f docker-compose.dev.yaml up redis",
|
||||
"dev:packages": "pnpm --parallel run dev",
|
||||
"dev": "run-p dev:*",
|
||||
"docker:up": "docker compose -f docker-compose.dev.yaml up",
|
||||
"docker:build": "docker compose -f docker-compose.dev.yaml build",
|
||||
"test": "playwright test --project chrome firefox safari",
|
||||
"test:local": "playwright test --project chrome",
|
||||
"test": "playwright test --project=chrome --project=firefox --project=safari",
|
||||
"test:local": "playwright test --project=chrome",
|
||||
"test:server": "run-s docker:up",
|
||||
"test:prepare": "run-p build docker:build",
|
||||
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/node": "^20.11.24",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@types/node": "^22.5.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"shelljs": "^0.8.5"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.4"
|
||||
"packageManager": "pnpm@9.9.0"
|
||||
}
|
||||
|
1462
packages/backend/Cargo.lock
generated
1462
packages/backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,27 @@
|
||||
[package]
|
||||
name = "cryptgeon"
|
||||
version = "2.5.0"
|
||||
version = "2.8.1"
|
||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
rust-version = "1.80"
|
||||
|
||||
[[bin]]
|
||||
name = "cryptgeon"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
actix-files = "0.6"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
# Core
|
||||
axum = "0.7.5"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
||||
tower = "0.5.0"
|
||||
tower-http = { version = "0.5.2", features = ["full"] }
|
||||
redis = { version = "0.25.2", features = ["tls-native-tls"] }
|
||||
|
||||
# Utility
|
||||
serde_json = "1"
|
||||
lazy_static = "1"
|
||||
ring = "0.16"
|
||||
bs62 = "0.1"
|
||||
byte-unit = "4"
|
||||
dotenv = "0.15"
|
||||
mime = "0.3"
|
||||
env_logger = "0.9"
|
||||
log = "0.4"
|
||||
redis = "0.23"
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"scripts": {
|
||||
"dev": "cargo watch -x 'run --bin cryptgeon'",
|
||||
"build": "cargo build --release",
|
||||
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:1234 cargo run",
|
||||
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:3000 cargo run",
|
||||
"test:prepare": "cargo build"
|
||||
}
|
||||
}
|
||||
|
@@ -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()),
|
||||
);
|
||||
}
|
@@ -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)?)
|
||||
}
|
@@ -14,26 +14,30 @@ lazy_static! {
|
||||
|
||||
// CONFIG
|
||||
lazy_static! {
|
||||
pub static ref LIMIT: usize =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as usize;
|
||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
|
||||
.unwrap_or("360".to_string()) // 6 hours in minutes
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH")
|
||||
.unwrap_or("32".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref LIMIT: usize =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as usize;
|
||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
|
||||
.unwrap_or("360".to_string()) // 6 hours in minutes
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH")
|
||||
.unwrap_or("32".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// THEME
|
||||
@@ -54,4 +58,8 @@ lazy_static! {
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
|
@@ -1,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,);
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
@@ -1,44 +1,67 @@
|
||||
use actix_web::{
|
||||
middleware::{self, Logger},
|
||||
web::{self},
|
||||
App, HttpServer,
|
||||
use axum::{
|
||||
extract::{DefaultBodyLimit, Request},
|
||||
routing::{delete, get, post},
|
||||
Router, ServiceExt,
|
||||
};
|
||||
use dotenv::dotenv;
|
||||
use log::error;
|
||||
use tower::Layer;
|
||||
use tower_http::{
|
||||
compression::CompressionLayer,
|
||||
normalize_path::NormalizePathLayer,
|
||||
services::{ServeDir, ServeFile},
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
mod api;
|
||||
mod client;
|
||||
mod config;
|
||||
mod health;
|
||||
mod note;
|
||||
mod size;
|
||||
mod status;
|
||||
mod store;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
|
||||
|
||||
if !store::can_reach_redis() {
|
||||
error!("cannot reach redis");
|
||||
println!("cannot reach redis");
|
||||
panic!("canont reach redis");
|
||||
}
|
||||
|
||||
return HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(Logger::new("\"%r\" %s %b %T"))
|
||||
.wrap(middleware::Compress::default())
|
||||
.wrap(middleware::DefaultHeaders::default())
|
||||
.configure(size::init)
|
||||
.configure(api::init)
|
||||
.configure(client::init)
|
||||
.default_service(web::to(client::index))
|
||||
})
|
||||
.bind(config::LISTEN_ADDR.to_string())?
|
||||
.run()
|
||||
.await;
|
||||
let notes_routes = Router::new()
|
||||
.route("/", post(note::create))
|
||||
.route("/:id", delete(note::delete))
|
||||
.route("/:id", get(note::preview));
|
||||
let health_routes = Router::new().route("/live", get(health::report_health));
|
||||
let status_routes = Router::new().route("/status", get(status::get_status));
|
||||
let api_routes = Router::new()
|
||||
.nest("/notes", notes_routes)
|
||||
.nest("/", health_routes)
|
||||
.nest("/", status_routes);
|
||||
|
||||
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
|
||||
let serve_dir =
|
||||
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
|
||||
let app = Router::new()
|
||||
.nest("/api", api_routes)
|
||||
.fallback_service(serve_dir)
|
||||
.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();
|
||||
}
|
||||
|
@@ -12,12 +12,12 @@ pub struct Note {
|
||||
pub expiration: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize)]
|
||||
pub struct NoteInfo {
|
||||
pub meta: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize)]
|
||||
pub struct NotePublic {
|
||||
pub meta: String,
|
||||
pub contents: String,
|
||||
|
@@ -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 std::time::SystemTime;
|
||||
|
||||
use crate::config;
|
||||
use crate::note::{generate_id, Note, NoteInfo, NotePublic};
|
||||
use crate::note::{generate_id, Note, NoteInfo};
|
||||
use crate::store;
|
||||
|
||||
use super::NotePublic;
|
||||
|
||||
pub fn now() -> u32 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
@@ -13,20 +20,18 @@ pub fn now() -> u32 {
|
||||
.as_secs() as u32
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct NotePath {
|
||||
#[derive(Deserialize)]
|
||||
pub struct OneNoteParams {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn one(path: web::Path<NotePath>) -> impl Responder {
|
||||
let p = path.into_inner();
|
||||
let note = store::get(&p.id);
|
||||
pub async fn preview(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
|
||||
let note = store::get(&id);
|
||||
|
||||
match note {
|
||||
Ok(Some(n)) => HttpResponse::Ok().json(NoteInfo { meta: n.meta }),
|
||||
Ok(None) => HttpResponse::NotFound().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Ok(Some(n)) => (StatusCode::OK, Json(NoteInfo { meta: n.meta })).into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +40,16 @@ struct CreateResponse {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[post("/")]
|
||||
async fn create(note: web::Json<Note>) -> impl Responder {
|
||||
let mut n = note.into_inner();
|
||||
pub async fn create(Json(mut n): Json<Note>) -> Response {
|
||||
// let mut n = note.into_inner();
|
||||
let id = generate_id();
|
||||
let bad_req = HttpResponse::BadRequest().finish();
|
||||
// let bad_req = HttpResponse::BadRequest().finish();
|
||||
if n.views == None && n.expiration == None {
|
||||
return bad_req;
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"At least views or expiration must be set",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if !*config::ALLOW_ADVANCED {
|
||||
n.views = Some(1);
|
||||
@@ -50,7 +58,7 @@ async fn create(note: web::Json<Note>) -> impl Responder {
|
||||
match n.views {
|
||||
Some(v) => {
|
||||
if v > *config::MAX_VIEWS || v < 1 {
|
||||
return bad_req;
|
||||
return (StatusCode::BAD_REQUEST, "Invalid views").into_response();
|
||||
}
|
||||
n.expiration = None; // views overrides expiration
|
||||
}
|
||||
@@ -58,8 +66,8 @@ async fn create(note: web::Json<Note>) -> impl Responder {
|
||||
}
|
||||
match n.expiration {
|
||||
Some(e) => {
|
||||
if e > *config::MAX_EXPIRATION {
|
||||
return bad_req;
|
||||
if e > *config::MAX_EXPIRATION || e < 1 {
|
||||
return (StatusCode::BAD_REQUEST, "Invalid expiration").into_response();
|
||||
}
|
||||
let expiration = now() + (e * 60);
|
||||
n.expiration = Some(expiration);
|
||||
@@ -67,38 +75,40 @@ async fn create(note: web::Json<Note>) -> impl Responder {
|
||||
_ => {}
|
||||
}
|
||||
match store::set(&id.clone(), &n.clone()) {
|
||||
Ok(_) => return HttpResponse::Ok().json(CreateResponse { id: id }),
|
||||
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Ok(_) => (StatusCode::OK, Json(CreateResponse { id })).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/{id}")]
|
||||
async fn delete(path: web::Path<NotePath>) -> impl Responder {
|
||||
let p = path.into_inner();
|
||||
let note = store::get(&p.id);
|
||||
pub async fn delete(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
|
||||
let note = store::get(&id);
|
||||
match note {
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Ok(None) => return HttpResponse::NotFound().finish(),
|
||||
// Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
// 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)) => {
|
||||
let mut changed = note.clone();
|
||||
if changed.views == None && changed.expiration == None {
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return (StatusCode::BAD_REQUEST).into_response();
|
||||
}
|
||||
match changed.views {
|
||||
Some(v) => {
|
||||
changed.views = Some(v - 1);
|
||||
let id = p.id.clone();
|
||||
let id = id.clone();
|
||||
if v <= 1 {
|
||||
match store::del(&id) {
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().body(e.to_string())
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
.into_response();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match store::set(&id, &changed.clone()) {
|
||||
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 {
|
||||
Some(e) => {
|
||||
if e < n {
|
||||
match store::del(&p.id.clone()) {
|
||||
Ok(_) => return HttpResponse::BadRequest().finish(),
|
||||
match store::del(&id.clone()) {
|
||||
Ok(_) => return (StatusCode::BAD_REQUEST).into_response(),
|
||||
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,
|
||||
meta: changed.meta,
|
||||
});
|
||||
|
||||
return (
|
||||
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)
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
@@ -1,5 +1,39 @@
|
||||
mod model;
|
||||
mod routes;
|
||||
use crate::config;
|
||||
use axum::{http::StatusCode, Json};
|
||||
use serde::Serialize;
|
||||
|
||||
pub use model::*;
|
||||
pub use routes::*;
|
||||
#[derive(Serialize)]
|
||||
pub struct Status {
|
||||
// General
|
||||
pub version: String,
|
||||
// Config
|
||||
pub max_size: u32,
|
||||
pub max_views: u32,
|
||||
pub max_expiration: u32,
|
||||
pub allow_advanced: bool,
|
||||
pub allow_files: bool,
|
||||
// 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))
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -36,7 +36,7 @@ pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
|
||||
match note.expiration {
|
||||
Some(e) => {
|
||||
let seconds = e - now();
|
||||
conn.expire(id, seconds as usize)
|
||||
conn.expire(id, seconds as i64)
|
||||
.map_err(|_| "Unable to set expiration on notion")?
|
||||
}
|
||||
None => {}
|
||||
|
15
packages/cli/build.js
Normal file
15
packages/cli/build.js
Normal 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,
|
||||
})
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cryptgeon",
|
||||
"version": "2.5.0",
|
||||
"version": "2.8.1",
|
||||
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -9,9 +9,10 @@
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
".": "./dist/index.js",
|
||||
"./shared": {
|
||||
"import": "./dist/shared/shared.js",
|
||||
"types": "./dist/shared/shared.d.ts"
|
||||
}
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -23,24 +24,21 @@
|
||||
],
|
||||
"scripts": {
|
||||
"bin": "run-s build package",
|
||||
"build": "tsc && ./scripts/build.js",
|
||||
"dev": "./scripts/build.js --watch",
|
||||
"package": "./scripts/package.js",
|
||||
"build": "tsc && node build.js",
|
||||
"dev": "node build.js --watch",
|
||||
"prepublishOnly": "run-s build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commander-js/extra-typings": "^12.0.1",
|
||||
"@cryptgeon/shared": "workspace:*",
|
||||
"@commander-js/extra-typings": "^12.1.0",
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/mime": "^4.0.0",
|
||||
"@types/node": "^20.11.24",
|
||||
"commander": "^12.0.0",
|
||||
"esbuild": "^0.20.1",
|
||||
"commander": "^12.1.0",
|
||||
"inquirer": "^9.2.15",
|
||||
"mime": "^4.0.1",
|
||||
"occulto": "^2.0.3",
|
||||
"pkg": "^5.8.1",
|
||||
"occulto": "^2.0.6",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"tsup": "^8.2.4",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
|
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { build, context } from 'esbuild'
|
||||
import pkg from '../package.json' assert { type: 'json' }
|
||||
|
||||
const options = {
|
||||
entryPoints: ['./src/cli.ts'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
platform: 'node',
|
||||
outfile: './dist/cli.cjs',
|
||||
define: { VERSION: `"${pkg.version}"` },
|
||||
}
|
||||
|
||||
const watch = process.argv.slice(2)[0] === '--watch'
|
||||
if (watch) {
|
||||
;(await context(options)).watch()
|
||||
} else {
|
||||
await build(options)
|
||||
|
||||
// Also build internals to expose
|
||||
await build({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
outfile: './dist/index.js',
|
||||
define: { VERSION: `"${pkg.version}"` },
|
||||
})
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { exec } from 'pkg'
|
||||
|
||||
const targets = [
|
||||
'node18-macos-arm64',
|
||||
'node18-macos-x64',
|
||||
'node18-linux-arm64',
|
||||
'node18-linux-x64',
|
||||
'node18-win-arm64',
|
||||
'node18-win-x64',
|
||||
]
|
||||
|
||||
for (const target of targets) {
|
||||
console.log(`🚀 Building ${target}`)
|
||||
await exec(['./dist/cli.cjs', '--target', target, '--output', `./bin/${target.replace('node18', 'cryptgeon')}`])
|
||||
}
|
@@ -1,14 +1,15 @@
|
||||
import { Adapters, get, info, setBase } from '@cryptgeon/shared'
|
||||
import inquirer from 'inquirer'
|
||||
import { access, constants, writeFile } from 'node:fs/promises'
|
||||
import { basename, resolve } from 'node:path'
|
||||
import { AES, Hex } from 'occulto'
|
||||
import pretty from 'pretty-bytes'
|
||||
import { Adapters } from '../shared/adapters.js'
|
||||
import { API } from '../shared/api.js'
|
||||
|
||||
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
|
||||
setBase(url.origin)
|
||||
API.setOptions({ server: url.origin })
|
||||
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')
|
||||
})
|
||||
|
||||
@@ -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 note = await get(id)
|
||||
const note = await API.get(id)
|
||||
|
||||
const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password')
|
||||
switch (note.meta.type) {
|
@@ -1,9 +1,10 @@
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { basename } from 'node:path'
|
||||
|
||||
import { Adapters, BASE, create, FileDTO, Note, NoteMeta } from '@cryptgeon/shared'
|
||||
import mime from 'mime'
|
||||
import { AES, Hex } from 'occulto'
|
||||
import { Adapters } from '../shared/adapters.js'
|
||||
import { API, FileDTO, Note, NoteMeta } from '../shared/api.js'
|
||||
|
||||
export type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
|
||||
|
||||
@@ -38,8 +39,8 @@ export async function upload(input: string | string[], options: UploadOptions):
|
||||
|
||||
// Create the actual note and upload it.
|
||||
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
|
||||
const result = await create(note)
|
||||
let url = `${BASE}/note/${result.id}`
|
||||
const result = await API.create(note)
|
||||
let url = `${API.getOptions().server}/note/${result.id}`
|
||||
if (!derived) url += `#${Hex.encode(key)}`
|
||||
return url
|
||||
}
|
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Argument, Option, program } from '@commander-js/extra-typings'
|
||||
import { setBase, status } from '@cryptgeon/shared'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { download } from './download.js'
|
||||
import { parseFile, parseNumber } from './parsers.js'
|
||||
import { getStdin } from './stdin.js'
|
||||
import { upload } from './upload.js'
|
||||
import { checkConstrains, exit } from './utils.js'
|
||||
import { download } from './actions/download.js'
|
||||
import { upload } from './actions/upload.js'
|
||||
import { API } from './shared/api.js'
|
||||
import { parseFile, parseNumber } from './utils/parsers.js'
|
||||
import { getStdin } from './utils/stdin.js'
|
||||
import { checkConstrains, exit } from './utils/utils.js'
|
||||
|
||||
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
|
||||
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer)
|
||||
@@ -33,8 +33,8 @@ program
|
||||
.description('show information about the server')
|
||||
.addOption(server)
|
||||
.action(async (options) => {
|
||||
setBase(options.server)
|
||||
const response = await status()
|
||||
API.setOptions({ server: options.server })
|
||||
const response = await API.status()
|
||||
const formatted = {
|
||||
...response,
|
||||
max_size: prettyBytes(response.max_size),
|
||||
@@ -54,7 +54,7 @@ send
|
||||
.addOption(minutes)
|
||||
.addOption(password)
|
||||
.action(async (files, options) => {
|
||||
setBase(options.server!)
|
||||
API.setOptions({ server: options.server })
|
||||
await checkConstrains(options)
|
||||
options.password ||= await getStdin()
|
||||
try {
|
||||
@@ -72,7 +72,7 @@ send
|
||||
.addOption(minutes)
|
||||
.addOption(password)
|
||||
.action(async (text, options) => {
|
||||
setBase(options.server!)
|
||||
API.setOptions({ server: options.server })
|
||||
await checkConstrains(options)
|
||||
options.password ||= await getStdin()
|
||||
try {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export * from '@cryptgeon/shared'
|
||||
export * from './download.js'
|
||||
export * from './upload.js'
|
||||
export * from './utils.js'
|
||||
export * from './actions/download.js'
|
||||
export * from './actions/upload.js'
|
||||
export * from './shared/adapters.js'
|
||||
export * from './shared/api.js'
|
||||
|
@@ -23,6 +23,10 @@ export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
||||
contents: string
|
||||
}
|
||||
|
||||
type ClientOptions = {
|
||||
server: string
|
||||
}
|
||||
|
||||
type CallOptions = {
|
||||
url: string
|
||||
method: string
|
||||
@@ -31,14 +35,21 @@ type CallOptions = {
|
||||
|
||||
export class PayloadToLargeError extends Error {}
|
||||
|
||||
export let BASE = ''
|
||||
|
||||
export function setBase(url: string) {
|
||||
BASE = url
|
||||
export let client: ClientOptions = {
|
||||
server: '',
|
||||
}
|
||||
|
||||
export async function call(options: CallOptions) {
|
||||
const response = await fetch(BASE + '/api/' + options.url, {
|
||||
function setOptions(options: Partial<ClientOptions>) {
|
||||
client = { ...client, ...options }
|
||||
}
|
||||
|
||||
function getOptions(): ClientOptions {
|
||||
return client
|
||||
}
|
||||
|
||||
async function call(options: CallOptions) {
|
||||
const url = client.server + '/api/' + options.url
|
||||
const response = await fetch(url, {
|
||||
method: options.method,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
mode: 'cors',
|
||||
@@ -54,7 +65,7 @@ export async function call(options: CallOptions) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function create(note: Note) {
|
||||
async function create(note: Note) {
|
||||
const { meta, ...rest } = note
|
||||
const body: NoteCreate = {
|
||||
...rest,
|
||||
@@ -68,7 +79,7 @@ export async function create(note: Note) {
|
||||
return data as { id: string }
|
||||
}
|
||||
|
||||
export async function get(id: string): Promise<NotePublic> {
|
||||
async function get(id: string): Promise<NotePublic> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'delete',
|
||||
@@ -82,7 +93,7 @@ export async function get(id: string): Promise<NotePublic> {
|
||||
return note
|
||||
}
|
||||
|
||||
export async function info(id: string): Promise<NoteInfo> {
|
||||
async function info(id: string): Promise<NoteInfo> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'get',
|
||||
@@ -101,16 +112,27 @@ export type Status = {
|
||||
max_views: number
|
||||
max_expiration: number
|
||||
allow_advanced: boolean
|
||||
allow_files: boolean
|
||||
theme_image: string
|
||||
theme_text: string
|
||||
theme_favicon: string
|
||||
theme_page_title: string
|
||||
theme_new_note_notice: boolean
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
async function status() {
|
||||
const data = await call({
|
||||
url: 'status/',
|
||||
method: 'get',
|
||||
})
|
||||
return data as Status
|
||||
}
|
||||
|
||||
export const API = {
|
||||
setOptions,
|
||||
getOptions,
|
||||
create,
|
||||
get,
|
||||
info,
|
||||
status,
|
||||
}
|
@@ -21,7 +21,7 @@ export function parseURL(value: string, _: URL): URL {
|
||||
}
|
||||
|
||||
export function parseNumber(value: string, _: number): number {
|
||||
const n = parseInt(value, 10)
|
||||
if (isNaN(n)) throw new InvalidOptionArgumentError('invalid number')
|
||||
const n = Number.parseInt(value, 10)
|
||||
if (Number.isNaN(n)) throw new InvalidOptionArgumentError('invalid number')
|
||||
return n
|
||||
}
|
@@ -18,6 +18,7 @@ export function getStdin(timeout: number = 10): Promise<string> {
|
||||
resolve('')
|
||||
}, timeout)
|
||||
|
||||
process.stdin.on('error', reject)
|
||||
process.stdin.on('data', dataHandler)
|
||||
process.stdin.on('end', endHandler)
|
||||
})
|
@@ -1,5 +1,5 @@
|
||||
import { status } from '@cryptgeon/shared'
|
||||
import { exit as exitNode } from 'node:process'
|
||||
import { API } from '../shared/api.js'
|
||||
|
||||
export function exit(message: string) {
|
||||
console.error(message)
|
||||
@@ -11,7 +11,7 @@ export async function checkConstrains(constrains: { views?: number; minutes?: nu
|
||||
if (views && minutes) exit('cannot set view and minutes constrains simultaneously')
|
||||
if (!views && !minutes) constrains.views = 1
|
||||
|
||||
const response = await status()
|
||||
const response = await API.status()
|
||||
if (views && views > response.max_views)
|
||||
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
|
||||
if (minutes && minutes > response.max_expiration)
|
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "Bundler",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"strict": true,
|
||||
|
@@ -3,24 +3,24 @@
|
||||
"note": "Notiz",
|
||||
"file": "Datei",
|
||||
"advanced": "Erweiterte Optionen",
|
||||
"create": "Notiz erstellen",
|
||||
"create": "Erstellen",
|
||||
"loading": "Lädt...",
|
||||
"mode": "Modus",
|
||||
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
|
||||
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
|
||||
"max": "max",
|
||||
"share_link": "Link teilen",
|
||||
"copy_clipboard": "Link in die Zwischenablage kopieren",
|
||||
"copied_to_clipboard": "Link in die Zwischenablage kopiert.",
|
||||
"encrypting": "Notiz wird verschlüsselt...",
|
||||
"decrypting": "Notiz wird entschlüsselt...",
|
||||
"copy_clipboard": "In die Zwischenablage kopieren",
|
||||
"copied_to_clipboard": "In die Zwischenablage kopiert.",
|
||||
"encrypting": "Wird verschlüsselt...",
|
||||
"decrypting": "Wird entschlüsselt...",
|
||||
"uploading": "Hochladen",
|
||||
"downloading": "Wird heruntergeladen",
|
||||
"qr_code": "QR-Code",
|
||||
"password": "Passwort"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Senden Sie mit ganz einfach einem Klick <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien. Erstellen Sie einfach eine Notiz und teilen Sie den Link.",
|
||||
"intro": "Erstellen Sie mit einem Klick <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien und teilen Sie diese über einen Link.",
|
||||
"explanation": "Die Notiz verfällt nach {type}.",
|
||||
"new_note": "Neue Notiz",
|
||||
"new_note_notice": "<b>Wichtiger Hinweis zur Verfügbarkeit:</b><br />Es kann nicht garantiert werden, dass diese Notiz gespeichert wird, da diese <b>ausschließlich im Speicher</b> gehalten werden. Ist dieser voll, werden die ältesten Notizen entfernt.<br />(Wahrscheinlich gibt es keine derartigen Probleme, seien Sie nur vorgewarnt).",
|
||||
@@ -34,24 +34,25 @@
|
||||
"note_created": "Notiz wurde erstellt."
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "Standardmäßig wird für jede Notiz ein sicher generiertes Passwort verwendet. Sie können alternativ ein eigenes Kennwort wählen, das nicht im Link enthalten ist.",
|
||||
"explanation": "Standardmäßig wird für jede Notiz ein generiertes, sicheres Passwort verwendet. Alternativ können Sie ein eigenes Kennwort festlegen, welches nicht im Link enthalten ist.",
|
||||
"custom_password": "Benutzerdefiniertes Passwort"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "Notiz konnte nicht gefunden werden oder wurde bereits gelöscht.",
|
||||
"decryption_failed": "Notiz konnte nicht entschlüsselt werden. Vermutlich ist das Passwort falsch oder der Link defekt. Notiz wurde zerstört.",
|
||||
"decryption_failed": "Notiz konnte nicht entschlüsselt werden. Vermutlich ist das Passwort falsch oder der Link defekt. Die Notiz wurde daher gelöscht.",
|
||||
"unsupported_type": "Nicht unterstützter Notiztyp."
|
||||
},
|
||||
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und anschließend zu löschen, falls das Limit erreicht ist.",
|
||||
"explanation": "Klicken Sie auf den Button, um die Notiz anzuzeigen und anschließend zu löschen, falls ein festgelegtes Limit erreicht wurde.",
|
||||
"show_note": "Notiz anzeigen",
|
||||
"warning_will_not_see_again": "Achtung! Sie haben anschließend <b>keine</b> Gelegenheit, die Notiz noch einmal anzusehen.",
|
||||
"warning_will_not_see_again": "ACHTUNG! Sie werden anschließend <b>keine</b> Gelegenheit mehr haben, die Notiz erneut anzusehen.",
|
||||
"download_all": "Alle Dateien herunterladen",
|
||||
"links_found": "Links in der Notiz:"
|
||||
"links_found": "Gefundene Links in der Notiz:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Ausgewählte Dateien",
|
||||
"no_files_selected": "Keine Dateien ausgewählt"
|
||||
"no_files_selected": "Keine Dateien ausgewählt",
|
||||
"clear": "Zurücksetzen"
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"new_note": "new note",
|
||||
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes will be removed.<br />(you probably will be fine, just be warned.)",
|
||||
"errors": {
|
||||
"note_to_big": "could not create note. note is to big",
|
||||
"note_to_big": "could not create note. note is too big",
|
||||
"note_error": "could not create note. please try again.",
|
||||
"max": "max: {n}",
|
||||
"empty_content": "note is empty."
|
||||
@@ -44,7 +44,7 @@
|
||||
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
|
||||
"unsupported_type": "unsupported note type."
|
||||
},
|
||||
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
||||
"explanation": "click below to show and delete the note if the counter has reached its limit",
|
||||
"show_note": "show note",
|
||||
"warning_will_not_see_again": "you will <b>not</b> get the chance to see the note again.",
|
||||
"download_all": "download all",
|
||||
@@ -52,6 +52,7 @@
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Selected Files",
|
||||
"no_files_selected": "No Files Selected"
|
||||
"no_files_selected": "No Files Selected",
|
||||
"clear": "Reset"
|
||||
}
|
||||
}
|
||||
|
@@ -52,6 +52,7 @@
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Archivos seleccionados",
|
||||
"no_files_selected": "No hay archivos seleccionados"
|
||||
"no_files_selected": "No hay archivos seleccionados",
|
||||
"clear": "Restablecer"
|
||||
}
|
||||
}
|
||||
|
@@ -12,20 +12,20 @@
|
||||
"share_link": "partager le lien",
|
||||
"copy_clipboard": "copier dans le presse-papiers",
|
||||
"copied_to_clipboard": "copié dans le presse-papiers",
|
||||
"encrypting": "cryptage",
|
||||
"encrypting": "chiffrer",
|
||||
"decrypting": "déchiffrer",
|
||||
"uploading": "téléchargement",
|
||||
"uploading": "téléversement",
|
||||
"downloading": "téléchargement",
|
||||
"qr_code": "code qr",
|
||||
"password": "mot de passe"
|
||||
},
|
||||
"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}.",
|
||||
"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": {
|
||||
"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.",
|
||||
"max": "max: {n}",
|
||||
"empty_content": "La note est vide."
|
||||
@@ -45,13 +45,14 @@
|
||||
"unsupported_type": "type de note non supporté."
|
||||
},
|
||||
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
||||
"show_note": "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.",
|
||||
"download_all": "télécharger tout",
|
||||
"download_all": "tout télécharger",
|
||||
"links_found": "liens trouvés à l’intérieur de la note :"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Fichiers sélectionnés",
|
||||
"no_files_selected": "Aucun fichier sélectionné"
|
||||
"no_files_selected": "Aucun fichier sélectionné",
|
||||
"clear": "Réinitialiser"
|
||||
}
|
||||
}
|
||||
|
@@ -52,6 +52,7 @@
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "File selezionati",
|
||||
"no_files_selected": "Nessun file selezionato"
|
||||
"no_files_selected": "Nessun file selezionato",
|
||||
"clear": "Reset"
|
||||
}
|
||||
}
|
||||
|
@@ -52,6 +52,7 @@
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "選択したファイル",
|
||||
"no_files_selected": "ファイルが選択されていません"
|
||||
"no_files_selected": "ファイルが選択されていません",
|
||||
"clear": "リセット"
|
||||
}
|
||||
}
|
||||
|
58
packages/frontend/locales/pl.json
Normal file
58
packages/frontend/locales/pl.json
Normal 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ść"
|
||||
}
|
||||
}
|
@@ -1,57 +1,58 @@
|
||||
{
|
||||
"common": {
|
||||
"note": "заметка",
|
||||
"file": "файл",
|
||||
"advanced": "расширенные",
|
||||
"create": "создать",
|
||||
"loading": "загрузка",
|
||||
"mode": "режим",
|
||||
"views": "{n, plural, =0 {просмотры} =1 {1 просмотр} other {# просмотры}}",
|
||||
"minutes": "{n, plural, =0 {минут} =1 {1 минута} other {# минуты}}",
|
||||
"max": "макс",
|
||||
"share_link": "поделиться ссылкой",
|
||||
"copy_clipboard": "скопировать в буфер обмена",
|
||||
"copied_to_clipboard": "скопировано в буфер обмена",
|
||||
"encrypting": "шифрование",
|
||||
"decrypting": "расшифровка",
|
||||
"uploading": "загрузка",
|
||||
"downloading": "скачивание",
|
||||
"qr_code": "qr код",
|
||||
"password": "пароль"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Легко отправляйте <i>полностью зашифрованные</i> защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.",
|
||||
"explanation": "заметка истечет и будет уничтожена после {type}.",
|
||||
"new_note": "новая заметка",
|
||||
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes will be removed.<br />(you probably will be fine, just be warned.)",
|
||||
"errors": {
|
||||
"note_to_big": "нельзя создать новую заметку. заметка слишком большая",
|
||||
"note_error": "нельзя создать новую заметку. пожалйста попробуйте позднее.",
|
||||
"max": "макс: {n}",
|
||||
"empty_content": "пустая заметка."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "заметка создана."
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.",
|
||||
"custom_password": "пользовательский пароль"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "заметка не найдена или была удалена.",
|
||||
"decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. записка уничтожена.",
|
||||
"unsupported_type": "неподдерживаемый тип заметки."
|
||||
},
|
||||
"explanation": "щелкните ниже, чтобы показать и удалить примечание, если счетчик достиг предела",
|
||||
"show_note": "показать заметку",
|
||||
"warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.",
|
||||
"download_all": "скачать всё",
|
||||
"links_found": "ссылки внутри заметки:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Выбранные файлы",
|
||||
"no_files_selected": "Файлы не выбраны"
|
||||
}
|
||||
}
|
||||
{
|
||||
"common": {
|
||||
"note": "заметка",
|
||||
"file": "файл",
|
||||
"advanced": "расширенные",
|
||||
"create": "создать",
|
||||
"loading": "загрузка",
|
||||
"mode": "режим",
|
||||
"views": "{n, plural, =0 {просмотры} =1 {1 просмотр} other {# просмотры}}",
|
||||
"minutes": "{n, plural, =0 {минут} =1 {1 минута} other {# минуты}}",
|
||||
"max": "макс",
|
||||
"share_link": "поделиться ссылкой",
|
||||
"copy_clipboard": "скопировать в буфер обмена",
|
||||
"copied_to_clipboard": "скопировано в буфер обмена",
|
||||
"encrypting": "шифрование",
|
||||
"decrypting": "расшифровка",
|
||||
"uploading": "загрузка",
|
||||
"downloading": "скачивание",
|
||||
"qr_code": "qr код",
|
||||
"password": "пароль"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Легко отправляйте <i>полностью зашифрованные</i> защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.",
|
||||
"explanation": "заметка истечет и будет уничтожена после {type}.",
|
||||
"new_note": "новая заметка",
|
||||
"new_note_notice": "<b>доступность:</b><br />сохранение заметки не гарантируется, поскольку все хранится в оперативной памяти; если она заполнится, самые старые заметки будут удалены.<br />( вероятно, все будет в порядке, просто будьте осторожны.)",
|
||||
"errors": {
|
||||
"note_to_big": "нельзя создать новую заметку. заметка слишком большая",
|
||||
"note_error": "нельзя создать новую заметку. пожалйста попробуйте позднее.",
|
||||
"max": "макс: {n}",
|
||||
"empty_content": "пустая заметка."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "заметка создана."
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.",
|
||||
"custom_password": "пользовательский пароль"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "заметка не найдена или была удалена.",
|
||||
"decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. записка уничтожена.",
|
||||
"unsupported_type": "неподдерживаемый тип заметки."
|
||||
},
|
||||
"explanation": "щелкните ниже, чтобы показать и удалить примечание, если счетчик достиг предела",
|
||||
"show_note": "показать заметку",
|
||||
"warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.",
|
||||
"download_all": "скачать всё",
|
||||
"links_found": "ссылки внутри заметки:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Выбранные файлы",
|
||||
"no_files_selected": "Файлы не выбраны",
|
||||
"clear": "Сброс"
|
||||
}
|
||||
}
|
||||
|
@@ -52,6 +52,7 @@
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "已选中的文件",
|
||||
"no_files_selected": "没有文件被选中"
|
||||
"no_files_selected": "没有文件被选中",
|
||||
"clear": "重置"
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@zerodevx/svelte-toast": "^0.9.5",
|
||||
"adm-zip": "^0.5.10",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -26,14 +25,12 @@
|
||||
"svelte-intl-precompile": "^0.12.3",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.4"
|
||||
"vite": "^5.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cryptgeon/shared": "workspace:*",
|
||||
"cryptgeon": "workspace:*",
|
||||
"@fontsource/fira-mono": "^5.0.8",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"occulto": "^2.0.3",
|
||||
"occulto": "^2.0.6",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"qrious": "^4.0.2"
|
||||
}
|
||||
|
@@ -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'
|
||||
|
||||
export const status = writable<null | Status>(null)
|
||||
|
||||
export async function init() {
|
||||
status.set(await getStatus())
|
||||
status.set(await API.status())
|
||||
}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { toast, type SvelteToastOptions } from '@zerodevx/svelte-toast'
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
|
||||
export enum NotifyType {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
const themeMapping: Record<NotifyType, SvelteToastOptions['theme']> = {
|
||||
const themeMapping: Record<NotifyType, Record<string, string>> = {
|
||||
[NotifyType.Success]: {
|
||||
'--toastBackground': 'var(--ui-clr-primary)',
|
||||
'--toastBarBackground': 'var(--ui-clr-primary-alt)',
|
||||
@@ -17,7 +17,7 @@ const themeMapping: Record<NotifyType, SvelteToastOptions['theme']> = {
|
||||
}
|
||||
|
||||
function notifyFN(message: string, type: NotifyType = NotifyType.Success) {
|
||||
const options: SvelteToastOptions = {
|
||||
const options = {
|
||||
duration: 5_000,
|
||||
theme: {
|
||||
...themeMapping[type],
|
||||
|
@@ -4,7 +4,7 @@
|
||||
import { status } from '$lib/stores/status'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import type { Note } from '@cryptgeon/shared'
|
||||
import type { Note } from 'cryptgeon/shared'
|
||||
|
||||
export let note: Note
|
||||
export let timeExpiration = false
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||
import type { FileDTO } from '@cryptgeon/shared'
|
||||
import type { FileDTO } from 'cryptgeon/shared'
|
||||
|
||||
export let label: string = ''
|
||||
export let files: FileDTO[] = []
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
<div class="spacer" />
|
||||
<Button on:click={clear}>Clear</Button>
|
||||
<Button on:click={clear}>{$t('file_upload.clear')}</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
|
@@ -18,19 +18,19 @@
|
||||
export let icon: keyof typeof map
|
||||
</script>
|
||||
|
||||
<div on:click {...$$restProps}>
|
||||
<button on:click {...$$restProps}>
|
||||
{#if map[icon]}
|
||||
<svelte:component this={map[icon]} />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
div {
|
||||
button {
|
||||
display: inline-block;
|
||||
contain: strict;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
div > :global(svg) {
|
||||
button > :global(svg) {
|
||||
display: block;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import { status } from '$lib/stores/status'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import Canvas from './Canvas.svelte'
|
||||
@@ -35,9 +35,11 @@
|
||||
<Canvas value={url} />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
{#if $status?.theme_new_note_notice}
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
{/if}
|
||||
<br />
|
||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||
|
||||
|
@@ -1,15 +1,26 @@
|
||||
<script lang="ts" context="module">
|
||||
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
|
||||
|
||||
function saveAs(file: File) {
|
||||
const url = window.URL.createObjectURL(file)
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = url
|
||||
a.download = file.name
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
a.remove()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { saveAs } from 'file-saver'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import { copy } from '$lib/utils'
|
||||
import type { FileDTO, NotePublic } from '@cryptgeon/shared'
|
||||
import type { FileDTO, NotePublic } from 'cryptgeon/shared'
|
||||
|
||||
export let note: DecryptedNote
|
||||
|
||||
|
@@ -39,13 +39,23 @@
|
||||
<input bind:value {...$$restProps} class:valid={valid === true} />
|
||||
<div class="icons">
|
||||
{#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 random}
|
||||
<Icon class="icon" icon="dice" on:click={randomFN} />
|
||||
<Icon disabled={$$restProps.disabled} class="icon" icon="dice" on:click={randomFN} />
|
||||
{/if}
|
||||
{#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}
|
||||
</div>
|
||||
</label>
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import { notify } from './toast'
|
||||
|
||||
export function copy(value: string) {
|
||||
copyToClipboard(value)
|
||||
window.navigator.clipboard.writeText(value)
|
||||
const msg = get(t)('common.copied_to_clipboard')
|
||||
notify.success(msg)
|
||||
}
|
||||
|
@@ -13,8 +13,7 @@
|
||||
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextArea from '$lib/ui/TextArea.svelte'
|
||||
import type { FileDTO, Note } from '@cryptgeon/shared'
|
||||
import { Adapters, PayloadToLargeError, create } from '@cryptgeon/shared'
|
||||
import { Adapters, API, PayloadToLargeError, type FileDTO, type Note } from 'cryptgeon/shared'
|
||||
|
||||
let note: Note = {
|
||||
contents: '',
|
||||
@@ -77,7 +76,7 @@
|
||||
else data.views = parseInt(note.views as any)
|
||||
|
||||
loading = $t('common.uploading')
|
||||
const response = await create(data)
|
||||
const response = await API.create(data)
|
||||
result = {
|
||||
id: response.id,
|
||||
password: customPassword ? undefined : Hex.encode(key),
|
||||
@@ -118,12 +117,14 @@
|
||||
{/if}
|
||||
|
||||
<div class="bottom">
|
||||
<Switch
|
||||
data-testid="switch-file"
|
||||
class="file"
|
||||
label={$t('common.file')}
|
||||
bind:value={isFile}
|
||||
/>
|
||||
{#if $status?.allow_files}
|
||||
<Switch
|
||||
data-testid="switch-file"
|
||||
class="file"
|
||||
label={$t('common.file')}
|
||||
bind:value={isFile}
|
||||
/>
|
||||
{/if}
|
||||
{#if $status?.allow_advanced}
|
||||
<Switch
|
||||
data-testid="switch-advanced"
|
||||
|
@@ -1,9 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { status } from '$lib/stores/status'
|
||||
|
||||
function reset() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<a href="/">
|
||||
<a on:click={reset} href="/">
|
||||
{#if $status?.theme_image}
|
||||
<img alt="logo" src={$status.theme_image} />
|
||||
{:else}
|
||||
|
@@ -40,8 +40,8 @@
|
||||
<br />
|
||||
you are welcomed to check & audit the
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
||||
source code
|
||||
</a>.
|
||||
source code</a
|
||||
>.
|
||||
</span>
|
||||
</AboutParagraph>
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
import Loader from '$lib/ui/Loader.svelte'
|
||||
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import { Adapters, get, info, type NoteMeta } from '@cryptgeon/shared'
|
||||
import { Adapters, API, type NoteMeta } from 'cryptgeon/shared'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
export let data: PageData
|
||||
@@ -28,7 +28,7 @@
|
||||
try {
|
||||
loading = $t('common.loading')
|
||||
password = window.location.hash.slice(1)
|
||||
const note = await info(id)
|
||||
const note = await API.info(id)
|
||||
meta = note.meta
|
||||
exists = true
|
||||
} catch {
|
||||
@@ -51,7 +51,7 @@
|
||||
// Load note
|
||||
error = null
|
||||
loading = $t('common.downloading')
|
||||
const data = await get(id)
|
||||
const data = await API.get(id)
|
||||
loading = $t('common.decrypting')
|
||||
const derived = meta?.derivation && (await AES.derive(password!, meta.derivation))
|
||||
const key = derived ? derived[0] : Hex.decode(password!)
|
||||
|
@@ -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'
|
||||
proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 })
|
||||
})
|
||||
server.listen(1234)
|
||||
console.log('Proxy on http://localhost:1234')
|
||||
server.listen(3000)
|
||||
console.log('Proxy on http://localhost:3000')
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"composite": true,
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"strict": true
|
||||
}
|
||||
}
|
@@ -3,17 +3,19 @@ import { devices, type PlaywrightTestConfig } from '@playwright/test'
|
||||
const config: PlaywrightTestConfig = {
|
||||
use: {
|
||||
video: 'retain-on-failure',
|
||||
baseURL: 'http://localhost:1234',
|
||||
actionTimeout: 60_000,
|
||||
baseURL: 'http://localhost:3000',
|
||||
actionTimeout: 30_000,
|
||||
},
|
||||
|
||||
outputDir: './test-results',
|
||||
testDir: './test',
|
||||
timeout: 60_000,
|
||||
timeout: 30_000,
|
||||
fullyParallel: true,
|
||||
retries: 2,
|
||||
|
||||
webServer: {
|
||||
command: 'docker compose -f docker-compose.dev.yaml up',
|
||||
port: 1234,
|
||||
command: 'pnpm run docker:up',
|
||||
port: 3000,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
|
||||
@@ -21,10 +23,6 @@ const config: PlaywrightTestConfig = {
|
||||
{ name: 'chrome', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'safari', use: { ...devices['Desktop Safari'] } },
|
||||
|
||||
{ name: 'cli', use: { ...devices['Desktop Chrome'] }, grep: [/@cli/] },
|
||||
{ name: 'web', use: { ...devices['Desktop Chrome'] }, grep: [/@web/] },
|
||||
{ name: 'cross', use: { ...devices['Desktop Chrome'] }, grep: [/@cross/] },
|
||||
],
|
||||
}
|
||||
|
||||
|
5183
pnpm-lock.yaml
generated
5183
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
import { test } from '@playwright/test'
|
||||
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'
|
||||
|
||||
test.describe('file @cli', () => {
|
||||
test('simple', async ({ page }) => {
|
||||
test('simple', async () => {
|
||||
const file = await tmpFile(Files.Image)
|
||||
const checksum = await getFileChecksum(file)
|
||||
const note = await CLI('send', 'file', file)
|
||||
@@ -17,7 +18,7 @@ test.describe('file @cli', () => {
|
||||
test.expect(checksum).toBe(c)
|
||||
})
|
||||
|
||||
test('simple with password', async ({ page }) => {
|
||||
test('simple with password', async () => {
|
||||
const file = await tmpFile(Files.Image)
|
||||
const password = 'password'
|
||||
const checksum = await getFileChecksum(file)
|
||||
|
@@ -2,7 +2,7 @@ import { test } from '@playwright/test'
|
||||
import { CLI, getLinkFromCLI } from '../../utils'
|
||||
|
||||
test.describe('text @cli', () => {
|
||||
test('simple', async ({ 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 note = await CLI('send', 'text', text)
|
||||
const link = getLinkFromCLI(note.stdout)
|
||||
@@ -11,7 +11,7 @@ test.describe('text @cli', () => {
|
||||
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||
})
|
||||
|
||||
test('simple with password', async ({ page }) => {
|
||||
test('simple with password', async () => {
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying.`
|
||||
const password = 'password'
|
||||
const note = await CLI('send', 'text', text, '--password', password)
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { CLI, checkLinkForDownload, checkLinkForText, createNote, getLinkFromCLI } from '../../utils'
|
||||
import { Files, getFileChecksum, rm, tmpFile } from '../../files'
|
||||
import { basename } from 'path'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { basename } from 'node: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 password = 'password'
|
||||
@@ -30,7 +31,7 @@ test.describe('text @cross', () => {
|
||||
test('web to cli', async ({ page }) => {
|
||||
const files = [Files.Image]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNote(page, { files })
|
||||
const link = await createNoteSuccessfully(page, { files })
|
||||
|
||||
const filename = basename(files[0])
|
||||
await CLI('open', link, '--all')
|
||||
@@ -42,7 +43,7 @@ test.describe('text @cross', () => {
|
||||
test('web to cli with password', async ({ page }) => {
|
||||
const files = [Files.Image]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNote(page, { files, password })
|
||||
const link = await createNoteSuccessfully(page, { files, password })
|
||||
|
||||
const filename = basename(files[0])
|
||||
await CLI('open', link, '--all', '--password', password)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { CLI, checkLinkForText, 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 password = 'password'
|
||||
@@ -13,7 +13,7 @@ test.describe('text @cross', () => {
|
||||
})
|
||||
|
||||
test('web to cli', async ({ page }) => {
|
||||
const link = await createNote(page, { text })
|
||||
const link = await createNoteSuccessfully(page, { text })
|
||||
const retrieved = await CLI('open', link)
|
||||
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||
})
|
||||
@@ -25,7 +25,7 @@ test.describe('text @cross', () => {
|
||||
})
|
||||
|
||||
test('web to cli with password', async ({ page }) => {
|
||||
const link = await createNote(page, { text, password })
|
||||
const link = await createNoteSuccessfully(page, { text, password })
|
||||
const retrieved = await CLI('open', link, '--password', password)
|
||||
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||
})
|
||||
|
@@ -1,10 +1,5 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { cp as cpFN, rm as rmFN } from 'fs'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { promisify } from 'util'
|
||||
|
||||
export const cp = promisify(cpFN)
|
||||
export const rm = promisify(rmFN)
|
||||
import { createHash } from 'node:crypto'
|
||||
import { cp, readFile } from 'node:fs/promises'
|
||||
|
||||
export const Files = {
|
||||
PDF: 'test/assets/AES.pdf',
|
||||
|
@@ -6,17 +6,22 @@ import { getFileChecksum } from './files'
|
||||
const exec = promisify(execFile)
|
||||
|
||||
type CreatePage = {
|
||||
text?: string
|
||||
files?: string[]
|
||||
views?: number
|
||||
expiration?: number
|
||||
error?: 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('/')
|
||||
|
||||
if (options.text) {
|
||||
if ('text' in options) {
|
||||
await page.getByTestId('text-field').fill(options.text)
|
||||
} else if (options.files) {
|
||||
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) {
|
||||
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('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()
|
||||
}
|
||||
|
||||
if (options.error) {
|
||||
await expect(page.locator('.error-text')).toContainText(options.error, { timeout: 60_000 })
|
||||
}
|
||||
|
||||
// Return share link
|
||||
export async function createNoteSuccessfully(page: Page, options: CreatePage): Promise<string> {
|
||||
await createNote(page, options)
|
||||
return await page.getByTestId('share-link').inputValue()
|
||||
}
|
||||
|
||||
export async function createNoteError(page: Page, options: CreatePage, error: string): Promise<void> {
|
||||
await createNote(page, options)
|
||||
await expect(page.locator('._toastContainer')).toContainText(error)
|
||||
}
|
||||
|
||||
type CheckLinkBase = {
|
||||
link: string
|
||||
text: string
|
||||
@@ -69,7 +78,7 @@ export async function checkLinkForDownload(page: Page, options: CheckLinkBase &
|
||||
const path = await download.path()
|
||||
if (!path) throw new Error('Download failed')
|
||||
const cs = await getFileChecksum(path)
|
||||
await expect(cs).toBe(options.checksum)
|
||||
expect(cs).toBe(options.checksum)
|
||||
}
|
||||
|
||||
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)
|
||||
await page.getByTestId('show-note-button').click()
|
||||
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) {
|
||||
@@ -91,7 +100,7 @@ export async function CLI(...args: string[]) {
|
||||
return await exec('./packages/cli/dist/cli.cjs', args, {
|
||||
env: {
|
||||
...process.env,
|
||||
CRYPTGEON_SERVER: 'http://localhost:1234',
|
||||
CRYPTGEON_SERVER: 'http://localhost:3000',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { Files, getFileChecksum } from '../../files'
|
||||
import { checkLinkForDownload, createNote } from '../../utils'
|
||||
import { checkLinkForDownload, createNoteSuccessfully } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('multiple', async ({ page }) => {
|
||||
const files = [Files.PDF, Files.Image]
|
||||
const checksums = await Promise.all(files.map(getFileChecksum))
|
||||
const link = await 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: 'AES.pdf', checksum: checksums[0] })
|
||||
})
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { Files, getFileChecksum } from '../../files'
|
||||
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote } from '../../utils'
|
||||
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNoteSuccessfully } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('simple pdf', async ({ page }) => {
|
||||
const files = [Files.PDF]
|
||||
const link = await createNote(page, { files })
|
||||
const link = await createNoteSuccessfully(page, { files })
|
||||
await checkLinkForText(page, { link, text: 'AES.pdf' })
|
||||
await checkLinkDoesNotExist(page, link)
|
||||
})
|
||||
@@ -13,21 +13,21 @@ test.describe('@web', () => {
|
||||
test('pdf content', async ({ page }) => {
|
||||
const files = [Files.PDF]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNote(page, { files })
|
||||
const link = await createNoteSuccessfully(page, { files })
|
||||
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum })
|
||||
})
|
||||
|
||||
test('image content', async ({ page }) => {
|
||||
const files = [Files.Image]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNote(page, { files })
|
||||
const link = await createNoteSuccessfully(page, { files })
|
||||
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum })
|
||||
})
|
||||
|
||||
test('simple pdf with password', async ({ page }) => {
|
||||
const files = [Files.PDF]
|
||||
const password = 'password'
|
||||
const link = await createNote(page, { files, password })
|
||||
const link = await createNoteSuccessfully(page, { files, password })
|
||||
await checkLinkForText(page, { link, text: 'AES.pdf', password })
|
||||
await checkLinkDoesNotExist(page, link)
|
||||
})
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { createNote } from '../../utils'
|
||||
import { Files } from '../../files'
|
||||
import { createNoteError } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test.skip('to big zip', async ({ page }) => {
|
||||
const files = [Files.Zip]
|
||||
const link = await createNote(page, { files, error: 'note is to big' })
|
||||
await createNoteError(page, { files }, 'could not create note. note is too big')
|
||||
})
|
||||
})
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNoteSuccessfully } from '../../utils'
|
||||
|
||||
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 minutes = 1
|
||||
const timeout = minutes * 60_000
|
||||
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 page.waitForTimeout(timeout)
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkForText, createNote } from '../../utils'
|
||||
import { checkLinkForText, createNoteSuccessfully } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('simple', async ({ page }) => {
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of deceive play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||
const link = await createNote(page, { text })
|
||||
const link = await createNoteSuccessfully(page, { text })
|
||||
await checkLinkForText(page, { link, text })
|
||||
})
|
||||
|
||||
test('simple with password', async ({ page }) => {
|
||||
const text = 'Foo bar'
|
||||
const password = '123'
|
||||
const shareLink = await createNote(page, { text, password })
|
||||
const shareLink = await createNoteSuccessfully(page, { text, password })
|
||||
await checkLinkForText(page, { link: shareLink, text, password })
|
||||
})
|
||||
})
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNoteSuccessfully } from '../../utils'
|
||||
|
||||
test.describe('@web', () => {
|
||||
test('only shown once', async ({ page }) => {
|
||||
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
|
||||
const link = await createNote(page, { text })
|
||||
const link = await createNoteSuccessfully(page, { text })
|
||||
await checkLinkForText(page, { link, text })
|
||||
await checkLinkDoesNotExist(page, link)
|
||||
})
|
||||
|
||||
test('view 3 times', async ({ page }) => {
|
||||
const text = `Justice holiest overcome fearful strong ultimate holiest christianity.`
|
||||
const link = await 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 })
|
||||
|
Reference in New Issue
Block a user