Compare commits

...
Author SHA1 Message Date
cupcakearmy 3cbe9fabf2 update docker image 2026-05-31 23:44:28 +02:00
cupcakearmy 2923aab916 cleanup docs 2026-05-31 23:41:02 +02:00
cupcakearmy b5102dc647 cleanup docs 2026-05-31 23:40:48 +02:00
cupcakearmy 12d8b87cc1 cleanup readme 2026-05-31 23:31:36 +02:00
cupcakearmy f42662812f cleanup translations 2026-05-31 23:31:31 +02:00
cupcakearmy a551b16216 frontend cleanup 2026-05-31 23:24:55 +02:00
cupcakearmy 09840dcf0a frontend cleanup 2026-05-31 23:24:46 +02:00
cupcakearmy 24e99b84e0 update gihub action 2026-05-31 22:57:28 +02:00
cupcakearmy 690b955d5d watchexec & breaking changes in axum 0.8 2026-05-31 22:55:11 +02:00
cupcakearmy 9b0155dc9a use mise 2026-05-31 22:24:30 +02:00
cupcakearmy 0a56c4c572 maintenance 2026-05-31 22:24:20 +02:00
cupcakearmyandGitHub f6bf8c656c Merge pull request #202 from smeinecke/update/redis-to-valkey
Replace Redis with Valkey in docker-compose files
2026-05-31 21:40:46 +02:00
Stefan Meinecke 1a243cc96a Replace Redis with Valkey in docker-compose files and fix Rust 2024 compat
Swap redis:7-alpine images to valkey/valkey:7-alpine across all
docker-compose files and example READMEs. Keep service name as
"redis" so that the default REDIS=redis://redis/ connection string
still resolves correctly in Docker networking.

Also add turbofish type annotations to redis crate calls in store.rs
for Rust 2024 never-type-fallback compatibility, and fix a typo
("notion" -> "note") in an error message.
2026-05-12 18:40:41 +02:00
cupcakearmyandGitHub 482795dd9a Merge pull request #181 from cupcakearmy/dependabot/cargo/packages/backend/cargo-362f336499
Bump ring from 0.16.20 to 0.17.12 in /packages/backend in the cargo group across 1 directory
2025-03-08 10:34:30 +01:00
dependabot[bot]andGitHub 2907e7c002 Bump ring in /packages/backend in the cargo group across 1 directory
Bumps the cargo group with 1 update in the /packages/backend directory: [ring](https://github.com/briansmith/ring).


Updates `ring` from 0.16.20 to 0.17.12
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 16:49:50 +00:00
cupcakearmyandGitHub 4cc9d8a758 Merge pull request #179 from larsgerber/main
docs(compose): prevent anonymous volume creation
2025-03-02 21:50:52 +01:00
Lars Gerber d652c4ee1e docs(compose): prevent anonymous volume creation 2025-03-01 21:28:34 +01:00
cupcakearmyandGitHub 096be03966 Merge pull request #178 from cupcakearmy/176-ram-only
update docs about ram only redis
2025-02-27 20:13:09 +01:00
cupcakearmyandGitHub c53cde6886 Merge branch 'main' into 176-ram-only 2025-02-27 20:06:56 +01:00
cupcakearmy 0fa5a35dae update some versions 2025-02-27 20:00:38 +01:00
cupcakearmy ebbb4efa04 version bump 2025-02-27 19:40:45 +01:00
cupcakearmyandGitHub a248440bfd Merge pull request #177 from cupcakearmy/password-eye-toggle
fix: password eye toggle not working
2025-02-27 19:38:37 +01:00
cupcakearmy a1db60d159 update docs about ram only redis 2025-02-27 19:38:07 +01:00
cupcakearmy c2653bee84 fix: password eye toggle not working 2025-02-27 19:28:16 +01:00
cupcakearmyandGitHub a2d2acc5de Merge pull request #168 from cupcakearmy/dependabot/npm_and_yarn/npm_and_yarn-545022be4d
Bump vite from 6.0.7 to 6.0.9 in the npm_and_yarn group across 1 directory
2025-01-22 17:58:41 +01:00
dependabot[bot]andGitHub 4cc821150d Bump vite in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.0.7 to 6.0.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/v6.0.9/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-21 21:00:55 +00:00
cupcakearmyandGitHub e7fb844f66 Merge pull request #167 from cupcakearmy/svelte-5
some frontend love
2025-01-18 14:33:46 +01:00
cupcakearmy c13e53404c add csp draft 2025-01-17 18:48:28 +01:00
cupcakearmy 82862f0e3e version bump 2025-01-17 18:13:49 +01:00
cupcakearmy e20f4626e7 Merge remote-tracking branch 'origin/main' into svelte-5 2025-01-17 18:11:34 +01:00
cupcakearmy e440e4b7e0 update to svelte 5 2025-01-17 18:11:26 +01:00
cupcakearmy 808d846737 remove proxy 2025-01-17 18:11:03 +01:00
57 changed files with 3226 additions and 3072 deletions
+9 -9
View File
@@ -10,10 +10,10 @@ jobs:
cli: cli:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v6
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
cache: 'pnpm' cache: 'pnpm'
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
@@ -31,14 +31,14 @@ jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v2 - uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v2 - uses: docker/setup-buildx-action@v4
with: with:
install: true install: true
- name: Docker Labels - name: Docker Labels
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v6
with: with:
images: cupcakearmy/cryptgeon images: cupcakearmy/cryptgeon
tags: | tags: |
@@ -46,12 +46,12 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v7
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
+3 -3
View File
@@ -13,15 +13,15 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Node # Node
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v6
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
cache: 'pnpm' cache: 'pnpm'
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
# Docker # Docker
- uses: docker/setup-qemu-action@v3 - uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v4
with: with:
install: true install: true
+1 -1
View File
@@ -1 +1 @@
v22.7.0 v24
+47
View File
@@ -0,0 +1,47 @@
# Contributing
## Requirements
- [mise](https://mise.jdx.dev) — manages pnpm, rust, node (see `mise.toml`)
- docker or [colima](https://github.com/abiosoft/colima) (for redis)
## Setup
```bash
mise install
pnpm install
```
## Development
```bash
pnpm run dev
```
Make sure docker/colima is running. This starts redis, the rust backend, the web client, and the CLI. The app is at [localhost:3000](http://localhost:3000).
## Tests
End-to-end tests with Playwright.
```sh
pnpm run test:prepare
pnpm run test:local
```
## Release
1. Update version across packages:
```sh
./version.mjs <semver>
```
2. Create and push the tag:
```sh
git tag v<semver>
git push --tags
```
The CI workflow publishes the CLI to npm and the Docker image to Docker Hub automatically.
+3 -3
View File
@@ -1,5 +1,5 @@
# FRONTEND # FRONTEND
FROM node:22-alpine as client FROM node:24-alpine AS client
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN corepack enable
@@ -11,7 +11,7 @@ RUN pnpm run build
# BACKEND # BACKEND
FROM rust:1.80-alpine as backend FROM rust:1.95-alpine AS backend
WORKDIR /tmp WORKDIR /tmp
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
COPY ./packages/backend ./ COPY ./packages/backend ./
@@ -19,7 +19,7 @@ RUN RUSTFLAGS="-Ctarget-feature=-crt-static" cargo build --release
# RUNNER # RUNNER
FROM alpine:3.19 FROM alpine:3
WORKDIR /app WORKDIR /app
RUN apk add --no-cache curl libgcc RUN apk add --no-cache curl libgcc
COPY --from=backend /tmp/target/release/cryptgeon . COPY --from=backend /tmp/target/release/cryptgeon .
+9 -48
View File
@@ -106,9 +106,14 @@ version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise. # Set a size limit. See link below on how to customise.
# https://redis.io/docs/manual/eviction/ # https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru # --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
@@ -157,53 +162,9 @@ There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-sy
- Italian by [@nicfab](https://notes.nicfab.eu/it/posts/cryptgeon/) - Italian by [@nicfab](https://notes.nicfab.eu/it/posts/cryptgeon/)
- English by [@nicfab](https://notes.nicfab.eu/en/posts/cryptgeon/) - English by [@nicfab](https://notes.nicfab.eu/en/posts/cryptgeon/)
## Development ## Contributing
**Requirements** See [CONTRIBUTING.md](./CONTRIBUTING.md).
- `pnpm`: `>=9`
- `node`: `>=22`
- `rust`: edition `2021`
**Install**
```bash
pnpm install
# Also you need cargo watch if you don't already have it installed.
# https://lib.rs/crates/cargo-watch
cargo install cargo-watch
```
**Run**
Make sure you have docker running.
```bash
pnpm run dev
```
Running `pnpm run dev` in the root folder will start the following things:
- redis docker container
- rust backend
- client
- cli
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)
### Tests
Tests are end to end tests written with Playwright.
```sh
pnpm run test:prepare
# Use the test or test:local script. The local version only runs in one browser for quicker development.
pnpm run test:local
```
## Security ## Security
+15 -54
View File
@@ -43,7 +43,7 @@ Puedes revisar la documentación sobre el CLI en este [readme](./packages/cli/RE
- enviar texto o archivos - enviar texto o archivos
- el servidor no puede desencriptar el contenido debido a que la encriptación se hace del lado del cliente - el servidor no puede desencriptar el contenido debido a que la encriptación se hace del lado del cliente
- restriccion de vistas o de tiempo - restricción de vistas o de tiempo
- en memoria, sin persistencia - en memoria, sin persistencia
- compatibilidad obligatoria con el modo oscuro - compatibilidad obligatoria con el modo oscuro
@@ -66,7 +66,7 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
| `MAX_VIEWS` | `100` | Número máximo de vistas. | | `MAX_VIEWS` | `100` | Número máximo de vistas. |
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. | | `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. | | `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. | | `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es útil para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` | | `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. | | `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. | | `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
@@ -75,25 +75,30 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
## Despliegue ## Despliegue
> ️ Se requiere `https` de lo contrario el navegador no soportará las funciones de encriptacón. > ️ Se requiere `https` de lo contrario el navegador no soportará las funciones de encriptación.
> ️ Hay un endpoint para verificar el estado, lo encontramos en `/api/health/`. Regresa un código 200 o 503. > ️ Hay un endpoint para verificar el estado, lo encontramos en `/api/health/`. Regresa un código 200 o 503.
### Docker ### Docker
Docker es la manera más fácil. Aquí encontramos [la imágen oficial](https://hub.docker.com/r/cupcakearmy/cryptgeon). Docker es la manera más fácil. Aquí encontramos [la imagen oficial](https://hub.docker.com/r/cupcakearmy/cryptgeon).
```yaml ```yaml
# docker-compose.yml # docker-compose.yml
version: '3.8' version: "3.8"
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise. # Set a size limit. See link below on how to customise.
# https://redis.io/docs/manual/eviction/ # https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru # --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
@@ -136,57 +141,13 @@ Hay una [guía](https://mariushosting.com/how-to-install-cryptgeon-on-your-synol
- En inglés, por [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE) - En inglés, por [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
- En alemán, por [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg) - En alemán, por [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
## Desarrollo ## Contribuir
**Requisitos** Ver [CONTRIBUTING.md](./CONTRIBUTING.md).
- `pnpm`: `>=6`
- `node`: `>=18`
- `rust`: edition `2021`
**Instalación**
```bash
pnpm install
# También necesitas cargo-watch, si no lo tienes instalado.
# https://lib.rs/crates/cargo-watch
cargo install cargo-watch
```
**Ejecutar**
Asegurate de que docker se esté ejecutando.
```bash
pnpm run dev
```
Ejecutando `pnpm run dev` en la carpeta raíz iniciará lo siguiente:
- redis docker container
- rust backend
- client
- cli
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)
### Tests
Los tests son end-to-end tests escritos con Playwright.
```sh
pnpm run test:prepare
# Usa el script test o test:local. La versión local solo corre en el navegador para acelerar el desarrollo.
pnpm run test:local
```
## Seguridad ## Seguridad
Por favor dirigite a la sección de seguridad [aquí](./SECURITY.md). Por favor dirígete a la sección de seguridad [aquí](./SECURITY.md).
--- ---
+21 -51
View File
@@ -48,7 +48,7 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
## 环境变量 ## 环境变量
| 变量名称 | 默认值 | 描述 | | 变量名称 | 默认值 | 描述 |
| ----------------- | ---------------- | --------------------------------------------------------------------------------- | | ---------------- | ---------------- | --------------------------------------------------------------------------------- |
| `REDIS` | `redis://redis/` | Redis 连接 URL。 | | `REDIS` | `redis://redis/` | Redis 连接 URL。 |
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) | | `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 | | `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
@@ -69,11 +69,19 @@ Docker 是最简单的部署方式。这里是[官方镜像的地址](https://hu
```yaml ```yaml
# docker-compose.yml # docker-compose.yml
version: '3.8' version: "3.8"
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
@@ -99,7 +107,7 @@ services:
- 域名 `example.org` - 域名 `example.org`
```yaml ```yaml
version: '3.8' version: "3.8"
networks: networks:
proxy: proxy:
@@ -108,7 +116,14 @@ networks:
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
restart: unless-stopped # This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
@@ -125,54 +140,9 @@ services:
- traefik.http.routers.cryptgeon.tls.certresolver=le - traefik.http.routers.cryptgeon.tls.certresolver=le
``` ```
## 开发 ## 贡献
**环境要求** 参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
- `pnpm`: `>=6`
- `node`: `>=14`
- `rust`: edition `2021`
**安装**
```bash
pnpm install
pnpm --prefix frontend install
# 你还需要安装CargoWatch.
# https://lib.rs/crates/cargo-watch
cargo install cargo-watch
```
**运行**
确保你的 Docker 正在运行
```bash
pnpm run dev
```
在根目录执行 `pnpm run dev` 会开启下列服务:
- 一个 redis docker 容器
- 无热重载的 rust 后端
- 可热重载的客户端
你可以通过 3000 端口进入该应用,即 [localhost:3000](http://localhost:3000).
## 测试
这些测试是用 Playwright 实现的一些端到端测试用例。
```sh
pnpm run test:prepare
docker compose up redis -d
pnpm run test:server
# 在另一个终端中:
# 使用test或者test:local script。为了更快的开发,本地版本只会在一个浏览器中运行。
pnpm run test:local
```
###### Attributions ###### Attributions
+9 -1
View File
@@ -3,7 +3,15 @@
services: services:
redis: redis:
image: redis:7-alpine image: valkey/valkey:7-alpine
# This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
ports: ports:
- 6379:6379 - 6379:6379
+8 -3
View File
@@ -1,9 +1,14 @@
services: services:
redis: redis:
image: redis:7-alpine image: valkey/valkey:7-alpine
# This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise. # Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/ # https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru # --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
+9 -1
View File
@@ -2,7 +2,15 @@ version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: valkey/valkey:7-alpine
# This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
+9 -2
View File
@@ -108,8 +108,15 @@ networks:
services: services:
redis: redis:
image: redis:7-alpine image: valkey/valkey:7-alpine
restart: unless-stopped # This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
+18 -3
View File
@@ -17,8 +17,15 @@ networks:
services: services:
redis: redis:
image: redis:7-alpine image: valkey/valkey:7-alpine
restart: unless-stopped # This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
@@ -54,7 +61,15 @@ services:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
redis: redis:
image: redis:7-alpine image: valkey/valkey:7-alpine
# This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
cryptgeon: cryptgeon:
image: cupcakearmy/cryptgeon image: cupcakearmy/cryptgeon
+8
View File
@@ -0,0 +1,8 @@
[tools]
pnpm = "11.5.0"
rust = "1.95"
watchexec = "latest"
# Node loaded below from .nvmrc
[settings]
idiomatic_version_file_enable_tools = ["node"]
+5 -4
View File
@@ -8,14 +8,15 @@
"test": "playwright test --project=chrome --project=firefox --project=safari", "test": "playwright test --project=chrome --project=firefox --project=safari",
"test:local": "playwright test --project=chrome", "test:local": "playwright test --project=chrome",
"test:server": "run-s docker:up", "test:server": "run-s docker:up",
"test:prepare": "run-p build docker:build", "test:dl-browsers": "playwright install",
"test:prepare": "run-p test:dl-browsers build docker:build",
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build" "build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.46.1", "@playwright/test": "^1.60.0",
"@types/node": "^22.5.0", "@types/node": "^24.12.4",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"shelljs": "^0.8.5" "shelljs": "^0.8.5"
}, },
"packageManager": "pnpm@9.11.0" "packageManager": "pnpm@11.5.0"
} }
+610 -465
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -1,9 +1,9 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "2.8.4" version = "2.9.1"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021" edition = "2024"
rust-version = "1.80" rust-version = "1.95"
[[bin]] [[bin]]
name = "cryptgeon" name = "cryptgeon"
@@ -11,17 +11,17 @@ path = "src/main.rs"
[dependencies] [dependencies]
# Core # Core
axum = "0.7.5" axum = "0.8"
serde = { version = "1.0.208", features = ["derive"] } serde = { version = "1", features = ["derive"] }
tokio = { version = "1.39.3", features = ["full"] } tokio = { version = "1", features = ["full"] }
tower = "0.5.0" tower = "0.5"
tower-http = { version = "0.5.2", features = ["full"] } tower-http = { version = "0.6", features = ["full"] }
redis = { version = "0.25.2", features = ["tls-native-tls"] } redis = { version = "1", features = ["tls-native-tls"] }
# Utility # Utility
serde_json = "1" serde_json = "1"
lazy_static = "1" lazy_static = "1"
ring = "0.16" ring = "0.17"
bs62 = "0.1" bs62 = "0.1"
byte-unit = "4" byte-unit = "4"
dotenv = "0.15" dotenv = "0.15"
+1 -1
View File
@@ -2,7 +2,7 @@
"private": true, "private": true,
"name": "@cryptgeon/backend", "name": "@cryptgeon/backend",
"scripts": { "scripts": {
"dev": "cargo watch -x 'run --bin cryptgeon'", "dev": "watchexec -r -e rs cargo run",
"build": "cargo build --release", "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:3000 cargo run",
"test:prepare": "cargo build" "test:prepare": "cargo build"
+16
View File
@@ -0,0 +1,16 @@
use axum::{body::Body, extract::Request, http::HeaderValue, middleware::Next, response::Response};
const CUSTOM_HEADER_NAME: &str = "Content-Security-Policy";
const CUSTOM_HEADER_VALUE: &str = "default-src 'self'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; object-src 'none'; base-uri 'self'; connect-src 'self' data:; font-src 'self'; frame-src 'self'; img-src 'self'; manifest-src 'self'; media-src 'self'; worker-src 'none';";
lazy_static! {
static ref HEADER_VALUE: HeaderValue = HeaderValue::from_static(CUSTOM_HEADER_VALUE);
}
pub async fn add_csp_header(request: Request<Body>, next: Next) -> Response {
let mut response = next.run(request).await;
response
.headers_mut()
.append(CUSTOM_HEADER_NAME, HEADER_VALUE.clone());
response
}
+8 -5
View File
@@ -1,9 +1,9 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use axum::{ use axum::{
Router, ServiceExt,
extract::{DefaultBodyLimit, Request}, extract::{DefaultBodyLimit, Request},
routing::{delete, get, post}, routing::{delete, get, post},
Router, ServiceExt,
}; };
use dotenv::dotenv; use dotenv::dotenv;
use lock::SharedState; use lock::SharedState;
@@ -19,6 +19,7 @@ use tower_http::{
extern crate lazy_static; extern crate lazy_static;
mod config; mod config;
mod csp;
mod health; mod health;
mod lock; mod lock;
mod note; mod note;
@@ -40,14 +41,14 @@ async fn main() {
let notes_routes = Router::new() let notes_routes = Router::new()
.route("/", post(note::create)) .route("/", post(note::create))
.route("/:id", delete(note::delete)) .route("/{id}", delete(note::delete))
.route("/:id", get(note::preview)); .route("/{id}", get(note::preview));
let health_routes = Router::new().route("/live", get(health::report_health)); let health_routes = Router::new().route("/live", get(health::report_health));
let status_routes = Router::new().route("/status", get(status::get_status)); let status_routes = Router::new().route("/status", get(status::get_status));
let api_routes = Router::new() let api_routes = Router::new()
.nest("/notes", notes_routes) .nest("/notes", notes_routes)
.nest("/", health_routes) .merge(health_routes)
.nest("/", status_routes); .merge(status_routes);
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html"); let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
let serve_dir = let serve_dir =
@@ -55,6 +56,8 @@ async fn main() {
let app = Router::new() let app = Router::new()
.nest("/api", api_routes) .nest("/api", api_routes)
.fallback_service(serve_dir) .fallback_service(serve_dir)
// Disabled for now, as svelte inlines scripts
// .layer(middleware::from_fn(csp::add_csp_header))
.layer(DefaultBodyLimit::max(*config::LIMIT)) .layer(DefaultBodyLimit::max(*config::LIMIT))
.layer( .layer(
CompressionLayer::new() CompressionLayer::new()
+4 -4
View File
@@ -31,13 +31,13 @@ pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
let serialized = serde_json::to_string(&note.clone()).unwrap(); let serialized = serde_json::to_string(&note.clone()).unwrap();
let mut conn = get_connection()?; let mut conn = get_connection()?;
conn.set(id, serialized) conn.set::<_, _, ()>(id, serialized)
.map_err(|_| "Unable to set note in redis")?; .map_err(|_| "Unable to set note in redis")?;
match note.expiration { match note.expiration {
Some(e) => { Some(e) => {
let seconds = e - now(); let seconds = e - now();
conn.expire(id, seconds as i64) conn.expire::<_, ()>(id, seconds as i64)
.map_err(|_| "Unable to set expiration on notion")? .map_err(|_| "Unable to set expiration on note")?
} }
None => {} None => {}
}; };
@@ -58,6 +58,6 @@ pub fn get(id: &String) -> Result<Option<Note>, &'static str> {
pub fn del(id: &String) -> Result<(), &'static str> { pub fn del(id: &String) -> Result<(), &'static str> {
let mut conn = get_connection()?; let mut conn = get_connection()?;
conn.del(id).map_err(|_| "Unable to delete note in redis")?; conn.del::<_, ()>(id).map_err(|_| "Unable to delete note in redis")?;
Ok(()) Ok(())
} }
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "cryptgeon", "name": "cryptgeon",
"version": "2.8.4", "version": "2.9.1",
"homepage": "https://github.com/cupcakearmy/cryptgeon", "homepage": "https://github.com/cupcakearmy/cryptgeon",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -30,16 +30,16 @@
}, },
"devDependencies": { "devDependencies": {
"@commander-js/extra-typings": "^12.1.0", "@commander-js/extra-typings": "^12.1.0",
"@types/inquirer": "^9.0.7", "@types/inquirer": "^9.0.9",
"@types/mime": "^4.0.0", "@types/mime": "^4.0.0",
"@types/node": "^20.11.24", "@types/node": "^20.19.41",
"commander": "^12.1.0", "commander": "^12.1.0",
"inquirer": "^9.2.15", "inquirer": "^9.3.8",
"mime": "^4.0.1", "mime": "^4.1.0",
"occulto": "^2.0.6", "occulto": "^2.0.6",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"tsup": "^8.2.4", "tsup": "^8.5.1",
"typescript": "^5.3.3" "typescript": "^5.9.3"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
+1 -1
View File
@@ -5,7 +5,7 @@
"advanced": "avanzato", "advanced": "avanzato",
"create": "crea", "create": "crea",
"loading": "carica", "loading": "carica",
"mode": "modalita", "mode": "modalità",
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}", "views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}", "minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
"max": "max", "max": "max",
+2 -2
View File
@@ -17,7 +17,7 @@
"uploading": "アップロード中", "uploading": "アップロード中",
"downloading": "ダウンロード中", "downloading": "ダウンロード中",
"qr_code": "QRコード", "qr_code": "QRコード",
"password": "暗号" "password": "パスワード"
}, },
"home": { "home": {
"intro": "<i>完全に暗号化された</i> 、安全なメモやファイルをワンクリックで簡単に送信できます。メモを作成してリンクを共有するだけです。", "intro": "<i>完全に暗号化された</i> 、安全なメモやファイルをワンクリックで簡単に送信できます。メモを作成してリンクを共有するだけです。",
@@ -46,7 +46,7 @@
}, },
"explanation": "カウンターが上限に達した場合、ノートの表示と削除を行うには、以下をクリックします。", "explanation": "カウンターが上限に達した場合、ノートの表示と削除を行うには、以下をクリックします。",
"show_note": "メモを表示", "show_note": "メモを表示",
"warning_will_not_see_again": "あなた <b>できません</b> このノートをもう一度見る", "warning_will_not_see_again": "このノートを再度表示することは<b>できません</b>",
"download_all": "すべてダウンロード", "download_all": "すべてダウンロード",
"links_found": "メモ内にあるリンク:" "links_found": "メモ内にあるリンク:"
}, },
+3 -3
View File
@@ -26,7 +26,7 @@
"new_note_notice": "<b>доступность:</b><br />сохранение заметки не гарантируется, поскольку все хранится в оперативной памяти; если она заполнится, самые старые заметки будут удалены.<br />( вероятно, все будет в порядке, просто будьте осторожны.)", "new_note_notice": "<b>доступность:</b><br />сохранение заметки не гарантируется, поскольку все хранится в оперативной памяти; если она заполнится, самые старые заметки будут удалены.<br />( вероятно, все будет в порядке, просто будьте осторожны.)",
"errors": { "errors": {
"note_to_big": "нельзя создать новую заметку. заметка слишком большая", "note_to_big": "нельзя создать новую заметку. заметка слишком большая",
"note_error": "нельзя создать новую заметку. пожалйста попробуйте позднее.", "note_error": "нельзя создать новую заметку. пожалуйста попробуйте позже.",
"max": "макс: {n}", "max": "макс: {n}",
"empty_content": "пустая заметка." "empty_content": "пустая заметка."
}, },
@@ -41,10 +41,10 @@
"show": { "show": {
"errors": { "errors": {
"not_found": "заметка не найдена или была удалена.", "not_found": "заметка не найдена или была удалена.",
"decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. записка уничтожена.", "decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. заметка уничтожена.",
"unsupported_type": "неподдерживаемый тип заметки." "unsupported_type": "неподдерживаемый тип заметки."
}, },
"explanation": "щелкните ниже, чтобы показать и удалить примечание, если счетчик достиг предела", "explanation": "щелкните ниже, чтобы показать и удалить заметку, если счетчик достиг предела",
"show_note": "показать заметку", "show_note": "показать заметку",
"warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.", "warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.",
"download_all": "скачать всё", "download_all": "скачать всё",
+15 -15
View File
@@ -13,25 +13,25 @@
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@lokalise/node-api": "^12.1.0", "@lokalise/node-api": "^13.2.1",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.5.2", "@sveltejs/kit": "^2.61.1",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^7.1.2",
"@zerodevx/svelte-toast": "^0.9.5", "@zerodevx/svelte-toast": "^0.9.6",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.17",
"dotenv": "^16.4.5", "dotenv": "^17.4.2",
"svelte": "^4.2.12", "svelte": "^5.55.9",
"svelte-check": "^3.6.6", "svelte-check": "^4.4.8",
"svelte-intl-precompile": "^0.12.3", "svelte-intl-precompile": "^0.12.3",
"tslib": "^2.6.2", "tslib": "^2.8.1",
"typescript": "^5.3.3", "typescript": "^6.0.3",
"vite": "^5.1.7" "vite": "^8.0.14"
}, },
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^5.2.7",
"cryptgeon": "workspace:*", "cryptgeon": "workspace:*",
"@fontsource/fira-mono": "^5.0.8",
"occulto": "^2.0.6", "occulto": "^2.0.6",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^7.1.0",
"qrious": "^4.0.2" "uqr": "^0.1.3"
} }
} }
@@ -1,3 +1,5 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Contrast</title><path ><title>Contrast</title><path
d="M256 32C132.29 32 32 132.29 32 256s100.29 224 224 224 224-100.29 224-224S379.71 32 256 32zM128.72 383.28A180 180 0 01256 76v360a178.82 178.82 0 01-127.28-52.72z" d="M256 32C132.29 32 32 132.29 32 256s100.29 224 224 224 224-100.29 224-224S379.71 32 256 32zM128.72 383.28A180 180 0 01256 76v360a178.82 178.82 0 01-127.28-52.72z"

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 316 B

@@ -1,3 +1,5 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Copy</title><path ><title>Copy</title><path
d="M456 480H136a24 24 0 01-24-24V128a16 16 0 0116-16h328a24 24 0 0124 24v320a24 24 0 01-24 24z" d="M456 480H136a24 24 0 01-24-24V128a16 16 0 0116-16h328a24 24 0 0124 24v320a24 24 0 01-24 24z"

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 354 B

@@ -1,3 +1,5 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Dice</title><path ><title>Dice</title><path
d="M48 366.92L240 480V284L48 170zM192 288c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zm-96 32c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zM272 284v196l192-113.08V170zm48 140c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm96 32c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm32 77.64zM256 32L64 144l192 112 192-112zm0 120c-13.25 0-24-7.16-24-16s10.75-16 24-16 24 7.16 24 16-10.75 16-24 16z" d="M48 366.92L240 480V284L48 170zM192 288c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zm-96 32c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zM272 284v196l192-113.08V170zm48 140c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm96 32c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm32 77.64zM256 32L64 144l192 112 192-112zm0 120c-13.25 0-24-7.16-24-16s10.75-16 24-16 24 7.16 24 16-10.75 16-24 16z"

Before

Width:  |  Height:  |  Size: 736 B

After

Width:  |  Height:  |  Size: 765 B

@@ -1,3 +1,5 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Eye</title><circle cx="256" cy="256" r="64" /><path ><title>Eye</title><circle cx="256" cy="256" r="64" /><path
d="M394.82 141.18C351.1 111.2 304.31 96 255.76 96c-43.69 0-86.28 13-126.59 38.48C88.52 160.23 48.67 207 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416c49 0 95.85-15.07 139.3-44.79C433.31 345 469.71 299.82 496 256c-26.38-43.43-62.9-88.56-101.18-114.82zM256 352a96 96 0 1196-96 96.11 96.11 0 01-96 96z" d="M394.82 141.18C351.1 111.2 304.31 96 255.76 96c-43.69 0-86.28 13-126.59 38.48C88.52 160.23 48.67 207 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416c49 0 95.85-15.07 139.3-44.79C433.31 345 469.71 299.82 496 256c-26.38-43.43-62.9-88.56-101.18-114.82zM256 352a96 96 0 1196-96 96.11 96.11 0 01-96 96z"

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 512 B

@@ -1,3 +1,5 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Eye Off</title><path ><title>Eye Off</title><path
d="M63.998 86.004l21.998-21.998L448 426.01l-21.998 21.998zM259.34 192.09l60.57 60.57a64.07 64.07 0 00-60.57-60.57zM252.66 319.91l-60.57-60.57a64.07 64.07 0 0060.57 60.57z" d="M63.998 86.004l21.998-21.998L448 426.01l-21.998 21.998zM259.34 192.09l60.57 60.57a64.07 64.07 0 00-60.57-60.57zM252.66 319.91l-60.57-60.57a64.07 64.07 0 0060.57 60.57z"

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 761 B

@@ -1,10 +1,17 @@
<script lang="ts"> <script lang="ts">
export let title: string import type { Snippet } from 'svelte'
interface Props {
title: string
children?: Snippet
}
let { title, children }: Props = $props()
</script> </script>
<p> <p>
<b>{title}</b> <b>{title}</b>
<slot /> {@render children?.()}
</p> </p>
<style> <style>
@@ -6,13 +6,23 @@
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import type { Note } from 'cryptgeon/shared' import type { Note } from 'cryptgeon/shared'
export let note: Note interface Props {
export let timeExpiration = false note: Note
export let customPassword: string | null = null timeExpiration?: boolean
customPassword?: string | null
}
let hasCustomPassword = false let {
note = $bindable(),
timeExpiration = $bindable(false),
customPassword = $bindable(null),
}: Props = $props()
$: if (!hasCustomPassword) customPassword = null let hasCustomPassword = $state(false)
$effect(() => {
if (!hasCustomPassword) customPassword = null
})
</script> </script>
<div class="flex col"> <div class="flex col">
+11 -1
View File
@@ -1,4 +1,14 @@
<button {...$$restProps} on:click><slot /></button> <script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements'
interface Props {
children?: import('svelte').Snippet
}
let { children, ...rest }: HTMLButtonAttributes & Props = $props()
</script>
<button {...rest}>{@render children?.()}</button>
<style> <style>
button { button {
@@ -1,42 +0,0 @@
<script lang="ts">
// @ts-ignore
import QR from 'qrious'
import { t } from 'svelte-intl-precompile'
import { getCSSVariable } from '$lib/utils'
export let value: string
let canvas: HTMLCanvasElement
$: {
new QR({
value,
level: 'Q',
size: 800,
background: getCSSVariable('--ui-bg-0'),
foreground: getCSSVariable('--ui-text-0'),
element: canvas,
})
}
</script>
<small>{$t('common.qr_code')}</small>
<div>
<canvas bind:this={canvas} />
</div>
<style>
div {
padding: 0.5rem;
width: fit-content;
border: 2px solid var(--ui-bg-1);
background-color: var(--ui-bg-0);
margin-top: 0.125rem;
}
canvas {
width: 100%;
height: auto;
}
</style>
+10 -5
View File
@@ -5,8 +5,13 @@
import MaxSize from '$lib/ui/MaxSize.svelte' import MaxSize from '$lib/ui/MaxSize.svelte'
import type { FileDTO } from 'cryptgeon/shared' import type { FileDTO } from 'cryptgeon/shared'
export let label: string = '' interface Props {
export let files: FileDTO[] = [] label?: string
files?: FileDTO[]
[key: string]: any
}
let { label = '', files = $bindable([]), ...rest }: Props = $props()
async function fileToDTO(file: File): Promise<FileDTO> { async function fileToDTO(file: File): Promise<FileDTO> {
return { return {
@@ -35,7 +40,7 @@
<small> <small>
{label} {label}
</small> </small>
<input {...$$restProps} type="file" on:change={onInput} multiple /> <input {...rest} type="file" onchange={onInput} multiple />
<div class="box"> <div class="box">
{#if files.length} {#if files.length}
<div> <div>
@@ -45,8 +50,8 @@
{file.name} {file.name}
</div> </div>
{/each} {/each}
<div class="spacer" /> <div class="spacer"></div>
<Button on:click={clear}>{$t('file_upload.clear')}</Button> <Button onclick={clear}>{$t('file_upload.clear')}</Button>
</div> </div>
{:else} {:else}
<div> <div>
+10 -4
View File
@@ -1,9 +1,10 @@
<script lang="ts" context="module"> <script lang="ts" module>
import IconContrast from '$lib/icons/IconContrast.svelte' import IconContrast from '$lib/icons/IconContrast.svelte'
import IconCopy from '$lib/icons/IconCopy.svelte' import IconCopy from '$lib/icons/IconCopy.svelte'
import IconDice from '$lib/icons/IconDice.svelte' import IconDice from '$lib/icons/IconDice.svelte'
import IconEye from '$lib/icons/IconEye.svelte' import IconEye from '$lib/icons/IconEye.svelte'
import IconEyeOff from '$lib/icons/IconEyeOff.svelte' import IconEyeOff from '$lib/icons/IconEyeOff.svelte'
import type { HTMLButtonAttributes } from 'svelte/elements'
const map = { const map = {
contrast: IconContrast, contrast: IconContrast,
@@ -15,12 +16,17 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let icon: keyof typeof map interface Props {
icon: keyof typeof map
}
let { icon, ...rest }: HTMLButtonAttributes & Props = $props()
</script> </script>
<button type="button" on:click {...$$restProps}> <button type="button" {...rest}>
{#if map[icon]} {#if map[icon]}
<svelte:component this={map[icon]} /> {@const SvelteComponent = map[icon]}
<SvelteComponent />
{/if} {/if}
</button> </button>
@@ -1,3 +1,5 @@
<script lang="ts"></script>
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

Before

Width:  |  Height:  |  Size: 784 B

After

Width:  |  Height:  |  Size: 813 B

+12 -5
View File
@@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type NoteResult = { export type NoteResult = {
id: string id: string
password?: string password?: string
@@ -10,12 +10,19 @@
import { status } from '$lib/stores/status' import { status } from '$lib/stores/status'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import Canvas from './Canvas.svelte' import QR from './QR.svelte'
export let result: NoteResult interface Props {
result: NoteResult
}
let { result }: Props = $props()
let url = $derived.by(() => {
let url = `${window.location.origin}/note/${result.id}` let url = `${window.location.origin}/note/${result.id}`
if (result.password) url += `#${result.password}` if (result.password) url += `#${result.password}`
return url
})
function reset() { function reset() {
window.location.reload() window.location.reload()
@@ -32,7 +39,7 @@
/> />
<div> <div>
<Canvas value={url} /> <QR value={url} />
</div> </div>
{#if $status?.theme_new_note_notice} {#if $status?.theme_new_note_notice}
@@ -41,7 +48,7 @@
</p> </p>
{/if} {/if}
<br /> <br />
<Button on:click={reset}>{$t('home.new_note')}</Button> <Button onclick={reset}>{$t('home.new_note')}</Button>
<style> <style>
div { div {
+45
View File
@@ -0,0 +1,45 @@
<script lang="ts">
import { getCSSVariable } from '$lib/utils'
import { t } from 'svelte-intl-precompile'
import { renderSVG } from 'uqr'
interface Props {
value: string
}
let { value }: Props = $props()
let qr: string | null = $state(null)
$effect(() => {
qr = renderSVG(value, {
ecc: 'Q',
blackColor: getCSSVariable('--ui-bg-0'),
whiteColor: getCSSVariable('--ui-text-0'),
})
})
</script>
<small>{$t('common.qr_code')}</small>
<div>
{#if qr}
{@html qr}
{/if}
</div>
<style>
div {
padding: 0.25rem;
width: fit-content;
border: 2px solid var(--ui-bg-1);
background-color: var(--ui-bg-0);
margin-top: 0.125rem;
overflow: hidden;
aspect-ratio: 1;
}
div :global(svg) {
width: 100%;
height: auto;
}
</style>
+22 -17
View File
@@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any } export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
function saveAs(file: File) { function saveAs(file: File) {
@@ -22,29 +22,34 @@
import { copy } from '$lib/utils' import { copy } from '$lib/utils'
import type { FileDTO, NotePublic } from 'cryptgeon/shared' import type { FileDTO, NotePublic } from 'cryptgeon/shared'
export let note: DecryptedNote interface Props {
note: DecryptedNote
}
let { note }: Props = $props()
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
let files: FileDTO[] = [] let files: FileDTO[] = $state([])
$: if (note.meta.type === 'file') {
files = note.contents
}
$: download = () => {
for (const file of files) {
downloadFile(file)
}
}
async function downloadFile(file: FileDTO) { async function downloadFile(file: FileDTO) {
// @ts-ignore
const f = new File([file.contents], file.name, { const f = new File([file.contents], file.name, {
type: file.type, type: file.type,
}) })
saveAs(f) saveAs(f)
} }
$: links = typeof note.contents === 'string' ? note.contents.match(RE_URL) : [] $effect(() => {
if (note.meta.type === 'file') {
files = note.contents
}
})
let download = $derived(() => {
for (const file of files) {
downloadFile(file)
}
})
let links = $derived(typeof note.contents === 'string' ? note.contents.match(RE_URL) : [])
</script> </script>
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p> <p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
@@ -53,7 +58,7 @@
<div class="note"> <div class="note">
{note.contents} {note.contents}
</div> </div>
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button> <Button onclick={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
{#if links && links.length} {#if links && links.length}
<div class="links"> <div class="links">
@@ -70,13 +75,13 @@
{:else} {:else}
{#each files as file} {#each files as file}
<div class="note file"> <div class="note file">
<button on:click={() => downloadFile(file)}> <button onclick={() => downloadFile(file)}>
<b>{file.name}</b> <b>{file.name}</b>
</button> </button>
<small> {file.type} {prettyBytes(file.size)}</small> <small> {file.type} {prettyBytes(file.size)}</small>
</div> </div>
{/each} {/each}
<Button on:click={download}>{$t('show.download_all')}</Button> <Button onclick={download}>{$t('show.download_all')}</Button>
{/if} {/if}
</div> </div>
+10 -5
View File
@@ -1,13 +1,18 @@
<script lang="ts"> <script lang="ts">
export let label: string = '' interface Props {
export let value: boolean label?: string
export let color = true value: boolean
color?: boolean
[key: string]: any
}
let { label = '', value = $bindable(), color = true, ...rest }: Props = $props()
</script> </script>
<label {...$$restProps}> <label {...rest}>
<small>{label}</small> <small>{label}</small>
<input type="checkbox" bind:checked={value} /> <input type="checkbox" bind:checked={value} />
<span class:color class="slider" /> <span class:color class="slider"></span>
</label> </label>
<style> <style>
+8 -3
View File
@@ -1,11 +1,16 @@
<script lang="ts"> <script lang="ts">
export let label: string = '' interface Props {
export let value: string label?: string
value: string
[key: string]: any
}
let { label = '', value = $bindable(), ...rest }: Props = $props()
</script> </script>
<label> <label>
<small> <small>
{label} {label}
</small> </small>
<textarea class="box" {...$$restProps} bind:value /> <textarea class="box" {...rest} bind:value></textarea>
</label> </label>
+32 -22
View File
@@ -2,25 +2,35 @@
import Icon from '$lib/ui/Icon.svelte' import Icon from '$lib/ui/Icon.svelte'
import { copy as copyFN } from '$lib/utils' import { copy as copyFN } from '$lib/utils'
import { getRandomBytes, Hex } from 'occulto' import { getRandomBytes, Hex } from 'occulto'
import type { HTMLInputAttributes } from 'svelte/elements'
export let label: string = '' interface Props {
export let value: any label?: string
export let validate: (value: any) => boolean | string = () => true value: any
export let copy: boolean = false validate?: (value: any) => boolean | string
export let random: boolean = false copy?: boolean
random?: boolean
const initialType = $$restProps.type
const isPassword = initialType === 'password'
let hidden = true
$: valid = validate(value)
$: if (isPassword) {
value
$$restProps.type = hidden ? initialType : 'text'
} }
let {
label = '',
value = $bindable(),
validate = () => true,
copy = false,
random = false,
...rest
}: HTMLInputAttributes & Props = $props()
// svelte-ignore state_referenced_locally
const initialType = $state(rest.type)
const isPassword = initialType === 'password'
let hidden = $state(true)
let valid = $derived(validate(value))
let type = $derived(isPassword ? (hidden ? 'password' : 'text') : rest.type)
function toggle() { function toggle() {
console.debug('toggle')
hidden = !hidden hidden = !hidden
} }
@@ -30,31 +40,31 @@
</script> </script>
<label> <label>
<small class:disabled={$$restProps.disabled}> <small class:disabled={rest.disabled}>
{label} {label}
{#if valid !== true} {#if valid !== true}
<span class="error-text">{valid}</span> <span class="error-text">{valid}</span>
{/if} {/if}
</small> </small>
<input bind:value {...$$restProps} class:valid={valid === true} /> <input bind:value {...rest} {type} autocomplete="off" class:valid={valid === true} />
<div class="icons"> <div class="icons">
{#if isPassword} {#if isPassword}
<Icon <Icon
disabled={$$restProps.disabled} disabled={rest.disabled}
class="icon" class="icon"
icon={hidden ? 'eye' : 'eye-off'} icon={hidden ? 'eye' : 'eye-off'}
on:click={toggle} onclick={toggle}
/> />
{/if} {/if}
{#if random} {#if random}
<Icon disabled={$$restProps.disabled} class="icon" icon="dice" on:click={randomFN} /> <Icon disabled={rest.disabled} class="icon" icon="dice" onclick={randomFN} />
{/if} {/if}
{#if copy} {#if copy}
<Icon <Icon
disabled={$$restProps.disabled} disabled={rest.disabled}
class="icon" class="icon"
icon="copy" icon="copy"
on:click={() => copyFN(value.toString())} onclick={() => copyFN(value.toString())}
/> />
{/if} {/if}
</div> </div>
+10 -13
View File
@@ -1,24 +1,21 @@
<script lang="ts" context="module"> <script lang="ts" module>
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
enum Theme { const themes = ['dark', 'light', 'auto'] as const
Dark = 'dark', type Theme = (typeof themes)[number]
Light = 'light',
Auto = 'auto',
}
const NextTheme = { const NextTheme: Record<Theme, Theme> = {
[Theme.Auto]: Theme.Light, auto: 'light',
[Theme.Light]: Theme.Dark, light: 'dark',
[Theme.Dark]: Theme.Auto, dark: 'auto',
} }
function init(): Theme { function init(): Theme {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const saved = window.localStorage.getItem('theme') as Theme const saved = window.localStorage.getItem('theme') as Theme
if (Object.values(Theme).includes(saved)) return saved if (themes.includes(saved)) return saved
} }
return Theme.Auto return 'auto'
} }
export const theme = writable<Theme>(init()) export const theme = writable<Theme>(init())
@@ -40,7 +37,7 @@
} }
</script> </script>
<button on:click={change}> <button onclick={change}>
<Icon class="icon" icon="contrast" /> <Icon class="icon" icon="contrast" />
{$theme} {$theme}
</button> </button>
+25 -18
View File
@@ -15,27 +15,29 @@
import TextArea from '$lib/ui/TextArea.svelte' import TextArea from '$lib/ui/TextArea.svelte'
import { Adapters, API, PayloadToLargeError, type FileDTO, type Note } from 'cryptgeon/shared' import { Adapters, API, PayloadToLargeError, type FileDTO, type Note } from 'cryptgeon/shared'
let note: Note = { let note: Note = $state({
contents: '', contents: '',
meta: { type: 'text' }, meta: { type: 'text' },
views: 1, views: 1,
expiration: 60, expiration: 60,
} })
let files: FileDTO[] let files: FileDTO[] = $state([])
let result: NoteResult | null = null let result: NoteResult | null = $state(null)
let advanced = false let advanced = $state(false)
let isFile = false let isFile = $state(false)
let timeExpiration = false let timeExpiration = $state(false)
let customPassword: string | null = null let customPassword: string | null = $state(null)
let description = '' let description = $state('')
let loading: string | null = null let loading: string | null = $state(null)
$: if (!advanced) { $effect(() => {
if (!advanced) {
note.views = 1 note.views = 1
timeExpiration = false timeExpiration = false
} }
})
$: { $effect(() => {
description = $t('home.explanation', { description = $t('home.explanation', {
values: { values: {
type: $t(timeExpiration ? 'common.minutes' : 'common.views', { type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
@@ -43,17 +45,22 @@
}), }),
}, },
}) })
} })
$: note.meta.type = isFile ? 'file' : 'text' $effect(() => {
note.meta.type = isFile ? 'file' : 'text'
})
$: if (!isFile) { $effect(() => {
if (!isFile) {
note.contents = '' note.contents = ''
} }
})
class EmptyContentError extends Error {} class EmptyContentError extends Error {}
async function submit() { async function submit(e: SubmitEvent) {
e.preventDefault()
try { try {
loading = $t('common.encrypting') loading = $t('common.encrypting')
@@ -103,7 +110,7 @@
<p> <p>
{@html $status?.theme_text || $t('home.intro')} {@html $status?.theme_text || $t('home.intro')}
</p> </p>
<form on:submit|preventDefault={submit}> <form onsubmit={submit}>
<fieldset disabled={loading !== null}> <fieldset disabled={loading !== null}>
{#if isFile} {#if isFile}
<FileUpload data-testid="file-upload" label={$t('common.file')} bind:files /> <FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
@@ -132,7 +139,7 @@
bind:value={advanced} bind:value={advanced}
/> />
{/if} {/if}
<div class="grow" /> <div class="grow"></div>
<div class="tr"> <div class="tr">
<small>{$t('common.max')}: <MaxSize /> </small> <small>{$t('common.max')}: <MaxSize /> </small>
<br /> <br />
@@ -7,7 +7,7 @@
</script> </script>
<header> <header>
<a on:click={reset} href="/"> <a onclick={reset} href="/">
{#if $status?.theme_image} {#if $status?.theme_image}
<img alt="logo" src={$status.theme_image} /> <img alt="logo" src={$status.theme_image} />
{:else} {:else}
+6 -1
View File
@@ -8,6 +8,11 @@
import { init as initStores, status } from '$lib/stores/status' import { init as initStores, status } from '$lib/stores/status'
import Footer from '$lib/views/Footer.svelte' import Footer from '$lib/views/Footer.svelte'
import Header from '$lib/views/Header.svelte' import Header from '$lib/views/Header.svelte'
interface Props {
children?: import('svelte').Snippet
}
let { children }: Props = $props()
onMount(() => { onMount(() => {
initStores() initStores()
@@ -22,7 +27,7 @@
{#await waitLocale() then _} {#await waitLocale() then _}
<main> <main>
<Header /> <Header />
<slot /> {@render children?.()}
</main> </main>
<SvelteToast /> <SvelteToast />
@@ -1,18 +1,16 @@
<script lang="ts"> <script lang="ts">
import { get } from 'svelte/store'; import { goto } from '$app/navigation'
import { goto } from '$app/navigation';
import { status } from '$lib/stores/status' import { status } from '$lib/stores/status'
status.subscribe((config) => { status.subscribe((config) => {
if (config != null) { if (config != null) {
if (config.imprint_url) { if (config.imprint_url) {
window.location = config.imprint_url; window.location.href = config.imprint_url
} } else if (config.imprint_html == '') {
else if (config.imprint_html == "") { goto('/about')
goto("/about");
} }
} }
}); })
</script> </script>
<svelte:head> <svelte:head>
@@ -10,18 +10,22 @@
import { Adapters, API, type NoteMeta } from 'cryptgeon/shared' import { Adapters, API, type NoteMeta } from 'cryptgeon/shared'
import type { PageData } from './$types' import type { PageData } from './$types'
export let data: PageData interface Props {
data: PageData
}
let id = data.id let { data }: Props = $props()
let password: string | null = null
let note: DecryptedNote | null = null
let exists = false
let meta: NoteMeta | null = null
let loading: string | null = null let id = $derived(data.id)
let error: string | null = null let password: string | null = $state<string | null>(null)
let note: DecryptedNote | null = $state(null)
let exists = $state(false)
let meta: NoteMeta | null = $state(null)
$: valid = !!password?.length let loading: string | null = $state(null)
let error: string | null = $state(null)
let valid = $derived(!!password?.length)
onMount(async () => { onMount(async () => {
// Check if note exists // Check if note exists
@@ -41,7 +45,8 @@
/** /**
* Get the actual contents of the note and decrypt it. * Get the actual contents of the note and decrypt it.
*/ */
async function show() { async function show(e: SubmitEvent) {
e.preventDefault()
try { try {
if (!valid) { if (!valid) {
error = $t('show.errors.no_password') error = $t('show.errors.no_password')
@@ -86,7 +91,7 @@
{:else if note && !error} {:else if note && !error}
<ShowNote {note} /> <ShowNote {note} />
{:else} {:else}
<form on:submit|preventDefault={show}> <form onsubmit={show}>
<fieldset> <fieldset>
<p>{$t('show.explanation')}</p> <p>{$t('show.explanation')}</p>
{#if meta?.derivation} {#if meta?.derivation}
+3 -2
View File
@@ -2,6 +2,7 @@
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
} "skipLibCheck": true,
},
} }
+7 -2
View File
@@ -1,12 +1,17 @@
import { sveltekit } from '@sveltejs/kit/vite' import { sveltekit } from '@sveltejs/kit/vite'
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin' import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
const port = 8001 const port = 3000
/** @type {import('vite').UserConfig} */ /** @type {import('vite').UserConfig} */
const config = { const config = {
clearScreen: false, clearScreen: false,
server: { port }, server: {
port,
proxy: {
'/api': 'http://localhost:8000',
},
},
preview: { port }, preview: { port },
plugins: [sveltekit(), precompileIntl('locales')], plugins: [sveltekit(), precompileIntl('locales')],
} }
-12
View File
@@ -1,12 +0,0 @@
{
"private": true,
"name": "@cryptgeon/proxy",
"type": "module",
"main": "./proxy.js",
"scripts": {
"dev": "node ."
},
"dependencies": {
"http-proxy": "^1.18.1"
}
}
-16
View File
@@ -1,16 +0,0 @@
import http from 'http'
import httpProxy from 'http-proxy'
const proxy = httpProxy.createProxyServer()
proxy.on('error', function (err, req, res) {
console.error(err)
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end('500 Internal Server Error')
})
const server = http.createServer(function (req, res) {
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 })
})
server.listen(3000)
console.log('Proxy on http://localhost:3000')
+2023 -2104
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -1,2 +1,7 @@
packages: packages:
- "packages/**" - "packages/**"
allowBuilds:
esbuild: true
minimumReleaseAge: 10080 # One week