Compare commits
65 Commits
Author | SHA1 | Date |
---|---|---|
Niccolo Borgioli | 6d2150b0b6 | |
Nicco | 3a68693be1 | |
Oleg Salnikov | a612eec220 | |
Nicco | 98d3b0d394 | |
Łukasz Pospiech | 6aed2e2756 | |
Nicco | 6bb527198a | |
Łukasz Pospiech | 7050389316 | |
Nicco | 0725a0c6f7 | |
dependabot[bot] | c8efcc04fc | |
Nicco | 19bf155653 | |
MairusuPawa | 9a4e84db62 | |
Niccolo Borgioli | 32cd3843a7 | |
Nicco | 9b48d39c82 | |
Tamás Kádár | 239e950f8e | |
Tamás Kádár | b00846ce9d | |
Tamás Kádár | e70f06f99f | |
Tamás Kádár | 4781882c72 | |
Tamás Kádár | 549dfb55db | |
Tamás Kádár | 2653a4bccf | |
Tamás Kádár | 7213e6c690 | |
Tamás Kádár | 8a5f667ff6 | |
Niccolo Borgioli | fc3938701e | |
Nicco | 23b4f81dac | |
Nicco | 7c68620d8b | |
Niccolo Borgioli | eb76fe085a | |
Christian | 38540b33f2 | |
Niccolo Borgioli | 39a9ac0dad | |
Nicco | ff1b5d500b | |
Niccolo Borgioli | 1698abe2eb | |
Niccolo Borgioli | 3036927a45 | |
Niccolo Borgioli | f9c26ba81c | |
Niccolo Borgioli | 752e68e213 | |
Niccolo Borgioli | 6eb3a59e33 | |
Niccolo Borgioli | 1a2728d21f | |
Niccolo Borgioli | a37a0932e0 | |
Nicco | 71a33a7939 | |
dependabot[bot] | 83033a4b85 | |
Nicco | 40570bbbaf | |
Christian | f591e589d0 | |
Nicco | d1eebb04f3 | |
pgschk | 5a76ea7778 | |
pgschk | 45a1af7569 | |
Nicco | 9578b2bed2 | |
Niccolo Borgioli | f94e4e3858 | |
Niccolo Borgioli | ffa2c49ea3 | |
Niccolo Borgioli | 0d850aadfc | |
Niccolo Borgioli | a9c646c981 | |
Niccolo Borgioli | a2803a13da | |
Niccolo Borgioli | deef56776d | |
Nicco | b089323990 | |
MVS | 6002ede685 | |
Nicco | 8a444ceb88 | |
Oleg Salnikov | 1e01ccb65a | |
Nicco | 2dfa9dd248 | |
Ronald | 618e914b55 | |
Ronald | 86f596fa4b | |
Ronald | dcb4613f66 | |
Nicco | c46f80aaa0 | |
Niccolo Borgioli | e2711cc887 | |
Niccolo Borgioli | e02224216a | |
Niccolo Borgioli | 1b0d5449a0 | |
Niccolo Borgioli | 9695d3a63f | |
Niccolo Borgioli | 22d4efb03e | |
Niccolo Borgioli | 97741ed73f | |
Niccolo Borgioli | c9e5de0f37 |
|
@ -10,18 +10,18 @@ jobs:
|
|||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Node
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
# Docker
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.4.0] - 2023-11-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed HTML sanitation, display the original message as string
|
||||
- Links are now displayed under the note in a separate section
|
||||
|
||||
## [2.3.1] - 2023-06-23
|
||||
|
||||
### Added
|
||||
|
||||
- #92: Endpoint (`/api/live/`) for checking health status.
|
||||
|
||||
## [2.3.0] - 2023-05-30
|
||||
|
||||
### Added
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "52d9e661-2d99-47f8-b09a-40b6a1c0b364",
|
||||
"_postman_id": "3aaeac19-4eac-4911-b3c8-912b17a48634",
|
||||
"name": "Cryptgeon",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
|
@ -137,7 +137,7 @@
|
|||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": null
|
||||
"body": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -479,7 +479,7 @@
|
|||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": null
|
||||
"body": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -489,7 +489,7 @@
|
|||
"name": "Status",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get",
|
||||
"name": "Get server status",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
|
@ -554,6 +554,106 @@
|
|||
"body": "{\n \"version\": \"2.3.0-beta.4\",\n \"max_size\": 10485760,\n \"max_views\": 100,\n \"max_expiration\": 360,\n \"allow_advanced\": true,\n \"theme_image\": \"\",\n \"theme_text\": \"\",\n \"theme_page_title\": \"\",\n \"theme_favicon\": \"\"\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Health Check",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{BASE}}/live/",
|
||||
"host": [
|
||||
"{{BASE}}"
|
||||
],
|
||||
"path": [
|
||||
"live",
|
||||
""
|
||||
]
|
||||
},
|
||||
"description": "Return `200` for healthy service. `503` if service is unavailable."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Healthy",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{BASE}}/live/",
|
||||
"host": [
|
||||
"{{BASE}}"
|
||||
],
|
||||
"path": [
|
||||
"live",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "plain",
|
||||
"header": [
|
||||
{
|
||||
"key": "transfer-encoding",
|
||||
"value": "chunked"
|
||||
},
|
||||
{
|
||||
"key": "vary",
|
||||
"value": "accept-encoding"
|
||||
},
|
||||
{
|
||||
"key": "content-encoding",
|
||||
"value": "gzip"
|
||||
},
|
||||
{
|
||||
"key": "date",
|
||||
"value": "Thu, 22 Jun 2023 20:17:58 GMT"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": null
|
||||
},
|
||||
{
|
||||
"name": "Service Unavilable",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{BASE}}/live/",
|
||||
"host": [
|
||||
"{{BASE}}"
|
||||
],
|
||||
"path": [
|
||||
"live",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "Service Unavailable",
|
||||
"code": 503,
|
||||
"_postman_previewlanguage": "plain",
|
||||
"header": [
|
||||
{
|
||||
"key": "transfer-encoding",
|
||||
"value": "chunked"
|
||||
},
|
||||
{
|
||||
"key": "content-encoding",
|
||||
"value": "gzip"
|
||||
},
|
||||
{
|
||||
"key": "vary",
|
||||
"value": "accept-encoding"
|
||||
},
|
||||
{
|
||||
"key": "date",
|
||||
"value": "Thu, 22 Jun 2023 20:18:55 GMT"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
17
Dockerfile
17
Dockerfile
|
@ -1,26 +1,27 @@
|
|||
# FRONTEND
|
||||
FROM node:18-alpine as client
|
||||
FROM node:20-alpine as client
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /tmp
|
||||
RUN npm install -g pnpm@8
|
||||
COPY . .
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
# BACKEND
|
||||
FROM rust:1.69-alpine as backend
|
||||
FROM rust:1.76-alpine as backend
|
||||
WORKDIR /tmp
|
||||
RUN apk add libc-dev openssl-dev alpine-sdk
|
||||
COPY ./packages/backend/Cargo.* ./
|
||||
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||
RUN cargo fetch
|
||||
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
|
||||
COPY ./packages/backend ./
|
||||
RUN cargo build --release
|
||||
|
||||
|
||||
# RUNNER
|
||||
FROM alpine
|
||||
FROM alpine:3.19
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache curl
|
||||
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||
COPY --from=client /tmp/packages/frontend/build ./frontend
|
||||
ENV FRONTEND_PATH="./frontend"
|
||||
|
|
44
README.md
44
README.md
|
@ -14,7 +14,7 @@
|
|||
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||
<br/><br/>
|
||||
|
||||
EN | [简体中文](README_zh-CN.md)
|
||||
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
|
||||
|
||||
## About?
|
||||
|
||||
|
@ -62,23 +62,27 @@ of the notes even if it tried to.
|
|||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||
| `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. |
|
||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||
| Variable | Default | Description |
|
||||
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||
| `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
|
||||
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
||||
| `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. |
|
||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||
|
||||
## Deployment
|
||||
|
||||
ℹ️ `https` is required otherwise browsers will not support the cryptographic functions.
|
||||
> ℹ️ `https` is required otherwise browsers will not support the cryptographic functions.
|
||||
|
||||
> ℹ️ There is a health endpoint available at `/api/health/`. It returns either 200 or 503.
|
||||
|
||||
### Docker
|
||||
|
||||
|
@ -94,7 +98,7 @@ services:
|
|||
image: redis:7-alpine
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/manual/eviction/
|
||||
command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
|
@ -105,6 +109,14 @@ services:
|
|||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:8000
|
||||
|
||||
# Optional health checks
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
|
||||
# interval: 1m
|
||||
# timeout: 3s
|
||||
# retries: 2
|
||||
# start_period: 5s
|
||||
```
|
||||
|
||||
### NGINX Proxy
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
<p align="center">
|
||||
<img src="./design/Github.png" alt="logo">
|
||||
</p>
|
||||
|
||||
<a href="https://discord.gg/nuby6RnxZt">
|
||||
<img alt="discord" src="https://img.shields.io/discord/252403122348097536?style=for-the-badge" />
|
||||
<img alt="docker pulls" src="https://img.shields.io/docker/pulls/cupcakearmy/cryptgeon?style=for-the-badge" />
|
||||
<img alt="Docker image size badge" src="https://img.shields.io/docker/image-size/cupcakearmy/cryptgeon?style=for-the-badge" />
|
||||
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
|
||||
</a>
|
||||
|
||||
<br/><br/>
|
||||
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a>
|
||||
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||
<br/><br/>
|
||||
|
||||
[EN](README.md) | [简体中文](README_zh-CN.md) | ES
|
||||
|
||||
## Acerca de
|
||||
|
||||
_cryptgeon_ es un servicio seguro y de código abierto para compartir notas o archivos inspirado en [_PrivNote_](https://privnote.com).
|
||||
Incluye un servidor, una página web y una interfaz de línea de comandos (CLI, por sus siglas en inglés).
|
||||
|
||||
> 🌍 Si quieres traducir este proyecto no dudes en ponerte en contacto conmigo.
|
||||
>
|
||||
> Gracias a [Lokalise](https://lokalise.com/) por darnos acceso gratis a su plataforma.
|
||||
|
||||
## Demo
|
||||
|
||||
### Web
|
||||
|
||||
Prueba la demo y experimenta por ti mismo [cryptgeon.org](https://cryptgeon.org)
|
||||
|
||||
### CLI
|
||||
|
||||
```
|
||||
npx cryptgeon send text "Esto es una nota secreta"
|
||||
```
|
||||
|
||||
Puedes revisar la documentación sobre el CLI en este [readme](./packages/cli/README.md).
|
||||
|
||||
## Características
|
||||
|
||||
- enviar texto o archivos
|
||||
- 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
|
||||
- en memoria, sin persistencia
|
||||
- compatibilidad obligatoria con el modo oscuro
|
||||
|
||||
## ¿Cómo funciona?
|
||||
|
||||
Se genera una <code>id (256bit)</code> y una <code>llave 256(bit)</code> para cada nota. La
|
||||
<code>id</code>
|
||||
se usa para guardar y recuperar la nota. Después la nota es encriptada con la <code>llave</code> y con aes en modo gcm del lado del cliente y por último se envía al servidor. La información es almacenada en memoria y nunca persiste en el disco. El servidor nunca ve la llave de encriptación por lo que no puede desencriptar el contenido de las notas aunque lo intentara.
|
||||
|
||||
## Capturas de pantalla
|
||||
|
||||
![screenshot](./design/Screens.png)
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
| Variable | Default | Descripción |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Tamaño máximo. Valores aceptados según la [unidad byte](https://docs.rs/byte-unit/). <br> `512 MiB` es el máximo permitido. <br> El frontend mostrará ese número, incluyendo el ~35% de sobrecarga de codificación. |
|
||||
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
||||
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
||||
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
||||
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
|
||||
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
||||
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
||||
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
|
||||
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
||||
|
||||
## Despliegue
|
||||
|
||||
> ℹ️ Se requiere `https` de lo contrario el navegador no soportará las funciones de encriptacón.
|
||||
|
||||
> ℹ️ Hay un endpoint para verificar el estado, lo encontramos en `/api/health/`. Regresa un código 200 o 503.
|
||||
|
||||
### Docker
|
||||
|
||||
Docker es la manera más fácil. Aquí encontramos [la imágen oficial](https://hub.docker.com/r/cupcakearmy/cryptgeon).
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/manual/eviction/
|
||||
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
# Size limit for a single note.
|
||||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:8000
|
||||
|
||||
# Optional health checks
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
|
||||
# interval: 1m
|
||||
# timeout: 3s
|
||||
# retries: 2
|
||||
# start_period: 5s
|
||||
```
|
||||
|
||||
### NGINX Proxy
|
||||
|
||||
Ver la carpeta de [ejemplo/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx). Hay un ejemplo con un proxy simple y otro con https. Es necesario que especifiques el nombre del servidor y los certificados.
|
||||
|
||||
### Traefik 2
|
||||
|
||||
Ver la carpeta de [ejemplo/traefik](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/traefik).
|
||||
|
||||
### Scratch
|
||||
|
||||
Ver la carpeta de [ejemplo/scratch](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/scratch). Ahí encontrarás una guía de cómo configurar el servidor e instalar cryptgeon desde cero.
|
||||
|
||||
### Synology
|
||||
|
||||
Hay una [guía](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) (en inglés) que puedes seguir.
|
||||
|
||||
### Guías en Youtube
|
||||
|
||||
- En inglés, por [Webnestify](https://www.youtube.com/watch?v=XAyD42I7wyI)
|
||||
- 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)
|
||||
|
||||
## Desarrollo
|
||||
|
||||
**Requisitos**
|
||||
|
||||
- `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:1234](http://localhost:1234).
|
||||
|
||||
> 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
|
||||
|
||||
Por favor dirigite a la sección de seguridad [aquí](./SECURITY.md).
|
||||
|
||||
---
|
||||
|
||||
_Atribuciones_
|
||||
|
||||
- Datos del Test:
|
||||
- Texto para los tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
|
||||
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
|
||||
- [Unsplash Imágenes](https://unsplash.com/)
|
||||
- Animación de carga por [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa)
|
||||
- Iconos hechos por <a href="https://www.freepik.com" title="Freepik">freepik</a> de <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
|
|
@ -14,7 +14,7 @@
|
|||
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||
<br/>
|
||||
|
||||
[EN](README.md) | 简体中文
|
||||
[EN](README.md) | 简体中文 | [ES](README_ES.md)
|
||||
|
||||
## 关于本项目
|
||||
|
||||
|
|
|
@ -14,5 +14,13 @@ services:
|
|||
env_file: .dev.env
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 1234:8000
|
||||
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/']
|
||||
interval: 1m
|
||||
timeout: 3s
|
||||
retries: 2
|
||||
start_period: 5s
|
||||
|
|
|
@ -16,3 +16,11 @@ services:
|
|||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:8000
|
||||
|
||||
# Optional health checks
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
|
||||
# interval: 1m
|
||||
# timeout: 3s
|
||||
# retries: 2
|
||||
# start_period: 5s
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"packageManager": "pnpm@8.5.1",
|
||||
"scripts": {
|
||||
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
|
||||
"dev:packages": "pnpm --parallel run dev",
|
||||
|
@ -13,9 +12,10 @@
|
|||
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.33.0",
|
||||
"@types/node": "^20.1.3",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@types/node": "^20.11.28",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"shelljs": "^0.8.5"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@8.15.4"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +1,9 @@
|
|||
[package]
|
||||
name = "cryptgeon"
|
||||
version = "2.3.0"
|
||||
version = "2.6.1"
|
||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[[bin]]
|
||||
name = "cryptgeon"
|
||||
|
@ -20,4 +21,5 @@ byte-unit = "4"
|
|||
dotenv = "0.15"
|
||||
mime = "0.3"
|
||||
env_logger = "0.9"
|
||||
redis = "0.21.5"
|
||||
log = "0.4"
|
||||
redis = "0.23"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use actix_web::web;
|
||||
|
||||
use crate::health;
|
||||
use crate::note;
|
||||
use crate::status;
|
||||
|
||||
|
@ -7,6 +8,7 @@ pub fn init(cfg: &mut web::ServiceConfig) {
|
|||
cfg.service(
|
||||
web::scope("/api")
|
||||
.service(note::init())
|
||||
.service(status::init()),
|
||||
.service(status::init())
|
||||
.service(health::init()),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,26 +14,34 @@ lazy_static! {
|
|||
|
||||
// CONFIG
|
||||
lazy_static! {
|
||||
pub static ref LIMIT: usize =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as usize;
|
||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
|
||||
.unwrap_or("360".to_string()) // 6 hours in minutes
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH")
|
||||
.unwrap_or("32".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref LIMIT: usize =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as usize;
|
||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
|
||||
.unwrap_or("360".to_string()) // 6 hours in minutes
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH")
|
||||
.unwrap_or("32".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// THEME
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
mod routes;
|
||||
|
||||
pub use routes::*;
|
|
@ -0,0 +1,16 @@
|
|||
use actix_web::{get, web, HttpResponse, Responder, Scope};
|
||||
|
||||
use crate::store;
|
||||
|
||||
#[get("/")]
|
||||
async fn get_live() -> impl Responder {
|
||||
if store::can_reach_redis() {
|
||||
return HttpResponse::Ok();
|
||||
} else {
|
||||
return HttpResponse::ServiceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> Scope {
|
||||
web::scope("/live").service(get_live)
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
use actix_web::{
|
||||
middleware::{self, Logger},
|
||||
web, App, HttpServer,
|
||||
web::{self},
|
||||
App, HttpServer,
|
||||
};
|
||||
use dotenv::dotenv;
|
||||
use log::error;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
@ -10,6 +12,7 @@ extern crate lazy_static;
|
|||
mod api;
|
||||
mod client;
|
||||
mod config;
|
||||
mod health;
|
||||
mod note;
|
||||
mod size;
|
||||
mod status;
|
||||
|
@ -20,6 +23,11 @@ async fn main() -> std::io::Result<()> {
|
|||
dotenv().ok();
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
|
||||
|
||||
if !store::can_reach_redis() {
|
||||
error!("cannot reach redis");
|
||||
panic!("canont reach redis");
|
||||
}
|
||||
|
||||
return HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(Logger::new("\"%r\" %s %b %T"))
|
||||
|
|
|
@ -9,6 +9,8 @@ pub struct Status {
|
|||
pub max_views: u32,
|
||||
pub max_expiration: u32,
|
||||
pub allow_advanced: bool,
|
||||
pub allow_files: bool,
|
||||
pub theme_new_note_notice: bool,
|
||||
// Theme
|
||||
pub theme_image: String,
|
||||
pub theme_text: String,
|
||||
|
|
|
@ -11,10 +11,12 @@ async fn get_status() -> impl Responder {
|
|||
max_views: *config::MAX_VIEWS,
|
||||
max_expiration: *config::MAX_EXPIRATION,
|
||||
allow_advanced: *config::ALLOW_ADVANCED,
|
||||
allow_files: *config::ALLOW_FILES,
|
||||
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
|
||||
theme_image: config::THEME_IMAGE.to_string(),
|
||||
theme_text: config::THEME_TEXT.to_string(),
|
||||
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
|
||||
theme_favicon: config::THEME_FAVICON.to_string()
|
||||
theme_favicon: config::THEME_FAVICON.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,14 @@ fn get_connection() -> Result<redis::Connection, &'static str> {
|
|||
.map_err(|_| "Unable to connect to redis")
|
||||
}
|
||||
|
||||
pub fn can_reach_redis() -> bool {
|
||||
let conn = get_connection();
|
||||
return match conn {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
|
||||
let serialized = serde_json::to_string(¬e.clone()).unwrap();
|
||||
let mut conn = get_connection()?;
|
||||
|
|
|
@ -1,43 +1,44 @@
|
|||
{
|
||||
"version": "2.3.0",
|
||||
"name": "cryptgeon",
|
||||
"version": "2.6.1",
|
||||
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cupcakearmy/cryptgeon.git",
|
||||
"directory": "packages/cli"
|
||||
},
|
||||
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"exports": {
|
||||
".": "./dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "./scripts/build.js --watch",
|
||||
"build": "./scripts/build.js",
|
||||
"package": "./scripts/package.js",
|
||||
"bin": "run-s build package",
|
||||
"prepublishOnly": "run-s build"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
"cryptgeon": "./dist/index.cjs"
|
||||
"cryptgeon": "./dist/cli.cjs"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"bin": "run-s build package",
|
||||
"build": "rm -rf dist && tsc && ./scripts/build.js",
|
||||
"dev": "./scripts/build.js --watch",
|
||||
"prepublishOnly": "run-s build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commander-js/extra-typings": "^10.0.3",
|
||||
"@commander-js/extra-typings": "^12.0.1",
|
||||
"@cryptgeon/shared": "workspace:*",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/mime": "^3.0.1",
|
||||
"@types/node": "^20.1.3",
|
||||
"commander": "^10.0.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"inquirer": "^9.2.2",
|
||||
"mime": "^3.0.0",
|
||||
"occulto": "^2.0.1",
|
||||
"pkg": "^5.8.1",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"typescript": "^5.0.4"
|
||||
"@types/inquirer": "^9.0.7",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node": "^20.11.24",
|
||||
"commander": "^12.0.0",
|
||||
"esbuild": "^0.20.1",
|
||||
"inquirer": "^9.2.15",
|
||||
"mime": "^4.0.1",
|
||||
"occulto": "^2.0.3",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,15 +3,32 @@
|
|||
import { build, context } from 'esbuild'
|
||||
import pkg from '../package.json' assert { type: 'json' }
|
||||
|
||||
const options = {
|
||||
entryPoints: ['./src/index.ts'],
|
||||
const common = {
|
||||
bundle: true,
|
||||
minify: true,
|
||||
platform: 'node',
|
||||
outfile: './dist/index.cjs',
|
||||
define: { VERSION: `"${pkg.version}"` },
|
||||
}
|
||||
|
||||
const cliOptions = {
|
||||
...common,
|
||||
entryPoints: ['./src/cli.ts'],
|
||||
format: 'cjs',
|
||||
outfile: './dist/cli.cjs',
|
||||
}
|
||||
|
||||
const indexOptions = {
|
||||
...common,
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outfile: './dist/index.mjs',
|
||||
format: 'esm',
|
||||
}
|
||||
|
||||
const watch = process.argv.slice(2)[0] === '--watch'
|
||||
if (watch) (await context(options)).watch()
|
||||
else await build(options)
|
||||
if (watch) {
|
||||
const ctx = await context(cliOptions)
|
||||
ctx.watch()
|
||||
} else {
|
||||
await build(cliOptions)
|
||||
await build(indexOptions)
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { exec } from 'pkg'
|
||||
|
||||
const targets = [
|
||||
'node18-macos-arm64',
|
||||
'node18-macos-x64',
|
||||
'node18-linux-arm64',
|
||||
'node18-linux-x64',
|
||||
'node18-win-arm64',
|
||||
'node18-win-x64',
|
||||
]
|
||||
|
||||
for (const target of targets) {
|
||||
console.log(`🚀 Building ${target}`)
|
||||
await exec(['./dist/index.cjs', '--target', target, '--output', `./bin/${target.replace('node18', 'cryptgeon')}`])
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { Argument, Option, program } from '@commander-js/extra-typings'
|
||||
import { setBase, status } from '@cryptgeon/shared'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { download } from './download.js'
|
||||
import { parseFile, parseNumber } from './parsers.js'
|
||||
import { getStdin } from './stdin.js'
|
||||
import { upload } from './upload.js'
|
||||
import { checkConstrains, exit } from './utils.js'
|
||||
|
||||
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
|
||||
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer)
|
||||
const files = new Argument('<file...>', 'Files to be sent').argParser(parseFile)
|
||||
const text = new Argument('<text>', 'Text content of the note')
|
||||
const password = new Option('-p --password <string>', 'manually set a password')
|
||||
const all = new Option('-a --all', 'Save all files without prompt').default(false)
|
||||
const url = new Argument('<url>', 'The url to open')
|
||||
const views = new Option('-v --views <number>', 'Amount of views before getting destroyed').argParser(parseNumber)
|
||||
const minutes = new Option('-m --minutes <number>', 'Minutes before the note expires').argParser(parseNumber)
|
||||
|
||||
// Node 18 guard
|
||||
parseInt(process.version.slice(1).split(',')[0]) < 18 && exit('Node 18 or higher is required')
|
||||
|
||||
// @ts-ignore
|
||||
const version: string = VERSION
|
||||
|
||||
program.name('cryptgeon').version(version).configureHelp({ showGlobalOptions: true })
|
||||
|
||||
program
|
||||
.command('info')
|
||||
.description('show information about the server')
|
||||
.addOption(server)
|
||||
.action(async (options) => {
|
||||
setBase(options.server)
|
||||
const response = await status()
|
||||
const formatted = {
|
||||
...response,
|
||||
max_size: prettyBytes(response.max_size),
|
||||
}
|
||||
for (const key of Object.keys(formatted)) {
|
||||
if (key.startsWith('theme_')) delete formatted[key as keyof typeof formatted]
|
||||
}
|
||||
console.table(formatted)
|
||||
})
|
||||
|
||||
const send = program.command('send').description('send a note')
|
||||
send
|
||||
.command('file')
|
||||
.addArgument(files)
|
||||
.addOption(server)
|
||||
.addOption(views)
|
||||
.addOption(minutes)
|
||||
.addOption(password)
|
||||
.action(async (files, options) => {
|
||||
setBase(options.server!)
|
||||
await checkConstrains(options)
|
||||
options.password ||= await getStdin()
|
||||
try {
|
||||
const url = await upload(files, { views: options.views, expiration: options.minutes, password: options.password })
|
||||
console.log(`Note created:\n\n${url}`)
|
||||
} catch {
|
||||
exit('Could not create note')
|
||||
}
|
||||
})
|
||||
send
|
||||
.command('text')
|
||||
.addArgument(text)
|
||||
.addOption(server)
|
||||
.addOption(views)
|
||||
.addOption(minutes)
|
||||
.addOption(password)
|
||||
.action(async (text, options) => {
|
||||
setBase(options.server!)
|
||||
await checkConstrains(options)
|
||||
options.password ||= await getStdin()
|
||||
try {
|
||||
const url = await upload(text, { views: options.views, expiration: options.minutes, password: options.password })
|
||||
console.log(`Note created:\n\n${url}`)
|
||||
} catch {
|
||||
exit('Could not create note')
|
||||
}
|
||||
})
|
||||
|
||||
program
|
||||
.command('open')
|
||||
.description('open a link with text or files inside')
|
||||
.addArgument(url)
|
||||
.addOption(password)
|
||||
.addOption(all)
|
||||
.action(async (note, options) => {
|
||||
try {
|
||||
const url = new URL(note)
|
||||
options.password ||= await getStdin()
|
||||
try {
|
||||
await download(url, options.all, options.password)
|
||||
} catch (e) {
|
||||
exit(e instanceof Error ? e.message : 'Unknown error occurred')
|
||||
}
|
||||
} catch {
|
||||
exit('Invalid URL')
|
||||
}
|
||||
})
|
||||
|
||||
program.parse()
|
|
@ -5,12 +5,12 @@ import { basename, resolve } from 'node:path'
|
|||
import { AES, Hex } from 'occulto'
|
||||
import pretty from 'pretty-bytes'
|
||||
|
||||
import { exit } from './utils'
|
||||
|
||||
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
|
||||
setBase(url.origin)
|
||||
const id = url.pathname.split('/')[2]
|
||||
const preview = await info(id).catch(() => exit('Note does not exist or is expired'))
|
||||
const preview = await info(id).catch(() => {
|
||||
throw new Error('Note does not exist or is expired')
|
||||
})
|
||||
|
||||
// Password
|
||||
let password: string
|
||||
|
@ -35,13 +35,14 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin
|
|||
const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password)
|
||||
const note = await get(id)
|
||||
|
||||
const couldNotDecrypt = () => exit('Could not decrypt note. Probably an invalid password')
|
||||
const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password')
|
||||
switch (note.meta.type) {
|
||||
case 'file':
|
||||
const files = await Adapters.Files.decrypt(note.contents, key).catch(couldNotDecrypt)
|
||||
const files = await Adapters.Files.decrypt(note.contents, key).catch(() => {
|
||||
throw couldNotDecrypt
|
||||
})
|
||||
if (!files) {
|
||||
exit('No files found in note')
|
||||
return
|
||||
throw new Error('No files found in note')
|
||||
}
|
||||
|
||||
let selected: typeof files
|
||||
|
@ -63,7 +64,7 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin
|
|||
selected = files.filter((file) => names.includes(file.name))
|
||||
}
|
||||
|
||||
if (!selected.length) exit('No files selected')
|
||||
if (!selected.length) throw new Error('No files selected')
|
||||
await Promise.all(
|
||||
selected.map(async (file) => {
|
||||
let filename = resolve(file.name)
|
||||
|
@ -79,7 +80,9 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin
|
|||
|
||||
break
|
||||
case 'text':
|
||||
const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(couldNotDecrypt)
|
||||
const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(() => {
|
||||
throw couldNotDecrypt
|
||||
})
|
||||
console.log(plaintext)
|
||||
break
|
||||
}
|
||||
|
|
|
@ -1,104 +1,4 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { Argument, Option, program } from '@commander-js/extra-typings'
|
||||
import { setBase, status } from '@cryptgeon/shared'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { download } from './download.js'
|
||||
import { parseFile, parseNumber } from './parsers.js'
|
||||
import { getStdin } from './stdin.js'
|
||||
import { upload } from './upload.js'
|
||||
import { exit } from './utils.js'
|
||||
|
||||
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
|
||||
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer)
|
||||
const files = new Argument('<file...>', 'Files to be sent').argParser(parseFile)
|
||||
const text = new Argument('<text>', 'Text content of the note')
|
||||
const password = new Option('-p --password <string>', 'manually set a password')
|
||||
const all = new Option('-a --all', 'Save all files without prompt').default(false)
|
||||
const url = new Argument('<url>', 'The url to open')
|
||||
const views = new Option('-v --views <number>', 'Amount of views before getting destroyed').argParser(parseNumber)
|
||||
const minutes = new Option('-m --minutes <number>', 'Minutes before the note expires').argParser(parseNumber)
|
||||
|
||||
// Node 18 guard
|
||||
parseInt(process.version.slice(1).split(',')[0]) < 18 && exit('Node 18 or higher is required')
|
||||
|
||||
// @ts-ignore
|
||||
const version: string = VERSION
|
||||
|
||||
async function checkConstrains(constrains: { views?: number; minutes?: number }) {
|
||||
const { views, minutes } = constrains
|
||||
if (views && minutes) exit('cannot set view and minutes constrains simultaneously')
|
||||
if (!views && !minutes) constrains.views = 1
|
||||
|
||||
const response = await status()
|
||||
if (views && views > response.max_views)
|
||||
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
|
||||
if (minutes && minutes > response.max_expiration)
|
||||
exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${minutes} given.`)
|
||||
}
|
||||
|
||||
program.name('cryptgeon').version(version).configureHelp({ showGlobalOptions: true })
|
||||
|
||||
program
|
||||
.command('info')
|
||||
.description('show information about the server')
|
||||
.addOption(server)
|
||||
.action(async (options) => {
|
||||
setBase(options.server)
|
||||
const response = await status()
|
||||
const formatted = {
|
||||
...response,
|
||||
max_size: prettyBytes(response.max_size),
|
||||
}
|
||||
for (const key of Object.keys(formatted)) {
|
||||
if (key.startsWith('theme_')) delete formatted[key as keyof typeof formatted]
|
||||
}
|
||||
console.table(formatted)
|
||||
})
|
||||
|
||||
const send = program.command('send').description('send a note')
|
||||
send
|
||||
.command('file')
|
||||
.addArgument(files)
|
||||
.addOption(server)
|
||||
.addOption(views)
|
||||
.addOption(minutes)
|
||||
.addOption(password)
|
||||
.action(async (files, options) => {
|
||||
setBase(options.server!)
|
||||
await checkConstrains(options)
|
||||
options.password ||= await getStdin()
|
||||
await upload(files, { views: options.views, expiration: options.minutes, password: options.password })
|
||||
})
|
||||
send
|
||||
.command('text')
|
||||
.addArgument(text)
|
||||
.addOption(server)
|
||||
.addOption(views)
|
||||
.addOption(minutes)
|
||||
.addOption(password)
|
||||
.action(async (text, options) => {
|
||||
setBase(options.server!)
|
||||
await checkConstrains(options)
|
||||
options.password ||= await getStdin()
|
||||
await upload(text, { views: options.views, expiration: options.minutes, password: options.password })
|
||||
})
|
||||
|
||||
program
|
||||
.command('open')
|
||||
.description('open a link with text or files inside')
|
||||
.addArgument(url)
|
||||
.addOption(password)
|
||||
.addOption(all)
|
||||
.action(async (note, options) => {
|
||||
try {
|
||||
const url = new URL(note)
|
||||
options.password ||= await getStdin()
|
||||
await download(url, options.all, options.password)
|
||||
} catch {
|
||||
exit('Invalid URL')
|
||||
}
|
||||
})
|
||||
|
||||
program.parse()
|
||||
export * from '@cryptgeon/shared'
|
||||
export * from './download.js'
|
||||
export * from './upload.js'
|
||||
export * from './utils.js'
|
||||
|
|
|
@ -3,49 +3,43 @@ import { basename } from 'node:path'
|
|||
|
||||
import { Adapters, BASE, create, FileDTO, Note, NoteMeta } from '@cryptgeon/shared'
|
||||
import mime from 'mime'
|
||||
import { AES, Hex, TypedArray } from 'occulto'
|
||||
import { AES, Hex } from 'occulto'
|
||||
|
||||
import { exit } from './utils.js'
|
||||
export type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
|
||||
|
||||
type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
|
||||
export async function upload(input: string | string[], options: UploadOptions): Promise<string> {
|
||||
const { password, ...noteOptions } = options
|
||||
const derived = options.password ? await AES.derive(options.password) : undefined
|
||||
const key = derived ? derived[0] : await AES.generateKey()
|
||||
|
||||
export async function upload(input: string | string[], options: UploadOptions) {
|
||||
try {
|
||||
const { password, ...noteOptions } = options
|
||||
const derived = options.password ? await AES.derive(options.password) : undefined
|
||||
const key = derived ? derived[0] : await AES.generateKey()
|
||||
|
||||
let contents: string
|
||||
let type: NoteMeta['type']
|
||||
if (typeof input === 'string') {
|
||||
contents = await Adapters.Text.encrypt(input, key)
|
||||
type = 'text'
|
||||
} else {
|
||||
const files: FileDTO[] = await Promise.all(
|
||||
input.map(async (path) => {
|
||||
const data = new Uint8Array(await readFile(path))
|
||||
const stats = await stat(path)
|
||||
const extension = path.substring(path.indexOf('.') + 1)
|
||||
const type = mime.getType(extension) ?? 'application/octet-stream'
|
||||
return {
|
||||
name: basename(path),
|
||||
size: stats.size,
|
||||
contents: data,
|
||||
type,
|
||||
} satisfies FileDTO
|
||||
})
|
||||
)
|
||||
contents = await Adapters.Files.encrypt(files, key)
|
||||
type = 'file'
|
||||
}
|
||||
|
||||
// Create the actual note and upload it.
|
||||
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
|
||||
const result = await create(note)
|
||||
let url = `${BASE}/note/${result.id}`
|
||||
if (!derived) url += `#${Hex.encode(key)}`
|
||||
console.log(`Note created:\n\n${url}`)
|
||||
} catch {
|
||||
exit('Could not create note')
|
||||
let contents: string
|
||||
let type: NoteMeta['type']
|
||||
if (typeof input === 'string') {
|
||||
contents = await Adapters.Text.encrypt(input, key)
|
||||
type = 'text'
|
||||
} else {
|
||||
const files: FileDTO[] = await Promise.all(
|
||||
input.map(async (path) => {
|
||||
const data = new Uint8Array(await readFile(path))
|
||||
const stats = await stat(path)
|
||||
const extension = path.substring(path.indexOf('.') + 1)
|
||||
const type = mime.getType(extension) ?? 'application/octet-stream'
|
||||
return {
|
||||
name: basename(path),
|
||||
size: stats.size,
|
||||
contents: data,
|
||||
type,
|
||||
} satisfies FileDTO
|
||||
})
|
||||
)
|
||||
contents = await Adapters.Files.encrypt(files, key)
|
||||
type = 'file'
|
||||
}
|
||||
|
||||
// Create the actual note and upload it.
|
||||
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
|
||||
const result = await create(note)
|
||||
let url = `${BASE}/note/${result.id}`
|
||||
if (!derived) url += `#${Hex.encode(key)}`
|
||||
return url
|
||||
}
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
import { status } from '@cryptgeon/shared'
|
||||
import { exit as exitNode } from 'node:process'
|
||||
|
||||
export function exit(message: string) {
|
||||
console.error(message)
|
||||
exitNode(1)
|
||||
}
|
||||
|
||||
export async function checkConstrains(constrains: { views?: number; minutes?: number }) {
|
||||
const { views, minutes } = constrains
|
||||
if (views && minutes) exit('cannot set view and minutes constrains simultaneously')
|
||||
if (!views && !minutes) constrains.views = 1
|
||||
|
||||
const response = await status()
|
||||
if (views && views > response.max_views)
|
||||
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
|
||||
if (minutes && minutes > response.max_expiration)
|
||||
exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${minutes} given.`)
|
||||
}
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"strict": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +1,58 @@
|
|||
{
|
||||
"common": {
|
||||
"note": "Hinweis",
|
||||
"note": "Notiz",
|
||||
"file": "Datei",
|
||||
"advanced": "erweitert",
|
||||
"create": "erstellen",
|
||||
"loading": "läd",
|
||||
"advanced": "Erweiterte Optionen",
|
||||
"create": "Erstellen",
|
||||
"loading": "Lädt...",
|
||||
"mode": "Modus",
|
||||
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
|
||||
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
|
||||
"max": "max",
|
||||
"share_link": "Link teilen",
|
||||
"copy_clipboard": "in die Zwischenablage kopieren",
|
||||
"copied_to_clipboard": "in die Zwischenablage kopiert",
|
||||
"encrypting": "verschlüsseln",
|
||||
"decrypting": "entschlüsselt",
|
||||
"uploading": "hochladen",
|
||||
"downloading": "wird heruntergeladen",
|
||||
"qr_code": "qr-code",
|
||||
"copy_clipboard": "In die Zwischenablage kopieren",
|
||||
"copied_to_clipboard": "In die Zwischenablage kopiert.",
|
||||
"encrypting": "Wird verschlüsselt...",
|
||||
"decrypting": "Wird entschlüsselt...",
|
||||
"uploading": "Hochladen",
|
||||
"downloading": "Wird heruntergeladen",
|
||||
"qr_code": "QR-Code",
|
||||
"password": "Passwort"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Senden Sie ganz einfach <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.",
|
||||
"explanation": "die Notiz verfällt und wird nach {type} zerstört.",
|
||||
"new_note": "neue Note",
|
||||
"new_note_notice": "<b>Verfügbarkeit:</b><br />es ist nicht garantiert, dass die Notiz gespeichert wird, da alles im Speicher gehalten wird. Wenn dieser voll ist, werden die ältesten Notizen entfernt.<br />(Sie werden wahrscheinlich keine Probleme haben, seien Sie nur gewarnt).",
|
||||
"intro": "Erstellen Sie mit einem Klick <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien und teilen Sie diese über einen Link.",
|
||||
"explanation": "Die Notiz verfällt nach {type}.",
|
||||
"new_note": "Neue Notiz",
|
||||
"new_note_notice": "<b>Wichtiger Hinweis zur Verfügbarkeit:</b><br />Es kann nicht garantiert werden, dass diese Notiz gespeichert wird, da diese <b>ausschließlich im Speicher</b> gehalten werden. Ist dieser voll, werden die ältesten Notizen entfernt.<br />(Wahrscheinlich gibt es keine derartigen Probleme, seien Sie nur vorgewarnt).",
|
||||
"errors": {
|
||||
"note_to_big": "Notiz konnte nicht erstellt werden. Notiz ist zu groß",
|
||||
"note_error": "konnte keine Notiz erstellen. Bitte versuchen Sie es erneut.",
|
||||
"note_to_big": "Notiz konnte nicht erstellt werden, da sie zu groß ist.",
|
||||
"note_error": "Notiz konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||
"max": "max: {n}",
|
||||
"empty_content": "Notiz ist leer."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "notiz erstellt."
|
||||
"note_created": "Notiz wurde erstellt."
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "Standardmäßig wird für jede Notiz ein sicher generiertes Passwort verwendet. Sie können jedoch auch ein eigenes Kennwort wählen, das nicht in dem Link enthalten ist.",
|
||||
"custom_password": "benutzerdefiniertes Passwort"
|
||||
"explanation": "Standardmäßig wird für jede Notiz ein generiertes, sicheres Passwort verwendet. Alternativ können Sie ein eigenes Kennwort festlegen, welches nicht im Link enthalten ist.",
|
||||
"custom_password": "Benutzerdefiniertes Passwort"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
|
||||
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört.",
|
||||
"unsupported_type": "nicht unterstützter Notiztyp."
|
||||
"not_found": "Notiz konnte nicht gefunden werden oder wurde bereits gelöscht.",
|
||||
"decryption_failed": "Notiz konnte nicht entschlüsselt werden. Vermutlich ist das Passwort falsch oder der Link defekt. Die Notiz wurde daher gelöscht.",
|
||||
"unsupported_type": "Nicht unterstützter Notiztyp."
|
||||
},
|
||||
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
|
||||
"explanation": "Klicken Sie auf den Button, um die Notiz anzuzeigen und anschließend zu löschen, falls ein festgelegtes Limit erreicht wurde.",
|
||||
"show_note": "Notiz anzeigen",
|
||||
"warning_will_not_see_again": "haben Sie <b>keine</b> Gelegenheit, die Notiz noch einmal zu sehen.",
|
||||
"download_all": "alle herunterladen"
|
||||
"warning_will_not_see_again": "ACHTUNG! Sie werden anschließend <b>keine</b> Gelegenheit mehr haben, die Notiz erneut anzusehen.",
|
||||
"download_all": "Alle Dateien herunterladen",
|
||||
"links_found": "Gefundene Links in der Notiz:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Ausgewählte Dateien",
|
||||
"no_files_selected": "Keine Dateien ausgewählt"
|
||||
"no_files_selected": "Keine Dateien ausgewählt",
|
||||
"clear": "Zurücksetzen"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,10 +47,12 @@
|
|||
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
||||
"show_note": "show note",
|
||||
"warning_will_not_see_again": "you will <b>not</b> get the chance to see the note again.",
|
||||
"download_all": "download all"
|
||||
"download_all": "download all",
|
||||
"links_found": "links found inside the note:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Selected Files",
|
||||
"no_files_selected": "No Files Selected"
|
||||
"no_files_selected": "No Files Selected",
|
||||
"clear": "Reset"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"intro": "Envía fácilmente notas o archivos <i>totalmente encriptados</i> y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.",
|
||||
"explanation": "la nota expirará y se destruirá después de {type}.",
|
||||
"new_note": "nueva nota",
|
||||
"new_note_notice": "<b>disponibilidad:</b><br />no se garantiza que la nota se almacene, ya que todo se guarda en la memoria RAM, si se llena se eliminarán las notas más antiguas.<br />(probablemente estará bien, sólo está advertido.)",
|
||||
"new_note_notice": "<b>disponibilidad:</b><br />no se garantiza que la nota se almacene, ya que todo se guarda en la memoria RAM, si se llena se eliminarán las notas más antiguas.<br />(probablemente estará bien, solo está advertido.)",
|
||||
"errors": {
|
||||
"note_to_big": "no se pudo crear la nota. la nota es demasiado grande",
|
||||
"note_error": "No se ha podido crear la nota. Por favor, inténtelo de nuevo.",
|
||||
|
@ -34,7 +34,7 @@
|
|||
"note_created": "nota creada."
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, que no se incluye en el enlace.",
|
||||
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, la cual no se incluye en el enlace.",
|
||||
"custom_password": "contraseña personalizada"
|
||||
}
|
||||
},
|
||||
|
@ -47,10 +47,12 @@
|
|||
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
|
||||
"show_note": "mostrar nota",
|
||||
"warning_will_not_see_again": "<b>no</b> tendrás la oportunidad de volver a ver la nota.",
|
||||
"download_all": "descargar todo"
|
||||
"download_all": "descargar todo",
|
||||
"links_found": "enlaces que se encuentran dentro de la nota:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Archivos seleccionados",
|
||||
"no_files_selected": "No hay archivos seleccionados"
|
||||
"no_files_selected": "No hay archivos seleccionados",
|
||||
"clear": "Restablecer"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,20 +12,20 @@
|
|||
"share_link": "partager le lien",
|
||||
"copy_clipboard": "copier dans le presse-papiers",
|
||||
"copied_to_clipboard": "copié dans le presse-papiers",
|
||||
"encrypting": "cryptage",
|
||||
"encrypting": "chiffrer",
|
||||
"decrypting": "déchiffrer",
|
||||
"uploading": "téléchargement",
|
||||
"uploading": "téléversement",
|
||||
"downloading": "téléchargement",
|
||||
"qr_code": "code qr",
|
||||
"password": "mot de passe"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Envoyez facilement des notes ou des fichiers <i>entièrement cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.",
|
||||
"intro": "Envoyez facilement des notes ou des fichiers <i>entièrement chiffrés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.",
|
||||
"explanation": "la note expirera et sera détruite après {type}.",
|
||||
"new_note": "nouvelle note",
|
||||
"new_note_notice": "<b>disponibilité :</b><br />la note n'est pas garantie d'être stockée car tout est conservé dans la mémoire vive, si elle se remplit les notes les plus anciennes seront supprimées.<br />(vous serez probablement bien, soyez juste averti.)",
|
||||
"new_note_notice": "<b>disponibilité :</b><br />la note n'est pas garantie d'être stockée car tout est conservé dans la mémoire vive; si elle se remplit, les notes les plus anciennes seront supprimées.<br />(tout ira probablement bien, soyez juste averti.)",
|
||||
"errors": {
|
||||
"note_to_big": "Impossible de créer une note. La note est trop grande",
|
||||
"note_to_big": "Impossible de créer une note. La note est trop grande.",
|
||||
"note_error": "n'a pas pu créer de note. Veuillez réessayer.",
|
||||
"max": "max: {n}",
|
||||
"empty_content": "La note est vide."
|
||||
|
@ -47,10 +47,12 @@
|
|||
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
||||
"show_note": "note de présentation",
|
||||
"warning_will_not_see_again": "vous <b>n'aurez pas</b> la chance de revoir la note.",
|
||||
"download_all": "télécharger tout"
|
||||
"download_all": "télécharger tout",
|
||||
"links_found": "liens trouvés à l’intérieur de la note :"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Fichiers sélectionnés",
|
||||
"no_files_selected": "Aucun fichier sélectionné"
|
||||
"no_files_selected": "Aucun fichier sélectionné",
|
||||
"clear": "Réinitialiser"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,10 +47,12 @@
|
|||
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
|
||||
"show_note": "mostra la nota",
|
||||
"warning_will_not_see_again": "<b>non</b> avrete la possibilità di rivedere la nota.",
|
||||
"download_all": "scarica tutti"
|
||||
"download_all": "scarica tutti",
|
||||
"links_found": "link presenti all'interno della nota:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "File selezionati",
|
||||
"no_files_selected": "Nessun file selezionato"
|
||||
"no_files_selected": "Nessun file selezionato",
|
||||
"clear": "Reset"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,10 +47,12 @@
|
|||
"explanation": "カウンターが上限に達した場合、ノートの表示と削除を行うには、以下をクリックします。",
|
||||
"show_note": "メモを表示",
|
||||
"warning_will_not_see_again": "あなた <b>できません</b> このノートをもう一度見る",
|
||||
"download_all": "すべてダウンロード"
|
||||
"download_all": "すべてダウンロード",
|
||||
"links_found": "メモ内にあるリンク:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "選択したファイル",
|
||||
"no_files_selected": "ファイルが選択されていません"
|
||||
"no_files_selected": "ファイルが選択されていません",
|
||||
"clear": "リセット"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"common": {
|
||||
"note": "notatka",
|
||||
"file": "plik",
|
||||
"advanced": "zaawansowane",
|
||||
"create": "utwórz",
|
||||
"loading": "ładowanie",
|
||||
"mode": "tryb",
|
||||
"views": "{n, plural, =0 {wyświetleń} =1 {1 wyświetlenie} other {# wyświetleń}}",
|
||||
"minutes": "{n, plural, =0 {minut} =1 {1 minuta} other {# minuty}}",
|
||||
"max": "maks.",
|
||||
"share_link": "link udostępniania",
|
||||
"copy_clipboard": "kopiuj do schowka",
|
||||
"copied_to_clipboard": "skopiowano do schowka",
|
||||
"encrypting": "szyfrowanie",
|
||||
"decrypting": "odszyfrowywanie",
|
||||
"uploading": "wysyłanie",
|
||||
"downloading": "pobieranie",
|
||||
"qr_code": "kod QR",
|
||||
"password": "hasło"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Łatwo wysyłaj <i>w pełni zaszyfrowane</i>, bezpieczne notatki lub pliki jednym kliknięciem. Po prostu utwórz notatkę i udostępnij link.",
|
||||
"explanation": "notatka wygaśnie i zostanie zniszczona po {type}.",
|
||||
"new_note": "nowa notatka",
|
||||
"new_note_notice": "<b>dostępność:</b><br />nie ma gwarancji, że notatka będzie przechowywana, ponieważ wszystko jest przechowywane w pamięci RAM, jeśli się zapełni, najstarsze notatki zostaną usunięte.<br />(prawdopodobnie nic się nie stanie, ale warto ostrzec.)",
|
||||
"errors": {
|
||||
"note_to_big": "nie można utworzyć notatki. notatka jest za duża",
|
||||
"note_error": "nie można utworzyć notatki. spróbuj ponownie.",
|
||||
"max": "maks .: {n}",
|
||||
"empty_content": "notatka jest pusta."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "notatka utworzona."
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "Domyślnie dla każdej notatki używane jest bezpiecznie wygenerowane hasło. Możesz jednak wybrać własne hasło, które nie jest uwzględnione w linku.",
|
||||
"custom_password": "własne hasło"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "notatka nie została znaleziona lub została już usunięta.",
|
||||
"decryption_failed": "błędne hasło. nie można odszyfrować. prawdopodobnie uszkodzony link. notatka została zniszczona.",
|
||||
"unsupported_type": "nieobsługiwany typ notatki."
|
||||
},
|
||||
"explanation": "kliknij poniżej, aby wyświetlić i usunąć notatkę, jeśli licznik osiągnie swój limit",
|
||||
"show_note": "pokaż notatkę",
|
||||
"warning_will_not_see_again": "<b>nie będziesz mieć</b> możliwości ponownego zobaczenia notatki.",
|
||||
"download_all": "pobierz wszystko",
|
||||
"links_found": "linki znalezione w notatce:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Wybrane pliki",
|
||||
"no_files_selected": "Nie wybrano plików",
|
||||
"clear": "Wyczyść"
|
||||
}
|
||||
}
|
|
@ -1,56 +1,58 @@
|
|||
{
|
||||
"common": {
|
||||
"note": "заметка",
|
||||
"file": "файл",
|
||||
"advanced": "расширенные",
|
||||
"create": "создать",
|
||||
"loading": "загрузка",
|
||||
"mode": "режим",
|
||||
"views": "{n, plural, =0 {просмотры} =1 {1 просмотр} other {# просмотры}}",
|
||||
"minutes": "{n, plural, =0 {минут} =1 {1 минута} other {# минуты}}",
|
||||
"max": "макс",
|
||||
"share_link": "поделиться ссылкой",
|
||||
"copy_clipboard": "скопировать в буфер обмена",
|
||||
"copied_to_clipboard": "скопировано в буфер обмена",
|
||||
"encrypting": "шифрование",
|
||||
"decrypting": "расшифровка",
|
||||
"uploading": "загрузка",
|
||||
"downloading": "скачивание",
|
||||
"qr_code": "qr код",
|
||||
"password": "пароль"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Легко отправляйте <i>полностью зашифрованные</i> защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.",
|
||||
"explanation": "заметка истечет и будет уничтожена после {type}.",
|
||||
"new_note": "новая заметка",
|
||||
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes will be removed.<br />(you probably will be fine, just be warned.)",
|
||||
"errors": {
|
||||
"note_to_big": "нельзя создать новую заметку. заметка слишком большая",
|
||||
"note_error": "нельзя создать новую заметку. пожалйста попробуйте позднее.",
|
||||
"max": "макс: {n}",
|
||||
"empty_content": "пустая заметка."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "заметка создана."
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.",
|
||||
"custom_password": "пользовательский пароль"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "заметка не найдена или была удалена.",
|
||||
"decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. записка уничтожена.",
|
||||
"unsupported_type": "неподдерживаемый тип заметки."
|
||||
},
|
||||
"explanation": "щелкните ниже, чтобы показать и удалить примечание, если счетчик достиг предела",
|
||||
"show_note": "показать заметку",
|
||||
"warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.",
|
||||
"download_all": "скачать всё"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Выбранные файлы",
|
||||
"no_files_selected": "Файлы не выбраны"
|
||||
}
|
||||
}
|
||||
{
|
||||
"common": {
|
||||
"note": "заметка",
|
||||
"file": "файл",
|
||||
"advanced": "расширенные",
|
||||
"create": "создать",
|
||||
"loading": "загрузка",
|
||||
"mode": "режим",
|
||||
"views": "{n, plural, =0 {просмотры} =1 {1 просмотр} other {# просмотры}}",
|
||||
"minutes": "{n, plural, =0 {минут} =1 {1 минута} other {# минуты}}",
|
||||
"max": "макс",
|
||||
"share_link": "поделиться ссылкой",
|
||||
"copy_clipboard": "скопировать в буфер обмена",
|
||||
"copied_to_clipboard": "скопировано в буфер обмена",
|
||||
"encrypting": "шифрование",
|
||||
"decrypting": "расшифровка",
|
||||
"uploading": "загрузка",
|
||||
"downloading": "скачивание",
|
||||
"qr_code": "qr код",
|
||||
"password": "пароль"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Легко отправляйте <i>полностью зашифрованные</i> защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.",
|
||||
"explanation": "заметка истечет и будет уничтожена после {type}.",
|
||||
"new_note": "новая заметка",
|
||||
"new_note_notice": "<b>доступность:</b><br />сохранение заметки не гарантируется, поскольку все хранится в оперативной памяти; если она заполнится, самые старые заметки будут удалены.<br />( вероятно, все будет в порядке, просто будьте осторожны.)",
|
||||
"errors": {
|
||||
"note_to_big": "нельзя создать новую заметку. заметка слишком большая",
|
||||
"note_error": "нельзя создать новую заметку. пожалйста попробуйте позднее.",
|
||||
"max": "макс: {n}",
|
||||
"empty_content": "пустая заметка."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "заметка создана."
|
||||
},
|
||||
"advanced": {
|
||||
"explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.",
|
||||
"custom_password": "пользовательский пароль"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "заметка не найдена или была удалена.",
|
||||
"decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. записка уничтожена.",
|
||||
"unsupported_type": "неподдерживаемый тип заметки."
|
||||
},
|
||||
"explanation": "щелкните ниже, чтобы показать и удалить примечание, если счетчик достиг предела",
|
||||
"show_note": "показать заметку",
|
||||
"warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.",
|
||||
"download_all": "скачать всё",
|
||||
"links_found": "ссылки внутри заметки:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "Выбранные файлы",
|
||||
"no_files_selected": "Файлы не выбраны",
|
||||
"clear": "Сброс"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,10 +47,12 @@
|
|||
"explanation": "点击下方按钮即可查看密信,阅后即焚。",
|
||||
"show_note": "查看密信",
|
||||
"warning_will_not_see_again": "你将<b>无法</b>再次查看该密信,请尽快复制到粘贴板。",
|
||||
"download_all": "下载全部"
|
||||
"download_all": "下载全部",
|
||||
"links_found": "注释中找到的链接:"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "已选中的文件",
|
||||
"no_files_selected": "没有文件被选中"
|
||||
"no_files_selected": "没有文件被选中",
|
||||
"clear": "重置"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,29 +13,28 @@
|
|||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@lokalise/node-api": "^9.8.0",
|
||||
"@sveltejs/adapter-static": "^2.0.2",
|
||||
"@sveltejs/kit": "^1.16.3",
|
||||
"@types/dompurify": "^3.0.2",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@zerodevx/svelte-toast": "^0.9.3",
|
||||
"@lokalise/node-api": "^12.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@zerodevx/svelte-toast": "^0.9.5",
|
||||
"adm-zip": "^0.5.10",
|
||||
"dotenv": "^16.0.3",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte-check": "^3.3.2",
|
||||
"svelte-intl-precompile": "^0.12.1",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.5"
|
||||
"dotenv": "^16.4.5",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.6",
|
||||
"svelte-intl-precompile": "^0.12.3",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cryptgeon/shared": "workspace:*",
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"@fontsource/fira-mono": "^5.0.8",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dompurify": "^3.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"occulto": "^2.0.1",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"occulto": "^2.0.3",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"qrious": "^4.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
</div>
|
||||
{/each}
|
||||
<div class="spacer" />
|
||||
<Button on:click={clear}>Clear</Button>
|
||||
<Button on:click={clear}>{$t('file_upload.clear')}</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import { status } from '$lib/stores/status'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import Canvas from './Canvas.svelte'
|
||||
|
@ -35,9 +35,11 @@
|
|||
<Canvas value={url} />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
{#if $status?.theme_new_note_notice}
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
{/if}
|
||||
<br />
|
||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import DOMPurify from 'dompurify'
|
||||
import { saveAs } from 'file-saver'
|
||||
import pkg from 'file-saver'
|
||||
const { saveAs } = pkg
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
|
@ -34,22 +34,29 @@
|
|||
saveAs(f)
|
||||
}
|
||||
|
||||
function contentWithLinks(content: string): string {
|
||||
const replaced = content.replace(
|
||||
RE_URL,
|
||||
(url) => `<a href="${url}" rel="noreferrer">${url}</a>`
|
||||
)
|
||||
return DOMPurify.sanitize(replaced, { USE_PROFILES: { html: true } })
|
||||
}
|
||||
$: links = typeof note.contents === 'string' ? note.contents.match(RE_URL) : []
|
||||
</script>
|
||||
|
||||
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
|
||||
<div data-testid="result">
|
||||
{#if note.meta.type === 'text'}
|
||||
<div class="note">
|
||||
{@html contentWithLinks(note.contents)}
|
||||
{note.contents}
|
||||
</div>
|
||||
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
||||
|
||||
{#if links && links.length}
|
||||
<div class="links">
|
||||
{$t('show.links_found')}
|
||||
<ul>
|
||||
{#each links as link}
|
||||
<li>
|
||||
<a href={link} target="_blank" rel="noopener noreferrer">{link}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each files as file}
|
||||
<div class="note file">
|
||||
|
@ -92,4 +99,20 @@
|
|||
.note.file small {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.links ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
.links ul li {
|
||||
margin-bottom: 0.5rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { AES, Hex, Bytes } from 'occulto'
|
||||
import { AES, Hex } from 'occulto'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { blur } from 'svelte/transition'
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
|||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextArea from '$lib/ui/TextArea.svelte'
|
||||
import type { FileDTO, Note } from '@cryptgeon/shared'
|
||||
import { Adapters, create, PayloadToLargeError } from '@cryptgeon/shared'
|
||||
import { Adapters, PayloadToLargeError, create } from '@cryptgeon/shared'
|
||||
|
||||
let note: Note = {
|
||||
contents: '',
|
||||
|
@ -118,12 +118,14 @@
|
|||
{/if}
|
||||
|
||||
<div class="bottom">
|
||||
<Switch
|
||||
data-testid="switch-file"
|
||||
class="file"
|
||||
label={$t('common.file')}
|
||||
bind:value={isFile}
|
||||
/>
|
||||
{#if $status?.allow_files}
|
||||
<Switch
|
||||
data-testid="switch-file"
|
||||
class="file"
|
||||
label={$t('common.file')}
|
||||
bind:value={isFile}
|
||||
/>
|
||||
{/if}
|
||||
{#if $status?.allow_advanced}
|
||||
<Switch
|
||||
data-testid="switch-advanced"
|
||||
|
@ -149,7 +151,7 @@
|
|||
</p>
|
||||
|
||||
{#if advanced}
|
||||
<div transition:blur={{ duration: 250 }}>
|
||||
<div transition:blur|global={{ duration: 250 }}>
|
||||
<hr />
|
||||
<AdvancedParameters bind:note bind:timeExpiration bind:customPassword />
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { status } from '$lib/stores/status'
|
||||
|
||||
function reset() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<a href="/">
|
||||
<a on:click={reset} href="/">
|
||||
{#if $status?.theme_image}
|
||||
<img alt="logo" src={$status.theme_image} />
|
||||
{:else}
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
<br />
|
||||
you are welcomed to check & audit the
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
||||
source code
|
||||
</a>.
|
||||
source code</a
|
||||
>.
|
||||
</span>
|
||||
</AboutParagraph>
|
||||
|
||||
|
@ -75,6 +75,9 @@
|
|||
<style>
|
||||
section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import adapter from '@sveltejs/adapter-static'
|
||||
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite'
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess([precompileIntl('locales')]),
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"occulto": "^2.0.1"
|
||||
"occulto": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
|
2642
pnpm-lock.yaml
2642
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -88,7 +88,7 @@ export async function checkLinkDoesNotExist(page: Page, link: string) {
|
|||
}
|
||||
|
||||
export async function CLI(...args: string[]) {
|
||||
return await exec('./packages/cli/dist/index.cjs', args, {
|
||||
return await exec('./packages/cli/dist/cli.cjs', args, {
|
||||
env: {
|
||||
...process.env,
|
||||
CRYPTGEON_SERVER: 'http://localhost:1234',
|
||||
|
|
Loading…
Reference in New Issue