Compare commits
48 Commits
v2.0.0-rc.
...
v2.2.0
Author | SHA1 | Date | |
---|---|---|---|
7ee1b8370a | |||
e7750699cc | |||
e14042ea28 | |||
6fb7518b6a | |||
436ae2a7e5 | |||
fe5ce580ab | |||
0f882da5d1 | |||
ad6f136dd0 | |||
da527a0857 | |||
a95931ae77 | |||
d6c2236673 | |||
42a8ab5d0f | |||
0934808a59 | |||
88ea828b66 | |||
41ed5c0e23 | |||
0a98481991 | |||
5d62c48a35 | |||
0ab39023b0 | |||
7b202962e8 | |||
7a045b3f34 | |||
cb80c8bfe4 | |||
74c3197e47 | |||
6ae927ce71 | |||
9d13e607f5 | |||
0db3ef4a1f | |||
03e9fb431f | |||
b84df2866b | |||
3d4fef7c23 | |||
9d787008a4 | |||
687f26bb40 | |||
371a869800 | |||
321c303a8a | |||
2f176d84e9 | |||
67d4f09bd7 | |||
c40f009523 | |||
026f8c69d7 | |||
cacb808117 | |||
2d573edcac | |||
4287cd429d | |||
024dfeeeb7 | |||
f24bcba20b | |||
1d95edc455 | |||
|
ec24ab3edd | ||
a552e4d766 | |||
c3b1772728 | |||
786878a3e4 | |||
a5d98b76bd | |||
9590c9b567 |
@@ -1,15 +1,15 @@
|
||||
*
|
||||
!/entry.sh
|
||||
|
||||
!/backend/src
|
||||
!/backend/Cargo.lock
|
||||
!/backend/Cargo.toml
|
||||
!/packages/backend/src
|
||||
!/packages/backend/Cargo.lock
|
||||
!/packages/backend/Cargo.toml
|
||||
|
||||
!/frontend/locales
|
||||
!/frontend/src
|
||||
!/frontend/static
|
||||
!/frontend/.npmrc
|
||||
!/frontend/package.json
|
||||
!/frontend/pnpm-lock.yaml
|
||||
!/frontend/svelte.config.js
|
||||
!/frontend/tsconfig.json
|
||||
!/packages/frontend/locales
|
||||
!/packages/frontend/src
|
||||
!/packages/frontend/static
|
||||
!/packages/frontend/.npmrc
|
||||
!/packages/frontend/package.json
|
||||
!/packages/frontend/pnpm-lock.yaml
|
||||
!/packages/frontend/svelte.config.js
|
||||
!/packages/frontend/tsconfig.json
|
||||
!/packages/frontend/vite.config.js
|
||||
|
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
*.afdesign filter=lfs diff=lfs merge=lfs -text
|
||||
test/assets/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
5
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: ci
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -31,11 +31,8 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
36
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
- name: Build docker image
|
||||
run: npm run test:prepare
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
npm install playwright
|
||||
npx playwright install --with-deps
|
||||
|
||||
- name: Run your tests
|
||||
run: npm test
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
3
.gitignore
vendored
@@ -9,3 +9,6 @@ node_modules
|
||||
/build
|
||||
/functions
|
||||
.env
|
||||
|
||||
General
|
||||
test-results
|
||||
|
6
.vscode/settings.json
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"cSpell.words": ["ciphertext", "cryptgeon"],
|
||||
"i18n-ally.localesPaths": ["frontend/locales"],
|
||||
"i18n-ally.enabledFrameworks": ["svelte"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
103
CHANGELOG.md
@@ -5,6 +5,109 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.2.0] - 2023-01-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Default port is now 8000, not 5000.
|
||||
- Moved to generic encryption library `occulto`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bad chinese language code.
|
||||
|
||||
### Security
|
||||
|
||||
- Updated dependencies.
|
||||
|
||||
## [2.1.0] - 2023-01-04
|
||||
|
||||
### Added
|
||||
|
||||
- QR Code to more easily copy and share links.
|
||||
|
||||
## [2.0.7] - 2022-12-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Svelte Kit now stable 🎉
|
||||
|
||||
## [2.0.6] - 2022-11-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- #66 Set minimum a view.
|
||||
|
||||
### Security
|
||||
|
||||
- Updated dependencies.
|
||||
|
||||
## [2.0.5] - 2022-11-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Docker build pipeline.
|
||||
|
||||
## [2.0.4] - 2022-10-29
|
||||
|
||||
### Added
|
||||
|
||||
- `THEME_PAGE_TITLE`.
|
||||
- `THEME_FAVICON`.
|
||||
|
||||
## [2.0.3] - 2022-10-07
|
||||
|
||||
### Added
|
||||
|
||||
- Flag for verbosity.
|
||||
|
||||
### Fixed
|
||||
|
||||
- #58 Fixed bug in the max views frontend form.
|
||||
|
||||
## [2.0.2] - 2022-07-20
|
||||
|
||||
### Added
|
||||
|
||||
- Toasts for events.
|
||||
- E2E Tests.
|
||||
- Make backend more configurable.
|
||||
|
||||
## [2.0.1] - 2022-07-18
|
||||
|
||||
### Added
|
||||
|
||||
- Max file size on the client now.
|
||||
- Loading information.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed encoding from hex to base64.
|
||||
- Chinese language code.
|
||||
- Notable speed improvements for big files.
|
||||
|
||||
## [2.0.0] - 2022-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- Theming for logo and description text.
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved to redis.
|
||||
- New html sanitizing library.
|
||||
|
||||
## [2.0.0-rc.0] - 2022-07-15
|
||||
|
||||
### Added
|
||||
|
||||
- Theming for logo and description text.
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved to redis.
|
||||
- New html sanitizing library.
|
||||
|
||||
## [1.5.3] - 2022-06-07
|
||||
|
||||
### Changed
|
||||
|
26
Dockerfile
@@ -1,26 +1,32 @@
|
||||
# FRONTEND
|
||||
FROM node:16-alpine as client
|
||||
WORKDIR /tmp
|
||||
RUN npm install -g pnpm
|
||||
COPY ./frontend ./
|
||||
RUN npm install -g pnpm@7
|
||||
COPY ./packages/frontend ./
|
||||
RUN pnpm install
|
||||
RUN pnpm exec svelte-kit sync
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
# BACKEND
|
||||
FROM rust:1.61-alpine as backend
|
||||
FROM rust:1.64-alpine as backend
|
||||
WORKDIR /tmp
|
||||
RUN apk add libc-dev openssl-dev alpine-sdk
|
||||
COPY ./backend ./
|
||||
RUN cargo build --release
|
||||
COPY ./packages/backend/Cargo.* ./
|
||||
# https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html
|
||||
RUN rustup update nightly
|
||||
ENV CARGO_UNSTABLE_SPARSE_REGISTRY=true
|
||||
RUN cargo +nightly fetch
|
||||
COPY ./packages/backend ./
|
||||
RUN cargo +nightly build --release
|
||||
|
||||
|
||||
# RUNNER
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
COPY ./entry.sh .
|
||||
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||
COPY --from=client /tmp/build ./frontend/build
|
||||
ENV MEMCACHE=memcached:11211
|
||||
EXPOSE 5000
|
||||
ENTRYPOINT [ "/app/entry.sh" ]
|
||||
COPY --from=client /tmp/build ./frontend
|
||||
ENV FRONTEND_PATH="./frontend"
|
||||
ENV REDIS="redis://redis/"
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/app/cryptgeon" ]
|
||||
|
114
README.md
@@ -24,9 +24,9 @@ _cryptgeon_ is a secure, open source sharing note or file service inspired by [_
|
||||
>
|
||||
> Thanks to [Lokalise](https://lokalise.com/) for providing free access to their platform.
|
||||
|
||||
## Demo
|
||||
## Live Service / Demo
|
||||
|
||||
Check out the demo and see for yourself https://cryptgeon.nicco.io.
|
||||
Check out the live service / demo and see for yourself [cryptgeon.org](https://cryptgeon.org)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -50,13 +50,18 @@ of the notes even if it tried to.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------- | ----------------- | --------------------------------------------------------------------------------------- |
|
||||
| `MEMCACHE` | `memcached:11211` | Memcached URL to connect to. |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/) |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
|
||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -69,21 +74,24 @@ Docker is the easiest way. There is the [official image here](https://hub.docker
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
|
||||
version: '3.7'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
memcached:
|
||||
image: memcached:1-alpine
|
||||
entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/manual/eviction/
|
||||
command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
depends_on:
|
||||
- memcached
|
||||
- redis
|
||||
environment:
|
||||
SIZE_LIMIT: 4M
|
||||
# Size limit for a single note.
|
||||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:5000
|
||||
- 80:8000
|
||||
```
|
||||
|
||||
### NGINX Proxy
|
||||
@@ -92,47 +100,27 @@ See the [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/exam
|
||||
|
||||
### Traefik 2
|
||||
|
||||
Assumptions:
|
||||
See the [examples/traefik](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/traefik) folder.
|
||||
|
||||
- External proxy docker network `proxy`
|
||||
- A certificate resolver `le`
|
||||
- A https entrypoint `secure`
|
||||
- Domain name `example.org`
|
||||
### Scratch
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
See the [examples/scratch](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/scratch) folder. There you'll find a guide how to setup a server and install cryptgeon from scratch.
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
### Synology
|
||||
|
||||
services:
|
||||
memcached:
|
||||
image: memcached:1-alpine
|
||||
restart: unless-stopped
|
||||
entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
|
||||
There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) you can follow.
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- memcached
|
||||
networks:
|
||||
- default
|
||||
- proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
|
||||
- traefik.http.routers.cryptgeon.entrypoints=secure
|
||||
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
||||
```
|
||||
### YouTube Guides
|
||||
|
||||
- English by [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
|
||||
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
||||
|
||||
## Development
|
||||
|
||||
**Requirements**
|
||||
|
||||
- `pnpm`: `>=6`
|
||||
- `node`: `>=14`
|
||||
- `node`: `>=16`
|
||||
- `rust`: edition `2021`
|
||||
|
||||
**Install**
|
||||
@@ -150,21 +138,41 @@ cargo install cargo-watch
|
||||
|
||||
Make sure you have docker running.
|
||||
|
||||
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
|
||||
> https://developer.apple.com/forums/thread/682332
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Running `pnpm run dev` in the root folder will start the following things:
|
||||
|
||||
- a memcache docker container
|
||||
- rust backend with hot reload
|
||||
- client with hot reload
|
||||
- redis docker container
|
||||
- rust backend
|
||||
- client
|
||||
|
||||
You can see the app under [localhost:1234](http://localhost:1234).
|
||||
|
||||
## Tests
|
||||
|
||||
Tests are end to end tests written with Playwright.
|
||||
|
||||
```sh
|
||||
pnpm run test:prepare
|
||||
docker compose up redis -d
|
||||
pnpm run test:server
|
||||
|
||||
# In another terminal.
|
||||
# Use the test or test:local script. The local version only runs in one browser for quicker development.
|
||||
pnpm run test:local
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
Please refer to the security section [here](./SECURITY.md).
|
||||
|
||||
###### Attributions
|
||||
|
||||
Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
|
||||
- Test data:
|
||||
- Text for tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
|
||||
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
|
||||
- [Unsplash Pictures](https://unsplash.com/)
|
||||
- Loading animation by [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa)
|
||||
- Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
|
||||
|
@@ -26,7 +26,7 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
||||
|
||||
## 演示示例
|
||||
|
||||
查看加密鸽的在线演示demo: https://cryptgeon.nicco.io.
|
||||
查看加密鸽的在线演示 demo: [cryptgeon.org](https://cryptgeon.org)
|
||||
|
||||
## 功能
|
||||
|
||||
@@ -39,7 +39,7 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
||||
|
||||
加密鸽会为每条笔记都生成一个独立的 <code>id (256bit)</code> 和 <code>key 256(bit)</code>。
|
||||
|
||||
其中<code>id</code>用于保存和提取密信, 在这之后这封密信将会被客户端使用AES算法的GCM模式和`key`进行加密然后发送至服务器,数据将会保存在服务器的内存中且永远不会被持久化到硬盘上,服务端永远不会得到密钥并且无法解读密信的内容。
|
||||
其中<code>id</code>用于保存和提取密信, 在这之后这封密信将会被客户端使用 AES 算法的 GCM 模式和`key`进行加密然后发送至服务器,数据将会保存在服务器的内存中且永远不会被持久化到硬盘上,服务端永远不会得到密钥并且无法解读密信的内容。
|
||||
|
||||
## 屏幕截图
|
||||
|
||||
@@ -47,13 +47,15 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量名称 | 默认值 | 描述 |
|
||||
| ------------------ | ----------------- | ------------------------------------------------------------ |
|
||||
| `MEMCACHE` | `memcached:11211` | Memcached 连接 URL |
|
||||
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
|
||||
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
|
||||
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
||||
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
||||
| 变量名称 | 默认值 | 描述 |
|
||||
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
|
||||
| `REDIS` | `redis://redis/` | Redis 连接 URL。 |
|
||||
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
|
||||
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
|
||||
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
||||
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
||||
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
|
||||
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
|
||||
|
||||
## 部署
|
||||
|
||||
@@ -61,40 +63,39 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
||||
|
||||
### Docker
|
||||
|
||||
Docker是最简单的部署方式。这里是[官方镜像的地址](https://hub.docker.com/r/cupcakearmy/cryptgeon)。
|
||||
Docker 是最简单的部署方式。这里是[官方镜像的地址](https://hub.docker.com/r/cupcakearmy/cryptgeon)。
|
||||
|
||||
附:译者的[部署笔记](https://www.hash070.top/archives/cryptgeon-docker-deploy.html)
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.7'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
memcached:
|
||||
image: memcached:1-alpine
|
||||
entrypoint: memcached -m 128M -I 4M # 限制到最大 128 MB 内存占用,每个密信最大 4Mb 占用, 请根据自己的服务器内存的实际情况设置。
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
depends_on:
|
||||
- memcached
|
||||
- redis
|
||||
environment:
|
||||
SIZE_LIMIT: 4M
|
||||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:5000
|
||||
- 80:8000
|
||||
```
|
||||
|
||||
### NGINX 反向代理
|
||||
|
||||
查看 [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx) 目录。那里有几个示例反代配置文件模板,其中一个是带https配置的反代配置模板,你需要指定服务器的名称和证书才能生效。
|
||||
查看 [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx) 目录。那里有几个示例反代配置文件模板,其中一个是带 https 配置的反代配置模板,你需要指定服务器的名称和证书才能生效。
|
||||
|
||||
### Traefik 2
|
||||
|
||||
假设:
|
||||
|
||||
- 外部Docker代理网络 `proxy`
|
||||
- 外部 Docker 代理网络 `proxy`
|
||||
- 证书解析器 `le`
|
||||
- 一个https入站点 `secure`
|
||||
- 一个 https 入站点 `secure`
|
||||
- 域名 `example.org`
|
||||
|
||||
```yaml
|
||||
@@ -105,16 +106,15 @@ networks:
|
||||
external: true
|
||||
|
||||
services:
|
||||
memcached:
|
||||
image: memcached:1-alpine
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- memcached
|
||||
- redis
|
||||
networks:
|
||||
- default
|
||||
- proxy
|
||||
@@ -139,31 +139,46 @@ services:
|
||||
pnpm install
|
||||
pnpm --prefix frontend install
|
||||
|
||||
# Also you need cargo watch if you don't already have it installed.
|
||||
# 你还需要安装CargoWatch.
|
||||
# https://lib.rs/crates/cargo-watch
|
||||
cargo install cargo-watch
|
||||
```
|
||||
|
||||
**运行**
|
||||
|
||||
确保你的Docker正在运行
|
||||
|
||||
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
|
||||
> https://developer.apple.com/forums/thread/682332
|
||||
确保你的 Docker 正在运行
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
在根目录执行 `pnpm run dev` 会开启下列服务:
|
||||
在根目录执行 `pnpm run dev` 会开启下列服务:
|
||||
|
||||
- 一个 memcache docker 容器
|
||||
- 一个 redis docker 容器
|
||||
- 无热重载的 rust 后端
|
||||
- 可热重载的客户端
|
||||
|
||||
你可以通过1234端口进入该应用,即 [localhost:1234](http://localhost:1234).
|
||||
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234).
|
||||
|
||||
## 测试
|
||||
|
||||
这些测试是用 Playwright 实现的一些端到端测试用例。
|
||||
|
||||
```sh
|
||||
pnpm run test:prepare
|
||||
docker compose up redis -d
|
||||
pnpm run test:server
|
||||
|
||||
# 在另一个终端中:
|
||||
# 使用test或者test:local script。为了更快的开发,本地版本只会在一个浏览器中运行。
|
||||
pnpm run test:local
|
||||
```
|
||||
|
||||
###### Attributions
|
||||
|
||||
本项目所使用的图标由<a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com的<a href="https://www.freepik.com" title="Freepik">freepik</a>制作</a>
|
||||
|
||||
- 测试数据:
|
||||
- 测试文本 [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
|
||||
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
|
||||
- [Unsplash Pictures](https://unsplash.com/)
|
||||
- 加载动画由 [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa) 提供
|
||||
- 图标由来自 <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> 的 <a href="https://www.freepik.com" title="Freepik">freepik</a> 提供
|
||||
|
18
SECURITY.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Please ensure that you are using the latest major version available.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| 2.x | ✅ |
|
||||
| < 1.x | ❌ |
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
_cryptgeon_ has a full disclosure vulnerability policy.
|
||||
Report any bug / vulnerability directly to the [issue tracker](https://github.com/cupcakearmy/cryptgeon/issues).
|
||||
Please do NOT attempt to report any security vulnerability in this code privately to anybody.
|
||||
|
||||
> Shamefully copied of the [ring security section](https://github.com/briansmith/ring#bug-reporting).
|
@@ -1,23 +0,0 @@
|
||||
use byte_unit::Byte;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
pub static ref LIMIT: u32 =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as u32;
|
||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
|
||||
.unwrap_or("360".to_string()) // 6 hours in minutes
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
use actix_web::web;
|
||||
use byte_unit::Byte;
|
||||
use mime;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref LIMIT: usize =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as usize;
|
||||
}
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
let json = web::JsonConfig::default().limit(*LIMIT);
|
||||
let plain = web::PayloadConfig::default()
|
||||
.limit(*LIMIT)
|
||||
.mimetype(mime::STAR_STAR);
|
||||
cfg.app_data(json).app_data(plain);
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
use memcache;
|
||||
|
||||
use crate::note::now;
|
||||
use crate::note::Note;
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: memcache::Client = memcache::connect(format!(
|
||||
"memcache://{}?timeout=10&tcp_nodelay=true",
|
||||
std::env::var("MEMCACHE").unwrap_or("127.0.0.1:11211".to_string())
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set(id: &String, note: &Note) {
|
||||
let serialized = serde_json::to_string(¬e.clone()).unwrap();
|
||||
let expiration: u32 = match note.expiration {
|
||||
Some(e) => e - now(),
|
||||
None => 0,
|
||||
};
|
||||
CLIENT.set(id, serialized, expiration).unwrap();
|
||||
}
|
||||
|
||||
pub fn get(id: &String) -> Option<Note> {
|
||||
let value: Option<String> = CLIENT.get(&id).unwrap();
|
||||
match value {
|
||||
None => return None,
|
||||
Some(s) => {
|
||||
let deserialize: Note = serde_json::from_str(&s).unwrap();
|
||||
return Some(deserialize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn del(id: &String) {
|
||||
CLIENT.delete(id).unwrap();
|
||||
}
|
16
cryptgeon.code-workspace
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "packages/backend"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"cSpell.words": ["ciphertext", "cryptgeon"],
|
||||
"i18n-ally.enabledFrameworks": ["svelte"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": ["packages/frontend/locales"]
|
||||
}
|
||||
}
|
19
docker-compose.dev.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# DEV Compose file.
|
||||
# For a production file see: README.md
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:test
|
||||
build: .
|
||||
env_file: .dev.env
|
||||
depends_on:
|
||||
- redis
|
||||
ports:
|
||||
- 1234:8000
|
18
docker-compose.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
# Set a size limit. See link below on how to customise.
|
||||
# https://redis.io/docs/manual/eviction/
|
||||
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
# Size limit for a single note.
|
||||
SIZE_LIMIT: 4 MiB
|
||||
ports:
|
||||
- 80:8000
|
@@ -1,21 +0,0 @@
|
||||
# DEV Compose file.
|
||||
# For a production file see: README.md
|
||||
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
memcached:
|
||||
image: memcached:1-alpine
|
||||
restart: unless-stopped
|
||||
entrypoint: memcached -m 256M -I 128M
|
||||
ports:
|
||||
- 11211:11211
|
||||
|
||||
app:
|
||||
build: .
|
||||
depends_on:
|
||||
- memcached
|
||||
environment:
|
||||
SIZE_LIMIT: 128M
|
||||
ports:
|
||||
- 80:5000
|
13
entry.sh
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
HOST=$(echo $MEMCACHE | cut -d: -f1)
|
||||
PORT=$(echo $MEMCACHE | cut -d: -f2)
|
||||
echo "Waiting for memcached at $HOST:$PORT"
|
||||
while ! nc -z -w 1 $HOST $PORT; do
|
||||
sleep 1
|
||||
echo "retrying..."
|
||||
done
|
||||
|
||||
echo "Starting server"
|
||||
/app/cryptgeon
|
@@ -1,14 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
memcached:
|
||||
image: memcached:1-alpine
|
||||
entrypoint: memcached -m 256 -I 128 # Limit to 128 MB Ram, customize at free will. -m must be at least double than -I.
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
depends_on:
|
||||
- memcached
|
||||
- redis
|
||||
|
||||
proxy:
|
||||
image: nginx:alpine
|
||||
|
@@ -4,7 +4,7 @@ server {
|
||||
server_name _;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:5000/;
|
||||
proxy_pass http://app:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
@@ -20,7 +20,7 @@ server {
|
||||
ssl_trusted_certificate /path/to/fullchain.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:5000/;
|
||||
proxy_pass http://app:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
@@ -7,7 +7,7 @@ This is a tiny guide to install cryptgeon on (probably) any unix system (and may
|
||||
3. Run the cryptgeon.
|
||||
4. [Optional] install watchtower to keep up to date.
|
||||
|
||||
## Install Docker & DOcker Compose
|
||||
## Install Docker & Docker Compose
|
||||
|
||||
- [Docker](https://docs.docker.com/engine/install/)
|
||||
- [Compose](https://docs.docker.com/compose/install/)
|
||||
@@ -107,16 +107,15 @@ networks:
|
||||
external: true
|
||||
|
||||
services:
|
||||
memcached:
|
||||
image: memcached:1-alpine
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
entrypoint: memcached -m 256M -I 4M # Limit to 128 MB Ram, customize at free will.
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- memcached
|
||||
- redis
|
||||
environment:
|
||||
SIZE_LIMIT: 4 MiB
|
||||
networks:
|
||||
|
36
examples/traefik/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Install Cryptgeon with Traefik
|
||||
|
||||
Assumptions:
|
||||
|
||||
- Traefik 2 installed.
|
||||
- External proxy docker network `proxy`.
|
||||
- A certificate resolver `le`.
|
||||
- A https entrypoint `secure`.
|
||||
- Domain name `example.org`.
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
|
||||
app:
|
||||
image: cupcakearmy/cryptgeon:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- default
|
||||
- proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
|
||||
- traefik.http.routers.cryptgeon.entrypoints=secure
|
||||
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
||||
```
|
@@ -1,5 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/.svelte
|
||||
/build
|
||||
/functions
|
@@ -1,7 +0,0 @@
|
||||
├─ MIT: 46
|
||||
├─ MIT*: 2
|
||||
├─ BSD-3-Clause: 2
|
||||
├─ ISC: 1
|
||||
├─ 0BSD: 1
|
||||
└─ Apache-2.0: 1
|
||||
|
|
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "svelte-kit dev",
|
||||
"build": "svelte-kit build",
|
||||
"preview": "svelte-kit preview",
|
||||
"check": "svelte-check --tsconfig tsconfig.json",
|
||||
"licenses": "license-checker --summary > licenses.csv",
|
||||
"locale:download": "node scripts/locale.js"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@lokalise/node-api": "^7.2.0",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.34",
|
||||
"@sveltejs/kit": "^1.0.0-next.348",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/sanitize-html": "^2.6.2",
|
||||
"adm-zip": "^0.5.9",
|
||||
"dotenv": "^16.0.1",
|
||||
"svelte": "^3.48.0",
|
||||
"svelte-check": "^2.7.2",
|
||||
"svelte-intl-precompile": "^0.10.1",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.3",
|
||||
"vite": "^2.9.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.8",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"sanitize-html": "^2.7.0"
|
||||
}
|
||||
}
|
1644
frontend/pnpm-lock.yaml
generated
@@ -1,71 +0,0 @@
|
||||
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)
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
export class Files {
|
||||
static toString(f: File | Blob): Promise<string> {
|
||||
const reader = new window.FileReader()
|
||||
reader.readAsDataURL(f)
|
||||
return new Promise((resolve) => {
|
||||
reader.onloadend = () => resolve(reader.result as string)
|
||||
})
|
||||
}
|
||||
|
||||
static async fromString(s: string): Promise<Blob> {
|
||||
return fetch(s).then((r) => r.blob())
|
||||
}
|
||||
}
|
@@ -1,200 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { create, Note, PayloadToLargeError } from '$lib/api'
|
||||
import { encrypt, getKeyFromString, getRandomBytes, Hex } from '$lib/crypto'
|
||||
import { status } from '$lib/stores/status'
|
||||
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 { t } from 'svelte-intl-precompile'
|
||||
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 timeExpiration = false
|
||||
let message = ''
|
||||
let loading = false
|
||||
let error: string | null = null
|
||||
|
||||
$: if (!advanced) {
|
||||
note.views = 1
|
||||
timeExpiration = false
|
||||
}
|
||||
|
||||
$: {
|
||||
message = $t('home.explanation', {
|
||||
values: {
|
||||
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
||||
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
$: note.meta.type = file ? 'file' : 'text'
|
||||
|
||||
$: if (!file) {
|
||||
note.contents = ''
|
||||
}
|
||||
|
||||
class EmptyContentError extends Error {}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
error = null
|
||||
loading = true
|
||||
const password = Hex.encode(getRandomBytes(32))
|
||||
const key = await getKeyFromString(password)
|
||||
if (note.contents === '') throw new EmptyContentError()
|
||||
const data: Note = {
|
||||
contents: await encrypt(note.contents, key),
|
||||
meta: note.meta,
|
||||
}
|
||||
if (timeExpiration) data.expiration = parseInt(note.expiration as any)
|
||||
else data.views = parseInt(note.views as any)
|
||||
|
||||
const response = await create(data)
|
||||
result = {
|
||||
password: password,
|
||||
id: response.id,
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof PayloadToLargeError) {
|
||||
error = $t('home.errors.note_to_big')
|
||||
} else if (e instanceof EmptyContentError) {
|
||||
error = $t('home.errors.empty_content')
|
||||
} else {
|
||||
error = $t('home.errors.note_error')
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if result}
|
||||
<TextInput
|
||||
type="text"
|
||||
readonly
|
||||
label={$t('common.share_link')}
|
||||
value="{window.location.origin}/note/{result.id}#{result.password}"
|
||||
copy
|
||||
/>
|
||||
<br />
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
<br />
|
||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||
{:else}
|
||||
<p>
|
||||
{@html $t('home.intro')}
|
||||
</p>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<fieldset disabled={loading}>
|
||||
{#if file}
|
||||
<FileUpload label={$t('common.file')} on:file={(f) => (note.contents = f.detail)} />
|
||||
{:else}
|
||||
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
|
||||
{/if}
|
||||
|
||||
<div class="bottom">
|
||||
<Switch class="file" label={$t('common.file')} bind:value={file} />
|
||||
{#if $status?.allow_advanced}
|
||||
<Switch label={$t('common.advanced')} bind:value={advanced} />
|
||||
{/if}
|
||||
<div class="grow" />
|
||||
<div class="tr">
|
||||
<small>{$t('common.max')}: <MaxSize /> </small>
|
||||
<br />
|
||||
<Button type="submit">{$t('common.create')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="error-text">{error}</div>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
<br />
|
||||
{#if loading}
|
||||
{$t('common.loading')}
|
||||
{:else}
|
||||
{message}
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
{#if advanced}
|
||||
<div transition:blur={{ duration: 250 }}>
|
||||
<br />
|
||||
<div class="fields">
|
||||
<TextInput
|
||||
type="number"
|
||||
label={$t('common.views', { values: { n: 0 } })}
|
||||
bind:value={note.views}
|
||||
disabled={timeExpiration}
|
||||
max={$status?.max_views}
|
||||
validate={(v) =>
|
||||
($status && v < $status?.max_views) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
||||
/>
|
||||
<div class="middle-switch">
|
||||
<Switch label={$t('common.mode')} bind:value={timeExpiration} color={false} />
|
||||
</div>
|
||||
<TextInput
|
||||
type="number"
|
||||
label={$t('common.minutes', { values: { n: 0 } })}
|
||||
bind:value={note.expiration}
|
||||
disabled={!timeExpiration}
|
||||
max={$status?.max_expiration}
|
||||
validate={(v) =>
|
||||
($status && v < $status?.max_expiration) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</fieldset>
|
||||
</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>
|
@@ -1,107 +0,0 @@
|
||||
<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: 3rem;
|
||||
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>
|
@@ -1,83 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit'
|
||||
|
||||
export const load: Load = async ({ params }) => {
|
||||
return {
|
||||
props: params,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import 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'
|
||||
|
||||
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 = false
|
||||
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">{$t('show.errors.not_found')}</p>
|
||||
{:else if note && !error}
|
||||
<ShowNote {note} />
|
||||
{:else}
|
||||
<form on:submit|preventDefault={show}>
|
||||
<fieldset>
|
||||
<p>{$t('show.explanation')}</p>
|
||||
<Button type="submit">{$t('show.show_note')}</Button>
|
||||
{#if error}
|
||||
<br />
|
||||
<p class="error-text">
|
||||
{$t('show.errors.decryption_failed')}
|
||||
<br />
|
||||
</p>
|
||||
{/if}
|
||||
</fieldset>
|
||||
</form>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if loading}
|
||||
<p>{$t('common.loading')}</p>
|
||||
{/if}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json"
|
||||
}
|
13
package.json
@@ -1,12 +1,17 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev:docker": "docker-compose up memcached",
|
||||
"dev:backend": "cd backend && cargo watch -x 'run --bin cryptgeon'",
|
||||
"dev:front": "pnpm --prefix frontend run dev",
|
||||
"dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
|
||||
"dev:packages": "pnpm --parallel run dev",
|
||||
"dev:proxy": "node proxy.mjs",
|
||||
"dev": "run-p dev:*"
|
||||
"dev": "run-p dev:*",
|
||||
"test": "playwright test --project chrome firefox safari",
|
||||
"test:local": "playwright test --project local",
|
||||
"test:server": "docker compose -f docker-compose.dev.yaml up",
|
||||
"test:prepare": "docker compose -f docker-compose.dev.yaml build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.29.2",
|
||||
"@types/node": "^16.18.11",
|
||||
"http-proxy": "^1.18.1",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
|
701
backend/Cargo.lock → packages/backend/Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cryptgeon"
|
||||
version = "1.5.3"
|
||||
version = "2.2.0"
|
||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -18,7 +18,8 @@ serde_json = "1"
|
||||
lazy_static = "1"
|
||||
ring = "0.16"
|
||||
bs62 = "0.1"
|
||||
memcache = "0.16"
|
||||
byte-unit = "4"
|
||||
dotenv = "0.15"
|
||||
mime = "0.3"
|
||||
env_logger = "0.9"
|
||||
redis = "0.21.5"
|
10
packages/backend/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cargo watch -x 'run --bin cryptgeon'",
|
||||
"build": "cargo build --release",
|
||||
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:1234 cargo run",
|
||||
"test:prepare": "cargo build"
|
||||
}
|
||||
}
|
@@ -1,14 +1,17 @@
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_web::{web, Result};
|
||||
|
||||
use crate::config;
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
Files::new("/", "./frontend/build")
|
||||
Files::new("/", config::FRONTEND_PATH.to_string())
|
||||
.index_file("index.html")
|
||||
.use_etag(true),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn index() -> Result<NamedFile> {
|
||||
Ok(NamedFile::open("./frontend/build/index.html")?)
|
||||
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
|
||||
Ok(NamedFile::open(index)?)
|
||||
}
|
53
packages/backend/src/config.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use byte_unit::Byte;
|
||||
|
||||
// Internal
|
||||
lazy_static! {
|
||||
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
pub static ref FRONTEND_PATH: String =
|
||||
std::env::var("FRONTEND_PATH").unwrap_or("../frontend/build".to_string());
|
||||
pub static ref LISTEN_ADDR: String =
|
||||
std::env::var("LISTEN_ADDR").unwrap_or("0.0.0.0:8000".to_string());
|
||||
pub static ref VERBOSITY: String = std::env::var("VERBOSITY").unwrap_or("warn".to_string());
|
||||
}
|
||||
|
||||
// CONFIG
|
||||
lazy_static! {
|
||||
pub static ref LIMIT: usize =
|
||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||
.unwrap()
|
||||
.get_bytes() as usize;
|
||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
|
||||
.unwrap_or("360".to_string()) // 6 hours in minutes
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// THEME
|
||||
lazy_static! {
|
||||
pub static ref THEME_IMAGE: String = std::env::var("THEME_IMAGE")
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref THEME_TEXT: String = std::env::var("THEME_TEXT")
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref THEME_PAGE_TITLE: String = std::env::var("THEME_PAGE_TITLE")
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref THEME_FAVICON: String = std::env::var("THEME_FAVICON")
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
@@ -1,4 +1,7 @@
|
||||
use actix_web::{middleware, web, App, HttpServer};
|
||||
use actix_web::{
|
||||
middleware::{self, Logger},
|
||||
web, App, HttpServer,
|
||||
};
|
||||
use dotenv::dotenv;
|
||||
|
||||
#[macro_use]
|
||||
@@ -15,8 +18,11 @@ mod store;
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv().ok();
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
|
||||
|
||||
return HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(Logger::new("\"%r\" %s %b %T"))
|
||||
.wrap(middleware::Compress::default())
|
||||
.wrap(middleware::DefaultHeaders::default())
|
||||
.configure(size::init)
|
||||
@@ -24,7 +30,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.configure(client::init)
|
||||
.default_service(web::to(client::index))
|
||||
})
|
||||
.bind("0.0.0.0:5000")?
|
||||
.bind(config::LISTEN_ADDR.to_string())?
|
||||
.run()
|
||||
.await;
|
||||
}
|
@@ -22,9 +22,11 @@ struct NotePath {
|
||||
async fn one(path: web::Path<NotePath>) -> impl Responder {
|
||||
let p = path.into_inner();
|
||||
let note = store::get(&p.id);
|
||||
|
||||
match note {
|
||||
None => return HttpResponse::NotFound().finish(),
|
||||
Some(_) => return HttpResponse::Ok().json(NoteInfo {}),
|
||||
Ok(Some(_)) => HttpResponse::Ok().json(NoteInfo {}),
|
||||
Ok(None) => HttpResponse::NotFound().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +49,7 @@ async fn create(note: web::Json<Note>) -> impl Responder {
|
||||
}
|
||||
match n.views {
|
||||
Some(v) => {
|
||||
if v > *config::MAX_VIEWS {
|
||||
if v > *config::MAX_VIEWS || v < 1 {
|
||||
return bad_req;
|
||||
}
|
||||
n.expiration = None; // views overrides expiration
|
||||
@@ -64,8 +66,10 @@ async fn create(note: web::Json<Note>) -> impl Responder {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
store::set(&id.clone(), &n.clone());
|
||||
return HttpResponse::Ok().json(CreateResponse { id: id });
|
||||
match store::set(&id.clone(), &n.clone()) {
|
||||
Ok(_) => return HttpResponse::Ok().json(CreateResponse { id: id }),
|
||||
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/{id}")]
|
||||
@@ -73,8 +77,9 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
|
||||
let p = path.into_inner();
|
||||
let note = store::get(&p.id);
|
||||
match note {
|
||||
None => return HttpResponse::NotFound().finish(),
|
||||
Some(note) => {
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Ok(None) => return HttpResponse::NotFound().finish(),
|
||||
Ok(Some(note)) => {
|
||||
let mut changed = note.clone();
|
||||
if changed.views == None && changed.expiration == None {
|
||||
return HttpResponse::BadRequest().finish();
|
||||
@@ -84,9 +89,19 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
|
||||
changed.views = Some(v - 1);
|
||||
let id = p.id.clone();
|
||||
if v <= 1 {
|
||||
store::del(&id);
|
||||
match store::del(&id) {
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().body(e.to_string())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
store::set(&id, &changed.clone());
|
||||
match store::set(&id, &changed.clone()) {
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().body(e.to_string())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -96,8 +111,12 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
|
||||
match changed.expiration {
|
||||
Some(e) => {
|
||||
if e < n {
|
||||
store::del(&p.id.clone());
|
||||
return HttpResponse::BadRequest().finish();
|
||||
match store::del(&p.id.clone()) {
|
||||
Ok(_) => return HttpResponse::BadRequest().finish(),
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().body(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
12
packages/backend/src/size.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use crate::config;
|
||||
use actix_web::web;
|
||||
use mime;
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
let json = web::JsonConfig::default().limit(*config::LIMIT);
|
||||
let plain = web::PayloadConfig::default()
|
||||
.limit(*config::LIMIT)
|
||||
.mimetype(mime::STAR_STAR);
|
||||
// cfg.app_data(plain);
|
||||
cfg.app_data(json).app_data(plain);
|
||||
}
|
@@ -2,9 +2,16 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Status {
|
||||
// General
|
||||
pub version: String,
|
||||
// Config
|
||||
pub max_size: u32,
|
||||
pub max_views: u32,
|
||||
pub max_expiration: u32,
|
||||
pub allow_advanced: bool,
|
||||
// Theme
|
||||
pub theme_image: String,
|
||||
pub theme_text: String,
|
||||
pub theme_page_title: String,
|
||||
pub theme_favicon: String,
|
||||
}
|
@@ -7,10 +7,14 @@ use crate::status::Status;
|
||||
async fn get_status() -> impl Responder {
|
||||
return HttpResponse::Ok().json(Status {
|
||||
version: config::VERSION.to_string(),
|
||||
max_size: *config::LIMIT,
|
||||
max_size: *config::LIMIT as u32,
|
||||
max_views: *config::MAX_VIEWS,
|
||||
max_expiration: *config::MAX_EXPIRATION,
|
||||
allow_advanced: *config::ALLOW_ADVANCED,
|
||||
theme_image: config::THEME_IMAGE.to_string(),
|
||||
theme_text: config::THEME_TEXT.to_string(),
|
||||
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
|
||||
theme_favicon: config::THEME_FAVICON.to_string()
|
||||
});
|
||||
}
|
||||
|
55
packages/backend/src/store.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use redis;
|
||||
use redis::Commands;
|
||||
|
||||
use crate::note::now;
|
||||
use crate::note::Note;
|
||||
|
||||
lazy_static! {
|
||||
static ref REDIS_CLIENT: String = std::env::var("REDIS")
|
||||
.unwrap_or("redis://127.0.0.1/".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn get_connection() -> Result<redis::Connection, &'static str> {
|
||||
let client =
|
||||
redis::Client::open(REDIS_CLIENT.to_string()).map_err(|_| "Unable to connect to redis")?;
|
||||
client
|
||||
.get_connection()
|
||||
.map_err(|_| "Unable to connect to redis")
|
||||
}
|
||||
|
||||
pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
|
||||
let serialized = serde_json::to_string(¬e.clone()).unwrap();
|
||||
let mut conn = get_connection()?;
|
||||
|
||||
conn.set(id, serialized)
|
||||
.map_err(|_| "Unable to set note in redis")?;
|
||||
match note.expiration {
|
||||
Some(e) => {
|
||||
let seconds = e - now();
|
||||
conn.expire(id, seconds as usize)
|
||||
.map_err(|_| "Unable to set expiration on notion")?
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(id: &String) -> Result<Option<Note>, &'static str> {
|
||||
let mut conn = get_connection()?;
|
||||
let value: Option<String> = conn.get(id).map_err(|_| "Could not load note in redis")?;
|
||||
match value {
|
||||
None => return Ok(None),
|
||||
Some(s) => {
|
||||
let deserialize: Note = serde_json::from_str(&s).unwrap();
|
||||
return Ok(Some(deserialize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn del(id: &String) -> Result<(), &'static str> {
|
||||
let mut conn = get_connection()?;
|
||||
conn.del(id).map_err(|_| "Unable to delete note in redis")?;
|
||||
Ok(())
|
||||
}
|
8
packages/frontend/licenses.csv
Normal file
@@ -0,0 +1,8 @@
|
||||
├─ MIT: 13
|
||||
├─ ISC: 2
|
||||
├─ BSD-3-Clause: 1
|
||||
├─ (MPL-2.0 OR Apache-2.0): 1
|
||||
├─ BSD-2-Clause: 1
|
||||
├─ 0BSD: 1
|
||||
└─ Apache-2.0: 1
|
||||
|
|
@@ -4,13 +4,19 @@
|
||||
"file": "Datei",
|
||||
"advanced": "erweitert",
|
||||
"create": "erstellen",
|
||||
"loading": "Läd...",
|
||||
"loading": "läd",
|
||||
"mode": "Modus",
|
||||
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
|
||||
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
|
||||
"max": "max",
|
||||
"share_link": "Link teilen",
|
||||
"copy_clipboard": "in die Zwischenablage kopieren"
|
||||
"copy_clipboard": "in die Zwischenablage kopieren",
|
||||
"copied_to_clipboard": "in die Zwischenablage kopiert",
|
||||
"encrypting": "verschlüsseln",
|
||||
"decrypting": "entschlüsselt",
|
||||
"uploading": "hochladen",
|
||||
"downloading": "wird heruntergeladen",
|
||||
"qr_code": "qr-code"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Senden Sie ganz einfach <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.",
|
||||
@@ -23,12 +29,15 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "Notiz ist leer."
|
||||
},
|
||||
"copied_to_clipboard": "in die Zwischenablage kopiert 🔗"
|
||||
"messages": {
|
||||
"note_created": "notiz erstellt."
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
|
||||
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört."
|
||||
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört.",
|
||||
"unsupported_type": "nicht unterstützter Notiztyp."
|
||||
},
|
||||
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
|
||||
"show_note": "Notiz anzeigen",
|
@@ -4,13 +4,19 @@
|
||||
"file": "file",
|
||||
"advanced": "advanced",
|
||||
"create": "create",
|
||||
"loading": "loading...",
|
||||
"loading": "loading",
|
||||
"mode": "mode",
|
||||
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
|
||||
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
||||
"max": "max",
|
||||
"share_link": "share link",
|
||||
"copy_clipboard": "copy to clipboard"
|
||||
"copy_clipboard": "copy to clipboard",
|
||||
"copied_to_clipboard": "copied to clipboard",
|
||||
"encrypting": "encrypting",
|
||||
"decrypting": "decrypting",
|
||||
"uploading": "uploading",
|
||||
"downloading": "downloading",
|
||||
"qr_code": "qr code"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
||||
@@ -23,12 +29,15 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "note is empty."
|
||||
},
|
||||
"copied_to_clipboard": "copied to clipboard 🔗"
|
||||
"messages": {
|
||||
"note_created": "note created."
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "note was not found or was already deleted.",
|
||||
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed."
|
||||
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
|
||||
"unsupported_type": "unsupported note type."
|
||||
},
|
||||
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
||||
"show_note": "show note",
|
@@ -4,13 +4,19 @@
|
||||
"file": "archivo",
|
||||
"advanced": "avanzado",
|
||||
"create": "crear",
|
||||
"loading": "cargando...",
|
||||
"loading": "cargando",
|
||||
"mode": "modo",
|
||||
"views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}",
|
||||
"minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}",
|
||||
"max": "max",
|
||||
"share_link": "compartir enlace",
|
||||
"copy_clipboard": "copiar al portapapeles"
|
||||
"copy_clipboard": "copiar al portapapeles",
|
||||
"copied_to_clipboard": "copiado al portapapeles",
|
||||
"encrypting": "encriptando",
|
||||
"decrypting": "descifrando",
|
||||
"uploading": "cargando",
|
||||
"downloading": "descargando",
|
||||
"qr_code": "código qr"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Envía fácilmente notas o archivos <i>totalmente encriptados</i> y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.",
|
||||
@@ -23,12 +29,15 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "la nota está vacía."
|
||||
},
|
||||
"copied_to_clipboard": "copiado al portapapeles 🔗"
|
||||
"messages": {
|
||||
"note_created": "nota creada."
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "la nota no se encontró o ya fue borrada.",
|
||||
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida."
|
||||
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida.",
|
||||
"unsupported_type": "tipo de nota no compatible."
|
||||
},
|
||||
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
|
||||
"show_note": "mostrar nota",
|
@@ -4,13 +4,19 @@
|
||||
"file": "fichier",
|
||||
"advanced": "avancé",
|
||||
"create": "créer",
|
||||
"loading": "chargement...",
|
||||
"loading": "chargement",
|
||||
"mode": "mode",
|
||||
"views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}",
|
||||
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
||||
"max": "max",
|
||||
"share_link": "partager le lien",
|
||||
"copy_clipboard": "copier dans le presse-papiers"
|
||||
"copy_clipboard": "copier dans le presse-papiers",
|
||||
"copied_to_clipboard": "copié dans le presse-papiers",
|
||||
"encrypting": "cryptage",
|
||||
"decrypting": "déchiffrer",
|
||||
"uploading": "téléchargement",
|
||||
"downloading": "téléchargement",
|
||||
"qr_code": "code qr"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Envoyez facilement des notes ou des fichiers <i>entièrement cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.",
|
||||
@@ -23,12 +29,15 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "La note est vide."
|
||||
},
|
||||
"copied_to_clipboard": "copié dans le presse-papiers 🔗"
|
||||
"messages": {
|
||||
"note_created": "note créée."
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "La note n'a pas été trouvée ou a déjà été supprimée.",
|
||||
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite."
|
||||
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite.",
|
||||
"unsupported_type": "type de note non supporté."
|
||||
},
|
||||
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
||||
"show_note": "note de présentation",
|
@@ -4,13 +4,19 @@
|
||||
"file": "file",
|
||||
"advanced": "avanzato",
|
||||
"create": "crea",
|
||||
"loading": "carica...",
|
||||
"loading": "carica",
|
||||
"mode": "modalita",
|
||||
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
|
||||
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
|
||||
"max": "max",
|
||||
"share_link": "condividi link",
|
||||
"copy_clipboard": "copia negli appunti"
|
||||
"copy_clipboard": "copia negli appunti",
|
||||
"copied_to_clipboard": "copiato negli appunti",
|
||||
"encrypting": "criptando",
|
||||
"decrypting": "decifrando",
|
||||
"uploading": "caricamento",
|
||||
"downloading": "scaricando",
|
||||
"qr_code": "codice qr"
|
||||
},
|
||||
"home": {
|
||||
"intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.",
|
||||
@@ -23,12 +29,15 @@
|
||||
"max": "max: {n}",
|
||||
"empty_content": "la nota è vuota."
|
||||
},
|
||||
"copied_to_clipboard": "copiato negli appunti 🔗"
|
||||
"messages": {
|
||||
"note_created": "nota creata."
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "non è stata trovata o è stata già cancellata.",
|
||||
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta."
|
||||
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta.",
|
||||
"unsupported_type": "tipo di nota non supportato."
|
||||
},
|
||||
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
|
||||
"show_note": "mostra la nota",
|
@@ -4,13 +4,19 @@
|
||||
"file": "上传文件",
|
||||
"advanced": "高级设置",
|
||||
"create": "创建",
|
||||
"loading": "加载中...",
|
||||
"loading": "加载中",
|
||||
"mode": "模式",
|
||||
"views": "{n, plural, =0 {可查看次数} =1 {1 次查看} other {# 次查看}}",
|
||||
"minutes": "{n, plural, =0 {有效期(分钟)} =1 {1 分钟} other {# 分钟}}",
|
||||
"max": "最大值",
|
||||
"share_link": "分享链接",
|
||||
"copy_clipboard": "复制到剪切版"
|
||||
"copy_clipboard": "复制到剪切版",
|
||||
"copied_to_clipboard": "已复制到剪切板",
|
||||
"encrypting": "加密",
|
||||
"decrypting": "解密",
|
||||
"uploading": "上传",
|
||||
"downloading": "下载",
|
||||
"qr_code": "二维码"
|
||||
},
|
||||
"home": {
|
||||
"intro": "一键轻松发送 <i>完全加密的</i> 密信或者文件。只需创建一个密信然后分享链接。",
|
||||
@@ -23,12 +29,15 @@
|
||||
"max": "最大文件大小: {n}",
|
||||
"empty_content": "密信为空!"
|
||||
},
|
||||
"copied_to_clipboard": "已复制到剪切板 🔗"
|
||||
"messages": {
|
||||
"note_created": "注释创建。"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"errors": {
|
||||
"not_found": "该密信无法被找到或者它已经被删除了!",
|
||||
"decryption_failed": "密钥错误!您可能不小心粘贴了一个不完整的链接或者正在尝试破解该密信!但无论如何,该密信已被销毁!"
|
||||
"decryption_failed": "密钥错误!您可能不小心粘贴了一个不完整的链接或者正在尝试破解该密信!但无论如何,该密信已被销毁!",
|
||||
"unsupported_type": "不支持的票据类型。"
|
||||
},
|
||||
"explanation": "点击下方的按钮可以查看密信,如果它到达了限制将会被删除",
|
||||
"show_note": "查看密信",
|
39
packages/frontend/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig tsconfig.json",
|
||||
"licenses": "license-checker --summary > licenses.csv",
|
||||
"locale:download": "node scripts/locale.js",
|
||||
"test:prepare": "pnpm run build"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@lokalise/node-api": "^9.5.0",
|
||||
"@sveltejs/adapter-static": "^1.0.2",
|
||||
"@sveltejs/kit": "^1.1.0",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@zerodevx/svelte-toast": "^0.7.2",
|
||||
"adm-zip": "^0.5.10",
|
||||
"dotenv": "^16.0.3",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^2.10.3",
|
||||
"svelte-intl-precompile": "^0.10.1",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dompurify": "^2.4.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"occulto": "2.0.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"qrious": "^4.0.2"
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import dotenv from 'dotenv'
|
||||
import { LokaliseApi } from '@lokalise/node-api'
|
||||
import https from 'https'
|
||||
import AdmZip from 'adm-zip'
|
||||
import dotenv from 'dotenv'
|
||||
import https from 'https'
|
||||
|
||||
dotenv.config()
|
||||
|
@@ -14,7 +14,9 @@
|
||||
--ui-text-0: #fefefe;
|
||||
--ui-text-1: #eee;
|
||||
--ui-clr-primary: hsl(186, 65%, 55%);
|
||||
--ui-clr-primary-alt: hsl(186, 85%, 35%);
|
||||
--ui-clr-error: hsl(357, 77%, 51%);
|
||||
--ui-clr-error-alt: hsl(357, 87%, 41%);
|
||||
|
||||
--ui-anim: all 150ms ease;
|
||||
}
|
||||
@@ -85,6 +87,8 @@ button {
|
||||
font-size: inherit;
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding-inline: initial;
|
||||
}
|
||||
|
||||
*:disabled,
|
@@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
%sveltekit.head%
|
60
packages/frontend/src/lib/adapters.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { AES, Bytes, type TypedArray } from 'occulto'
|
||||
import type { EncryptedFileDTO, FileDTO } from './api'
|
||||
|
||||
abstract class CryptAdapter<T> {
|
||||
abstract encrypt(plaintext: T, key: TypedArray): Promise<string>
|
||||
abstract decrypt(ciphertext: string, key: TypedArray): Promise<T>
|
||||
}
|
||||
|
||||
class CryptTextAdapter implements CryptAdapter<string> {
|
||||
async encrypt(plaintext: string, key: TypedArray) {
|
||||
return await AES.encrypt(Bytes.encode(plaintext), key)
|
||||
}
|
||||
async decrypt(ciphertext: string, key: TypedArray) {
|
||||
return Bytes.decode(await AES.decrypt(ciphertext, key))
|
||||
}
|
||||
}
|
||||
|
||||
class CryptBlobAdapter implements CryptAdapter<Blob> {
|
||||
async encrypt(plaintext: Blob, key: TypedArray) {
|
||||
return await AES.encrypt(new Uint8Array(await plaintext.arrayBuffer()), key)
|
||||
}
|
||||
|
||||
async decrypt(ciphertext: string, key: TypedArray) {
|
||||
const plaintext = await AES.decrypt(ciphertext, key)
|
||||
return new Blob([plaintext], { type: 'application/octet-stream' })
|
||||
}
|
||||
}
|
||||
|
||||
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
|
||||
async encrypt(plaintext: FileDTO[], key: TypedArray) {
|
||||
const adapter = new CryptBlobAdapter()
|
||||
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
contents: await adapter.encrypt(file.contents, key),
|
||||
}))
|
||||
return JSON.stringify(await Promise.all(data))
|
||||
}
|
||||
|
||||
async decrypt(ciphertext: string, key: TypedArray) {
|
||||
const adapter = new CryptBlobAdapter()
|
||||
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
|
||||
const files: FileDTO[] = await Promise.all(
|
||||
data.map(async (file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
contents: await adapter.decrypt(file.contents, key),
|
||||
}))
|
||||
)
|
||||
return files
|
||||
}
|
||||
}
|
||||
|
||||
export const Adapters = {
|
||||
Text: new CryptTextAdapter(),
|
||||
Blob: new CryptBlobAdapter(),
|
||||
Files: new CryptFilesAdapter(),
|
||||
}
|
@@ -11,6 +11,10 @@ export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
||||
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
||||
|
||||
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
||||
contents: Blob
|
||||
}
|
||||
|
||||
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
||||
contents: string
|
||||
}
|
||||
|
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 287 B |
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 736 B After Width: | Height: | Size: 736 B |
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 483 B |
Before Width: | Height: | Size: 732 B After Width: | Height: | Size: 732 B |
@@ -7,6 +7,10 @@ export type Status = {
|
||||
max_views: number
|
||||
max_expiration: number
|
||||
allow_advanced: boolean
|
||||
theme_image: string
|
||||
theme_text: string
|
||||
theme_favicon: string
|
||||
theme_page_title: string
|
||||
}
|
||||
|
||||
export const status = writable<null | Status>(null)
|
37
packages/frontend/src/lib/toast.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { toast, type SvelteToastOptions } from '@zerodevx/svelte-toast'
|
||||
|
||||
export enum NotifyType {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
const themeMapping: Record<NotifyType, SvelteToastOptions['theme']> = {
|
||||
[NotifyType.Success]: {
|
||||
'--toastBackground': 'var(--ui-clr-primary)',
|
||||
'--toastBarBackground': 'var(--ui-clr-primary-alt)',
|
||||
},
|
||||
[NotifyType.Error]: {
|
||||
'--toastBackground': 'var(--ui-clr-error)',
|
||||
'--toastBarBackground': 'var(--ui-clr-error-alt)',
|
||||
},
|
||||
}
|
||||
|
||||
function notifyFN(message: string, type: NotifyType = NotifyType.Success) {
|
||||
const options: SvelteToastOptions = {
|
||||
duration: 5_000,
|
||||
theme: {
|
||||
...themeMapping[type],
|
||||
'--toastBarHeight': '0.25rem',
|
||||
'--toastMinHeight': 'auto',
|
||||
'--toastMsgPadding': '0.5rem',
|
||||
'--toastBorderRadius': '0',
|
||||
},
|
||||
}
|
||||
|
||||
toast.push(message, options)
|
||||
}
|
||||
|
||||
export const notify = {
|
||||
success: (message: string) => notifyFN(message, NotifyType.Success),
|
||||
error: (message: string) => notifyFN(message, NotifyType.Error),
|
||||
}
|
55
packages/frontend/src/lib/ui/AdvancedParameters.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import type { Note } from '$lib/api'
|
||||
import { status } from '$lib/stores/status'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
|
||||
export let note: Note
|
||||
export let timeExpiration = false
|
||||
</script>
|
||||
|
||||
<div class="fields">
|
||||
<TextInput
|
||||
data-testid="field-views"
|
||||
type="number"
|
||||
label={$t('common.views', { values: { n: 0 } })}
|
||||
bind:value={note.views}
|
||||
disabled={timeExpiration}
|
||||
max={$status?.max_views}
|
||||
min={1}
|
||||
validate={(v) =>
|
||||
($status && v <= $status?.max_views && v > 0) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
||||
/>
|
||||
<div class="middle-switch">
|
||||
<Switch
|
||||
data-testid="switch-advanced-toggle"
|
||||
label={$t('common.mode')}
|
||||
bind:value={timeExpiration}
|
||||
color={false}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
data-testid="field-expiration"
|
||||
type="number"
|
||||
label={$t('common.minutes', { values: { n: 0 } })}
|
||||
bind:value={note.expiration}
|
||||
disabled={!timeExpiration}
|
||||
max={$status?.max_expiration}
|
||||
validate={(v) =>
|
||||
($status && v < $status?.max_expiration) ||
|
||||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.middle-switch {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
41
packages/frontend/src/lib/ui/Canvas.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import QR from 'qrious'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import { getCSSVariable } from '$lib/utils'
|
||||
|
||||
export let value: string
|
||||
|
||||
let canvas: HTMLCanvasElement
|
||||
|
||||
$: {
|
||||
new QR({
|
||||
value,
|
||||
level: 'Q',
|
||||
size: 800,
|
||||
background: getCSSVariable('--ui-bg-0'),
|
||||
foreground: getCSSVariable('--ui-text-0'),
|
||||
element: canvas,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<small>{$t('common.qr_code')}</small>
|
||||
<div>
|
||||
<canvas bind:this={canvas} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 0.5rem;
|
||||
width: fit-content;
|
||||
border: 2px solid var(--ui-bg-1);
|
||||
background-color: var(--ui-bg-0);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
@@ -1,38 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { FileDTO } from '$lib/api'
|
||||
import { Files } from '$lib/files'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import Button from './Button.svelte'
|
||||
import MaxSize from './MaxSize.svelte'
|
||||
|
||||
import type { FileDTO } from '$lib/api'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
let files: File[] = []
|
||||
export let files: FileDTO[] = []
|
||||
|
||||
const dispatch = createEventDispatcher<{ file: string }>()
|
||||
function fileToDTO(file: File): FileDTO {
|
||||
return {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
contents: file,
|
||||
}
|
||||
}
|
||||
|
||||
async function onInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (input?.files?.length) {
|
||||
files = [...files, ...Array.from(input.files)]
|
||||
const data: FileDTO[] = await Promise.all(
|
||||
files.map(async (file) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
contents: await Files.toString(file),
|
||||
}))
|
||||
)
|
||||
dispatch('file', JSON.stringify(data))
|
||||
} else {
|
||||
dispatch('file', '')
|
||||
files = [...files, ...Array.from(input.files).map(fileToDTO)]
|
||||
}
|
||||
}
|
||||
|
||||
function clear(e: Event) {
|
||||
e.preventDefault()
|
||||
files = []
|
||||
dispatch('file', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -40,7 +34,7 @@
|
||||
<small>
|
||||
{label}
|
||||
</small>
|
||||
<input type="file" on:change={onInput} multiple />
|
||||
<input {...$$restProps} type="file" on:change={onInput} multiple />
|
||||
<div class="box">
|
||||
{#if files.length}
|
||||
<div>
|
||||
@@ -57,7 +51,9 @@
|
||||
<div>
|
||||
<b>{$t('file_upload.no_files_selected')}</b>
|
||||
<br />
|
||||
<small>{$t('common.max')}: <MaxSize /></small>
|
||||
<small>
|
||||
{$t('common.max')}: <MaxSize />
|
||||
</small>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
41
packages/frontend/src/lib/ui/Loader.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 100 100"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<rect fill="none" stroke="currentColor" stroke-width="4" x="25" y="25" width="50" height="50">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="0.5s"
|
||||
from="0 50 50"
|
||||
to="180 50 50"
|
||||
type="rotate"
|
||||
id="strokeBox"
|
||||
attributeType="XML"
|
||||
begin="rectBox.end"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="27" y="27" fill="currentColor" width="46" height="50">
|
||||
<animate
|
||||
attributeName="height"
|
||||
dur="1.3s"
|
||||
attributeType="XML"
|
||||
from="50"
|
||||
to="0"
|
||||
id="rectBox"
|
||||
fill="freeze"
|
||||
begin="0s;strokeBox.end"
|
||||
/>
|
||||
</rect>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
height: 2em;
|
||||
position: relative;
|
||||
top: 0.6em;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
After Width: | Height: | Size: 784 B |
@@ -1,12 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { status } from '$lib/stores/status'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { _ } from 'svelte-intl-precompile'
|
||||
|
||||
import { status } from '$lib/stores/status'
|
||||
|
||||
// Due to encoding overhead (~35%) with base64
|
||||
// https://en.wikipedia.org/wiki/Base64
|
||||
const overhead = 1 / 1.35
|
||||
</script>
|
||||
|
||||
<span>
|
||||
{#if $status !== null}
|
||||
{prettyBytes($status.max_size, { binary: true })}
|
||||
{prettyBytes($status.max_size * overhead, { binary: true })}
|
||||
{:else}
|
||||
{$_('common.loading')}
|
||||
{/if}
|
49
packages/frontend/src/lib/ui/NoteResult.svelte
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" context="module">
|
||||
export type NoteResult = {
|
||||
password: string
|
||||
id: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import TextInput from '$lib/ui/TextInput.svelte'
|
||||
import Canvas from './Canvas.svelte'
|
||||
|
||||
export let result: NoteResult
|
||||
|
||||
$: url = `${window.location.origin}/note/${result.id}#${result.password}`
|
||||
|
||||
function reset() {
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<TextInput
|
||||
type="text"
|
||||
readonly
|
||||
label={$t('common.share_link')}
|
||||
value={url}
|
||||
copy
|
||||
data-testid="share-link"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Canvas value={url} />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{@html $t('home.new_note_notice')}
|
||||
</p>
|
||||
<br />
|
||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: min(12rem, 100%);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
@@ -1,20 +1,24 @@
|
||||
<script lang="ts" context="module">
|
||||
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { FileDTO, NotePublic } from '$lib/api'
|
||||
import { Files } from '$lib/files'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { saveAs } from 'file-saver'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import sanitize from 'sanitize-html'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import Button from './Button.svelte'
|
||||
|
||||
export let note: NotePublic
|
||||
import type { FileDTO, NotePublic } from '$lib/api'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import { copy } from '$lib/utils'
|
||||
|
||||
export let note: DecryptedNote
|
||||
|
||||
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
|
||||
let files: FileDTO[] = []
|
||||
|
||||
$: if (note.meta.type === 'file') {
|
||||
files = JSON.parse(note.contents) as FileDTO[]
|
||||
files = note.contents
|
||||
}
|
||||
|
||||
$: download = () => {
|
||||
@@ -24,36 +28,40 @@
|
||||
}
|
||||
|
||||
async function downloadFile(file: FileDTO) {
|
||||
const f = new File([await Files.fromString(file.contents)], file.name, {
|
||||
const f = new File([file.contents], file.name, {
|
||||
type: file.type,
|
||||
})
|
||||
saveAs(f)
|
||||
}
|
||||
|
||||
function contentWithLinks(content: string): string {
|
||||
const replaced = note.contents.replace(
|
||||
const replaced = content.replace(
|
||||
RE_URL,
|
||||
(url) => `<a href="${url}" rel="noreferrer">${url}</a>`
|
||||
)
|
||||
return sanitize(replaced, { allowedTags: ['a'], allowedAttributes: { a: ['href', 'rel'] } })
|
||||
return DOMPurify.sanitize(replaced, { USE_PROFILES: { html: true } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
|
||||
{#if note.meta.type === 'text'}
|
||||
<div class="note">
|
||||
{@html contentWithLinks(note.contents)}
|
||||
</div>
|
||||
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
||||
{:else}
|
||||
{#each files as file}
|
||||
<div class="note file">
|
||||
<b on:click={() => downloadFile(file)}>↓ {file.name}</b>
|
||||
<small> {file.type} - {prettyBytes(file.size)}</small>
|
||||
<div data-testid="result">
|
||||
{#if note.meta.type === 'text'}
|
||||
<div class="note">
|
||||
{@html contentWithLinks(note.contents)}
|
||||
</div>
|
||||
{/each}
|
||||
<Button on:click={download}>{$t('show.download_all')}</Button>
|
||||
{/if}
|
||||
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
||||
{:else}
|
||||
{#each files as file}
|
||||
<div class="note file">
|
||||
<button on:click={() => downloadFile(file)}>
|
||||
<b>↓ {file.name}</b>
|
||||
</button>
|
||||
<small> {file.type} - {prettyBytes(file.size)}</small>
|
||||
</div>
|
||||
{/each}
|
||||
<Button on:click={download}>{$t('show.download_all')}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.note {
|
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getRandomBytes, Hex } from '$lib/crypto'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { fade } from 'svelte/transition'
|
||||
import Icon from './Icon.svelte'
|
||||
import Icon from '$lib/ui/Icon.svelte'
|
||||
import { copy as copyFN } from '$lib/utils'
|
||||
import { getRandomBytes, Hex } from 'occulto'
|
||||
|
||||
export let label: string = ''
|
||||
export let value: any
|
||||
@@ -14,8 +12,6 @@
|
||||
const initialType = $$restProps.type
|
||||
const isPassword = initialType === 'password'
|
||||
let hidden = true
|
||||
let notification: string | null = null
|
||||
let notificationTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
$: valid = validate(value)
|
||||
|
||||
@@ -27,22 +23,9 @@
|
||||
function toggle() {
|
||||
hidden = !hidden
|
||||
}
|
||||
function copyFN() {
|
||||
copyToClipboard(value.toString())
|
||||
notify($t('home.copied_to_clipboard'))
|
||||
}
|
||||
function randomFN() {
|
||||
value = Hex.encode(getRandomBytes(20))
|
||||
}
|
||||
|
||||
function notify(msg: string, delay: number = 2000) {
|
||||
if (notificationTimeout) {
|
||||
clearTimeout(notificationTimeout)
|
||||
}
|
||||
notificationTimeout = setTimeout(() => {
|
||||
notification = null
|
||||
}, delay)
|
||||
notification = msg
|
||||
async function randomFN() {
|
||||
value = Hex.encode(await getRandomBytes(32))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -62,12 +45,9 @@
|
||||
<Icon class="icon" icon="dice" on:click={randomFN} />
|
||||
{/if}
|
||||
{#if copy}
|
||||
<Icon class="icon" icon="copy" on:click={copyFN} />
|
||||
<Icon class="icon" icon="copy" on:click={() => copyFN(value.toString())} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if notification}
|
||||
<div class="notification" transition:fade><small>{notification}</small></div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<style>
|
||||
@@ -117,11 +97,4 @@
|
||||
.icons > :global(.icon:hover) {
|
||||
border-color: var(--ui-clr-primary);
|
||||
}
|
||||
|
||||
.notification {
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -1.5em;
|
||||
}
|
||||
</style>
|
@@ -40,19 +40,19 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div on:click={change}>
|
||||
<button on:click={change}>
|
||||
<Icon class="icon" icon="contrast" />
|
||||
{$theme}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
div :global(.icon) {
|
||||
button :global(.icon) {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
div {
|
||||
button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
16
packages/frontend/src/lib/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import { notify } from './toast'
|
||||
|
||||
export function copy(value: string) {
|
||||
copyToClipboard(value)
|
||||
const msg = get(t)('common.copied_to_clipboard')
|
||||
notify.success(msg)
|
||||
}
|
||||
|
||||
export function getCSSVariable(variable: string): string {
|
||||
if (typeof window === 'undefined') return ''
|
||||
return window.getComputedStyle(window.document.body).getPropertyValue(variable)
|
||||
}
|
174
packages/frontend/src/lib/views/Create.svelte
Normal file
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { AES, Hex } from 'occulto'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
import { blur } from 'svelte/transition'
|
||||
|
||||
import { Adapters } from '$lib/adapters'
|
||||
import type { FileDTO, Note } from '$lib/api'
|
||||
import { create, PayloadToLargeError } from '$lib/api'
|
||||
import { status } from '$lib/stores/status'
|
||||
import { notify } from '$lib/toast'
|
||||
import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import FileUpload from '$lib/ui/FileUpload.svelte'
|
||||
import Loader from '$lib/ui/Loader.svelte'
|
||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||
import Switch from '$lib/ui/Switch.svelte'
|
||||
import TextArea from '$lib/ui/TextArea.svelte'
|
||||
|
||||
let note: Note = {
|
||||
contents: '',
|
||||
meta: { type: 'text' },
|
||||
views: 1,
|
||||
expiration: 60,
|
||||
}
|
||||
let files: FileDTO[]
|
||||
let result: NoteResult | null = null
|
||||
let advanced = false
|
||||
let isFile = false
|
||||
let timeExpiration = false
|
||||
let description = ''
|
||||
let loading: string | null = null
|
||||
|
||||
$: if (!advanced) {
|
||||
note.views = 1
|
||||
timeExpiration = false
|
||||
}
|
||||
|
||||
$: {
|
||||
description = $t('home.explanation', {
|
||||
values: {
|
||||
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
||||
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
$: note.meta.type = isFile ? 'file' : 'text'
|
||||
|
||||
$: if (!isFile) {
|
||||
note.contents = ''
|
||||
}
|
||||
|
||||
class EmptyContentError extends Error {}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
loading = $t('common.encrypting')
|
||||
|
||||
const key = await AES.generateKey()
|
||||
const password = await Hex.encode(key)
|
||||
|
||||
const data: Note = {
|
||||
contents: '',
|
||||
meta: note.meta,
|
||||
}
|
||||
if (isFile) {
|
||||
if (files.length === 0) throw new EmptyContentError()
|
||||
data.contents = await Adapters.Files.encrypt(files, key)
|
||||
} else {
|
||||
if (note.contents === '') throw new EmptyContentError()
|
||||
data.contents = await Adapters.Text.encrypt(note.contents, key)
|
||||
}
|
||||
if (timeExpiration) data.expiration = parseInt(note.expiration as any)
|
||||
else data.views = parseInt(note.views as any)
|
||||
|
||||
loading = $t('common.uploading')
|
||||
const response = await create(data)
|
||||
result = {
|
||||
password: password,
|
||||
id: response.id,
|
||||
}
|
||||
notify.success($t('home.messages.note_created'))
|
||||
} catch (e) {
|
||||
if (e instanceof PayloadToLargeError) {
|
||||
notify.error($t('home.errors.note_to_big'))
|
||||
} else if (e instanceof EmptyContentError) {
|
||||
notify.error($t('home.errors.empty_content'))
|
||||
} else {
|
||||
console.error(e)
|
||||
notify.error($t('home.errors.note_error'))
|
||||
}
|
||||
} finally {
|
||||
loading = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if result}
|
||||
<Result {result} />
|
||||
{:else}
|
||||
<p>
|
||||
{@html $status?.theme_text || $t('home.intro')}
|
||||
</p>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<fieldset disabled={loading !== null}>
|
||||
{#if isFile}
|
||||
<FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
|
||||
{:else}
|
||||
<TextArea
|
||||
data-testid="text-field"
|
||||
label={$t('common.note')}
|
||||
bind:value={note.contents}
|
||||
placeholder="..."
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="bottom">
|
||||
<Switch
|
||||
data-testid="switch-file"
|
||||
class="file"
|
||||
label={$t('common.file')}
|
||||
bind:value={isFile}
|
||||
/>
|
||||
{#if $status?.allow_advanced}
|
||||
<Switch
|
||||
data-testid="switch-advanced"
|
||||
label={$t('common.advanced')}
|
||||
bind:value={advanced}
|
||||
/>
|
||||
{/if}
|
||||
<div class="grow" />
|
||||
<div class="tr">
|
||||
<small>{$t('common.max')}: <MaxSize /> </small>
|
||||
<br />
|
||||
<Button type="submit">{$t('common.create')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<br />
|
||||
{#if loading}
|
||||
{loading} <Loader />
|
||||
{:else}
|
||||
{description}
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
{#if advanced}
|
||||
<div transition:blur={{ duration: 250 }}>
|
||||
<br />
|
||||
<AdvancedParameters bind:note bind:timeExpiration />
|
||||
</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;
|
||||
}
|
||||
</style>
|
@@ -7,7 +7,9 @@
|
||||
<nav>
|
||||
<a href="/">/home</a>
|
||||
<a href="/about">/about</a>
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">/code</a>
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
||||
code
|
||||
</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
122
packages/frontend/src/lib/views/Header.svelte
Normal file
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { status } from '$lib/stores/status'
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<a href="/">
|
||||
{#if $status?.theme_image}
|
||||
<img alt="logo" src={$status.theme_image} />
|
||||
{:else}
|
||||
<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
|
||||
>
|
||||
{/if}
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
a {
|
||||
border: none;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 30rem) {
|
||||
header {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
header svg,
|
||||
header img {
|
||||
max-height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
header svg,
|
||||
header img {
|
||||
width: 100%;
|
||||
max-height: 8rem;
|
||||
transform: translateX(-1rem);
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
@@ -1,17 +1,13 @@
|
||||
<script lang="ts" context="module">
|
||||
import { init, waitLocale, getLocaleFromNavigator } from 'svelte-intl-precompile'
|
||||
// @ts-ignore
|
||||
import { registerAll } from '$locales'
|
||||
registerAll()
|
||||
init({ initialLocale: getLocaleFromNavigator() ?? undefined, fallbackLocale: 'en' })
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { init as initStores } from '$lib/stores/status'
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast'
|
||||
import { onMount } from 'svelte'
|
||||
import { waitLocale } from 'svelte-intl-precompile'
|
||||
|
||||
import '../app.css'
|
||||
|
||||
import { init as initStores, status } from '$lib/stores/status'
|
||||
import Footer from '$lib/views/Footer.svelte'
|
||||
import Header from '$lib/views/Header.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import '../app.css'
|
||||
|
||||
onMount(() => {
|
||||
initStores()
|
||||
@@ -19,7 +15,8 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>cryptgeon</title>
|
||||
<title>{$status?.theme_page_title || 'cryptgeon'}</title>
|
||||
<link rel="icon" href={$status?.theme_favicon || '/favicon.png'} />
|
||||
</svelte:head>
|
||||
|
||||
{#await waitLocale() then _}
|
||||
@@ -28,6 +25,8 @@
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<SvelteToast />
|
||||
|
||||
<Footer />
|
||||
{/await}
|
||||
|
5
packages/frontend/src/routes/+layout.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { getLocaleFromNavigator, init } from 'svelte-intl-precompile'
|
||||
// @ts-ignore
|
||||
import { registerAll } from '$locales'
|
||||
registerAll()
|
||||
init({ initialLocale: getLocaleFromNavigator() ?? undefined, fallbackLocale: 'en' })
|
@@ -1,10 +1,6 @@
|
||||
<script context="module">
|
||||
import { browser, dev } from '$app/env'
|
||||
<script lang="ts">
|
||||
import { status } from '$lib/stores/status'
|
||||
import AboutParagraph from '$lib/ui/AboutParagraph.svelte'
|
||||
|
||||
export const hydrate = dev
|
||||
export const router = browser
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -43,7 +39,7 @@
|
||||
the backend is written in rust and the frontend is svelte and typescript.
|
||||
<br />
|
||||
you are welcomed to check & audit the
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">
|
||||
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
|
||||
source code
|
||||
</a>.
|
||||
</span>
|
||||
@@ -51,9 +47,12 @@
|
||||
|
||||
<AboutParagraph title="translations">
|
||||
<span
|
||||
>translations are managed on <a href="https://lokalise.com/" target="_blank">Lokalise</a>,
|
||||
which granted an open source license to use the paid version. If you are interested in helping
|
||||
translating don't hesitate to contact me!
|
||||
>translations are managed on <a
|
||||
href="https://lokalise.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Lokalise</a
|
||||
>, which granted an open source license to use the paid version. If you are interested in
|
||||
helping translating don't hesitate to contact me!
|
||||
</span>
|
||||
</AboutParagraph>
|
||||
|
101
packages/frontend/src/routes/note/[id]/+page.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { Hex } from 'occulto'
|
||||
import { onMount } from 'svelte'
|
||||
import { t } from 'svelte-intl-precompile'
|
||||
|
||||
import { Adapters } from '$lib/adapters'
|
||||
import { get, info } from '$lib/api'
|
||||
import Button from '$lib/ui/Button.svelte'
|
||||
import Loader from '$lib/ui/Loader.svelte'
|
||||
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
export let data: PageData
|
||||
|
||||
let id = data.id
|
||||
let password: string
|
||||
let note: DecryptedNote | null = null
|
||||
let exists = false
|
||||
|
||||
let loading: string | null = null
|
||||
let error: string | null = null
|
||||
|
||||
onMount(async () => {
|
||||
// Check if note exists
|
||||
try {
|
||||
loading = $t('common.loading')
|
||||
password = window.location.hash.slice(1)
|
||||
await info(id)
|
||||
exists = true
|
||||
} catch {
|
||||
exists = false
|
||||
} finally {
|
||||
loading = null
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the actual contents of the note and decrypt it.
|
||||
*/
|
||||
async function show() {
|
||||
try {
|
||||
error = null
|
||||
loading = $t('common.downloading')
|
||||
const data = await get(id)
|
||||
loading = $t('common.decrypting')
|
||||
const key = await Hex.decode(password)
|
||||
switch (data.meta.type) {
|
||||
case 'text':
|
||||
note = {
|
||||
meta: { type: 'text' },
|
||||
contents: await Adapters.Text.decrypt(data.contents, key),
|
||||
}
|
||||
break
|
||||
case 'file':
|
||||
note = {
|
||||
meta: { type: 'file' },
|
||||
contents: await Adapters.Files.decrypt(data.contents, key),
|
||||
}
|
||||
break
|
||||
default:
|
||||
error = $t('show.errors.unsupported_type')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
error = $t('show.errors.decryption_failed')
|
||||
} finally {
|
||||
loading = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !loading}
|
||||
{#if !exists}
|
||||
<p class="error-text">{$t('show.errors.not_found')}</p>
|
||||
{:else if note && !error}
|
||||
<ShowNote {note} />
|
||||
{:else}
|
||||
<form on:submit|preventDefault={show}>
|
||||
<fieldset>
|
||||
<p>{$t('show.explanation')}</p>
|
||||
<Button data-testid="show-note-button" type="submit">{$t('show.show_note')}</Button>
|
||||
{#if error}
|
||||
<br />
|
||||
<p class="error-text">
|
||||
{error}
|
||||
<br />
|
||||
</p>
|
||||
{/if}
|
||||
</fieldset>
|
||||
</form>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if loading}
|
||||
<p class="loader">{loading} <Loader /></p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.loader {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|