mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2025-09-04 00:20:39 +00:00
Compare commits
7 Commits
v2.0.2
...
v2.0.0-rc.
Author | SHA1 | Date | |
---|---|---|---|
0829d78481 | |||
a4ffde78e4 | |||
0e47dab6d4 | |||
8d5e348f56 | |||
d852edc3c8 | |||
2bb256b07e | |||
ff36d375ea |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,2 +1 @@
|
||||
*.afdesign filter=lfs diff=lfs merge=lfs -text
|
||||
test/assets/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
36
.github/workflows/test.yaml
vendored
36
.github/workflows/test.yaml
vendored
@@ -1,36 +0,0 @@
|
||||
on:
|
||||
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
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.61
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run ci:prepare
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run your tests
|
||||
run: pnpm run test
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,6 +9,3 @@ node_modules
|
||||
/build
|
||||
/functions
|
||||
.env
|
||||
|
||||
General
|
||||
test-results
|
||||
|
38
CHANGELOG.md
38
CHANGELOG.md
@@ -5,48 +5,16 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.0.2] - 2022-07-20
|
||||
|
||||
### Added
|
||||
|
||||
- Toasts for events.
|
||||
- E2E Tests.
|
||||
- Make backend more configurable
|
||||
|
||||
## [2.0.1] - 2022-07-18
|
||||
|
||||
### Added
|
||||
|
||||
- Max file size on the client now.
|
||||
- Loading information.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed encoding from hex to base64.
|
||||
- Chinese language code.
|
||||
- Notable speed improvements for big files.
|
||||
|
||||
## [2.0.0] - 2022-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- Theming for logo and description text.
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved to redis.
|
||||
- New html sanitizing library.
|
||||
|
||||
## [2.0.0-rc.0] - 2022-07-15
|
||||
|
||||
### Added
|
||||
|
||||
- Theming for logo and description text.
|
||||
- Theming for logo and description text
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved to redis.
|
||||
- New html sanitizing library.
|
||||
- Moved to redis
|
||||
- New html sanitizing library
|
||||
|
||||
## [1.5.3] - 2022-06-07
|
||||
|
||||
|
@@ -4,7 +4,6 @@ WORKDIR /tmp
|
||||
RUN npm install -g pnpm@7
|
||||
COPY ./frontend ./
|
||||
RUN pnpm install
|
||||
RUN pnpm exec svelte-kit sync
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
@@ -12,8 +11,6 @@ RUN pnpm run build
|
||||
FROM rust:1.61-alpine as backend
|
||||
WORKDIR /tmp
|
||||
RUN apk add libc-dev openssl-dev alpine-sdk
|
||||
COPY ./backend/Cargo.* ./
|
||||
RUN cargo fetch
|
||||
COPY ./backend ./
|
||||
RUN cargo build --release
|
||||
|
||||
@@ -22,8 +19,7 @@ RUN cargo build --release
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||
COPY --from=client /tmp/build ./frontend
|
||||
ENV FRONTEND_PATH="./frontend"
|
||||
ENV REDIS="redis://redis/"
|
||||
COPY --from=client /tmp/build ./frontend/build
|
||||
ENV REDIS=redis://redis/
|
||||
EXPOSE 5000
|
||||
ENTRYPOINT [ "/app/cryptgeon" ]
|
||||
|
41
README.md
41
README.md
@@ -50,15 +50,15 @@ 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. |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). `512 MiB` is the maximum allowed |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_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 |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -82,7 +82,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
SIZE_LIMIT: 4 MiB
|
||||
SIZE_LIMIT: 4M
|
||||
ports:
|
||||
- 80:5000
|
||||
```
|
||||
@@ -165,25 +165,6 @@ 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 ci:prepare
|
||||
docker compose up redis -d
|
||||
pnpm run ci: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
|
||||
```
|
||||
|
||||
###### Attributions
|
||||
|
||||
- Test data:
|
||||
- Text for tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
|
||||
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
|
||||
- [Unsplash Pictures](https://unsplash.com/)
|
||||
- Loading animation by [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa)
|
||||
- Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
|
||||
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>
|
||||
|
@@ -78,7 +78,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
SIZE_LIMIT: 4 MiB
|
||||
SIZE_LIMIT: 4M
|
||||
ports:
|
||||
- 80:5000
|
||||
```
|
||||
|
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -424,7 +424,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptgeon"
|
||||
version = "2.0.2"
|
||||
version = "2.0.0-rc.0"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-web",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cryptgeon"
|
||||
version = "2.0.2"
|
||||
version = "2.0.0-rc.0"
|
||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||
edition = "2021"
|
||||
|
||||
|
@@ -1,17 +1,14 @@
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_web::{web, Result};
|
||||
|
||||
use crate::config;
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
Files::new("/", config::FRONTEND_PATH.to_string())
|
||||
Files::new("/", "./frontend/build")
|
||||
.index_file("index.html")
|
||||
.use_etag(true),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn index() -> Result<NamedFile> {
|
||||
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
|
||||
Ok(NamedFile::open(index)?)
|
||||
Ok(NamedFile::open("./frontend/build/index.html")?)
|
||||
}
|
||||
|
@@ -1,22 +1,18 @@
|
||||
use byte_unit::Byte;
|
||||
|
||||
// Internal
|
||||
// General
|
||||
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());
|
||||
}
|
||||
|
||||
// CONFIG
|
||||
lazy_static! {
|
||||
pub static ref LIMIT: usize =
|
||||
pub static ref LIMIT: u32 =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as usize;
|
||||
.get_bytes() as u32;
|
||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
|
@@ -29,7 +29,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.configure(client::init)
|
||||
.default_service(web::to(client::index))
|
||||
})
|
||||
.bind(config::LISTEN_ADDR.to_string())?
|
||||
.bind("0.0.0.0:5000")?
|
||||
.run()
|
||||
.await;
|
||||
}
|
||||
|
@@ -1,11 +1,18 @@
|
||||
use crate::config;
|
||||
use actix_web::web;
|
||||
use byte_unit::Byte;
|
||||
use mime;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref LIMIT: usize =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as usize;
|
||||
}
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
let json = web::JsonConfig::default().limit(*config::LIMIT);
|
||||
let json = web::JsonConfig::default().limit(*LIMIT);
|
||||
let plain = web::PayloadConfig::default()
|
||||
.limit(*config::LIMIT)
|
||||
.limit(*LIMIT)
|
||||
.mimetype(mime::STAR_STAR);
|
||||
cfg.app_data(json).app_data(plain);
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ use crate::status::Status;
|
||||
async fn get_status() -> impl Responder {
|
||||
return HttpResponse::Ok().json(Status {
|
||||
version: config::VERSION.to_string(),
|
||||
max_size: *config::LIMIT as u32,
|
||||
max_size: *config::LIMIT,
|
||||
max_views: *config::MAX_VIEWS,
|
||||
max_expiration: *config::MAX_EXPIRATION,
|
||||
allow_advanced: *config::ALLOW_ADVANCED,
|
||||
|
@@ -14,6 +14,6 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
SIZE_LIMIT: 128 MiB
|
||||
SIZE_LIMIT: 128M
|
||||
ports:
|
||||
- 1234:5000
|
||||
- 80:5000
|
||||
|
@@ -4,18 +4,13 @@
|
||||
"file": "上传文件",
|
||||
"advanced": "高级设置",
|
||||
"create": "创建",
|
||||
"loading": "加载中",
|
||||
"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": "下载"
|
||||
"copy_clipboard": "复制到剪切版"
|
||||
},
|
||||
"home": {
|
||||
"intro": "一键轻松发送 <i>完全加密的</i> 密信或者文件。只需创建一个密信然后分享链接。",
|
||||
@@ -28,15 +23,12 @@
|
||||
"max": "最大文件大小: {n}",
|
||||
"empty_content": "密信为空!"
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "注释创建。"
|
||||
}
|
||||
"copied_to_clipboard": "已复制到剪切板 🔗"
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "该密信无法被找到或者它已经被删除了!",
|
||||
"decryption_failed": "密钥错误!您可能不小心粘贴了一个不完整的链接或者正在尝试破解该密信!但无论如何,该密信已被销毁!",
|
||||
"unsupported_type": "不支持的票据类型。"
|
||||
"decryption_failed": "密钥错误!您可能不小心粘贴了一个不完整的链接或者正在尝试破解该密信!但无论如何,该密信已被销毁!"
|
||||
},
|
||||
"explanation": "点击下方的按钮可以查看密信,如果它到达了限制将会被删除",
|
||||
"show_note": "查看密信",
|
@@ -4,18 +4,13 @@
|
||||
"file": "Datei",
|
||||
"advanced": "erweitert",
|
||||
"create": "erstellen",
|
||||
"loading": "läd",
|
||||
"loading": "Läd...",
|
||||
"mode": "Modus",
|
||||
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
|
||||
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
|
||||
"max": "max",
|
||||
"share_link": "Link teilen",
|
||||
"copy_clipboard": "in die Zwischenablage kopieren",
|
||||
"copied_to_clipboard": "in die Zwischenablage kopiert",
|
||||
"encrypting": "verschlüsseln",
|
||||
"decrypting": "entschlüsselt",
|
||||
"uploading": "hochladen",
|
||||
"downloading": "wird heruntergeladen"
|
||||
"copy_clipboard": "in die Zwischenablage kopieren"
|
||||
},
|
||||
"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.",
|
||||
@@ -28,15 +23,12 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "Notiz ist leer."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "notiz erstellt."
|
||||
}
|
||||
"copied_to_clipboard": "in die Zwischenablage kopiert 🔗"
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
|
||||
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört.",
|
||||
"unsupported_type": "nicht unterstützter Notiztyp."
|
||||
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört."
|
||||
},
|
||||
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
|
||||
"show_note": "Notiz anzeigen",
|
||||
|
@@ -4,18 +4,13 @@
|
||||
"file": "file",
|
||||
"advanced": "advanced",
|
||||
"create": "create",
|
||||
"loading": "loading",
|
||||
"loading": "loading...",
|
||||
"mode": "mode",
|
||||
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
|
||||
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
||||
"max": "max",
|
||||
"share_link": "share link",
|
||||
"copy_clipboard": "copy to clipboard",
|
||||
"copied_to_clipboard": "copied to clipboard",
|
||||
"encrypting": "encrypting",
|
||||
"decrypting": "decrypting",
|
||||
"uploading": "uploading",
|
||||
"downloading": "downloading"
|
||||
"copy_clipboard": "copy to clipboard"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
||||
@@ -28,15 +23,12 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "note is empty."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "note created."
|
||||
}
|
||||
"copied_to_clipboard": "copied to clipboard 🔗"
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "note was not found or was already deleted.",
|
||||
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
|
||||
"unsupported_type": "unsupported note type."
|
||||
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed."
|
||||
},
|
||||
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
||||
"show_note": "show note",
|
||||
|
@@ -4,18 +4,13 @@
|
||||
"file": "archivo",
|
||||
"advanced": "avanzado",
|
||||
"create": "crear",
|
||||
"loading": "cargando",
|
||||
"loading": "cargando...",
|
||||
"mode": "modo",
|
||||
"views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}",
|
||||
"minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}",
|
||||
"max": "max",
|
||||
"share_link": "compartir enlace",
|
||||
"copy_clipboard": "copiar al portapapeles",
|
||||
"copied_to_clipboard": "copiado al portapapeles",
|
||||
"encrypting": "encriptando",
|
||||
"decrypting": "descifrando",
|
||||
"uploading": "cargando",
|
||||
"downloading": "descargando"
|
||||
"copy_clipboard": "copiar al portapapeles"
|
||||
},
|
||||
"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.",
|
||||
@@ -28,15 +23,12 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "la nota está vacía."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "nota creada."
|
||||
}
|
||||
"copied_to_clipboard": "copiado al portapapeles 🔗"
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "la nota no se encontró o ya fue borrada.",
|
||||
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida.",
|
||||
"unsupported_type": "tipo de nota no compatible."
|
||||
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida."
|
||||
},
|
||||
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
|
||||
"show_note": "mostrar nota",
|
||||
|
@@ -4,18 +4,13 @@
|
||||
"file": "fichier",
|
||||
"advanced": "avancé",
|
||||
"create": "créer",
|
||||
"loading": "chargement",
|
||||
"loading": "chargement...",
|
||||
"mode": "mode",
|
||||
"views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}",
|
||||
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
||||
"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",
|
||||
"downloading": "téléchargement"
|
||||
"copy_clipboard": "copier dans le presse-papiers"
|
||||
},
|
||||
"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.",
|
||||
@@ -28,15 +23,12 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "La note est vide."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "note créée."
|
||||
}
|
||||
"copied_to_clipboard": "copié dans le presse-papiers 🔗"
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "La note n'a pas été trouvée ou a déjà été supprimée.",
|
||||
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite.",
|
||||
"unsupported_type": "type de note non supporté."
|
||||
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite."
|
||||
},
|
||||
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
||||
"show_note": "note de présentation",
|
||||
|
@@ -4,18 +4,13 @@
|
||||
"file": "file",
|
||||
"advanced": "avanzato",
|
||||
"create": "crea",
|
||||
"loading": "carica",
|
||||
"loading": "carica...",
|
||||
"mode": "modalita",
|
||||
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
|
||||
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
|
||||
"max": "max",
|
||||
"share_link": "condividi link",
|
||||
"copy_clipboard": "copia negli appunti",
|
||||
"copied_to_clipboard": "copiato negli appunti",
|
||||
"encrypting": "criptando",
|
||||
"decrypting": "decifrando",
|
||||
"uploading": "caricamento",
|
||||
"downloading": "scaricando"
|
||||
"copy_clipboard": "copia negli appunti"
|
||||
},
|
||||
"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.",
|
||||
@@ -28,15 +23,12 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "la nota è vuota."
|
||||
},
|
||||
"messages": {
|
||||
"note_created": "nota creata."
|
||||
}
|
||||
"copied_to_clipboard": "copiato negli appunti 🔗"
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "non è stata trovata o è stata già cancellata.",
|
||||
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta.",
|
||||
"unsupported_type": "tipo di nota non supportato."
|
||||
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta."
|
||||
},
|
||||
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
|
||||
"show_note": "mostra la nota",
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 3000",
|
||||
"preview": "svelte-kit preview",
|
||||
"check": "svelte-check --tsconfig tsconfig.json",
|
||||
"licenses": "license-checker --summary > licenses.csv",
|
||||
"locale:download": "node scripts/locale.js"
|
||||
@@ -11,11 +11,10 @@
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@lokalise/node-api": "^7.3.1",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.38",
|
||||
"@sveltejs/kit": "^1.0.0-next.384",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.34",
|
||||
"@sveltejs/kit": "^1.0.0-next.361",
|
||||
"@types/dompurify": "^2.3.3",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@zerodevx/svelte-toast": "^0.7.2",
|
||||
"adm-zip": "^0.5.9",
|
||||
"dotenv": "^16.0.1",
|
||||
"svelte": "^3.49.0",
|
||||
@@ -24,12 +23,12 @@
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.2"
|
||||
"vite": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.8",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"dompurify": "^2.3.10",
|
||||
"dompurify": "^2.3.9",
|
||||
"file-saver": "^2.0.5",
|
||||
"pretty-bytes": "^5.6.0"
|
||||
}
|
||||
|
222
frontend/pnpm-lock.yaml
generated
222
frontend/pnpm-lock.yaml
generated
@@ -3,14 +3,13 @@ lockfileVersion: 5.4
|
||||
specifiers:
|
||||
'@fontsource/fira-mono': ^4.5.8
|
||||
'@lokalise/node-api': ^7.3.1
|
||||
'@sveltejs/adapter-static': ^1.0.0-next.38
|
||||
'@sveltejs/kit': ^1.0.0-next.384
|
||||
'@sveltejs/adapter-static': ^1.0.0-next.34
|
||||
'@sveltejs/kit': ^1.0.0-next.361
|
||||
'@types/dompurify': ^2.3.3
|
||||
'@types/file-saver': ^2.0.5
|
||||
'@zerodevx/svelte-toast': ^0.7.2
|
||||
adm-zip: ^0.5.9
|
||||
copy-to-clipboard: ^3.3.1
|
||||
dompurify: ^2.3.10
|
||||
dompurify: ^2.3.9
|
||||
dotenv: ^16.0.1
|
||||
file-saver: ^2.0.5
|
||||
pretty-bytes: ^5.6.0
|
||||
@@ -20,22 +19,21 @@ specifiers:
|
||||
svelte-preprocess: ^4.10.7
|
||||
tslib: ^2.4.0
|
||||
typescript: ^4.7.4
|
||||
vite: ^3.0.2
|
||||
vite: ^3.0.0
|
||||
|
||||
dependencies:
|
||||
'@fontsource/fira-mono': 4.5.8
|
||||
copy-to-clipboard: 3.3.1
|
||||
dompurify: 2.3.10
|
||||
dompurify: 2.3.9
|
||||
file-saver: 2.0.5
|
||||
pretty-bytes: 5.6.0
|
||||
|
||||
devDependencies:
|
||||
'@lokalise/node-api': 7.3.1
|
||||
'@sveltejs/adapter-static': 1.0.0-next.38
|
||||
'@sveltejs/kit': 1.0.0-next.384_svelte@3.49.0+vite@3.0.2
|
||||
'@sveltejs/adapter-static': 1.0.0-next.35
|
||||
'@sveltejs/kit': 1.0.0-next.371_svelte@3.49.0+vite@3.0.0
|
||||
'@types/dompurify': 2.3.3
|
||||
'@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
|
||||
@@ -44,7 +42,7 @@ devDependencies:
|
||||
svelte-preprocess: 4.10.7_uslzfc62di2n2otc2tvfklnwji
|
||||
tslib: 2.4.0
|
||||
typescript: 4.7.4
|
||||
vite: 3.0.2
|
||||
vite: 3.0.0
|
||||
|
||||
packages:
|
||||
|
||||
@@ -68,20 +66,20 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/core/7.18.9:
|
||||
resolution: {integrity: sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==}
|
||||
/@babel/core/7.18.6:
|
||||
resolution: {integrity: sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.2.0
|
||||
'@babel/code-frame': 7.18.6
|
||||
'@babel/generator': 7.18.9
|
||||
'@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.9
|
||||
'@babel/helper-module-transforms': 7.18.9
|
||||
'@babel/helpers': 7.18.9
|
||||
'@babel/parser': 7.18.9
|
||||
'@babel/generator': 7.18.7
|
||||
'@babel/helper-compilation-targets': 7.18.6_@babel+core@7.18.6
|
||||
'@babel/helper-module-transforms': 7.18.8
|
||||
'@babel/helpers': 7.18.6
|
||||
'@babel/parser': 7.18.8
|
||||
'@babel/template': 7.18.6
|
||||
'@babel/traverse': 7.18.9
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/traverse': 7.18.8
|
||||
'@babel/types': 7.18.8
|
||||
convert-source-map: 1.8.0
|
||||
debug: 4.3.4
|
||||
gensync: 1.0.0-beta.2
|
||||
@@ -91,73 +89,73 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@babel/generator/7.18.9:
|
||||
resolution: {integrity: sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==}
|
||||
/@babel/generator/7.18.7:
|
||||
resolution: {integrity: sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/types': 7.18.8
|
||||
'@jridgewell/gen-mapping': 0.3.2
|
||||
jsesc: 2.5.2
|
||||
dev: true
|
||||
|
||||
/@babel/helper-compilation-targets/7.18.9_@babel+core@7.18.9:
|
||||
resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==}
|
||||
/@babel/helper-compilation-targets/7.18.6_@babel+core@7.18.6:
|
||||
resolution: {integrity: sha512-vFjbfhNCzqdeAtZflUFrG5YIFqGTqsctrtkZ1D/NB0mDW9TwW3GmmUepYY4G9wCET5rY5ugz4OGTcLd614IzQg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.18.8
|
||||
'@babel/core': 7.18.9
|
||||
'@babel/core': 7.18.6
|
||||
'@babel/helper-validator-option': 7.18.6
|
||||
browserslist: 4.21.2
|
||||
browserslist: 4.21.1
|
||||
semver: 6.3.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-environment-visitor/7.18.9:
|
||||
resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==}
|
||||
/@babel/helper-environment-visitor/7.18.6:
|
||||
resolution: {integrity: sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-function-name/7.18.9:
|
||||
resolution: {integrity: sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==}
|
||||
/@babel/helper-function-name/7.18.6:
|
||||
resolution: {integrity: sha512-0mWMxV1aC97dhjCah5U5Ua7668r5ZmSC2DLfH2EZnf9c3/dHZKiFa5pRLMH5tjSl471tY6496ZWk/kjNONBxhw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.18.6
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/types': 7.18.8
|
||||
dev: true
|
||||
|
||||
/@babel/helper-hoist-variables/7.18.6:
|
||||
resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/types': 7.18.8
|
||||
dev: true
|
||||
|
||||
/@babel/helper-module-imports/7.18.6:
|
||||
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/types': 7.18.8
|
||||
dev: true
|
||||
|
||||
/@babel/helper-module-transforms/7.18.9:
|
||||
resolution: {integrity: sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==}
|
||||
/@babel/helper-module-transforms/7.18.8:
|
||||
resolution: {integrity: sha512-che3jvZwIcZxrwh63VfnFTUzcAM9v/lznYkkRxIBGMPt1SudOKHAEec0SIRCfiuIzTcF7VGj/CaTT6gY4eWxvA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-environment-visitor': 7.18.9
|
||||
'@babel/helper-environment-visitor': 7.18.6
|
||||
'@babel/helper-module-imports': 7.18.6
|
||||
'@babel/helper-simple-access': 7.18.6
|
||||
'@babel/helper-split-export-declaration': 7.18.6
|
||||
'@babel/helper-validator-identifier': 7.18.6
|
||||
'@babel/template': 7.18.6
|
||||
'@babel/traverse': 7.18.9
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/traverse': 7.18.8
|
||||
'@babel/types': 7.18.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@babel/helper-plugin-utils/7.18.9:
|
||||
resolution: {integrity: sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==}
|
||||
/@babel/helper-plugin-utils/7.18.6:
|
||||
resolution: {integrity: sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
@@ -165,14 +163,14 @@ packages:
|
||||
resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/types': 7.18.8
|
||||
dev: true
|
||||
|
||||
/@babel/helper-split-export-declaration/7.18.6:
|
||||
resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/types': 7.18.8
|
||||
dev: true
|
||||
|
||||
/@babel/helper-validator-identifier/7.18.6:
|
||||
@@ -185,13 +183,13 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helpers/7.18.9:
|
||||
resolution: {integrity: sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==}
|
||||
/@babel/helpers/7.18.6:
|
||||
resolution: {integrity: sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.18.6
|
||||
'@babel/traverse': 7.18.9
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/traverse': 7.18.8
|
||||
'@babel/types': 7.18.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -205,12 +203,12 @@ packages:
|
||||
js-tokens: 4.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/parser/7.18.9:
|
||||
resolution: {integrity: sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==}
|
||||
/@babel/parser/7.18.8:
|
||||
resolution: {integrity: sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/types': 7.18.8
|
||||
dev: true
|
||||
|
||||
/@babel/template/7.18.6:
|
||||
@@ -218,30 +216,30 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.18.6
|
||||
'@babel/parser': 7.18.9
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/parser': 7.18.8
|
||||
'@babel/types': 7.18.8
|
||||
dev: true
|
||||
|
||||
/@babel/traverse/7.18.9:
|
||||
resolution: {integrity: sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==}
|
||||
/@babel/traverse/7.18.8:
|
||||
resolution: {integrity: sha512-UNg/AcSySJYR/+mIcJQDCv00T+AqRO7j/ZEJLzpaYtgM48rMg5MnkJgyNqkzo88+p4tfRvZJCEiwwfG6h4jkRg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.18.6
|
||||
'@babel/generator': 7.18.9
|
||||
'@babel/helper-environment-visitor': 7.18.9
|
||||
'@babel/helper-function-name': 7.18.9
|
||||
'@babel/generator': 7.18.7
|
||||
'@babel/helper-environment-visitor': 7.18.6
|
||||
'@babel/helper-function-name': 7.18.6
|
||||
'@babel/helper-hoist-variables': 7.18.6
|
||||
'@babel/helper-split-export-declaration': 7.18.6
|
||||
'@babel/parser': 7.18.9
|
||||
'@babel/types': 7.18.9
|
||||
'@babel/parser': 7.18.8
|
||||
'@babel/types': 7.18.8
|
||||
debug: 4.3.4
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@babel/types/7.18.9:
|
||||
resolution: {integrity: sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==}
|
||||
/@babel/types/7.18.8:
|
||||
resolution: {integrity: sha512-qwpdsmraq0aJ3osLJRApsc2ouSJCdnMeZwB0DhbtHAtRpZNZCdlbRnHIgcRKzdE1g0iOGg644fzjOBcdOz9cPw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.18.6
|
||||
@@ -359,37 +357,37 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/@sveltejs/adapter-static/1.0.0-next.38:
|
||||
resolution: {integrity: sha512-O1b264K62E3OrUnsFxMjKn3CUJF50fxGcW0rWk8fa5kjzskPsSyTxS3jnWNryFaVJ3oSUtx57m4qFW43S1910Q==}
|
||||
/@sveltejs/adapter-static/1.0.0-next.35:
|
||||
resolution: {integrity: sha512-iIg5nCMJF2/s/Y7zmy9pzp+U3YDBL6OQKmwfJm2H3Afde/XlhOuNlSO6K//hxmLmvrd7Oh6Kb0MLhwVKp0cuUA==}
|
||||
dependencies:
|
||||
tiny-glob: 0.2.9
|
||||
dev: true
|
||||
|
||||
/@sveltejs/kit/1.0.0-next.384_svelte@3.49.0+vite@3.0.2:
|
||||
resolution: {integrity: sha512-m1i9dhma1JAuDYp8nrPyqsN6VWv/ye90h6mWQvAC3lCNmKlb4F8cxhMYIhGYgzaZYSqToNiTaK2QRVPuB4i2/g==}
|
||||
/@sveltejs/kit/1.0.0-next.371_svelte@3.49.0+vite@3.0.0:
|
||||
resolution: {integrity: sha512-2MXNb0M97lsEbJx167YNb6ofGRNuNEBUJM9ntIce6usWaurh+2mMg185sA4p0Kl7gl9xSM2D9GXGT7mlaEmJGA==}
|
||||
engines: {node: '>=16.9'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
svelte: ^3.44.0
|
||||
vite: ^3.0.0
|
||||
vite: ^2.9.10
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 1.0.1_svelte@3.49.0+vite@3.0.2
|
||||
'@sveltejs/vite-plugin-svelte': 1.0.0-next.49_svelte@3.49.0+vite@3.0.0
|
||||
chokidar: 3.5.3
|
||||
sade: 1.8.1
|
||||
svelte: 3.49.0
|
||||
vite: 3.0.2
|
||||
vite: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- diff-match-patch
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@sveltejs/vite-plugin-svelte/1.0.1_svelte@3.49.0+vite@3.0.2:
|
||||
resolution: {integrity: sha512-PorCgUounn0VXcpeJu+hOweZODKmGuLHsLomwqSj+p26IwjjGffmYQfVHtiTWq+NqaUuuHWWG7vPge6UFw4Aeg==}
|
||||
engines: {node: ^14.18.0 || >= 16}
|
||||
/@sveltejs/vite-plugin-svelte/1.0.0-next.49_svelte@3.49.0+vite@3.0.0:
|
||||
resolution: {integrity: sha512-AKh0Ka8EDgidnxWUs8Hh2iZLZovkETkefO99XxZ4sW4WGJ7VFeBx5kH/NIIGlaNHLcrIvK3CK0HkZwC3Cici0A==}
|
||||
engines: {node: ^14.13.1 || >= 16}
|
||||
peerDependencies:
|
||||
diff-match-patch: ^1.0.5
|
||||
svelte: ^3.44.0
|
||||
vite: ^3.0.0
|
||||
vite: ^2.9.0
|
||||
peerDependenciesMeta:
|
||||
diff-match-patch:
|
||||
optional: true
|
||||
@@ -401,7 +399,7 @@ packages:
|
||||
magic-string: 0.26.2
|
||||
svelte: 3.49.0
|
||||
svelte-hmr: 0.14.12_svelte@3.49.0
|
||||
vite: 3.0.2
|
||||
vite: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -418,7 +416,7 @@ packages:
|
||||
dependencies:
|
||||
'@types/http-cache-semantics': 4.0.1
|
||||
'@types/keyv': 3.1.4
|
||||
'@types/node': 18.0.6
|
||||
'@types/node': 18.0.3
|
||||
'@types/responselike': 1.0.0
|
||||
dev: true
|
||||
|
||||
@@ -443,11 +441,11 @@ packages:
|
||||
/@types/keyv/3.1.4:
|
||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||
dependencies:
|
||||
'@types/node': 18.0.6
|
||||
'@types/node': 18.0.3
|
||||
dev: true
|
||||
|
||||
/@types/node/18.0.6:
|
||||
resolution: {integrity: sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==}
|
||||
/@types/node/18.0.3:
|
||||
resolution: {integrity: sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==}
|
||||
dev: true
|
||||
|
||||
/@types/pug/2.0.6:
|
||||
@@ -457,23 +455,19 @@ packages:
|
||||
/@types/responselike/1.0.0:
|
||||
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
|
||||
dependencies:
|
||||
'@types/node': 18.0.6
|
||||
'@types/node': 18.0.3
|
||||
dev: true
|
||||
|
||||
/@types/sass/1.43.1:
|
||||
resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
|
||||
dependencies:
|
||||
'@types/node': 18.0.6
|
||||
'@types/node': 18.0.3
|
||||
dev: true
|
||||
|
||||
/@types/trusted-types/2.0.2:
|
||||
resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==}
|
||||
dev: true
|
||||
|
||||
/@zerodevx/svelte-toast/0.7.2:
|
||||
resolution: {integrity: sha512-vWiY6IqsstcOoQ8PFBuFuxgPkj1JFAGhUF9gC7wLx7c5A9SSfdtxWs/39ekGSIeyJK0yqWhTcmzGrCEWSELzDw==}
|
||||
dev: true
|
||||
|
||||
/adm-zip/0.5.9:
|
||||
resolution: {integrity: sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -501,8 +495,8 @@ packages:
|
||||
/babel-plugin-precompile-intl/0.4.0:
|
||||
resolution: {integrity: sha512-kfZPPgjutWg7nPxvwHscgxdhiOUEgWI+MZwh7NZ8lIAqf/tVKzuaoVNC4Bnl4pgzCMpuRktQz2bwFypF2ehJWg==}
|
||||
dependencies:
|
||||
'@babel/core': 7.18.9
|
||||
'@babel/helper-plugin-utils': 7.18.9
|
||||
'@babel/core': 7.18.6
|
||||
'@babel/helper-plugin-utils': 7.18.6
|
||||
'@formatjs/icu-messageformat-parser': 2.1.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -531,15 +525,15 @@ packages:
|
||||
fill-range: 7.0.1
|
||||
dev: true
|
||||
|
||||
/browserslist/4.21.2:
|
||||
resolution: {integrity: sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==}
|
||||
/browserslist/4.21.1:
|
||||
resolution: {integrity: sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001367
|
||||
electron-to-chromium: 1.4.195
|
||||
caniuse-lite: 1.0.30001366
|
||||
electron-to-chromium: 1.4.188
|
||||
node-releases: 2.0.6
|
||||
update-browserslist-db: 1.0.5_browserslist@4.21.2
|
||||
update-browserslist-db: 1.0.4_browserslist@4.21.1
|
||||
dev: true
|
||||
|
||||
/buffer-crc32/0.2.13:
|
||||
@@ -555,13 +549,13 @@ packages:
|
||||
resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
clone-response: 1.0.3
|
||||
clone-response: 1.0.2
|
||||
get-stream: 5.2.0
|
||||
http-cache-semantics: 4.1.0
|
||||
keyv: 4.3.3
|
||||
keyv: 4.3.2
|
||||
lowercase-keys: 2.0.0
|
||||
normalize-url: 6.1.0
|
||||
responselike: 2.0.1
|
||||
responselike: 2.0.0
|
||||
dev: true
|
||||
|
||||
/callsites/3.1.0:
|
||||
@@ -569,8 +563,8 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/caniuse-lite/1.0.30001367:
|
||||
resolution: {integrity: sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==}
|
||||
/caniuse-lite/1.0.30001366:
|
||||
resolution: {integrity: sha512-yy7XLWCubDobokgzudpkKux8e0UOOnLHE6mlNJBzT3lZJz6s5atSEzjoL+fsCPkI0G8MP5uVdDx1ur/fXEWkZA==}
|
||||
dev: true
|
||||
|
||||
/chalk/2.4.2:
|
||||
@@ -597,8 +591,8 @@ packages:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/clone-response/1.0.3:
|
||||
resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
|
||||
/clone-response/1.0.2:
|
||||
resolution: {integrity: sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==}
|
||||
dependencies:
|
||||
mimic-response: 1.0.1
|
||||
dev: true
|
||||
@@ -671,8 +665,8 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/dompurify/2.3.10:
|
||||
resolution: {integrity: sha512-o7Fg/AgC7p/XpKjf/+RC3Ok6k4St5F7Q6q6+Nnm3p2zGWioAY6dh0CbbuwOhH2UcSzKsdniE/YnE2/92JcsA+g==}
|
||||
/dompurify/2.3.9:
|
||||
resolution: {integrity: sha512-3zOnuTwup4lPV/GfGS6UzG4ub9nhSYagR/5tB3AvDEwqyy5dtyCM2dVjwGDCnrPerXifBKTYh/UWCGKK7ydhhw==}
|
||||
dev: false
|
||||
|
||||
/dotenv/16.0.1:
|
||||
@@ -680,8 +674,8 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/electron-to-chromium/1.4.195:
|
||||
resolution: {integrity: sha512-vefjEh0sk871xNmR5whJf9TEngX+KTKS3hOHpjoMpauKkwlGwtMz1H8IaIjAT/GNnX0TbGwAdmVoXCAzXf+PPg==}
|
||||
/electron-to-chromium/1.4.188:
|
||||
resolution: {integrity: sha512-Zpa1+E+BVmD/orkyz1Z2dAT1XNUuVAHB3GrogfyY66dXN0ZWSsygI8+u6QTDai1ZayLcATDJpcv2Z2AZjEcr1A==}
|
||||
dev: true
|
||||
|
||||
/end-of-stream/1.4.4:
|
||||
@@ -1017,7 +1011,7 @@ packages:
|
||||
http2-wrapper: 1.0.3
|
||||
lowercase-keys: 2.0.0
|
||||
p-cancelable: 2.1.1
|
||||
responselike: 2.0.1
|
||||
responselike: 2.0.0
|
||||
dev: true
|
||||
|
||||
/graceful-fs/4.2.10:
|
||||
@@ -1124,8 +1118,8 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/keyv/4.3.3:
|
||||
resolution: {integrity: sha512-AcysI17RvakTh8ir03+a3zJr5r0ovnAH/XTXei/4HIv3bL2K/jzvgivLK9UuI/JbU1aJjM3NSAnVvVVd3n+4DQ==}
|
||||
/keyv/4.3.2:
|
||||
resolution: {integrity: sha512-kn8WmodVBe12lmHpA6W8OY7SNh6wVR+Z+wZESF4iF5FCazaVXGWOtnbnvX0tMQ1bO+/TmOD9LziuYMvrIIs0xw==}
|
||||
dependencies:
|
||||
compress-brotli: 1.3.8
|
||||
json-buffer: 3.0.1
|
||||
@@ -1332,8 +1326,8 @@ packages:
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
dev: true
|
||||
|
||||
/responselike/2.0.1:
|
||||
resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
|
||||
/responselike/2.0.0:
|
||||
resolution: {integrity: sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==}
|
||||
dependencies:
|
||||
lowercase-keys: 2.0.0
|
||||
dev: true
|
||||
@@ -1350,8 +1344,8 @@ packages:
|
||||
glob: 7.2.3
|
||||
dev: true
|
||||
|
||||
/rollup/2.77.0:
|
||||
resolution: {integrity: sha512-vL8xjY4yOQEw79DvyXLijhnhh+R/O9zpF/LEgkCebZFtb6ELeN9H3/2T0r8+mp+fFTBHZ5qGpOpW2ela2zRt3g==}
|
||||
/rollup/2.76.0:
|
||||
resolution: {integrity: sha512-9jwRIEY1jOzKLj3nsY/yot41r19ITdQrhs+q3ggNWhr9TQgduHqANvPpS32RNpzGklJu3G1AJfvlZLi/6wFgWA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
@@ -1572,20 +1566,20 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/update-browserslist-db/1.0.5_browserslist@4.21.2:
|
||||
resolution: {integrity: sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==}
|
||||
/update-browserslist-db/1.0.4_browserslist@4.21.1:
|
||||
resolution: {integrity: sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
dependencies:
|
||||
browserslist: 4.21.2
|
||||
browserslist: 4.21.1
|
||||
escalade: 3.1.1
|
||||
picocolors: 1.0.0
|
||||
dev: true
|
||||
|
||||
/vite/3.0.2:
|
||||
resolution: {integrity: sha512-TAqydxW/w0U5AoL5AsD9DApTvGb2iNbGs3sN4u2VdT1GFkQVUfgUldt+t08TZgi23uIauh1TUOQJALduo9GXqw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
/vite/3.0.0:
|
||||
resolution: {integrity: sha512-M7phQhY3+fRZa0H+1WzI6N+/onruwPTBTMvaj7TzgZ0v2TE+N2sdLKxJOfOv9CckDWt5C4HmyQP81xB4dwRKzA==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
less: '*'
|
||||
@@ -1605,7 +1599,7 @@ packages:
|
||||
esbuild: 0.14.49
|
||||
postcss: 8.4.14
|
||||
resolve: 1.22.1
|
||||
rollup: 2.77.0
|
||||
rollup: 2.76.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
@@ -14,9 +14,7 @@
|
||||
--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;
|
||||
}
|
||||
|
@@ -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(),
|
||||
}
|
@@ -11,10 +11,6 @@ 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
|
||||
}
|
||||
|
||||
|
@@ -19,79 +19,53 @@ export class Hex {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
const ALG = 'AES-GCM'
|
||||
|
||||
static async fromString(s: string): Promise<ArrayBuffer> {
|
||||
return fetch(s)
|
||||
.then((r) => r.blob())
|
||||
.then((b) => b.arrayBuffer())
|
||||
}
|
||||
export function getRandomBytes(size = 16): Uint8Array {
|
||||
return window.crypto.getRandomValues(new Uint8Array(size))
|
||||
}
|
||||
|
||||
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 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 encrypted: ArrayBuffer = await window.crypto.subtle.encrypt(
|
||||
{ name: this.ALG, iv },
|
||||
derived,
|
||||
plaintext
|
||||
)
|
||||
const data = [
|
||||
Hex.encode(salt),
|
||||
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)
|
||||
return plaintext
|
||||
}
|
||||
export function getKeyFromString(password: string) {
|
||||
return window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
)
|
||||
}
|
||||
|
||||
export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
|
||||
const iterations = 100_000
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt,
|
||||
iterations,
|
||||
hash: 'SHA-512',
|
||||
},
|
||||
key,
|
||||
{ name: ALG, length: 256 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
}
|
||||
|
||||
export async function encrypt(plaintext: string, key: CryptoKey) {
|
||||
const salt = getRandomBytes(16)
|
||||
const derived = await getDerivedForKey(key, salt)
|
||||
const iv = getRandomBytes(16)
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: ALG, iv },
|
||||
derived,
|
||||
new TextEncoder().encode(plaintext)
|
||||
)
|
||||
return [salt, iv, encrypted].map(Hex.encode).join(':')
|
||||
}
|
||||
|
||||
export async function decrypt(ciphertext: string, key: CryptoKey) {
|
||||
const [salt, iv, encrypted] = ciphertext.split(':').map(Hex.decode)
|
||||
const derived = await getDerivedForKey(key, salt)
|
||||
const plaintext = await window.crypto.subtle.decrypt({ name: ALG, iv }, derived, encrypted)
|
||||
return new TextDecoder().decode(plaintext)
|
||||
}
|
||||
|
13
frontend/src/lib/files.ts
Normal file
13
frontend/src/lib/files.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export class Files {
|
||||
static toString(f: File | Blob): Promise<string> {
|
||||
const reader = new window.FileReader()
|
||||
reader.readAsDataURL(f)
|
||||
return new Promise((resolve) => {
|
||||
reader.onloadend = () => resolve(reader.result as string)
|
||||
})
|
||||
}
|
||||
|
||||
static async fromString(s: string): Promise<Blob> {
|
||||
return fetch(s).then((r) => r.blob())
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
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),
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
<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'
|
||||
|
||||
export let note: Note
|
||||
export let timeExpiration = false
|
||||
</script>
|
||||
|
||||
<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}
|
||||
validate={(v) =>
|
||||
($status && v < $status?.max_views) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
||||
/>
|
||||
<div class="middle-switch">
|
||||
<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}
|
||||
disabled={!timeExpiration}
|
||||
max={$status?.max_expiration}
|
||||
validate={(v) =>
|
||||
($status && v < $status?.max_expiration) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.middle-switch {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
@@ -1,32 +1,38 @@
|
||||
<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 { Files } from '$lib/files'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import Button from './Button.svelte'
|
||||
import MaxSize from './MaxSize.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
export let files: FileDTO[] = []
|
||||
let files: File[] = []
|
||||
|
||||
function fileToDTO(file: File): FileDTO {
|
||||
return {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
contents: file,
|
||||
}
|
||||
}
|
||||
const dispatch = createEventDispatcher<{ file: string }>()
|
||||
|
||||
async function onInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (input?.files?.length) {
|
||||
files = [...files, ...Array.from(input.files).map(fileToDTO)]
|
||||
files = [...files, ...Array.from(input.files)]
|
||||
const data: FileDTO[] = await Promise.all(
|
||||
files.map(async (file) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
contents: await Files.toString(file),
|
||||
}))
|
||||
)
|
||||
dispatch('file', JSON.stringify(data))
|
||||
} else {
|
||||
dispatch('file', '')
|
||||
}
|
||||
}
|
||||
|
||||
function clear(e: Event) {
|
||||
e.preventDefault()
|
||||
files = []
|
||||
dispatch('file', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -34,7 +40,7 @@
|
||||
<small>
|
||||
{label}
|
||||
</small>
|
||||
<input {...$$restProps} type="file" on:change={onInput} multiple />
|
||||
<input type="file" on:change={onInput} multiple />
|
||||
<div class="box">
|
||||
{#if files.length}
|
||||
<div>
|
||||
@@ -51,9 +57,7 @@
|
||||
<div>
|
||||
<b>{$t('file_upload.no_files_selected')}</b>
|
||||
<br />
|
||||
<small>
|
||||
{$t('common.max')}: <MaxSize />
|
||||
</small>
|
||||
<small>{$t('common.max')}: <MaxSize /></small>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@@ -1,41 +0,0 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 100 100"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<rect fill="none" stroke="currentColor" stroke-width="4" x="25" y="25" width="50" height="50">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="0.5s"
|
||||
from="0 50 50"
|
||||
to="180 50 50"
|
||||
type="rotate"
|
||||
id="strokeBox"
|
||||
attributeType="XML"
|
||||
begin="rectBox.end"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="27" y="27" fill="currentColor" width="46" height="50">
|
||||
<animate
|
||||
attributeName="height"
|
||||
dur="1.3s"
|
||||
attributeType="XML"
|
||||
from="50"
|
||||
to="0"
|
||||
id="rectBox"
|
||||
fill="freeze"
|
||||
begin="0s;strokeBox.end"
|
||||
/>
|
||||
</rect>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
height: 2em;
|
||||
position: relative;
|
||||
top: 0.6em;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
Before Width: | Height: | Size: 784 B |
@@ -1,17 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { status } from '$lib/stores/status'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { _ } from 'svelte-intl-precompile'
|
||||
|
||||
import { status } from '$lib/stores/status'
|
||||
|
||||
// Due to encoding overhead (~35%) with base64
|
||||
// https://en.wikipedia.org/wiki/Base64
|
||||
const overhead = 1 / 1.35
|
||||
</script>
|
||||
|
||||
<span>
|
||||
{#if $status !== null}
|
||||
{prettyBytes($status.max_size * overhead, { binary: true })}
|
||||
{prettyBytes($status.max_size, { binary: true })}
|
||||
{:else}
|
||||
{$_('common.loading')}
|
||||
{/if}
|
||||
|
@@ -1,37 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
export type NoteResult = {
|
||||
password: string
|
||||
id: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
|
||||
export let result: NoteResult
|
||||
|
||||
function reset() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<TextInput
|
||||
type="text"
|
||||
readonly
|
||||
label={$t('common.share_link')}
|
||||
value="{window.location.origin}/note/{result.id}#{result.password}"
|
||||
copy
|
||||
data-testid="share-link"
|
||||
/>
|
||||
<br />
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
<br />
|
||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||
|
||||
<style>
|
||||
</style>
|
@@ -1,24 +1,20 @@
|
||||
<script lang="ts" context="module">
|
||||
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { FileDTO, NotePublic } from '$lib/api'
|
||||
import { Files } from '$lib/files'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { saveAs } from 'file-saver'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import Button from './Button.svelte'
|
||||
|
||||
import type { FileDTO, NotePublic } from '$lib/api'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import { copy } from '$lib/utils'
|
||||
|
||||
export let note: DecryptedNote
|
||||
export let note: NotePublic
|
||||
|
||||
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
|
||||
let files: FileDTO[] = []
|
||||
|
||||
$: if (note.meta.type === 'file') {
|
||||
files = note.contents
|
||||
files = JSON.parse(note.contents) as FileDTO[]
|
||||
}
|
||||
|
||||
$: download = () => {
|
||||
@@ -28,7 +24,7 @@
|
||||
}
|
||||
|
||||
async function downloadFile(file: FileDTO) {
|
||||
const f = new File([file.contents], file.name, {
|
||||
const f = new File([await Files.fromString(file.contents)], file.name, {
|
||||
type: file.type,
|
||||
})
|
||||
saveAs(f)
|
||||
@@ -44,22 +40,20 @@
|
||||
</script>
|
||||
|
||||
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
|
||||
<div data-testid="result">
|
||||
{#if note.meta.type === 'text'}
|
||||
<div class="note">
|
||||
{@html contentWithLinks(note.contents)}
|
||||
{#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>
|
||||
<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>
|
||||
{/each}
|
||||
<Button on:click={download}>{$t('show.download_all')}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<Button on:click={download}>{$t('show.download_all')}</Button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.note {
|
||||
|
@@ -1,7 +1,9 @@
|
||||
<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 '$lib/crypto'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { fade } from 'svelte/transition'
|
||||
import Icon from './Icon.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
export let value: any
|
||||
@@ -12,6 +14,8 @@
|
||||
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)
|
||||
|
||||
@@ -23,8 +27,22 @@
|
||||
function toggle() {
|
||||
hidden = !hidden
|
||||
}
|
||||
function copyFN() {
|
||||
copyToClipboard(value.toString())
|
||||
notify($t('home.copied_to_clipboard'))
|
||||
}
|
||||
function randomFN() {
|
||||
value = Hex.encode(Crypto.getRandomBytes(20))
|
||||
value = Hex.encode(getRandomBytes(20))
|
||||
}
|
||||
|
||||
function notify(msg: string, delay: number = 2000) {
|
||||
if (notificationTimeout) {
|
||||
clearTimeout(notificationTimeout)
|
||||
}
|
||||
notificationTimeout = setTimeout(() => {
|
||||
notification = null
|
||||
}, delay)
|
||||
notification = msg
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -44,9 +62,12 @@
|
||||
<Icon class="icon" icon="dice" on:click={randomFN} />
|
||||
{/if}
|
||||
{#if copy}
|
||||
<Icon class="icon" icon="copy" on:click={() => copyFN(value.toString())} />
|
||||
<Icon class="icon" icon="copy" on:click={copyFN} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if notification}
|
||||
<div class="notification" transition:fade><small>{notification}</small></div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<style>
|
||||
@@ -96,4 +117,11 @@
|
||||
.icons > :global(.icon:hover) {
|
||||
border-color: var(--ui-clr-primary);
|
||||
}
|
||||
|
||||
.notification {
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -1.5em;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,11 +0,0 @@
|
||||
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)
|
||||
}
|
@@ -1,21 +1,16 @@
|
||||
<script lang="ts">
|
||||
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 { Crypto, Hex } from '$lib/crypto'
|
||||
import type { Note } from '$lib/api'
|
||||
import { create,PayloadToLargeError } from '$lib/api'
|
||||
import { encrypt,getKeyFromString,getRandomBytes,Hex } from '$lib/crypto'
|
||||
import { status } from '$lib/stores/status'
|
||||
import { 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'
|
||||
import Loader from '$lib/ui/Loader.svelte'
|
||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextArea from '$lib/ui/TextArea.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { blur } from 'svelte/transition'
|
||||
|
||||
let note: Note = {
|
||||
contents: '',
|
||||
@@ -23,13 +18,13 @@
|
||||
views: 1,
|
||||
expiration: 60,
|
||||
}
|
||||
let files: FileDTO[]
|
||||
let result: NoteResult | null = null
|
||||
let result: { password: string; id: string } | null = null
|
||||
let advanced = false
|
||||
let isFile = false
|
||||
let file = false
|
||||
let timeExpiration = false
|
||||
let description = ''
|
||||
let loading: string | null = null
|
||||
let message = ''
|
||||
let loading = false
|
||||
let error: string | null = null
|
||||
|
||||
$: if (!advanced) {
|
||||
note.views = 1
|
||||
@@ -37,7 +32,7 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
description = $t('home.explanation', {
|
||||
message = $t('home.explanation', {
|
||||
values: {
|
||||
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
||||
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
|
||||
@@ -46,9 +41,9 @@
|
||||
})
|
||||
}
|
||||
|
||||
$: note.meta.type = isFile ? 'file' : 'text'
|
||||
$: note.meta.type = file ? 'file' : 'text'
|
||||
|
||||
$: if (!isFile) {
|
||||
$: if (!file) {
|
||||
note.contents = ''
|
||||
}
|
||||
|
||||
@@ -56,79 +51,71 @@
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
loading = $t('common.encrypting')
|
||||
|
||||
const password = Hex.encode(Crypto.getRandomBytes(32))
|
||||
const key = await Crypto.getKeyFromString(password)
|
||||
|
||||
error = null
|
||||
loading = true
|
||||
const password = Hex.encode(getRandomBytes(32))
|
||||
const key = await getKeyFromString(password)
|
||||
if (note.contents === '') throw new EmptyContentError()
|
||||
const data: Note = {
|
||||
contents: '',
|
||||
contents: await encrypt(note.contents, key),
|
||||
meta: note.meta,
|
||||
}
|
||||
if (isFile) {
|
||||
if (files.length === 0) throw new EmptyContentError()
|
||||
data.contents = await Adapters.Files.encrypt(files, key)
|
||||
} else {
|
||||
if (note.contents === '') throw new EmptyContentError()
|
||||
data.contents = await Adapters.Text.encrypt(note.contents, key)
|
||||
}
|
||||
if (timeExpiration) data.expiration = parseInt(note.expiration as any)
|
||||
else data.views = parseInt(note.views as any)
|
||||
|
||||
loading = $t('common.uploading')
|
||||
const response = await create(data)
|
||||
result = {
|
||||
password: password,
|
||||
id: response.id,
|
||||
}
|
||||
notify.success($t('home.messages.note_created'))
|
||||
} catch (e) {
|
||||
if (e instanceof PayloadToLargeError) {
|
||||
notify.error($t('home.errors.note_to_big'))
|
||||
error = $t('home.errors.note_to_big')
|
||||
} else if (e instanceof EmptyContentError) {
|
||||
notify.error($t('home.errors.empty_content'))
|
||||
error = $t('home.errors.empty_content')
|
||||
} else {
|
||||
console.error(e)
|
||||
notify.error($t('home.errors.note_error'))
|
||||
error = $t('home.errors.note_error')
|
||||
}
|
||||
} finally {
|
||||
loading = null
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if result}
|
||||
<Result {result} />
|
||||
<TextInput
|
||||
type="text"
|
||||
readonly
|
||||
label={$t('common.share_link')}
|
||||
value="{window.location.origin}/note/{result.id}#{result.password}"
|
||||
copy
|
||||
/>
|
||||
<br />
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
<br />
|
||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||
{:else}
|
||||
<p>
|
||||
{@html $status?.theme_text || $t('home.intro')}
|
||||
</p>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<fieldset disabled={loading !== null}>
|
||||
{#if isFile}
|
||||
<FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
|
||||
<fieldset disabled={loading}>
|
||||
{#if file}
|
||||
<FileUpload label={$t('common.file')} on:file={(f) => (note.contents = f.detail)} />
|
||||
{:else}
|
||||
<TextArea
|
||||
data-testid="text-field"
|
||||
label={$t('common.note')}
|
||||
bind:value={note.contents}
|
||||
placeholder="..."
|
||||
/>
|
||||
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
|
||||
{/if}
|
||||
|
||||
<div class="bottom">
|
||||
<Switch
|
||||
data-testid="switch-file"
|
||||
class="file"
|
||||
label={$t('common.file')}
|
||||
bind:value={isFile}
|
||||
/>
|
||||
<Switch class="file" label={$t('common.file')} bind:value={file} />
|
||||
{#if $status?.allow_advanced}
|
||||
<Switch
|
||||
data-testid="switch-advanced"
|
||||
label={$t('common.advanced')}
|
||||
bind:value={advanced}
|
||||
/>
|
||||
<Switch label={$t('common.advanced')} bind:value={advanced} />
|
||||
{/if}
|
||||
<div class="grow" />
|
||||
<div class="tr">
|
||||
@@ -138,19 +125,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="error-text">{error}</div>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
<br />
|
||||
{#if loading}
|
||||
{loading} <Loader />
|
||||
{$t('common.loading')}
|
||||
{:else}
|
||||
{description}
|
||||
{message}
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
{#if advanced}
|
||||
<div transition:blur={{ duration: 250 }}>
|
||||
<br />
|
||||
<AdvancedParameters bind:note bind:timeExpiration />
|
||||
<div class="fields">
|
||||
<TextInput
|
||||
type="number"
|
||||
label={$t('common.views', { values: { n: 0 } })}
|
||||
bind:value={note.views}
|
||||
disabled={timeExpiration}
|
||||
max={$status?.max_views}
|
||||
validate={(v) =>
|
||||
($status && v < $status?.max_views) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
||||
/>
|
||||
<div class="middle-switch">
|
||||
<Switch label={$t('common.mode')} bind:value={timeExpiration} color={false} />
|
||||
</div>
|
||||
<TextInput
|
||||
type="number"
|
||||
label={$t('common.minutes', { values: { n: 0 } })}
|
||||
bind:value={note.expiration}
|
||||
disabled={!timeExpiration}
|
||||
max={$status?.max_expiration}
|
||||
validate={(v) =>
|
||||
($status && v < $status?.max_expiration) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</fieldset>
|
||||
@@ -171,4 +186,16 @@
|
||||
.grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.middle-switch {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" context="module">
|
||||
import { getLocaleFromNavigator, init, waitLocale } from 'svelte-intl-precompile'
|
||||
import { init, waitLocale, getLocaleFromNavigator } from 'svelte-intl-precompile'
|
||||
// @ts-ignore
|
||||
import { registerAll } from '$locales'
|
||||
registerAll()
|
||||
@@ -7,14 +7,11 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import '../app.css'
|
||||
|
||||
import { init as initStores } 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()
|
||||
@@ -31,8 +28,6 @@
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<SvelteToast />
|
||||
|
||||
<Footer />
|
||||
{/await}
|
||||
|
||||
|
@@ -12,67 +12,47 @@
|
||||
import { onMount } from 'svelte'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import { Adapters } from '$lib/adapters'
|
||||
import type { NotePublic } from '$lib/api'
|
||||
import { get, info } from '$lib/api'
|
||||
import { Crypto } from '$lib/crypto'
|
||||
import { decrypt, getKeyFromString } 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 ShowNote from '$lib/ui/ShowNote.svelte'
|
||||
|
||||
export let id: string
|
||||
|
||||
let password: string
|
||||
let note: DecryptedNote | null = null
|
||||
let note: NotePublic | null = null
|
||||
let exists = false
|
||||
|
||||
let loading: string | null = null
|
||||
let error: string | null = null
|
||||
let loading = true
|
||||
let error = false
|
||||
|
||||
onMount(async () => {
|
||||
// Check if note exists
|
||||
try {
|
||||
loading = $t('common.loading')
|
||||
loading = true
|
||||
error = false
|
||||
password = window.location.hash.slice(1)
|
||||
await info(id)
|
||||
exists = true
|
||||
} catch {
|
||||
exists = false
|
||||
} finally {
|
||||
loading = null
|
||||
loading = false
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the actual contents of the note and decrypt it.
|
||||
*/
|
||||
async function show() {
|
||||
try {
|
||||
error = null
|
||||
loading = $t('common.downloading')
|
||||
const data = await get(id)
|
||||
loading = $t('common.decrypting')
|
||||
const key = await Crypto.getKeyFromString(password)
|
||||
switch (data.meta.type) {
|
||||
case 'text':
|
||||
note = {
|
||||
meta: { type: 'text' },
|
||||
contents: await Adapters.Text.decrypt(data.contents, key),
|
||||
}
|
||||
break
|
||||
case 'file':
|
||||
note = {
|
||||
meta: { type: 'file' },
|
||||
contents: await Adapters.Files.decrypt(data.contents, key),
|
||||
}
|
||||
break
|
||||
default:
|
||||
error = $t('show.errors.unsupported_type')
|
||||
return
|
||||
}
|
||||
error = false
|
||||
loading = true
|
||||
const data = note || (await get(id)) // Don't get the content twice on wrong password.
|
||||
const key = await getKeyFromString(password)
|
||||
data.contents = await decrypt(data.contents, key)
|
||||
note = data
|
||||
} catch {
|
||||
error = $t('show.errors.decryption_failed')
|
||||
error = true
|
||||
} finally {
|
||||
loading = null
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -86,11 +66,11 @@
|
||||
<form on:submit|preventDefault={show}>
|
||||
<fieldset>
|
||||
<p>{$t('show.explanation')}</p>
|
||||
<Button data-testid="show-note-button" type="submit">{$t('show.show_note')}</Button>
|
||||
<Button type="submit">{$t('show.show_note')}</Button>
|
||||
{#if error}
|
||||
<br />
|
||||
<p class="error-text">
|
||||
{error}
|
||||
{$t('show.errors.decryption_failed')}
|
||||
<br />
|
||||
</p>
|
||||
{/if}
|
||||
@@ -99,11 +79,5 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{#if loading}
|
||||
<p class="loader">{loading} <Loader /></p>
|
||||
<p>{$t('common.loading')}</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.loader {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
13
package.json
13
package.json
@@ -4,20 +4,9 @@
|
||||
"dev:backend": "cd backend && cargo watch -x 'run --bin cryptgeon'",
|
||||
"dev:front": "pnpm --prefix frontend run dev",
|
||||
"dev:proxy": "node proxy.mjs",
|
||||
"dev": "run-p dev:*",
|
||||
"test": "playwright test --project chrome firefox safari",
|
||||
"test:local": "playwright test --project local",
|
||||
"ci:server": "cd backend && SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:1234 cargo run",
|
||||
"ci:server:backend": "cd backend && cargo run",
|
||||
"ci:server:front": "pnpm --prefix frontend run preview",
|
||||
"ci:server:proxy": "node proxy.mjs",
|
||||
"ci:prepare": "run-p ci:prepare:*",
|
||||
"ci:prepare:backend": "cd backend && cargo build",
|
||||
"ci:prepare:front": "pnpm --prefix frontend install && pnpm --prefix frontend run build"
|
||||
"dev": "run-p dev:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.23.4",
|
||||
"@types/node": "16",
|
||||
"http-proxy": "^1.18.1",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
|
@@ -1,30 +0,0 @@
|
||||
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',
|
||||
|
||||
webServer: {
|
||||
command: 'pnpm run ci:server',
|
||||
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
|
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -1,36 +1,15 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
'@playwright/test': ^1.23.4
|
||||
'@types/node': '16'
|
||||
http-proxy: ^1.18.1
|
||||
npm-run-all: ^4.1.5
|
||||
|
||||
devDependencies:
|
||||
'@playwright/test': 1.23.4
|
||||
'@types/node': 16.11.45
|
||||
http-proxy: 1.18.1
|
||||
npm-run-all: 4.1.5
|
||||
|
||||
packages:
|
||||
|
||||
/@playwright/test/1.23.4:
|
||||
resolution: {integrity: sha512-iIsoMJDS/lyuhw82FtcV/B3PXikgVD3hNe5hyvOpRM0uRr1OIpN3LgPeRbBjhzBWmyf6RgRg5fqK5sVcpA03yA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@types/node': 18.0.6
|
||||
playwright-core: 1.23.4
|
||||
dev: true
|
||||
|
||||
/@types/node/16.11.45:
|
||||
resolution: {integrity: sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==}
|
||||
dev: true
|
||||
|
||||
/@types/node/18.0.6:
|
||||
resolution: {integrity: sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==}
|
||||
dev: true
|
||||
|
||||
/ansi-styles/3.2.1:
|
||||
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -451,12 +430,6 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/playwright-core/1.23.4:
|
||||
resolution: {integrity: sha512-h5V2yw7d8xIwotjyNrkLF13nV9RiiZLHdXeHo+nVJIYGVlZ8U2qV0pMxNJKNTvfQVT0N8/A4CW6/4EW2cOcTiA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/read-pkg/3.0.0:
|
||||
resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
|
||||
engines: {node: '>=4'}
|
||||
|
BIN
test/assets/AES.pdf
(Stored with Git LFS)
BIN
test/assets/AES.pdf
(Stored with Git LFS)
Binary file not shown.
BIN
test/assets/Pigeons.zip
(Stored with Git LFS)
BIN
test/assets/Pigeons.zip
(Stored with Git LFS)
Binary file not shown.
BIN
test/assets/alfred-kenneally-UIu4RmMxnHU-unsplash.jpg
(Stored with Git LFS)
BIN
test/assets/alfred-kenneally-UIu4RmMxnHU-unsplash.jpg
(Stored with Git LFS)
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
PDF: 'test/assets/AES.pdf',
|
||||
Image: 'test/assets/alfred-kenneally-UIu4RmMxnHU-unsplash.jpg',
|
||||
Zip: 'test/assets/Pigeons.zip',
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkForDownload, createNote, getFileChecksum } from '../utils'
|
||||
import Files from './files'
|
||||
|
||||
test('multiple', async ({ page }) => {
|
||||
const files = [Files.PDF, Files.Image]
|
||||
const checksums = await Promise.all(files.map(getFileChecksum))
|
||||
const link = await createNote(page, { files, views: 2 })
|
||||
await checkLinkForDownload(page, link, 'alfred-kenneally', checksums[1])
|
||||
await checkLinkForDownload(page, link, 'AES.pdf', checksums[0])
|
||||
})
|
@@ -1,24 +0,0 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote, getFileChecksum } from '../utils'
|
||||
import Files from './files'
|
||||
|
||||
test('simple pdf', async ({ page }) => {
|
||||
const files = [Files.PDF]
|
||||
const link = await createNote(page, { files })
|
||||
await checkLinkForText(page, link, 'AES.pdf')
|
||||
await checkLinkDoesNotExist(page, link)
|
||||
})
|
||||
|
||||
test('pdf content', async ({ page }) => {
|
||||
const files = [Files.PDF]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNote(page, { files })
|
||||
await checkLinkForDownload(page, link, 'AES.pdf', checksum)
|
||||
})
|
||||
|
||||
test('image content', async ({ page }) => {
|
||||
const files = [Files.Image]
|
||||
const checksum = await getFileChecksum(files[0])
|
||||
const link = await createNote(page, { files })
|
||||
await checkLinkForDownload(page, link, 'alfred-kenneally', checksum)
|
||||
})
|
37
test/text.ts
37
test/text.ts
@@ -1,37 +0,0 @@
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
async function createNote(page: Page, text: string): Promise<string> {
|
||||
await page.goto('/')
|
||||
await page.locator('textarea').click()
|
||||
await page.locator('textarea').fill(text)
|
||||
await page.locator('button:has-text("create")').click()
|
||||
|
||||
await page.locator('[data-testid="share-link"]').click()
|
||||
const shareLink = await page.locator('[data-testid="share-link"]').inputValue()
|
||||
return shareLink
|
||||
}
|
||||
|
||||
async function checkLinkForText(page: Page, link: string, text: string) {
|
||||
await page.goto(link)
|
||||
await page.locator('[data-testid="show-note-button"]').click()
|
||||
expect(await page.locator('[data-testid="result"] >> .note').innerText()).toBe(text)
|
||||
}
|
||||
|
||||
async function checkLinkDoesNotExist(page: Page, link: string) {
|
||||
await page.goto('/') // Required due to firefox: https://github.com/microsoft/playwright/issues/15781
|
||||
await page.goto(link)
|
||||
await expect(page.locator('main')).toContainText('note was not found or was already deleted')
|
||||
}
|
||||
|
||||
test('simple', async ({ page }) => {
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||
const shareLink = await createNote(page, text)
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
})
|
||||
|
||||
test('only shown once', async ({ page }) => {
|
||||
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
|
||||
const shareLink = await createNote(page, text)
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
await checkLinkDoesNotExist(page, shareLink)
|
||||
})
|
@@ -1,14 +0,0 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../utils'
|
||||
|
||||
test('1 minute', async ({ page }) => {
|
||||
const text = `Virtues value ascetic revaluation sea dead strong burying.`
|
||||
const minutes = 1
|
||||
const timeout = minutes * 60_000
|
||||
test.setTimeout(timeout * 2)
|
||||
const shareLink = await createNote(page, { text, expiration: minutes })
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
await page.waitForTimeout(timeout)
|
||||
await checkLinkDoesNotExist(page, shareLink)
|
||||
})
|
@@ -1,8 +0,0 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkForText, createNote } from '../utils'
|
||||
|
||||
test('simple', async ({ page }) => {
|
||||
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
|
||||
const shareLink = await createNote(page, { text })
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
})
|
@@ -1,18 +0,0 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../utils'
|
||||
|
||||
test('only shown once', async ({ page }) => {
|
||||
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
|
||||
const shareLink = await createNote(page, { text })
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
await checkLinkDoesNotExist(page, shareLink)
|
||||
})
|
||||
|
||||
test('view 3 times', async ({ page }) => {
|
||||
const text = `Justice holiest overcome fearful strong ultimate holiest christianity.`
|
||||
const shareLink = await createNote(page, { text, views: 3 })
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
await checkLinkForText(page, shareLink, text)
|
||||
await checkLinkDoesNotExist(page, shareLink)
|
||||
})
|
@@ -1,71 +0,0 @@
|
||||
import { expect, type Page } from '@playwright/test'
|
||||
import { createHash } from 'crypto'
|
||||
import { readFile } from 'fs/promises'
|
||||
|
||||
type CreatePage = { text?: string; files?: string[]; views?: number; expiration?: number; error?: string }
|
||||
export async function createNote(page: Page, options: CreatePage): Promise<string> {
|
||||
await page.goto('/')
|
||||
|
||||
if (options.text) {
|
||||
await page.locator('[data-testid="text-field"]').fill(options.text)
|
||||
} else if (options.files) {
|
||||
await page.locator('[data-testid="switch-file"]').click()
|
||||
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent('filechooser'),
|
||||
page.locator('text=No Files Selected').click(),
|
||||
])
|
||||
await fileChooser.setFiles(options.files)
|
||||
}
|
||||
|
||||
if (options.views) {
|
||||
await page.locator('[data-testid="switch-advanced"]').click()
|
||||
await page.locator('[data-testid="field-views"]').fill(options.views.toString())
|
||||
} else if (options.expiration) {
|
||||
await page.locator('[data-testid="switch-advanced"]').click()
|
||||
await page.locator('[data-testid="switch-advanced-toggle"]').click()
|
||||
await page.locator('[data-testid="field-expiration"]').fill(options.expiration.toString())
|
||||
}
|
||||
|
||||
await page.locator('button:has-text("create")').click()
|
||||
|
||||
if (options.error) {
|
||||
await expect(page.locator('.error-text')).toContainText(options.error, { timeout: 60_000 })
|
||||
}
|
||||
|
||||
const shareLink = await page.locator('[data-testid="share-link"]').inputValue()
|
||||
return shareLink
|
||||
}
|
||||
|
||||
export async function checkLinkForDownload(page: Page, link: string, text: string, checksum: string) {
|
||||
await page.goto('/')
|
||||
await page.goto(link)
|
||||
await page.locator('[data-testid="show-note-button"]').click()
|
||||
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.locator(`[data-testid="result"] >> text=${text}`).click(),
|
||||
])
|
||||
const path = await download.path()
|
||||
if (!path) throw new Error('Download failed')
|
||||
const cs = await getFileChecksum(path)
|
||||
await expect(cs).toBe(checksum)
|
||||
}
|
||||
export async function checkLinkForText(page: Page, link: string, text: string) {
|
||||
await page.goto('/')
|
||||
await page.goto(link)
|
||||
await page.locator('[data-testid="show-note-button"]').click()
|
||||
await expect(await page.locator('[data-testid="result"] >> .note').innerText()).toContain(text)
|
||||
}
|
||||
|
||||
export 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')
|
||||
}
|
||||
|
||||
export async function getFileChecksum(file: string) {
|
||||
const buffer = await readFile(file)
|
||||
const hash = createHash('sha3-256').update(buffer).digest('hex')
|
||||
return hash
|
||||
}
|
Reference in New Issue
Block a user