Compare commits
7 Commits
v2.3.3
...
v2.0.0-rc.
Author | SHA1 | Date | |
---|---|---|---|
0829d78481 | |||
a4ffde78e4 | |||
0e47dab6d4 | |||
8d5e348f56 | |||
d852edc3c8 | |||
2bb256b07e | |||
ff36d375ea |
@@ -1,15 +1,15 @@
|
|||||||
*
|
*
|
||||||
|
|
||||||
!/packages
|
!/backend/src
|
||||||
!/package.json
|
!/backend/Cargo.lock
|
||||||
!/pnpm-lock.yaml
|
!/backend/Cargo.toml
|
||||||
!/pnpm-workspace.yaml
|
|
||||||
|
|
||||||
**/target
|
!/frontend/locales
|
||||||
**/node_modules
|
!/frontend/src
|
||||||
**/dist
|
!/frontend/static
|
||||||
**/bin
|
!/frontend/.npmrc
|
||||||
**/*.tsbuildinfo
|
!/frontend/package.json
|
||||||
**/build
|
!/frontend/pnpm-lock.yaml
|
||||||
**/.svelte
|
!/frontend/svelte.config.js
|
||||||
**/.svelte-kit
|
!/frontend/tsconfig.json
|
||||||
|
!/frontend/vite.config.js
|
||||||
|
1
.gitattributes
vendored
@@ -1,2 +1 @@
|
|||||||
*.afdesign filter=lfs diff=lfs merge=lfs -text
|
*.afdesign filter=lfs diff=lfs merge=lfs -text
|
||||||
test/assets/** filter=lfs diff=lfs merge=lfs -text
|
|
||||||
|
41
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
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
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
58
.github/workflows/release.yml
vendored
@@ -1,58 +0,0 @@
|
|||||||
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 }}
|
|
39
.github/workflows/test.yaml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Node
|
|
||||||
- uses: pnpm/action-setup@v2
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
cache: 'pnpm'
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
- uses: docker/setup-qemu-action@v2
|
|
||||||
- uses: docker/setup-buildx-action@v2
|
|
||||||
with:
|
|
||||||
install: true
|
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
pnpm exec playwright install --with-deps
|
|
||||||
pnpm run test:prepare
|
|
||||||
|
|
||||||
- name: Run your tests
|
|
||||||
run: pnpm test
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: test-results
|
|
||||||
path: test-results
|
|
15
.gitignore
vendored
@@ -1,10 +1,11 @@
|
|||||||
.env
|
|
||||||
*.tsbuildinfo
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
bin
|
|
||||||
|
|
||||||
|
# Backend
|
||||||
target
|
target
|
||||||
|
|
||||||
# Testing
|
# Client
|
||||||
test-results
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/.svelte
|
||||||
|
/build
|
||||||
|
/functions
|
||||||
|
.env
|
||||||
|
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": ["ciphertext", "cryptgeon"],
|
||||||
|
"i18n-ally.localesPaths": ["frontend/locales"],
|
||||||
|
"i18n-ally.enabledFrameworks": ["svelte"],
|
||||||
|
"i18n-ally.keystyle": "nested"
|
||||||
|
}
|
114
CHANGELOG.md
@@ -5,124 +5,16 @@ 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.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
|
|
||||||
|
|
||||||
- Default port is now 8000, not 5000.
|
|
||||||
- Moved to generic encryption library `occulto`.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Bad chinese language code.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Updated dependencies.
|
|
||||||
|
|
||||||
## [2.1.0] - 2023-01-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- QR Code to more easily copy and share links.
|
|
||||||
|
|
||||||
## [2.0.7] - 2022-12-26
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Svelte Kit now stable 🎉
|
|
||||||
|
|
||||||
## [2.0.6] - 2022-11-12
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- #66 Set minimum a view.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Updated dependencies.
|
|
||||||
|
|
||||||
## [2.0.5] - 2022-11-04
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Docker build pipeline.
|
|
||||||
|
|
||||||
## [2.0.4] - 2022-10-29
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `THEME_PAGE_TITLE`.
|
|
||||||
- `THEME_FAVICON`.
|
|
||||||
|
|
||||||
## [2.0.3] - 2022-10-07
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Flag for verbosity.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- #58 Fixed bug in the max views frontend form.
|
|
||||||
|
|
||||||
## [2.0.2] - 2022-07-20
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Toasts for events.
|
|
||||||
- E2E Tests.
|
|
||||||
- Make backend more configurable.
|
|
||||||
|
|
||||||
## [2.0.1] - 2022-07-18
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Max file size on the client now.
|
|
||||||
- Loading information.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Changed encoding from hex to base64.
|
|
||||||
- Chinese language code.
|
|
||||||
- Notable speed improvements for big files.
|
|
||||||
|
|
||||||
## [2.0.0] - 2022-07-16
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Theming for logo and description text.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Moved to redis.
|
|
||||||
- New html sanitizing library.
|
|
||||||
|
|
||||||
## [2.0.0-rc.0] - 2022-07-15
|
## [2.0.0-rc.0] - 2022-07-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Theming for logo and description text.
|
- Theming for logo and description text
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Moved to redis.
|
- Moved to redis
|
||||||
- New html sanitizing library.
|
- New html sanitizing library
|
||||||
|
|
||||||
## [1.5.3] - 2022-06-07
|
## [1.5.3] - 2022-06-07
|
||||||
|
|
||||||
|
@@ -1,693 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
23
Dockerfile
@@ -1,30 +1,25 @@
|
|||||||
# FRONTEND
|
# FRONTEND
|
||||||
FROM node:18-alpine as client
|
FROM node:16-alpine as client
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
RUN npm install -g pnpm@8
|
RUN npm install -g pnpm@7
|
||||||
COPY . .
|
COPY ./frontend ./
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
|
|
||||||
# BACKEND
|
# BACKEND
|
||||||
FROM rust:1.71-alpine as backend
|
FROM rust:1.61-alpine as backend
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
RUN apk add libc-dev openssl-dev alpine-sdk
|
RUN apk add libc-dev openssl-dev alpine-sdk
|
||||||
# COPY ./packages/backend/Cargo.* ./
|
COPY ./backend ./
|
||||||
# ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
|
||||||
# RUN cargo fetch
|
|
||||||
COPY ./packages/backend ./
|
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
|
|
||||||
# RUNNER
|
# RUNNER
|
||||||
FROM alpine
|
FROM alpine
|
||||||
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/packages/frontend/build ./frontend
|
COPY --from=client /tmp/build ./frontend/build
|
||||||
ENV FRONTEND_PATH="./frontend"
|
ENV REDIS=redis://redis/
|
||||||
ENV REDIS="redis://redis/"
|
EXPOSE 5000
|
||||||
EXPOSE 8000
|
|
||||||
ENTRYPOINT [ "/app/cryptgeon" ]
|
ENTRYPOINT [ "/app/cryptgeon" ]
|
||||||
|
135
README.md
@@ -14,34 +14,22 @@
|
|||||||
<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) | [ES](README_ES.md)
|
EN | [简体中文](README_zh-CN.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.
|
||||||
>
|
>
|
||||||
> Thanks to [Lokalise](https://lokalise.com/) for providing free access to their platform.
|
> Thanks to [Lokalise](https://lokalise.com/) for providing free access to their platform.
|
||||||
|
|
||||||
## Live Service / Demo
|
## Demo
|
||||||
|
|
||||||
### Web
|
Check out the demo and see for yourself https://cryptgeon.nicco.io.
|
||||||
|
|
||||||
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
|
||||||
@@ -62,25 +50,19 @@ 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. |
|
||||||
| `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/). `512 MiB` is the maximum allowed |
|
||||||
| `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. |
|
||||||
| `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_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||||
| `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_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||||
| `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
|
||||||
|
|
||||||
@@ -94,27 +76,15 @@ version: '3.8'
|
|||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
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:
|
app:
|
||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
environment:
|
environment:
|
||||||
# Size limit for a single note.
|
SIZE_LIMIT: 4M
|
||||||
SIZE_LIMIT: 4 MiB
|
|
||||||
ports:
|
ports:
|
||||||
- 80:8000
|
- 80:5000
|
||||||
|
|
||||||
# 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
|
||||||
@@ -123,34 +93,53 @@ See the [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/exam
|
|||||||
|
|
||||||
### Traefik 2
|
### Traefik 2
|
||||||
|
|
||||||
See the [examples/traefik](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/traefik) folder.
|
Assumptions:
|
||||||
|
|
||||||
### Scratch
|
- External proxy docker network `proxy`
|
||||||
|
- A certificate resolver `le`
|
||||||
|
- A https entrypoint `secure`
|
||||||
|
- Domain name `example.org`
|
||||||
|
|
||||||
See the [examples/scratch](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/scratch) folder. There you'll find a guide how to setup a server and install cryptgeon from scratch.
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
### Synology
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
|
||||||
There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) you can follow.
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
### YouTube Guides
|
app:
|
||||||
|
image: cupcakearmy/cryptgeon:latest
|
||||||
- English by [Webnestify](https://www.youtube.com/watch?v=XAyD42I7wyI)
|
restart: unless-stopped
|
||||||
- English by [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
|
depends_on:
|
||||||
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
- redis
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
|
||||||
|
- traefik.http.routers.cryptgeon.entrypoints=secure
|
||||||
|
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
**Requirements**
|
**Requirements**
|
||||||
|
|
||||||
- `pnpm`: `>=6`
|
- `pnpm`: `>=6`
|
||||||
- `node`: `>=18`
|
- `node`: `>=16`
|
||||||
- `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
|
||||||
@@ -161,6 +150,9 @@ cargo install cargo-watch
|
|||||||
|
|
||||||
Make sure you have docker running.
|
Make sure you have docker running.
|
||||||
|
|
||||||
|
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
|
||||||
|
> https://developer.apple.com/forums/thread/682332
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
@@ -170,34 +162,9 @@ 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).
|
||||||
|
|
||||||
> There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json)
|
###### Attributions
|
||||||
|
|
||||||
### Tests
|
Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
|
||||||
|
|
||||||
Tests are end to end tests written with Playwright.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm run test:prepare
|
|
||||||
|
|
||||||
# Use the test or test:local script. The local version only runs in one browser for quicker development.
|
|
||||||
pnpm run test:local
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
Please refer to the security section [here](./SECURITY.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_Attributions_
|
|
||||||
|
|
||||||
- Test data:
|
|
||||||
- Text for 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 Pictures](https://unsplash.com/)
|
|
||||||
- Loading animation by [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa)
|
|
||||||
- Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
|
|
||||||
|
200
README_ES.md
@@ -1,200 +0,0 @@
|
|||||||
<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
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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) | 简体中文 | [ES](README_ES.md)
|
[EN](README.md) | 简体中文
|
||||||
|
|
||||||
## 关于本项目
|
## 关于本项目
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
|||||||
|
|
||||||
## 演示示例
|
## 演示示例
|
||||||
|
|
||||||
查看加密鸽的在线演示 demo: [cryptgeon.org](https://cryptgeon.org)
|
查看加密鸽的在线演示 demo: https://cryptgeon.nicco.io.
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@@ -49,13 +49,11 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
|||||||
|
|
||||||
| 变量名称 | 默认值 | 描述 |
|
| 变量名称 | 默认值 | 描述 |
|
||||||
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
|
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
|
||||||
| `REDIS` | `redis://redis/` | Redis 连接 URL。 |
|
| `REDIS` | `redis://redis/` | Redis URL to connect to. |
|
||||||
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
|
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
|
||||||
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
|
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
|
||||||
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
||||||
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
||||||
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
|
|
||||||
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
|
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
@@ -80,9 +78,9 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
environment:
|
environment:
|
||||||
SIZE_LIMIT: 4 MiB
|
SIZE_LIMIT: 4M
|
||||||
ports:
|
ports:
|
||||||
- 80:8000
|
- 80:5000
|
||||||
```
|
```
|
||||||
|
|
||||||
### NGINX 反向代理
|
### NGINX 反向代理
|
||||||
@@ -139,7 +137,7 @@ services:
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm --prefix frontend install
|
pnpm --prefix frontend install
|
||||||
|
|
||||||
# 你还需要安装CargoWatch.
|
# 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
|
||||||
cargo install cargo-watch
|
cargo install cargo-watch
|
||||||
```
|
```
|
||||||
@@ -148,6 +146,9 @@ cargo install cargo-watch
|
|||||||
|
|
||||||
确保你的 Docker 正在运行
|
确保你的 Docker 正在运行
|
||||||
|
|
||||||
|
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
|
||||||
|
> https://developer.apple.com/forums/thread/682332
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
@@ -160,25 +161,6 @@ pnpm run dev
|
|||||||
|
|
||||||
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234).
|
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234).
|
||||||
|
|
||||||
## 测试
|
|
||||||
|
|
||||||
这些测试是用 Playwright 实现的一些端到端测试用例。
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm run test:prepare
|
|
||||||
docker compose up redis -d
|
|
||||||
pnpm run test:server
|
|
||||||
|
|
||||||
# 在另一个终端中:
|
|
||||||
# 使用test或者test:local script。为了更快的开发,本地版本只会在一个浏览器中运行。
|
|
||||||
pnpm run test:local
|
|
||||||
```
|
|
||||||
|
|
||||||
###### Attributions
|
###### Attributions
|
||||||
|
|
||||||
- 测试数据:
|
本项目所使用的图标由<a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com 的<a href="https://www.freepik.com" title="Freepik">freepik</a>制作</a>
|
||||||
- 测试文本 [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
|
|
||||||
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
|
|
||||||
- [Unsplash Pictures](https://unsplash.com/)
|
|
||||||
- 加载动画由 [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa) 提供
|
|
||||||
- 图标由来自 <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> 的 <a href="https://www.freepik.com" title="Freepik">freepik</a> 提供
|
|
||||||
|
18
SECURITY.md
@@ -1,18 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Please ensure that you are using the latest major version available.
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| ------- | --------- |
|
|
||||||
| 2.x | ✅ |
|
|
||||||
| < 1.x | ❌ |
|
|
||||||
|
|
||||||
## Reporting a vulnerability
|
|
||||||
|
|
||||||
_cryptgeon_ has a full disclosure vulnerability policy.
|
|
||||||
Report any bug / vulnerability directly to the [issue tracker](https://github.com/cupcakearmy/cryptgeon/issues).
|
|
||||||
Please do NOT attempt to report any security vulnerability in this code privately to anybody.
|
|
||||||
|
|
||||||
> Shamefully copied of the [ring security section](https://github.com/briansmith/ring#bug-reporting).
|
|
700
packages/backend/Cargo.lock → backend/Cargo.lock
generated
@@ -1,14 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cryptgeon"
|
name = "cryptgeon"
|
||||||
version = "2.3.3"
|
version = "2.0.0-rc.0"
|
||||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.71"
|
|
||||||
|
|
||||||
[[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"
|
||||||
@@ -21,5 +22,4 @@ byte-unit = "4"
|
|||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
log = "0.4"
|
redis = "0.21.5"
|
||||||
redis = "0.23"
|
|
@@ -1,6 +1,5 @@
|
|||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
|
|
||||||
use crate::health;
|
|
||||||
use crate::note;
|
use crate::note;
|
||||||
use crate::status;
|
use crate::status;
|
||||||
|
|
||||||
@@ -8,7 +7,6 @@ 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()),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@@ -1,17 +1,14 @@
|
|||||||
use actix_files::{Files, NamedFile};
|
use actix_files::{Files, NamedFile};
|
||||||
use actix_web::{web, Result};
|
use actix_web::{web, Result};
|
||||||
|
|
||||||
use crate::config;
|
|
||||||
|
|
||||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
Files::new("/", config::FRONTEND_PATH.to_string())
|
Files::new("/", "./frontend/build")
|
||||||
.index_file("index.html")
|
.index_file("index.html")
|
||||||
.use_etag(true),
|
.use_etag(true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn index() -> Result<NamedFile> {
|
pub async fn index() -> Result<NamedFile> {
|
||||||
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
|
Ok(NamedFile::open("./frontend/build/index.html")?)
|
||||||
Ok(NamedFile::open(index)?)
|
|
||||||
}
|
}
|
@@ -1,23 +1,18 @@
|
|||||||
use byte_unit::Byte;
|
use byte_unit::Byte;
|
||||||
|
|
||||||
// Internal
|
// General
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
|
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
|
||||||
.unwrap_or("Unknown")
|
.unwrap_or("Unknown")
|
||||||
.to_string();
|
.to_string();
|
||||||
pub static ref FRONTEND_PATH: String =
|
|
||||||
std::env::var("FRONTEND_PATH").unwrap_or("../frontend/build".to_string());
|
|
||||||
pub static ref LISTEN_ADDR: String =
|
|
||||||
std::env::var("LISTEN_ADDR").unwrap_or("0.0.0.0:8000".to_string());
|
|
||||||
pub static ref VERBOSITY: String = std::env::var("VERBOSITY").unwrap_or("warn".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONFIG
|
// CONFIG
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref LIMIT: usize =
|
pub static ref LIMIT: u32 =
|
||||||
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 u32;
|
||||||
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()
|
||||||
@@ -30,10 +25,6 @@ lazy_static! {
|
|||||||
.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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// THEME
|
// THEME
|
||||||
@@ -46,12 +37,4 @@ lazy_static! {
|
|||||||
.unwrap_or("".to_string())
|
.unwrap_or("".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pub static ref THEME_PAGE_TITLE: String = std::env::var("THEME_PAGE_TITLE")
|
|
||||||
.unwrap_or("".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
pub static ref THEME_FAVICON: String = std::env::var("THEME_FAVICON")
|
|
||||||
.unwrap_or("".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
@@ -1,10 +1,8 @@
|
|||||||
use actix_web::{
|
use actix_web::{
|
||||||
middleware::{self, Logger},
|
middleware::{self, Logger},
|
||||||
web::{self},
|
web, App, HttpServer,
|
||||||
App, HttpServer,
|
|
||||||
};
|
};
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use log::error;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
@@ -12,7 +10,6 @@ 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;
|
||||||
@@ -21,16 +18,10 @@ mod store;
|
|||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
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("warning"));
|
||||||
|
|
||||||
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("%a \"%r\" %s %b %T"))
|
||||||
.wrap(middleware::Compress::default())
|
.wrap(middleware::Compress::default())
|
||||||
.wrap(middleware::DefaultHeaders::default())
|
.wrap(middleware::DefaultHeaders::default())
|
||||||
.configure(size::init)
|
.configure(size::init)
|
||||||
@@ -38,7 +29,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.configure(client::init)
|
.configure(client::init)
|
||||||
.default_service(web::to(client::index))
|
.default_service(web::to(client::index))
|
||||||
})
|
})
|
||||||
.bind(config::LISTEN_ADDR.to_string())?
|
.bind("0.0.0.0:5000")?
|
||||||
.run()
|
.run()
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
@@ -2,8 +2,6 @@ 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,
|
||||||
@@ -13,9 +11,7 @@ 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 {
|
||||||
@@ -24,13 +20,8 @@ pub struct NotePublic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_id() -> String {
|
pub fn generate_id() -> String {
|
||||||
let mut result = "".to_owned();
|
let mut id: [u8; 32] = [0; 32];
|
||||||
let mut id: [u8; 1] = [0; 1];
|
|
||||||
let sr = ring::rand::SystemRandom::new();
|
let sr = ring::rand::SystemRandom::new();
|
||||||
|
let _ = sr.fill(&mut id);
|
||||||
for _ in 0..*config::ID_LENGTH {
|
return bs62::encode_data(&id);
|
||||||
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(n)) => HttpResponse::Ok().json(NoteInfo { meta: n.meta }),
|
Ok(Some(_)) => HttpResponse::Ok().json(NoteInfo {}),
|
||||||
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()),
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ async fn create(note: web::Json<Note>) -> impl Responder {
|
|||||||
}
|
}
|
||||||
match n.views {
|
match n.views {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
if v > *config::MAX_VIEWS || v < 1 {
|
if v > *config::MAX_VIEWS {
|
||||||
return bad_req;
|
return bad_req;
|
||||||
}
|
}
|
||||||
n.expiration = None; // views overrides expiration
|
n.expiration = None; // views overrides expiration
|
18
backend/src/size.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use actix_web::web;
|
||||||
|
use byte_unit::Byte;
|
||||||
|
use mime;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref LIMIT: usize =
|
||||||
|
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||||
|
.unwrap()
|
||||||
|
.get_bytes() as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||||
|
let json = web::JsonConfig::default().limit(*LIMIT);
|
||||||
|
let plain = web::PayloadConfig::default()
|
||||||
|
.limit(*LIMIT)
|
||||||
|
.mimetype(mime::STAR_STAR);
|
||||||
|
cfg.app_data(json).app_data(plain);
|
||||||
|
}
|
@@ -12,6 +12,4 @@ pub struct Status {
|
|||||||
// Theme
|
// Theme
|
||||||
pub theme_image: String,
|
pub theme_image: String,
|
||||||
pub theme_text: String,
|
pub theme_text: String,
|
||||||
pub theme_page_title: String,
|
|
||||||
pub theme_favicon: String,
|
|
||||||
}
|
}
|
@@ -7,14 +7,12 @@ use crate::status::Status;
|
|||||||
async fn get_status() -> impl Responder {
|
async fn get_status() -> impl Responder {
|
||||||
return HttpResponse::Ok().json(Status {
|
return HttpResponse::Ok().json(Status {
|
||||||
version: config::VERSION.to_string(),
|
version: config::VERSION.to_string(),
|
||||||
max_size: *config::LIMIT as u32,
|
max_size: *config::LIMIT,
|
||||||
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,
|
||||||
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_favicon: config::THEME_FAVICON.to_string()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@@ -19,14 +19,6 @@ 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()?;
|
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "packages/backend"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "packages/frontend"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "packages/cli"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "packages/shared"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"i18n-ally.localesPaths": ["locales"],
|
|
||||||
"cSpell.words": ["cryptgeon"]
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,26 +0,0 @@
|
|||||||
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
|
|
@@ -11,15 +11,9 @@ services:
|
|||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
env_file: .dev.env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
environment:
|
||||||
|
SIZE_LIMIT: 128M
|
||||||
ports:
|
ports:
|
||||||
- 1234:8000
|
- 80:5000
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
|
|
||||||
interval: 1m
|
|
||||||
timeout: 3s
|
|
||||||
retries: 2
|
|
||||||
start_period: 5s
|
|
@@ -4,7 +4,7 @@ server {
|
|||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://app:8000/;
|
proxy_pass http://app:5000/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
@@ -20,7 +20,7 @@ server {
|
|||||||
ssl_trusted_certificate /path/to/fullchain.pem;
|
ssl_trusted_certificate /path/to/fullchain.pem;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://app:8000/;
|
proxy_pass http://app:5000/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
@@ -1,36 +0,0 @@
|
|||||||
# Install Cryptgeon with Traefik
|
|
||||||
|
|
||||||
Assumptions:
|
|
||||||
|
|
||||||
- Traefik 2 installed.
|
|
||||||
- External proxy docker network `proxy`.
|
|
||||||
- A certificate resolver `le`.
|
|
||||||
- A https entrypoint `secure`.
|
|
||||||
- Domain name `example.org`.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
networks:
|
|
||||||
proxy:
|
|
||||||
external: true
|
|
||||||
|
|
||||||
services:
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
app:
|
|
||||||
image: cupcakearmy/cryptgeon:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
- proxy
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
|
|
||||||
- traefik.http.routers.cryptgeon.entrypoints=secure
|
|
||||||
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
|
||||||
```
|
|
5
frontend/.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/.svelte
|
||||||
|
/build
|
||||||
|
/functions
|
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
@@ -1,8 +1,8 @@
|
|||||||
├─ MIT: 13
|
├─ MIT: 12
|
||||||
├─ ISC: 2
|
|
||||||
├─ BSD-3-Clause: 1
|
├─ BSD-3-Clause: 1
|
||||||
├─ (MPL-2.0 OR Apache-2.0): 1
|
├─ (MPL-2.0 OR Apache-2.0): 1
|
||||||
├─ BSD-2-Clause: 1
|
├─ BSD-2-Clause: 1
|
||||||
|
├─ ISC: 1
|
||||||
├─ 0BSD: 1
|
├─ 0BSD: 1
|
||||||
└─ Apache-2.0: 1
|
└─ Apache-2.0: 1
|
||||||
|
|
|
42
frontend/locales/cn.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"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": "复制到剪切版"
|
||||||
|
},
|
||||||
|
"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": "密信为空!"
|
||||||
|
},
|
||||||
|
"copied_to_clipboard": "已复制到剪切板 🔗"
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"errors": {
|
||||||
|
"not_found": "该密信无法被找到或者它已经被删除了!",
|
||||||
|
"decryption_failed": "密钥错误!您可能不小心粘贴了一个不完整的链接或者正在尝试破解该密信!但无论如何,该密信已被销毁!"
|
||||||
|
},
|
||||||
|
"explanation": "点击下方的按钮可以查看密信,如果它到达了限制将会被删除",
|
||||||
|
"show_note": "查看密信",
|
||||||
|
"warning_will_not_see_again": "您将<b>无法</b>再次查看该密信",
|
||||||
|
"download_all": "下载全部"
|
||||||
|
},
|
||||||
|
"file_upload": {
|
||||||
|
"selected_files": "已选中的文件",
|
||||||
|
"no_files_selected": "没有文件被选中"
|
||||||
|
}
|
||||||
|
}
|
@@ -4,20 +4,13 @@
|
|||||||
"file": "Datei",
|
"file": "Datei",
|
||||||
"advanced": "erweitert",
|
"advanced": "erweitert",
|
||||||
"create": "erstellen",
|
"create": "erstellen",
|
||||||
"loading": "läd",
|
"loading": "Läd...",
|
||||||
"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",
|
|
||||||
"encrypting": "verschlüsseln",
|
|
||||||
"decrypting": "entschlüsselt",
|
|
||||||
"uploading": "hochladen",
|
|
||||||
"downloading": "wird heruntergeladen",
|
|
||||||
"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": "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.",
|
||||||
@@ -30,19 +23,12 @@
|
|||||||
"max": "max: {n}",
|
"max": "max: {n}",
|
||||||
"empty_content": "Notiz ist leer."
|
"empty_content": "Notiz ist leer."
|
||||||
},
|
},
|
||||||
"messages": {
|
"copied_to_clipboard": "in die Zwischenablage kopiert 🔗"
|
||||||
"note_created": "notiz erstellt."
|
|
||||||
},
|
|
||||||
"advanced": {
|
|
||||||
"explanation": "Standardmäßig wird für jede Notiz ein sicher generiertes Passwort verwendet. Sie können jedoch auch ein eigenes Kennwort wählen, das nicht in dem Link enthalten ist.",
|
|
||||||
"custom_password": "benutzerdefiniertes Passwort"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
|
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
|
||||||
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört.",
|
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört."
|
||||||
"unsupported_type": "nicht unterstützter Notiztyp."
|
|
||||||
},
|
},
|
||||||
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
|
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
|
||||||
"show_note": "Notiz anzeigen",
|
"show_note": "Notiz anzeigen",
|
@@ -4,47 +4,33 @@
|
|||||||
"file": "file",
|
"file": "file",
|
||||||
"advanced": "advanced",
|
"advanced": "advanced",
|
||||||
"create": "create",
|
"create": "create",
|
||||||
"loading": "loading",
|
"loading": "loading...",
|
||||||
"mode": "mode",
|
"mode": "mode",
|
||||||
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
|
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
|
||||||
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "share link",
|
"share_link": "share link",
|
||||||
"copy_clipboard": "copy to clipboard",
|
"copy_clipboard": "copy to clipboard"
|
||||||
"copied_to_clipboard": "copied to clipboard",
|
|
||||||
"encrypting": "encrypting",
|
|
||||||
"decrypting": "decrypting",
|
|
||||||
"uploading": "uploading",
|
|
||||||
"downloading": "downloading",
|
|
||||||
"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.",
|
||||||
"explanation": "the note will expire and be destroyed after {type}.",
|
"explanation": "the note will expire and be destroyed after {type}.",
|
||||||
"new_note": "new note",
|
"new_note": "new note",
|
||||||
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored, as everything is kept in ram. if it fills up, the oldest notes will be removed.<br />(you probably will be fine, but just be warned.)",
|
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes will be removed.<br />(you probably will be fine, just be warned.)",
|
||||||
"errors": {
|
"errors": {
|
||||||
"note_to_big": "could not create note. note is too big.",
|
"note_to_big": "could not create note. note is to big",
|
||||||
"note_error": "could not create note. please try again.",
|
"note_error": "could not create note. please try again.",
|
||||||
"max": "max: {n}",
|
"max": "max: {n}",
|
||||||
"empty_content": "note is empty."
|
"empty_content": "note is empty."
|
||||||
},
|
},
|
||||||
"messages": {
|
"copied_to_clipboard": "copied to clipboard 🔗"
|
||||||
"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": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "note was not found or was already deleted.",
|
"not_found": "note was not found or was already deleted.",
|
||||||
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
|
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed."
|
||||||
"unsupported_type": "unsupported note type."
|
|
||||||
},
|
},
|
||||||
"explanation": "click below to show and delete the note if the counter has reached its limit.",
|
"explanation": "click below to show and delete the note if the counter has reached 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"
|
@@ -4,20 +4,13 @@
|
|||||||
"file": "archivo",
|
"file": "archivo",
|
||||||
"advanced": "avanzado",
|
"advanced": "avanzado",
|
||||||
"create": "crear",
|
"create": "crear",
|
||||||
"loading": "cargando",
|
"loading": "cargando...",
|
||||||
"mode": "modo",
|
"mode": "modo",
|
||||||
"views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}",
|
"views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}",
|
||||||
"minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}",
|
"minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "compartir enlace",
|
"share_link": "compartir enlace",
|
||||||
"copy_clipboard": "copiar al portapapeles",
|
"copy_clipboard": "copiar al portapapeles"
|
||||||
"copied_to_clipboard": "copiado al portapapeles",
|
|
||||||
"encrypting": "encriptando",
|
|
||||||
"decrypting": "descifrando",
|
|
||||||
"uploading": "cargando",
|
|
||||||
"downloading": "descargando",
|
|
||||||
"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.",
|
||||||
@@ -30,23 +23,16 @@
|
|||||||
"max": "max: {n}",
|
"max": "max: {n}",
|
||||||
"empty_content": "la nota está vacía."
|
"empty_content": "la nota está vacía."
|
||||||
},
|
},
|
||||||
"messages": {
|
"copied_to_clipboard": "copiado al portapapeles 🔗"
|
||||||
"note_created": "nota creada."
|
|
||||||
},
|
|
||||||
"advanced": {
|
|
||||||
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, que no se incluye en el enlace.",
|
|
||||||
"custom_password": "contraseña personalizada"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "la nota no se encontró o ya fue borrada.",
|
"not_found": "la nota no se encontró o ya fue borrada.",
|
||||||
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida.",
|
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida."
|
||||||
"unsupported_type": "tipo de nota no compatible."
|
|
||||||
},
|
},
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"file_upload": {
|
"file_upload": {
|
@@ -4,20 +4,13 @@
|
|||||||
"file": "fichier",
|
"file": "fichier",
|
||||||
"advanced": "avancé",
|
"advanced": "avancé",
|
||||||
"create": "créer",
|
"create": "créer",
|
||||||
"loading": "chargement",
|
"loading": "chargement...",
|
||||||
"mode": "mode",
|
"mode": "mode",
|
||||||
"views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}",
|
"views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}",
|
||||||
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"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",
|
|
||||||
"encrypting": "cryptage",
|
|
||||||
"decrypting": "déchiffrer",
|
|
||||||
"uploading": "téléchargement",
|
|
||||||
"downloading": "téléchargement",
|
|
||||||
"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 cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.",
|
||||||
@@ -30,19 +23,12 @@
|
|||||||
"max": "max: {n}",
|
"max": "max: {n}",
|
||||||
"empty_content": "La note est vide."
|
"empty_content": "La note est vide."
|
||||||
},
|
},
|
||||||
"messages": {
|
"copied_to_clipboard": "copié dans le presse-papiers 🔗"
|
||||||
"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": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "La note n'a pas été trouvée ou a déjà été supprimée.",
|
"not_found": "La note n'a pas été trouvée ou a déjà été supprimée.",
|
||||||
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite.",
|
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite."
|
||||||
"unsupported_type": "type de note non supporté."
|
|
||||||
},
|
},
|
||||||
"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",
|
@@ -4,20 +4,13 @@
|
|||||||
"file": "file",
|
"file": "file",
|
||||||
"advanced": "avanzato",
|
"advanced": "avanzato",
|
||||||
"create": "crea",
|
"create": "crea",
|
||||||
"loading": "carica",
|
"loading": "carica...",
|
||||||
"mode": "modalita",
|
"mode": "modalita",
|
||||||
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
|
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
|
||||||
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
|
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "condividi link",
|
"share_link": "condividi link",
|
||||||
"copy_clipboard": "copia negli appunti",
|
"copy_clipboard": "copia negli appunti"
|
||||||
"copied_to_clipboard": "copiato negli appunti",
|
|
||||||
"encrypting": "criptando",
|
|
||||||
"decrypting": "decifrando",
|
|
||||||
"uploading": "caricamento",
|
|
||||||
"downloading": "scaricando",
|
|
||||||
"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.",
|
||||||
@@ -30,23 +23,16 @@
|
|||||||
"max": "max: {n}",
|
"max": "max: {n}",
|
||||||
"empty_content": "la nota è vuota."
|
"empty_content": "la nota è vuota."
|
||||||
},
|
},
|
||||||
"messages": {
|
"copied_to_clipboard": "copiato negli appunti 🔗"
|
||||||
"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": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "non è stata trovata o è stata già cancellata.",
|
"not_found": "non è stata trovata o è stata già cancellata.",
|
||||||
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta.",
|
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta."
|
||||||
"unsupported_type": "tipo di nota non supportato."
|
|
||||||
},
|
},
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"file_upload": {
|
"file_upload": {
|
35
frontend/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "svelte-kit preview",
|
||||||
|
"check": "svelte-check --tsconfig tsconfig.json",
|
||||||
|
"licenses": "license-checker --summary > licenses.csv",
|
||||||
|
"locale:download": "node scripts/locale.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@lokalise/node-api": "^7.3.1",
|
||||||
|
"@sveltejs/adapter-static": "^1.0.0-next.34",
|
||||||
|
"@sveltejs/kit": "^1.0.0-next.361",
|
||||||
|
"@types/dompurify": "^2.3.3",
|
||||||
|
"@types/file-saver": "^2.0.5",
|
||||||
|
"adm-zip": "^0.5.9",
|
||||||
|
"dotenv": "^16.0.1",
|
||||||
|
"svelte": "^3.49.0",
|
||||||
|
"svelte-check": "^2.8.0",
|
||||||
|
"svelte-intl-precompile": "^0.10.1",
|
||||||
|
"svelte-preprocess": "^4.10.7",
|
||||||
|
"tslib": "^2.4.0",
|
||||||
|
"typescript": "^4.7.4",
|
||||||
|
"vite": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/fira-mono": "^4.5.8",
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"dompurify": "^2.3.9",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"pretty-bytes": "^5.6.0"
|
||||||
|
}
|
||||||
|
}
|
1609
frontend/pnpm-lock.yaml
generated
Normal file
@@ -1,19 +1,14 @@
|
|||||||
import { LokaliseApi } from '@lokalise/node-api'
|
|
||||||
import AdmZip from 'adm-zip'
|
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
import { LokaliseApi } from '@lokalise/node-api'
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
|
import AdmZip from 'adm-zip'
|
||||||
|
|
||||||
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) exit('No API Key set for Lokalize! Set with "LOKALISE_API_KEY"')
|
if (!apiKey) throw new Error('No API Key set for Lokalize! Set with "LOKALISE_API_KEY"')
|
||||||
if (!project_id) exit('No project id set for Lokalize! Set with "LOKALISE_PROJECT"')
|
if (!project_id) throw new Error('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) =>
|
@@ -14,9 +14,7 @@
|
|||||||
--ui-text-0: #fefefe;
|
--ui-text-0: #fefefe;
|
||||||
--ui-text-1: #eee;
|
--ui-text-1: #eee;
|
||||||
--ui-clr-primary: hsl(186, 65%, 55%);
|
--ui-clr-primary: hsl(186, 65%, 55%);
|
||||||
--ui-clr-primary-alt: hsl(186, 85%, 35%);
|
|
||||||
--ui-clr-error: hsl(357, 77%, 51%);
|
--ui-clr-error: hsl(357, 77%, 51%);
|
||||||
--ui-clr-error-alt: hsl(357, 87%, 41%);
|
|
||||||
|
|
||||||
--ui-anim: all 150ms ease;
|
--ui-anim: all 150ms ease;
|
||||||
}
|
}
|
||||||
@@ -87,12 +85,10 @@ button {
|
|||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
border: none;
|
|
||||||
padding-inline: initial;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*:disabled,
|
*:disabled,
|
||||||
.disabled {
|
*[disabled='true'] {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,13 +122,3 @@ 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;
|
|
||||||
}
|
|
@@ -2,6 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
3
frontend/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="@sveltejs/kit" />
|
||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
74
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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: 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
|
||||||
|
}
|
71
frontend/src/lib/crypto.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export class Hex {
|
||||||
|
static encode(buffer: ArrayBuffer): string {
|
||||||
|
let s = ''
|
||||||
|
for (const i of new Uint8Array(buffer)) {
|
||||||
|
s += i.toString(16).padStart(2, '0')
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
static decode(s: string): ArrayBuffer {
|
||||||
|
const size = s.length / 2
|
||||||
|
const buffer = new Uint8Array(size)
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
const idx = i * 2
|
||||||
|
const segment = s.slice(idx, idx + 2)
|
||||||
|
buffer[i] = parseInt(segment, 16)
|
||||||
|
}
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALG = 'AES-GCM'
|
||||||
|
|
||||||
|
export function getRandomBytes(size = 16): Uint8Array {
|
||||||
|
return window.crypto.getRandomValues(new Uint8Array(size))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKeyFromString(password: string) {
|
||||||
|
return window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
new TextEncoder().encode(password),
|
||||||
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveBits', 'deriveKey']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
|
||||||
|
const iterations = 100_000
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt,
|
||||||
|
iterations,
|
||||||
|
hash: 'SHA-512',
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
{ name: ALG, length: 256 },
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encrypt(plaintext: string, key: CryptoKey) {
|
||||||
|
const salt = getRandomBytes(16)
|
||||||
|
const derived = await getDerivedForKey(key, salt)
|
||||||
|
const iv = getRandomBytes(16)
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt(
|
||||||
|
{ name: ALG, iv },
|
||||||
|
derived,
|
||||||
|
new TextEncoder().encode(plaintext)
|
||||||
|
)
|
||||||
|
return [salt, iv, encrypted].map(Hex.encode).join(':')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decrypt(ciphertext: string, key: CryptoKey) {
|
||||||
|
const [salt, iv, encrypted] = ciphertext.split(':').map(Hex.decode)
|
||||||
|
const derived = await getDerivedForKey(key, salt)
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt({ name: ALG, iv }, derived, encrypted)
|
||||||
|
return new TextDecoder().decode(plaintext)
|
||||||
|
}
|
13
frontend/src/lib/files.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export class Files {
|
||||||
|
static toString(f: File | Blob): Promise<string> {
|
||||||
|
const reader = new window.FileReader()
|
||||||
|
reader.readAsDataURL(f)
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
reader.onloadend = () => resolve(reader.result as string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromString(s: string): Promise<Blob> {
|
||||||
|
return fetch(s).then((r) => r.blob())
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 287 B |
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 736 B After Width: | Height: | Size: 736 B |
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 483 B |
Before Width: | Height: | Size: 732 B After Width: | Height: | Size: 732 B |
22
frontend/src/lib/stores/status.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { call } from '$lib/api'
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export const status = writable<null | Status>(null)
|
||||||
|
|
||||||
|
export async function init() {
|
||||||
|
const data = await call({
|
||||||
|
url: 'status/',
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
status.set(data)
|
||||||
|
}
|
@@ -1,33 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { FileDTO } from '$lib/api'
|
||||||
|
import { Files } from '$lib/files'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
import Button from './Button.svelte'
|
||||||
import Button from '$lib/ui/Button.svelte'
|
import MaxSize from './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[] = []
|
let files: File[] = []
|
||||||
|
|
||||||
async function fileToDTO(file: File): Promise<FileDTO> {
|
const dispatch = createEventDispatcher<{ file: string }>()
|
||||||
return {
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
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) {
|
||||||
const toAdd = await Promise.all(Array.from(input.files).map(fileToDTO))
|
files = [...files, ...Array.from(input.files)]
|
||||||
files = [...files, ...toAdd]
|
const data: FileDTO[] = await Promise.all(
|
||||||
|
files.map(async (file) => ({
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
contents: await Files.toString(file),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
dispatch('file', JSON.stringify(data))
|
||||||
|
} else {
|
||||||
|
dispatch('file', '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear(e: Event) {
|
function clear(e: Event) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
files = []
|
files = []
|
||||||
|
dispatch('file', '')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -35,7 +40,7 @@
|
|||||||
<small>
|
<small>
|
||||||
{label}
|
{label}
|
||||||
</small>
|
</small>
|
||||||
<input {...$$restProps} type="file" on:change={onInput} multiple />
|
<input type="file" on:change={onInput} multiple />
|
||||||
<div class="box">
|
<div class="box">
|
||||||
{#if files.length}
|
{#if files.length}
|
||||||
<div>
|
<div>
|
||||||
@@ -52,9 +57,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<b>{$t('file_upload.no_files_selected')}</b>
|
<b>{$t('file_upload.no_files_selected')}</b>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<small>{$t('common.max')}: <MaxSize /></small>
|
||||||
{$t('common.max')}: <MaxSize />
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
@@ -1,17 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { status } from '$lib/stores/status'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { _ } from 'svelte-intl-precompile'
|
import { _ } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
import { status } from '$lib/stores/status'
|
|
||||||
|
|
||||||
// Due to encoding overhead (~35%) with base64
|
|
||||||
// https://en.wikipedia.org/wiki/Base64
|
|
||||||
const overhead = 1 / 1.35
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{#if $status !== null}
|
{#if $status !== null}
|
||||||
{prettyBytes($status.max_size * overhead, { binary: true })}
|
{prettyBytes($status.max_size, { binary: true })}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('common.loading')}
|
{$_('common.loading')}
|
||||||
{/if}
|
{/if}
|
@@ -1,24 +1,20 @@
|
|||||||
<script lang="ts" context="module">
|
|
||||||
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { FileDTO, NotePublic } from '$lib/api'
|
||||||
|
import { Files } from '$lib/files'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
import Button from './Button.svelte'
|
||||||
|
|
||||||
import Button from '$lib/ui/Button.svelte'
|
export let note: NotePublic
|
||||||
import { copy } from '$lib/utils'
|
|
||||||
import type { FileDTO, NotePublic } from '@cryptgeon/shared'
|
|
||||||
|
|
||||||
export let note: DecryptedNote
|
|
||||||
|
|
||||||
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
|
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
|
||||||
let files: FileDTO[] = []
|
let files: FileDTO[] = []
|
||||||
|
|
||||||
$: if (note.meta.type === 'file') {
|
$: if (note.meta.type === 'file') {
|
||||||
files = note.contents
|
files = JSON.parse(note.contents) as FileDTO[]
|
||||||
}
|
}
|
||||||
|
|
||||||
$: download = () => {
|
$: download = () => {
|
||||||
@@ -28,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile(file: FileDTO) {
|
async function downloadFile(file: FileDTO) {
|
||||||
const f = new File([file.contents], file.name, {
|
const f = new File([await Files.fromString(file.contents)], file.name, {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
})
|
})
|
||||||
saveAs(f)
|
saveAs(f)
|
||||||
@@ -44,24 +40,20 @@
|
|||||||
</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">
|
{#if note.meta.type === 'text'}
|
||||||
{#if note.meta.type === 'text'}
|
<div class="note">
|
||||||
<div class="note">
|
{@html contentWithLinks(note.contents)}
|
||||||
{@html contentWithLinks(note.contents)}
|
</div>
|
||||||
|
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
||||||
|
{:else}
|
||||||
|
{#each files as file}
|
||||||
|
<div class="note file">
|
||||||
|
<b on:click={() => downloadFile(file)}>↓ {file.name}</b>
|
||||||
|
<small> {file.type} - {prettyBytes(file.size)}</small>
|
||||||
</div>
|
</div>
|
||||||
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
{/each}
|
||||||
{:else}
|
<Button on:click={download}>{$t('show.download_all')}</Button>
|
||||||
{#each files as file}
|
{/if}
|
||||||
<div class="note file">
|
|
||||||
<button on:click={() => downloadFile(file)}>
|
|
||||||
<b>↓ {file.name}</b>
|
|
||||||
</button>
|
|
||||||
<small> {file.type} - {prettyBytes(file.size)}</small>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<Button on:click={download}>{$t('show.download_all')}</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.note {
|
.note {
|
@@ -4,35 +4,43 @@
|
|||||||
export let color = true
|
export let color = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label {...$$restProps}>
|
<div {...$$restProps}>
|
||||||
<small>{label}</small>
|
<label class="switch">
|
||||||
<input type="checkbox" bind:checked={value} />
|
<small>{label}</small>
|
||||||
<span class:color class="slider" />
|
<input type="checkbox" bind:checked={value} />
|
||||||
</label>
|
<span class:color class="slider" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
label {
|
div {
|
||||||
|
height: 3.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
width: 4rem;
|
||||||
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
label input {
|
.switch input {
|
||||||
display: none;
|
opacity: 0;
|
||||||
}
|
width: 0;
|
||||||
|
height: 0;
|
||||||
small {
|
|
||||||
display: block;
|
|
||||||
width: max-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
display: block;
|
position: absolute;
|
||||||
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 {
|
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/ui/Icon.svelte'
|
import { getRandomBytes, Hex } from '$lib/crypto'
|
||||||
import { copy as copyFN } from '$lib/utils'
|
import copyToClipboard from 'copy-to-clipboard'
|
||||||
import { getRandomBytes, Hex } from 'occulto'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
import { fade } from 'svelte/transition'
|
||||||
|
import Icon from './Icon.svelte'
|
||||||
|
|
||||||
export let label: string = ''
|
export let label: string = ''
|
||||||
export let value: any
|
export let value: any
|
||||||
@@ -12,6 +14,8 @@
|
|||||||
const initialType = $$restProps.type
|
const initialType = $$restProps.type
|
||||||
const isPassword = initialType === 'password'
|
const isPassword = initialType === 'password'
|
||||||
let hidden = true
|
let hidden = true
|
||||||
|
let notification: string | null = null
|
||||||
|
let notificationTimeout: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
$: valid = validate(value)
|
$: valid = validate(value)
|
||||||
|
|
||||||
@@ -23,14 +27,27 @@
|
|||||||
function toggle() {
|
function toggle() {
|
||||||
hidden = !hidden
|
hidden = !hidden
|
||||||
}
|
}
|
||||||
|
function copyFN() {
|
||||||
|
copyToClipboard(value.toString())
|
||||||
|
notify($t('home.copied_to_clipboard'))
|
||||||
|
}
|
||||||
|
function randomFN() {
|
||||||
|
value = Hex.encode(getRandomBytes(20))
|
||||||
|
}
|
||||||
|
|
||||||
async function randomFN() {
|
function notify(msg: string, delay: number = 2000) {
|
||||||
value = Hex.encode(await getRandomBytes(32))
|
if (notificationTimeout) {
|
||||||
|
clearTimeout(notificationTimeout)
|
||||||
|
}
|
||||||
|
notificationTimeout = setTimeout(() => {
|
||||||
|
notification = null
|
||||||
|
}, delay)
|
||||||
|
notification = msg
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<small class:disabled={$$restProps.disabled}>
|
<small disabled={$$restProps.disabled}>
|
||||||
{label}
|
{label}
|
||||||
{#if valid !== true}
|
{#if valid !== true}
|
||||||
<span class="error-text">{valid}</span>
|
<span class="error-text">{valid}</span>
|
||||||
@@ -45,16 +62,18 @@
|
|||||||
<Icon class="icon" icon="dice" on:click={randomFN} />
|
<Icon class="icon" icon="dice" on:click={randomFN} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if copy}
|
{#if copy}
|
||||||
<Icon class="icon" icon="copy" on:click={() => copyFN(value.toString())} />
|
<Icon class="icon" icon="copy" on:click={copyFN} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if notification}
|
||||||
|
<div class="notification" transition:fade><small>{notification}</small></div>
|
||||||
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
label {
|
label {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label > small {
|
label > small {
|
||||||
@@ -98,4 +117,11 @@
|
|||||||
.icons > :global(.icon:hover) {
|
.icons > :global(.icon:hover) {
|
||||||
border-color: var(--ui-clr-primary);
|
border-color: var(--ui-clr-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
text-align: right;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: -1.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@@ -40,19 +40,19 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button on:click={change}>
|
<div on:click={change}>
|
||||||
<Icon class="icon" icon="contrast" />
|
<Icon class="icon" icon="contrast" />
|
||||||
{$theme}
|
{$theme}
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button :global(.icon) {
|
div :global(.icon) {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
201
frontend/src/lib/views/Create.svelte
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Note } from '$lib/api'
|
||||||
|
import { create,PayloadToLargeError } from '$lib/api'
|
||||||
|
import { encrypt,getKeyFromString,getRandomBytes,Hex } from '$lib/crypto'
|
||||||
|
import { status } from '$lib/stores/status'
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
import FileUpload from '$lib/ui/FileUpload.svelte'
|
||||||
|
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||||
|
import Switch from '$lib/ui/Switch.svelte'
|
||||||
|
import TextArea from '$lib/ui/TextArea.svelte'
|
||||||
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
import { blur } from 'svelte/transition'
|
||||||
|
|
||||||
|
let note: Note = {
|
||||||
|
contents: '',
|
||||||
|
meta: { type: 'text' },
|
||||||
|
views: 1,
|
||||||
|
expiration: 60,
|
||||||
|
}
|
||||||
|
let result: { password: string; id: string } | null = null
|
||||||
|
let advanced = false
|
||||||
|
let file = false
|
||||||
|
let timeExpiration = false
|
||||||
|
let message = ''
|
||||||
|
let loading = false
|
||||||
|
let error: string | null = null
|
||||||
|
|
||||||
|
$: if (!advanced) {
|
||||||
|
note.views = 1
|
||||||
|
timeExpiration = false
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
message = $t('home.explanation', {
|
||||||
|
values: {
|
||||||
|
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
||||||
|
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: note.meta.type = file ? 'file' : 'text'
|
||||||
|
|
||||||
|
$: if (!file) {
|
||||||
|
note.contents = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmptyContentError extends Error {}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
try {
|
||||||
|
error = null
|
||||||
|
loading = true
|
||||||
|
const password = Hex.encode(getRandomBytes(32))
|
||||||
|
const key = await getKeyFromString(password)
|
||||||
|
if (note.contents === '') throw new EmptyContentError()
|
||||||
|
const data: Note = {
|
||||||
|
contents: await encrypt(note.contents, key),
|
||||||
|
meta: note.meta,
|
||||||
|
}
|
||||||
|
if (timeExpiration) data.expiration = parseInt(note.expiration as any)
|
||||||
|
else data.views = parseInt(note.views as any)
|
||||||
|
|
||||||
|
const response = await create(data)
|
||||||
|
result = {
|
||||||
|
password: password,
|
||||||
|
id: response.id,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof PayloadToLargeError) {
|
||||||
|
error = $t('home.errors.note_to_big')
|
||||||
|
} else if (e instanceof EmptyContentError) {
|
||||||
|
error = $t('home.errors.empty_content')
|
||||||
|
} else {
|
||||||
|
error = $t('home.errors.note_error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if result}
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
label={$t('common.share_link')}
|
||||||
|
value="{window.location.origin}/note/{result.id}#{result.password}"
|
||||||
|
copy
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{@html $t('home.new_note_notice')}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
{@html $status?.theme_text || $t('home.intro')}
|
||||||
|
</p>
|
||||||
|
<form on:submit|preventDefault={submit}>
|
||||||
|
<fieldset disabled={loading}>
|
||||||
|
{#if file}
|
||||||
|
<FileUpload label={$t('common.file')} on:file={(f) => (note.contents = f.detail)} />
|
||||||
|
{:else}
|
||||||
|
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="bottom">
|
||||||
|
<Switch class="file" label={$t('common.file')} bind:value={file} />
|
||||||
|
{#if $status?.allow_advanced}
|
||||||
|
<Switch label={$t('common.advanced')} bind:value={advanced} />
|
||||||
|
{/if}
|
||||||
|
<div class="grow" />
|
||||||
|
<div class="tr">
|
||||||
|
<small>{$t('common.max')}: <MaxSize /> </small>
|
||||||
|
<br />
|
||||||
|
<Button type="submit">{$t('common.create')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error-text">{error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<br />
|
||||||
|
{#if loading}
|
||||||
|
{$t('common.loading')}
|
||||||
|
{:else}
|
||||||
|
{message}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if advanced}
|
||||||
|
<div transition:blur={{ duration: 250 }}>
|
||||||
|
<br />
|
||||||
|
<div class="fields">
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
label={$t('common.views', { values: { n: 0 } })}
|
||||||
|
bind:value={note.views}
|
||||||
|
disabled={timeExpiration}
|
||||||
|
max={$status?.max_views}
|
||||||
|
validate={(v) =>
|
||||||
|
($status && v < $status?.max_views) ||
|
||||||
|
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
||||||
|
/>
|
||||||
|
<div class="middle-switch">
|
||||||
|
<Switch label={$t('common.mode')} bind:value={timeExpiration} color={false} />
|
||||||
|
</div>
|
||||||
|
<TextInput
|
||||||
|
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>
|
||||||
|
{/if}
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bottom {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom :global(.file) {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-switch {
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -7,9 +7,7 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<a href="/">/home</a>
|
<a href="/">/home</a>
|
||||||
<a href="/about">/about</a>
|
<a href="/about">/about</a>
|
||||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">/code</a>
|
||||||
code
|
|
||||||
</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -23,7 +21,6 @@
|
|||||||
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,13 +1,17 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
import { init, waitLocale, getLocaleFromNavigator } from 'svelte-intl-precompile'
|
||||||
|
// @ts-ignore
|
||||||
|
import { registerAll } from '$locales'
|
||||||
|
registerAll()
|
||||||
|
init({ initialLocale: getLocaleFromNavigator() ?? undefined, fallbackLocale: 'en' })
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { SvelteToast } from '@zerodevx/svelte-toast'
|
import { init as initStores } from '$lib/stores/status'
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import { waitLocale } from 'svelte-intl-precompile'
|
|
||||||
|
|
||||||
import '../app.css'
|
|
||||||
|
|
||||||
import { init as initStores, status } from '$lib/stores/status'
|
|
||||||
import Footer from '$lib/views/Footer.svelte'
|
import Footer from '$lib/views/Footer.svelte'
|
||||||
import Header from '$lib/views/Header.svelte'
|
import Header from '$lib/views/Header.svelte'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import '../app.css'
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
initStores()
|
initStores()
|
||||||
@@ -15,8 +19,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{$status?.theme_page_title || 'cryptgeon'}</title>
|
<title>cryptgeon</title>
|
||||||
<link rel="icon" href={$status?.theme_favicon || '/favicon.png'} />
|
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#await waitLocale() then _}
|
{#await waitLocale() then _}
|
||||||
@@ -25,8 +28,6 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<SvelteToast />
|
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
{/await}
|
{/await}
|
||||||
|
|
@@ -1,6 +1,10 @@
|
|||||||
<script lang="ts">
|
<script context="module">
|
||||||
|
import { browser, dev } from '$app/env'
|
||||||
import { status } from '$lib/stores/status'
|
import { status } from '$lib/stores/status'
|
||||||
import AboutParagraph from '$lib/ui/AboutParagraph.svelte'
|
import AboutParagraph from '$lib/ui/AboutParagraph.svelte'
|
||||||
|
|
||||||
|
export const hydrate = dev
|
||||||
|
export const router = browser
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -39,7 +43,7 @@
|
|||||||
the backend is written in rust and the frontend is svelte and typescript.
|
the backend is written in rust and the frontend is svelte and typescript.
|
||||||
<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">
|
||||||
source code
|
source code
|
||||||
</a>.
|
</a>.
|
||||||
</span>
|
</span>
|
||||||
@@ -47,12 +51,9 @@
|
|||||||
|
|
||||||
<AboutParagraph title="translations">
|
<AboutParagraph title="translations">
|
||||||
<span
|
<span
|
||||||
>translations are managed on <a
|
>translations are managed on <a href="https://lokalise.com/" target="_blank">Lokalise</a>,
|
||||||
href="https://lokalise.com/"
|
which granted an open source license to use the paid version. If you are interested in helping
|
||||||
target="_blank"
|
translating don't hesitate to contact me!
|
||||||
rel="noopener noreferrer">Lokalise</a
|
|
||||||
>, which granted an open source license to use the paid version. If you are interested in
|
|
||||||
helping translating don't hesitate to contact me!
|
|
||||||
</span>
|
</span>
|
||||||
</AboutParagraph>
|
</AboutParagraph>
|
||||||
|
|
||||||
@@ -75,9 +76,6 @@
|
|||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
83
frontend/src/routes/note/[id].svelte
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
import type { Load } from '@sveltejs/kit'
|
||||||
|
|
||||||
|
export const load: Load = async ({ params }) => {
|
||||||
|
return {
|
||||||
|
props: params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
|
import type { NotePublic } from '$lib/api'
|
||||||
|
import { get, info } from '$lib/api'
|
||||||
|
import { decrypt, getKeyFromString } from '$lib/crypto'
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
import ShowNote from '$lib/ui/ShowNote.svelte'
|
||||||
|
|
||||||
|
export let id: string
|
||||||
|
|
||||||
|
let password: string
|
||||||
|
let note: NotePublic | null = null
|
||||||
|
let exists = false
|
||||||
|
|
||||||
|
let loading = true
|
||||||
|
let error = false
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
loading = true
|
||||||
|
error = false
|
||||||
|
password = window.location.hash.slice(1)
|
||||||
|
await info(id)
|
||||||
|
exists = true
|
||||||
|
} catch {
|
||||||
|
exists = false
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function show() {
|
||||||
|
try {
|
||||||
|
error = false
|
||||||
|
loading = true
|
||||||
|
const data = note || (await get(id)) // Don't get the content twice on wrong password.
|
||||||
|
const key = await getKeyFromString(password)
|
||||||
|
data.contents = await decrypt(data.contents, key)
|
||||||
|
note = data
|
||||||
|
} catch {
|
||||||
|
error = true
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !loading}
|
||||||
|
{#if !exists}
|
||||||
|
<p class="error-text">{$t('show.errors.not_found')}</p>
|
||||||
|
{:else if note && !error}
|
||||||
|
<ShowNote {note} />
|
||||||
|
{:else}
|
||||||
|
<form on:submit|preventDefault={show}>
|
||||||
|
<fieldset>
|
||||||
|
<p>{$t('show.explanation')}</p>
|
||||||
|
<Button type="submit">{$t('show.show_note')}</Button>
|
||||||
|
{#if error}
|
||||||
|
<br />
|
||||||
|
<p class="error-text">
|
||||||
|
{$t('show.errors.decryption_failed')}
|
||||||
|
<br />
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if loading}
|
||||||
|
<p>{$t('common.loading')}</p>
|
||||||
|
{/if}
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
12
frontend/svelte.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static'
|
||||||
|
import preprocess from 'svelte-preprocess'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preprocess: preprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
fallback: 'index.html',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
@@ -1,13 +1,11 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite'
|
import { sveltekit } from '@sveltejs/kit/vite'
|
||||||
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
||||||
|
|
||||||
const port = 8001
|
|
||||||
|
|
||||||
/** @type {import('vite').UserConfig} */
|
/** @type {import('vite').UserConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
clearScreen: false,
|
server: {
|
||||||
server: { port },
|
port: 3000,
|
||||||
preview: { port },
|
},
|
||||||
plugins: [sveltekit(), precompileIntl('locales')],
|
plugins: [sveltekit(), precompileIntl('locales')],
|
||||||
}
|
}
|
||||||
|
|
22
package.json
@@ -1,21 +1,13 @@
|
|||||||
{
|
{
|
||||||
"packageManager": "pnpm@8.6.3",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
|
"dev:docker": "docker-compose up redis",
|
||||||
"dev:packages": "pnpm --parallel run dev",
|
"dev:backend": "cd backend && cargo watch -x 'run --bin cryptgeon'",
|
||||||
"dev": "run-p dev:*",
|
"dev:front": "pnpm --prefix frontend run dev",
|
||||||
"docker:up": "docker compose -f docker-compose.dev.yaml up",
|
"dev:proxy": "node proxy.mjs",
|
||||||
"docker:build": "docker compose -f docker-compose.dev.yaml build",
|
"dev": "run-p dev:*"
|
||||||
"test": "playwright test --project chrome firefox safari",
|
|
||||||
"test:local": "playwright test --project chrome",
|
|
||||||
"test:server": "run-s docker:up",
|
|
||||||
"test:prepare": "run-p build docker:build",
|
|
||||||
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.33.0",
|
"http-proxy": "^1.18.1",
|
||||||
"@types/node": "^20.1.3",
|
"npm-run-all": "^4.1.5"
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"shelljs": "^0.8.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"name": "@cryptgeon/backend",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "cargo watch -x 'run --bin cryptgeon'",
|
|
||||||
"build": "cargo build --release",
|
|
||||||
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:1234 cargo run",
|
|
||||||
"test:prepare": "cargo build"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,3 +0,0 @@
|
|||||||
mod routes;
|
|
||||||
|
|
||||||
pub use routes::*;
|
|
@@ -1,16 +0,0 @@
|
|||||||
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,12 +0,0 @@
|
|||||||
use crate::config;
|
|
||||||
use actix_web::web;
|
|
||||||
use mime;
|
|
||||||
|
|
||||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
|
||||||
let json = web::JsonConfig::default().limit(*config::LIMIT);
|
|
||||||
let plain = web::PayloadConfig::default()
|
|
||||||
.limit(*config::LIMIT)
|
|
||||||
.mimetype(mime::STAR_STAR);
|
|
||||||
// cfg.app_data(plain);
|
|
||||||
cfg.app_data(json).app_data(plain);
|
|
||||||
}
|
|
@@ -1,54 +0,0 @@
|
|||||||
# 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"
|
|
||||||
```
|
|
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.3.3",
|
|
||||||
"name": "cryptgeon",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/cupcakearmy/cryptgeon.git",
|
|
||||||
"directory": "packages/cli"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
|
||||||
"type": "module",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"dev": "./scripts/build.js --watch",
|
|
||||||
"build": "./scripts/build.js",
|
|
||||||
"package": "./scripts/package.js",
|
|
||||||
"bin": "run-s build package",
|
|
||||||
"prepublishOnly": "run-s build"
|
|
||||||
},
|
|
||||||
"main": "./dist/index.cjs",
|
|
||||||
"bin": {
|
|
||||||
"cryptgeon": "./dist/index.cjs"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
|
||||||
"@commander-js/extra-typings": "^11.0.0",
|
|
||||||
"@cryptgeon/shared": "workspace:*",
|
|
||||||
"@types/inquirer": "^9.0.3",
|
|
||||||
"@types/mime": "^3.0.1",
|
|
||||||
"@types/node": "^20.5.0",
|
|
||||||
"commander": "^11.0.0",
|
|
||||||
"esbuild": "^0.19.2",
|
|
||||||
"inquirer": "^9.2.10",
|
|
||||||
"mime": "^3.0.0",
|
|
||||||
"occulto": "^2.0.1",
|
|
||||||
"pkg": "^5.8.1",
|
|
||||||
"pretty-bytes": "^6.1.1",
|
|
||||||
"typescript": "^5.1.6"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { build, context } from 'esbuild'
|
|
||||||
import pkg from '../package.json' assert { type: 'json' }
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
entryPoints: ['./src/index.ts'],
|
|
||||||
bundle: true,
|
|
||||||
minify: true,
|
|
||||||
platform: 'node',
|
|
||||||
outfile: './dist/index.cjs',
|
|
||||||
define: { VERSION: `"${pkg.version}"` },
|
|
||||||
}
|
|
||||||
|
|
||||||
const watch = process.argv.slice(2)[0] === '--watch'
|
|
||||||
if (watch) (await context(options)).watch()
|
|
||||||
else await build(options)
|
|
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { exec } from 'pkg'
|
|
||||||
|
|
||||||
const targets = [
|
|
||||||
'node18-macos-arm64',
|
|
||||||
'node18-macos-x64',
|
|
||||||
'node18-linux-arm64',
|
|
||||||
'node18-linux-x64',
|
|
||||||
'node18-win-arm64',
|
|
||||||
'node18-win-x64',
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const target of targets) {
|
|
||||||
console.log(`🚀 Building ${target}`)
|
|
||||||
await exec(['./dist/index.cjs', '--target', target, '--output', `./bin/${target.replace('node18', 'cryptgeon')}`])
|
|
||||||
}
|
|
@@ -1,86 +0,0 @@
|
|||||||
import { Adapters, get, info, setBase } from '@cryptgeon/shared'
|
|
||||||
import inquirer from 'inquirer'
|
|
||||||
import { access, constants, writeFile } from 'node:fs/promises'
|
|
||||||
import { basename, resolve } from 'node:path'
|
|
||||||
import { AES, Hex } from 'occulto'
|
|
||||||
import pretty from 'pretty-bytes'
|
|
||||||
|
|
||||||
import { exit } from './utils'
|
|
||||||
|
|
||||||
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
|
|
||||||
setBase(url.origin)
|
|
||||||
const id = url.pathname.split('/')[2]
|
|
||||||
const preview = await info(id).catch(() => exit('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 = () => exit('Could not decrypt note. Probably an invalid password')
|
|
||||||
switch (note.meta.type) {
|
|
||||||
case 'file':
|
|
||||||
const files = await Adapters.Files.decrypt(note.contents, key).catch(couldNotDecrypt)
|
|
||||||
if (!files) {
|
|
||||||
exit('No files found in note')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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) exit('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(couldNotDecrypt)
|
|
||||||
console.log(plaintext)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,104 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { Argument, Option, program } from '@commander-js/extra-typings'
|
|
||||||
import { setBase, status } from '@cryptgeon/shared'
|
|
||||||
import prettyBytes from 'pretty-bytes'
|
|
||||||
|
|
||||||
import { download } from './download.js'
|
|
||||||
import { parseFile, parseNumber } from './parsers.js'
|
|
||||||
import { getStdin } from './stdin.js'
|
|
||||||
import { upload } from './upload.js'
|
|
||||||
import { exit } from './utils.js'
|
|
||||||
|
|
||||||
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
|
|
||||||
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer)
|
|
||||||
const files = new Argument('<file...>', 'Files to be sent').argParser(parseFile)
|
|
||||||
const text = new Argument('<text>', 'Text content of the note')
|
|
||||||
const password = new Option('-p --password <string>', 'manually set a password')
|
|
||||||
const all = new Option('-a --all', 'Save all files without prompt').default(false)
|
|
||||||
const url = new Argument('<url>', 'The url to open')
|
|
||||||
const views = new Option('-v --views <number>', 'Amount of views before getting destroyed').argParser(parseNumber)
|
|
||||||
const minutes = new Option('-m --minutes <number>', 'Minutes before the note expires').argParser(parseNumber)
|
|
||||||
|
|
||||||
// Node 18 guard
|
|
||||||
parseInt(process.version.slice(1).split(',')[0]) < 18 && exit('Node 18 or higher is required')
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const version: string = VERSION
|
|
||||||
|
|
||||||
async function checkConstrains(constrains: { views?: number; minutes?: number }) {
|
|
||||||
const { views, minutes } = constrains
|
|
||||||
if (views && minutes) exit('cannot set view and minutes constrains simultaneously')
|
|
||||||
if (!views && !minutes) constrains.views = 1
|
|
||||||
|
|
||||||
const response = await status()
|
|
||||||
if (views && views > response.max_views)
|
|
||||||
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
|
|
||||||
if (minutes && minutes > response.max_expiration)
|
|
||||||
exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${minutes} given.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
program.name('cryptgeon').version(version).configureHelp({ showGlobalOptions: true })
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('info')
|
|
||||||
.description('show information about the server')
|
|
||||||
.addOption(server)
|
|
||||||
.action(async (options) => {
|
|
||||||
setBase(options.server)
|
|
||||||
const response = await status()
|
|
||||||
const formatted = {
|
|
||||||
...response,
|
|
||||||
max_size: prettyBytes(response.max_size),
|
|
||||||
}
|
|
||||||
for (const key of Object.keys(formatted)) {
|
|
||||||
if (key.startsWith('theme_')) delete formatted[key as keyof typeof formatted]
|
|
||||||
}
|
|
||||||
console.table(formatted)
|
|
||||||
})
|
|
||||||
|
|
||||||
const send = program.command('send').description('send a note')
|
|
||||||
send
|
|
||||||
.command('file')
|
|
||||||
.addArgument(files)
|
|
||||||
.addOption(server)
|
|
||||||
.addOption(views)
|
|
||||||
.addOption(minutes)
|
|
||||||
.addOption(password)
|
|
||||||
.action(async (files, options) => {
|
|
||||||
setBase(options.server!)
|
|
||||||
await checkConstrains(options)
|
|
||||||
options.password ||= await getStdin()
|
|
||||||
await upload(files, { views: options.views, expiration: options.minutes, password: options.password })
|
|
||||||
})
|
|
||||||
send
|
|
||||||
.command('text')
|
|
||||||
.addArgument(text)
|
|
||||||
.addOption(server)
|
|
||||||
.addOption(views)
|
|
||||||
.addOption(minutes)
|
|
||||||
.addOption(password)
|
|
||||||
.action(async (text, options) => {
|
|
||||||
setBase(options.server!)
|
|
||||||
await checkConstrains(options)
|
|
||||||
options.password ||= await getStdin()
|
|
||||||
await upload(text, { views: options.views, expiration: options.minutes, password: options.password })
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('open')
|
|
||||||
.description('open a link with text or files inside')
|
|
||||||
.addArgument(url)
|
|
||||||
.addOption(password)
|
|
||||||
.addOption(all)
|
|
||||||
.action(async (note, options) => {
|
|
||||||
try {
|
|
||||||
const url = new URL(note)
|
|
||||||
options.password ||= await getStdin()
|
|
||||||
await download(url, options.all, options.password)
|
|
||||||
} catch {
|
|
||||||
exit('Invalid URL')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
program.parse()
|
|
@@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
@@ -1,51 +0,0 @@
|
|||||||
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, TypedArray } from 'occulto'
|
|
||||||
|
|
||||||
import { exit } from './utils.js'
|
|
||||||
|
|
||||||
type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
|
|
||||||
|
|
||||||
export async function upload(input: string | string[], options: UploadOptions) {
|
|
||||||
try {
|
|
||||||
const { password, ...noteOptions } = options
|
|
||||||
const derived = options.password ? await AES.derive(options.password) : undefined
|
|
||||||
const key = derived ? derived[0] : await AES.generateKey()
|
|
||||||
|
|
||||||
let contents: string
|
|
||||||
let type: NoteMeta['type']
|
|
||||||
if (typeof input === 'string') {
|
|
||||||
contents = await Adapters.Text.encrypt(input, key)
|
|
||||||
type = 'text'
|
|
||||||
} else {
|
|
||||||
const files: FileDTO[] = await Promise.all(
|
|
||||||
input.map(async (path) => {
|
|
||||||
const data = new Uint8Array(await readFile(path))
|
|
||||||
const stats = await stat(path)
|
|
||||||
const extension = path.substring(path.indexOf('.') + 1)
|
|
||||||
const type = mime.getType(extension) ?? 'application/octet-stream'
|
|
||||||
return {
|
|
||||||
name: basename(path),
|
|
||||||
size: stats.size,
|
|
||||||
contents: data,
|
|
||||||
type,
|
|
||||||
} satisfies FileDTO
|
|
||||||
})
|
|
||||||
)
|
|
||||||
contents = await Adapters.Files.encrypt(files, key)
|
|
||||||
type = 'file'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the actual note and upload it.
|
|
||||||
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
|
|
||||||
const result = await create(note)
|
|
||||||
let url = `${BASE}/note/${result.id}`
|
|
||||||
if (!derived) url += `#${Hex.encode(key)}`
|
|
||||||
console.log(`Note created:\n\n${url}`)
|
|
||||||
} catch {
|
|
||||||
exit('Could not create note')
|
|
||||||
}
|
|
||||||
}
|
|