Compare commits

..

No commits in common. "main" and "v2.8.1" have entirely different histories.
main ... v2.8.1

51 changed files with 1977 additions and 2010 deletions

2
.nvmrc
View File

@ -1 +1 @@
v22.14.0 v22.7.0

View File

@ -11,7 +11,7 @@ RUN pnpm run build
# BACKEND # BACKEND
FROM rust:1.85-alpine as backend FROM rust:1.80-alpine as backend
WORKDIR /tmp WORKDIR /tmp
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
COPY ./packages/backend ./ COPY ./packages/backend ./

View File

@ -12,7 +12,6 @@
<br/><br/> <br/><br/>
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a> <a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a>
<a href=""><img src="./.github/lokalise.png" height="50"> <a href=""><img src="./.github/lokalise.png" height="50">
<a title="Install cryptgeon Raycast Extension" href="https://www.raycast.com/cupcakearmy/cryptgeon"><img src="https://www.raycast.com/cupcakearmy/cryptgeon/install_button@2x.png?v=1.1" height="64" alt="" style="height: 64px;"></a>
<br/><br/> <br/><br/>
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md) EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
@ -40,12 +39,6 @@ npx cryptgeon send text "This is a secret note"
For more documentation about the CLI see the [readme](./packages/cli/README.md). For more documentation about the CLI see the [readme](./packages/cli/README.md).
### Raycast Extension
There is an [official Raycast extension](https://www.raycast.com/cupcakearmy/cryptgeon).
<a title="Install cryptgeon Raycast Extension" href="https://www.raycast.com/cupcakearmy/cryptgeon"><img src="https://www.raycast.com/cupcakearmy/cryptgeon/install_button@2x.png?v=1.1" height="64" alt="" style="height: 64px;"></a>
## Features ## Features
- send text or files - send text or files
@ -63,8 +56,6 @@ client side with the <code>key</code> and then sent to the server. data is store
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents never persisted to disk. the server never sees the encryption key and cannot decrypt the contents
of the notes even if it tried to. of the notes even if it tried to.
> View counts are guaranteed with one running instance of cryptgeon. Multiple instances connected to the same Redis instance can run into race conditions, where a note might be retrieved more than the view count allows.
## Screenshot ## Screenshot
![screenshot](./design/Screens.png) ![screenshot](./design/Screens.png)
@ -86,8 +77,7 @@ of the notes even if it tried to.
| `THEME_PAGE_TITLE` | `""` | Custom text the page title | | `THEME_PAGE_TITLE` | `""` | Custom text the page title |
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable | | `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. | | `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. |
## Deployment ## Deployment
> `https` is required otherwise browsers will not support the cryptographic functions. > `https` is required otherwise browsers will not support the cryptographic functions.
@ -106,14 +96,9 @@ version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise. # Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/ # https://redis.io/docs/manual/eviction/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine # command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest

View File

@ -91,14 +91,9 @@ version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise. # Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/ # https://redis.io/docs/manual/eviction/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine # command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest

View File

@ -74,14 +74,6 @@ version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
@ -116,14 +108,7 @@ networks:
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only. restart: unless-stopped
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest

View File

@ -4,14 +4,6 @@
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
ports: ports:
- 6379:6379 - 6379:6379

View File

@ -1,14 +1,9 @@
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise. # Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/ # https://redis.io/docs/manual/eviction/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine # command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest

View File

@ -3,20 +3,12 @@ version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
depends_on: depends_on:
- redis - redis
proxy: proxy:
image: nginx:alpine image: nginx:alpine
depends_on: depends_on:

View File

@ -109,14 +109,7 @@ networks:
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only. restart: unless-stopped
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest

View File

@ -2,7 +2,7 @@
Assumptions: Assumptions:
- Traefik 2/3 installed. - Traefik 2 installed.
- External proxy docker network `proxy`. - External proxy docker network `proxy`.
- A certificate resolver `le`. - A certificate resolver `le`.
- A https entrypoint `secure`. - A https entrypoint `secure`.
@ -18,14 +18,7 @@ networks:
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# This is required to stay in RAM only. restart: unless-stopped
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
@ -41,51 +34,3 @@ services:
- traefik.http.routers.cryptgeon.entrypoints=secure - traefik.http.routers.cryptgeon.entrypoints=secure
- traefik.http.routers.cryptgeon.tls.certresolver=le - traefik.http.routers.cryptgeon.tls.certresolver=le
``` ```
## With basic auth
Some times it's useful to hide the service behind auth. This is easily achieved with traefik middleware. Many reverse proxies support similar features, so while traefik is used in this example, other reverse proxies can do the same.
```yaml
services:
traefik:
image: traefik:v3.0
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
cryptgeon:
image: cupcakearmy/cryptgeon
depends_on:
- redis
labels:
- "traefik.enable=true"
- "traefik.http.routers.cryptgeon.rule=Host(`cryptgeon.localhost`)"
- "traefik.http.routers.cryptgeon.entrypoints=web"
- "traefik.http.routers.cryptgeon.middlewares=cryptgeon-auth"
- "traefik.http.middlewares.cryptgeon-auth.basicauth.users=user:$$2y$$05$$juUw0zgc5ebvJ00MFPVVLujF6P.rcEMbGZ99Jfq6ZWEa1dgetacEq"
```
```bash
docker compose up -d
```
1. Open http://cryptgeon.localhost
2. Log in with `user` and `secret`

View File

@ -17,5 +17,5 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"shelljs": "^0.8.5" "shelljs": "^0.8.5"
}, },
"packageManager": "pnpm@10.3.0" "packageManager": "pnpm@9.9.0"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "2.9.1" version = "2.8.1"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021" edition = "2021"
rust-version = "1.85" rust-version = "1.80"
[[bin]] [[bin]]
name = "cryptgeon" name = "cryptgeon"
@ -21,7 +21,7 @@ redis = { version = "0.25.2", features = ["tls-native-tls"] }
# Utility # Utility
serde_json = "1" serde_json = "1"
lazy_static = "1" lazy_static = "1"
ring = "0.17" ring = "0.16"
bs62 = "0.1" bs62 = "0.1"
byte-unit = "4" byte-unit = "4"
dotenv = "0.15" dotenv = "0.15"

View File

@ -38,14 +38,6 @@ pub static ref ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
.unwrap_or("true".to_string()) .unwrap_or("true".to_string())
.parse() .parse()
.unwrap(); .unwrap();
pub static ref IMPRINT_URL: String = std::env::var("IMPRINT_URL")
.unwrap_or("".to_string())
.parse()
.unwrap();
pub static ref IMPRINT_HTML: String = std::env::var("IMPRINT_HTML")
.unwrap_or("".to_string())
.parse()
.unwrap();
} }
// THEME // THEME

View File

@ -1,16 +0,0 @@
use axum::{body::Body, extract::Request, http::HeaderValue, middleware::Next, response::Response};
const CUSTOM_HEADER_NAME: &str = "Content-Security-Policy";
const CUSTOM_HEADER_VALUE: &str = "default-src 'self'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; object-src 'none'; base-uri 'self'; connect-src 'self' data:; font-src 'self'; frame-src 'self'; img-src 'self'; manifest-src 'self'; media-src 'self'; worker-src 'none';";
lazy_static! {
static ref HEADER_VALUE: HeaderValue = HeaderValue::from_static(CUSTOM_HEADER_VALUE);
}
pub async fn add_csp_header(request: Request<Body>, next: Next) -> Response {
let mut response = next.run(request).await;
response
.headers_mut()
.append(CUSTOM_HEADER_NAME, HEADER_VALUE.clone());
response
}

View File

@ -1,10 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Clone)]
pub struct SharedState {
pub locks: LockMap,
}
pub type LockMap = Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>;

View File

@ -1,13 +1,9 @@
use std::{collections::HashMap, sync::Arc};
use axum::{ use axum::{
extract::{DefaultBodyLimit, Request}, extract::{DefaultBodyLimit, Request},
routing::{delete, get, post}, routing::{delete, get, post},
Router, ServiceExt, Router, ServiceExt,
}; };
use dotenv::dotenv; use dotenv::dotenv;
use lock::SharedState;
use tokio::sync::Mutex;
use tower::Layer; use tower::Layer;
use tower_http::{ use tower_http::{
compression::CompressionLayer, compression::CompressionLayer,
@ -19,9 +15,7 @@ use tower_http::{
extern crate lazy_static; extern crate lazy_static;
mod config; mod config;
mod csp;
mod health; mod health;
mod lock;
mod note; mod note;
mod status; mod status;
mod store; mod store;
@ -30,13 +24,9 @@ mod store;
async fn main() { async fn main() {
dotenv().ok(); dotenv().ok();
let shared_state = SharedState {
locks: Arc::new(Mutex::new(HashMap::new())),
};
if !store::can_reach_redis() { if !store::can_reach_redis() {
println!("cannot reach redis"); println!("cannot reach redis");
panic!("cannot reach redis"); panic!("canont reach redis");
} }
let notes_routes = Router::new() let notes_routes = Router::new()
@ -56,8 +46,6 @@ async fn main() {
let app = Router::new() let app = Router::new()
.nest("/api", api_routes) .nest("/api", api_routes)
.fallback_service(serve_dir) .fallback_service(serve_dir)
// Disabled for now, as svelte inlines scripts
// .layer(middleware::from_fn(csp::add_csp_header))
.layer(DefaultBodyLimit::max(*config::LIMIT)) .layer(DefaultBodyLimit::max(*config::LIMIT))
.layer( .layer(
CompressionLayer::new() CompressionLayer::new()
@ -65,8 +53,7 @@ async fn main() {
.deflate(true) .deflate(true)
.gzip(true) .gzip(true)
.zstd(true), .zstd(true),
) );
.with_state(shared_state);
let app = NormalizePathLayer::trim_trailing_slash().layer(app); let app = NormalizePathLayer::trim_trailing_slash().layer(app);

View File

@ -5,12 +5,11 @@ use axum::{
Json, Json,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::SystemTime}; use std::time::SystemTime;
use tokio::sync::Mutex;
use crate::config;
use crate::note::{generate_id, Note, NoteInfo}; use crate::note::{generate_id, Note, NoteInfo};
use crate::store; use crate::store;
use crate::{config, lock::SharedState};
use super::NotePublic; use super::NotePublic;
@ -81,20 +80,11 @@ pub async fn create(Json(mut n): Json<Note>) -> Response {
} }
} }
pub async fn delete( pub async fn delete(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
Path(OneNoteParams { id }): Path<OneNoteParams>,
state: axum::extract::State<SharedState>,
) -> Response {
let mut locks_map = state.locks.lock().await;
let lock = locks_map
.entry(id.clone())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
drop(locks_map);
let _guard = lock.lock().await;
let note = store::get(&id); let note = store::get(&id);
match note { match note {
// Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
// Ok(None) => return HttpResponse::NotFound().finish(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Ok(None) => (StatusCode::NOT_FOUND).into_response(), Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Ok(Some(note)) => { Ok(Some(note)) => {

View File

@ -12,8 +12,6 @@ pub struct Status {
pub max_expiration: u32, pub max_expiration: u32,
pub allow_advanced: bool, pub allow_advanced: bool,
pub allow_files: bool, pub allow_files: bool,
pub imprint_url: String,
pub imprint_html: String,
// Theme // Theme
pub theme_image: String, pub theme_image: String,
pub theme_text: String, pub theme_text: String,
@ -30,8 +28,6 @@ pub async fn get_status() -> (StatusCode, Json<Status>) {
max_expiration: *config::MAX_EXPIRATION, max_expiration: *config::MAX_EXPIRATION,
allow_advanced: *config::ALLOW_ADVANCED, allow_advanced: *config::ALLOW_ADVANCED,
allow_files: *config::ALLOW_FILES, allow_files: *config::ALLOW_FILES,
imprint_url: config::IMPRINT_URL.to_string(),
imprint_html: config::IMPRINT_HTML.to_string(),
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE, theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
theme_image: config::THEME_IMAGE.to_string(), theme_image: config::THEME_IMAGE.to_string(),
theme_text: config::THEME_TEXT.to_string(), theme_text: config::THEME_TEXT.to_string(),

View File

@ -1,6 +1,6 @@
{ {
"name": "cryptgeon", "name": "cryptgeon",
"version": "2.9.1", "version": "2.8.1",
"homepage": "https://github.com/cupcakearmy/cryptgeon", "homepage": "https://github.com/cupcakearmy/cryptgeon",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -113,8 +113,6 @@ export type Status = {
max_expiration: number max_expiration: number
allow_advanced: boolean allow_advanced: boolean
allow_files: boolean allow_files: boolean
imprint_url: string
imprint_html: string
theme_image: string theme_image: string
theme_text: string theme_text: string
theme_favicon: string theme_favicon: string

View File

@ -1,58 +0,0 @@
{
"common": {
"note": "筆記",
"file": "檔案",
"advanced": "進階",
"create": "創建",
"loading": "載入中",
"mode": "模式",
"views": "{n, plural, =0 {瀏覽次數} =1 {1 次瀏覽} other {# 次瀏覽}}",
"minutes": "{n, plural, =0 {分鐘} =1 {1 分鐘} other {# 分鐘}}",
"max": "最大",
"share_link": "分享連結",
"copy_clipboard": "複製到剪貼板",
"copied_to_clipboard": "已複製到剪貼板",
"encrypting": "加密中",
"decrypting": "解密中",
"uploading": "上傳中",
"downloading": "下載中",
"qr_code": "QR 碼",
"password": "密碼"
},
"home": {
"intro": "輕鬆地以一鍵傳送<i>完全加密</i>的安全筆記或檔案。只需創建筆記並分享連結。",
"explanation": "筆記將在 {type} 後過期並被銷毀。",
"new_note": "新筆記",
"new_note_notice": "<b>可用性:</b><br />筆記不保證被儲存,因為所有內容都保留在 RAM 中,如果 RAM 填滿,最舊的筆記將被移除。<br />(您可能會沒事,只是提醒一下。)",
"errors": {
"note_to_big": "無法創建筆記。筆記過大",
"note_error": "無法創建筆記。請再試一次。",
"max": "最大值:{n}",
"empty_content": "筆記內容為空。"
},
"messages": {
"note_created": "筆記已創建。"
},
"advanced": {
"explanation": "預設情況下,每個筆記都會使用安全生成的密碼。您也可以選擇自己的密碼,該密碼不會包含在連結中。",
"custom_password": "自定義密碼"
}
},
"show": {
"errors": {
"not_found": "筆記未找到或已被刪除。",
"decryption_failed": "密碼錯誤。無法解密。可能是連結已損壞。筆記已被銷毀。",
"unsupported_type": "不支持的筆記類型。"
},
"explanation": "如果計數器達到限制,請點擊下方以顯示並刪除筆記",
"show_note": "顯示筆記",
"warning_will_not_see_again": "您將<b>無法</b>再次查看筆記。",
"download_all": "全部下載",
"links_found": "在筆記中找到的連結:"
},
"file_upload": {
"selected_files": "已選擇的檔案",
"no_files_selected": "未選擇檔案",
"clear": "重置"
}
}

View File

@ -13,23 +13,23 @@
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@lokalise/node-api": "^13.2.1", "@lokalise/node-api": "^12.1.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.17.3", "@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@zerodevx/svelte-toast": "^0.9.6", "@zerodevx/svelte-toast": "^0.9.5",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.10",
"dotenv": "^16.4.7", "dotenv": "^16.4.5",
"svelte": "^5.20.5", "svelte": "^4.2.12",
"svelte-check": "^4.1.4", "svelte-check": "^3.6.6",
"svelte-intl-precompile": "^0.12.3", "svelte-intl-precompile": "^0.12.3",
"tslib": "^2.8.1", "tslib": "^2.6.2",
"typescript": "^5.7.3", "typescript": "^5.3.3",
"vite": "^6.2.0" "vite": "^5.1.7"
}, },
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^5.1.1",
"cryptgeon": "workspace:*", "cryptgeon": "workspace:*",
"@fontsource/fira-mono": "^5.0.8",
"occulto": "^2.0.6", "occulto": "^2.0.6",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"qrious": "^4.0.2" "qrious": "^4.0.2"

View File

@ -1,5 +1,3 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Contrast</title><path ><title>Contrast</title><path
d="M256 32C132.29 32 32 132.29 32 256s100.29 224 224 224 224-100.29 224-224S379.71 32 256 32zM128.72 383.28A180 180 0 01256 76v360a178.82 178.82 0 01-127.28-52.72z" d="M256 32C132.29 32 32 132.29 32 256s100.29 224 224 224 224-100.29 224-224S379.71 32 256 32zM128.72 383.28A180 180 0 01256 76v360a178.82 178.82 0 01-127.28-52.72z"

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 287 B

View File

@ -1,5 +1,3 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Copy</title><path ><title>Copy</title><path
d="M456 480H136a24 24 0 01-24-24V128a16 16 0 0116-16h328a24 24 0 0124 24v320a24 24 0 01-24 24z" d="M456 480H136a24 24 0 01-24-24V128a16 16 0 0116-16h328a24 24 0 0124 24v320a24 24 0 01-24 24z"

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 325 B

View File

@ -1,5 +1,3 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Dice</title><path ><title>Dice</title><path
d="M48 366.92L240 480V284L48 170zM192 288c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zm-96 32c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zM272 284v196l192-113.08V170zm48 140c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm96 32c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm32 77.64zM256 32L64 144l192 112 192-112zm0 120c-13.25 0-24-7.16-24-16s10.75-16 24-16 24 7.16 24 16-10.75 16-24 16z" d="M48 366.92L240 480V284L48 170zM192 288c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zm-96 32c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zM272 284v196l192-113.08V170zm48 140c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm96 32c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm32 77.64zM256 32L64 144l192 112 192-112zm0 120c-13.25 0-24-7.16-24-16s10.75-16 24-16 24 7.16 24 16-10.75 16-24 16z"

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 736 B

View File

@ -1,5 +1,3 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Eye</title><circle cx="256" cy="256" r="64" /><path ><title>Eye</title><circle cx="256" cy="256" r="64" /><path
d="M394.82 141.18C351.1 111.2 304.31 96 255.76 96c-43.69 0-86.28 13-126.59 38.48C88.52 160.23 48.67 207 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416c49 0 95.85-15.07 139.3-44.79C433.31 345 469.71 299.82 496 256c-26.38-43.43-62.9-88.56-101.18-114.82zM256 352a96 96 0 1196-96 96.11 96.11 0 01-96 96z" d="M394.82 141.18C351.1 111.2 304.31 96 255.76 96c-43.69 0-86.28 13-126.59 38.48C88.52 160.23 48.67 207 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416c49 0 95.85-15.07 139.3-44.79C433.31 345 469.71 299.82 496 256c-26.38-43.43-62.9-88.56-101.18-114.82zM256 352a96 96 0 1196-96 96.11 96.11 0 01-96 96z"

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 483 B

View File

@ -1,5 +1,3 @@
<script lang="ts"></script>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Eye Off</title><path ><title>Eye Off</title><path
d="M63.998 86.004l21.998-21.998L448 426.01l-21.998 21.998zM259.34 192.09l60.57 60.57a64.07 64.07 0 00-60.57-60.57zM252.66 319.91l-60.57-60.57a64.07 64.07 0 0060.57 60.57z" d="M63.998 86.004l21.998-21.998L448 426.01l-21.998 21.998zM259.34 192.09l60.57 60.57a64.07 64.07 0 00-60.57-60.57zM252.66 319.91l-60.57-60.57a64.07 64.07 0 0060.57 60.57z"

Before

Width:  |  Height:  |  Size: 761 B

After

Width:  |  Height:  |  Size: 732 B

View File

@ -1,17 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte' export let title: string
interface Props {
title: string
children?: Snippet
}
let { title, children }: Props = $props()
</script> </script>
<p> <p>
<b>{title}</b> <b>{title}</b>
{@render children?.()} <slot />
</p> </p>
<style> <style>

View File

@ -6,23 +6,13 @@
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import type { Note } from 'cryptgeon/shared' import type { Note } from 'cryptgeon/shared'
interface Props { export let note: Note
note: Note export let timeExpiration = false
timeExpiration?: boolean export let customPassword: string | null = null
customPassword?: string | null
}
let { let hasCustomPassword = false
note = $bindable(),
timeExpiration = $bindable(false),
customPassword = $bindable(null),
}: Props = $props()
let hasCustomPassword = $state(false) $: if (!hasCustomPassword) customPassword = null
$effect(() => {
if (!hasCustomPassword) customPassword = null
})
</script> </script>
<div class="flex col"> <div class="flex col">

View File

@ -1,14 +1,4 @@
<script lang="ts"> <button {...$$restProps} on:click><slot /></button>
import type { HTMLButtonAttributes } from 'svelte/elements'
interface Props {
children?: import('svelte').Snippet
}
let { children, ...rest }: HTMLButtonAttributes & Props = $props()
</script>
<button {...rest}>{@render children?.()}</button>
<style> <style>
button { button {

View File

@ -5,15 +5,11 @@
import { getCSSVariable } from '$lib/utils' import { getCSSVariable } from '$lib/utils'
interface Props { export let value: string
value: string
}
let { value }: Props = $props() let canvas: HTMLCanvasElement
let canvas: HTMLCanvasElement | null = $state(null) $: {
$effect(() => {
new QR({ new QR({
value, value,
level: 'Q', level: 'Q',
@ -22,12 +18,12 @@
foreground: getCSSVariable('--ui-text-0'), foreground: getCSSVariable('--ui-text-0'),
element: canvas, element: canvas,
}) })
}) }
</script> </script>
<small>{$t('common.qr_code')}</small> <small>{$t('common.qr_code')}</small>
<div> <div>
<canvas bind:this={canvas}></canvas> <canvas bind:this={canvas} />
</div> </div>
<style> <style>

View File

@ -5,13 +5,8 @@
import MaxSize from '$lib/ui/MaxSize.svelte' import MaxSize from '$lib/ui/MaxSize.svelte'
import type { FileDTO } from 'cryptgeon/shared' import type { FileDTO } from 'cryptgeon/shared'
interface Props { export let label: string = ''
label?: string export let files: FileDTO[] = []
files?: FileDTO[]
[key: string]: any
}
let { label = '', files = $bindable([]), ...rest }: Props = $props()
async function fileToDTO(file: File): Promise<FileDTO> { async function fileToDTO(file: File): Promise<FileDTO> {
return { return {
@ -40,7 +35,7 @@
<small> <small>
{label} {label}
</small> </small>
<input {...rest} type="file" onchange={onInput} multiple /> <input {...$$restProps} type="file" on:change={onInput} multiple />
<div class="box"> <div class="box">
{#if files.length} {#if files.length}
<div> <div>
@ -50,8 +45,8 @@
{file.name} {file.name}
</div> </div>
{/each} {/each}
<div class="spacer"></div> <div class="spacer" />
<Button onclick={clear}>{$t('file_upload.clear')}</Button> <Button on:click={clear}>{$t('file_upload.clear')}</Button>
</div> </div>
{:else} {:else}
<div> <div>

View File

@ -1,10 +1,9 @@
<script lang="ts" module> <script lang="ts" context="module">
import IconContrast from '$lib/icons/IconContrast.svelte' import IconContrast from '$lib/icons/IconContrast.svelte'
import IconCopy from '$lib/icons/IconCopy.svelte' import IconCopy from '$lib/icons/IconCopy.svelte'
import IconDice from '$lib/icons/IconDice.svelte' import IconDice from '$lib/icons/IconDice.svelte'
import IconEye from '$lib/icons/IconEye.svelte' import IconEye from '$lib/icons/IconEye.svelte'
import IconEyeOff from '$lib/icons/IconEyeOff.svelte' import IconEyeOff from '$lib/icons/IconEyeOff.svelte'
import type { HTMLButtonAttributes } from 'svelte/elements'
const map = { const map = {
contrast: IconContrast, contrast: IconContrast,
@ -16,17 +15,12 @@
</script> </script>
<script lang="ts"> <script lang="ts">
interface Props { export let icon: keyof typeof map
icon: keyof typeof map
}
let { icon, ...rest }: HTMLButtonAttributes & Props = $props()
</script> </script>
<button type="button" {...rest}> <button on:click {...$$restProps}>
{#if map[icon]} {#if map[icon]}
{@const SvelteComponent = map[icon]} <svelte:component this={map[icon]} />
<SvelteComponent />
{/if} {/if}
</button> </button>

View File

@ -1,5 +1,3 @@
<script lang="ts"></script>
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 784 B

View File

@ -1,4 +1,4 @@
<script lang="ts" module> <script lang="ts" context="module">
export type NoteResult = { export type NoteResult = {
id: string id: string
password?: string password?: string
@ -12,13 +12,9 @@
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import Canvas from './Canvas.svelte' import Canvas from './Canvas.svelte'
interface Props { export let result: NoteResult
result: NoteResult
}
let { result }: Props = $props() let url = `${window.location.origin}/note/${result.id}`
let url = $state(`${window.location.origin}/note/${result.id}`)
if (result.password) url += `#${result.password}` if (result.password) url += `#${result.password}`
function reset() { function reset() {
@ -45,7 +41,7 @@
</p> </p>
{/if} {/if}
<br /> <br />
<Button onclick={reset}>{$t('home.new_note')}</Button> <Button on:click={reset}>{$t('home.new_note')}</Button>
<style> <style>
div { div {

View File

@ -1,4 +1,4 @@
<script lang="ts" module> <script lang="ts" context="module">
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any } export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
function saveAs(file: File) { function saveAs(file: File) {
@ -22,14 +22,20 @@
import { copy } from '$lib/utils' import { copy } from '$lib/utils'
import type { FileDTO, NotePublic } from 'cryptgeon/shared' import type { FileDTO, NotePublic } from 'cryptgeon/shared'
interface Props { export let note: DecryptedNote
note: DecryptedNote
}
let { note }: Props = $props()
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
let files: FileDTO[] = $state([]) let files: FileDTO[] = []
$: if (note.meta.type === 'file') {
files = note.contents
}
$: download = () => {
for (const file of files) {
downloadFile(file)
}
}
async function downloadFile(file: FileDTO) { async function downloadFile(file: FileDTO) {
const f = new File([file.contents], file.name, { const f = new File([file.contents], file.name, {
@ -38,17 +44,7 @@
saveAs(f) saveAs(f)
} }
$effect(() => { $: links = typeof note.contents === 'string' ? note.contents.match(RE_URL) : []
if (note.meta.type === 'file') {
files = note.contents
}
})
let download = $derived(() => {
for (const file of files) {
downloadFile(file)
}
})
let links = $derived(typeof note.contents === 'string' ? note.contents.match(RE_URL) : [])
</script> </script>
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p> <p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
@ -57,7 +53,7 @@
<div class="note"> <div class="note">
{note.contents} {note.contents}
</div> </div>
<Button onclick={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button> <Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
{#if links && links.length} {#if links && links.length}
<div class="links"> <div class="links">
@ -74,13 +70,13 @@
{:else} {:else}
{#each files as file} {#each files as file}
<div class="note file"> <div class="note file">
<button onclick={() => downloadFile(file)}> <button on:click={() => downloadFile(file)}>
<b>{file.name}</b> <b>{file.name}</b>
</button> </button>
<small> {file.type} {prettyBytes(file.size)}</small> <small> {file.type} {prettyBytes(file.size)}</small>
</div> </div>
{/each} {/each}
<Button onclick={download}>{$t('show.download_all')}</Button> <Button on:click={download}>{$t('show.download_all')}</Button>
{/if} {/if}
</div> </div>

View File

@ -1,18 +1,13 @@
<script lang="ts"> <script lang="ts">
interface Props { export let label: string = ''
label?: string export let value: boolean
value: boolean export let color = true
color?: boolean
[key: string]: any
}
let { label = '', value = $bindable(), color = true, ...rest }: Props = $props()
</script> </script>
<label {...rest}> <label {...$$restProps}>
<small>{label}</small> <small>{label}</small>
<input type="checkbox" bind:checked={value} /> <input type="checkbox" bind:checked={value} />
<span class:color class="slider"></span> <span class:color class="slider" />
</label> </label>
<style> <style>

View File

@ -1,16 +1,11 @@
<script lang="ts"> <script lang="ts">
interface Props { export let label: string = ''
label?: string export let value: string
value: string
[key: string]: any
}
let { label = '', value = $bindable(), ...rest }: Props = $props()
</script> </script>
<label> <label>
<small> <small>
{label} {label}
</small> </small>
<textarea class="box" {...rest} bind:value></textarea> <textarea class="box" {...$$restProps} bind:value />
</label> </label>

View File

@ -2,34 +2,25 @@
import Icon from '$lib/ui/Icon.svelte' import Icon from '$lib/ui/Icon.svelte'
import { copy as copyFN } from '$lib/utils' import { copy as copyFN } from '$lib/utils'
import { getRandomBytes, Hex } from 'occulto' import { getRandomBytes, Hex } from 'occulto'
import type { HTMLInputAttributes } from 'svelte/elements'
interface Props { export let label: string = ''
label?: string export let value: any
value: any export let validate: (value: any) => boolean | string = () => true
validate?: (value: any) => boolean | string export let copy: boolean = false
copy?: boolean export let random: boolean = false
random?: boolean
const initialType = $$restProps.type
const isPassword = initialType === 'password'
let hidden = true
$: valid = validate(value)
$: if (isPassword) {
value
$$restProps.type = hidden ? initialType : 'text'
} }
let {
label = '',
value = $bindable(),
validate = () => true,
copy = false,
random = false,
...rest
}: HTMLInputAttributes & Props = $props()
const initialType = $state(rest.type)
const isPassword = initialType === 'password'
let hidden = $state(true)
let valid = $derived(validate(value))
let type = $derived(isPassword ? (hidden ? 'password' : 'text') : rest.type)
function toggle() { function toggle() {
console.debug('toggle')
hidden = !hidden hidden = !hidden
} }
@ -39,31 +30,31 @@
</script> </script>
<label> <label>
<small class:disabled={rest.disabled}> <small class:disabled={$$restProps.disabled}>
{label} {label}
{#if valid !== true} {#if valid !== true}
<span class="error-text">{valid}</span> <span class="error-text">{valid}</span>
{/if} {/if}
</small> </small>
<input bind:value {...rest} {type} autocomplete="off" class:valid={valid === true} /> <input bind:value {...$$restProps} class:valid={valid === true} />
<div class="icons"> <div class="icons">
{#if isPassword} {#if isPassword}
<Icon <Icon
disabled={rest.disabled} disabled={$$restProps.disabled}
class="icon" class="icon"
icon={hidden ? 'eye' : 'eye-off'} icon={hidden ? 'eye' : 'eye-off'}
onclick={toggle} on:click={toggle}
/> />
{/if} {/if}
{#if random} {#if random}
<Icon disabled={rest.disabled} class="icon" icon="dice" onclick={randomFN} /> <Icon disabled={$$restProps.disabled} class="icon" icon="dice" on:click={randomFN} />
{/if} {/if}
{#if copy} {#if copy}
<Icon <Icon
disabled={rest.disabled} disabled={$$restProps.disabled}
class="icon" class="icon"
icon="copy" icon="copy"
onclick={() => copyFN(value.toString())} on:click={() => copyFN(value.toString())}
/> />
{/if} {/if}
</div> </div>

View File

@ -1,21 +1,24 @@
<script lang="ts" module> <script lang="ts" context="module">
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
const themes = ['dark', 'light', 'auto'] as const enum Theme {
type Theme = (typeof themes)[number] Dark = 'dark',
Light = 'light',
Auto = 'auto',
}
const NextTheme: Record<Theme, Theme> = { const NextTheme = {
auto: 'light', [Theme.Auto]: Theme.Light,
light: 'dark', [Theme.Light]: Theme.Dark,
dark: 'auto', [Theme.Dark]: Theme.Auto,
} }
function init(): Theme { function init(): Theme {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const saved = window.localStorage.getItem('theme') as Theme const saved = window.localStorage.getItem('theme') as Theme
if (themes.includes(saved)) return saved if (Object.values(Theme).includes(saved)) return saved
} }
return 'auto' return Theme.Auto
} }
export const theme = writable<Theme>(init()) export const theme = writable<Theme>(init())
@ -37,7 +40,7 @@
} }
</script> </script>
<button onclick={change}> <button on:click={change}>
<Icon class="icon" icon="contrast" /> <Icon class="icon" icon="contrast" />
{$theme} {$theme}
</button> </button>

View File

@ -15,29 +15,27 @@
import TextArea from '$lib/ui/TextArea.svelte' import TextArea from '$lib/ui/TextArea.svelte'
import { Adapters, API, PayloadToLargeError, type FileDTO, type Note } from 'cryptgeon/shared' import { Adapters, API, PayloadToLargeError, type FileDTO, type Note } from 'cryptgeon/shared'
let note: Note = $state({ let note: Note = {
contents: '', contents: '',
meta: { type: 'text' }, meta: { type: 'text' },
views: 1, views: 1,
expiration: 60, expiration: 60,
}) }
let files: FileDTO[] = $state([]) let files: FileDTO[]
let result: NoteResult | null = $state(null) let result: NoteResult | null = null
let advanced = $state(false) let advanced = false
let isFile = $state(false) let isFile = false
let timeExpiration = $state(false) let timeExpiration = false
let customPassword: string | null = $state(null) let customPassword: string | null = null
let description = $state('') let description = ''
let loading: string | null = $state(null) let loading: string | null = null
$effect(() => { $: if (!advanced) {
if (!advanced) { note.views = 1
note.views = 1 timeExpiration = false
timeExpiration = false }
}
})
$effect(() => { $: {
description = $t('home.explanation', { description = $t('home.explanation', {
values: { values: {
type: $t(timeExpiration ? 'common.minutes' : 'common.views', { type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
@ -45,22 +43,17 @@
}), }),
}, },
}) })
}) }
$effect(() => { $: note.meta.type = isFile ? 'file' : 'text'
note.meta.type = isFile ? 'file' : 'text'
})
$effect(() => { $: if (!isFile) {
if (!isFile) { note.contents = ''
note.contents = '' }
}
})
class EmptyContentError extends Error {} class EmptyContentError extends Error {}
async function submit(e: SubmitEvent) { async function submit() {
e.preventDefault()
try { try {
loading = $t('common.encrypting') loading = $t('common.encrypting')
@ -110,7 +103,7 @@
<p> <p>
{@html $status?.theme_text || $t('home.intro')} {@html $status?.theme_text || $t('home.intro')}
</p> </p>
<form onsubmit={submit}> <form on:submit|preventDefault={submit}>
<fieldset disabled={loading !== null}> <fieldset disabled={loading !== null}>
{#if isFile} {#if isFile}
<FileUpload data-testid="file-upload" label={$t('common.file')} bind:files /> <FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
@ -139,7 +132,7 @@
bind:value={advanced} bind:value={advanced}
/> />
{/if} {/if}
<div class="grow"></div> <div class="grow" />
<div class="tr"> <div class="tr">
<small>{$t('common.max')}: <MaxSize /> </small> <small>{$t('common.max')}: <MaxSize /> </small>
<br /> <br />

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import ThemeToggle from '$lib/ui/ThemeToggle.svelte' import ThemeToggle from '$lib/ui/ThemeToggle.svelte'
import { status } from '$lib/stores/status'
</script> </script>
<footer> <footer>
@ -8,11 +7,6 @@
<nav> <nav>
<a href="/">/home</a> <a href="/">/home</a>
<a href="/about">/about</a> <a href="/about">/about</a>
{#if $status?.imprint_url}
<a href={$status.imprint_url} target="_blank" rel="noopener noreferrer">/imprint</a>
{:else if $status?.imprint_html}
<a href="/imprint">/imprint</a>
{/if}
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer"> <a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
code code
</a> </a>

View File

@ -7,7 +7,7 @@
</script> </script>
<header> <header>
<a onclick={reset} href="/"> <a on:click={reset} href="/">
{#if $status?.theme_image} {#if $status?.theme_image}
<img alt="logo" src={$status.theme_image} /> <img alt="logo" src={$status.theme_image} />
{:else} {:else}

View File

@ -8,11 +8,6 @@
import { init as initStores, status } from '$lib/stores/status' import { init as initStores, status } from '$lib/stores/status'
import Footer from '$lib/views/Footer.svelte' import Footer from '$lib/views/Footer.svelte'
import Header from '$lib/views/Header.svelte' import Header from '$lib/views/Header.svelte'
interface Props {
children?: import('svelte').Snippet
}
let { children }: Props = $props()
onMount(() => { onMount(() => {
initStores() initStores()
@ -27,7 +22,7 @@
{#await waitLocale() then _} {#await waitLocale() then _}
<main> <main>
<Header /> <Header />
{@render children?.()} <slot />
</main> </main>
<SvelteToast /> <SvelteToast />

View File

@ -1,33 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { status } from '$lib/stores/status'
status.subscribe((config) => {
if (config != null) {
if (config.imprint_url) {
window.location.href = config.imprint_url
} else if (config.imprint_html == '') {
goto('/about')
}
}
})
</script>
<svelte:head>
<title>Imprint</title>
</svelte:head>
<section class="content">
{#if $status?.imprint_html}
{@html $status.imprint_html}
{/if}
</section>
<style>
section {
width: 100%;
display: flex;
flex-direction: column;
gap: 2rem;
}
</style>

View File

@ -10,22 +10,18 @@
import { Adapters, API, type NoteMeta } from 'cryptgeon/shared' import { Adapters, API, type NoteMeta } from 'cryptgeon/shared'
import type { PageData } from './$types' import type { PageData } from './$types'
interface Props { export let data: PageData
data: PageData
}
let { data }: Props = $props()
let id = data.id let id = data.id
let password: string | null = $state<string | null>(null) let password: string | null = null
let note: DecryptedNote | null = $state(null) let note: DecryptedNote | null = null
let exists = $state(false) let exists = false
let meta: NoteMeta | null = $state(null) let meta: NoteMeta | null = null
let loading: string | null = $state(null) let loading: string | null = null
let error: string | null = $state(null) let error: string | null = null
let valid = $derived(!!password?.length) $: valid = !!password?.length
onMount(async () => { onMount(async () => {
// Check if note exists // Check if note exists
@ -45,8 +41,7 @@
/** /**
* Get the actual contents of the note and decrypt it. * Get the actual contents of the note and decrypt it.
*/ */
async function show(e: SubmitEvent) { async function show() {
e.preventDefault()
try { try {
if (!valid) { if (!valid) {
error = $t('show.errors.no_password') error = $t('show.errors.no_password')
@ -91,7 +86,7 @@
{:else if note && !error} {:else if note && !error}
<ShowNote {note} /> <ShowNote {note} />
{:else} {:else}
<form onsubmit={show}> <form on:submit|preventDefault={show}>
<fieldset> <fieldset>
<p>{$t('show.explanation')}</p> <p>{$t('show.explanation')}</p>
{#if meta?.derivation} {#if meta?.derivation}

View File

@ -1,17 +1,12 @@
import { sveltekit } from '@sveltejs/kit/vite' import { sveltekit } from '@sveltejs/kit/vite'
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin' import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
const port = 3000 const port = 8001
/** @type {import('vite').UserConfig} */ /** @type {import('vite').UserConfig} */
const config = { const config = {
clearScreen: false, clearScreen: false,
server: { server: { port },
port,
proxy: {
'/api': 'http://localhost:8000',
},
},
preview: { port }, preview: { port },
plugins: [sveltekit(), precompileIntl('locales')], plugins: [sveltekit(), precompileIntl('locales')],
} }

View File

@ -0,0 +1,12 @@
{
"private": true,
"name": "@cryptgeon/proxy",
"type": "module",
"main": "./proxy.js",
"scripts": {
"dev": "node ."
},
"dependencies": {
"http-proxy": "^1.18.1"
}
}

16
packages/proxy/proxy.js Normal file
View File

@ -0,0 +1,16 @@
import http from 'http'
import httpProxy from 'http-proxy'
const proxy = httpProxy.createProxyServer()
proxy.on('error', function (err, req, res) {
console.error(err)
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end('500 Internal Server Error')
})
const server = http.createServer(function (req, res) {
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 })
})
server.listen(3000)
console.log('Proxy on http://localhost:3000')

2468
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff