mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2024-06-02 05:14:48 +02:00
Compare commits
96 Commits
Author | SHA1 | Date | |
---|---|---|---|
6d2150b0b6 | |||
3a68693be1 | |||
|
a612eec220 | ||
98d3b0d394 | |||
|
6aed2e2756 | ||
6bb527198a | |||
|
7050389316 | ||
0725a0c6f7 | |||
|
c8efcc04fc | ||
19bf155653 | |||
|
9a4e84db62 | ||
32cd3843a7 | |||
9b48d39c82 | |||
|
239e950f8e | ||
|
b00846ce9d | ||
|
e70f06f99f | ||
|
4781882c72 | ||
|
549dfb55db | ||
|
2653a4bccf | ||
|
7213e6c690 | ||
|
8a5f667ff6 | ||
fc3938701e | |||
23b4f81dac | |||
7c68620d8b | |||
eb76fe085a | |||
|
38540b33f2 | ||
39a9ac0dad | |||
ff1b5d500b | |||
1698abe2eb | |||
3036927a45 | |||
f9c26ba81c | |||
752e68e213 | |||
6eb3a59e33 | |||
1a2728d21f | |||
a37a0932e0 | |||
71a33a7939 | |||
|
83033a4b85 | ||
40570bbbaf | |||
|
f591e589d0 | ||
d1eebb04f3 | |||
|
5a76ea7778 | ||
|
45a1af7569 | ||
9578b2bed2 | |||
f94e4e3858 | |||
ffa2c49ea3 | |||
0d850aadfc | |||
a9c646c981 | |||
a2803a13da | |||
deef56776d | |||
b089323990 | |||
|
6002ede685 | ||
8a444ceb88 | |||
|
1e01ccb65a | ||
2dfa9dd248 | |||
|
618e914b55 | ||
|
86f596fa4b | ||
|
dcb4613f66 | ||
c46f80aaa0 | |||
e2711cc887 | |||
e02224216a | |||
1b0d5449a0 | |||
9695d3a63f | |||
22d4efb03e | |||
97741ed73f | |||
c9e5de0f37 | |||
dc1c03d912 | |||
2a75acae3f | |||
815ac4e8ba | |||
7c85c1e621 | |||
a323d48c41 | |||
2bff6a37db | |||
f8223dfc62 | |||
063d073c27 | |||
ac32b97383 | |||
9c9c23d958 | |||
92893a5b2d | |||
ac68f4a540 | |||
83b2fa5372 | |||
3c86f3f3be | |||
80e64ad207 | |||
a5809c216c | |||
fb95a68b0d | |||
b43b802221 | |||
2e89007c83 | |||
fdc2722fb9 | |||
6da28a701e | |||
e6d1e0f44a | |||
6000553b95 | |||
85204776d7 | |||
c2bfe9dd0d | |||
b05841a562 | |||
d7e5a34b14 | |||
13dfd933af | |||
|
74840416f1 | ||
|
9aaad5b910 | ||
|
c246207420 |
|
@ -1,15 +1,15 @@
|
||||||
*
|
*
|
||||||
|
|
||||||
!/packages/backend/src
|
!/packages
|
||||||
!/packages/backend/Cargo.lock
|
!/package.json
|
||||||
!/packages/backend/Cargo.toml
|
!/pnpm-lock.yaml
|
||||||
|
!/pnpm-workspace.yaml
|
||||||
|
|
||||||
!/packages/frontend/locales
|
**/target
|
||||||
!/packages/frontend/src
|
**/node_modules
|
||||||
!/packages/frontend/static
|
**/dist
|
||||||
!/packages/frontend/.npmrc
|
**/bin
|
||||||
!/packages/frontend/package.json
|
**/*.tsbuildinfo
|
||||||
!/packages/frontend/pnpm-lock.yaml
|
**/build
|
||||||
!/packages/frontend/svelte.config.js
|
**/.svelte
|
||||||
!/packages/frontend/tsconfig.json
|
**/.svelte-kit
|
||||||
!/packages/frontend/vite.config.js
|
|
||||||
|
|
38
.github/workflows/docker.yml
vendored
38
.github/workflows/docker.yml
vendored
|
@ -1,38 +0,0 @@
|
||||||
name: Publish
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
with:
|
|
||||||
install: true
|
|
||||||
- name: Docker Labels
|
|
||||||
id: meta
|
|
||||||
uses: crazy-max/ghaction-docker-meta@v2
|
|
||||||
with:
|
|
||||||
images: cupcakearmy/cryptgeon
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
58
.github/workflows/release.yml
vendored
Normal file
58
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
name: Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cli:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'pnpm'
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
- run: npm publish
|
||||||
|
working-directory: ./packages/cli
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: docker/setup-qemu-action@v2
|
||||||
|
- uses: docker/setup-buildx-action@v2
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
- name: Docker Labels
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: cupcakearmy/cryptgeon
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
29
.github/workflows/test.yaml
vendored
29
.github/workflows/test.yaml
vendored
|
@ -10,27 +10,30 @@ jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v1
|
# Node
|
||||||
- uses: docker/setup-buildx-action@v1
|
- uses: pnpm/action-setup@v3
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
cache: 'pnpm'
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
- name: Build docker image
|
|
||||||
run: npm run test:prepare
|
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
npm install playwright
|
pnpm install --frozen-lockfile
|
||||||
npx playwright install --with-deps
|
pnpm exec playwright install --with-deps
|
||||||
|
pnpm run test:prepare
|
||||||
|
|
||||||
- name: Run your tests
|
- name: Run your tests
|
||||||
run: npm test
|
run: pnpm test
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
path: test-results
|
path: test-results
|
||||||
|
|
16
.gitignore
vendored
16
.gitignore
vendored
|
@ -1,14 +1,10 @@
|
||||||
|
.env
|
||||||
|
*.tsbuildinfo
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
bin
|
||||||
|
|
||||||
# Backend
|
|
||||||
target
|
target
|
||||||
|
|
||||||
# Client
|
# Testing
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/.svelte
|
|
||||||
/build
|
|
||||||
/functions
|
|
||||||
.env
|
|
||||||
|
|
||||||
General
|
|
||||||
test-results
|
test-results
|
||||||
|
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -5,7 +5,30 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [2.2.0] - 2023-01-14
|
## [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
|
||||||
|
|
||||||
|
- New CLI 🎉.
|
||||||
|
- Russian language.
|
||||||
|
- Option for reducing note id size (`ID_LENGTH`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved to monorepo.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
693
Cryptgeon.postman_collection.json
Normal file
693
Cryptgeon.postman_collection.json
Normal file
|
@ -0,0 +1,693 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "3aaeac19-4eac-4911-b3c8-912b17a48634",
|
||||||
|
"name": "Cryptgeon",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Notes",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Preview",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}",
|
||||||
|
"description": "Id of the Note"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "This endpoint is to query wether a note exists, without actually opening it. No view limits are used here, as contents of the note are not available, only the `meta` field is returned, which is public."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "200",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}",
|
||||||
|
"description": "Id of the Note"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:24:29 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "404",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}",
|
||||||
|
"description": "Id of the Note"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Not Found",
|
||||||
|
"code": 404,
|
||||||
|
"_postman_previewlanguage": "plain",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:25:26 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"const jsonData = pm.response.json();",
|
||||||
|
"pm.collectionVariables.set('NOTE_ID', jsonData.id)",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 1,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Simple",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 1,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "5 Minutes",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"expiration\": 5,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "3 Views",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 3,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "This endpoint gets the actual contents of a note. It's a `DELETE` endpoint, es it decreases the `view` counter, and deletes the note if `0` is reached."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "200",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:59:07 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\",\n \"contents\": \"Some encrypted content\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "404",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Not Found",
|
||||||
|
"code": 404,
|
||||||
|
"_postman_previewlanguage": "plain",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:59:15 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Status",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get server status",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/status/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"status",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "200",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/status/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"status",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:56:45 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "BASE",
|
||||||
|
"value": "http://localhost:1234/api",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "NOTE_ID",
|
||||||
|
"value": "",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
28
Dockerfile
28
Dockerfile
|
@ -1,31 +1,29 @@
|
||||||
# FRONTEND
|
# FRONTEND
|
||||||
FROM node:16-alpine as client
|
FROM node:20-alpine as client
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
RUN npm install -g pnpm@7
|
COPY . .
|
||||||
COPY ./packages/frontend ./
|
RUN pnpm install --frozen-lockfile
|
||||||
RUN pnpm install
|
|
||||||
RUN pnpm exec svelte-kit sync
|
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
|
|
||||||
# BACKEND
|
# BACKEND
|
||||||
FROM rust:1.64-alpine as backend
|
FROM rust:1.76-alpine as backend
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
RUN apk add libc-dev openssl-dev alpine-sdk
|
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
|
||||||
COPY ./packages/backend/Cargo.* ./
|
|
||||||
# https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html
|
|
||||||
RUN rustup update nightly
|
|
||||||
ENV CARGO_UNSTABLE_SPARSE_REGISTRY=true
|
|
||||||
RUN cargo +nightly fetch
|
|
||||||
COPY ./packages/backend ./
|
COPY ./packages/backend ./
|
||||||
RUN cargo +nightly build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
|
|
||||||
# RUNNER
|
# RUNNER
|
||||||
FROM alpine
|
FROM alpine:3.19
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache curl
|
||||||
COPY --from=backend /tmp/target/release/cryptgeon .
|
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||||
COPY --from=client /tmp/build ./frontend
|
COPY --from=client /tmp/packages/frontend/build ./frontend
|
||||||
ENV FRONTEND_PATH="./frontend"
|
ENV FRONTEND_PATH="./frontend"
|
||||||
ENV REDIS="redis://redis/"
|
ENV REDIS="redis://redis/"
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
73
README.md
73
README.md
|
@ -14,11 +14,12 @@
|
||||||
<a href=""><img src="./.github/lokalise.png" height="50">
|
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
|
|
||||||
EN | [简体中文](README_zh-CN.md)
|
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
|
||||||
|
|
||||||
## About?
|
## About?
|
||||||
|
|
||||||
_cryptgeon_ is a secure, open source sharing note or file service inspired by [_PrivNote_](https://privnote.com)
|
_cryptgeon_ is a secure, open source sharing note or file service inspired by [_PrivNote_](https://privnote.com).
|
||||||
|
It includes a server, a web page and a CLI client.
|
||||||
|
|
||||||
> 🌍 If you want to translate the project feel free to reach out to me.
|
> 🌍 If you want to translate the project feel free to reach out to me.
|
||||||
>
|
>
|
||||||
|
@ -26,10 +27,21 @@ _cryptgeon_ is a secure, open source sharing note or file service inspired by [_
|
||||||
|
|
||||||
## Live Service / Demo
|
## Live Service / Demo
|
||||||
|
|
||||||
|
### Web
|
||||||
|
|
||||||
Check out the live service / demo and see for yourself [cryptgeon.org](https://cryptgeon.org)
|
Check out the live service / demo and see for yourself [cryptgeon.org](https://cryptgeon.org)
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
```
|
||||||
|
npx cryptgeon send text "This is a secret note"
|
||||||
|
```
|
||||||
|
|
||||||
|
For more documentation about the CLI see the [readme](./packages/cli/README.md).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- send text or files
|
||||||
- server cannot decrypt contents due to client side encryption
|
- server cannot decrypt contents due to client side encryption
|
||||||
- view or time constraints
|
- view or time constraints
|
||||||
- in memory, no persistence
|
- in memory, no persistence
|
||||||
|
@ -50,22 +62,27 @@ of the notes even if it tried to.
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
| `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. |
|
| `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_VIEWS` | `100` | Maximal number of views. |
|
||||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
| `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_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||||
| `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` |
|
| `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
|
||||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. 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_TEXT` | `""` | Custom text for replacing the description below the logo |
|
| `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_. |
|
||||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
| `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_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
| `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
|
## 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
|
### Docker
|
||||||
|
|
||||||
|
@ -81,7 +98,7 @@ services:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
# 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/manual/eviction/
|
||||||
command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
|
@ -92,6 +109,14 @@ services:
|
||||||
SIZE_LIMIT: 4 MiB
|
SIZE_LIMIT: 4 MiB
|
||||||
ports:
|
ports:
|
||||||
- 80:8000
|
- 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
|
### NGINX Proxy
|
||||||
|
@ -112,6 +137,7 @@ There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-sy
|
||||||
|
|
||||||
### YouTube Guides
|
### YouTube Guides
|
||||||
|
|
||||||
|
- English by [Webnestify](https://www.youtube.com/watch?v=XAyD42I7wyI)
|
||||||
- English by [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
|
- English by [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
|
||||||
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
||||||
|
|
||||||
|
@ -120,14 +146,13 @@ There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-sy
|
||||||
**Requirements**
|
**Requirements**
|
||||||
|
|
||||||
- `pnpm`: `>=6`
|
- `pnpm`: `>=6`
|
||||||
- `node`: `>=16`
|
- `node`: `>=18`
|
||||||
- `rust`: edition `2021`
|
- `rust`: edition `2021`
|
||||||
|
|
||||||
**Install**
|
**Install**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm --prefix frontend install
|
|
||||||
|
|
||||||
# Also you need cargo watch if you don't already have it installed.
|
# Also you need cargo watch if you don't already have it installed.
|
||||||
# https://lib.rs/crates/cargo-watch
|
# https://lib.rs/crates/cargo-watch
|
||||||
|
@ -147,19 +172,19 @@ Running `pnpm run dev` in the root folder will start the following things:
|
||||||
- redis docker container
|
- redis docker container
|
||||||
- rust backend
|
- rust backend
|
||||||
- client
|
- client
|
||||||
|
- cli
|
||||||
|
|
||||||
You can see the app under [localhost:1234](http://localhost:1234).
|
You can see the app under [localhost:1234](http://localhost:1234).
|
||||||
|
|
||||||
## Tests
|
> 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.
|
Tests are end to end tests written with Playwright.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm run test:prepare
|
pnpm run test:prepare
|
||||||
docker compose up redis -d
|
|
||||||
pnpm run test:server
|
|
||||||
|
|
||||||
# In another terminal.
|
|
||||||
# Use the test or test:local script. The local version only runs in one browser for quicker development.
|
# Use the test or test:local script. The local version only runs in one browser for quicker development.
|
||||||
pnpm run test:local
|
pnpm run test:local
|
||||||
```
|
```
|
||||||
|
@ -168,7 +193,9 @@ pnpm run test:local
|
||||||
|
|
||||||
Please refer to the security section [here](./SECURITY.md).
|
Please refer to the security section [here](./SECURITY.md).
|
||||||
|
|
||||||
###### Attributions
|
---
|
||||||
|
|
||||||
|
_Attributions_
|
||||||
|
|
||||||
- Test data:
|
- Test data:
|
||||||
- Text for tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
|
- Text for tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
|
||||||
|
|
200
README_ES.md
Normal file
200
README_ES.md
Normal file
|
@ -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">
|
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
[EN](README.md) | 简体中文
|
[EN](README.md) | 简体中文 | [ES](README_ES.md)
|
||||||
|
|
||||||
## 关于本项目
|
## 关于本项目
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,19 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/backend"
|
"path": "packages/backend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packages/frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packages/cli"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packages/shared"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"cSpell.words": ["ciphertext", "cryptgeon"],
|
"i18n-ally.localesPaths": ["locales"],
|
||||||
"i18n-ally.enabledFrameworks": ["svelte"],
|
"cSpell.words": ["cryptgeon"]
|
||||||
"i18n-ally.keystyle": "nested",
|
|
||||||
"i18n-ally.localesPaths": ["packages/frontend/locales"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,17 @@ services:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: cupcakearmy/cryptgeon:test
|
|
||||||
build: .
|
build: .
|
||||||
env_file: .dev.env
|
env_file: .dev.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 1234:8000
|
- 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
|
SIZE_LIMIT: 4 MiB
|
||||||
ports:
|
ports:
|
||||||
- 80:8000
|
- 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
|
||||||
|
|
21
package.json
21
package.json
|
@ -2,17 +2,20 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
|
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
|
||||||
"dev:packages": "pnpm --parallel run dev",
|
"dev:packages": "pnpm --parallel run dev",
|
||||||
"dev:proxy": "node proxy.mjs",
|
|
||||||
"dev": "run-p dev:*",
|
"dev": "run-p dev:*",
|
||||||
|
"docker:up": "docker compose -f docker-compose.dev.yaml up",
|
||||||
|
"docker:build": "docker compose -f docker-compose.dev.yaml build",
|
||||||
"test": "playwright test --project chrome firefox safari",
|
"test": "playwright test --project chrome firefox safari",
|
||||||
"test:local": "playwright test --project local",
|
"test:local": "playwright test --project chrome",
|
||||||
"test:server": "docker compose -f docker-compose.dev.yaml up",
|
"test:server": "run-s docker:up",
|
||||||
"test:prepare": "docker compose -f docker-compose.dev.yaml build"
|
"test:prepare": "run-p build docker:build",
|
||||||
|
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.29.2",
|
"@playwright/test": "^1.42.1",
|
||||||
"@types/node": "^16.18.11",
|
"@types/node": "^20.11.28",
|
||||||
"http-proxy": "^1.18.1",
|
"npm-run-all": "^4.1.5",
|
||||||
"npm-run-all": "^4.1.5"
|
"shelljs": "^0.8.5"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@8.15.4"
|
||||||
}
|
}
|
||||||
|
|
794
packages/backend/Cargo.lock
generated
794
packages/backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,15 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cryptgeon"
|
name = "cryptgeon"
|
||||||
version = "2.2.0"
|
version = "2.6.1"
|
||||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
rust-version = "1.76"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "cryptgeon"
|
name = "cryptgeon"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
actix-files = "0.6"
|
actix-files = "0.6"
|
||||||
|
@ -22,4 +21,5 @@ byte-unit = "4"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
redis = "0.21.5"
|
log = "0.4"
|
||||||
|
redis = "0.23"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "backend",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"name": "@cryptgeon/backend",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cargo watch -x 'run --bin cryptgeon'",
|
"dev": "cargo watch -x 'run --bin cryptgeon'",
|
||||||
"build": "cargo build --release",
|
"build": "cargo build --release",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
|
|
||||||
|
use crate::health;
|
||||||
use crate::note;
|
use crate::note;
|
||||||
use crate::status;
|
use crate::status;
|
||||||
|
|
||||||
|
@ -7,6 +8,7 @@ pub fn init(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/api")
|
web::scope("/api")
|
||||||
.service(note::init())
|
.service(note::init())
|
||||||
.service(status::init()),
|
.service(status::init())
|
||||||
|
.service(health::init()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,22 +14,34 @@ lazy_static! {
|
||||||
|
|
||||||
// CONFIG
|
// CONFIG
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref LIMIT: usize =
|
pub static ref LIMIT: usize =
|
||||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get_bytes() as usize;
|
.get_bytes() as usize;
|
||||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||||
.unwrap_or("100".to_string())
|
.unwrap_or("100".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
|
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
|
||||||
.unwrap_or("360".to_string()) // 6 hours in minutes
|
.unwrap_or("360".to_string()) // 6 hours in minutes
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
|
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
|
||||||
.unwrap_or("true".to_string())
|
.unwrap_or("true".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.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
|
// THEME
|
||||||
|
|
3
packages/backend/src/health/mod.rs
Normal file
3
packages/backend/src/health/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod routes;
|
||||||
|
|
||||||
|
pub use routes::*;
|
16
packages/backend/src/health/routes.rs
Normal file
16
packages/backend/src/health/routes.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use actix_web::{get, web, HttpResponse, Responder, Scope};
|
||||||
|
|
||||||
|
use crate::store;
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn get_live() -> impl Responder {
|
||||||
|
if store::can_reach_redis() {
|
||||||
|
return HttpResponse::Ok();
|
||||||
|
} else {
|
||||||
|
return HttpResponse::ServiceUnavailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init() -> Scope {
|
||||||
|
web::scope("/live").service(get_live)
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
middleware::{self, Logger},
|
middleware::{self, Logger},
|
||||||
web, App, HttpServer,
|
web::{self},
|
||||||
|
App, HttpServer,
|
||||||
};
|
};
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
|
use log::error;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
@ -10,6 +12,7 @@ extern crate lazy_static;
|
||||||
mod api;
|
mod api;
|
||||||
mod client;
|
mod client;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod health;
|
||||||
mod note;
|
mod note;
|
||||||
mod size;
|
mod size;
|
||||||
mod status;
|
mod status;
|
||||||
|
@ -20,6 +23,11 @@ async fn main() -> std::io::Result<()> {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
|
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(|| {
|
return HttpServer::new(|| {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(Logger::new("\"%r\" %s %b %T"))
|
.wrap(Logger::new("\"%r\" %s %b %T"))
|
||||||
|
|
|
@ -2,6 +2,8 @@ use bs62;
|
||||||
use ring::rand::SecureRandom;
|
use ring::rand::SecureRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Note {
|
pub struct Note {
|
||||||
pub meta: String,
|
pub meta: String,
|
||||||
|
@ -11,7 +13,9 @@ pub struct Note {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct NoteInfo {}
|
pub struct NoteInfo {
|
||||||
|
pub meta: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct NotePublic {
|
pub struct NotePublic {
|
||||||
|
@ -20,8 +24,13 @@ pub struct NotePublic {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_id() -> String {
|
pub fn generate_id() -> String {
|
||||||
let mut id: [u8; 32] = [0; 32];
|
let mut result = "".to_owned();
|
||||||
|
let mut id: [u8; 1] = [0; 1];
|
||||||
let sr = ring::rand::SystemRandom::new();
|
let sr = ring::rand::SystemRandom::new();
|
||||||
let _ = sr.fill(&mut id);
|
|
||||||
return bs62::encode_data(&id);
|
for _ in 0..*config::ID_LENGTH {
|
||||||
|
let _ = sr.fill(&mut id);
|
||||||
|
result.push_str(&bs62::encode_data(&id));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ async fn one(path: web::Path<NotePath>) -> impl Responder {
|
||||||
let note = store::get(&p.id);
|
let note = store::get(&p.id);
|
||||||
|
|
||||||
match note {
|
match note {
|
||||||
Ok(Some(_)) => HttpResponse::Ok().json(NoteInfo {}),
|
Ok(Some(n)) => HttpResponse::Ok().json(NoteInfo { meta: n.meta }),
|
||||||
Ok(None) => HttpResponse::NotFound().finish(),
|
Ok(None) => HttpResponse::NotFound().finish(),
|
||||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ pub struct Status {
|
||||||
pub max_views: u32,
|
pub max_views: u32,
|
||||||
pub max_expiration: u32,
|
pub max_expiration: u32,
|
||||||
pub allow_advanced: bool,
|
pub allow_advanced: bool,
|
||||||
|
pub allow_files: bool,
|
||||||
|
pub theme_new_note_notice: bool,
|
||||||
// Theme
|
// Theme
|
||||||
pub theme_image: String,
|
pub theme_image: String,
|
||||||
pub theme_text: String,
|
pub theme_text: String,
|
||||||
|
|
|
@ -11,10 +11,12 @@ async fn get_status() -> impl Responder {
|
||||||
max_views: *config::MAX_VIEWS,
|
max_views: *config::MAX_VIEWS,
|
||||||
max_expiration: *config::MAX_EXPIRATION,
|
max_expiration: *config::MAX_EXPIRATION,
|
||||||
allow_advanced: *config::ALLOW_ADVANCED,
|
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_image: config::THEME_IMAGE.to_string(),
|
||||||
theme_text: config::THEME_TEXT.to_string(),
|
theme_text: config::THEME_TEXT.to_string(),
|
||||||
theme_page_title: config::THEME_PAGE_TITLE.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")
|
.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> {
|
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()?;
|
||||||
|
|
54
packages/cli/README.md
Normal file
54
packages/cli/README.md
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# Cryptgeon CLI
|
||||||
|
|
||||||
|
The CLI is a functionally identical way to interact with cryptgeon notes.
|
||||||
|
It supports text, files, expiration, password, etc.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx cryptgeon
|
||||||
|
|
||||||
|
# Or install globally
|
||||||
|
npm -g install cryptgeon
|
||||||
|
cryptgeon
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create simple note
|
||||||
|
cryptgeon send text "Foo bar"
|
||||||
|
|
||||||
|
# Send two files
|
||||||
|
cryptgeon send file my.pdf picture.png
|
||||||
|
|
||||||
|
# 3 views
|
||||||
|
cryptgeon send text "My message" --views 3
|
||||||
|
|
||||||
|
# 10 minutes
|
||||||
|
cryptgeon send text "My message" --minutes 10
|
||||||
|
|
||||||
|
# Custom password
|
||||||
|
cryptgeon send text "My message" --password "1337"
|
||||||
|
|
||||||
|
# Password from stdin
|
||||||
|
echo "1337" | cryptgeon send text "My message"
|
||||||
|
|
||||||
|
# Open a link
|
||||||
|
cryptgeon open https://cryptgeon.org/note/16gOIkxWjCxYNuXM8tCqMUzl...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### Custom server
|
||||||
|
|
||||||
|
The default server is `cryptgeon.org`, however you can use any cryptgeon server by passing the `-s` or `--server` option, or by setting the `CRYPTGEON_SERVER` environment variable.
|
||||||
|
|
||||||
|
### Password
|
||||||
|
|
||||||
|
Optionally, just like in the web ui, you can choose to use a manual password. You can do that by passing the `-p` or `--password` options, or by piping it into stdin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "my pw" | cryptgeon send text "my text"
|
||||||
|
cat pass.txt | cryptgeon send text "my text"
|
||||||
|
```
|
44
packages/cli/package.json
Normal file
44
packages/cli/package.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"bin": {
|
||||||
|
"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": "^12.0.1",
|
||||||
|
"@cryptgeon/shared": "workspace:*",
|
||||||
|
"@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"
|
||||||
|
}
|
||||||
|
}
|
34
packages/cli/scripts/build.js
Executable file
34
packages/cli/scripts/build.js
Executable file
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { build, context } from 'esbuild'
|
||||||
|
import pkg from '../package.json' assert { type: 'json' }
|
||||||
|
|
||||||
|
const common = {
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
platform: 'node',
|
||||||
|
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) {
|
||||||
|
const ctx = await context(cliOptions)
|
||||||
|
ctx.watch()
|
||||||
|
} else {
|
||||||
|
await build(cliOptions)
|
||||||
|
await build(indexOptions)
|
||||||
|
}
|
106
packages/cli/src/cli.ts
Normal file
106
packages/cli/src/cli.ts
Normal file
|
@ -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()
|
89
packages/cli/src/download.ts
Normal file
89
packages/cli/src/download.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { Adapters, get, info, setBase } from '@cryptgeon/shared'
|
||||||
|
import inquirer from 'inquirer'
|
||||||
|
import { access, constants, writeFile } from 'node:fs/promises'
|
||||||
|
import { basename, resolve } from 'node:path'
|
||||||
|
import { AES, Hex } from 'occulto'
|
||||||
|
import pretty from 'pretty-bytes'
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
throw new Error('Note does not exist or is expired')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Password
|
||||||
|
let password: string
|
||||||
|
const derivation = preview?.meta.derivation
|
||||||
|
if (derivation) {
|
||||||
|
if (suggestedPassword) {
|
||||||
|
password = suggestedPassword
|
||||||
|
} else {
|
||||||
|
const response = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
message: 'Note password',
|
||||||
|
name: 'password',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
password = response.password
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
password = url.hash.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password)
|
||||||
|
const note = await get(id)
|
||||||
|
|
||||||
|
const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password')
|
||||||
|
switch (note.meta.type) {
|
||||||
|
case 'file':
|
||||||
|
const files = await Adapters.Files.decrypt(note.contents, key).catch(() => {
|
||||||
|
throw couldNotDecrypt
|
||||||
|
})
|
||||||
|
if (!files) {
|
||||||
|
throw new Error('No files found in note')
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected: typeof files
|
||||||
|
if (all) {
|
||||||
|
selected = files
|
||||||
|
} else {
|
||||||
|
const { names } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
message: 'What files should be saved?',
|
||||||
|
name: 'names',
|
||||||
|
choices: files.map((file) => ({
|
||||||
|
value: file.name,
|
||||||
|
name: `${file.name} - ${file.type} - ${pretty(file.size, { binary: true })}`,
|
||||||
|
checked: true,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
selected = files.filter((file) => names.includes(file.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected.length) throw new Error('No files selected')
|
||||||
|
await Promise.all(
|
||||||
|
selected.map(async (file) => {
|
||||||
|
let filename = resolve(file.name)
|
||||||
|
try {
|
||||||
|
// If exists -> prepend timestamp to not overwrite the current file
|
||||||
|
await access(filename, constants.R_OK)
|
||||||
|
filename = resolve(`${Date.now()}-${file.name}`)
|
||||||
|
} catch {}
|
||||||
|
await writeFile(filename, file.contents)
|
||||||
|
console.log(`Saved: ${basename(filename)}`)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'text':
|
||||||
|
const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(() => {
|
||||||
|
throw couldNotDecrypt
|
||||||
|
})
|
||||||
|
console.log(plaintext)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
4
packages/cli/src/index.ts
Normal file
4
packages/cli/src/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from '@cryptgeon/shared'
|
||||||
|
export * from './download.js'
|
||||||
|
export * from './upload.js'
|
||||||
|
export * from './utils.js'
|
27
packages/cli/src/parsers.ts
Normal file
27
packages/cli/src/parsers.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { InvalidArgumentError, InvalidOptionArgumentError } from '@commander-js/extra-typings'
|
||||||
|
import { accessSync, constants } from 'node:fs'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
|
export function parseFile(value: string, before: string[] = []) {
|
||||||
|
try {
|
||||||
|
const file = resolve(value)
|
||||||
|
accessSync(file, constants.R_OK)
|
||||||
|
return [...before, file]
|
||||||
|
} catch {
|
||||||
|
throw new InvalidArgumentError('cannot access file')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseURL(value: string, _: URL): URL {
|
||||||
|
try {
|
||||||
|
return new URL(value)
|
||||||
|
} catch {
|
||||||
|
throw new InvalidArgumentError('is not a valid url')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseNumber(value: string, _: number): number {
|
||||||
|
const n = parseInt(value, 10)
|
||||||
|
if (isNaN(n)) throw new InvalidOptionArgumentError('invalid number')
|
||||||
|
return n
|
||||||
|
}
|
24
packages/cli/src/stdin.ts
Normal file
24
packages/cli/src/stdin.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export function getStdin(timeout: number = 10): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
// Store the data from stdin in a buffer
|
||||||
|
let buffer = ''
|
||||||
|
let t: NodeJS.Timeout
|
||||||
|
|
||||||
|
const dataHandler = (d: Buffer) => (buffer += d.toString())
|
||||||
|
const endHandler = () => {
|
||||||
|
clearTimeout(t)
|
||||||
|
resolve(buffer.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop listening for data after the timeout, otherwise hangs indefinitely
|
||||||
|
t = setTimeout(() => {
|
||||||
|
process.stdin.removeListener('data', dataHandler)
|
||||||
|
process.stdin.removeListener('end', endHandler)
|
||||||
|
process.stdin.pause()
|
||||||
|
resolve('')
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
process.stdin.on('data', dataHandler)
|
||||||
|
process.stdin.on('end', endHandler)
|
||||||
|
})
|
||||||
|
}
|
45
packages/cli/src/upload.ts
Normal file
45
packages/cli/src/upload.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { readFile, stat } from 'node:fs/promises'
|
||||||
|
import { basename } from 'node:path'
|
||||||
|
|
||||||
|
import { Adapters, BASE, create, FileDTO, Note, NoteMeta } from '@cryptgeon/shared'
|
||||||
|
import mime from 'mime'
|
||||||
|
import { AES, Hex } from 'occulto'
|
||||||
|
|
||||||
|
export 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()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
19
packages/cli/src/utils.ts
Normal file
19
packages/cli/src/utils.ts
Normal file
|
@ -0,0 +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.`)
|
||||||
|
}
|
13
packages/cli/tsconfig.json
Normal file
13
packages/cli/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "es2022",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
engine-strict=true
|
|
|
@ -1,51 +1,58 @@
|
||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"note": "Hinweis",
|
"note": "Notiz",
|
||||||
"file": "Datei",
|
"file": "Datei",
|
||||||
"advanced": "erweitert",
|
"advanced": "Erweiterte Optionen",
|
||||||
"create": "erstellen",
|
"create": "Erstellen",
|
||||||
"loading": "läd",
|
"loading": "Lädt...",
|
||||||
"mode": "Modus",
|
"mode": "Modus",
|
||||||
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
|
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
|
||||||
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
|
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "Link teilen",
|
"share_link": "Link teilen",
|
||||||
"copy_clipboard": "in die Zwischenablage kopieren",
|
"copy_clipboard": "In die Zwischenablage kopieren",
|
||||||
"copied_to_clipboard": "in die Zwischenablage kopiert",
|
"copied_to_clipboard": "In die Zwischenablage kopiert.",
|
||||||
"encrypting": "verschlüsseln",
|
"encrypting": "Wird verschlüsselt...",
|
||||||
"decrypting": "entschlüsselt",
|
"decrypting": "Wird entschlüsselt...",
|
||||||
"uploading": "hochladen",
|
"uploading": "Hochladen",
|
||||||
"downloading": "wird heruntergeladen",
|
"downloading": "Wird heruntergeladen",
|
||||||
"qr_code": "qr-code"
|
"qr_code": "QR-Code",
|
||||||
|
"password": "Passwort"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"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 und wird nach {type} zerstört.",
|
"explanation": "Die Notiz verfällt nach {type}.",
|
||||||
"new_note": "neue Note",
|
"new_note": "Neue Notiz",
|
||||||
"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).",
|
"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": {
|
"errors": {
|
||||||
"note_to_big": "Notiz konnte nicht erstellt werden. Notiz ist zu groß",
|
"note_to_big": "Notiz konnte nicht erstellt werden, da sie zu groß ist.",
|
||||||
"note_error": "konnte keine Notiz erstellen. Bitte versuchen Sie es erneut.",
|
"note_error": "Notiz konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||||
"max": "max: {n}",
|
"max": "max: {n}",
|
||||||
"empty_content": "Notiz ist leer."
|
"empty_content": "Notiz ist leer."
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "notiz erstellt."
|
"note_created": "Notiz wurde erstellt."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Standardmäßig wird für jede Notiz ein generiertes, sicheres Passwort verwendet. Alternativ können Sie ein eigenes Kennwort festlegen, welches nicht im Link enthalten ist.",
|
||||||
|
"custom_password": "Benutzerdefiniertes Passwort"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
|
"not_found": "Notiz konnte nicht gefunden werden oder wurde bereits gelöscht.",
|
||||||
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört.",
|
"decryption_failed": "Notiz konnte nicht entschlüsselt werden. Vermutlich ist das Passwort falsch oder der Link defekt. Die Notiz wurde daher gelöscht.",
|
||||||
"unsupported_type": "nicht unterstützter Notiztyp."
|
"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",
|
"show_note": "Notiz anzeigen",
|
||||||
"warning_will_not_see_again": "haben Sie <b>keine</b> Gelegenheit, die Notiz noch einmal zu sehen.",
|
"warning_will_not_see_again": "ACHTUNG! Sie werden anschließend <b>keine</b> Gelegenheit mehr haben, die Notiz erneut anzusehen.",
|
||||||
"download_all": "alle herunterladen"
|
"download_all": "Alle Dateien herunterladen",
|
||||||
|
"links_found": "Gefundene Links in der Notiz:"
|
||||||
},
|
},
|
||||||
"file_upload": {
|
"file_upload": {
|
||||||
"selected_files": "Ausgewählte Dateien",
|
"selected_files": "Ausgewählte Dateien",
|
||||||
"no_files_selected": "Keine Dateien ausgewählt"
|
"no_files_selected": "Keine Dateien ausgewählt",
|
||||||
|
"clear": "Zurücksetzen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
"decrypting": "decrypting",
|
"decrypting": "decrypting",
|
||||||
"uploading": "uploading",
|
"uploading": "uploading",
|
||||||
"downloading": "downloading",
|
"downloading": "downloading",
|
||||||
"qr_code": "qr code"
|
"qr_code": "qr code",
|
||||||
|
"password": "password"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
||||||
|
@ -31,6 +32,10 @@
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "note created."
|
"note_created": "note created."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "By default, a securely generated password is used for each note. You can however also choose your own password, which is not included in the link.",
|
||||||
|
"custom_password": "custom password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -42,10 +47,12 @@
|
||||||
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
||||||
"show_note": "show note",
|
"show_note": "show note",
|
||||||
"warning_will_not_see_again": "you will <b>not</b> get the chance to see the note again.",
|
"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": {
|
"file_upload": {
|
||||||
"selected_files": "Selected Files",
|
"selected_files": "Selected Files",
|
||||||
"no_files_selected": "No Files Selected"
|
"no_files_selected": "No Files Selected",
|
||||||
|
"clear": "Reset"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,14 @@
|
||||||
"decrypting": "descifrando",
|
"decrypting": "descifrando",
|
||||||
"uploading": "cargando",
|
"uploading": "cargando",
|
||||||
"downloading": "descargando",
|
"downloading": "descargando",
|
||||||
"qr_code": "código qr"
|
"qr_code": "código qr",
|
||||||
|
"password": "contraseña"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"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.",
|
"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}.",
|
"explanation": "la nota expirará y se destruirá después de {type}.",
|
||||||
"new_note": "nueva nota",
|
"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": {
|
"errors": {
|
||||||
"note_to_big": "no se pudo crear la nota. la nota es demasiado grande",
|
"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.",
|
"note_error": "No se ha podido crear la nota. Por favor, inténtelo de nuevo.",
|
||||||
|
@ -31,6 +32,10 @@
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "nota creada."
|
"note_created": "nota creada."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, la cual no se incluye en el enlace.",
|
||||||
|
"custom_password": "contraseña personalizada"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -41,11 +46,13 @@
|
||||||
},
|
},
|
||||||
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
|
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
|
||||||
"show_note": "mostrar nota",
|
"show_note": "mostrar nota",
|
||||||
"warning_will_not_see_again": " <b>no</b> tendrás la oportunidad de volver a ver la 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": {
|
"file_upload": {
|
||||||
"selected_files": "Archivos seleccionados",
|
"selected_files": "Archivos seleccionados",
|
||||||
"no_files_selected": "No hay archivos seleccionados"
|
"no_files_selected": "No hay archivos seleccionados",
|
||||||
|
"clear": "Restablecer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,25 +12,30 @@
|
||||||
"share_link": "partager le lien",
|
"share_link": "partager le lien",
|
||||||
"copy_clipboard": "copier dans le presse-papiers",
|
"copy_clipboard": "copier dans le presse-papiers",
|
||||||
"copied_to_clipboard": "copié dans le presse-papiers",
|
"copied_to_clipboard": "copié dans le presse-papiers",
|
||||||
"encrypting": "cryptage",
|
"encrypting": "chiffrer",
|
||||||
"decrypting": "déchiffrer",
|
"decrypting": "déchiffrer",
|
||||||
"uploading": "téléchargement",
|
"uploading": "téléversement",
|
||||||
"downloading": "téléchargement",
|
"downloading": "téléchargement",
|
||||||
"qr_code": "code qr"
|
"qr_code": "code qr",
|
||||||
|
"password": "mot de passe"
|
||||||
},
|
},
|
||||||
"home": {
|
"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}.",
|
"explanation": "la note expirera et sera détruite après {type}.",
|
||||||
"new_note": "nouvelle note",
|
"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": {
|
"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.",
|
"note_error": "n'a pas pu créer de note. Veuillez réessayer.",
|
||||||
"max": "max: {n}",
|
"max": "max: {n}",
|
||||||
"empty_content": "La note est vide."
|
"empty_content": "La note est vide."
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "note créée."
|
"note_created": "note créée."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Par défaut, un mot de passe généré de manière sécurisée est utilisé pour chaque note. Vous pouvez toutefois choisir votre propre mot de passe, qui n'est pas inclus dans le lien.",
|
||||||
|
"custom_password": "mot de passe personnalisé"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -42,10 +47,12 @@
|
||||||
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
||||||
"show_note": "note de présentation",
|
"show_note": "note de présentation",
|
||||||
"warning_will_not_see_again": "vous <b>n'aurez pas</b> la chance de revoir la note.",
|
"warning_will_not_see_again": "vous <b>n'aurez pas</b> la chance de revoir la note.",
|
||||||
"download_all": "télécharger tout"
|
"download_all": "télécharger tout",
|
||||||
|
"links_found": "liens trouvés à l’intérieur de la note :"
|
||||||
},
|
},
|
||||||
"file_upload": {
|
"file_upload": {
|
||||||
"selected_files": "Fichiers sélectionnés",
|
"selected_files": "Fichiers sélectionnés",
|
||||||
"no_files_selected": "Aucun fichier sélectionné"
|
"no_files_selected": "Aucun fichier sélectionné",
|
||||||
|
"clear": "Réinitialiser"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
"decrypting": "decifrando",
|
"decrypting": "decifrando",
|
||||||
"uploading": "caricamento",
|
"uploading": "caricamento",
|
||||||
"downloading": "scaricando",
|
"downloading": "scaricando",
|
||||||
"qr_code": "codice qr"
|
"qr_code": "codice qr",
|
||||||
|
"password": "password"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.",
|
"intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.",
|
||||||
|
@ -31,6 +32,10 @@
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "nota creata."
|
"note_created": "nota creata."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Per impostazione predefinita, per ogni nota viene utilizzata una password generata in modo sicuro. È tuttavia possibile scegliere la propria password, che non è inclusa nel link.",
|
||||||
|
"custom_password": "password personalizzata"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -41,11 +46,13 @@
|
||||||
},
|
},
|
||||||
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
|
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
|
||||||
"show_note": "mostra la nota",
|
"show_note": "mostra la nota",
|
||||||
"warning_will_not_see_again": " <b>non</b> avrete la possibilità di rivedere 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": {
|
"file_upload": {
|
||||||
"selected_files": "File selezionati",
|
"selected_files": "File selezionati",
|
||||||
"no_files_selected": "Nessun file selezionato"
|
"no_files_selected": "Nessun file selezionato",
|
||||||
|
"clear": "Reset"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
58
packages/frontend/locales/ja.json
Normal file
58
packages/frontend/locales/ja.json
Normal file
|
@ -0,0 +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>可用性: </b> <br />すべてが RAM に保持されるため、メモが保存されるとは限りません。いっぱいになると、最も古いメモが削除されます。 <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": "リセット"
|
||||||
|
}
|
||||||
|
}
|
58
packages/frontend/locales/pl.json
Normal file
58
packages/frontend/locales/pl.json
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"note": "notatka",
|
||||||
|
"file": "plik",
|
||||||
|
"advanced": "zaawansowane",
|
||||||
|
"create": "utwórz",
|
||||||
|
"loading": "ładowanie",
|
||||||
|
"mode": "tryb",
|
||||||
|
"views": "{n, plural, =0 {wyświetleń} =1 {1 wyświetlenie} other {# wyświetleń}}",
|
||||||
|
"minutes": "{n, plural, =0 {minut} =1 {1 minuta} other {# minuty}}",
|
||||||
|
"max": "maks.",
|
||||||
|
"share_link": "link udostępniania",
|
||||||
|
"copy_clipboard": "kopiuj do schowka",
|
||||||
|
"copied_to_clipboard": "skopiowano do schowka",
|
||||||
|
"encrypting": "szyfrowanie",
|
||||||
|
"decrypting": "odszyfrowywanie",
|
||||||
|
"uploading": "wysyłanie",
|
||||||
|
"downloading": "pobieranie",
|
||||||
|
"qr_code": "kod QR",
|
||||||
|
"password": "hasło"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"intro": "Łatwo wysyłaj <i>w pełni zaszyfrowane</i>, bezpieczne notatki lub pliki jednym kliknięciem. Po prostu utwórz notatkę i udostępnij link.",
|
||||||
|
"explanation": "notatka wygaśnie i zostanie zniszczona po {type}.",
|
||||||
|
"new_note": "nowa notatka",
|
||||||
|
"new_note_notice": "<b>dostępność:</b><br />nie ma gwarancji, że notatka będzie przechowywana, ponieważ wszystko jest przechowywane w pamięci RAM, jeśli się zapełni, najstarsze notatki zostaną usunięte.<br />(prawdopodobnie nic się nie stanie, ale warto ostrzec.)",
|
||||||
|
"errors": {
|
||||||
|
"note_to_big": "nie można utworzyć notatki. notatka jest za duża",
|
||||||
|
"note_error": "nie można utworzyć notatki. spróbuj ponownie.",
|
||||||
|
"max": "maks .: {n}",
|
||||||
|
"empty_content": "notatka jest pusta."
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"note_created": "notatka utworzona."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Domyślnie dla każdej notatki używane jest bezpiecznie wygenerowane hasło. Możesz jednak wybrać własne hasło, które nie jest uwzględnione w linku.",
|
||||||
|
"custom_password": "własne hasło"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"errors": {
|
||||||
|
"not_found": "notatka nie została znaleziona lub została już usunięta.",
|
||||||
|
"decryption_failed": "błędne hasło. nie można odszyfrować. prawdopodobnie uszkodzony link. notatka została zniszczona.",
|
||||||
|
"unsupported_type": "nieobsługiwany typ notatki."
|
||||||
|
},
|
||||||
|
"explanation": "kliknij poniżej, aby wyświetlić i usunąć notatkę, jeśli licznik osiągnie swój limit",
|
||||||
|
"show_note": "pokaż notatkę",
|
||||||
|
"warning_will_not_see_again": "<b>nie będziesz mieć</b> możliwości ponownego zobaczenia notatki.",
|
||||||
|
"download_all": "pobierz wszystko",
|
||||||
|
"links_found": "linki znalezione w notatce:"
|
||||||
|
},
|
||||||
|
"file_upload": {
|
||||||
|
"selected_files": "Wybrane pliki",
|
||||||
|
"no_files_selected": "Nie wybrano plików",
|
||||||
|
"clear": "Wyczyść"
|
||||||
|
}
|
||||||
|
}
|
58
packages/frontend/locales/ru.json
Normal file
58
packages/frontend/locales/ru.json
Normal file
|
@ -0,0 +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>доступность:</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": "Сброс"
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,46 +6,53 @@
|
||||||
"create": "创建",
|
"create": "创建",
|
||||||
"loading": "加载中",
|
"loading": "加载中",
|
||||||
"mode": "模式",
|
"mode": "模式",
|
||||||
"views": "{n, plural, =0 {可查看次数} =1 {1 次查看} other {# 次查看}}",
|
"views": "{n, plural, =0 {可读次数} =1 {1 次查看} other {# 次查看}}",
|
||||||
"minutes": "{n, plural, =0 {有效期(分钟)} =1 {1 分钟} other {# 分钟}}",
|
"minutes": "{n, plural, =0 {有效期(分钟)} =1 {1 分钟} other {# 分钟}}",
|
||||||
"max": "最大值",
|
"max": "最大值",
|
||||||
"share_link": "分享链接",
|
"share_link": "分享链接",
|
||||||
"copy_clipboard": "复制到剪切版",
|
"copy_clipboard": "复制到粘贴板",
|
||||||
"copied_to_clipboard": "已复制到剪切板",
|
"copied_to_clipboard": "已成功复制到粘贴板",
|
||||||
"encrypting": "加密",
|
"encrypting": "加密",
|
||||||
"decrypting": "解密",
|
"decrypting": "解密",
|
||||||
"uploading": "上传",
|
"uploading": "上传",
|
||||||
"downloading": "下载",
|
"downloading": "下载",
|
||||||
"qr_code": "二维码"
|
"qr_code": "二维码",
|
||||||
|
"password": "密码"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "一键轻松发送 <i>完全加密的</i> 密信或者文件。只需创建一个密信然后分享链接。",
|
"intro": "飞鸽传书,一键传输完全加密的密信或文件,阅后即焚。",
|
||||||
"explanation": "该密信会在{type}后失效。",
|
"explanation": "该密信会在{type}后失效。",
|
||||||
"new_note": "新建密信",
|
"new_note": "新建密信",
|
||||||
"new_note_notice": "<b>可用性警示:</b><br />由于加密鸽的所有数据是全部保存在内存中的,所以如果加密鸽的可用内存被用光了那么它将会删除最早的密信以释放内存,因此不保证该密信的可用性。<br />(一般情况下是您应该是不会遇到这个问题,只是警示一下。)",
|
"new_note_notice": "<b>提醒:</b><br>密信保存在内存中,如果内存满了,则最早的密信将被删除以释放内存,因此不保证该密信的可用性。一般不会出现这种情况,无需担心。",
|
||||||
"errors": {
|
"errors": {
|
||||||
"note_to_big": "无法创建密信,这个密信太大了!",
|
"note_to_big": "创建失败,密信过大。",
|
||||||
"note_error": "无法创建密信,请再试一遍。",
|
"note_error": "创建失败,请稍后重试。",
|
||||||
"max": "最大文件大小: {n}",
|
"max": "次数上限:{n}",
|
||||||
"empty_content": "密信为空!"
|
"empty_content": "密信不能为空。"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "注释创建。"
|
"note_created": "密信创建成功。"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "默认情况下,每个笔记都使用安全生成的密码。但是,您也可以选择您自己的密码,该密码未包含在链接中。",
|
||||||
|
"custom_password": "自定义密码"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": "下载全部",
|
||||||
|
"links_found": "注释中找到的链接:"
|
||||||
},
|
},
|
||||||
"file_upload": {
|
"file_upload": {
|
||||||
"selected_files": "已选中的文件",
|
"selected_files": "已选中的文件",
|
||||||
"no_files_selected": "没有文件被选中"
|
"no_files_selected": "没有文件被选中",
|
||||||
|
"clear": "重置"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"name": "@cryptgeon/web",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "svelte-kit sync",
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
@ -11,29 +13,28 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lokalise/node-api": "^9.5.0",
|
"@lokalise/node-api": "^12.1.0",
|
||||||
"@sveltejs/adapter-static": "^1.0.2",
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
"@sveltejs/kit": "^1.1.0",
|
"@sveltejs/kit": "^2.5.2",
|
||||||
"@types/dompurify": "^2.4.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@zerodevx/svelte-toast": "^0.7.2",
|
"@zerodevx/svelte-toast": "^0.9.5",
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.4.5",
|
||||||
"svelte": "^3.55.1",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^2.10.3",
|
"svelte-check": "^3.6.6",
|
||||||
"svelte-intl-precompile": "^0.10.1",
|
"svelte-intl-precompile": "^0.12.3",
|
||||||
"svelte-preprocess": "^4.10.7",
|
"tslib": "^2.6.2",
|
||||||
"tslib": "^2.4.1",
|
"typescript": "^5.3.3",
|
||||||
"typescript": "^4.9.4",
|
"vite": "^5.1.7"
|
||||||
"vite": "^4.0.4"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/fira-mono": "^4.5.10",
|
"@cryptgeon/shared": "workspace:*",
|
||||||
|
"@fontsource/fira-mono": "^5.0.8",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"dompurify": "^2.4.3",
|
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"occulto": "2.0.0",
|
"occulto": "^2.0.3",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.1.1",
|
||||||
"qrious": "^4.0.2"
|
"qrious": "^4.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,15 @@ import https from 'https'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
function exit(msg) {
|
||||||
|
console.error(msg)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
const apiKey = process.env.LOKALISE_API_KEY
|
const apiKey = process.env.LOKALISE_API_KEY
|
||||||
const project_id = process.env.LOKALISE_PROJECT
|
const project_id = process.env.LOKALISE_PROJECT
|
||||||
if (!apiKey) throw new Error('No API Key set for Lokalize! Set with "LOKALISE_API_KEY"')
|
if (!apiKey) exit('No API Key set for Lokalize! Set with "LOKALISE_API_KEY"')
|
||||||
if (!project_id) throw new Error('No project id set for Lokalize! Set with "LOKALISE_PROJECT"')
|
if (!project_id) exit('No project id set for Lokalize! Set with "LOKALISE_PROJECT"')
|
||||||
const client = new LokaliseApi({ apiKey })
|
const client = new LokaliseApi({ apiKey })
|
||||||
|
|
||||||
const WGet = (url) =>
|
const WGet = (url) =>
|
||||||
|
|
|
@ -92,7 +92,7 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
*:disabled,
|
*:disabled,
|
||||||
*[disabled='true'] {
|
.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,3 +126,13 @@ fieldset {
|
||||||
.tr {
|
.tr {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid var(--ui-bg-1);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
9
packages/frontend/src/app.d.ts
vendored
Normal file
9
packages/frontend/src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
// and what to do when importing types
|
||||||
|
declare namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
3
packages/frontend/src/global.d.ts
vendored
3
packages/frontend/src/global.d.ts
vendored
|
@ -1,3 +0,0 @@
|
||||||
/// <reference types="@sveltejs/kit" />
|
|
||||||
/// <reference types="svelte" />
|
|
||||||
/// <reference types="vite/client" />
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { AES, Bytes, type TypedArray } from 'occulto'
|
|
||||||
import type { EncryptedFileDTO, FileDTO } from './api'
|
|
||||||
|
|
||||||
abstract class CryptAdapter<T> {
|
|
||||||
abstract encrypt(plaintext: T, key: TypedArray): Promise<string>
|
|
||||||
abstract decrypt(ciphertext: string, key: TypedArray): Promise<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
class CryptTextAdapter implements CryptAdapter<string> {
|
|
||||||
async encrypt(plaintext: string, key: TypedArray) {
|
|
||||||
return await AES.encrypt(Bytes.encode(plaintext), key)
|
|
||||||
}
|
|
||||||
async decrypt(ciphertext: string, key: TypedArray) {
|
|
||||||
return Bytes.decode(await AES.decrypt(ciphertext, key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CryptBlobAdapter implements CryptAdapter<Blob> {
|
|
||||||
async encrypt(plaintext: Blob, key: TypedArray) {
|
|
||||||
return await AES.encrypt(new Uint8Array(await plaintext.arrayBuffer()), key)
|
|
||||||
}
|
|
||||||
|
|
||||||
async decrypt(ciphertext: string, key: TypedArray) {
|
|
||||||
const plaintext = await AES.decrypt(ciphertext, key)
|
|
||||||
return new Blob([plaintext], { type: 'application/octet-stream' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
|
|
||||||
async encrypt(plaintext: FileDTO[], key: TypedArray) {
|
|
||||||
const adapter = new CryptBlobAdapter()
|
|
||||||
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
contents: await adapter.encrypt(file.contents, key),
|
|
||||||
}))
|
|
||||||
return JSON.stringify(await Promise.all(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
async decrypt(ciphertext: string, key: TypedArray) {
|
|
||||||
const adapter = new CryptBlobAdapter()
|
|
||||||
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
|
|
||||||
const files: FileDTO[] = await Promise.all(
|
|
||||||
data.map(async (file) => ({
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
contents: await adapter.decrypt(file.contents, key),
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Adapters = {
|
|
||||||
Text: new CryptTextAdapter(),
|
|
||||||
Blob: new CryptBlobAdapter(),
|
|
||||||
Files: new CryptFilesAdapter(),
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
export type NoteMeta = { type: 'text' | 'file' }
|
|
||||||
|
|
||||||
export type Note = {
|
|
||||||
contents: string
|
|
||||||
meta: NoteMeta
|
|
||||||
views?: number
|
|
||||||
expiration?: number
|
|
||||||
}
|
|
||||||
export type NoteInfo = {}
|
|
||||||
export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
|
||||||
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
|
||||||
|
|
||||||
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
|
||||||
contents: Blob
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
|
||||||
contents: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CallOptions = {
|
|
||||||
url: string
|
|
||||||
method: string
|
|
||||||
body?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PayloadToLargeError extends Error {}
|
|
||||||
|
|
||||||
export async function call(options: CallOptions) {
|
|
||||||
const response = await fetch('/api/' + options.url, {
|
|
||||||
method: options.method,
|
|
||||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
||||||
mode: 'cors',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 413) throw new PayloadToLargeError()
|
|
||||||
else throw new Error('API call failed')
|
|
||||||
}
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function create(note: Note) {
|
|
||||||
const { meta, ...rest } = note
|
|
||||||
const body: NoteCreate = {
|
|
||||||
...rest,
|
|
||||||
meta: JSON.stringify(meta),
|
|
||||||
}
|
|
||||||
const data = await call({
|
|
||||||
url: 'notes/',
|
|
||||||
method: 'post',
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
return data as { id: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function get(id: string): Promise<NotePublic> {
|
|
||||||
const data = await call({
|
|
||||||
url: `notes/${id}`,
|
|
||||||
method: 'delete',
|
|
||||||
})
|
|
||||||
const { contents, meta } = data
|
|
||||||
return {
|
|
||||||
contents,
|
|
||||||
meta: JSON.parse(meta) as NoteMeta,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function info(id: string): Promise<NoteInfo> {
|
|
||||||
const data = await call({
|
|
||||||
url: `notes/${id}`,
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
}
|
|
|
@ -1,24 +1,8 @@
|
||||||
import { call } from '$lib/api'
|
import { status as getStatus, type Status } from '@cryptgeon/shared'
|
||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
export type Status = {
|
|
||||||
version: string
|
|
||||||
max_size: number
|
|
||||||
max_views: number
|
|
||||||
max_expiration: number
|
|
||||||
allow_advanced: boolean
|
|
||||||
theme_image: string
|
|
||||||
theme_text: string
|
|
||||||
theme_favicon: string
|
|
||||||
theme_page_title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const status = writable<null | Status>(null)
|
export const status = writable<null | Status>(null)
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
const data = await call({
|
status.set(await getStatus())
|
||||||
url: 'status/',
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
status.set(data)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +1,82 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
import type { Note } from '$lib/api'
|
|
||||||
import { status } from '$lib/stores/status'
|
import { status } from '$lib/stores/status'
|
||||||
import Switch from '$lib/ui/Switch.svelte'
|
import Switch from '$lib/ui/Switch.svelte'
|
||||||
import TextInput from '$lib/ui/TextInput.svelte'
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
|
import type { Note } from '@cryptgeon/shared'
|
||||||
|
|
||||||
export let note: Note
|
export let note: Note
|
||||||
export let timeExpiration = false
|
export let timeExpiration = false
|
||||||
|
export let customPassword: string | null = null
|
||||||
|
|
||||||
|
let hasCustomPassword = false
|
||||||
|
|
||||||
|
$: if (!hasCustomPassword) customPassword = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="flex col">
|
||||||
<TextInput
|
<div class="flex">
|
||||||
data-testid="field-views"
|
<TextInput
|
||||||
type="number"
|
data-testid="field-views"
|
||||||
label={$t('common.views', { values: { n: 0 } })}
|
type="number"
|
||||||
bind:value={note.views}
|
label={$t('common.views', { values: { n: 0 } })}
|
||||||
disabled={timeExpiration}
|
bind:value={note.views}
|
||||||
max={$status?.max_views}
|
disabled={timeExpiration}
|
||||||
min={1}
|
max={$status?.max_views}
|
||||||
validate={(v) =>
|
min={1}
|
||||||
($status && v <= $status?.max_views && v > 0) ||
|
validate={(v) =>
|
||||||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
($status && v <= $status?.max_views && v > 0) ||
|
||||||
/>
|
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
||||||
<div class="middle-switch">
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
data-testid="switch-advanced-toggle"
|
data-testid="switch-advanced-toggle"
|
||||||
label={$t('common.mode')}
|
label={$t('common.mode')}
|
||||||
bind:value={timeExpiration}
|
bind:value={timeExpiration}
|
||||||
color={false}
|
color={false}
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
data-testid="field-expiration"
|
||||||
|
type="number"
|
||||||
|
label={$t('common.minutes', { values: { n: 0 } })}
|
||||||
|
bind:value={note.expiration}
|
||||||
|
disabled={!timeExpiration}
|
||||||
|
max={$status?.max_expiration}
|
||||||
|
validate={(v) =>
|
||||||
|
($status && v < $status?.max_expiration) ||
|
||||||
|
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<Switch
|
||||||
|
data-testid="custom-password"
|
||||||
|
bind:value={hasCustomPassword}
|
||||||
|
label={$t('home.advanced.custom_password')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
data-testid="password"
|
||||||
|
type="password"
|
||||||
|
bind:value={customPassword}
|
||||||
|
label={$t('common.password')}
|
||||||
|
disabled={!hasCustomPassword}
|
||||||
|
random
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{$t('home.advanced.explanation')}
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
|
||||||
data-testid="field-expiration"
|
|
||||||
type="number"
|
|
||||||
label={$t('common.minutes', { values: { n: 0 } })}
|
|
||||||
bind:value={note.expiration}
|
|
||||||
disabled={!timeExpiration}
|
|
||||||
max={$status?.max_expiration}
|
|
||||||
validate={(v) =>
|
|
||||||
($status && v < $status?.max_expiration) ||
|
|
||||||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.middle-switch {
|
.flex {
|
||||||
margin: 0 1rem;
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.col {
|
||||||
display: flex;
|
gap: 1.5rem;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
// @ts-ignore
|
||||||
import QR from 'qrious'
|
import QR from 'qrious'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
import type { FileDTO } from '$lib/api'
|
|
||||||
import Button from '$lib/ui/Button.svelte'
|
import Button from '$lib/ui/Button.svelte'
|
||||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||||
|
import type { FileDTO } from '@cryptgeon/shared'
|
||||||
|
|
||||||
export let label: string = ''
|
export let label: string = ''
|
||||||
export let files: FileDTO[] = []
|
export let files: FileDTO[] = []
|
||||||
|
|
||||||
function fileToDTO(file: File): FileDTO {
|
async function fileToDTO(file: File): Promise<FileDTO> {
|
||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
contents: file,
|
contents: new Uint8Array(await file.arrayBuffer()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onInput(e: Event) {
|
async function onInput(e: Event) {
|
||||||
const input = e.target as HTMLInputElement
|
const input = e.target as HTMLInputElement
|
||||||
if (input?.files?.length) {
|
if (input?.files?.length) {
|
||||||
files = [...files, ...Array.from(input.files).map(fileToDTO)]
|
const toAdd = await Promise.all(Array.from(input.files).map(fileToDTO))
|
||||||
|
files = [...files, ...toAdd]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
<Button on:click={clear}>Clear</Button>
|
<Button on:click={clear}>{$t('file_upload.clear')}</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
export type NoteResult = {
|
export type NoteResult = {
|
||||||
password: string
|
|
||||||
id: string
|
id: string
|
||||||
|
password?: string
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
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 Canvas from './Canvas.svelte'
|
||||||
|
|
||||||
export let result: NoteResult
|
export let result: NoteResult
|
||||||
|
|
||||||
$: url = `${window.location.origin}/note/${result.id}#${result.password}`
|
let url = `${window.location.origin}/note/${result.id}`
|
||||||
|
if (result.password) url += `#${result.password}`
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
|
@ -34,9 +35,11 @@
|
||||||
<Canvas value={url} />
|
<Canvas value={url} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
{#if $status?.theme_new_note_notice}
|
||||||
{@html $t('home.new_note_notice')}
|
<p>
|
||||||
</p>
|
{@html $t('home.new_note_notice')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
<br />
|
<br />
|
||||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DOMPurify from 'dompurify'
|
import pkg from 'file-saver'
|
||||||
import { saveAs } from 'file-saver'
|
const { saveAs } = pkg
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
import type { FileDTO, NotePublic } from '$lib/api'
|
|
||||||
import Button from '$lib/ui/Button.svelte'
|
import Button from '$lib/ui/Button.svelte'
|
||||||
import { copy } from '$lib/utils'
|
import { copy } from '$lib/utils'
|
||||||
|
import type { FileDTO, NotePublic } from '@cryptgeon/shared'
|
||||||
|
|
||||||
export let note: DecryptedNote
|
export let note: DecryptedNote
|
||||||
|
|
||||||
|
@ -34,22 +34,29 @@
|
||||||
saveAs(f)
|
saveAs(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
function contentWithLinks(content: string): string {
|
$: links = typeof note.contents === 'string' ? note.contents.match(RE_URL) : []
|
||||||
const replaced = content.replace(
|
|
||||||
RE_URL,
|
|
||||||
(url) => `<a href="${url}" rel="noreferrer">${url}</a>`
|
|
||||||
)
|
|
||||||
return DOMPurify.sanitize(replaced, { USE_PROFILES: { html: true } })
|
|
||||||
}
|
|
||||||
</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>
|
||||||
<div data-testid="result">
|
<div data-testid="result">
|
||||||
{#if note.meta.type === 'text'}
|
{#if note.meta.type === 'text'}
|
||||||
<div class="note">
|
<div class="note">
|
||||||
{@html contentWithLinks(note.contents)}
|
{note.contents}
|
||||||
</div>
|
</div>
|
||||||
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
<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}
|
{:else}
|
||||||
{#each files as file}
|
{#each files as file}
|
||||||
<div class="note file">
|
<div class="note file">
|
||||||
|
@ -92,4 +99,20 @@
|
||||||
.note.file small {
|
.note.file small {
|
||||||
padding-left: 1rem;
|
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>
|
</style>
|
||||||
|
|
|
@ -4,43 +4,35 @@
|
||||||
export let color = true
|
export let color = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {...$$restProps}>
|
<label {...$$restProps}>
|
||||||
<label class="switch">
|
<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" />
|
</label>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
label {
|
||||||
height: 3.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 4rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch input {
|
label input {
|
||||||
opacity: 0;
|
display: none;
|
||||||
width: 0;
|
}
|
||||||
height: 0;
|
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
position: absolute;
|
display: block;
|
||||||
|
width: 4rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border: 2px solid var(--ui-bg-1);
|
border: 2px solid var(--ui-bg-1);
|
||||||
background-color: var(--ui-bg-0);
|
background-color: var(--ui-bg-0);
|
||||||
transition: var(--ui-anim);
|
|
||||||
transform: translateY(1.2rem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider:before {
|
.slider:before {
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<small disabled={$$restProps.disabled}>
|
<small class:disabled={$$restProps.disabled}>
|
||||||
{label}
|
{label}
|
||||||
{#if valid !== true}
|
{#if valid !== true}
|
||||||
<span class="error-text">{valid}</span>
|
<span class="error-text">{valid}</span>
|
||||||
|
@ -54,6 +54,7 @@
|
||||||
label {
|
label {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
label > small {
|
label > small {
|
||||||
|
|
|
@ -3,9 +3,6 @@
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import { blur } from 'svelte/transition'
|
import { blur } from 'svelte/transition'
|
||||||
|
|
||||||
import { Adapters } from '$lib/adapters'
|
|
||||||
import type { FileDTO, Note } from '$lib/api'
|
|
||||||
import { create, PayloadToLargeError } from '$lib/api'
|
|
||||||
import { status } from '$lib/stores/status'
|
import { status } from '$lib/stores/status'
|
||||||
import { notify } from '$lib/toast'
|
import { notify } from '$lib/toast'
|
||||||
import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte'
|
import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte'
|
||||||
|
@ -16,6 +13,8 @@
|
||||||
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||||
import Switch from '$lib/ui/Switch.svelte'
|
import Switch from '$lib/ui/Switch.svelte'
|
||||||
import TextArea from '$lib/ui/TextArea.svelte'
|
import TextArea from '$lib/ui/TextArea.svelte'
|
||||||
|
import type { FileDTO, Note } from '@cryptgeon/shared'
|
||||||
|
import { Adapters, PayloadToLargeError, create } from '@cryptgeon/shared'
|
||||||
|
|
||||||
let note: Note = {
|
let note: Note = {
|
||||||
contents: '',
|
contents: '',
|
||||||
|
@ -28,6 +27,7 @@
|
||||||
let advanced = false
|
let advanced = false
|
||||||
let isFile = false
|
let isFile = false
|
||||||
let timeExpiration = false
|
let timeExpiration = false
|
||||||
|
let customPassword: string | null = null
|
||||||
let description = ''
|
let description = ''
|
||||||
let loading: string | null = null
|
let loading: string | null = null
|
||||||
|
|
||||||
|
@ -58,13 +58,14 @@
|
||||||
try {
|
try {
|
||||||
loading = $t('common.encrypting')
|
loading = $t('common.encrypting')
|
||||||
|
|
||||||
const key = await AES.generateKey()
|
const derived = customPassword && (await AES.derive(customPassword))
|
||||||
const password = await Hex.encode(key)
|
const key = derived ? derived[0] : await AES.generateKey()
|
||||||
|
|
||||||
const data: Note = {
|
const data: Note = {
|
||||||
contents: '',
|
contents: '',
|
||||||
meta: note.meta,
|
meta: note.meta,
|
||||||
}
|
}
|
||||||
|
if (derived) data.meta.derivation = derived[1]
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
if (files.length === 0) throw new EmptyContentError()
|
if (files.length === 0) throw new EmptyContentError()
|
||||||
data.contents = await Adapters.Files.encrypt(files, key)
|
data.contents = await Adapters.Files.encrypt(files, key)
|
||||||
|
@ -78,8 +79,8 @@
|
||||||
loading = $t('common.uploading')
|
loading = $t('common.uploading')
|
||||||
const response = await create(data)
|
const response = await create(data)
|
||||||
result = {
|
result = {
|
||||||
password: password,
|
|
||||||
id: response.id,
|
id: response.id,
|
||||||
|
password: customPassword ? undefined : Hex.encode(key),
|
||||||
}
|
}
|
||||||
notify.success($t('home.messages.note_created'))
|
notify.success($t('home.messages.note_created'))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -117,12 +118,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<Switch
|
{#if $status?.allow_files}
|
||||||
data-testid="switch-file"
|
<Switch
|
||||||
class="file"
|
data-testid="switch-file"
|
||||||
label={$t('common.file')}
|
class="file"
|
||||||
bind:value={isFile}
|
label={$t('common.file')}
|
||||||
/>
|
bind:value={isFile}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{#if $status?.allow_advanced}
|
{#if $status?.allow_advanced}
|
||||||
<Switch
|
<Switch
|
||||||
data-testid="switch-advanced"
|
data-testid="switch-advanced"
|
||||||
|
@ -148,9 +151,9 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if advanced}
|
{#if advanced}
|
||||||
<div transition:blur={{ duration: 250 }}>
|
<div transition:blur|global={{ duration: 250 }}>
|
||||||
<br />
|
<hr />
|
||||||
<AdvancedParameters bind:note bind:timeExpiration />
|
<AdvancedParameters bind:note bind:timeExpiration bind:customPassword />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--ui-bg-0-85);
|
background-color: var(--ui-bg-0-85);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { status } from '$lib/stores/status'
|
import { status } from '$lib/stores/status'
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<a href="/">
|
<a on:click={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}
|
||||||
|
|
|
@ -40,8 +40,8 @@
|
||||||
<br />
|
<br />
|
||||||
you are welcomed to check & audit the
|
you are welcomed to check & audit the
|
||||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
||||||
source code
|
source code</a
|
||||||
</a>.
|
>.
|
||||||
</span>
|
</span>
|
||||||
</AboutParagraph>
|
</AboutParagraph>
|
||||||
|
|
||||||
|
@ -75,6 +75,9 @@
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
|
|
@ -1,31 +1,35 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Hex } from 'occulto'
|
import { AES, Hex } from 'occulto'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
import { Adapters } from '$lib/adapters'
|
|
||||||
import { get, info } from '$lib/api'
|
|
||||||
import Button from '$lib/ui/Button.svelte'
|
import Button from '$lib/ui/Button.svelte'
|
||||||
import Loader from '$lib/ui/Loader.svelte'
|
import Loader from '$lib/ui/Loader.svelte'
|
||||||
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
||||||
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
|
import { Adapters, get, info, type NoteMeta } from '@cryptgeon/shared'
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
|
|
||||||
export let data: PageData
|
export let data: PageData
|
||||||
|
|
||||||
let id = data.id
|
let id = data.id
|
||||||
let password: string
|
let password: string | null = null
|
||||||
let note: DecryptedNote | null = null
|
let note: DecryptedNote | null = null
|
||||||
let exists = false
|
let exists = false
|
||||||
|
let meta: NoteMeta | null = null
|
||||||
|
|
||||||
let loading: string | null = null
|
let loading: string | null = null
|
||||||
let error: string | null = null
|
let error: string | null = null
|
||||||
|
|
||||||
|
$: valid = !!password?.length
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Check if note exists
|
// Check if note exists
|
||||||
try {
|
try {
|
||||||
loading = $t('common.loading')
|
loading = $t('common.loading')
|
||||||
password = window.location.hash.slice(1)
|
password = window.location.hash.slice(1)
|
||||||
await info(id)
|
const note = await info(id)
|
||||||
|
meta = note.meta
|
||||||
exists = true
|
exists = true
|
||||||
} catch {
|
} catch {
|
||||||
exists = false
|
exists = false
|
||||||
|
@ -39,11 +43,18 @@
|
||||||
*/
|
*/
|
||||||
async function show() {
|
async function show() {
|
||||||
try {
|
try {
|
||||||
|
if (!valid) {
|
||||||
|
error = $t('show.errors.no_password')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load note
|
||||||
error = null
|
error = null
|
||||||
loading = $t('common.downloading')
|
loading = $t('common.downloading')
|
||||||
const data = await get(id)
|
const data = await get(id)
|
||||||
loading = $t('common.decrypting')
|
loading = $t('common.decrypting')
|
||||||
const key = await Hex.decode(password)
|
const derived = meta?.derivation && (await AES.derive(password!, meta.derivation))
|
||||||
|
const key = derived ? derived[0] : Hex.decode(password!)
|
||||||
switch (data.meta.type) {
|
switch (data.meta.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
note = {
|
note = {
|
||||||
|
@ -78,9 +89,18 @@
|
||||||
<form on:submit|preventDefault={show}>
|
<form on:submit|preventDefault={show}>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<p>{$t('show.explanation')}</p>
|
<p>{$t('show.explanation')}</p>
|
||||||
<Button data-testid="show-note-button" type="submit">{$t('show.show_note')}</Button>
|
{#if meta?.derivation}
|
||||||
|
<TextInput
|
||||||
|
data-testid="show-note-password"
|
||||||
|
type="password"
|
||||||
|
bind:value={password}
|
||||||
|
label={$t('common.password')}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Button disabled={!valid} data-testid="show-note-button" type="submit"
|
||||||
|
>{$t('show.show_note')}</Button
|
||||||
|
>
|
||||||
{#if error}
|
{#if error}
|
||||||
<br />
|
|
||||||
<p class="error-text">
|
<p class="error-text">
|
||||||
{error}
|
{error}
|
||||||
<br />
|
<br />
|
||||||
|
@ -98,4 +118,10 @@
|
||||||
.loader {
|
.loader {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import adapter from '@sveltejs/adapter-static'
|
import adapter from '@sveltejs/adapter-static'
|
||||||
import preprocess from 'svelte-preprocess'
|
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
preprocess: preprocess(),
|
preprocess: vitePreprocess([precompileIntl('locales')]),
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
fallback: 'index.html',
|
fallback: 'index.html',
|
||||||
|
|
12
packages/proxy/package.json
Normal file
12
packages/proxy/package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "@cryptgeon/proxy",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./proxy.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"http-proxy": "^1.18.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ proxy.on('error', function (err, req, res) {
|
||||||
|
|
||||||
const server = http.createServer(function (req, res) {
|
const server = http.createServer(function (req, res) {
|
||||||
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
|
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
|
||||||
proxy.web(req, res, { target })
|
proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 })
|
||||||
})
|
})
|
||||||
server.listen(1234)
|
server.listen(1234)
|
||||||
console.log('Proxy on http://localhost:1234')
|
console.log('Proxy on http://localhost:1234')
|
22
packages/shared/package.json
Normal file
22
packages/shared/package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "@cryptgeon/shared",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc -w",
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"occulto": "^2.0.3"
|
||||||
|
}
|
||||||
|
}
|
61
packages/shared/src/adapters.ts
Normal file
61
packages/shared/src/adapters.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { AES, Bytes, type TypedArray } from 'occulto'
|
||||||
|
import type { EncryptedFileDTO, FileDTO } from './api'
|
||||||
|
|
||||||
|
abstract class CryptAdapter<T> {
|
||||||
|
abstract encrypt(plaintext: T, key: TypedArray): Promise<string>
|
||||||
|
abstract decrypt(ciphertext: string, key: TypedArray): Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptTextAdapter implements CryptAdapter<string> {
|
||||||
|
async encrypt(plaintext: string, key: TypedArray) {
|
||||||
|
return await AES.encrypt(Bytes.encode(plaintext), key)
|
||||||
|
}
|
||||||
|
async decrypt(ciphertext: string, key: TypedArray) {
|
||||||
|
return Bytes.decode(await AES.decrypt(ciphertext, key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptBlobAdapter implements CryptAdapter<TypedArray> {
|
||||||
|
async encrypt(plaintext: TypedArray, key: TypedArray) {
|
||||||
|
return await AES.encrypt(plaintext, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(ciphertext: string, key: TypedArray) {
|
||||||
|
return await AES.decrypt(ciphertext, key)
|
||||||
|
// const plaintext = await AES.decrypt(ciphertext, key)
|
||||||
|
// return new Blob([plaintext], { type: 'application/octet-stream' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
|
||||||
|
async encrypt(plaintext: FileDTO[], key: TypedArray) {
|
||||||
|
const adapter = new CryptBlobAdapter()
|
||||||
|
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
contents: await adapter.encrypt(file.contents, key),
|
||||||
|
}))
|
||||||
|
return JSON.stringify(await Promise.all(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(ciphertext: string, key: TypedArray) {
|
||||||
|
const adapter = new CryptBlobAdapter()
|
||||||
|
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
|
||||||
|
const files: FileDTO[] = await Promise.all(
|
||||||
|
data.map(async (file) => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
contents: await adapter.decrypt(file.contents, key),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Adapters = {
|
||||||
|
Text: new CryptTextAdapter(),
|
||||||
|
Blob: new CryptBlobAdapter(),
|
||||||
|
Files: new CryptFilesAdapter(),
|
||||||
|
}
|
116
packages/shared/src/api.ts
Normal file
116
packages/shared/src/api.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import type { KeyData, TypedArray } from 'occulto'
|
||||||
|
|
||||||
|
export type NoteMeta = {
|
||||||
|
type: 'text' | 'file'
|
||||||
|
derivation?: KeyData
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Note = {
|
||||||
|
contents: string
|
||||||
|
meta: NoteMeta
|
||||||
|
views?: number
|
||||||
|
expiration?: number
|
||||||
|
}
|
||||||
|
export type NoteInfo = Pick<Note, 'meta'>
|
||||||
|
export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
||||||
|
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
||||||
|
|
||||||
|
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
||||||
|
contents: TypedArray
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
||||||
|
contents: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallOptions = {
|
||||||
|
url: string
|
||||||
|
method: string
|
||||||
|
body?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PayloadToLargeError extends Error {}
|
||||||
|
|
||||||
|
export let BASE = ''
|
||||||
|
|
||||||
|
export function setBase(url: string) {
|
||||||
|
BASE = url
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function call(options: CallOptions) {
|
||||||
|
const response = await fetch(BASE + '/api/' + options.url, {
|
||||||
|
method: options.method,
|
||||||
|
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||||
|
mode: 'cors',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 413) throw new PayloadToLargeError()
|
||||||
|
else throw new Error('API call failed')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(note: Note) {
|
||||||
|
const { meta, ...rest } = note
|
||||||
|
const body: NoteCreate = {
|
||||||
|
...rest,
|
||||||
|
meta: JSON.stringify(meta),
|
||||||
|
}
|
||||||
|
const data = await call({
|
||||||
|
url: 'notes/',
|
||||||
|
method: 'post',
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
return data as { id: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(id: string): Promise<NotePublic> {
|
||||||
|
const data = await call({
|
||||||
|
url: `notes/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
})
|
||||||
|
const { contents, meta } = data
|
||||||
|
const note = {
|
||||||
|
contents,
|
||||||
|
meta: JSON.parse(meta),
|
||||||
|
} satisfies NotePublic
|
||||||
|
if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt))
|
||||||
|
return note
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function info(id: string): Promise<NoteInfo> {
|
||||||
|
const data = await call({
|
||||||
|
url: `notes/${id}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
const { meta } = data
|
||||||
|
const note = {
|
||||||
|
meta: JSON.parse(meta),
|
||||||
|
} satisfies NoteInfo
|
||||||
|
if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt))
|
||||||
|
return note
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Status = {
|
||||||
|
version: string
|
||||||
|
max_size: number
|
||||||
|
max_views: number
|
||||||
|
max_expiration: number
|
||||||
|
allow_advanced: boolean
|
||||||
|
theme_image: string
|
||||||
|
theme_text: string
|
||||||
|
theme_favicon: string
|
||||||
|
theme_page_title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function status() {
|
||||||
|
const data = await call({
|
||||||
|
url: 'status/',
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
return data as Status
|
||||||
|
}
|
2
packages/shared/src/index.ts
Normal file
2
packages/shared/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './adapters.js'
|
||||||
|
export * from './api.js'
|
12
packages/shared/tsconfig.json
Normal file
12
packages/shared/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"composite": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "es2022",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ const config: PlaywrightTestConfig = {
|
||||||
outputDir: './test-results',
|
outputDir: './test-results',
|
||||||
testDir: './test',
|
testDir: './test',
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
testIgnore: ['file/too-big.spec.ts'],
|
|
||||||
|
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'docker compose -f docker-compose.dev.yaml up',
|
command: 'docker compose -f docker-compose.dev.yaml up',
|
||||||
|
@ -22,11 +21,10 @@ const config: PlaywrightTestConfig = {
|
||||||
{ name: 'chrome', use: { ...devices['Desktop Chrome'] } },
|
{ name: 'chrome', use: { ...devices['Desktop Chrome'] } },
|
||||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||||
{ name: 'safari', use: { ...devices['Desktop Safari'] } },
|
{ name: 'safari', use: { ...devices['Desktop Safari'] } },
|
||||||
{
|
|
||||||
name: 'local',
|
{ name: 'cli', use: { ...devices['Desktop Chrome'] }, grep: [/@cli/] },
|
||||||
use: { ...devices['Desktop Chrome'] },
|
{ name: 'web', use: { ...devices['Desktop Chrome'] }, grep: [/@web/] },
|
||||||
// testMatch: 'file/too-big.spec.ts',
|
{ name: 'cross', use: { ...devices['Desktop Chrome'] }, grep: [/@cross/] },
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2877
pnpm-lock.yaml
2877
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
33
test/cli/file/simple.spec.ts
Normal file
33
test/cli/file/simple.spec.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { basename } from 'node:path'
|
||||||
|
import { Files, getFileChecksum, rm, tmpFile } from '../../files'
|
||||||
|
import { CLI, getLinkFromCLI } from '../../utils'
|
||||||
|
|
||||||
|
test.describe('file @cli', () => {
|
||||||
|
test('simple', async ({ page }) => {
|
||||||
|
const file = await tmpFile(Files.Image)
|
||||||
|
const checksum = await getFileChecksum(file)
|
||||||
|
const note = await CLI('send', 'file', file)
|
||||||
|
const link = getLinkFromCLI(note.stdout)
|
||||||
|
await rm(file)
|
||||||
|
|
||||||
|
await CLI('open', link, '--all')
|
||||||
|
const c = await getFileChecksum(basename(file))
|
||||||
|
await rm(basename(file))
|
||||||
|
test.expect(checksum).toBe(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('simple with password', async ({ page }) => {
|
||||||
|
const file = await tmpFile(Files.Image)
|
||||||
|
const password = 'password'
|
||||||
|
const checksum = await getFileChecksum(file)
|
||||||
|
const note = await CLI('send', 'file', file, '--password', password)
|
||||||
|
const link = getLinkFromCLI(note.stdout)
|
||||||
|
await rm(file)
|
||||||
|
|
||||||
|
await CLI('open', link, '--all', '--password', password)
|
||||||
|
const c = await getFileChecksum(basename(file))
|
||||||
|
await rm(basename(file))
|
||||||
|
test.expect(checksum).toBe(c)
|
||||||
|
})
|
||||||
|
})
|
23
test/cli/text/simple.spec.ts
Normal file
23
test/cli/text/simple.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { CLI, getLinkFromCLI } from '../../utils'
|
||||||
|
|
||||||
|
test.describe('text @cli', () => {
|
||||||
|
test('simple', async ({ page }) => {
|
||||||
|
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||||
|
const note = await CLI('send', 'text', text)
|
||||||
|
const link = getLinkFromCLI(note.stdout)
|
||||||
|
|
||||||
|
const retrieved = await CLI('open', link)
|
||||||
|
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('simple with password', async ({ page }) => {
|
||||||
|
const text = `Endless prejudice endless play derive joy eternal-return selfish burying.`
|
||||||
|
const password = 'password'
|
||||||
|
const note = await CLI('send', 'text', text, '--password', password)
|
||||||
|
const link = getLinkFromCLI(note.stdout)
|
||||||
|
|
||||||
|
const retrieved = await CLI('open', link, '--password', password)
|
||||||
|
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||||
|
})
|
||||||
|
})
|
53
test/cross/file/simple.spec.ts
Normal file
53
test/cross/file/simple.spec.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { CLI, checkLinkForDownload, checkLinkForText, createNote, getLinkFromCLI } from '../../utils'
|
||||||
|
import { Files, getFileChecksum, rm, tmpFile } from '../../files'
|
||||||
|
import { basename } from 'path'
|
||||||
|
|
||||||
|
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||||
|
const password = 'password'
|
||||||
|
|
||||||
|
test.describe('text @cross', () => {
|
||||||
|
test('cli to web', async ({ page }) => {
|
||||||
|
const file = await tmpFile(Files.Image)
|
||||||
|
const checksum = await getFileChecksum(file)
|
||||||
|
const note = await CLI('send', 'file', file)
|
||||||
|
const link = getLinkFromCLI(note.stdout)
|
||||||
|
await rm(file)
|
||||||
|
|
||||||
|
await checkLinkForDownload(page, { link, text: basename(file), checksum })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('cli to web with password', async ({ page }) => {
|
||||||
|
const file = await tmpFile(Files.Image)
|
||||||
|
const checksum = await getFileChecksum(file)
|
||||||
|
const note = await CLI('send', 'file', file, '--password', password)
|
||||||
|
const link = getLinkFromCLI(note.stdout)
|
||||||
|
await rm(file)
|
||||||
|
|
||||||
|
await checkLinkForDownload(page, { link, text: basename(file), checksum, password })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('web to cli', async ({ page }) => {
|
||||||
|
const files = [Files.Image]
|
||||||
|
const checksum = await getFileChecksum(files[0])
|
||||||
|
const link = await createNote(page, { files })
|
||||||
|
|
||||||
|
const filename = basename(files[0])
|
||||||
|
await CLI('open', link, '--all')
|
||||||
|
const c = await getFileChecksum(filename)
|
||||||
|
await rm(basename(filename))
|
||||||
|
test.expect(checksum).toBe(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('web to cli with password', async ({ page }) => {
|
||||||
|
const files = [Files.Image]
|
||||||
|
const checksum = await getFileChecksum(files[0])
|
||||||
|
const link = await createNote(page, { files, password })
|
||||||
|
|
||||||
|
const filename = basename(files[0])
|
||||||
|
await CLI('open', link, '--all', '--password', password)
|
||||||
|
const c = await getFileChecksum(filename)
|
||||||
|
await rm(basename(filename))
|
||||||
|
test.expect(checksum).toBe(c)
|
||||||
|
})
|
||||||
|
})
|
32
test/cross/text/simple.spec.ts
Normal file
32
test/cross/text/simple.spec.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { CLI, checkLinkForText, createNote, getLinkFromCLI } from '../../utils'
|
||||||
|
|
||||||
|
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||||
|
const password = 'password'
|
||||||
|
|
||||||
|
test.describe('text @cross', () => {
|
||||||
|
test('cli to web', async ({ page }) => {
|
||||||
|
const note = await CLI('send', 'text', text)
|
||||||
|
const link = getLinkFromCLI(note.stdout)
|
||||||
|
|
||||||
|
await checkLinkForText(page, { link, text })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('web to cli', async ({ page }) => {
|
||||||
|
const link = await createNote(page, { text })
|
||||||
|
const retrieved = await CLI('open', link)
|
||||||
|
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('cli to web with password', async ({ page }) => {
|
||||||
|
const note = await CLI('send', 'text', text, '--password', password)
|
||||||
|
const link = getLinkFromCLI(note.stdout)
|
||||||
|
await checkLinkForText(page, { link, text, password })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('web to cli with password', async ({ page }) => {
|
||||||
|
const link = await createNote(page, { text, password })
|
||||||
|
const retrieved = await CLI('open', link, '--password', password)
|
||||||
|
test.expect(retrieved.stdout.trim()).toBe(text)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +0,0 @@
|
||||||
export default {
|
|
||||||
PDF: 'test/assets/AES.pdf',
|
|
||||||
Image: 'test/assets/image.jpg',
|
|
||||||
Zip: 'test/assets/Pigeons.zip',
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { test } from '@playwright/test'
|
|
||||||
import { checkLinkForDownload, createNote, getFileChecksum } from '../utils'
|
|
||||||
import Files from './files'
|
|
||||||
|
|
||||||
test('multiple', async ({ page }) => {
|
|
||||||
const files = [Files.PDF, Files.Image]
|
|
||||||
const checksums = await Promise.all(files.map(getFileChecksum))
|
|
||||||
const link = await createNote(page, { files, views: 2 })
|
|
||||||
await checkLinkForDownload(page, link, 'image.jpg', checksums[1])
|
|
||||||
await checkLinkForDownload(page, link, 'AES.pdf', checksums[0])
|
|
||||||
})
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { test } from '@playwright/test'
|
|
||||||
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote, getFileChecksum } from '../utils'
|
|
||||||
import Files from './files'
|
|
||||||
|
|
||||||
test('simple pdf', async ({ page }) => {
|
|
||||||
const files = [Files.PDF]
|
|
||||||
const link = await createNote(page, { files })
|
|
||||||
await checkLinkForText(page, link, 'AES.pdf')
|
|
||||||
await checkLinkDoesNotExist(page, link)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('pdf content', async ({ page }) => {
|
|
||||||
const files = [Files.PDF]
|
|
||||||
const checksum = await getFileChecksum(files[0])
|
|
||||||
const link = await createNote(page, { files })
|
|
||||||
await checkLinkForDownload(page, link, 'AES.pdf', checksum)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('image content', async ({ page }) => {
|
|
||||||
const files = [Files.Image]
|
|
||||||
const checksum = await getFileChecksum(files[0])
|
|
||||||
const link = await createNote(page, { files })
|
|
||||||
await checkLinkForDownload(page, link, 'image.jpg', checksum)
|
|
||||||
})
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { test } from '@playwright/test'
|
|
||||||
import { createNote } from '../utils'
|
|
||||||
import Files from './files'
|
|
||||||
|
|
||||||
test('to big zip', async ({ page }) => {
|
|
||||||
const files = [Files.Zip]
|
|
||||||
const link = await createNote(page, { files, error: 'note is to big' })
|
|
||||||
})
|
|
25
test/files.ts
Normal file
25
test/files.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { createHash } from 'crypto'
|
||||||
|
import { cp as cpFN, rm as rmFN } from 'fs'
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
export const cp = promisify(cpFN)
|
||||||
|
export const rm = promisify(rmFN)
|
||||||
|
|
||||||
|
export const Files = {
|
||||||
|
PDF: 'test/assets/AES.pdf',
|
||||||
|
Image: 'test/assets/image.jpg',
|
||||||
|
Zip: 'test/assets/Pigeons.zip',
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFileChecksum(file: string) {
|
||||||
|
const buffer = await readFile(file)
|
||||||
|
const hash = createHash('sha3-256').update(buffer).digest('hex')
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tmpFile(file: string) {
|
||||||
|
const name = `./tmp/${Math.random().toString(36).substring(7)}`
|
||||||
|
await cp(file, name)
|
||||||
|
return name
|
||||||
|
}
|
37
test/text.ts
37
test/text.ts
|
@ -1,37 +0,0 @@
|
||||||
import { expect, test, type Page } from '@playwright/test'
|
|
||||||
|
|
||||||
async function createNote(page: Page, text: string): Promise<string> {
|
|
||||||
await page.goto('/')
|
|
||||||
await page.locator('textarea').click()
|
|
||||||
await page.locator('textarea').fill(text)
|
|
||||||
await page.locator('button:has-text("create")').click()
|
|
||||||
|
|
||||||
await page.locator('[data-testid="share-link"]').click()
|
|
||||||
const shareLink = await page.locator('[data-testid="share-link"]').inputValue()
|
|
||||||
return shareLink
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkLinkForText(page: Page, link: string, text: string) {
|
|
||||||
await page.goto(link)
|
|
||||||
await page.locator('[data-testid="show-note-button"]').click()
|
|
||||||
expect(await page.locator('[data-testid="result"] >> .note').innerText()).toBe(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkLinkDoesNotExist(page: Page, link: string) {
|
|
||||||
await page.goto('/') // Required due to firefox: https://github.com/microsoft/playwright/issues/15781
|
|
||||||
await page.goto(link)
|
|
||||||
await expect(page.locator('main')).toContainText('note was not found or was already deleted')
|
|
||||||
}
|
|
||||||
|
|
||||||
test('simple', async ({ page }) => {
|
|
||||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
|
||||||
const shareLink = await createNote(page, text)
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('only shown once', async ({ page }) => {
|
|
||||||
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
|
|
||||||
const shareLink = await createNote(page, text)
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
await checkLinkDoesNotExist(page, shareLink)
|
|
||||||
})
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { test } from '@playwright/test'
|
|
||||||
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../utils'
|
|
||||||
|
|
||||||
test('1 minute', async ({ page }) => {
|
|
||||||
const text = `Virtues value ascetic revaluation sea dead strong burying.`
|
|
||||||
const minutes = 1
|
|
||||||
const timeout = minutes * 60_000
|
|
||||||
test.setTimeout(timeout * 2)
|
|
||||||
const shareLink = await createNote(page, { text, expiration: minutes })
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
await page.waitForTimeout(timeout)
|
|
||||||
await checkLinkDoesNotExist(page, shareLink)
|
|
||||||
})
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { test } from '@playwright/test'
|
|
||||||
import { checkLinkForText, createNote } from '../utils'
|
|
||||||
|
|
||||||
test('simple', async ({ page }) => {
|
|
||||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
|
||||||
const shareLink = await createNote(page, { text })
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
})
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { test } from '@playwright/test'
|
|
||||||
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../utils'
|
|
||||||
|
|
||||||
test('only shown once', async ({ page }) => {
|
|
||||||
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
|
|
||||||
const shareLink = await createNote(page, { text })
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
await checkLinkDoesNotExist(page, shareLink)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('view 3 times', async ({ page }) => {
|
|
||||||
const text = `Justice holiest overcome fearful strong ultimate holiest christianity.`
|
|
||||||
const shareLink = await createNote(page, { text, views: 3 })
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
await checkLinkForText(page, shareLink, text)
|
|
||||||
await checkLinkDoesNotExist(page, shareLink)
|
|
||||||
})
|
|
|
@ -1,15 +1,25 @@
|
||||||
import { expect, type Page } from '@playwright/test'
|
import { expect, type Page } from '@playwright/test'
|
||||||
import { createHash } from 'crypto'
|
import { execFile } from 'node:child_process'
|
||||||
import { readFile } from 'fs/promises'
|
import { promisify } from 'node:util'
|
||||||
|
import { getFileChecksum } from './files'
|
||||||
|
|
||||||
type CreatePage = { text?: string; files?: string[]; views?: number; expiration?: number; error?: string }
|
const exec = promisify(execFile)
|
||||||
|
|
||||||
|
type CreatePage = {
|
||||||
|
text?: string
|
||||||
|
files?: string[]
|
||||||
|
views?: number
|
||||||
|
expiration?: number
|
||||||
|
error?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
export async function createNote(page: Page, options: CreatePage): Promise<string> {
|
export async function createNote(page: Page, options: CreatePage): Promise<string> {
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
|
|
||||||
if (options.text) {
|
if (options.text) {
|
||||||
await page.locator('[data-testid="text-field"]').fill(options.text)
|
await page.getByTestId('text-field').fill(options.text)
|
||||||
} else if (options.files) {
|
} else if (options.files) {
|
||||||
await page.locator('[data-testid="switch-file"]').click()
|
await page.getByTestId('switch-file').click()
|
||||||
|
|
||||||
const [fileChooser] = await Promise.all([
|
const [fileChooser] = await Promise.all([
|
||||||
page.waitForEvent('filechooser'),
|
page.waitForEvent('filechooser'),
|
||||||
|
@ -18,13 +28,16 @@ export async function createNote(page: Page, options: CreatePage): Promise<strin
|
||||||
await fileChooser.setFiles(options.files)
|
await fileChooser.setFiles(options.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.views || options.expiration || options.password) await page.getByTestId('switch-advanced').click()
|
||||||
if (options.views) {
|
if (options.views) {
|
||||||
await page.locator('[data-testid="switch-advanced"]').click()
|
await page.getByTestId('field-views').fill(options.views.toString())
|
||||||
await page.locator('[data-testid="field-views"]').fill(options.views.toString())
|
|
||||||
} else if (options.expiration) {
|
} else if (options.expiration) {
|
||||||
await page.locator('[data-testid="switch-advanced"]').click()
|
await page.getByTestId('switch-advanced-toggle').click()
|
||||||
await page.locator('[data-testid="switch-advanced-toggle"]').click()
|
await page.getByTestId('field-expiration').fill(options.expiration.toString())
|
||||||
await page.locator('[data-testid="field-expiration"]').fill(options.expiration.toString())
|
}
|
||||||
|
if (options.password) {
|
||||||
|
await page.getByTestId('custom-password').click()
|
||||||
|
await page.getByTestId('password').fill(options.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.locator('button:has-text("create")').click()
|
await page.locator('button:has-text("create")').click()
|
||||||
|
@ -33,29 +46,39 @@ export async function createNote(page: Page, options: CreatePage): Promise<strin
|
||||||
await expect(page.locator('.error-text')).toContainText(options.error, { timeout: 60_000 })
|
await expect(page.locator('.error-text')).toContainText(options.error, { timeout: 60_000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareLink = await page.locator('[data-testid="share-link"]').inputValue()
|
// Return share link
|
||||||
return shareLink
|
return await page.getByTestId('share-link').inputValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkLinkForDownload(page: Page, link: string, text: string, checksum: string) {
|
type CheckLinkBase = {
|
||||||
|
link: string
|
||||||
|
text: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkLinkForDownload(page: Page, options: CheckLinkBase & { checksum: string }) {
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await page.goto(link)
|
await page.goto(options.link)
|
||||||
await page.locator('[data-testid="show-note-button"]').click()
|
if (options.password) await page.getByTestId('show-note-password').fill(options.password)
|
||||||
|
await page.getByTestId('show-note-button').click()
|
||||||
|
|
||||||
const [download] = await Promise.all([
|
const [download] = await Promise.all([
|
||||||
page.waitForEvent('download'),
|
page.waitForEvent('download'),
|
||||||
page.locator(`[data-testid="result"] >> text=${text}`).click(),
|
page.getByTestId(`result`).locator(`text=${options.text}`).click(),
|
||||||
])
|
])
|
||||||
const path = await download.path()
|
const path = await download.path()
|
||||||
if (!path) throw new Error('Download failed')
|
if (!path) throw new Error('Download failed')
|
||||||
const cs = await getFileChecksum(path)
|
const cs = await getFileChecksum(path)
|
||||||
await expect(cs).toBe(checksum)
|
await expect(cs).toBe(options.checksum)
|
||||||
}
|
}
|
||||||
export async function checkLinkForText(page: Page, link: string, text: string) {
|
|
||||||
|
export async function checkLinkForText(page: Page, options: CheckLinkBase) {
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await page.goto(link)
|
await page.goto(options.link)
|
||||||
await page.locator('[data-testid="show-note-button"]').click()
|
if (options.password) await page.getByTestId('show-note-password').fill(options.password)
|
||||||
await expect(await page.locator('[data-testid="result"] >> .note').innerText()).toContain(text)
|
await page.getByTestId('show-note-button').click()
|
||||||
|
const text = await page.getByTestId('result').locator('.note').innerText()
|
||||||
|
await expect(text).toContain(options.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkLinkDoesNotExist(page: Page, link: string) {
|
export async function checkLinkDoesNotExist(page: Page, link: string) {
|
||||||
|
@ -64,8 +87,17 @@ export async function checkLinkDoesNotExist(page: Page, link: string) {
|
||||||
await expect(page.locator('main')).toContainText('note was not found or was already deleted')
|
await expect(page.locator('main')).toContainText('note was not found or was already deleted')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileChecksum(file: string) {
|
export async function CLI(...args: string[]) {
|
||||||
const buffer = await readFile(file)
|
return await exec('./packages/cli/dist/cli.cjs', args, {
|
||||||
const hash = createHash('sha3-256').update(buffer).digest('hex')
|
env: {
|
||||||
return hash
|
...process.env,
|
||||||
|
CRYPTGEON_SERVER: 'http://localhost:1234',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLinkFromCLI(output: string): string {
|
||||||
|
const match = output.match(/(https?:\/\/[^\s]+)/)
|
||||||
|
if (!match) throw new Error('No link found in CLI output')
|
||||||
|
return match[0]
|
||||||
}
|
}
|
||||||
|
|
13
test/web/file/multiple.spec.ts
Normal file
13
test/web/file/multiple.spec.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { Files, getFileChecksum } from '../../files'
|
||||||
|
import { checkLinkForDownload, createNote } from '../../utils'
|
||||||
|
|
||||||
|
test.describe('@web', () => {
|
||||||
|
test('multiple', async ({ page }) => {
|
||||||
|
const files = [Files.PDF, Files.Image]
|
||||||
|
const checksums = await Promise.all(files.map(getFileChecksum))
|
||||||
|
const link = await createNote(page, { files, views: 2 })
|
||||||
|
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum: checksums[1] })
|
||||||
|
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum: checksums[0] })
|
||||||
|
})
|
||||||
|
})
|
34
test/web/file/simple.spec.ts
Normal file
34
test/web/file/simple.spec.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { Files, getFileChecksum } from '../../files'
|
||||||
|
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote } from '../../utils'
|
||||||
|
|
||||||
|
test.describe('@web', () => {
|
||||||
|
test('simple pdf', async ({ page }) => {
|
||||||
|
const files = [Files.PDF]
|
||||||
|
const link = await createNote(page, { files })
|
||||||
|
await checkLinkForText(page, { link, text: 'AES.pdf' })
|
||||||
|
await checkLinkDoesNotExist(page, link)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pdf content', async ({ page }) => {
|
||||||
|
const files = [Files.PDF]
|
||||||
|
const checksum = await getFileChecksum(files[0])
|
||||||
|
const link = await createNote(page, { files })
|
||||||
|
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('image content', async ({ page }) => {
|
||||||
|
const files = [Files.Image]
|
||||||
|
const checksum = await getFileChecksum(files[0])
|
||||||
|
const link = await createNote(page, { files })
|
||||||
|
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('simple pdf with password', async ({ page }) => {
|
||||||
|
const files = [Files.PDF]
|
||||||
|
const password = 'password'
|
||||||
|
const link = await createNote(page, { files, password })
|
||||||
|
await checkLinkForText(page, { link, text: 'AES.pdf', password })
|
||||||
|
await checkLinkDoesNotExist(page, link)
|
||||||
|
})
|
||||||
|
})
|
10
test/web/file/too-big.spec.ts
Normal file
10
test/web/file/too-big.spec.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { createNote } from '../../utils'
|
||||||
|
import { Files } from '../../files'
|
||||||
|
|
||||||
|
test.describe('@web', () => {
|
||||||
|
test.skip('to big zip', async ({ page }) => {
|
||||||
|
const files = [Files.Zip]
|
||||||
|
const link = await createNote(page, { files, error: 'note is to big' })
|
||||||
|
})
|
||||||
|
})
|
16
test/web/text/expiration.spec.ts
Normal file
16
test/web/text/expiration.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils'
|
||||||
|
|
||||||
|
test.describe('@web', () => {
|
||||||
|
test('1 minute', async ({ page }) => {
|
||||||
|
const text = `Virtues value ascetic revaluation sea dead strong burying.`
|
||||||
|
const minutes = 1
|
||||||
|
const timeout = minutes * 60_000
|
||||||
|
test.setTimeout(timeout * 2)
|
||||||
|
const link = await createNote(page, { text, expiration: minutes })
|
||||||
|
await checkLinkForText(page, { link, text })
|
||||||
|
await checkLinkForText(page, { link, text })
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await checkLinkDoesNotExist(page, link)
|
||||||
|
})
|
||||||
|
})
|
17
test/web/text/simple.spec.ts
Normal file
17
test/web/text/simple.spec.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { checkLinkForText, createNote } from '../../utils'
|
||||||
|
|
||||||
|
test.describe('@web', () => {
|
||||||
|
test('simple', async ({ page }) => {
|
||||||
|
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of deceive play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||||
|
const link = await createNote(page, { text })
|
||||||
|
await checkLinkForText(page, { link, text })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('simple with password', async ({ page }) => {
|
||||||
|
const text = 'Foo bar'
|
||||||
|
const password = '123'
|
||||||
|
const shareLink = await createNote(page, { text, password })
|
||||||
|
await checkLinkForText(page, { link: shareLink, text, password })
|
||||||
|
})
|
||||||
|
})
|
20
test/web/text/views.spec.ts
Normal file
20
test/web/text/views.spec.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils'
|
||||||
|
|
||||||
|
test.describe('@web', () => {
|
||||||
|
test('only shown once', async ({ page }) => {
|
||||||
|
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
|
||||||
|
const link = await createNote(page, { text })
|
||||||
|
await checkLinkForText(page, { link, text })
|
||||||
|
await checkLinkDoesNotExist(page, link)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('view 3 times', async ({ page }) => {
|
||||||
|
const text = `Justice holiest overcome fearful strong ultimate holiest christianity.`
|
||||||
|
const link = await createNote(page, { text, views: 3 })
|
||||||
|
await checkLinkForText(page, { link, text })
|
||||||
|
await checkLinkForText(page, { link, text })
|
||||||
|
await checkLinkForText(page, { link, text })
|
||||||
|
await checkLinkDoesNotExist(page, link)
|
||||||
|
})
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user