Compare commits

..

3 Commits

Author SHA1 Message Date
dependabot[bot]
24c66bd08a Merge d4a68b058b into b089323990 2023-08-04 10:49:36 +02:00
d4a68b058b Merge branch 'main' into dependabot/npm_and_yarn/vite-4.3.9 2023-08-04 10:49:33 +02:00
dependabot[bot]
27c1ad460c Bump vite from 4.3.5 to 4.3.9
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.5 to 4.3.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-03 05:55:31 +00:00
75 changed files with 4394 additions and 5457 deletions

View File

View File

@@ -10,33 +10,30 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
# Node
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
node-version-file: '.nvmrc'
# Docker
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
with:
install: true
- name: Prepare
run: |
pnpm install
pnpm install --frozen-lockfile
pnpm exec playwright install --with-deps
pnpm run test:prepare
- name: Run your tests
run: pnpm test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
- uses: actions/upload-artifact@v3
with:
name: playwright-report
path: test-results/
retention-days: 7
name: test-results
path: test-results

1
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
v22.7.0
v18.16

View File

@@ -5,13 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.4.0] - 2023-11-01
### Changed
- Removed HTML sanitation, display the original message as string
- Links are now displayed under the note in a separate section
## [2.3.1] - 2023-06-23
### Added

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,27 @@
# FRONTEND
FROM node:22-alpine as client
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM node:18-alpine as client
WORKDIR /tmp
RUN npm install -g pnpm@8
COPY . .
RUN pnpm install --frozen-lockfile
RUN pnpm run build
# BACKEND
FROM rust:1.80-alpine as backend
FROM rust:1.69-alpine as backend
WORKDIR /tmp
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
RUN apk add libc-dev openssl-dev alpine-sdk
COPY ./packages/backend/Cargo.* ./
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
RUN cargo fetch
COPY ./packages/backend ./
RUN RUSTFLAGS="-Ctarget-feature=-crt-static" cargo build --release
RUN cargo build --release
# RUNNER
FROM alpine:3.19
FROM alpine
WORKDIR /app
RUN apk add --no-cache curl libgcc
RUN apk add --no-cache curl
COPY --from=backend /tmp/target/release/cryptgeon .
COPY --from=client /tmp/packages/frontend/build ./frontend
ENV FRONTEND_PATH="./frontend"

View File

@@ -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,21 +62,19 @@ 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. |
| `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
| `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. |
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
| 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 |
## Deployment
@@ -141,16 +139,12 @@ There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-sy
- English by [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
### Written Guides
- French by [zarevskaya](https://belginux.com/installer-cryptgeon-avec-docker/)
## Development
**Requirements**
- `pnpm`: `>=9`
- `node`: `>=22`
- `pnpm`: `>=6`
- `node`: `>=18`
- `rust`: edition `2021`
**Install**
@@ -178,7 +172,7 @@ Running `pnpm run dev` in the root folder will start the following things:
- client
- cli
You can see the app under [localhost:3000](http://localhost:3000).
You can see the app under [localhost:1234](http://localhost:1234).
> There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json)

View File

@@ -59,19 +59,19 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
## Variables de entorno
| 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:3000](http://localhost:3000).
Puedes ver la app en [localhost:1234](http://localhost:1234).
> Existe una colección de Postman con algunas peticiones de ejemplo [disponible en el repo](./Cryptgeon.postman_collection.json)

View File

@@ -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 后端
- 可热重载的客户端
你可以通过 3000 端口进入该应用,即 [localhost:3000](http://localhost:3000).
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234).
## 测试

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
{
"packageManager": "pnpm@8.6.3",
"scripts": {
"dev:docker": "docker compose -f docker-compose.dev.yaml up redis",
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
"dev:packages": "pnpm --parallel run dev",
"dev": "run-p dev:*",
"docker:up": "docker compose -f docker-compose.dev.yaml up",
"docker:build": "docker compose -f docker-compose.dev.yaml build",
"test": "playwright test --project=chrome --project=firefox --project=safari",
"test:local": "playwright test --project=chrome",
"test": "playwright test --project chrome firefox safari",
"test:local": "playwright test --project chrome",
"test:server": "run-s docker:up",
"test:prepare": "run-p build docker:build",
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
"@types/node": "^22.5.0",
"@playwright/test": "^1.33.0",
"@types/node": "^20.1.3",
"npm-run-all": "^4.1.5",
"shelljs": "^0.8.5"
},
"packageManager": "pnpm@9.9.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,24 @@
[package]
name = "cryptgeon"
version = "2.8.0"
version = "2.3.1"
authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021"
rust-version = "1.80"
[[bin]]
name = "cryptgeon"
path = "src/main.rs"
[dependencies]
# Core
axum = "0.7.5"
serde = { version = "1.0.208", features = ["derive"] }
tokio = { version = "1.39.3", features = ["full"] }
tower = "0.5.0"
tower-http = { version = "0.5.2", features = ["full"] }
redis = { version = "0.25.2", features = ["tls-native-tls"] }
# Utility
actix-web = "4"
actix-files = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
lazy_static = "1"
ring = "0.16"
bs62 = "0.1"
byte-unit = "4"
dotenv = "0.15"
mime = "0.3"
env_logger = "0.9"
log = "0.4"
redis = "0.23"

View File

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

View File

@@ -0,0 +1,14 @@
use actix_web::web;
use crate::health;
use crate::note;
use crate::status;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/api")
.service(note::init())
.service(status::init())
.service(health::init()),
);
}

View File

@@ -0,0 +1,17 @@
use actix_files::{Files, NamedFile};
use actix_web::{web, Result};
use crate::config;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(
Files::new("/", config::FRONTEND_PATH.to_string())
.index_file("index.html")
.use_etag(true),
);
}
pub async fn index() -> Result<NamedFile> {
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
Ok(NamedFile::open(index)?)
}

View File

@@ -14,30 +14,26 @@ 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 ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
.unwrap_or("true".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();
}
// THEME
@@ -58,8 +54,4 @@ lazy_static! {
.unwrap_or("".to_string())
.parse()
.unwrap();
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
.unwrap_or("true".to_string())
.parse()
.unwrap();
}

View File

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

View File

@@ -0,0 +1,16 @@
use actix_web::{get, web, HttpResponse, Responder, Scope};
use crate::store;
#[get("/")]
async fn get_live() -> impl Responder {
if store::can_reach_redis() {
return HttpResponse::Ok();
} else {
return HttpResponse::ServiceUnavailable();
}
}
pub fn init() -> Scope {
web::scope("/live").service(get_live)
}

View File

@@ -1,67 +1,44 @@
use axum::{
extract::{DefaultBodyLimit, Request},
routing::{delete, get, post},
Router, ServiceExt,
use actix_web::{
middleware::{self, Logger},
web::{self},
App, HttpServer,
};
use dotenv::dotenv;
use tower::Layer;
use tower_http::{
compression::CompressionLayer,
normalize_path::NormalizePathLayer,
services::{ServeDir, ServeFile},
};
use log::error;
#[macro_use]
extern crate lazy_static;
mod api;
mod client;
mod config;
mod health;
mod note;
mod size;
mod status;
mod store;
#[tokio::main]
async fn main() {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
if !store::can_reach_redis() {
println!("cannot reach redis");
error!("cannot reach redis");
panic!("canont reach redis");
}
let notes_routes = Router::new()
.route("/", post(note::create))
.route("/:id", delete(note::delete))
.route("/:id", get(note::preview));
let health_routes = Router::new().route("/live", get(health::report_health));
let status_routes = Router::new().route("/status", get(status::get_status));
let api_routes = Router::new()
.nest("/notes", notes_routes)
.nest("/", health_routes)
.nest("/", status_routes);
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
let serve_dir =
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
let app = Router::new()
.nest("/api", api_routes)
.fallback_service(serve_dir)
.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();
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;
}

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
use crate::config;
use actix_web::web;
use mime;
pub fn init(cfg: &mut web::ServiceConfig) {
let json = web::JsonConfig::default().limit(*config::LIMIT);
let plain = web::PayloadConfig::default()
.limit(*config::LIMIT)
.mimetype(mime::STAR_STAR);
// cfg.app_data(plain);
cfg.app_data(json).app_data(plain);
}

View File

@@ -1,39 +1,5 @@
use crate::config;
use axum::{http::StatusCode, Json};
use serde::Serialize;
mod model;
mod routes;
#[derive(Serialize)]
pub struct Status {
// General
pub version: String,
// Config
pub max_size: u32,
pub max_views: u32,
pub max_expiration: u32,
pub allow_advanced: bool,
pub allow_files: bool,
// 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))
}
pub use model::*;
pub use routes::*;

View File

@@ -0,0 +1,17 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Status {
// General
pub version: String,
// Config
pub max_size: u32,
pub max_views: u32,
pub max_expiration: u32,
pub allow_advanced: bool,
// Theme
pub theme_image: String,
pub theme_text: String,
pub theme_page_title: String,
pub theme_favicon: String,
}

View File

@@ -0,0 +1,23 @@
use actix_web::{get, web, HttpResponse, Responder, Scope};
use crate::config;
use crate::status::Status;
#[get("/")]
async fn get_status() -> impl Responder {
return HttpResponse::Ok().json(Status {
version: config::VERSION.to_string(),
max_size: *config::LIMIT as u32,
max_views: *config::MAX_VIEWS,
max_expiration: *config::MAX_EXPIRATION,
allow_advanced: *config::ALLOW_ADVANCED,
theme_image: config::THEME_IMAGE.to_string(),
theme_text: config::THEME_TEXT.to_string(),
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
theme_favicon: config::THEME_FAVICON.to_string()
});
}
pub fn init() -> Scope {
web::scope("/status").service(get_status)
}

View File

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

View File

@@ -1,14 +0,0 @@
import pkg from './package.json' with { type: 'json' }
import { build } from 'tsup'
const watch = process.argv.slice(2)[0] === '--watch'
await build({
entry: ['src/index.ts', 'src/cli.ts'],
dts: true,
minify: true,
format: ['esm', 'cjs'],
clean: true,
define: { VERSION: `"${pkg.version}"` },
watch,
})

View File

@@ -1,44 +1,43 @@
{
"version": "2.3.1",
"name": "cryptgeon",
"version": "2.8.0",
"homepage": "https://github.com/cupcakearmy/cryptgeon",
"repository": {
"type": "git",
"url": "https://github.com/cupcakearmy/cryptgeon.git",
"directory": "packages/cli"
},
"homepage": "https://github.com/cupcakearmy/cryptgeon",
"type": "module",
"exports": {
".": "./dist/index.js"
"engines": {
"node": ">=18"
},
"types": "./dist/index.d.ts",
"scripts": {
"dev": "./scripts/build.js --watch",
"build": "./scripts/build.js",
"package": "./scripts/package.js",
"bin": "run-s build package",
"prepublishOnly": "run-s build"
},
"main": "./dist/index.cjs",
"bin": {
"cryptgeon": "./dist/cli.cjs"
"cryptgeon": "./dist/index.cjs"
},
"files": [
"dist"
],
"scripts": {
"bin": "run-s build package",
"build": "tsc && node build.js",
"dev": "node build.js --watch",
"prepublishOnly": "run-s build"
},
"devDependencies": {
"@commander-js/extra-typings": "^12.0.1",
"@commander-js/extra-typings": "^10.0.3",
"@cryptgeon/shared": "workspace:*",
"@types/inquirer": "^9.0.7",
"@types/mime": "^3.0.4",
"@types/node": "^20.11.24",
"commander": "^12.0.0",
"inquirer": "^9.2.15",
"mime": "^4.0.1",
"occulto": "^2.0.3",
"pretty-bytes": "^6.1.1",
"tsup": "^8.2.4",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18"
"@types/inquirer": "^9.0.3",
"@types/mime": "^3.0.1",
"@types/node": "^20.1.3",
"commander": "^10.0.1",
"esbuild": "^0.17.19",
"inquirer": "^9.2.2",
"mime": "^3.0.0",
"occulto": "^2.0.1",
"pkg": "^5.8.1",
"pretty-bytes": "^6.1.0",
"typescript": "^5.0.4"
}
}

17
packages/cli/scripts/build.js Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env node
import { build, context } from 'esbuild'
import pkg from '../package.json' assert { type: 'json' }
const options = {
entryPoints: ['./src/index.ts'],
bundle: true,
minify: true,
platform: 'node',
outfile: './dist/index.cjs',
define: { VERSION: `"${pkg.version}"` },
}
const watch = process.argv.slice(2)[0] === '--watch'
if (watch) (await context(options)).watch()
else await build(options)

17
packages/cli/scripts/package.js Executable file
View File

@@ -0,0 +1,17 @@
#!/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/index.cjs', '--target', target, '--output', `./bin/${target.replace('node18', 'cryptgeon')}`])
}

View File

@@ -1,106 +0,0 @@
#!/usr/bin/env node
import { Argument, Option, program } from '@commander-js/extra-typings'
import { setOptions, 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'
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer)
const files = new Argument('<file...>', 'Files to be sent').argParser(parseFile)
const text = new Argument('<text>', 'Text content of the note')
const password = new Option('-p --password <string>', 'manually set a password')
const all = new Option('-a --all', 'Save all files without prompt').default(false)
const url = new Argument('<url>', 'The url to open')
const views = new Option('-v --views <number>', 'Amount of views before getting destroyed').argParser(parseNumber)
const minutes = new Option('-m --minutes <number>', 'Minutes before the note expires').argParser(parseNumber)
// Node 18 guard
parseInt(process.version.slice(1).split(',')[0]) < 18 && exit('Node 18 or higher is required')
// @ts-ignore
const version: string = VERSION
program.name('cryptgeon').version(version).configureHelp({ showGlobalOptions: true })
program
.command('info')
.description('show information about the server')
.addOption(server)
.action(async (options) => {
setOptions({ server: options.server })
const response = await status()
const formatted = {
...response,
max_size: prettyBytes(response.max_size),
}
for (const key of Object.keys(formatted)) {
if (key.startsWith('theme_')) delete formatted[key as keyof typeof formatted]
}
console.table(formatted)
})
const send = program.command('send').description('send a note')
send
.command('file')
.addArgument(files)
.addOption(server)
.addOption(views)
.addOption(minutes)
.addOption(password)
.action(async (files, options) => {
setOptions({ server: options.server })
await checkConstrains(options)
options.password ||= await getStdin()
try {
const url = await upload(files, { views: options.views, expiration: options.minutes, password: options.password })
console.log(`Note created:\n\n${url}`)
} catch {
exit('Could not create note')
}
})
send
.command('text')
.addArgument(text)
.addOption(server)
.addOption(views)
.addOption(minutes)
.addOption(password)
.action(async (text, options) => {
setOptions({ server: options.server })
await checkConstrains(options)
options.password ||= await getStdin()
try {
const url = await upload(text, { views: options.views, expiration: options.minutes, password: options.password })
console.log(`Note created:\n\n${url}`)
} catch {
exit('Could not create note')
}
})
program
.command('open')
.description('open a link with text or files inside')
.addArgument(url)
.addOption(password)
.addOption(all)
.action(async (note, options) => {
try {
const url = new URL(note)
options.password ||= await getStdin()
try {
await download(url, options.all, options.password)
} catch (e) {
exit(e instanceof Error ? e.message : 'Unknown error occurred')
}
} catch {
exit('Invalid URL')
}
})
program.parse()

View File

@@ -1,16 +1,16 @@
import { Adapters, get, info, setOptions } from '@cryptgeon/shared'
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 { exit } from './utils'
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
setOptions({ server: url.origin })
setBase(url.origin)
const id = url.pathname.split('/')[2]
const preview = await info(id).catch(() => {
throw new Error('Note does not exist or is expired')
})
const preview = await info(id).catch(() => exit('Note does not exist or is expired'))
// Password
let password: string
@@ -35,14 +35,13 @@ 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 couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password')
const couldNotDecrypt = () => exit('Could not decrypt note. Probably an invalid password')
switch (note.meta.type) {
case 'file':
const files = await Adapters.Files.decrypt(note.contents, key).catch(() => {
throw couldNotDecrypt
})
const files = await Adapters.Files.decrypt(note.contents, key).catch(couldNotDecrypt)
if (!files) {
throw new Error('No files found in note')
exit('No files found in note')
return
}
let selected: typeof files
@@ -64,7 +63,7 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin
selected = files.filter((file) => names.includes(file.name))
}
if (!selected.length) throw new Error('No files selected')
if (!selected.length) exit('No files selected')
await Promise.all(
selected.map(async (file) => {
let filename = resolve(file.name)
@@ -80,9 +79,7 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin
break
case 'text':
const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(() => {
throw couldNotDecrypt
})
const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(couldNotDecrypt)
console.log(plaintext)
break
}

View File

@@ -1,4 +1,104 @@
export * from '@cryptgeon/shared'
export * from './download.js'
export * from './upload.js'
export * from './utils.js'
#!/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 { exit } from './utils.js'
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer)
const files = new Argument('<file...>', 'Files to be sent').argParser(parseFile)
const text = new Argument('<text>', 'Text content of the note')
const password = new Option('-p --password <string>', 'manually set a password')
const all = new Option('-a --all', 'Save all files without prompt').default(false)
const url = new Argument('<url>', 'The url to open')
const views = new Option('-v --views <number>', 'Amount of views before getting destroyed').argParser(parseNumber)
const minutes = new Option('-m --minutes <number>', 'Minutes before the note expires').argParser(parseNumber)
// Node 18 guard
parseInt(process.version.slice(1).split(',')[0]) < 18 && exit('Node 18 or higher is required')
// @ts-ignore
const version: string = VERSION
async function checkConstrains(constrains: { views?: number; minutes?: number }) {
const { views, minutes } = constrains
if (views && minutes) exit('cannot set view and minutes constrains simultaneously')
if (!views && !minutes) constrains.views = 1
const response = await status()
if (views && views > response.max_views)
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
if (minutes && minutes > response.max_expiration)
exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${minutes} given.`)
}
program.name('cryptgeon').version(version).configureHelp({ showGlobalOptions: true })
program
.command('info')
.description('show information about the server')
.addOption(server)
.action(async (options) => {
setBase(options.server)
const response = await status()
const formatted = {
...response,
max_size: prettyBytes(response.max_size),
}
for (const key of Object.keys(formatted)) {
if (key.startsWith('theme_')) delete formatted[key as keyof typeof formatted]
}
console.table(formatted)
})
const send = program.command('send').description('send a note')
send
.command('file')
.addArgument(files)
.addOption(server)
.addOption(views)
.addOption(minutes)
.addOption(password)
.action(async (files, options) => {
setBase(options.server!)
await checkConstrains(options)
options.password ||= await getStdin()
await upload(files, { views: options.views, expiration: options.minutes, password: options.password })
})
send
.command('text')
.addArgument(text)
.addOption(server)
.addOption(views)
.addOption(minutes)
.addOption(password)
.action(async (text, options) => {
setBase(options.server!)
await checkConstrains(options)
options.password ||= await getStdin()
await upload(text, { views: options.views, expiration: options.minutes, password: options.password })
})
program
.command('open')
.description('open a link with text or files inside')
.addArgument(url)
.addOption(password)
.addOption(all)
.action(async (note, options) => {
try {
const url = new URL(note)
options.password ||= await getStdin()
await download(url, options.all, options.password)
} catch {
exit('Invalid URL')
}
})
program.parse()

View File

@@ -1,45 +1,51 @@
import { readFile, stat } from 'node:fs/promises'
import { basename } from 'node:path'
import { Adapters, create, getOptions, FileDTO, Note, NoteMeta } from '@cryptgeon/shared'
import { Adapters, BASE, create, FileDTO, Note, NoteMeta } from '@cryptgeon/shared'
import mime from 'mime'
import { AES, Hex } from 'occulto'
import { AES, Hex, TypedArray } from 'occulto'
export type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
import { exit } from './utils.js'
export async function upload(input: string | string[], options: UploadOptions): Promise<string> {
const { password, ...noteOptions } = options
const derived = options.password ? await AES.derive(options.password) : undefined
const key = derived ? derived[0] : await AES.generateKey()
type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
let contents: string
let type: NoteMeta['type']
if (typeof input === 'string') {
contents = await Adapters.Text.encrypt(input, key)
type = 'text'
} else {
const files: FileDTO[] = await Promise.all(
input.map(async (path) => {
const data = new Uint8Array(await readFile(path))
const stats = await stat(path)
const extension = path.substring(path.indexOf('.') + 1)
const type = mime.getType(extension) ?? 'application/octet-stream'
return {
name: basename(path),
size: stats.size,
contents: data,
type,
} satisfies FileDTO
})
)
contents = await Adapters.Files.encrypt(files, key)
type = 'file'
export async function upload(input: string | string[], options: UploadOptions) {
try {
const { password, ...noteOptions } = options
const derived = options.password ? await AES.derive(options.password) : undefined
const key = derived ? derived[0] : await AES.generateKey()
let contents: string
let type: NoteMeta['type']
if (typeof input === 'string') {
contents = await Adapters.Text.encrypt(input, key)
type = 'text'
} else {
const files: FileDTO[] = await Promise.all(
input.map(async (path) => {
const data = new Uint8Array(await readFile(path))
const stats = await stat(path)
const extension = path.substring(path.indexOf('.') + 1)
const type = mime.getType(extension) ?? 'application/octet-stream'
return {
name: basename(path),
size: stats.size,
contents: data,
type,
} satisfies FileDTO
})
)
contents = await Adapters.Files.encrypt(files, key)
type = 'file'
}
// 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}`
if (!derived) url += `#${Hex.encode(key)}`
console.log(`Note created:\n\n${url}`)
} catch {
exit('Could not create note')
}
// Create the actual note and upload it.
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
const result = await create(note)
let url = `${getOptions().server}/note/${result.id}`
if (!derived) url += `#${Hex.encode(key)}`
return url
}

View File

@@ -1,19 +1,6 @@
import { status } from '@cryptgeon/shared'
import { exit as exitNode } from 'node:process'
export function exit(message: string) {
console.error(message)
exitNode(1)
}
export async function checkConstrains(constrains: { views?: number; minutes?: number }) {
const { views, minutes } = constrains
if (views && minutes) exit('cannot set view and minutes constrains simultaneously')
if (!views && !minutes) constrains.views = 1
const response = await status()
if (views && views > response.max_views)
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
if (minutes && minutes > response.max_expiration)
exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${minutes} given.`)
}

View File

@@ -3,11 +3,8 @@
"target": "es2022",
"module": "es2022",
"moduleResolution": "node",
"declaration": true,
"emitDeclarationOnly": true,
"noEmit": true,
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"allowSyntheticDefaultImports": true
}
}

View File

@@ -1,58 +1,56 @@
{
"common": {
"note": "Notiz",
"note": "Hinweis",
"file": "Datei",
"advanced": "Erweiterte Optionen",
"create": "Erstellen",
"loading": "Lädt...",
"advanced": "erweitert",
"create": "erstellen",
"loading": "läd",
"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": "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",
"copy_clipboard": "in die Zwischenablage kopieren",
"copied_to_clipboard": "in die Zwischenablage kopiert",
"encrypting": "verschlüsseln",
"decrypting": "entschlüsselt",
"uploading": "hochladen",
"downloading": "wird heruntergeladen",
"qr_code": "qr-code",
"password": "Passwort"
},
"home": {
"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).",
"intro": "Senden Sie ganz einfach <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.",
"explanation": "die Notiz verfällt und wird nach {type} zerstört.",
"new_note": "neue Note",
"new_note_notice": "<b>Verfügbarkeit:</b><br />es ist nicht garantiert, dass die Notiz gespeichert wird, da alles im Speicher gehalten wird. Wenn dieser voll ist, werden die ältesten Notizen entfernt.<br />(Sie werden wahrscheinlich keine Probleme haben, seien Sie nur gewarnt).",
"errors": {
"note_to_big": "Notiz konnte nicht erstellt werden, da sie zu groß ist.",
"note_error": "Notiz konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
"note_to_big": "Notiz konnte nicht erstellt werden. Notiz ist zu groß",
"note_error": "konnte keine Notiz erstellen. Bitte versuchen Sie es erneut.",
"max": "max: {n}",
"empty_content": "Notiz ist leer."
},
"messages": {
"note_created": "Notiz wurde erstellt."
"note_created": "notiz erstellt."
},
"advanced": {
"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"
"explanation": "Standardmäßig wird für jede Notiz ein sicher generiertes Passwort verwendet. Sie können jedoch auch ein eigenes Kennwort wählen, das nicht in dem 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. Die Notiz wurde daher gelöscht.",
"unsupported_type": "Nicht unterstützter Notiztyp."
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört.",
"unsupported_type": "nicht unterstützter Notiztyp."
},
"explanation": "Klicken Sie auf den Button, um die Notiz anzuzeigen und anschließend zu löschen, falls ein festgelegtes Limit erreicht wurde.",
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
"show_note": "Notiz anzeigen",
"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": "Gefundene Links in der Notiz:"
"warning_will_not_see_again": "haben Sie <b>keine</b> Gelegenheit, die Notiz noch einmal zu sehen.",
"download_all": "alle herunterladen"
},
"file_upload": {
"selected_files": "Ausgewählte Dateien",
"no_files_selected": "Keine Dateien ausgewählt",
"clear": "Zurücksetzen"
"no_files_selected": "Keine Dateien ausgewählt"
}
}

View File

@@ -23,9 +23,9 @@
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
"explanation": "the note will expire and be destroyed after {type}.",
"new_note": "new note",
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes will be removed.<br />(you probably will be fine, just be warned.)",
"new_note_notice": "<b>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, but just be warned.)",
"errors": {
"note_to_big": "could not create note. note is too 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."
@@ -34,7 +34,7 @@
"note_created": "note created."
},
"advanced": {
"explanation": "By default, a securely generated password is used for each note. You can however also choose your own password, which is not included in the link.",
"explanation": "By default, a securely generated password is used for each note. You can, however, also choose your own password, which is not included in the link.",
"custom_password": "custom password"
}
},
@@ -44,15 +44,13 @@
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
"unsupported_type": "unsupported note type."
},
"explanation": "click below to show and delete the note if the counter has reached its limit",
"explanation": "click below to show and delete the note if the counter has reached 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",
"links_found": "links found inside the note:"
"download_all": "download all"
},
"file_upload": {
"selected_files": "Selected Files",
"no_files_selected": "No Files Selected",
"clear": "Reset"
"no_files_selected": "No Files Selected"
}
}

View File

@@ -23,7 +23,7 @@
"intro": "Envía fácilmente notas o archivos <i>totalmente encriptados</i> y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.",
"explanation": "la nota expirará y se destruirá después de {type}.",
"new_note": "nueva nota",
"new_note_notice": "<b>disponibilidad:</b><br />no se garantiza que la nota se almacene, ya que todo se guarda en la memoria RAM, si se llena se eliminarán las notas más antiguas.<br />(probablemente estará bien, solo está advertido.)",
"new_note_notice": "<b>disponibilidad:</b><br />no se garantiza que la nota se almacene, ya que todo se guarda en la memoria RAM, si se llena se eliminarán las notas más antiguas.<br />(probablemente estará bien, sólo está advertido.)",
"errors": {
"note_to_big": "no se pudo crear la nota. la nota es demasiado grande",
"note_error": "No se ha podido crear la nota. Por favor, inténtelo de nuevo.",
@@ -34,7 +34,7 @@
"note_created": "nota creada."
},
"advanced": {
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, la cual no se incluye en el enlace.",
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, que no se incluye en el enlace.",
"custom_password": "contraseña personalizada"
}
},
@@ -47,12 +47,10 @@
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
"show_note": "mostrar nota",
"warning_will_not_see_again": "<b>no</b> tendrás la oportunidad de volver a ver la nota.",
"download_all": "descargar todo",
"links_found": "enlaces que se encuentran dentro de la nota:"
"download_all": "descargar todo"
},
"file_upload": {
"selected_files": "Archivos seleccionados",
"no_files_selected": "No hay archivos seleccionados",
"clear": "Restablecer"
"no_files_selected": "No hay archivos seleccionados"
}
}

View File

@@ -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": "chiffrer",
"encrypting": "cryptage",
"decrypting": "déchiffrer",
"uploading": "téléversement",
"uploading": "téléchargement",
"downloading": "téléchargement",
"qr_code": "code qr",
"password": "mot de passe"
},
"home": {
"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.",
"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.",
"explanation": "la note expirera et sera détruite après {type}.",
"new_note": "nouvelle note",
"new_note_notice": "<b>disponibilité :</b><br />il n'est pas garanti que la note reste stockée car tout est conservé dans la mémoire vive; si elle se remplit, les notes les plus anciennes seront supprimées.<br />(tout ira probablement bien, soyez juste averti.)",
"new_note_notice": "<b>disponibilité :</b><br />la note n'est pas garantie d'être stockée car tout est conservé dans la mémoire vive, si elle se remplit les notes les plus anciennes seront supprimées.<br />(vous serez 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,14 +45,12 @@
"unsupported_type": "type de note non supporté."
},
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
"show_note": "afficher la note",
"show_note": "note de présentation",
"warning_will_not_see_again": "vous <b>n'aurez pas</b> la chance de revoir la note.",
"download_all": "tout télécharger",
"links_found": "liens trouvés à lintérieur de la note :"
"download_all": "télécharger tout"
},
"file_upload": {
"selected_files": "Fichiers sélectionnés",
"no_files_selected": "Aucun fichier sélectionné",
"clear": "Réinitialiser"
"no_files_selected": "Aucun fichier sélectionné"
}
}

View File

@@ -47,12 +47,10 @@
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
"show_note": "mostra la nota",
"warning_will_not_see_again": "<b>non</b> avrete la possibilità di rivedere la nota.",
"download_all": "scarica tutti",
"links_found": "link presenti all'interno della nota:"
"download_all": "scarica tutti"
},
"file_upload": {
"selected_files": "File selezionati",
"no_files_selected": "Nessun file selezionato",
"clear": "Reset"
"no_files_selected": "Nessun file selezionato"
}
}

View File

@@ -47,12 +47,10 @@
"explanation": "カウンターが上限に達した場合、ノートの表示と削除を行うには、以下をクリックします。",
"show_note": "メモを表示",
"warning_will_not_see_again": "あなた <b>できません</b> このノートをもう一度見る",
"download_all": "すべてダウンロード",
"links_found": "メモ内にあるリンク:"
"download_all": "すべてダウンロード"
},
"file_upload": {
"selected_files": "選択したファイル",
"no_files_selected": "ファイルが選択されていません",
"clear": "リセット"
"no_files_selected": "ファイルが選択されていません"
}
}

View File

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

View File

@@ -1,58 +1,56 @@
{
"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": "Сброс"
}
}
{
"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": "скачать всё"
},
"file_upload": {
"selected_files": "Выбранные файлы",
"no_files_selected": "Файлы не выбраны"
}
}

View File

@@ -47,12 +47,10 @@
"explanation": "点击下方按钮即可查看密信,阅后即焚。",
"show_note": "查看密信",
"warning_will_not_see_again": "你将<b>无法</b>再次查看该密信,请尽快复制到粘贴板。",
"download_all": "下载全部",
"links_found": "注释中找到的链接:"
"download_all": "下载全部"
},
"file_upload": {
"selected_files": "已选中的文件",
"no_files_selected": "没有文件被选中",
"clear": "重置"
"no_files_selected": "没有文件被选中"
}
}

View File

@@ -13,28 +13,29 @@
},
"type": "module",
"devDependencies": {
"@lokalise/node-api": "^12.1.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/file-saver": "^2.0.7",
"@zerodevx/svelte-toast": "^0.9.5",
"@lokalise/node-api": "^9.8.0",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.16.3",
"@types/dompurify": "^3.0.2",
"@types/file-saver": "^2.0.5",
"@zerodevx/svelte-toast": "^0.9.3",
"adm-zip": "^0.5.10",
"dotenv": "^16.4.5",
"svelte": "^4.2.12",
"svelte-check": "^3.6.6",
"svelte-intl-precompile": "^0.12.3",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.1.7"
"dotenv": "^16.0.3",
"svelte": "^3.59.1",
"svelte-check": "^3.3.2",
"svelte-intl-precompile": "^0.12.1",
"tslib": "^2.5.0",
"typescript": "^5.0.4",
"vite": "^4.3.9"
},
"dependencies": {
"@cryptgeon/shared": "workspace:*",
"@fontsource/fira-mono": "^5.0.8",
"@fontsource/fira-mono": "^4.5.10",
"copy-to-clipboard": "^3.3.3",
"dompurify": "^3.0.3",
"file-saver": "^2.0.5",
"occulto": "^2.0.3",
"pretty-bytes": "^6.1.1",
"occulto": "^2.0.1",
"pretty-bytes": "^6.1.0",
"qrious": "^4.0.2"
}
}

View File

@@ -46,7 +46,7 @@
</div>
{/each}
<div class="spacer" />
<Button on:click={clear}>{$t('file_upload.clear')}</Button>
<Button on:click={clear}>Clear</Button>
</div>
{:else}
<div>

View File

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

View File

@@ -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,11 +35,9 @@
<Canvas value={url} />
</div>
{#if $status?.theme_new_note_notice}
<p>
{@html $t('home.new_note_notice')}
</p>
{/if}
<p>
{@html $t('home.new_note_notice')}
</p>
<br />
<Button on:click={reset}>{$t('home.new_note')}</Button>

View File

@@ -3,8 +3,8 @@
</script>
<script lang="ts">
import pkg from 'file-saver'
const { saveAs } = pkg
import DOMPurify from 'dompurify'
import { saveAs } from 'file-saver'
import prettyBytes from 'pretty-bytes'
import { t } from 'svelte-intl-precompile'
@@ -34,29 +34,22 @@
saveAs(f)
}
$: links = typeof note.contents === 'string' ? note.contents.match(RE_URL) : []
function contentWithLinks(content: string): string {
const replaced = content.replace(
RE_URL,
(url) => `<a href="${url}" rel="noreferrer">${url}</a>`
)
return DOMPurify.sanitize(replaced, { USE_PROFILES: { html: true } })
}
</script>
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
<div data-testid="result">
{#if note.meta.type === 'text'}
<div class="note">
{note.contents}
{@html contentWithLinks(note.contents)}
</div>
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
{#if links && links.length}
<div class="links">
{$t('show.links_found')}
<ul>
{#each links as link}
<li>
<a href={link} target="_blank" rel="noopener noreferrer">{link}</a>
</li>
{/each}
</ul>
</div>
{/if}
{:else}
{#each files as file}
<div class="note file">
@@ -99,20 +92,4 @@
.note.file small {
padding-left: 1rem;
}
.links {
margin-top: 2rem;
}
.links ul {
margin: 0;
padding: 0;
margin-top: 0.5rem;
padding-left: 1rem;
list-style: square;
}
.links ul li {
margin-bottom: 0.5rem;
word-wrap: break-word;
}
</style>

View File

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

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { AES, Hex } from 'occulto'
import { AES, Hex, Bytes } from 'occulto'
import { t } from 'svelte-intl-precompile'
import { blur } from 'svelte/transition'
@@ -14,7 +14,7 @@
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, create, PayloadToLargeError } from '@cryptgeon/shared'
let note: Note = {
contents: '',
@@ -118,14 +118,12 @@
{/if}
<div class="bottom">
{#if $status?.allow_files}
<Switch
data-testid="switch-file"
class="file"
label={$t('common.file')}
bind:value={isFile}
/>
{/if}
<Switch
data-testid="switch-file"
class="file"
label={$t('common.file')}
bind:value={isFile}
/>
{#if $status?.allow_advanced}
<Switch
data-testid="switch-advanced"
@@ -151,7 +149,7 @@
</p>
{#if advanced}
<div transition:blur|global={{ duration: 250 }}>
<div transition:blur={{ duration: 250 }}>
<hr />
<AdvancedParameters bind:note bind:timeExpiration bind:customPassword />
</div>

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import adapter from '@sveltejs/adapter-static'
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
import { vitePreprocess } from '@sveltejs/kit/vite'
export default {
preprocess: vitePreprocess([precompileIntl('locales')]),

View File

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

View File

@@ -14,9 +14,9 @@
"build": "tsc"
},
"devDependencies": {
"typescript": "^5.3.3"
"typescript": "^5.0.4"
},
"dependencies": {
"occulto": "^2.0.3"
"occulto": "^2.0.1"
}
}

View File

@@ -23,10 +23,6 @@ export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
contents: string
}
type ClientOptions = {
server: string
}
type CallOptions = {
url: string
method: string
@@ -35,21 +31,14 @@ type CallOptions = {
export class PayloadToLargeError extends Error {}
export let client: ClientOptions = {
server: '',
}
export let BASE = ''
export function setOptions(options: Partial<ClientOptions>) {
client = { ...client, ...options }
}
export function getOptions(): ClientOptions {
return client
export function setBase(url: string) {
BASE = url
}
export async function call(options: CallOptions) {
const url = client.server + '/api/' + options.url
const response = await fetch(url, {
const response = await fetch(BASE + '/api/' + options.url, {
method: options.method,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
mode: 'cors',
@@ -116,7 +105,6 @@ export type Status = {
theme_text: string
theme_favicon: string
theme_page_title: string
theme_new_note_notice: boolean
}
export async function status() {

View File

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

5540
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,7 +2,7 @@ import { test } from '@playwright/test'
import { CLI, getLinkFromCLI } from '../../utils'
test.describe('text @cli', () => {
test('simple', async () => {
test('simple', async ({ page }) => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const note = await CLI('send', 'text', text)
const link = getLinkFromCLI(note.stdout)
@@ -11,7 +11,7 @@ test.describe('text @cli', () => {
test.expect(retrieved.stdout.trim()).toBe(text)
})
test('simple with password', async () => {
test('simple with password', async ({ page }) => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying.`
const password = 'password'
const note = await CLI('send', 'text', text, '--password', password)

View File

@@ -1,8 +1,7 @@
import { test } from '@playwright/test'
import { rm } from 'node:fs/promises'
import { basename } from 'node:path'
import { Files, getFileChecksum, tmpFile } from '../../files'
import { CLI, checkLinkForDownload, createNoteSuccessfully, getLinkFromCLI } from '../../utils'
import { CLI, checkLinkForDownload, checkLinkForText, createNote, getLinkFromCLI } from '../../utils'
import { Files, getFileChecksum, rm, tmpFile } from '../../files'
import { basename } from 'path'
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const password = 'password'
@@ -31,7 +30,7 @@ test.describe('text @cross', () => {
test('web to cli', async ({ page }) => {
const files = [Files.Image]
const checksum = await getFileChecksum(files[0])
const link = await createNoteSuccessfully(page, { files })
const link = await createNote(page, { files })
const filename = basename(files[0])
await CLI('open', link, '--all')
@@ -43,7 +42,7 @@ test.describe('text @cross', () => {
test('web to cli with password', async ({ page }) => {
const files = [Files.Image]
const checksum = await getFileChecksum(files[0])
const link = await createNoteSuccessfully(page, { files, password })
const link = await createNote(page, { files, password })
const filename = basename(files[0])
await CLI('open', link, '--all', '--password', password)

View File

@@ -1,5 +1,5 @@
import { test } from '@playwright/test'
import { CLI, checkLinkForText, createNoteSuccessfully, getLinkFromCLI } from '../../utils'
import { CLI, checkLinkForText, createNote, getLinkFromCLI } from '../../utils'
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const password = 'password'
@@ -13,7 +13,7 @@ test.describe('text @cross', () => {
})
test('web to cli', async ({ page }) => {
const link = await createNoteSuccessfully(page, { text })
const link = await createNote(page, { text })
const retrieved = await CLI('open', link)
test.expect(retrieved.stdout.trim()).toBe(text)
})
@@ -25,7 +25,7 @@ test.describe('text @cross', () => {
})
test('web to cli with password', async ({ page }) => {
const link = await createNoteSuccessfully(page, { text, password })
const link = await createNote(page, { text, password })
const retrieved = await CLI('open', link, '--password', password)
test.expect(retrieved.stdout.trim()).toBe(text)
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
import { test } from '@playwright/test'
import { checkLinkForText, createNoteSuccessfully } from '../../utils'
import { checkLinkForText, createNote } from '../../utils'
test.describe('@web', () => {
test('simple', async ({ page }) => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of deceive play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const link = await createNoteSuccessfully(page, { text })
const link = await createNote(page, { text })
await checkLinkForText(page, { link, text })
})
test('simple with password', async ({ page }) => {
const text = 'Foo bar'
const password = '123'
const shareLink = await createNoteSuccessfully(page, { text, password })
const shareLink = await createNote(page, { text, password })
await checkLinkForText(page, { link: shareLink, text, password })
})
})

View File

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