Compare commits

..

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

46 changed files with 1988 additions and 1860 deletions

2
.nvmrc
View File

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

View File

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

View File

@ -63,8 +63,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
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](./design/Screens.png)
@ -86,8 +84,8 @@ of the notes even if it tried to.
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
| `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`. |
| `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. |
| `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
> `https` is required otherwise browsers will not support the cryptographic functions.
@ -106,14 +104,9 @@ version: '3.8'
services:
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
# https://redis.io/docs/manual/eviction/
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
app:
image: cupcakearmy/cryptgeon:latest

View File

@ -91,14 +91,9 @@ version: '3.8'
services:
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
# https://redis.io/docs/manual/eviction/
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
app:
image: cupcakearmy/cryptgeon:latest

View File

@ -74,14 +74,6 @@ version: '3.8'
services:
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
app:
image: cupcakearmy/cryptgeon:latest
@ -116,14 +108,7 @@ networks:
services:
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
restart: unless-stopped
app:
image: cupcakearmy/cryptgeon:latest

View File

@ -4,14 +4,6 @@
services:
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
ports:
- 6379:6379

View File

@ -1,14 +1,9 @@
services:
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
# https://redis.io/docs/manual/eviction/
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
app:
image: cupcakearmy/cryptgeon:latest

View File

@ -3,20 +3,12 @@ version: '3.8'
services:
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
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- redis
proxy:
image: nginx:alpine
depends_on:

View File

@ -109,14 +109,7 @@ networks:
services:
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
restart: unless-stopped
app:
image: cupcakearmy/cryptgeon:latest

View File

@ -18,14 +18,7 @@ networks:
services:
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
restart: unless-stopped
app:
image: cupcakearmy/cryptgeon:latest
@ -62,14 +55,6 @@ services:
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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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::{
extract::{DefaultBodyLimit, Request},
routing::{delete, get, post},
Router, ServiceExt,
};
use dotenv::dotenv;
use lock::SharedState;
use tokio::sync::Mutex;
use tower::Layer;
use tower_http::{
compression::CompressionLayer,
@ -19,9 +15,7 @@ use tower_http::{
extern crate lazy_static;
mod config;
mod csp;
mod health;
mod lock;
mod note;
mod status;
mod store;
@ -30,13 +24,9 @@ mod store;
async fn main() {
dotenv().ok();
let shared_state = SharedState {
locks: Arc::new(Mutex::new(HashMap::new())),
};
if !store::can_reach_redis() {
println!("cannot reach redis");
panic!("cannot reach redis");
panic!("canont reach redis");
}
let notes_routes = Router::new()
@ -56,8 +46,6 @@ async fn main() {
let app = Router::new()
.nest("/api", api_routes)
.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(
CompressionLayer::new()
@ -65,8 +53,7 @@ async fn main() {
.deflate(true)
.gzip(true)
.zstd(true),
)
.with_state(shared_state);
);
let app = NormalizePathLayer::trim_trailing_slash().layer(app);

View File

@ -5,12 +5,11 @@ use axum::{
Json,
};
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::SystemTime};
use tokio::sync::Mutex;
use std::time::SystemTime;
use crate::config;
use crate::note::{generate_id, Note, NoteInfo};
use crate::store;
use crate::{config, lock::SharedState};
use super::NotePublic;
@ -81,20 +80,11 @@ pub async fn create(Json(mut n): Json<Note>) -> Response {
}
}
pub async fn delete(
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;
pub async fn delete(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
let note = store::get(&id);
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(),
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Ok(Some(note)) => {

View File

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

View File

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

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

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

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

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

Before

Width:  |  Height:  |  Size: 761 B

After

Width:  |  Height:  |  Size: 732 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<script lang="ts" module>
<script lang="ts" context="module">
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
function saveAs(file: File) {
@ -22,14 +22,20 @@
import { copy } from '$lib/utils'
import type { FileDTO, NotePublic } from 'cryptgeon/shared'
interface Props {
note: DecryptedNote
}
let { note }: Props = $props()
export let note: DecryptedNote
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) {
const f = new File([file.contents], file.name, {
@ -38,17 +44,7 @@
saveAs(f)
}
$effect(() => {
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) : [])
$: links = typeof note.contents === 'string' ? note.contents.match(RE_URL) : []
</script>
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
@ -57,7 +53,7 @@
<div class="note">
{note.contents}
</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}
<div class="links">
@ -74,13 +70,13 @@
{:else}
{#each files as file}
<div class="note file">
<button onclick={() => downloadFile(file)}>
<button on:click={() => downloadFile(file)}>
<b>{file.name}</b>
</button>
<small> {file.type} {prettyBytes(file.size)}</small>
</div>
{/each}
<Button onclick={download}>{$t('show.download_all')}</Button>
<Button on:click={download}>{$t('show.download_all')}</Button>
{/if}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,18 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { get } from 'svelte/store';
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')
window.location = config.imprint_url;
}
else if (config.imprint_html == "") {
goto("/about");
}
}
})
});
</script>
<svelte:head>
@ -18,9 +20,9 @@
</svelte:head>
<section class="content">
{#if $status?.imprint_html}
{@html $status.imprint_html}
{/if}
{#if $status?.imprint_html}
{@html $status.imprint_html}
{/if}
</section>
<style>

View File

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

View File

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