Compare commits

...

35 Commits

Author SHA1 Message Date
42a8ab5d0f test command 2022-11-15 13:15:34 +01:00
0934808a59 testing 2022-11-15 13:05:13 +01:00
88ea828b66 upload 2022-11-15 12:35:02 +01:00
41ed5c0e23 ci 2022-11-15 12:17:52 +01:00
0a98481991 use npx 2022-11-15 12:13:03 +01:00
5d62c48a35 don't use pnpm 2022-11-14 16:26:51 +01:00
0ab39023b0 test 2022-11-14 16:16:50 +01:00
7b202962e8 testing 2022-11-14 15:55:49 +01:00
7a045b3f34 test on docker image 2022-11-14 15:47:12 +01:00
cb80c8bfe4 changelog 2022-11-12 14:40:21 +01:00
74c3197e47 update dependencies and fix some a11y issues 2022-11-12 14:40:17 +01:00
6ae927ce71 update version and dependencies 2022-11-12 13:55:33 +01:00
9d13e607f5 #66 set minimum 2022-11-12 13:42:09 +01:00
0db3ef4a1f changelog and only test on x86 2022-11-04 23:34:36 +01:00
03e9fb431f put flows back together 2022-11-04 22:10:19 +01:00
b84df2866b build matrix 2022-11-02 13:36:56 +01:00
3d4fef7c23 try with matrix build 2022-11-02 13:34:41 +01:00
9d787008a4 also build docker when testing 2022-11-02 13:29:19 +01:00
687f26bb40 name the workflow 2022-11-02 13:26:57 +01:00
371a869800 use nightly cargo with sparse registry 2022-11-02 13:24:30 +01:00
321c303a8a changelog 2022-10-29 19:45:14 +02:00
2f176d84e9 wrong docker compose 2022-10-29 19:43:59 +02:00
67d4f09bd7 #62 (#63)
* #62 add theme options for title and favicon

* docs

* version bump
2022-10-27 17:26:56 +02:00
c40f009523 Update README.md 2022-10-24 16:35:54 +02:00
026f8c69d7 add size limit to redis 2022-10-24 16:11:50 +02:00
cacb808117 restructuring (#56)
* restructuring

* pin svelte kit version & parallel execution

* update svelte kit

* correct test result assets

* add timeout

* correct locale path

* simplify crypto

* fix for #58

* add verbosity flag

* disable flaky test
2022-10-07 21:28:25 +02:00
2d573edcac change link 2022-09-12 14:24:05 +02:00
4287cd429d security reporting 2022-09-10 13:13:09 +02:00
024dfeeeb7 add url spec 2022-07-26 23:48:53 +02:00
f24bcba20b remove ununsed 2022-07-26 15:49:12 +02:00
1d95edc455 readme 2022-07-26 15:49:06 +02:00
hash070
ec24ab3edd Update CN README translate (#47) 2022-07-21 11:32:38 +02:00
a552e4d766 Toasts (#45)
* locales

* add toasts and update deps

* changelog

* lock file
2022-07-20 10:41:37 +02:00
c3b1772728 Testing (#44)
* testing

* try playwrigth

* testing

* add pr support

* not on each commit

* add test ids

* make backend more configuratble

* 2.0.2

* spec
2022-07-19 21:55:05 +02:00
786878a3e4 Testing (#41)
* testing

* try playwrigth

* testing

* add pr support

* not on each commit
2022-07-19 14:12:51 +02:00
104 changed files with 2636 additions and 2121 deletions

2
.dev.env Normal file
View File

@@ -0,0 +1,2 @@
SIZE_LIMIT=10MiB
VERBOSITY=debug

View File

@@ -1,15 +1,15 @@
*
!/backend/src
!/backend/Cargo.lock
!/backend/Cargo.toml
!/packages/backend/src
!/packages/backend/Cargo.lock
!/packages/backend/Cargo.toml
!/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
!/packages/frontend/locales
!/packages/frontend/src
!/packages/frontend/static
!/packages/frontend/.npmrc
!/packages/frontend/package.json
!/packages/frontend/pnpm-lock.yaml
!/packages/frontend/svelte.config.js
!/packages/frontend/tsconfig.json
!/packages/frontend/vite.config.js

1
.gitattributes vendored
View File

@@ -1 +1,2 @@
*.afdesign filter=lfs diff=lfs merge=lfs -text
test/assets/** filter=lfs diff=lfs merge=lfs -text

View File

@@ -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 }}

36
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Test
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "16"
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
with:
install: true
- name: Build docker image
run: npm run test:prepare
- name: Prepare
run: |
npm install playwright
npx playwright install --with-deps
- name: Run your tests
run: npm test
- uses: actions/upload-artifact@v2
if: always()
with:
name: test-results
path: test-results

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@ node_modules
/build
/functions
.env
General
test-results

View File

@@ -1,6 +1,6 @@
{
"cSpell.words": ["ciphertext", "cryptgeon"],
"i18n-ally.localesPaths": ["frontend/locales"],
"i18n-ally.localesPaths": ["packages/frontend/locales"],
"i18n-ally.enabledFrameworks": ["svelte"],
"i18n-ally.keystyle": "nested"
}

View File

@@ -5,6 +5,47 @@ 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.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

View File

@@ -2,25 +2,31 @@
FROM node:16-alpine as client
WORKDIR /tmp
RUN npm install -g pnpm@7
COPY ./frontend ./
COPY ./packages/frontend ./
RUN pnpm install
RUN pnpm exec svelte-kit sync
RUN pnpm run build
# BACKEND
FROM rust:1.61-alpine as backend
FROM rust:1.64-alpine as backend
WORKDIR /tmp
RUN apk add libc-dev openssl-dev alpine-sdk
COPY ./backend ./
RUN cargo build --release
COPY ./packages/backend/Cargo.* ./
# https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html
RUN rustup update nightly
ENV CARGO_UNSTABLE_SPARSE_REGISTRY=true
RUN cargo +nightly fetch
COPY ./packages/backend ./
RUN cargo +nightly build --release
# RUNNER
FROM alpine
WORKDIR /app
COPY --from=backend /tmp/target/release/cryptgeon .
COPY --from=client /tmp/build ./frontend/build
ENV REDIS=redis://redis/
COPY --from=client /tmp/build ./frontend
ENV FRONTEND_PATH="./frontend"
ENV REDIS="redis://redis/"
EXPOSE 5000
ENTRYPOINT [ "/app/cryptgeon" ]

View File

@@ -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 https://cryptgeon.nicco.io.
Check out the live service / demo and see for yourself [cryptgeon.org](https://cryptgeon.org)
## Features
@@ -50,15 +50,18 @@ of the notes even if it tried to.
## Environment Variables
| Variable | Default | Description |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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. |
| `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 |
| 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
@@ -76,12 +79,16 @@ 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
@@ -93,39 +100,20 @@ See the [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/exam
### Traefik 2
Assumptions:
See the [examples/traefik](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/traefik) folder.
- External proxy docker network `proxy`
- A certificate resolver `le`
- A https entrypoint `secure`
- Domain name `example.org`
### Scratch
```yaml
version: '3.8'
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.
networks:
proxy:
external: true
### Synology
services:
redis:
image: redis:7-alpine
restart: unless-stopped
There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) you can follow.
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
```
### 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
@@ -165,6 +153,29 @@ Running `pnpm run dev` in the root folder will start the following things:
You can see the app under [localhost:1234](http://localhost:1234).
## Tests
Tests are end to end tests written with Playwright.
```sh
pnpm run test:prepare
docker compose up redis -d
pnpm run test:server
# In another terminal.
# Use the test or test:local script. The local version only runs in one browser for quicker development.
pnpm run test:local
```
## Security
Please refer to the security section [here](./SECURITY.md).
###### Attributions
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>
- 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>

View File

@@ -26,7 +26,7 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
## 演示示例
查看加密鸽的在线演示 demo https://cryptgeon.nicco.io.
查看加密鸽的在线演示 demo [cryptgeon.org](https://cryptgeon.org)
## 功能
@@ -49,11 +49,13 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
| 变量名称 | 默认值 | 描述 |
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
| `REDIS` | `redis://redis/` | Redis URL to connect to. |
| `REDIS` | `redis://redis/` | Redis 连接 URL。 |
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
## 部署
@@ -137,7 +139,7 @@ services:
pnpm install
pnpm --prefix frontend install
# Also you need cargo watch if you don't already have it installed.
# 你还需要安装CargoWatch.
# https://lib.rs/crates/cargo-watch
cargo install cargo-watch
```
@@ -146,7 +148,7 @@ cargo install cargo-watch
确保你的 Docker 正在运行
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
> 如果你用的是 `macOS` 的话你可能需要关闭 AirPlay 接收功能因为该功能需要占用 5000 端口...)
> https://developer.apple.com/forums/thread/682332
```bash
@@ -161,6 +163,25 @@ pnpm run dev
你可以通过 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
本项目所使用的图标由<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 Normal file
View File

@@ -0,0 +1,18 @@
# 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).

View File

@@ -10,10 +10,10 @@ services:
- 6379:6379
app:
image: cupcakearmy/cryptgeon:test
build: .
env_file: .dev.env
depends_on:
- redis
environment:
SIZE_LIMIT: 128 MiB
ports:
- 80:5000
- 1234:5000

18
docker-compose.yaml Normal file
View 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:5000

View File

@@ -0,0 +1,36 @@
# 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
```

View File

@@ -1,5 +0,0 @@
.DS_Store
node_modules
/.svelte
/build
/functions

View File

@@ -1,35 +0,0 @@
{
"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.37",
"@sveltejs/kit": "^1.0.0-next.377",
"@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.1"
},
"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

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,17 @@
{
"scripts": {
"dev:docker": "docker-compose up redis",
"dev:backend": "cd backend && cargo watch -x 'run --bin cryptgeon'",
"dev:front": "pnpm --prefix frontend run dev",
"dev:packages": "pnpm --parallel run dev",
"dev:proxy": "node proxy.mjs",
"dev": "run-p dev:*"
"dev": "run-p dev:*",
"test": "playwright test --project chrome firefox safari",
"test:local": "playwright test --project local",
"test:server": "docker compose -f docker-compose.dev.yaml up",
"test:prepare": "docker compose -f docker-compose.dev.yaml build"
},
"devDependencies": {
"@playwright/test": "^1.25.1",
"@types/node": "^16.11.57",
"http-proxy": "^1.18.1",
"npm-run-all": "^4.1.5"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "cryptgeon"
version = "2.0.1"
version = "2.0.5"
authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021"

View File

@@ -0,0 +1,10 @@
{
"name": "backend",
"private": true,
"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"
}
}

View File

@@ -1,14 +1,17 @@
use actix_files::{Files, NamedFile};
use actix_web::{web, Result};
use crate::config;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(
Files::new("/", "./frontend/build")
Files::new("/", config::FRONTEND_PATH.to_string())
.index_file("index.html")
.use_etag(true),
);
}
pub async fn index() -> Result<NamedFile> {
Ok(NamedFile::open("./frontend/build/index.html")?)
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
Ok(NamedFile::open(index)?)
}

View File

@@ -1,10 +1,15 @@
use byte_unit::Byte;
// General
// Internal
lazy_static! {
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
.unwrap_or("Unknown")
.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:5000".to_string());
pub static ref VERBOSITY: String = std::env::var("VERBOSITY").unwrap_or("warn".to_string());
}
// CONFIG
@@ -37,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();
}

View File

@@ -18,10 +18,11 @@ mod store;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("warning"));
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
return HttpServer::new(|| {
App::new()
.wrap(Logger::new("%a \"%r\" %s %b %T"))
.wrap(Logger::new("\"%r\" %s %b %T"))
.wrap(middleware::Compress::default())
.wrap(middleware::DefaultHeaders::default())
.configure(size::init)
@@ -29,7 +30,7 @@ async fn main() -> std::io::Result<()> {
.configure(client::init)
.default_service(web::to(client::index))
})
.bind("0.0.0.0:5000")?
.bind(config::LISTEN_ADDR.to_string())?
.run()
.await;
}

View File

@@ -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

View File

@@ -7,5 +7,6 @@ pub fn init(cfg: &mut web::ServiceConfig) {
let plain = web::PayloadConfig::default()
.limit(*config::LIMIT)
.mimetype(mime::STAR_STAR);
// cfg.app_data(plain);
cfg.app_data(json).app_data(plain);
}

View File

@@ -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,
}

View File

@@ -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()
});
}

View File

@@ -1,8 +1,8 @@
├─ MIT: 12
├─ MIT: 13
├─ ISC: 2
├─ BSD-3-Clause: 1
├─ (MPL-2.0 OR Apache-2.0): 1
├─ BSD-2-Clause: 1
├─ ISC: 1
├─ 0BSD: 1
└─ Apache-2.0: 1
1 ├─ MIT: 12 ├─ MIT: 13
2 ├─ ISC: 2
3 ├─ BSD-3-Clause: 1 ├─ BSD-3-Clause: 1
4 ├─ (MPL-2.0 OR Apache-2.0): 1 ├─ (MPL-2.0 OR Apache-2.0): 1
5 ├─ BSD-2-Clause: 1 ├─ BSD-2-Clause: 1
├─ ISC: 1
6 ├─ 0BSD: 1 ├─ 0BSD: 1
7 └─ Apache-2.0: 1 └─ Apache-2.0: 1
8

View File

@@ -11,6 +11,7 @@
"max": "max",
"share_link": "Link teilen",
"copy_clipboard": "in die Zwischenablage kopieren",
"copied_to_clipboard": "in die Zwischenablage kopiert",
"encrypting": "verschlüsseln",
"decrypting": "entschlüsselt",
"uploading": "hochladen",
@@ -27,7 +28,9 @@
"max": "max: {n}",
"empty_content": "Notiz ist leer."
},
"copied_to_clipboard": "in die Zwischenablage kopiert 🔗"
"messages": {
"note_created": "notiz erstellt."
}
},
"show": {
"errors": {

View File

@@ -11,6 +11,7 @@
"max": "max",
"share_link": "share link",
"copy_clipboard": "copy to clipboard",
"copied_to_clipboard": "copied to clipboard",
"encrypting": "encrypting",
"decrypting": "decrypting",
"uploading": "uploading",
@@ -27,7 +28,9 @@
"max": "max: {n}",
"empty_content": "note is empty."
},
"copied_to_clipboard": "copied to clipboard 🔗"
"messages": {
"note_created": "note created."
}
},
"show": {
"errors": {

View File

@@ -11,6 +11,7 @@
"max": "max",
"share_link": "compartir enlace",
"copy_clipboard": "copiar al portapapeles",
"copied_to_clipboard": "copiado al portapapeles",
"encrypting": "encriptando",
"decrypting": "descifrando",
"uploading": "cargando",
@@ -27,7 +28,9 @@
"max": "max: {n}",
"empty_content": "la nota está vacía."
},
"copied_to_clipboard": "copiado al portapapeles 🔗"
"messages": {
"note_created": "nota creada."
}
},
"show": {
"errors": {

View File

@@ -11,6 +11,7 @@
"max": "max",
"share_link": "partager le lien",
"copy_clipboard": "copier dans le presse-papiers",
"copied_to_clipboard": "copié dans le presse-papiers",
"encrypting": "cryptage",
"decrypting": "déchiffrer",
"uploading": "téléchargement",
@@ -27,7 +28,9 @@
"max": "max: {n}",
"empty_content": "La note est vide."
},
"copied_to_clipboard": "copié dans le presse-papiers 🔗"
"messages": {
"note_created": "note créée."
}
},
"show": {
"errors": {

View File

@@ -11,6 +11,7 @@
"max": "max",
"share_link": "condividi link",
"copy_clipboard": "copia negli appunti",
"copied_to_clipboard": "copiato negli appunti",
"encrypting": "criptando",
"decrypting": "decifrando",
"uploading": "caricamento",
@@ -27,7 +28,9 @@
"max": "max: {n}",
"empty_content": "la nota è vuota."
},
"copied_to_clipboard": "copiato negli appunti 🔗"
"messages": {
"note_created": "nota creata."
}
},
"show": {
"errors": {

View File

@@ -11,6 +11,7 @@
"max": "最大值",
"share_link": "分享链接",
"copy_clipboard": "复制到剪切版",
"copied_to_clipboard": "已复制到剪切板",
"encrypting": "加密",
"decrypting": "解密",
"uploading": "上传",
@@ -27,7 +28,9 @@
"max": "最大文件大小: {n}",
"empty_content": "密信为空!"
},
"copied_to_clipboard": "已复制到剪切板 🔗"
"messages": {
"note_created": "注释创建。"
}
},
"show": {
"errors": {

View File

@@ -0,0 +1,37 @@
{
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview --port 3000",
"check": "svelte-check --tsconfig tsconfig.json",
"licenses": "license-checker --summary > licenses.csv",
"locale:download": "node scripts/locale.js",
"test:prepare": "pnpm run build"
},
"type": "module",
"devDependencies": {
"@lokalise/node-api": "^7.3.1",
"@sveltejs/adapter-static": "^1.0.0-next.48",
"@sveltejs/kit": "^1.0.0-next.544",
"@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.3",
"svelte": "^3.53.1",
"svelte-check": "^2.9.2",
"svelte-intl-precompile": "^0.10.1",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.1",
"typescript": "^4.8.4",
"vite": "^3.2.3"
},
"dependencies": {
"@fontsource/fira-mono": "^4.5.10",
"copy-to-clipboard": "^3.3.2",
"dompurify": "^2.4.1",
"file-saver": "^2.0.5",
"pretty-bytes": "^6.0.0"
}
}

View File

@@ -14,7 +14,9 @@
--ui-text-0: #fefefe;
--ui-text-1: #eee;
--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-alt: hsl(357, 87%, 41%);
--ui-anim: all 150ms ease;
}
@@ -85,6 +87,8 @@ button {
font-size: inherit;
background: inherit;
color: inherit;
border: none;
padding-inline: initial;
}
*:disabled,

View File

@@ -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%

View File

@@ -35,6 +35,31 @@ export class ArrayBufferUtils {
}
}
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 = ':::'
@@ -43,55 +68,22 @@ export class Crypto {
return window.crypto.getRandomValues(new Uint8Array(size))
}
public static getKeyFromString(password: string) {
return window.crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
)
}
public static async getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
const iterations = 100_000
return window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations,
hash: 'SHA-512',
},
key,
{ name: this.ALG, length: 256 },
true,
['encrypt', 'decrypt']
)
}
public static async encrypt(plaintext: ArrayBuffer, key: CryptoKey): Promise<string> {
const salt = this.getRandomBytes(16)
const derived = await this.getDerivedForKey(key, salt)
const iv = this.getRandomBytes(16)
const iv = this.getRandomBytes(12) // AES-GCM needs a 96bit IV
const encrypted: ArrayBuffer = await window.crypto.subtle.encrypt(
{ name: this.ALG, iv },
derived,
key,
plaintext
)
const data = [
Hex.encode(salt),
Hex.encode(iv),
await ArrayBufferUtils.toString(encrypted),
].join(this.DELIMITER)
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 salt = Hex.decode(splitted[0])
const iv = Hex.decode(splitted[1])
const encrypted = await ArrayBufferUtils.fromString(splitted[2])
const derived = await this.getDerivedForKey(key, salt)
const plaintext = await window.crypto.subtle.decrypt({ name: this.ALG, iv }, derived, encrypted)
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
}
}

View File

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 287 B

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

Before

Width:  |  Height:  |  Size: 736 B

After

Width:  |  Height:  |  Size: 736 B

View File

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 483 B

View File

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 732 B

View File

@@ -9,6 +9,8 @@ export type Status = {
allow_advanced: boolean
theme_image: string
theme_text: string
theme_favicon: string
theme_page_title: string
}
export const status = writable<null | Status>(null)

View File

@@ -0,0 +1,37 @@
import { toast, type SvelteToastOptions } from '@zerodevx/svelte-toast'
export enum NotifyType {
Success = 'success',
Error = 'error',
}
const themeMapping: Record<NotifyType, SvelteToastOptions['theme']> = {
[NotifyType.Success]: {
'--toastBackground': 'var(--ui-clr-primary)',
'--toastBarBackground': 'var(--ui-clr-primary-alt)',
},
[NotifyType.Error]: {
'--toastBackground': 'var(--ui-clr-error)',
'--toastBarBackground': 'var(--ui-clr-error-alt)',
},
}
function notifyFN(message: string, type: NotifyType = NotifyType.Success) {
const options: SvelteToastOptions = {
duration: 5_000,
theme: {
...themeMapping[type],
'--toastBarHeight': '0.25rem',
'--toastMinHeight': 'auto',
'--toastMsgPadding': '0.5rem',
'--toastBorderRadius': '0',
},
}
toast.push(message, options)
}
export const notify = {
success: (message: string) => notifyFN(message, NotifyType.Success),
error: (message: string) => notifyFN(message, NotifyType.Error),
}

View File

@@ -12,19 +12,27 @@
<div class="fields">
<TextInput
data-testid="field-views"
type="number"
label={$t('common.views', { values: { n: 0 } })}
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">
<Switch label={$t('common.mode')} bind:value={timeExpiration} color={false} />
<Switch
data-testid="switch-advanced-toggle"
label={$t('common.mode')}
bind:value={timeExpiration}
color={false}
/>
</div>
<TextInput
data-testid="field-expiration"
type="number"
label={$t('common.minutes', { values: { n: 0 } })}
bind:value={note.expiration}

View File

@@ -34,7 +34,7 @@
<small>
{label}
</small>
<input type="file" on:change={onInput} multiple />
<input {...$$restProps} type="file" on:change={onInput} multiple />
<div class="box">
{#if files.length}
<div>

View File

Before

Width:  |  Height:  |  Size: 784 B

After

Width:  |  Height:  |  Size: 784 B

View File

@@ -24,6 +24,7 @@
label={$t('common.share_link')}
value="{window.location.origin}/note/{result.id}#{result.password}"
copy
data-testid="share-link"
/>
<br />
<p>

View File

@@ -3,7 +3,6 @@
</script>
<script lang="ts">
import copy from 'copy-to-clipboard'
import DOMPurify from 'dompurify'
import { saveAs } from 'file-saver'
import prettyBytes from 'pretty-bytes'
@@ -11,6 +10,7 @@
import type { FileDTO, NotePublic } from '$lib/api'
import Button from '$lib/ui/Button.svelte'
import { copy } from '$lib/utils'
export let note: DecryptedNote
@@ -44,20 +44,24 @@
</script>
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
{#if note.meta.type === 'text'}
<div class="note">
{@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 data-testid="result">
{#if note.meta.type === 'text'}
<div class="note">
{@html contentWithLinks(note.contents)}
</div>
{/each}
<Button on:click={download}>{$t('show.download_all')}</Button>
{/if}
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
{:else}
{#each files as file}
<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>
.note {

View File

@@ -1,10 +1,7 @@
<script lang="ts">
import copyToClipboard from 'copy-to-clipboard'
import { t } from 'svelte-intl-precompile'
import { fade } from 'svelte/transition'
import { Crypto, Hex } from '$lib/crypto'
import Icon from '$lib/ui/Icon.svelte'
import { copy as copyFN } from '$lib/utils'
export let label: string = ''
export let value: any
@@ -15,8 +12,6 @@
const initialType = $$restProps.type
const isPassword = initialType === 'password'
let hidden = true
let notification: string | null = null
let notificationTimeout: NodeJS.Timeout | null = null
$: valid = validate(value)
@@ -28,22 +23,8 @@
function toggle() {
hidden = !hidden
}
function copyFN() {
copyToClipboard(value.toString())
notify($t('home.copied_to_clipboard'))
}
function randomFN() {
value = Hex.encode(Crypto.getRandomBytes(20))
}
function notify(msg: string, delay: number = 2000) {
if (notificationTimeout) {
clearTimeout(notificationTimeout)
}
notificationTimeout = setTimeout(() => {
notification = null
}, delay)
notification = msg
value = Hex.encode(Crypto.getRandomBytes(32))
}
</script>
@@ -63,12 +44,9 @@
<Icon class="icon" icon="dice" on:click={randomFN} />
{/if}
{#if copy}
<Icon class="icon" icon="copy" on:click={copyFN} />
<Icon class="icon" icon="copy" on:click={() => copyFN(value.toString())} />
{/if}
</div>
{#if notification}
<div class="notification" transition:fade><small>{notification}</small></div>
{/if}
</label>
<style>
@@ -118,11 +96,4 @@
.icons > :global(.icon:hover) {
border-color: var(--ui-clr-primary);
}
.notification {
text-align: right;
position: absolute;
right: 0;
bottom: -1.5em;
}
</style>

View File

@@ -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;

View File

@@ -0,0 +1,11 @@
import copyToClipboard from 'copy-to-clipboard'
import { t } from 'svelte-intl-precompile'
import { get } from 'svelte/store'
import { notify } from './toast'
export function copy(value: string) {
copyToClipboard(value)
const msg = get(t)('common.copied_to_clipboard')
notify.success(msg)
}

View File

@@ -5,8 +5,9 @@
import { Adapters } from '$lib/adapters'
import type { FileDTO, Note } from '$lib/api'
import { create, PayloadToLargeError } from '$lib/api'
import { Crypto, Hex } from '$lib/crypto'
import { Keys } from '$lib/crypto'
import { status } from '$lib/stores/status'
import { notify } from '$lib/toast'
import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte'
import Button from '$lib/ui/Button.svelte'
import FileUpload from '$lib/ui/FileUpload.svelte'
@@ -29,7 +30,6 @@
let timeExpiration = false
let description = ''
let loading: string | null = null
let error: string | null = null
$: if (!advanced) {
note.views = 1
@@ -56,11 +56,10 @@
async function submit() {
try {
error = null
loading = $t('common.encrypting')
const password = Hex.encode(Crypto.getRandomBytes(32))
const key = await Crypto.getKeyFromString(password)
const key = await Keys.generateKey()
const password = await Keys.export(key)
const data: Note = {
contents: '',
@@ -82,14 +81,15 @@
password: password,
id: response.id,
}
notify.success($t('home.messages.note_created'))
} catch (e) {
if (e instanceof PayloadToLargeError) {
error = $t('home.errors.note_to_big')
notify.error($t('home.errors.note_to_big'))
} else if (e instanceof EmptyContentError) {
error = $t('home.errors.empty_content')
notify.error($t('home.errors.empty_content'))
} else {
console.error(e)
error = $t('home.errors.note_error')
notify.error($t('home.errors.note_error'))
}
} finally {
loading = null
@@ -106,15 +106,29 @@
<form on:submit|preventDefault={submit}>
<fieldset disabled={loading !== null}>
{#if isFile}
<FileUpload label={$t('common.file')} bind:files />
<FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
{:else}
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
<TextArea
data-testid="text-field"
label={$t('common.note')}
bind:value={note.contents}
placeholder="..."
/>
{/if}
<div class="bottom">
<Switch class="file" label={$t('common.file')} bind:value={isFile} />
<Switch
data-testid="switch-file"
class="file"
label={$t('common.file')}
bind:value={isFile}
/>
{#if $status?.allow_advanced}
<Switch label={$t('common.advanced')} bind:value={advanced} />
<Switch
data-testid="switch-advanced"
label={$t('common.advanced')}
bind:value={advanced}
/>
{/if}
<div class="grow" />
<div class="tr">
@@ -124,10 +138,6 @@
</div>
</div>
{#if error}
<div class="error-text">{error}</div>
{/if}
<p>
<br />
{#if loading}
@@ -161,8 +171,4 @@
.grow {
flex: 1;
}
.error-text {
margin-top: 0.5rem;
}
</style>

View File

@@ -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>

View File

@@ -1,17 +1,13 @@
<script lang="ts" context="module">
import { getLocaleFromNavigator, init, waitLocale } from 'svelte-intl-precompile'
// @ts-ignore
import { registerAll } from '$locales'
registerAll()
init({ initialLocale: getLocaleFromNavigator() ?? undefined, fallbackLocale: 'en' })
</script>
<script lang="ts">
import { init as initStores } from '$lib/stores/status'
import { SvelteToast } from '@zerodevx/svelte-toast'
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 Header from '$lib/views/Header.svelte'
import { onMount } from 'svelte'
import '../app.css'
onMount(() => {
initStores()
@@ -19,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 _}
@@ -28,6 +25,8 @@
<slot />
</main>
<SvelteToast />
<Footer />
{/await}

View File

@@ -0,0 +1,5 @@
import { getLocaleFromNavigator, init } from 'svelte-intl-precompile'
// @ts-ignore
import { registerAll } from '$locales'
registerAll()
init({ initialLocale: getLocaleFromNavigator() ?? undefined, fallbackLocale: 'en' })

View File

@@ -1,10 +1,6 @@
<script context="module">
import { browser, dev } from '$app/env'
<script lang="ts">
import { status } from '$lib/stores/status'
import AboutParagraph from '$lib/ui/AboutParagraph.svelte'
export const hydrate = dev
export const router = browser
</script>
<svelte:head>
@@ -43,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>
@@ -51,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>

View File

@@ -1,26 +1,18 @@
<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 { Adapters } from '$lib/adapters'
import { get, info } from '$lib/api'
import { Crypto } from '$lib/crypto'
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 type { PageData } from './$types'
export let id: string
export let data: PageData
let id = data.id
let password: string
let note: DecryptedNote | null = null
let exists = false
@@ -51,7 +43,7 @@
loading = $t('common.downloading')
const data = await get(id)
loading = $t('common.decrypting')
const key = await Crypto.getKeyFromString(password)
const key = await Keys.import(password)
switch (data.meta.type) {
case 'text':
note = {
@@ -86,7 +78,7 @@
<form on:submit|preventDefault={show}>
<fieldset>
<p>{$t('show.explanation')}</p>
<Button type="submit">{$t('show.show_note')}</Button>
<Button data-testid="show-note-button" type="submit">{$t('show.show_note')}</Button>
{#if error}
<br />
<p class="error-text">

View File

@@ -0,0 +1,5 @@
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ params }) => {
return params
}

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -3,6 +3,7 @@ import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
/** @type {import('vite').UserConfig} */
const config = {
clearScreen: false,
server: {
port: 3000,
},

33
playwright.config.ts Normal file
View File

@@ -0,0 +1,33 @@
import { devices, type PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
use: {
video: 'retain-on-failure',
baseURL: 'http://localhost:1234',
actionTimeout: 60_000,
},
outputDir: './test-results',
testDir: './test',
timeout: 60_000,
testIgnore: ['file/too-big.spec.ts'],
webServer: {
command: 'docker compose -f docker-compose.dev.yaml up',
port: 1234,
reuseExistingServer: true,
},
projects: [
{ name: 'chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'safari', use: { ...devices['Desktop Safari'] } },
{
name: 'local',
use: { ...devices['Desktop Chrome'] },
// testMatch: 'file/too-big.spec.ts',
},
],
}
export default config

1631
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
packages:
- "packages/**"

BIN
test/assets/AES.pdf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
test/assets/Pigeons.zip (Stored with Git LFS) Normal file

Binary file not shown.

BIN
test/assets/image.jpg (Stored with Git LFS) Normal file

Binary file not shown.

5
test/file/files.ts Normal file
View File

@@ -0,0 +1,5 @@
export default {
PDF: 'test/assets/AES.pdf',
Image: 'test/assets/image.jpg',
Zip: 'test/assets/Pigeons.zip',
}

View File

@@ -0,0 +1,11 @@
import { test } from '@playwright/test'
import { checkLinkForDownload, createNote, getFileChecksum } from '../utils'
import Files from './files'
test('multiple', async ({ page }) => {
const files = [Files.PDF, Files.Image]
const checksums = await Promise.all(files.map(getFileChecksum))
const link = await createNote(page, { files, views: 2 })
await checkLinkForDownload(page, link, 'image.jpg', checksums[1])
await checkLinkForDownload(page, link, 'AES.pdf', checksums[0])
})

24
test/file/simple.spec.ts Normal file
View File

@@ -0,0 +1,24 @@
import { test } from '@playwright/test'
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote, getFileChecksum } from '../utils'
import Files from './files'
test('simple pdf', async ({ page }) => {
const files = [Files.PDF]
const link = await createNote(page, { files })
await checkLinkForText(page, link, 'AES.pdf')
await checkLinkDoesNotExist(page, link)
})
test('pdf content', async ({ page }) => {
const files = [Files.PDF]
const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files })
await checkLinkForDownload(page, link, 'AES.pdf', checksum)
})
test('image content', async ({ page }) => {
const files = [Files.Image]
const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files })
await checkLinkForDownload(page, link, 'image.jpg', checksum)
})

View File

@@ -0,0 +1,8 @@
import { test } from '@playwright/test'
import { createNote } from '../utils'
import Files from './files'
test('to big zip', async ({ page }) => {
const files = [Files.Zip]
const link = await createNote(page, { files, error: 'note is to big' })
})

37
test/text.ts Normal file
View File

@@ -0,0 +1,37 @@
import { expect, test, type Page } from '@playwright/test'
async function createNote(page: Page, text: string): Promise<string> {
await page.goto('/')
await page.locator('textarea').click()
await page.locator('textarea').fill(text)
await page.locator('button:has-text("create")').click()
await page.locator('[data-testid="share-link"]').click()
const shareLink = await page.locator('[data-testid="share-link"]').inputValue()
return shareLink
}
async function checkLinkForText(page: Page, link: string, text: string) {
await page.goto(link)
await page.locator('[data-testid="show-note-button"]').click()
expect(await page.locator('[data-testid="result"] >> .note').innerText()).toBe(text)
}
async function checkLinkDoesNotExist(page: Page, link: string) {
await page.goto('/') // Required due to firefox: https://github.com/microsoft/playwright/issues/15781
await page.goto(link)
await expect(page.locator('main')).toContainText('note was not found or was already deleted')
}
test('simple', async ({ page }) => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const shareLink = await createNote(page, text)
await checkLinkForText(page, shareLink, text)
})
test('only shown once', async ({ page }) => {
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
const shareLink = await createNote(page, text)
await checkLinkForText(page, shareLink, text)
await checkLinkDoesNotExist(page, shareLink)
})

Some files were not shown because too many files have changed in this diff Show More