mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2025-09-06 01:10:40 +00:00
move folder
This commit is contained in:
124
frontend/src/app.css
Normal file
124
frontend/src/app.css
Normal file
@@ -0,0 +1,124 @@
|
||||
@import '@fontsource/fira-mono';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Fira Mono', monospace;
|
||||
|
||||
--ui-bg-0: #111;
|
||||
--ui-bg-0-85: #111111d9;
|
||||
--ui-bg-1: #333;
|
||||
--ui-bg-2: #444;
|
||||
--ui-text-0: #fefefe;
|
||||
--ui-text-1: #eee;
|
||||
--ui-clr-primary: hsl(186, 65%, 55%);
|
||||
--ui-clr-error: hsl(357, 77%, 51%);
|
||||
|
||||
--ui-anim: all 150ms ease;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--ui-bg-0: #fefefe;
|
||||
--ui-bg-0-85: #fefefed9;
|
||||
--ui-bg-1: #eee;
|
||||
--ui-bg-2: #e2e2e2;
|
||||
--ui-text-0: #111;
|
||||
--ui-text-1: #333;
|
||||
}
|
||||
}
|
||||
|
||||
:root[theme='dark'] {
|
||||
--ui-bg-0: #111 !important;
|
||||
--ui-bg-0-85: #111111d9 !important;
|
||||
--ui-bg-1: #333 !important;
|
||||
--ui-bg-2: #444 !important;
|
||||
--ui-text-0: #fefefe !important;
|
||||
--ui-text-1: #eee !important;
|
||||
}
|
||||
:root[theme='light'] {
|
||||
--ui-bg-0: #fefefe !important;
|
||||
--ui-bg-0-85: #fefefed9 !important;
|
||||
--ui-bg-1: #eee !important;
|
||||
--ui-bg-2: #e2e2e2 !important;
|
||||
--ui-text-0: #111 !important;
|
||||
--ui-text-1: #333 !important;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: var(--ui-clr-error);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background-color: var(--ui-bg-0);
|
||||
color: var(--ui-text-0);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
box-sizing: content-box;
|
||||
border-bottom: 2px solid var(--ui-bg-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:active {
|
||||
border-color: var(--ui-clr-primary);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
border-color: var(--ui-text-0);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0;
|
||||
transition: var(--ui-anim);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
*:disabled,
|
||||
*[disabled='true'] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 100%;
|
||||
min-height: min(calc(100vh - 30rem), 20rem);
|
||||
margin: 0;
|
||||
border: 2px solid var(--ui-bg-1);
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 30rem) {
|
||||
.box {
|
||||
min-height: calc(100vh - 25rem);
|
||||
}
|
||||
}
|
||||
|
||||
.box:hover,
|
||||
.box:focus {
|
||||
border-color: var(--ui-clr-primary);
|
||||
}
|
||||
|
||||
.tr {
|
||||
text-align: right;
|
||||
}
|
13
frontend/src/app.html
Normal file
13
frontend/src/app.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
%svelte.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id="svelte">%svelte.body%</div>
|
||||
</body>
|
||||
</html>
|
3
frontend/src/global.d.ts
vendored
Normal file
3
frontend/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
74
frontend/src/lib/api.ts
Normal file
74
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
export type NoteMeta = { type: 'text' | 'file' }
|
||||
|
||||
export type Note = {
|
||||
contents: string
|
||||
meta: NoteMeta
|
||||
views?: number
|
||||
expiration?: number
|
||||
}
|
||||
export type NoteInfo = {}
|
||||
export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
||||
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
||||
|
||||
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
||||
contents: string
|
||||
}
|
||||
|
||||
type CallOptions = {
|
||||
url: string
|
||||
method: string
|
||||
body?: any
|
||||
}
|
||||
|
||||
export class PayloadToLargeError extends Error {}
|
||||
|
||||
export async function call(options: CallOptions) {
|
||||
const response = await fetch('/api/' + options.url, {
|
||||
method: options.method,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 413) throw new PayloadToLargeError()
|
||||
else throw new Error('API call failed')
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function create(note: Note) {
|
||||
const { meta, ...rest } = note
|
||||
const body: NoteCreate = {
|
||||
...rest,
|
||||
meta: JSON.stringify(meta),
|
||||
}
|
||||
const data = await call({
|
||||
url: 'notes/',
|
||||
method: 'post',
|
||||
body,
|
||||
})
|
||||
return data as { id: string }
|
||||
}
|
||||
|
||||
export async function get(id: string): Promise<NotePublic> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
const { contents, meta } = data
|
||||
return {
|
||||
contents,
|
||||
meta: JSON.parse(meta) as NoteMeta,
|
||||
}
|
||||
}
|
||||
|
||||
export async function info(id: string): Promise<NoteInfo> {
|
||||
const data = await call({
|
||||
url: `notes/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
return data
|
||||
}
|
71
frontend/src/lib/crypto.ts
Normal file
71
frontend/src/lib/crypto.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export class Hex {
|
||||
static encode(buffer: ArrayBuffer): string {
|
||||
let s = ''
|
||||
for (const i of new Uint8Array(buffer)) {
|
||||
s += i.toString(16).padStart(2, '0')
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
static decode(s: string): ArrayBuffer {
|
||||
const size = s.length / 2
|
||||
const buffer = new Uint8Array(size)
|
||||
for (let i = 0; i < size; i++) {
|
||||
const idx = i * 2
|
||||
const segment = s.slice(idx, idx + 2)
|
||||
buffer[i] = parseInt(segment, 16)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
|
||||
const ALG = 'AES-GCM'
|
||||
|
||||
export function getRandomBytes(size = 16): Uint8Array {
|
||||
return window.crypto.getRandomValues(new Uint8Array(size))
|
||||
}
|
||||
|
||||
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 fromString(s: string): Promise<Blob> {
|
||||
return fetch(s).then((r) => r.blob())
|
||||
}
|
||||
}
|
17
frontend/src/lib/stores/status.ts
Normal file
17
frontend/src/lib/stores/status.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { call } from '$lib/api'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export type Status = {
|
||||
version: string
|
||||
max_size: number
|
||||
}
|
||||
|
||||
export const status = writable<null | Status>(null)
|
||||
|
||||
export async function init() {
|
||||
const data = await call({
|
||||
url: 'status',
|
||||
method: 'get',
|
||||
})
|
||||
status.set(data)
|
||||
}
|
19
frontend/src/lib/ui/AboutParagraph.svelte
Normal file
19
frontend/src/lib/ui/AboutParagraph.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let title: string
|
||||
</script>
|
||||
|
||||
<p>
|
||||
<b>▶ {title}</b>
|
||||
<slot />
|
||||
</p>
|
||||
|
||||
<style>
|
||||
b {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
p > :global(span) {
|
||||
padding-left: 1.25em;
|
||||
}
|
||||
</style>
|
18
frontend/src/lib/ui/Button.svelte
Normal file
18
frontend/src/lib/ui/Button.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<button {...$$restProps} on:click><slot /></button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
padding: 0.25rem 2.5rem;
|
||||
border: 2px solid var(--ui-bg-2);
|
||||
background: var(--ui-bg-1);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: var(--ui-clr-primary);
|
||||
}
|
||||
</style>
|
72
frontend/src/lib/ui/FileUpload.svelte
Normal file
72
frontend/src/lib/ui/FileUpload.svelte
Normal file
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import type { FileDTO } from '$lib/api'
|
||||
import { Files } from '$lib/files'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import MaxSize from './MaxSize.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
let files: File[] = []
|
||||
|
||||
const dispatch = createEventDispatcher<{ file: string }>()
|
||||
|
||||
async function onInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (input.files.length) {
|
||||
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),
|
||||
}))
|
||||
)
|
||||
console.debug(
|
||||
'files',
|
||||
data.map((d) => d.contents.length)
|
||||
)
|
||||
dispatch('file', JSON.stringify(data))
|
||||
} else {
|
||||
dispatch('file', '')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<small>
|
||||
{label}
|
||||
</small>
|
||||
<input type="file" on:change={onInput} multiple />
|
||||
<div class="box">
|
||||
{#if files.length}
|
||||
<div>
|
||||
<b>Selected Files</b>
|
||||
{#each files as file}
|
||||
<div class="file">
|
||||
{file.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<b>No Files Selected</b>
|
||||
<br />
|
||||
<small>max: <MaxSize /></small>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
34
frontend/src/lib/ui/Icon.svelte
Normal file
34
frontend/src/lib/ui/Icon.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
export let icon: string = ''
|
||||
export let href: string = ''
|
||||
|
||||
$: src = href || `/icons/${icon}.svg`
|
||||
|
||||
let html = null
|
||||
|
||||
onMount(async () => {
|
||||
html = await fetch(src).then((res) => res.text())
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if html === null}
|
||||
<img on:click {...$$restProps} {src} alt={icon} />
|
||||
{:else}
|
||||
<div on:click {...$$restProps}>
|
||||
{@html html}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
img,
|
||||
div {
|
||||
display: inline-block;
|
||||
contain: strict;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
div > :global(svg) {
|
||||
display: block;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
12
frontend/src/lib/ui/MaxSize.svelte
Normal file
12
frontend/src/lib/ui/MaxSize.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { status } from '$lib/stores/status'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
</script>
|
||||
|
||||
<span>
|
||||
{#if $status !== null}
|
||||
{prettyBytes($status.max_size, { binary: true })}
|
||||
{:else}
|
||||
loading...
|
||||
{/if}
|
||||
</span>
|
69
frontend/src/lib/ui/ShowNote.svelte
Normal file
69
frontend/src/lib/ui/ShowNote.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { FileDTO, NotePublic } from '$lib/api'
|
||||
import { Files } from '$lib/files'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { saveAs } from 'file-saver'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import Button from './Button.svelte'
|
||||
|
||||
export let note: NotePublic
|
||||
|
||||
let files: FileDTO[] = []
|
||||
|
||||
$: if (note.meta.type === 'file') {
|
||||
files = JSON.parse(note.contents) as FileDTO[]
|
||||
}
|
||||
|
||||
$: download = () => {
|
||||
for (const file of files) {
|
||||
downloadFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(file: FileDTO) {
|
||||
const f = new File([await Files.fromString(file.contents)], file.name, {
|
||||
type: file.type,
|
||||
})
|
||||
saveAs(f)
|
||||
}
|
||||
</script>
|
||||
|
||||
<p class="error-text">you will <b>not</b> get the chance to see the note again.</p>
|
||||
{#if note.meta.type === 'text'}
|
||||
<div class="note" data-testid="note-result">
|
||||
{note.contents}
|
||||
</div>
|
||||
<Button on:click={() => copy(note.contents)}>copy to clipboard</Button>
|
||||
{:else}
|
||||
{#each files as file}
|
||||
<div class="note file" data-testid="note-result">
|
||||
<b on:click={() => downloadFile(file)}>↓ {file.name}</b>
|
||||
<small> {file.type} - {prettyBytes(file.size)}</small>
|
||||
</div>
|
||||
{/each}
|
||||
<Button on:click={download}>download all</Button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.note {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 2px solid var(--ui-bg-1);
|
||||
outline: none;
|
||||
padding: 0.5rem;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.note b {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note.file {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
65
frontend/src/lib/ui/Switch.svelte
Normal file
65
frontend/src/lib/ui/Switch.svelte
Normal file
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
export let label: string = ''
|
||||
export let value: boolean
|
||||
export let color = true
|
||||
</script>
|
||||
|
||||
<div {...$$restProps}>
|
||||
<label class="switch">
|
||||
<small>{label}</small>
|
||||
<input type="checkbox" bind:checked={value} />
|
||||
<span class:color class="slider" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
height: 3.75rem;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 4rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 2px solid var(--ui-bg-1);
|
||||
background-color: var(--ui-bg-0);
|
||||
transition: var(--ui-anim);
|
||||
transform: translateY(1.2rem);
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 2rem;
|
||||
width: 1.25rem;
|
||||
left: 0.125rem;
|
||||
bottom: 0.1rem;
|
||||
background-color: var(--ui-bg-1);
|
||||
-webkit-transition: 0.4s;
|
||||
transition: var(--ui-anim);
|
||||
}
|
||||
|
||||
input:checked + .slider.color:before {
|
||||
background-color: var(--ui-clr-primary);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(calc(2.25rem - 1px));
|
||||
}
|
||||
</style>
|
11
frontend/src/lib/ui/TextArea.svelte
Normal file
11
frontend/src/lib/ui/TextArea.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
export let label: string = ''
|
||||
export let value: string
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<small>
|
||||
{label}
|
||||
</small>
|
||||
<textarea class="box" {...$$restProps} bind:value />
|
||||
</label>
|
91
frontend/src/lib/ui/TextInput.svelte
Normal file
91
frontend/src/lib/ui/TextInput.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { getRandomBytes, Hex } from '$lib/crypto'
|
||||
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
|
||||
import Icon from './Icon.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
export let value
|
||||
|
||||
export let copy: boolean = false
|
||||
export let random: boolean = false
|
||||
|
||||
const initialType = $$restProps.type
|
||||
const isPassword = initialType === 'password'
|
||||
let hidden = true
|
||||
|
||||
$: if (isPassword) {
|
||||
value
|
||||
$$restProps.type = hidden ? initialType : 'text'
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
hidden = !hidden
|
||||
}
|
||||
function copyFN() {
|
||||
copyToClipboard(value)
|
||||
}
|
||||
function randomFN() {
|
||||
value = Hex.encode(getRandomBytes(20))
|
||||
}
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<small disabled={$$restProps.disabled}>
|
||||
{label}
|
||||
</small>
|
||||
<input bind:value {...$$restProps} />
|
||||
<div class="icons">
|
||||
{#if isPassword}
|
||||
<Icon class="icon" icon={hidden ? 'eye-sharp' : 'eye-off-sharp'} on:click={toggle} />
|
||||
{/if}
|
||||
{#if random}
|
||||
<Icon class="icon" icon="dice-sharp" on:click={randomFN} />
|
||||
{/if}
|
||||
{#if copy}
|
||||
<Icon class="icon" icon="copy-sharp" on:click={copyFN} />
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
label {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: 2px solid var(--ui-bg-1);
|
||||
outline: none;
|
||||
padding: 0.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
input:hover,
|
||||
input:focus {
|
||||
border-color: var(--ui-clr-primary);
|
||||
}
|
||||
|
||||
.icons {
|
||||
border: 1px red;
|
||||
position: absolute;
|
||||
right: 0.3rem;
|
||||
bottom: 0.3rem;
|
||||
display: flex;
|
||||
color: var(--ui-clr-primary);
|
||||
}
|
||||
.icons > :global(.icon) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
background-color: var(--ui-bg-1);
|
||||
border: 2px solid var(--ui-bg-2);
|
||||
padding: 1px;
|
||||
cursor: pointer;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
.icons > :global(.icon:hover) {
|
||||
border-color: var(--ui-clr-primary);
|
||||
}
|
||||
</style>
|
62
frontend/src/lib/ui/ThemeToggle.svelte
Normal file
62
frontend/src/lib/ui/ThemeToggle.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" context="module">
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
enum Theme {
|
||||
Dark = 'dark',
|
||||
Light = 'light',
|
||||
Auto = '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 (Object.values(Theme).includes(saved)) return saved
|
||||
}
|
||||
return Theme.Auto
|
||||
}
|
||||
|
||||
export const theme = writable<Theme>(init())
|
||||
|
||||
theme.subscribe((theme) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem('theme', theme)
|
||||
const html = window.document.getElementsByTagName('html')[0]
|
||||
html.setAttribute('theme', theme)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/ui/Icon.svelte'
|
||||
|
||||
function change() {
|
||||
theme.update((current) => NextTheme[current])
|
||||
}
|
||||
</script>
|
||||
|
||||
<div on:click={change}>
|
||||
<Icon class="icon" icon="contrast-sharp" />
|
||||
{$theme}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div :global(.icon) {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
195
frontend/src/lib/views/Create.svelte
Normal file
195
frontend/src/lib/views/Create.svelte
Normal file
@@ -0,0 +1,195 @@
|
||||
<script lang="ts">
|
||||
import { create, Note, PayloadToLargeError } from '$lib/api'
|
||||
import { encrypt, getKeyFromString, getRandomBytes, Hex } from '$lib/crypto'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import FileUpload from '$lib/ui/FileUpload.svelte'
|
||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextArea from '$lib/ui/TextArea.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import { blur } from 'svelte/transition'
|
||||
|
||||
let note: Note = {
|
||||
contents: '',
|
||||
meta: { type: 'text' },
|
||||
views: 1,
|
||||
expiration: 60,
|
||||
}
|
||||
let result: { password: string; id: string } | null = null
|
||||
let advanced = false
|
||||
let file = false
|
||||
let type = false
|
||||
let message = ''
|
||||
let loading = false
|
||||
let error: string | null = null
|
||||
|
||||
$: if (!advanced) {
|
||||
note.views = 1
|
||||
type = false
|
||||
}
|
||||
|
||||
$: {
|
||||
let fraction: string
|
||||
fraction = type ? `${note.expiration} minutes` : `${note.views} views`
|
||||
message = 'the note will expire and be destroyed after ' + fraction
|
||||
}
|
||||
|
||||
$: note.meta.type = file ? 'file' : 'text'
|
||||
|
||||
$: if (!file) {
|
||||
note.contents = ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
error = null
|
||||
loading = true
|
||||
const password = Hex.encode(getRandomBytes(32))
|
||||
const key = await getKeyFromString(password)
|
||||
const data: Note = {
|
||||
contents: await encrypt(note.contents, key),
|
||||
meta: note.meta,
|
||||
}
|
||||
// @ts-ignore
|
||||
if (type) data.expiration = parseInt(note.expiration)
|
||||
// @ts-ignore
|
||||
else data.views = parseInt(note.views)
|
||||
|
||||
const response = await create(data)
|
||||
result = {
|
||||
password: password,
|
||||
id: response.id,
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof PayloadToLargeError) {
|
||||
error = 'could not create not. note is to big'
|
||||
} else {
|
||||
error = 'could not create note. please try again.'
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if result}
|
||||
<TextInput
|
||||
type="text"
|
||||
readonly
|
||||
label="share link"
|
||||
value="{window.location.origin}/note/{result.id}#{result.password}"
|
||||
copy
|
||||
data-testid="note-share-link"
|
||||
/>
|
||||
<br />
|
||||
<p>
|
||||
<b>availability:</b>
|
||||
<br />
|
||||
the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes
|
||||
will be removed.
|
||||
<br />
|
||||
(you probably will be fine, just be warned.)
|
||||
</p>
|
||||
<br />
|
||||
<Button on:click={reset}>new note</Button>
|
||||
{:else}
|
||||
<p>
|
||||
Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and
|
||||
share the link.
|
||||
</p>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<fieldset disabled={loading}>
|
||||
{#if file}
|
||||
<FileUpload label="file" on:file={(f) => (note.contents = f.detail)} />
|
||||
{:else}
|
||||
<TextArea
|
||||
label="note"
|
||||
bind:value={note.contents}
|
||||
placeholder="..."
|
||||
data-testid="input-note"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="bottom">
|
||||
<Switch class="file" label="file" bind:value={file} />
|
||||
<Switch label="advanced" bind:value={advanced} />
|
||||
<div class="grow" />
|
||||
<div class="tr">
|
||||
<small>max: <MaxSize /> </small>
|
||||
<br />
|
||||
<Button type="submit" data-testid="button-create">create</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="error-text">{error}</div>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
<br />
|
||||
{#if loading}
|
||||
loading...
|
||||
{:else}
|
||||
{message}
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
{#if advanced}
|
||||
<div transition:blur={{ duration: 250 }}>
|
||||
<br />
|
||||
<div class="fields">
|
||||
<TextInput
|
||||
type="number"
|
||||
label="views"
|
||||
bind:value={note.views}
|
||||
disabled={type}
|
||||
max={100}
|
||||
/>
|
||||
<div class="middle-switch">
|
||||
<Switch label="mode" bind:value={type} color={false} />
|
||||
</div>
|
||||
<TextInput
|
||||
type="number"
|
||||
label="minutes"
|
||||
bind:value={note.expiration}
|
||||
disabled={!type}
|
||||
max={360}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</fieldset>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.bottom {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.bottom :global(.file) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.middle-switch {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
37
frontend/src/lib/views/Footer.svelte
Normal file
37
frontend/src/lib/views/Footer.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/ui/Icon.svelte'
|
||||
import ThemeToggle from '$lib/ui/ThemeToggle.svelte'
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<ThemeToggle />
|
||||
<nav>
|
||||
<a href="/">/home</a>
|
||||
<a href="/about">/about</a>
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">/code</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
background-color: var(--ui-bg-0-85);
|
||||
}
|
||||
|
||||
a {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
107
frontend/src/lib/views/Header.svelte
Normal file
107
frontend/src/lib/views/Header.svelte
Normal file
@@ -0,0 +1,107 @@
|
||||
<header>
|
||||
<a href="/">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 450 200"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xml:space="preserve"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||
><g id="Logo"
|
||||
><clipPath id="_clip1"><rect x="6.336" y="3.225" width="193.55" height="193.55" /></clipPath
|
||||
><g clip-path="url(#_clip1)"
|
||||
><g
|
||||
><g
|
||||
><path
|
||||
d="M173.425,43.296c-2.087,-0 -3.78,1.693 -3.78,3.78c-0,2.087 1.693,3.78 3.78,3.78c2.087,0 3.78,-1.693 3.78,-3.78c0,-2.087 -1.693,-3.78 -3.78,-3.78Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/></g
|
||||
></g
|
||||
><g
|
||||
><g
|
||||
><path
|
||||
d="M103.112,134.023c-2.087,-0 -3.781,1.693 -3.781,3.78c0,2.087 1.694,3.78 3.781,3.78c2.086,0 3.78,-1.693 3.78,-3.78c-0,-2.087 -1.694,-3.78 -3.78,-3.78Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/></g
|
||||
></g
|
||||
><g
|
||||
><g
|
||||
><path
|
||||
d="M156.036,96.22c-2.088,-0 -3.781,1.692 -3.781,3.78c0,18.76 -15.262,34.023 -34.022,34.023c-2.088,-0 -3.781,1.692 -3.781,3.78c0,2.088 1.693,3.78 3.781,3.78c22.929,0 41.583,-18.654 41.583,-41.583c-0,-2.088 -1.693,-3.78 -3.78,-3.78Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/></g
|
||||
></g
|
||||
><g
|
||||
><g
|
||||
><path
|
||||
d="M199.488,60.507l-9.515,-19.026c-4.102,-8.207 -12.35,-13.306 -21.527,-13.306c-7.479,-0 -14.626,3.547 -19.154,9.498l-10.679,12.016c-2.846,-4.047 -7.021,-7.049 -11.83,-8.421l-19.102,-5.459c-14.623,-4.178 -28.92,-15.441 -39.227,-30.901c-0.924,-1.386 -2.646,-2.003 -4.241,-1.521c-1.594,0.483 -2.684,1.953 -2.684,3.618l-0,20.372c-0,9.468 1.417,18.804 4.219,27.813c-2.936,0.73 -5.896,1.34 -8.843,1.816c-5.772,0.936 -11.653,1.411 -17.48,1.411l-29.308,-0c-1.374,-0 -2.64,0.746 -3.307,1.948c-0.666,1.202 -0.627,2.671 0.101,3.836l22.637,36.219c9.672,15.473 26.329,25.183 44.578,25.983l-36.36,41.158c-5.602,5.728 -3.655,15.315 3.746,18.396l20.017,9.887c0.089,0.044 0.179,0.084 0.271,0.121c5.79,2.313 12.389,-0.496 14.726,-6.279l13.969,-32.982l27.738,0c31.966,0 58.972,-25.967 58.972,-56.704l0,-22.682c0,-6.253 5.088,-11.341 11.341,-11.341l7.56,0c1.311,0 2.528,-0.678 3.216,-1.793c0.689,-1.114 0.752,-2.506 0.166,-3.677Zm-130.399,-42.236c10.418,12.203 23.307,21.034 36.515,24.808l19.103,5.459c3.726,1.063 6.866,3.624 8.666,7.048l-10.048,11.307l-17.652,-10.591c-7.646,-4.588 -16.746,-6.412 -25.776,-4.951c-2.838,0.46 -4.649,1.038 -6.877,1.759c-2.609,-8.332 -3.931,-16.971 -3.931,-25.733l0,-9.106Zm119.457,40.146c-10.422,-0 -18.901,8.479 -18.901,18.901l-0,22.682c-0,26.639 -23.544,49.144 -51.412,49.144l-30.243,-0c-10.77,-0 -20.451,5.983 -25.265,15.615l-0.797,1.596c-0.934,1.867 -0.177,4.137 1.691,5.071c1.867,0.934 4.138,0.176 5.071,-1.691c0.44,-0.586 3.306,-9.102 13.21,-12.125l-12.349,29.159c-0.01,0.024 -0.02,0.047 -0.029,0.071c-0.75,1.877 -2.864,2.851 -4.8,2.148c-21.279,-10.506 -19.997,-9.888 -20.252,-9.99c-2.526,-1.01 -3.191,-4.259 -1.267,-6.182c0.13,-0.131 8.026,-9.078 41.009,-46.411c13.867,-0.617 26.841,-6.319 36.694,-16.172c1.476,-1.476 1.476,-3.87 0,-5.346c-1.476,-1.477 -3.87,-1.476 -5.346,-0c-16.827,16.828 -36.803,13.634 -39.028,14.014c-16.603,0 -31.771,-8.407 -40.573,-22.488l-2.417,-3.868l2.729,1.065c17.307,6.753 38.919,4.347 53.816,-5.586c1.737,-1.158 2.207,-3.505 1.049,-5.242c-1.159,-1.737 -3.505,-2.207 -5.243,-1.048c-13.085,8.724 -31.922,10.666 -46.875,4.832l-12.185,-4.753l-9.896,-15.836l22.488,0c6.231,0 12.519,-0.507 18.689,-1.507c12.711,-2.055 18.051,-4.855 22.992,-5.655c7.182,-1.163 14.516,0.274 20.678,3.97l29.625,17.775c1.789,1.074 4.112,0.494 5.186,-1.296c1.075,-1.79 0.495,-4.113 -1.295,-5.187l-5.378,-3.226c26.56,-29.893 25.14,-28.272 25.32,-28.511c3.101,-4.136 8.038,-6.605 13.204,-6.605c6.294,0 11.951,3.497 14.764,9.127l6.779,13.555l-1.443,-0Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/></g
|
||||
></g
|
||||
></g
|
||||
><g
|
||||
><path
|
||||
d="M222.664,114.692c0.85,-0 1.274,0.586 1.274,1.759c0,1.174 -0.242,2.347 -0.728,3.52c-0.485,1.173 -1.183,2.326 -2.093,3.459c-0.91,1.132 -2.185,2.063 -3.823,2.791c-1.639,0.728 -3.489,1.092 -5.552,1.092c-4.531,0 -8.081,-1.143 -10.65,-3.428c-2.569,-2.286 -3.853,-5.644 -3.853,-10.073c-0,-4.43 1.133,-7.889 3.398,-10.377c2.265,-2.488 5.158,-3.732 8.677,-3.732c2.549,0 4.41,0.678 5.583,2.033c1.173,1.355 1.76,2.964 1.76,4.824c-0,1.012 -0.304,1.821 -0.91,2.428c-0.607,0.606 -1.194,0.91 -1.76,0.91c-1.052,-0 -1.74,-0.243 -2.063,-0.728c0.242,-0.607 0.364,-1.477 0.364,-2.61c-0,-1.132 -0.385,-2.164 -1.153,-3.094c-0.769,-0.931 -1.699,-1.396 -2.792,-1.396c-1.901,0 -3.266,0.93 -4.095,2.791c-0.83,1.861 -1.244,4.784 -1.244,8.769c-0,3.984 0.778,6.867 2.336,8.647c1.557,1.78 3.732,2.67 6.523,2.67c2.791,-0 5.138,-0.86 7.039,-2.579c1.901,-1.72 2.852,-4.157 2.852,-7.312c0.04,-0.243 0.344,-0.364 0.91,-0.364Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
d="M227.337,116.815l-0,9.649c-0.405,0.566 -1.113,0.849 -2.124,0.849c-1.012,0 -1.821,-0.313 -2.428,-0.94c-0.606,-0.627 -0.91,-1.548 -0.91,-2.761l0,-23.059c0.405,-0.566 1.113,-0.85 2.124,-0.85c1.011,0 1.821,0.314 2.427,0.941c0.607,0.627 0.911,1.547 0.911,2.761l-0,4.612c2.022,-5.017 4.814,-7.525 8.373,-7.525c2.549,0 4.41,0.678 5.583,2.033c1.173,1.355 1.76,2.963 1.76,4.824c-0,1.012 -0.304,1.821 -0.91,2.427c-0.607,0.607 -1.194,0.911 -1.76,0.911c-1.052,-0 -1.74,-0.243 -2.063,-0.728c0.242,-0.607 0.364,-1.477 0.364,-2.61c-0,-1.132 -0.384,-2.164 -1.153,-3.094c-0.769,-0.931 -1.699,-1.396 -2.791,-1.396c-1.902,-0 -3.611,1.729 -5.128,5.188c-1.517,3.459 -2.275,6.382 -2.275,8.768Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
d="M245.419,103.405c0,-1.214 0.304,-2.134 0.911,-2.761c0.606,-0.627 1.375,-0.941 2.306,-0.941c0.93,0 1.678,0.284 2.245,0.85l-0,17.415c-0,4.814 1.213,7.221 3.641,7.221c2.184,0 3.974,-1.153 5.37,-3.458c1.395,-2.306 2.093,-6.19 2.093,-11.651l0,-6.675c0,-1.214 0.304,-2.134 0.91,-2.761c0.607,-0.627 1.376,-0.941 2.306,-0.941c0.931,0 1.679,0.284 2.246,0.85l-0,30.826c0.485,-0.041 1.254,-0.061 2.305,-0.061c1.052,0 1.578,0.445 1.578,1.335l-0.364,0.91c-1.537,0 -2.71,0.021 -3.519,0.061l-0,3.034c-0,4.167 -0.941,7.454 -2.822,9.861c-1.881,2.407 -4.399,3.61 -7.555,3.61c-1.861,0 -3.509,-0.556 -4.945,-1.669c-1.436,-1.112 -2.155,-2.862 -2.155,-5.248c0,-3.075 1.093,-5.522 3.277,-7.343c2.185,-1.82 5.097,-3.095 8.738,-3.823l0,-9.466c-1.699,3.156 -4.429,4.733 -8.192,4.733c-2.589,0 -4.632,-0.829 -6.128,-2.488c-1.497,-1.658 -2.246,-4.045 -2.246,-7.16l0,-14.26Zm7.646,39.989c0,1.416 0.385,2.498 1.153,3.246c0.769,0.748 1.699,1.123 2.791,1.123c1.093,-0 1.983,-0.324 2.67,-0.971c1.538,-1.376 2.306,-5.522 2.306,-12.44c-5.946,1.335 -8.92,4.349 -8.92,9.042Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
d="M298.576,104.861c1.335,2.306 2.002,5.411 2.002,9.315c0,3.904 -0.981,7.059 -2.943,9.466c-1.962,2.407 -4.52,3.611 -7.676,3.611c-1.861,-0 -3.509,-0.648 -4.945,-1.942c-1.436,-1.295 -2.155,-3.358 -2.155,-6.19c0,-6.634 3.297,-11.59 9.891,-14.866c-1.051,-1.457 -2.518,-2.185 -4.399,-2.185c-1.881,0 -3.671,0.93 -5.37,2.791c-1.699,1.861 -2.549,4.653 -2.549,8.374l0,36.105c-0.404,0.567 -1.112,0.85 -2.124,0.85c-1.011,-0 -1.82,-0.314 -2.427,-0.941c-0.607,-0.627 -0.91,-1.547 -0.91,-2.761l-0,-45.935c0.405,-0.566 1.112,-0.85 2.124,-0.85c1.011,0 1.82,0.314 2.427,0.941c0.607,0.627 0.91,1.547 0.91,2.761l0,1.335c2.104,-3.358 4.885,-5.037 8.344,-5.037c3.459,0 6.159,0.971 8.101,2.913c4.247,-1.375 9.001,-2.063 14.26,-2.063c1.092,-0 1.638,0.445 1.638,1.335l-0.364,0.91c-4.895,0 -9.507,0.688 -13.835,2.063Zm-8.738,20.025c3.317,-0 4.976,-3.297 4.976,-9.891c-0,-3.479 -0.284,-6.21 -0.85,-8.192c-5.34,2.913 -8.01,7.08 -8.01,12.5c0,1.78 0.385,3.156 1.153,4.127c0.769,0.971 1.679,1.456 2.731,1.456Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
d="M308.285,94.424c-0,-1.213 0.303,-2.134 0.91,-2.761c0.607,-0.627 1.375,-0.94 2.306,-0.94c0.93,-0 1.679,0.283 2.245,0.849l0,8.981l8.313,-0c0.85,-0 1.406,0.162 1.669,0.485c0.263,0.324 0.394,0.87 0.394,1.639l-10.376,-0l0,15.291c0,2.225 0.637,3.945 1.911,5.158c1.275,1.214 2.741,1.821 4.4,1.821c2.508,-0 4.349,-0.931 5.522,-2.792c1.173,-1.861 1.76,-4.874 1.76,-9.041c0.08,-0.243 0.384,-0.364 0.91,-0.364c0.728,-0 1.092,1.072 1.092,3.216c-0,3.277 -0.819,5.987 -2.458,8.131c-1.638,2.144 -4.156,3.216 -7.554,3.216c-3.398,0 -6.089,-0.89 -8.071,-2.67c-1.982,-1.78 -2.973,-4.429 -2.973,-7.949l-0,-22.27Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
d="M345.543,101.463c0.526,-1.173 1.365,-1.76 2.518,-1.76c1.153,0 2.013,0.284 2.579,0.85l-0,30.826c0.485,-0.041 1.254,-0.061 2.306,-0.061c1.052,0 1.578,0.445 1.578,1.335l-0.365,0.91c-1.537,0 -2.71,0.021 -3.519,0.061l-0,3.155c-0.041,4.167 -1.001,7.434 -2.882,9.8c-1.882,2.367 -4.38,3.55 -7.494,3.55c-1.861,0 -3.51,-0.556 -4.946,-1.669c-1.436,-1.112 -2.154,-2.862 -2.154,-5.248c-0,-3.075 1.092,-5.522 3.277,-7.343c2.184,-1.82 5.097,-3.095 8.738,-3.823l-0,-8.92c-1.699,2.792 -4.046,4.187 -7.039,4.187c-7.08,0 -10.619,-4.632 -10.619,-13.896c-0,-4.692 1.102,-8.151 3.307,-10.376c2.204,-2.225 5.127,-3.338 8.768,-3.338c2.791,0 4.774,0.587 5.947,1.76Zm-0.364,12.986l-0,-4.308c-0,-2.873 -0.486,-4.936 -1.457,-6.19c-0.971,-1.254 -2.093,-1.881 -3.367,-1.881c-1.275,0 -2.276,0.121 -3.004,0.364c-0.728,0.243 -1.436,0.748 -2.124,1.517c-1.295,1.456 -1.942,4.612 -1.942,9.466c0,3.196 0.283,5.684 0.85,7.464c0.566,1.78 1.213,2.903 1.942,3.368c0.728,0.465 1.749,0.698 3.064,0.698c1.315,-0 2.65,-0.84 4.005,-2.519c1.355,-1.678 2.033,-4.338 2.033,-7.979Zm-0,22.209l-0,-2.306c-5.947,1.335 -8.92,4.349 -8.92,9.042c-0,1.416 0.384,2.498 1.153,3.246c0.768,0.748 1.678,1.123 2.73,1.123c1.699,-0 2.963,-0.789 3.793,-2.367c0.829,-1.578 1.244,-4.49 1.244,-8.738Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
d="M381.891,113.478c0.849,-0 1.274,0.647 1.274,1.942c-0,2.993 -1.042,5.724 -3.125,8.192c-2.084,2.467 -5.249,3.701 -9.497,3.701c-4.247,0 -7.656,-1.143 -10.224,-3.428c-2.569,-2.286 -3.854,-5.644 -3.854,-10.073c0,-4.43 1.133,-7.889 3.398,-10.377c2.266,-2.488 5.158,-3.732 8.678,-3.732c2.548,0 4.409,0.678 5.582,2.033c1.174,1.355 1.76,2.964 1.76,4.824c0,3.034 -1.264,5.553 -3.792,7.555c-2.529,2.003 -5.755,3.004 -9.679,3.004c0.364,2.589 1.305,4.541 2.822,5.856c1.517,1.314 3.519,1.972 6.007,1.972c2.488,-0 4.733,-0.941 6.736,-2.822c2.002,-1.881 3.003,-4.642 3.003,-8.283c0.041,-0.243 0.344,-0.364 0.911,-0.364Zm-19.661,0.85l-0,0.546c2.508,-0.324 4.682,-1.113 6.523,-2.367c1.841,-1.254 2.761,-3.115 2.761,-5.583c0,-1.577 -0.344,-2.781 -1.031,-3.61c-0.688,-0.829 -1.659,-1.244 -2.913,-1.244c-1.901,0 -3.267,0.93 -4.096,2.791c-0.829,1.861 -1.244,5.017 -1.244,9.467Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
d="M393.359,99.703c3.601,0 6.534,1.194 8.799,3.581c2.265,2.386 3.398,5.633 3.398,9.739c0,4.106 -1.062,7.514 -3.186,10.225c-2.124,2.71 -4.965,4.065 -8.525,4.065c-3.56,0 -6.524,-1.224 -8.89,-3.671c-2.367,-2.447 -3.55,-5.846 -3.55,-10.194c0,-4.349 1.102,-7.727 3.307,-10.134c2.205,-2.407 5.087,-3.611 8.647,-3.611Zm0.061,2.367c-1.092,0 -2.033,0.273 -2.822,0.819c-0.789,0.546 -1.183,1.325 -1.183,2.336c-0,2.589 1.011,5.067 3.034,7.434c2.023,2.366 4.43,3.934 7.221,4.703c0.081,-1.538 0.121,-2.852 0.121,-3.945c0,-4.207 -0.445,-7.15 -1.335,-8.829c-0.89,-1.679 -2.568,-2.518 -5.036,-2.518Zm-6.25,11.347c-0,4.127 0.516,7.08 1.547,8.86c1.032,1.78 2.721,2.67 5.067,2.67c1.618,-0 2.862,-0.415 3.732,-1.244c0.87,-0.83 1.466,-2.175 1.79,-4.036c-2.63,-0.566 -5.047,-1.759 -7.251,-3.58c-2.205,-1.82 -3.813,-3.843 -4.825,-6.068c-0.04,0.688 -0.06,1.821 -0.06,3.398Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
d="M442.571,112.75c0.729,-0 1.093,1.072 1.093,3.216c-0,3.438 -0.739,6.189 -2.215,8.252c-1.477,2.064 -3.631,3.095 -6.463,3.095c-2.832,0 -5.006,-0.971 -6.523,-2.912c-1.517,-1.942 -2.275,-4.693 -2.275,-8.253l-0,-7.1c-0,-4.814 -1.214,-7.221 -3.641,-7.221c-2.144,0 -3.904,1.093 -5.28,3.277c-1.375,2.185 -2.063,5.846 -2.063,10.983l0,10.377c-0.404,0.566 -1.112,0.849 -2.124,0.849c-1.011,0 -1.82,-0.313 -2.427,-0.94c-0.607,-0.627 -0.91,-1.548 -0.91,-2.761l0,-23.059c0.405,-0.566 1.173,-0.85 2.306,-0.85c2.103,0 3.155,1.517 3.155,4.552c1.699,-3.035 4.39,-4.552 8.071,-4.552c2.629,0 4.723,0.83 6.28,2.488c1.558,1.659 2.337,4.046 2.337,7.161l-0,8.616c-0,2.306 0.364,4.046 1.092,5.219c0.728,1.173 1.81,1.76 3.246,1.76c1.436,-0 2.7,-0.9 3.793,-2.701c1.092,-1.8 1.638,-4.844 1.638,-9.132c0.041,-0.243 0.344,-0.364 0.91,-0.364Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/></g
|
||||
></g
|
||||
></svg
|
||||
>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
a {
|
||||
border: none;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 30rem) {
|
||||
header {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
header svg {
|
||||
max-height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
header svg {
|
||||
width: 100%;
|
||||
max-width: 16rem;
|
||||
transform: translateX(-1rem);
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
32
frontend/src/routes/__layout.svelte
Normal file
32
frontend/src/routes/__layout.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { init } 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(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>cryptgeon</title>
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
<Header />
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
||||
<style>
|
||||
main {
|
||||
padding: 1rem;
|
||||
padding-bottom: 4rem;
|
||||
width: 100%;
|
||||
max-width: 35rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
80
frontend/src/routes/about.svelte
Normal file
80
frontend/src/routes/about.svelte
Normal file
@@ -0,0 +1,80 @@
|
||||
<script context="module">
|
||||
import { browser, dev } from '$app/env'
|
||||
import { status } from '$lib/stores/status'
|
||||
import AboutParagraph from '$lib/ui/AboutParagraph.svelte'
|
||||
|
||||
export const hydrate = dev
|
||||
export const router = browser
|
||||
export const prerender = true
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>About</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="content">
|
||||
<h1>About</h1>
|
||||
|
||||
<p>
|
||||
<i>cryptgeon</i> is a secure, open source sharing note / file service inspired by
|
||||
<a href="https://privnote.com"><i>PrivNote</i></a>.
|
||||
</p>
|
||||
|
||||
<AboutParagraph title="how does it work?">
|
||||
<span>
|
||||
each note has a generated <code>id (256bit)</code> and <code>key 256(bit)</code>. The
|
||||
<code>id</code>
|
||||
is used to save & retrieve the note. the note is then encrypted with aes in gcm mode on the client
|
||||
side with the <code>key</code> and then sent to the server. data is stored in memory and never
|
||||
persisted to disk. the server never sees the encryption key and cannot decrypt the contents of
|
||||
the notes even if it tried to.
|
||||
</span>
|
||||
</AboutParagraph>
|
||||
|
||||
<AboutParagraph title="features">
|
||||
<ul>
|
||||
<li>server cannot decrypt contents due to client side encryption</li>
|
||||
<li>view and time constraints</li>
|
||||
<li>in memory, no persistence</li>
|
||||
</ul>
|
||||
</AboutParagraph>
|
||||
|
||||
<AboutParagraph title="tech stack">
|
||||
<span>
|
||||
the backend is written in rust and the frontend is svelte and typescript.
|
||||
<br />
|
||||
you are welcomed to check & audit the
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">
|
||||
source code
|
||||
</a>.
|
||||
</span>
|
||||
</AboutParagraph>
|
||||
|
||||
<AboutParagraph title="attribution">
|
||||
<span>
|
||||
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>
|
||||
</span>
|
||||
</AboutParagraph>
|
||||
|
||||
<AboutParagraph title="version">
|
||||
<span>
|
||||
{#if $status}
|
||||
<code>v{$status.version}</code>
|
||||
{/if}
|
||||
</span>
|
||||
</AboutParagraph>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-left: 1rem;
|
||||
list-style: square;
|
||||
}
|
||||
</style>
|
9
frontend/src/routes/index.svelte
Normal file
9
frontend/src/routes/index.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = true
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Create from '$lib/views/Create.svelte'
|
||||
</script>
|
||||
|
||||
<Create />
|
81
frontend/src/routes/note/[id].svelte
Normal file
81
frontend/src/routes/note/[id].svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script context="module" lang="ts">
|
||||
export async function load({ page }) {
|
||||
return {
|
||||
props: page.params,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { NotePublic } from '$lib/api'
|
||||
import { get, info } from '$lib/api'
|
||||
import { decrypt, getKeyFromString } from '$lib/crypto'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import ShowNote from '$lib/ui/ShowNote.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let id: string
|
||||
|
||||
let password: string
|
||||
let note: NotePublic | null = null
|
||||
let exists = false
|
||||
|
||||
let loading = true
|
||||
let error = false
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
loading = true
|
||||
error = null
|
||||
password = window.location.hash.slice(1)
|
||||
await info(id)
|
||||
exists = true
|
||||
} catch {
|
||||
exists = false
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
})
|
||||
|
||||
async function show() {
|
||||
try {
|
||||
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 = true
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !loading}
|
||||
{#if !exists}
|
||||
<p class="error-text" data-testid="note-not-found">
|
||||
note was not found or was already deleted.
|
||||
</p>
|
||||
{:else if note && !error}
|
||||
<ShowNote {note} />
|
||||
{:else}
|
||||
<form on:submit|preventDefault={show}>
|
||||
<fieldset>
|
||||
<p>click below to show and delete the note if the counter has reached it's limit</p>
|
||||
<Button type="submit" data-testid="button-show">show note</Button>
|
||||
{#if error}
|
||||
<br />
|
||||
<p class="error-text">
|
||||
wrong password. could not decipher. probably a broken link. note was destroyed.
|
||||
<br />
|
||||
</p>
|
||||
{/if}
|
||||
</fieldset>
|
||||
</form>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if loading}
|
||||
<p>loading...</p>
|
||||
{/if}
|
Reference in New Issue
Block a user