mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2025-09-06 09:20:40 +00:00
Compare commits
40 Commits
v2.0.3
...
420370acaf
Author | SHA1 | Date | |
---|---|---|---|
420370acaf | |||
4c25ca005e | |||
|
9aaad5b910 | ||
|
c246207420 | ||
7ee1b8370a | |||
e7750699cc | |||
e14042ea28 | |||
6fb7518b6a | |||
436ae2a7e5 | |||
fe5ce580ab | |||
0f882da5d1 | |||
ad6f136dd0 | |||
da527a0857 | |||
a95931ae77 | |||
d6c2236673 | |||
42a8ab5d0f | |||
0934808a59 | |||
88ea828b66 | |||
41ed5c0e23 | |||
0a98481991 | |||
5d62c48a35 | |||
0ab39023b0 | |||
7b202962e8 | |||
7a045b3f34 | |||
cb80c8bfe4 | |||
74c3197e47 | |||
6ae927ce71 | |||
9d13e607f5 | |||
0db3ef4a1f | |||
03e9fb431f | |||
b84df2866b | |||
3d4fef7c23 | |||
9d787008a4 | |||
687f26bb40 | |||
371a869800 | |||
321c303a8a | |||
2f176d84e9 | |||
67d4f09bd7 | |||
c40f009523 | |||
026f8c69d7 |
@@ -1,15 +1,15 @@
|
||||
*
|
||||
|
||||
!/backend/src
|
||||
!/backend/Cargo.lock
|
||||
!/backend/Cargo.toml
|
||||
!/packages
|
||||
!/package.json
|
||||
!/pnpm-lock.yaml
|
||||
!/pnpm-workspace.yaml
|
||||
|
||||
!/frontend/locales
|
||||
!/frontend/src
|
||||
!/frontend/static
|
||||
!/frontend/.npmrc
|
||||
!/frontend/package.json
|
||||
!/frontend/pnpm-lock.yaml
|
||||
!/frontend/svelte.config.js
|
||||
!/frontend/tsconfig.json
|
||||
!/frontend/vite.config.js
|
||||
**/target
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/bin
|
||||
**/*.tsbuildinfo
|
||||
**/build
|
||||
**/.svelte
|
||||
**/.svelte-kit
|
||||
|
5
.github/workflows/docker.yml
vendored
5
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: ci
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -31,11 +31,8 @@ jobs:
|
||||
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 }}
|
||||
|
37
.github/workflows/test.yaml
vendored
37
.github/workflows/test.yaml
vendored
@@ -1,36 +1,35 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
- uses: pnpm/action-setup@v2
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.61
|
||||
install: true
|
||||
- name: Build docker image
|
||||
run: npm run test:prepare
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run test:prepare
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps
|
||||
npm install playwright
|
||||
npx playwright install --with-deps
|
||||
|
||||
- name: Run your tests
|
||||
run: pnpm run test:run
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
run: npm test
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
|
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,14 +1,10 @@
|
||||
.env
|
||||
*.tsbuildinfo
|
||||
node_modules
|
||||
dist
|
||||
bin
|
||||
|
||||
# Backend
|
||||
target
|
||||
|
||||
# Client
|
||||
.DS_Store
|
||||
node_modules
|
||||
/.svelte
|
||||
/build
|
||||
/functions
|
||||
.env
|
||||
|
||||
General
|
||||
# Testing
|
||||
test-results
|
||||
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"cSpell.words": ["ciphertext", "cryptgeon"],
|
||||
"i18n-ally.localesPaths": ["packages/frontend/locales"],
|
||||
"i18n-ally.enabledFrameworks": ["svelte"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
52
CHANGELOG.md
52
CHANGELOG.md
@@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.2.0] - 2023-01-14
|
||||
|
||||
### 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
|
||||
@@ -21,7 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Toasts for events.
|
||||
- E2E Tests.
|
||||
- Make backend more configurable
|
||||
- Make backend more configurable.
|
||||
|
||||
## [2.0.1] - 2022-07-18
|
||||
|
||||
|
15
Dockerfile
15
Dockerfile
@@ -1,15 +1,16 @@
|
||||
# FRONTEND
|
||||
FROM node:16-alpine as client
|
||||
WORKDIR /tmp
|
||||
RUN npm install -g pnpm@7
|
||||
COPY ./packages/frontend ./
|
||||
RUN pnpm install
|
||||
RUN pnpm exec svelte-kit sync
|
||||
RUN npm install -g pnpm@8
|
||||
COPY . .
|
||||
RUN pnpm install --frozen-lockfile
|
||||
# WORKDIR /tmp/packages/frontend
|
||||
# RUN pnpm exec svelte-kit sync
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
# BACKEND
|
||||
FROM rust:1.61-alpine as backend
|
||||
FROM rust:1.69-alpine as backend
|
||||
WORKDIR /tmp
|
||||
RUN apk add libc-dev openssl-dev alpine-sdk
|
||||
COPY ./packages/backend/Cargo.* ./
|
||||
@@ -22,8 +23,8 @@ RUN cargo build --release
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||
COPY --from=client /tmp/build ./frontend
|
||||
COPY --from=client /tmp/packages/frontend/build ./frontend
|
||||
ENV FRONTEND_PATH="./frontend"
|
||||
ENV REDIS="redis://redis/"
|
||||
EXPOSE 5000
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/app/cryptgeon" ]
|
||||
|
40
README.md
40
README.md
@@ -24,9 +24,9 @@ _cryptgeon_ is a secure, open source sharing note or file service inspired by [_
|
||||
>
|
||||
> Thanks to [Lokalise](https://lokalise.com/) for providing free access to their platform.
|
||||
|
||||
## Demo
|
||||
## Live Service / Demo
|
||||
|
||||
Check out the demo and see for yourself [cryptgeon.org](https://cryptgeon.org)
|
||||
Check out the live service / demo and see for yourself [cryptgeon.org](https://cryptgeon.org)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -50,16 +50,18 @@ of the notes even if it tried to.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||
| `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` |
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -77,15 +79,19 @@ 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:5000
|
||||
- 80:8000
|
||||
```
|
||||
|
||||
### NGINX Proxy
|
||||
@@ -104,6 +110,11 @@ See the [examples/scratch](https://github.com/cupcakearmy/cryptgeon/tree/main/ex
|
||||
|
||||
There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) you can follow.
|
||||
|
||||
### YouTube Guides
|
||||
|
||||
- English by [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
|
||||
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
||||
|
||||
## Development
|
||||
|
||||
**Requirements**
|
||||
@@ -127,9 +138,6 @@ cargo install cargo-watch
|
||||
|
||||
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
|
||||
pnpm run dev
|
||||
```
|
||||
|
@@ -82,7 +82,7 @@ services:
|
||||
environment:
|
||||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:5000
|
||||
- 80:8000
|
||||
```
|
||||
|
||||
### NGINX 反向代理
|
||||
@@ -148,9 +148,6 @@ cargo install cargo-watch
|
||||
|
||||
确保你的 Docker 正在运行
|
||||
|
||||
> 如果你用的是 `macOS` 的话你可能需要关闭 AirPlay 接收功能因为该功能需要占用 5000 端口...)
|
||||
> https://developer.apple.com/forums/thread/682332
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
17
cryptgeon.code-workspace
Normal file
17
cryptgeon.code-workspace
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "packages/backend"
|
||||
},
|
||||
{
|
||||
"path": "packages/frontend"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"i18n-ally.localesPaths": ["packages/frontend/locales"],
|
||||
"cSpell.words": ["cryptgeon"]
|
||||
}
|
||||
}
|
@@ -10,11 +10,9 @@ services:
|
||||
- 6379:6379
|
||||
|
||||
app:
|
||||
# build: .
|
||||
image: cupcakearmy/cryptgeon
|
||||
build: .
|
||||
env_file: .dev.env
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
SIZE_LIMIT: 10 MiB
|
||||
ports:
|
||||
- 1234:5000
|
||||
- 1234:8000
|
18
docker-compose.yaml
Normal file
18
docker-compose.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
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
|
@@ -4,7 +4,7 @@ server {
|
||||
server_name _;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:5000/;
|
||||
proxy_pass http://app:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
@@ -20,7 +20,7 @@ server {
|
||||
ssl_trusted_certificate /path/to/fullchain.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:5000/;
|
||||
proxy_pass http://app:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
15
package.json
15
package.json
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev:docker": "docker-compose up redis",
|
||||
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
|
||||
"dev:packages": "pnpm --parallel run dev",
|
||||
"dev:proxy": "node proxy.mjs",
|
||||
"dev": "run-p dev:*",
|
||||
"test:run": "playwright test --project chrome firefox safari",
|
||||
"test": "playwright test --project chrome firefox safari",
|
||||
"test:local": "playwright test --project local",
|
||||
"test:server": "pnpm --parallel run test:server",
|
||||
"test:prepare": "pnpm --parallel run test:prepare"
|
||||
"test:server": "docker compose -f docker-compose.dev.yaml up",
|
||||
"test:prepare": "docker compose -f docker-compose.dev.yaml build",
|
||||
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.25.1",
|
||||
"@types/node": "^16.11.57",
|
||||
"http-proxy": "^1.18.1",
|
||||
"@playwright/test": "^1.32.3",
|
||||
"@types/node": "^16.18.24",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
479
packages/backend/Cargo.lock
generated
479
packages/backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cryptgeon"
|
||||
version = "2.0.3"
|
||||
version = "2.2.0"
|
||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -8,6 +8,9 @@ edition = "2021"
|
||||
name = "cryptgeon"
|
||||
path = "src/main.rs"
|
||||
|
||||
[registries.crates-io]
|
||||
protocol = "sparse"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"private": true,
|
||||
"name": "@cryptgeon/backend",
|
||||
"scripts": {
|
||||
"dev": "cargo watch -x 'run --bin cryptgeon'",
|
||||
"build": "cargo build --release",
|
||||
|
@@ -8,7 +8,7 @@ lazy_static! {
|
||||
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:5000".to_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());
|
||||
}
|
||||
|
||||
@@ -42,4 +42,12 @@ lazy_static! {
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.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();
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ async fn create(note: web::Json<Note>) -> impl Responder {
|
||||
}
|
||||
match n.views {
|
||||
Some(v) => {
|
||||
if v > *config::MAX_VIEWS {
|
||||
if v > *config::MAX_VIEWS || v < 1 {
|
||||
return bad_req;
|
||||
}
|
||||
n.expiration = None; // views overrides expiration
|
||||
|
@@ -12,4 +12,6 @@ pub struct Status {
|
||||
// Theme
|
||||
pub theme_image: String,
|
||||
pub theme_text: String,
|
||||
pub theme_page_title: String,
|
||||
pub theme_favicon: String,
|
||||
}
|
||||
|
@@ -13,6 +13,8 @@ async fn get_status() -> impl Responder {
|
||||
allow_advanced: *config::ALLOW_ADVANCED,
|
||||
theme_image: config::THEME_IMAGE.to_string(),
|
||||
theme_text: config::THEME_TEXT.to_string(),
|
||||
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
|
||||
theme_favicon: config::THEME_FAVICON.to_string()
|
||||
});
|
||||
}
|
||||
|
||||
|
43
packages/cli/package.json
Normal file
43
packages/cli/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@cryptgeon/cli",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.cjs --watch",
|
||||
"build": "esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.cjs",
|
||||
"bin": "pkg ."
|
||||
},
|
||||
"bin": {
|
||||
"cryptgeon": "./dist/index.cjs"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"pkg": {
|
||||
"scripts": "dist/**/*.js",
|
||||
"targets": [
|
||||
"node18-macos-arm64",
|
||||
"node18-macos-x64",
|
||||
"node18-linux-arm64",
|
||||
"node18-linux-x64",
|
||||
"node18-win-arm64",
|
||||
"node18-win-x64"
|
||||
],
|
||||
"outputPath": "bin"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/mime": "^3.0.1",
|
||||
"esbuild": "^0.17.18",
|
||||
"pkg": "^5.8.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@commander-js/extra-typings": "^9.5.0",
|
||||
"@cryptgeon/shared": "workspace:*",
|
||||
"commander": "^9.5.0",
|
||||
"inquirer": "^9.2.0",
|
||||
"mime": "^3.0.0",
|
||||
"occulto": "^2.0.1",
|
||||
"pretty-bytes": "^6.1.0"
|
||||
}
|
||||
}
|
63
packages/cli/src/download.ts
Normal file
63
packages/cli/src/download.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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 { Hex } from 'occulto'
|
||||
import pretty from 'pretty-bytes'
|
||||
|
||||
import { exit } from './utils'
|
||||
|
||||
export async function download(url: URL) {
|
||||
setBase(url.origin)
|
||||
const id = url.pathname.split('/')[2]
|
||||
await info(id).catch(() => exit('Note does not exist or is expired'))
|
||||
const note = await get(id)
|
||||
|
||||
const password = url.hash.slice(1)
|
||||
const key = Hex.decode(password)
|
||||
|
||||
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
|
||||
}
|
||||
console.log(files)
|
||||
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,
|
||||
})),
|
||||
},
|
||||
])
|
||||
|
||||
const selected = files.filter((file) => names.includes(file.name))
|
||||
|
||||
if (!selected.length) exit('No files selected')
|
||||
|
||||
await Promise.all(
|
||||
files.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, new Uint8Array(await file.contents.arrayBuffer()))
|
||||
console.log(`Saved: ${basename(filename)}`)
|
||||
})
|
||||
)
|
||||
break
|
||||
case 'text':
|
||||
const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(couldNotDecrypt)
|
||||
console.log(plaintext)
|
||||
break
|
||||
}
|
||||
}
|
82
packages/cli/src/index.ts
Normal file
82
packages/cli/src/index.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Argument, Option, program } from '@commander-js/extra-typings'
|
||||
import { setBase, status } from '@cryptgeon/shared'
|
||||
|
||||
import { download } from './download.js'
|
||||
import { parseFile, parseNumber } from './parsers.js'
|
||||
import { uploadFiles, uploadText } 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 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)
|
||||
|
||||
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('1.0.0').configureHelp({ showGlobalOptions: true })
|
||||
|
||||
program
|
||||
.command('info')
|
||||
.addOption(server)
|
||||
.action(async (options) => {
|
||||
setBase(options.server)
|
||||
const response = await status()
|
||||
for (const key of Object.keys(response)) {
|
||||
if (key.startsWith('theme_')) delete response[key as keyof typeof response]
|
||||
}
|
||||
console.table(response)
|
||||
})
|
||||
|
||||
const send = program.command('send')
|
||||
send
|
||||
.command('file')
|
||||
.addArgument(files)
|
||||
.addOption(server)
|
||||
.addOption(views)
|
||||
.addOption(minutes)
|
||||
.action(async (files, options) => {
|
||||
setBase(options.server!)
|
||||
await checkConstrains(options)
|
||||
await uploadFiles(files, { views: options.views, expiration: options.minutes })
|
||||
})
|
||||
send
|
||||
.command('text')
|
||||
.addArgument(text)
|
||||
.addOption(server)
|
||||
.addOption(views)
|
||||
.addOption(minutes)
|
||||
.action(async (text, options) => {
|
||||
setBase(options.server!)
|
||||
await checkConstrains(options)
|
||||
await uploadText(text, { views: options.views, expiration: options.minutes })
|
||||
})
|
||||
|
||||
program
|
||||
.command('open')
|
||||
.addArgument(url)
|
||||
.action(async (note, options) => {
|
||||
try {
|
||||
const url = new URL(note)
|
||||
await download(url)
|
||||
} catch {
|
||||
exit('Invalid URL')
|
||||
}
|
||||
})
|
||||
|
||||
program.parse()
|
27
packages/cli/src/parsers.ts
Normal file
27
packages/cli/src/parsers.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { InvalidArgumentError, InvalidOptionArgumentError } from '@commander-js/extra-typings'
|
||||
import { accessSync, constants } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export function parseFile(value: string, before: string[] = []) {
|
||||
try {
|
||||
const file = path.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
|
||||
}
|
49
packages/cli/src/upload.ts
Normal file
49
packages/cli/src/upload.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Blob } from 'node:buffer'
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { basename } from 'node:path'
|
||||
|
||||
import { Adapters, BASE, create, FileDTO, Note } from '@cryptgeon/shared'
|
||||
import mime from 'mime'
|
||||
import { AES, Hex, TypedArray } from 'occulto'
|
||||
|
||||
import { exit } from './utils.js'
|
||||
|
||||
type UploadOptions = Pick<Note, 'views' | 'expiration'>
|
||||
|
||||
export async function upload(key: TypedArray, note: Note) {
|
||||
try {
|
||||
const result = await create(note)
|
||||
const password = Hex.encode(key)
|
||||
const url = `${BASE}/note/${result.id}#${password}`
|
||||
console.log(`Note created under:\n\n${url}`)
|
||||
} catch {
|
||||
exit('Could not create note')
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFiles(paths: string[], options: UploadOptions) {
|
||||
const key = await AES.generateKey()
|
||||
const files: FileDTO[] = await Promise.all(
|
||||
paths.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: new Blob([data]) as FileDTO['contents'],
|
||||
type,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const contents = await Adapters.Files.encrypt(files, key)
|
||||
await upload(key, { ...options, contents, meta: { type: 'file' } })
|
||||
}
|
||||
|
||||
export async function uploadText(text: string, options: UploadOptions) {
|
||||
const key = await AES.generateKey()
|
||||
const contents = await Adapters.Text.encrypt(text, key)
|
||||
await upload(key, { ...options, contents, meta: { type: 'text' } })
|
||||
}
|
6
packages/cli/src/utils.ts
Normal file
6
packages/cli/src/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import process from 'node:process'
|
||||
|
||||
export function exit(message: string) {
|
||||
console.error(message)
|
||||
process.exit(1)
|
||||
}
|
110
packages/cli/tsconfig.json
Normal file
110
packages/cli/tsconfig.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "es2022" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
// "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */,
|
||||
// "paths": {
|
||||
// "@shared/*": ["../shared/*"],
|
||||
// "@shared": ["../shared"]
|
||||
// } /* Specify a set of entries that re-map imports to additional lookup locations. */,
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [
|
||||
// "../shared/types.ts",
|
||||
// "node"
|
||||
// ] /* Specify type package names to be included without being referenced in a source file. */,
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
// "references": [{ "path": "../shared" }]
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/.svelte
|
||||
/build
|
||||
/functions
|
@@ -1 +0,0 @@
|
||||
engine-strict=true
|
@@ -15,7 +15,8 @@
|
||||
"encrypting": "verschlüsseln",
|
||||
"decrypting": "entschlüsselt",
|
||||
"uploading": "hochladen",
|
||||
"downloading": "wird heruntergeladen"
|
||||
"downloading": "wird heruntergeladen",
|
||||
"qr_code": "qr-code"
|
||||
},
|
||||
"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.",
|
||||
|
@@ -15,7 +15,8 @@
|
||||
"encrypting": "encrypting",
|
||||
"decrypting": "decrypting",
|
||||
"uploading": "uploading",
|
||||
"downloading": "downloading"
|
||||
"downloading": "downloading",
|
||||
"qr_code": "qr code"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
||||
|
@@ -15,7 +15,8 @@
|
||||
"encrypting": "encriptando",
|
||||
"decrypting": "descifrando",
|
||||
"uploading": "cargando",
|
||||
"downloading": "descargando"
|
||||
"downloading": "descargando",
|
||||
"qr_code": "código qr"
|
||||
},
|
||||
"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.",
|
||||
|
@@ -15,7 +15,8 @@
|
||||
"encrypting": "cryptage",
|
||||
"decrypting": "déchiffrer",
|
||||
"uploading": "téléchargement",
|
||||
"downloading": "téléchargement"
|
||||
"downloading": "téléchargement",
|
||||
"qr_code": "code qr"
|
||||
},
|
||||
"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.",
|
||||
|
@@ -15,7 +15,8 @@
|
||||
"encrypting": "criptando",
|
||||
"decrypting": "decifrando",
|
||||
"uploading": "caricamento",
|
||||
"downloading": "scaricando"
|
||||
"downloading": "scaricando",
|
||||
"qr_code": "codice qr"
|
||||
},
|
||||
"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.",
|
||||
|
51
packages/frontend/locales/zh.json
Normal file
51
packages/frontend/locales/zh.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"common": {
|
||||
"note": "密信",
|
||||
"file": "上传文件",
|
||||
"advanced": "高级设置",
|
||||
"create": "创建",
|
||||
"loading": "加载中",
|
||||
"mode": "模式",
|
||||
"views": "{n, plural, =0 {可读次数} =1 {1 次查看} other {# 次查看}}",
|
||||
"minutes": "{n, plural, =0 {有效期(分钟)} =1 {1 分钟} other {# 分钟}}",
|
||||
"max": "最大值",
|
||||
"share_link": "分享链接",
|
||||
"copy_clipboard": "复制到粘贴板",
|
||||
"copied_to_clipboard": "已成功复制到粘贴板",
|
||||
"encrypting": "加密",
|
||||
"decrypting": "解密",
|
||||
"uploading": "上传",
|
||||
"downloading": "下载",
|
||||
"qr_code": "二维码"
|
||||
},
|
||||
"home": {
|
||||
"intro": "飞鸽传书,一键传输完全加密的密信或文件,阅后即焚。",
|
||||
"explanation": "该密信会在{type}后失效。",
|
||||
"new_note": "新建密信",
|
||||
"new_note_notice": "<b>提醒:</b><br>密信保存在内存中,如果内存满了,则最早的密信将被删除以释放内存,因此不保证该密信的可用性。一般不会出现这种情况,无需担心。",
|
||||
"errors": {
|
||||
"note_to_big": "创建失败,密信过大。",
|
||||
"note_error": "创建失败,请稍后重试。",
|
||||
"max": "次数上限:{n}",
|
||||
"empty_content": "密信不能为空。"
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "密信创建成功。"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "密信不存在,可能已被查看或删除。",
|
||||
"decryption_failed": "密钥错误,无法查看。",
|
||||
"unsupported_type": "不支持的类型。"
|
||||
},
|
||||
"explanation": "点击下方按钮即可查看密信,阅后即焚。",
|
||||
"show_note": "查看密信",
|
||||
"warning_will_not_see_again": "你将<b>无法</b>再次查看该密信,请尽快复制到粘贴板。",
|
||||
"download_all": "下载全部"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "已选中的文件",
|
||||
"no_files_selected": "没有文件被选中"
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"note": "密信",
|
||||
"file": "上传文件",
|
||||
"advanced": "高级设置",
|
||||
"create": "创建",
|
||||
"loading": "加载中",
|
||||
"mode": "模式",
|
||||
"views": "{n, plural, =0 {可查看次数} =1 {1 次查看} other {# 次查看}}",
|
||||
"minutes": "{n, plural, =0 {有效期(分钟)} =1 {1 分钟} other {# 分钟}}",
|
||||
"max": "最大值",
|
||||
"share_link": "分享链接",
|
||||
"copy_clipboard": "复制到剪切版",
|
||||
"copied_to_clipboard": "已复制到剪切板",
|
||||
"encrypting": "加密",
|
||||
"decrypting": "解密",
|
||||
"uploading": "上传",
|
||||
"downloading": "下载"
|
||||
},
|
||||
"home": {
|
||||
"intro": "一键轻松发送 <i>完全加密的</i> 密信或者文件。只需创建一个密信然后分享链接。",
|
||||
"explanation": "该密信会在{type}后失效。",
|
||||
"new_note": "新建密信",
|
||||
"new_note_notice": "<b>可用性警示:</b><br />由于加密鸽的所有数据是全部保存在内存中的,所以如果加密鸽的可用内存被用光了那么它将会删除最早的密信以释放内存,因此不保证该密信的可用性。<br />(一般情况下是您应该是不会遇到这个问题,只是警示一下。)",
|
||||
"errors": {
|
||||
"note_to_big": "无法创建密信,这个密信太大了!",
|
||||
"note_error": "无法创建密信,请再试一遍。",
|
||||
"max": "最大文件大小: {n}",
|
||||
"empty_content": "密信为空!"
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "注释创建。"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "该密信无法被找到或者它已经被删除了!",
|
||||
"decryption_failed": "密钥错误!您可能不小心粘贴了一个不完整的链接或者正在尝试破解该密信!但无论如何,该密信已被销毁!",
|
||||
"unsupported_type": "不支持的票据类型。"
|
||||
},
|
||||
"explanation": "点击下方的按钮可以查看密信,如果它到达了限制将会被删除",
|
||||
"show_note": "查看密信",
|
||||
"warning_will_not_see_again": "您将<b>无法</b>再次查看该密信",
|
||||
"download_all": "下载全部"
|
||||
},
|
||||
"file_upload": {
|
||||
"selected_files": "已选中的文件",
|
||||
"no_files_selected": "没有文件被选中"
|
||||
}
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@cryptgeon/web",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 3000",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig tsconfig.json",
|
||||
"licenses": "license-checker --summary > licenses.csv",
|
||||
"locale:download": "node scripts/locale.js",
|
||||
@@ -11,27 +12,30 @@
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@lokalise/node-api": "^7.3.1",
|
||||
"@sveltejs/adapter-static": "1.0.0-next.42",
|
||||
"@sveltejs/kit": "1.0.0-next.480",
|
||||
"@types/dompurify": "^2.3.3",
|
||||
"@lokalise/node-api": "^9.8.0",
|
||||
"@sveltejs/adapter-static": "^1.0.6",
|
||||
"@sveltejs/kit": "^1.15.8",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@zerodevx/svelte-toast": "^0.7.2",
|
||||
"adm-zip": "^0.5.9",
|
||||
"dotenv": "^16.0.1",
|
||||
"svelte": "^3.49.0",
|
||||
"svelte-check": "^2.8.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"dotenv": "^16.0.3",
|
||||
"svelte": "^3.58.0",
|
||||
"svelte-check": "^2.10.3",
|
||||
"svelte-intl-precompile": "^0.10.1",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.2"
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.8",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"dompurify": "^2.3.10",
|
||||
"@cryptgeon/shared": "workspace:*",
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dompurify": "^2.4.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"pretty-bytes": "^5.6.0"
|
||||
"occulto": "^2.0.0",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"qrious": "^4.0.2"
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import dotenv from 'dotenv'
|
||||
import { LokaliseApi } from '@lokalise/node-api'
|
||||
import https from 'https'
|
||||
import AdmZip from 'adm-zip'
|
||||
import dotenv from 'dotenv'
|
||||
import https from 'https'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
|
@@ -87,6 +87,8 @@ button {
|
||||
font-size: inherit;
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding-inline: initial;
|
||||
}
|
||||
|
||||
*:disabled,
|
||||
|
9
packages/frontend/src/app.d.ts
vendored
Normal file
9
packages/frontend/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
@@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
%sveltekit.head%
|
||||
|
3
packages/frontend/src/global.d.ts
vendored
3
packages/frontend/src/global.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
@@ -1,61 +0,0 @@
|
||||
import type { EncryptedFileDTO, FileDTO } from './api'
|
||||
import { Crypto } from './crypto'
|
||||
|
||||
abstract class CryptAdapter<T> {
|
||||
abstract encrypt(plaintext: T, key: CryptoKey): Promise<string>
|
||||
abstract decrypt(ciphertext: string, key: CryptoKey): Promise<T>
|
||||
}
|
||||
|
||||
class CryptTextAdapter implements CryptAdapter<string> {
|
||||
async encrypt(plaintext: string, key: CryptoKey) {
|
||||
return await Crypto.encrypt(new TextEncoder().encode(plaintext), key)
|
||||
}
|
||||
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||
const plaintext = await Crypto.decrypt(ciphertext, key)
|
||||
return new TextDecoder().decode(plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
class CryptBlobAdapter implements CryptAdapter<Blob> {
|
||||
async encrypt(plaintext: Blob, key: CryptoKey) {
|
||||
return await Crypto.encrypt(await plaintext.arrayBuffer(), key)
|
||||
}
|
||||
|
||||
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||
const plaintext = await Crypto.decrypt(ciphertext, key)
|
||||
return new Blob([plaintext], { type: 'application/octet-stream' })
|
||||
}
|
||||
}
|
||||
|
||||
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
|
||||
async encrypt(plaintext: FileDTO[], key: CryptoKey) {
|
||||
const adapter = new CryptBlobAdapter()
|
||||
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
contents: await adapter.encrypt(file.contents, key),
|
||||
}))
|
||||
return JSON.stringify(await Promise.all(data))
|
||||
}
|
||||
|
||||
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||
const adapter = new CryptBlobAdapter()
|
||||
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
|
||||
const files: FileDTO[] = await Promise.all(
|
||||
data.map(async (file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
contents: await adapter.decrypt(file.contents, key),
|
||||
}))
|
||||
)
|
||||
return files
|
||||
}
|
||||
}
|
||||
|
||||
export const Adapters = {
|
||||
Text: new CryptTextAdapter(),
|
||||
Blob: new CryptBlobAdapter(),
|
||||
Files: new CryptFilesAdapter(),
|
||||
}
|
@@ -1,78 +0,0 @@
|
||||
export type NoteMeta = { type: 'text' | 'file' }
|
||||
|
||||
export type Note = {
|
||||
contents: string
|
||||
meta: NoteMeta
|
||||
views?: number
|
||||
expiration?: number
|
||||
}
|
||||
export type NoteInfo = {}
|
||||
export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
||||
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
||||
|
||||
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
||||
contents: Blob
|
||||
}
|
||||
|
||||
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
||||
contents: string
|
||||
}
|
||||
|
||||
type CallOptions = {
|
||||
url: string
|
||||
method: string
|
||||
body?: any
|
||||
}
|
||||
|
||||
export class PayloadToLargeError extends Error {}
|
||||
|
||||
export async function call(options: CallOptions) {
|
||||
const response = await fetch('/api/' + options.url, {
|
||||
method: options.method,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 413) throw new PayloadToLargeError()
|
||||
else throw new Error('API call failed')
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function create(note: Note) {
|
||||
const { meta, ...rest } = note
|
||||
const body: NoteCreate = {
|
||||
...rest,
|
||||
meta: JSON.stringify(meta),
|
||||
}
|
||||
const data = await call({
|
||||
url: 'notes/',
|
||||
method: 'post',
|
||||
body,
|
||||
})
|
||||
return data as { id: string }
|
||||
}
|
||||
|
||||
export async function get(id: string): Promise<NotePublic> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
const { contents, meta } = data
|
||||
return {
|
||||
contents,
|
||||
meta: JSON.parse(meta) as NoteMeta,
|
||||
}
|
||||
}
|
||||
|
||||
export async function info(id: string): Promise<NoteInfo> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
return data
|
||||
}
|
@@ -1,89 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export class ArrayBufferUtils {
|
||||
static async toString(buffer: ArrayBuffer): Promise<string> {
|
||||
const reader = new window.FileReader()
|
||||
reader.readAsDataURL(new Blob([buffer]))
|
||||
return new Promise((resolve) => {
|
||||
reader.onloadend = () => resolve(reader.result as string)
|
||||
})
|
||||
}
|
||||
|
||||
static async fromString(s: string): Promise<ArrayBuffer> {
|
||||
return fetch(s)
|
||||
.then((r) => r.blob())
|
||||
.then((b) => b.arrayBuffer())
|
||||
}
|
||||
}
|
||||
|
||||
export class Keys {
|
||||
public static async generateKey(size: 128 | 192 | 256 = 256): Promise<CryptoKey> {
|
||||
const key = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: size,
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
return key
|
||||
}
|
||||
|
||||
public static async export(key: CryptoKey): Promise<string> {
|
||||
return Hex.encode(await window.crypto.subtle.exportKey('raw', key))
|
||||
}
|
||||
|
||||
public static async import(key: string): Promise<CryptoKey> {
|
||||
return window.crypto.subtle.importKey('raw', Hex.decode(key), { name: 'AES-GCM' }, true, [
|
||||
'encrypt',
|
||||
'decrypt',
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export class Crypto {
|
||||
private static ALG = 'AES-GCM'
|
||||
private static DELIMITER = ':::'
|
||||
|
||||
public static getRandomBytes(size: number): Uint8Array {
|
||||
return window.crypto.getRandomValues(new Uint8Array(size))
|
||||
}
|
||||
|
||||
public static async encrypt(plaintext: ArrayBuffer, key: CryptoKey): Promise<string> {
|
||||
const iv = this.getRandomBytes(12) // AES-GCM needs a 96bit IV
|
||||
const encrypted: ArrayBuffer = await window.crypto.subtle.encrypt(
|
||||
{ name: this.ALG, iv },
|
||||
key,
|
||||
plaintext
|
||||
)
|
||||
const data = [Hex.encode(iv), await ArrayBufferUtils.toString(encrypted)].join(this.DELIMITER)
|
||||
return data
|
||||
}
|
||||
|
||||
public static async decrypt(ciphertext: string, key: CryptoKey): Promise<ArrayBuffer> {
|
||||
const splitted = ciphertext.split(this.DELIMITER)
|
||||
const iv = Hex.decode(splitted[0])
|
||||
const encrypted = await ArrayBufferUtils.fromString(splitted[1])
|
||||
const plaintext = await window.crypto.subtle.decrypt({ name: this.ALG, iv }, key, encrypted)
|
||||
return plaintext
|
||||
}
|
||||
}
|
@@ -1,22 +1,8 @@
|
||||
import { call } from '$lib/api'
|
||||
import { status as getStatus, type Status } from '@cryptgeon/shared'
|
||||
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)
|
||||
status.set(await getStatus())
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import type { Note } from '$lib/api'
|
||||
import { status } from '$lib/stores/status'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import type { Note } from '@cryptgeon/shared'
|
||||
|
||||
export let note: Note
|
||||
export let timeExpiration = false
|
||||
@@ -18,8 +18,9 @@
|
||||
bind:value={note.views}
|
||||
disabled={timeExpiration}
|
||||
max={$status?.max_views}
|
||||
min={1}
|
||||
validate={(v) =>
|
||||
($status && v <= $status?.max_views) ||
|
||||
($status && v <= $status?.max_views && v > 0) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
||||
/>
|
||||
<div class="middle-switch">
|
||||
|
42
packages/frontend/src/lib/ui/Canvas.svelte
Normal file
42
packages/frontend/src/lib/ui/Canvas.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
// @ts-ignore
|
||||
import QR from 'qrious'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import { getCSSVariable } from '$lib/utils'
|
||||
|
||||
export let value: string
|
||||
|
||||
let canvas: HTMLCanvasElement
|
||||
|
||||
$: {
|
||||
new QR({
|
||||
value,
|
||||
level: 'Q',
|
||||
size: 800,
|
||||
background: getCSSVariable('--ui-bg-0'),
|
||||
foreground: getCSSVariable('--ui-text-0'),
|
||||
element: canvas,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<small>{$t('common.qr_code')}</small>
|
||||
<div>
|
||||
<canvas bind:this={canvas} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 0.5rem;
|
||||
width: fit-content;
|
||||
border: 2px solid var(--ui-bg-1);
|
||||
background-color: var(--ui-bg-0);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import type { FileDTO } from '$lib/api'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||
import type { FileDTO } from '@cryptgeon/shared'
|
||||
|
||||
export let label: string = ''
|
||||
export let files: FileDTO[] = []
|
||||
|
@@ -10,9 +10,12 @@
|
||||
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import Canvas from './Canvas.svelte'
|
||||
|
||||
export let result: NoteResult
|
||||
|
||||
$: url = `${window.location.origin}/note/${result.id}#${result.password}`
|
||||
|
||||
function reset() {
|
||||
window.location.reload()
|
||||
}
|
||||
@@ -22,11 +25,15 @@
|
||||
type="text"
|
||||
readonly
|
||||
label={$t('common.share_link')}
|
||||
value="{window.location.origin}/note/{result.id}#{result.password}"
|
||||
value={url}
|
||||
copy
|
||||
data-testid="share-link"
|
||||
/>
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<Canvas value={url} />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
@@ -34,4 +41,9 @@
|
||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: min(12rem, 100%);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
@@ -8,9 +8,9 @@
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import type { FileDTO, NotePublic } from '$lib/api'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import { copy } from '$lib/utils'
|
||||
import type { FileDTO, NotePublic } from '@cryptgeon/shared'
|
||||
|
||||
export let note: DecryptedNote
|
||||
|
||||
@@ -53,7 +53,9 @@
|
||||
{:else}
|
||||
{#each files as file}
|
||||
<div class="note file">
|
||||
<b on:click={() => downloadFile(file)}>↓ {file.name}</b>
|
||||
<button on:click={() => downloadFile(file)}>
|
||||
<b>↓ {file.name}</b>
|
||||
</button>
|
||||
<small> {file.type} - {prettyBytes(file.size)}</small>
|
||||
</div>
|
||||
{/each}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Crypto, Hex } from '$lib/crypto'
|
||||
import Icon from '$lib/ui/Icon.svelte'
|
||||
import { copy as copyFN } from '$lib/utils'
|
||||
import { getRandomBytes, Hex } from 'occulto'
|
||||
|
||||
export let label: string = ''
|
||||
export let value: any
|
||||
@@ -23,8 +23,9 @@
|
||||
function toggle() {
|
||||
hidden = !hidden
|
||||
}
|
||||
function randomFN() {
|
||||
value = Hex.encode(Crypto.getRandomBytes(32))
|
||||
|
||||
async function randomFN() {
|
||||
value = Hex.encode(await getRandomBytes(32))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@@ -40,19 +40,19 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div on:click={change}>
|
||||
<button on:click={change}>
|
||||
<Icon class="icon" icon="contrast" />
|
||||
{$theme}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
div :global(.icon) {
|
||||
button :global(.icon) {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
div {
|
||||
button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
@@ -9,3 +9,8 @@ export function copy(value: string) {
|
||||
const msg = get(t)('common.copied_to_clipboard')
|
||||
notify.success(msg)
|
||||
}
|
||||
|
||||
export function getCSSVariable(variable: string): string {
|
||||
if (typeof window === 'undefined') return ''
|
||||
return window.getComputedStyle(window.document.body).getPropertyValue(variable)
|
||||
}
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { AES, Hex } from 'occulto'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { blur } from 'svelte/transition'
|
||||
|
||||
import { Adapters } from '$lib/adapters'
|
||||
import type { FileDTO, Note } from '$lib/api'
|
||||
import { create, PayloadToLargeError } from '$lib/api'
|
||||
import { Keys } from '$lib/crypto'
|
||||
import { status } from '$lib/stores/status'
|
||||
import { notify } from '$lib/toast'
|
||||
import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte'
|
||||
@@ -16,6 +13,8 @@
|
||||
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextArea from '$lib/ui/TextArea.svelte'
|
||||
import type { FileDTO, Note } from '@cryptgeon/shared'
|
||||
import { Adapters, create, PayloadToLargeError } from '@cryptgeon/shared'
|
||||
|
||||
let note: Note = {
|
||||
contents: '',
|
||||
@@ -58,8 +57,8 @@
|
||||
try {
|
||||
loading = $t('common.encrypting')
|
||||
|
||||
const key = await Keys.generateKey()
|
||||
const password = await Keys.export(key)
|
||||
const key = await AES.generateKey()
|
||||
const password = Hex.encode(key)
|
||||
|
||||
const data: Note = {
|
||||
contents: '',
|
||||
|
@@ -7,7 +7,9 @@
|
||||
<nav>
|
||||
<a href="/">/home</a>
|
||||
<a href="/about">/about</a>
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">/code</a>
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
||||
code
|
||||
</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import '../app.css'
|
||||
|
||||
import { init as initStores } from '$lib/stores/status'
|
||||
import { init as initStores, status } from '$lib/stores/status'
|
||||
import Footer from '$lib/views/Footer.svelte'
|
||||
import Header from '$lib/views/Header.svelte'
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>cryptgeon</title>
|
||||
<title>{$status?.theme_page_title || 'cryptgeon'}</title>
|
||||
<link rel="icon" href={$status?.theme_favicon || '/favicon.png'} />
|
||||
</svelte:head>
|
||||
|
||||
{#await waitLocale() then _}
|
||||
|
@@ -39,7 +39,7 @@
|
||||
the backend is written in rust and the frontend is svelte and typescript.
|
||||
<br />
|
||||
you are welcomed to check & audit the
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
||||
source code
|
||||
</a>.
|
||||
</span>
|
||||
@@ -47,9 +47,12 @@
|
||||
|
||||
<AboutParagraph title="translations">
|
||||
<span
|
||||
>translations are managed on <a href="https://lokalise.com/" target="_blank">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!
|
||||
>translations are managed on <a
|
||||
href="https://lokalise.com/"
|
||||
target="_blank"
|
||||
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>
|
||||
</AboutParagraph>
|
||||
|
||||
|
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Hex } from 'occulto'
|
||||
import { onMount } from 'svelte'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import { Adapters } from '$lib/adapters'
|
||||
import { get, info } from '$lib/api'
|
||||
import { Keys } from '$lib/crypto'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import Loader from '$lib/ui/Loader.svelte'
|
||||
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
||||
import { Adapters, get, info } from '@cryptgeon/shared'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
export let data: PageData
|
||||
@@ -43,7 +42,7 @@
|
||||
loading = $t('common.downloading')
|
||||
const data = await get(id)
|
||||
loading = $t('common.decrypting')
|
||||
const key = await Keys.import(password)
|
||||
const key = Hex.decode(password)
|
||||
switch (data.meta.type) {
|
||||
case 'text':
|
||||
note = {
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
||||
|
||||
const port = 8001
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
server: { port },
|
||||
preview: { port },
|
||||
plugins: [sveltekit(), precompileIntl('locales')],
|
||||
}
|
||||
|
||||
|
12
packages/proxy/package.json
Normal file
12
packages/proxy/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@cryptgeon/proxy",
|
||||
"type": "module",
|
||||
"main": "./proxy.js",
|
||||
"scripts": {
|
||||
"dev": "node ."
|
||||
},
|
||||
"dependencies": {
|
||||
"http-proxy": "^1.18.1"
|
||||
}
|
||||
}
|
@@ -3,12 +3,13 @@ import httpProxy from 'http-proxy'
|
||||
|
||||
const proxy = httpProxy.createProxyServer()
|
||||
proxy.on('error', function (err, req, res) {
|
||||
console.error(err)
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
||||
res.end('500 Internal Server Error')
|
||||
})
|
||||
|
||||
const server = http.createServer(function (req, res) {
|
||||
const target = req.url.startsWith('/api/') ? 'http://localhost:5000' : 'http://localhost:3000'
|
||||
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
|
||||
proxy.web(req, res, { target })
|
||||
})
|
||||
server.listen(1234)
|
22
packages/shared/package.json
Normal file
22
packages/shared/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@cryptgeon/shared",
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsc -w",
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"occulto": "^2.0.0"
|
||||
}
|
||||
}
|
60
packages/shared/src/adapters.ts
Normal file
60
packages/shared/src/adapters.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { AES, Bytes, type TypedArray } from 'occulto'
|
||||
import type { EncryptedFileDTO, FileDTO } from './api'
|
||||
|
||||
abstract class CryptAdapter<T> {
|
||||
abstract encrypt(plaintext: T, key: TypedArray): Promise<string>
|
||||
abstract decrypt(ciphertext: string, key: TypedArray): Promise<T>
|
||||
}
|
||||
|
||||
class CryptTextAdapter implements CryptAdapter<string> {
|
||||
async encrypt(plaintext: string, key: TypedArray) {
|
||||
return await AES.encrypt(Bytes.encode(plaintext), key)
|
||||
}
|
||||
async decrypt(ciphertext: string, key: TypedArray) {
|
||||
return Bytes.decode(await AES.decrypt(ciphertext, key))
|
||||
}
|
||||
}
|
||||
|
||||
class CryptBlobAdapter implements CryptAdapter<Blob> {
|
||||
async encrypt(plaintext: Blob, key: TypedArray) {
|
||||
return await AES.encrypt(new Uint8Array(await plaintext.arrayBuffer()), key)
|
||||
}
|
||||
|
||||
async decrypt(ciphertext: string, key: TypedArray) {
|
||||
const plaintext = await AES.decrypt(ciphertext, key)
|
||||
return new Blob([plaintext], { type: 'application/octet-stream' })
|
||||
}
|
||||
}
|
||||
|
||||
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
|
||||
async encrypt(plaintext: FileDTO[], key: TypedArray) {
|
||||
const adapter = new CryptBlobAdapter()
|
||||
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
contents: await adapter.encrypt(file.contents, key),
|
||||
}))
|
||||
return JSON.stringify(await Promise.all(data))
|
||||
}
|
||||
|
||||
async decrypt(ciphertext: string, key: TypedArray) {
|
||||
const adapter = new CryptBlobAdapter()
|
||||
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
|
||||
const files: FileDTO[] = await Promise.all(
|
||||
data.map(async (file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
contents: await adapter.decrypt(file.contents, key),
|
||||
}))
|
||||
)
|
||||
return files
|
||||
}
|
||||
}
|
||||
|
||||
export const Adapters = {
|
||||
Text: new CryptTextAdapter(),
|
||||
Blob: new CryptBlobAdapter(),
|
||||
Files: new CryptFilesAdapter(),
|
||||
}
|
104
packages/shared/src/api.ts
Normal file
104
packages/shared/src/api.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
export type NoteMeta = { type: 'text' | 'file' }
|
||||
|
||||
export type Note = {
|
||||
contents: string
|
||||
meta: NoteMeta
|
||||
views?: number
|
||||
expiration?: number
|
||||
}
|
||||
export type NoteInfo = {}
|
||||
export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
||||
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
||||
|
||||
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
||||
contents: Blob
|
||||
}
|
||||
|
||||
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
||||
contents: string
|
||||
}
|
||||
|
||||
type CallOptions = {
|
||||
url: string
|
||||
method: string
|
||||
body?: any
|
||||
}
|
||||
|
||||
export class PayloadToLargeError extends Error {}
|
||||
|
||||
export let BASE = ''
|
||||
|
||||
export function setBase(url: string) {
|
||||
BASE = url
|
||||
}
|
||||
|
||||
export async function call(options: CallOptions) {
|
||||
const response = await fetch(BASE + '/api/' + options.url, {
|
||||
method: options.method,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 413) throw new PayloadToLargeError()
|
||||
else throw new Error('API call failed')
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function create(note: Note) {
|
||||
const { meta, ...rest } = note
|
||||
const body: NoteCreate = {
|
||||
...rest,
|
||||
meta: JSON.stringify(meta),
|
||||
}
|
||||
const data = await call({
|
||||
url: 'notes/',
|
||||
method: 'post',
|
||||
body,
|
||||
})
|
||||
return data as { id: string }
|
||||
}
|
||||
|
||||
export async function get(id: string): Promise<NotePublic> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
const { contents, meta } = data
|
||||
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
|
||||
}
|
||||
|
||||
export type Status = {
|
||||
version: string
|
||||
max_size: number
|
||||
max_views: number
|
||||
max_expiration: number
|
||||
allow_advanced: boolean
|
||||
theme_image: string
|
||||
theme_text: string
|
||||
theme_favicon: string
|
||||
theme_page_title: string
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
const data = await call({
|
||||
url: 'status/',
|
||||
method: 'get',
|
||||
})
|
||||
return data as Status
|
||||
}
|
2
packages/shared/src/index.ts
Normal file
2
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './adapters.js'
|
||||
export * from './api.js'
|
103
packages/shared/tsconfig.json
Normal file
103
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
"incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */,
|
||||
"composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */,
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "es2022" /* Specify what module code is generated. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
@@ -10,10 +10,9 @@ const config: PlaywrightTestConfig = {
|
||||
outputDir: './test-results',
|
||||
testDir: './test',
|
||||
timeout: 60_000,
|
||||
testIgnore: ['file/too-big.spec.ts'],
|
||||
|
||||
webServer: {
|
||||
command: 'pnpm run test:server',
|
||||
command: 'docker compose -f docker-compose.dev.yaml up',
|
||||
port: 1234,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
@@ -25,7 +24,6 @@ const config: PlaywrightTestConfig = {
|
||||
{
|
||||
name: 'local',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
// testMatch: 'file/too-big.spec.ts',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
2835
pnpm-lock.yaml
generated
2835
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
PDF: 'test/assets/AES.pdf',
|
||||
Image: 'test/assets/alfred-kenneally-UIu4RmMxnHU-unsplash.jpg',
|
||||
Image: 'test/assets/image.jpg',
|
||||
Zip: 'test/assets/Pigeons.zip',
|
||||
}
|
||||
|
@@ -6,6 +6,6 @@ test('multiple', async ({ page }) => {
|
||||
const files = [Files.PDF, Files.Image]
|
||||
const checksums = await Promise.all(files.map(getFileChecksum))
|
||||
const link = await createNote(page, { files, views: 2 })
|
||||
await checkLinkForDownload(page, link, 'alfred-kenneally', checksums[1])
|
||||
await checkLinkForDownload(page, link, 'image.jpg', checksums[1])
|
||||
await checkLinkForDownload(page, link, 'AES.pdf', checksums[0])
|
||||
})
|
||||
|
@@ -20,5 +20,5 @@ test('image content', async ({ page }) => {
|
||||
const files = [Files.Image]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNote(page, { files })
|
||||
await checkLinkForDownload(page, link, 'alfred-kenneally', checksum)
|
||||
await checkLinkForDownload(page, link, 'image.jpg', checksum)
|
||||
})
|
||||
|
@@ -2,7 +2,7 @@ import { test } from '@playwright/test'
|
||||
import { createNote } from '../utils'
|
||||
import Files from './files'
|
||||
|
||||
test('to big zip', async ({ page }) => {
|
||||
test.skip('to big zip', async ({ page }) => {
|
||||
const files = [Files.Zip]
|
||||
const link = await createNote(page, { files, error: 'note is to big' })
|
||||
})
|
||||
|
Reference in New Issue
Block a user