Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cbe9fabf2 | |||
| 2923aab916 | |||
| b5102dc647 | |||
| 12d8b87cc1 | |||
| f42662812f | |||
| a551b16216 | |||
| 09840dcf0a | |||
| 24e99b84e0 | |||
| 690b955d5d | |||
| 9b0155dc9a | |||
| 0a56c4c572 | |||
| f6bf8c656c | |||
| 1a243cc96a | |||
| 482795dd9a | |||
| 2907e7c002 | |||
| 4cc9d8a758 | |||
| d652c4ee1e | |||
| 096be03966 | |||
| c53cde6886 | |||
| 0fa5a35dae | |||
| ebbb4efa04 | |||
| a248440bfd | |||
| a1db60d159 | |||
| c2653bee84 | |||
| a2d2acc5de | |||
| 4cc821150d | |||
| e7fb844f66 | |||
| 567a0bed68 | |||
| c13e53404c | |||
| 82862f0e3e | |||
| e20f4626e7 | |||
| e440e4b7e0 | |||
| 808d846737 | |||
| 63c16a797b | |||
| ea50590532 | |||
| b22c3122d7 | |||
| 18af2b2f45 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 .
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ client side with the <code>key</code> and then sent to the server. data is store
|
|||||||
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents
|
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents
|
||||||
of the notes even if it tried to.
|
of the notes even if it tried to.
|
||||||
|
|
||||||
|
> View counts are guaranteed with one running instance of cryptgeon. Multiple instances connected to the same Redis instance can run into race conditions, where a note might be retrieved more than the view count allows.
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||

|

|
||||||
@@ -85,7 +87,7 @@ of the notes even if it tried to.
|
|||||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||||
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
||||||
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
|
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
|
||||||
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both.|
|
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. |
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
> ℹ️ `https` is required otherwise browsers will not support the cryptographic functions.
|
> ℹ️ `https` is required otherwise browsers will not support the cryptographic functions.
|
||||||
@@ -104,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
|
||||||
@@ -155,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
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ _加密鸽_ 是一个受 [_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` | 密信最多查看次数限制 |
|
||||||
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
| `MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
||||||
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
||||||
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
|
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
|
||||||
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
|
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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/manual/eviction/
|
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SharedState {
|
||||||
|
pub locks: LockMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type LockMap = Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>;
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
|
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 tokio::sync::Mutex;
|
||||||
use tower::Layer;
|
use tower::Layer;
|
||||||
use tower_http::{
|
use tower_http::{
|
||||||
compression::CompressionLayer,
|
compression::CompressionLayer,
|
||||||
@@ -15,7 +19,9 @@ use tower_http::{
|
|||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod csp;
|
||||||
mod health;
|
mod health;
|
||||||
|
mod lock;
|
||||||
mod note;
|
mod note;
|
||||||
mod status;
|
mod status;
|
||||||
mod store;
|
mod store;
|
||||||
@@ -24,21 +30,25 @@ mod store;
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
|
||||||
|
let shared_state = SharedState {
|
||||||
|
locks: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
};
|
||||||
|
|
||||||
if !store::can_reach_redis() {
|
if !store::can_reach_redis() {
|
||||||
println!("cannot reach redis");
|
println!("cannot reach redis");
|
||||||
panic!("canont reach redis");
|
panic!("cannot reach redis");
|
||||||
}
|
}
|
||||||
|
|
||||||
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 =
|
||||||
@@ -46,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()
|
||||||
@@ -53,7 +65,8 @@ async fn main() {
|
|||||||
.deflate(true)
|
.deflate(true)
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.zstd(true),
|
.zstd(true),
|
||||||
);
|
)
|
||||||
|
.with_state(shared_state);
|
||||||
|
|
||||||
let app = NormalizePathLayer::trim_trailing_slash().layer(app);
|
let app = NormalizePathLayer::trim_trailing_slash().layer(app);
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ use axum::{
|
|||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::SystemTime;
|
use std::{sync::Arc, time::SystemTime};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::config;
|
|
||||||
use crate::note::{generate_id, Note, NoteInfo};
|
use crate::note::{generate_id, Note, NoteInfo};
|
||||||
use crate::store;
|
use crate::store;
|
||||||
|
use crate::{config, lock::SharedState};
|
||||||
|
|
||||||
use super::NotePublic;
|
use super::NotePublic;
|
||||||
|
|
||||||
@@ -80,11 +81,20 @@ pub async fn create(Json(mut n): Json<Note>) -> Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
|
pub async fn delete(
|
||||||
|
Path(OneNoteParams { id }): Path<OneNoteParams>,
|
||||||
|
state: axum::extract::State<SharedState>,
|
||||||
|
) -> Response {
|
||||||
|
let mut locks_map = state.locks.lock().await;
|
||||||
|
let lock = locks_map
|
||||||
|
.entry(id.clone())
|
||||||
|
.or_insert_with(|| Arc::new(Mutex::new(())))
|
||||||
|
.clone();
|
||||||
|
drop(locks_map);
|
||||||
|
let _guard = lock.lock().await;
|
||||||
|
|
||||||
let note = store::get(&id);
|
let note = store::get(&id);
|
||||||
match note {
|
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(),
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||||
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
|
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
|
||||||
Ok(Some(note)) => {
|
Ok(Some(note)) => {
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
|
|||||||
let serialized = serde_json::to_string(¬e.clone()).unwrap();
|
let serialized = serde_json::to_string(¬e.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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "メモ内にあるリンク:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"note_to_big": "nie można utworzyć notatki. notatka jest za duża",
|
"note_to_big": "nie można utworzyć notatki. notatka jest za duża",
|
||||||
"note_error": "nie można utworzyć notatki. spróbuj ponownie.",
|
"note_error": "nie można utworzyć notatki. spróbuj ponownie.",
|
||||||
"max": "maks .: {n}",
|
"max": "maks.: {n}",
|
||||||
"empty_content": "notatka jest pusta."
|
"empty_content": "notatka jest pusta."
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
|
|||||||
@@ -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": "скачать всё",
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 |
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -20,9 +18,9 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="content">
|
<section class="content">
|
||||||
{#if $status?.imprint_html}
|
{#if $status?.imprint_html}
|
||||||
{@html $status.imprint_html}
|
{@html $status.imprint_html}
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"name": "@cryptgeon/proxy",
|
|
||||||
"type": "module",
|
|
||||||
"main": "./proxy.js",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "node ."
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"http-proxy": "^1.18.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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')
|
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
packages:
|
packages:
|
||||||
- "packages/**"
|
- "packages/**"
|
||||||
|
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
|
|
||||||
|
minimumReleaseAge: 10080 # One week
|
||||||
|
|||||||