mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2024-12-22 16:26:28 +00:00
Compare commits
7 Commits
d7e5a34b14
...
fdc2722fb9
Author | SHA1 | Date | |
---|---|---|---|
fdc2722fb9 | |||
6da28a701e | |||
e6d1e0f44a | |||
6000553b95 | |||
85204776d7 | |||
c2bfe9dd0d | |||
b05841a562 |
593
Cryptgeon.postman_collection.json
Normal file
593
Cryptgeon.postman_collection.json
Normal file
@ -0,0 +1,593 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "52d9e661-2d99-47f8-b09a-40b6a1c0b364",
|
||||||
|
"name": "Cryptgeon",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Notes",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Preview",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}",
|
||||||
|
"description": "Id of the Note"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "This endpoint is to query wether a note exists, without actually opening it. No view limits are used here, as contents of the note are not available, only the `meta` field is returned, which is public."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "200",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}",
|
||||||
|
"description": "Id of the Note"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:24:29 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "404",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}",
|
||||||
|
"description": "Id of the Note"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Not Found",
|
||||||
|
"code": 404,
|
||||||
|
"_postman_previewlanguage": "plain",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:25:26 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"const jsonData = pm.response.json();",
|
||||||
|
"pm.collectionVariables.set('NOTE_ID', jsonData.id)",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 1,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Simple",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 1,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "5 Minutes",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"expiration\": 5,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "3 Views",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 3,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "This endpoint gets the actual contents of a note. It's a `DELETE` endpoint, es it decreases the `view` counter, and deletes the note if `0` is reached."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "200",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:59:07 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\",\n \"contents\": \"Some encrypted content\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "404",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/notes/:id",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"notes",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{NOTE_ID}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Not Found",
|
||||||
|
"code": 404,
|
||||||
|
"_postman_previewlanguage": "plain",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:59:15 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Status",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/status/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"status",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "200",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{BASE}}/status/",
|
||||||
|
"host": [
|
||||||
|
"{{BASE}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"status",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "transfer-encoding",
|
||||||
|
"value": "chunked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "connection",
|
||||||
|
"value": "close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-encoding",
|
||||||
|
"value": "gzip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vary",
|
||||||
|
"value": "accept-encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "date",
|
||||||
|
"value": "Tue, 23 May 2023 05:56:45 GMT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"version\": \"2.3.0-beta.4\",\n \"max_size\": 10485760,\n \"max_views\": 100,\n \"max_expiration\": 360,\n \"allow_advanced\": true,\n \"theme_image\": \"\",\n \"theme_text\": \"\",\n \"theme_page_title\": \"\",\n \"theme_favicon\": \"\"\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "BASE",
|
||||||
|
"value": "http://localhost:1234/api",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "NOTE_ID",
|
||||||
|
"value": "",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -11,7 +11,9 @@ pub struct Note {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct NoteInfo {}
|
pub struct NoteInfo {
|
||||||
|
pub meta: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct NotePublic {
|
pub struct NotePublic {
|
||||||
|
@ -24,7 +24,7 @@ async fn one(path: web::Path<NotePath>) -> impl Responder {
|
|||||||
let note = store::get(&p.id);
|
let note = store::get(&p.id);
|
||||||
|
|
||||||
match note {
|
match note {
|
||||||
Ok(Some(_)) => HttpResponse::Ok().json(NoteInfo {}),
|
Ok(Some(n)) => HttpResponse::Ok().json(NoteInfo { meta: n.meta }),
|
||||||
Ok(None) => HttpResponse::NotFound().finish(),
|
Ok(None) => HttpResponse::NotFound().finish(),
|
||||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { Adapters, get, info, setBase } from '@cryptgeon/shared'
|
|||||||
import inquirer from 'inquirer'
|
import inquirer from 'inquirer'
|
||||||
import { access, constants, writeFile } from 'node:fs/promises'
|
import { access, constants, writeFile } from 'node:fs/promises'
|
||||||
import { basename, resolve } from 'node:path'
|
import { basename, resolve } from 'node:path'
|
||||||
import { Hex } from 'occulto'
|
import { AES, Hex } from 'occulto'
|
||||||
import pretty from 'pretty-bytes'
|
import pretty from 'pretty-bytes'
|
||||||
|
|
||||||
import { exit } from './utils'
|
import { exit } from './utils'
|
||||||
@ -10,11 +10,26 @@ import { exit } from './utils'
|
|||||||
export async function download(url: URL) {
|
export async function download(url: URL) {
|
||||||
setBase(url.origin)
|
setBase(url.origin)
|
||||||
const id = url.pathname.split('/')[2]
|
const id = url.pathname.split('/')[2]
|
||||||
await info(id).catch(() => exit('Note does not exist or is expired'))
|
const preview = await info(id).catch(() => exit('Note does not exist or is expired'))
|
||||||
const note = await get(id)
|
|
||||||
|
|
||||||
const password = url.hash.slice(1)
|
// Password
|
||||||
const key = Hex.decode(password)
|
let password: string
|
||||||
|
const derivation = preview?.meta.derivation
|
||||||
|
if (derivation) {
|
||||||
|
const response = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
message: 'Note password',
|
||||||
|
name: 'password',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
password = response.password
|
||||||
|
} else {
|
||||||
|
password = url.hash.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password)
|
||||||
|
const note = await get(id)
|
||||||
|
|
||||||
const couldNotDecrypt = () => exit('Could not decrypt note. Probably an invalid password')
|
const couldNotDecrypt = () => exit('Could not decrypt note. Probably an invalid password')
|
||||||
switch (note.meta.type) {
|
switch (note.meta.type) {
|
||||||
|
@ -6,7 +6,8 @@ import prettyBytes from 'pretty-bytes'
|
|||||||
|
|
||||||
import { download } from './download.js'
|
import { download } from './download.js'
|
||||||
import { parseFile, parseNumber } from './parsers.js'
|
import { parseFile, parseNumber } from './parsers.js'
|
||||||
import { uploadFiles, uploadText } from './upload.js'
|
import { getStdin } from './stdin.js'
|
||||||
|
import { upload } from './upload.js'
|
||||||
import { exit } from './utils.js'
|
import { exit } from './utils.js'
|
||||||
|
|
||||||
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
|
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
|
||||||
@ -61,10 +62,12 @@ send
|
|||||||
.addOption(server)
|
.addOption(server)
|
||||||
.addOption(views)
|
.addOption(views)
|
||||||
.addOption(minutes)
|
.addOption(minutes)
|
||||||
|
.addOption(password)
|
||||||
.action(async (files, options) => {
|
.action(async (files, options) => {
|
||||||
setBase(options.server!)
|
setBase(options.server!)
|
||||||
await checkConstrains(options)
|
await checkConstrains(options)
|
||||||
await uploadFiles(files, { views: options.views, expiration: options.minutes })
|
options.password ||= await getStdin()
|
||||||
|
await upload(files, { views: options.views, expiration: options.minutes, password: options.password })
|
||||||
})
|
})
|
||||||
send
|
send
|
||||||
.command('text')
|
.command('text')
|
||||||
@ -72,10 +75,12 @@ send
|
|||||||
.addOption(server)
|
.addOption(server)
|
||||||
.addOption(views)
|
.addOption(views)
|
||||||
.addOption(minutes)
|
.addOption(minutes)
|
||||||
|
.addOption(password)
|
||||||
.action(async (text, options) => {
|
.action(async (text, options) => {
|
||||||
setBase(options.server!)
|
setBase(options.server!)
|
||||||
await checkConstrains(options)
|
await checkConstrains(options)
|
||||||
await uploadText(text, { views: options.views, expiration: options.minutes })
|
options.password ||= await getStdin()
|
||||||
|
await upload(text, { views: options.views, expiration: options.minutes, password: options.password })
|
||||||
})
|
})
|
||||||
|
|
||||||
program
|
program
|
||||||
|
20
packages/cli/src/stdin.ts
Normal file
20
packages/cli/src/stdin.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export function getStdin(timeout: number = 10): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
// Store the data from stdin in a buffer
|
||||||
|
let buffer = ''
|
||||||
|
process.stdin.on('data', (d) => (buffer += d.toString()))
|
||||||
|
|
||||||
|
// Stop listening for data after the timeout, otherwise hangs indefinitely
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
process.stdin.destroy()
|
||||||
|
resolve('')
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
// Listen for end and error events
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
clearTimeout(t)
|
||||||
|
resolve(buffer.trim())
|
||||||
|
})
|
||||||
|
process.stdin.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
@ -1,48 +1,51 @@
|
|||||||
import { readFile, stat } from 'node:fs/promises'
|
import { readFile, stat } from 'node:fs/promises'
|
||||||
import { basename } from 'node:path'
|
import { basename } from 'node:path'
|
||||||
|
|
||||||
import { Adapters, BASE, create, FileDTO, Note } from '@cryptgeon/shared'
|
import { Adapters, BASE, create, FileDTO, Note, NoteMeta } from '@cryptgeon/shared'
|
||||||
import mime from 'mime'
|
import mime from 'mime'
|
||||||
import { AES, Hex, TypedArray } from 'occulto'
|
import { AES, Hex, TypedArray } from 'occulto'
|
||||||
|
|
||||||
import { exit } from './utils.js'
|
import { exit } from './utils.js'
|
||||||
|
|
||||||
type UploadOptions = Pick<Note, 'views' | 'expiration'>
|
type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
|
||||||
|
|
||||||
export async function upload(key: TypedArray, note: Note) {
|
export async function upload(input: string | string[], options: UploadOptions) {
|
||||||
try {
|
try {
|
||||||
|
const { password, ...noteOptions } = options
|
||||||
|
const derived = options.password ? await AES.derive(options.password) : undefined
|
||||||
|
const key = derived ? derived[0] : await AES.generateKey()
|
||||||
|
|
||||||
|
let contents: string
|
||||||
|
let type: NoteMeta['type']
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
contents = await Adapters.Text.encrypt(input, key)
|
||||||
|
type = 'text'
|
||||||
|
} else {
|
||||||
|
const files: FileDTO[] = await Promise.all(
|
||||||
|
input.map(async (path) => {
|
||||||
|
const data = new Uint8Array(await readFile(path))
|
||||||
|
const stats = await stat(path)
|
||||||
|
const extension = path.substring(path.indexOf('.') + 1)
|
||||||
|
const type = mime.getType(extension) ?? 'application/octet-stream'
|
||||||
|
return {
|
||||||
|
name: basename(path),
|
||||||
|
size: stats.size,
|
||||||
|
contents: data,
|
||||||
|
type,
|
||||||
|
} satisfies FileDTO
|
||||||
|
})
|
||||||
|
)
|
||||||
|
contents = await Adapters.Files.encrypt(files, key)
|
||||||
|
type = 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the actual note and upload it.
|
||||||
|
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
|
||||||
const result = await create(note)
|
const result = await create(note)
|
||||||
const password = Hex.encode(key)
|
let url = `${BASE}/note/${result.id}`
|
||||||
const url = `${BASE}/note/${result.id}#${password}`
|
if (!derived) url += `#${Hex.encode(key)}`
|
||||||
console.log(`Note created under:\n\n${url}`)
|
console.log(`Note created:\n\n${url}`)
|
||||||
} catch {
|
} catch {
|
||||||
exit('Could not create note')
|
exit('Could not create note')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadFiles(paths: string[], options: UploadOptions) {
|
|
||||||
const key = await AES.generateKey()
|
|
||||||
const files: FileDTO[] = await Promise.all(
|
|
||||||
paths.map(async (path) => {
|
|
||||||
const data = new Uint8Array(await readFile(path))
|
|
||||||
const stats = await stat(path)
|
|
||||||
const extension = path.substring(path.indexOf('.') + 1)
|
|
||||||
const type = mime.getType(extension) ?? 'application/octet-stream'
|
|
||||||
return {
|
|
||||||
name: basename(path),
|
|
||||||
size: stats.size,
|
|
||||||
contents: data,
|
|
||||||
type,
|
|
||||||
} satisfies FileDTO
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const contents = await Adapters.Files.encrypt(files, key)
|
|
||||||
await upload(key, { ...options, contents, meta: { type: 'file' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadText(text: string, options: UploadOptions) {
|
|
||||||
const key = await AES.generateKey()
|
|
||||||
const contents = await Adapters.Text.encrypt(text, key)
|
|
||||||
await upload(key, { ...options, contents, meta: { type: 'text' } })
|
|
||||||
}
|
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"decrypting": "entschlüsselt",
|
"decrypting": "entschlüsselt",
|
||||||
"uploading": "hochladen",
|
"uploading": "hochladen",
|
||||||
"downloading": "wird heruntergeladen",
|
"downloading": "wird heruntergeladen",
|
||||||
"qr_code": "qr-code"
|
"qr_code": "qr-code",
|
||||||
|
"password": "Passwort"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"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.",
|
||||||
@ -31,6 +32,10 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "notiz erstellt."
|
"note_created": "notiz erstellt."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Standardmäßig wird für jede Notiz ein sicher generiertes Passwort verwendet. Sie können jedoch auch ein eigenes Kennwort wählen, das nicht in dem Link enthalten ist.",
|
||||||
|
"custom_password": "benutzerdefiniertes Passwort"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"decrypting": "decrypting",
|
"decrypting": "decrypting",
|
||||||
"uploading": "uploading",
|
"uploading": "uploading",
|
||||||
"downloading": "downloading",
|
"downloading": "downloading",
|
||||||
"qr_code": "qr code"
|
"qr_code": "qr code",
|
||||||
|
"password": "password"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
||||||
@ -31,6 +32,10 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "note created."
|
"note_created": "note created."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "By default, a securely generated password is used for each note. You can however also choose your own password, which is not included in the link.",
|
||||||
|
"custom_password": "custom password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"decrypting": "descifrando",
|
"decrypting": "descifrando",
|
||||||
"uploading": "cargando",
|
"uploading": "cargando",
|
||||||
"downloading": "descargando",
|
"downloading": "descargando",
|
||||||
"qr_code": "código qr"
|
"qr_code": "código qr",
|
||||||
|
"password": "contraseña"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"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.",
|
||||||
@ -31,6 +32,10 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "nota creada."
|
"note_created": "nota creada."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, que no se incluye en el enlace.",
|
||||||
|
"custom_password": "contraseña personalizada"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"decrypting": "déchiffrer",
|
"decrypting": "déchiffrer",
|
||||||
"uploading": "téléchargement",
|
"uploading": "téléchargement",
|
||||||
"downloading": "téléchargement",
|
"downloading": "téléchargement",
|
||||||
"qr_code": "code qr"
|
"qr_code": "code qr",
|
||||||
|
"password": "mot de passe"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"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.",
|
||||||
@ -31,6 +32,10 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "note créée."
|
"note_created": "note créée."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Par défaut, un mot de passe généré de manière sécurisée est utilisé pour chaque note. Vous pouvez toutefois choisir votre propre mot de passe, qui n'est pas inclus dans le lien.",
|
||||||
|
"custom_password": "mot de passe personnalisé"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"decrypting": "decifrando",
|
"decrypting": "decifrando",
|
||||||
"uploading": "caricamento",
|
"uploading": "caricamento",
|
||||||
"downloading": "scaricando",
|
"downloading": "scaricando",
|
||||||
"qr_code": "codice qr"
|
"qr_code": "codice qr",
|
||||||
|
"password": "password"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.",
|
||||||
@ -31,6 +32,10 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "nota creata."
|
"note_created": "nota creata."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "Per impostazione predefinita, per ogni nota viene utilizzata una password generata in modo sicuro. È tuttavia possibile scegliere la propria password, che non è inclusa nel link.",
|
||||||
|
"custom_password": "password personalizzata"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"decrypting": "復号化",
|
"decrypting": "復号化",
|
||||||
"uploading": "アップロード中",
|
"uploading": "アップロード中",
|
||||||
"downloading": "ダウンロード中",
|
"downloading": "ダウンロード中",
|
||||||
"qr_code": "QRコード"
|
"qr_code": "QRコード",
|
||||||
|
"password": "暗号"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "<i>完全に暗号化された</i> 、安全なメモやファイルをワンクリックで簡単に送信できます。メモを作成してリンクを共有するだけです。",
|
"intro": "<i>完全に暗号化された</i> 、安全なメモやファイルをワンクリックで簡単に送信できます。メモを作成してリンクを共有するだけです。",
|
||||||
@ -31,6 +32,10 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "メモが作成されました。"
|
"note_created": "メモが作成されました。"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "デフォルトでは、安全に生成されたパスワードが各ノートに使用されます。しかし、リンクに含まれない独自のパスワードを選択することもできます。",
|
||||||
|
"custom_password": "カスタムパスワード"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"decrypting": "расшифровка",
|
"decrypting": "расшифровка",
|
||||||
"uploading": "загрузка",
|
"uploading": "загрузка",
|
||||||
"downloading": "скачивание",
|
"downloading": "скачивание",
|
||||||
"qr_code": "qr код"
|
"qr_code": "qr код",
|
||||||
|
"password": "пароль"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "Легко отправляйте <i>полностью зашифрованные</i> защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.",
|
"intro": "Легко отправляйте <i>полностью зашифрованные</i> защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.",
|
||||||
@ -31,6 +32,10 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "заметка создана."
|
"note_created": "заметка создана."
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.",
|
||||||
|
"custom_password": "пользовательский пароль"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"decrypting": "解密",
|
"decrypting": "解密",
|
||||||
"uploading": "上传",
|
"uploading": "上传",
|
||||||
"downloading": "下载",
|
"downloading": "下载",
|
||||||
"qr_code": "二维码"
|
"qr_code": "二维码",
|
||||||
|
"password": "密码"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "飞鸽传书,一键传输完全加密的密信或文件,阅后即焚。",
|
"intro": "飞鸽传书,一键传输完全加密的密信或文件,阅后即焚。",
|
||||||
@ -31,6 +32,10 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"note_created": "密信创建成功。"
|
"note_created": "密信创建成功。"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"explanation": "默认情况下,每个笔记都使用安全生成的密码。但是,您也可以选择您自己的密码,该密码未包含在链接中。",
|
||||||
|
"custom_password": "自定义密码"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
|
@ -92,7 +92,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
*:disabled,
|
*:disabled,
|
||||||
*[disabled='true'] {
|
.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,3 +126,13 @@ fieldset {
|
|||||||
.tr {
|
.tr {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid var(--ui-bg-1);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
@ -8,48 +8,69 @@
|
|||||||
|
|
||||||
export let note: Note
|
export let note: Note
|
||||||
export let timeExpiration = false
|
export let timeExpiration = false
|
||||||
|
|
||||||
|
let customPassword = false
|
||||||
|
|
||||||
|
$: if (!customPassword) note.password = undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="flex col">
|
||||||
<TextInput
|
<div class="flex">
|
||||||
data-testid="field-views"
|
<TextInput
|
||||||
type="number"
|
data-testid="field-views"
|
||||||
label={$t('common.views', { values: { n: 0 } })}
|
type="number"
|
||||||
bind:value={note.views}
|
label={$t('common.views', { values: { n: 0 } })}
|
||||||
disabled={timeExpiration}
|
bind:value={note.views}
|
||||||
max={$status?.max_views}
|
disabled={timeExpiration}
|
||||||
min={1}
|
max={$status?.max_views}
|
||||||
validate={(v) =>
|
min={1}
|
||||||
($status && v <= $status?.max_views && v > 0) ||
|
validate={(v) =>
|
||||||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
($status && v <= $status?.max_views && v > 0) ||
|
||||||
/>
|
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
|
||||||
<div class="middle-switch">
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
data-testid="switch-advanced-toggle"
|
data-testid="switch-advanced-toggle"
|
||||||
label={$t('common.mode')}
|
label={$t('common.mode')}
|
||||||
bind:value={timeExpiration}
|
bind:value={timeExpiration}
|
||||||
color={false}
|
color={false}
|
||||||
/>
|
/>
|
||||||
|
<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>
|
||||||
|
<div class="flex">
|
||||||
|
<Switch bind:value={customPassword} label={$t('home.advanced.custom_password')} />
|
||||||
|
<TextInput
|
||||||
|
type="password"
|
||||||
|
bind:value={note.password}
|
||||||
|
label={$t('common.password')}
|
||||||
|
disabled={!customPassword}
|
||||||
|
random
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{$t('home.advanced.explanation')}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.middle-switch {
|
.flex {
|
||||||
margin: 0 1rem;
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.col {
|
||||||
display: flex;
|
gap: 1.5rem;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
export type NoteResult = {
|
export type NoteResult = {
|
||||||
password: string
|
|
||||||
id: string
|
id: string
|
||||||
|
password?: string
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -14,7 +14,8 @@
|
|||||||
|
|
||||||
export let result: NoteResult
|
export let result: NoteResult
|
||||||
|
|
||||||
$: url = `${window.location.origin}/note/${result.id}#${result.password}`
|
let url = `${window.location.origin}/note/${result.id}`
|
||||||
|
if (result.password) url += `#${result.password}`
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
|
@ -4,43 +4,35 @@
|
|||||||
export let color = true
|
export let color = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {...$$restProps}>
|
<label {...$$restProps}>
|
||||||
<label class="switch">
|
<small>{label}</small>
|
||||||
<small>{label}</small>
|
<input type="checkbox" bind:checked={value} />
|
||||||
<input type="checkbox" bind:checked={value} />
|
<span class:color class="slider" />
|
||||||
<span class:color class="slider" />
|
</label>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
label {
|
||||||
height: 3.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 4rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch input {
|
label input {
|
||||||
opacity: 0;
|
display: none;
|
||||||
width: 0;
|
}
|
||||||
height: 0;
|
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
position: absolute;
|
display: block;
|
||||||
|
width: 4rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border: 2px solid var(--ui-bg-1);
|
border: 2px solid var(--ui-bg-1);
|
||||||
background-color: var(--ui-bg-0);
|
background-color: var(--ui-bg-0);
|
||||||
transition: var(--ui-anim);
|
|
||||||
transform: translateY(1.2rem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider:before {
|
.slider:before {
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<small disabled={$$restProps.disabled}>
|
<small class:disabled={$$restProps.disabled}>
|
||||||
{label}
|
{label}
|
||||||
{#if valid !== true}
|
{#if valid !== true}
|
||||||
<span class="error-text">{valid}</span>
|
<span class="error-text">{valid}</span>
|
||||||
@ -54,6 +54,7 @@
|
|||||||
label {
|
label {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
label > small {
|
label > small {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AES, Hex } from 'occulto'
|
import { AES, Hex, Bytes } from 'occulto'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import { blur } from 'svelte/transition'
|
import { blur } from 'svelte/transition'
|
||||||
|
|
||||||
@ -57,13 +57,14 @@
|
|||||||
try {
|
try {
|
||||||
loading = $t('common.encrypting')
|
loading = $t('common.encrypting')
|
||||||
|
|
||||||
const key = await AES.generateKey()
|
const derived = note.password && (await AES.derive(note.password))
|
||||||
const password = Hex.encode(key)
|
const key = derived ? derived[0] : await AES.generateKey()
|
||||||
|
|
||||||
const data: Note = {
|
const data: Note = {
|
||||||
contents: '',
|
contents: '',
|
||||||
meta: note.meta,
|
meta: note.meta,
|
||||||
}
|
}
|
||||||
|
if (derived) data.meta.derivation = derived[1]
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
if (files.length === 0) throw new EmptyContentError()
|
if (files.length === 0) throw new EmptyContentError()
|
||||||
data.contents = await Adapters.Files.encrypt(files, key)
|
data.contents = await Adapters.Files.encrypt(files, key)
|
||||||
@ -77,8 +78,8 @@
|
|||||||
loading = $t('common.uploading')
|
loading = $t('common.uploading')
|
||||||
const response = await create(data)
|
const response = await create(data)
|
||||||
result = {
|
result = {
|
||||||
password: password,
|
|
||||||
id: response.id,
|
id: response.id,
|
||||||
|
password: note.password ? undefined : Hex.encode(key),
|
||||||
}
|
}
|
||||||
notify.success($t('home.messages.note_created'))
|
notify.success($t('home.messages.note_created'))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -148,7 +149,7 @@
|
|||||||
|
|
||||||
{#if advanced}
|
{#if advanced}
|
||||||
<div transition:blur={{ duration: 250 }}>
|
<div transition:blur={{ duration: 250 }}>
|
||||||
<br />
|
<hr />
|
||||||
<AdvancedParameters bind:note bind:timeExpiration />
|
<AdvancedParameters bind:note bind:timeExpiration />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--ui-bg-0-85);
|
background-color: var(--ui-bg-0-85);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -1,30 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Hex } from 'occulto'
|
import { AES, Hex } from 'occulto'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
import Button from '$lib/ui/Button.svelte'
|
import Button from '$lib/ui/Button.svelte'
|
||||||
import Loader from '$lib/ui/Loader.svelte'
|
import Loader from '$lib/ui/Loader.svelte'
|
||||||
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
||||||
import { Adapters, get, info } from '@cryptgeon/shared'
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
|
import { Adapters, get, info, type NoteMeta } from '@cryptgeon/shared'
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
|
|
||||||
export let data: PageData
|
export let data: PageData
|
||||||
|
|
||||||
let id = data.id
|
let id = data.id
|
||||||
let password: string
|
let password: string | null = null
|
||||||
let note: DecryptedNote | null = null
|
let note: DecryptedNote | null = null
|
||||||
let exists = false
|
let exists = false
|
||||||
|
let meta: NoteMeta | null = null
|
||||||
|
|
||||||
let loading: string | null = null
|
let loading: string | null = null
|
||||||
let error: string | null = null
|
let error: string | null = null
|
||||||
|
|
||||||
|
$: valid = !!password?.length
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Check if note exists
|
// Check if note exists
|
||||||
try {
|
try {
|
||||||
loading = $t('common.loading')
|
loading = $t('common.loading')
|
||||||
password = window.location.hash.slice(1)
|
password = window.location.hash.slice(1)
|
||||||
await info(id)
|
const note = await info(id)
|
||||||
|
meta = note.meta
|
||||||
exists = true
|
exists = true
|
||||||
} catch {
|
} catch {
|
||||||
exists = false
|
exists = false
|
||||||
@ -38,11 +43,18 @@
|
|||||||
*/
|
*/
|
||||||
async function show() {
|
async function show() {
|
||||||
try {
|
try {
|
||||||
|
if (!valid) {
|
||||||
|
error = $t('show.errors.no_password')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load note
|
||||||
error = null
|
error = null
|
||||||
loading = $t('common.downloading')
|
loading = $t('common.downloading')
|
||||||
const data = await get(id)
|
const data = await get(id)
|
||||||
loading = $t('common.decrypting')
|
loading = $t('common.decrypting')
|
||||||
const key = Hex.decode(password)
|
const derived = meta?.derivation && (await AES.derive(password!, meta.derivation))
|
||||||
|
const key = derived ? derived[0] : Hex.decode(password!)
|
||||||
switch (data.meta.type) {
|
switch (data.meta.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
note = {
|
note = {
|
||||||
@ -77,9 +89,18 @@
|
|||||||
<form on:submit|preventDefault={show}>
|
<form on:submit|preventDefault={show}>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<p>{$t('show.explanation')}</p>
|
<p>{$t('show.explanation')}</p>
|
||||||
<Button data-testid="show-note-button" type="submit">{$t('show.show_note')}</Button>
|
{#if meta?.derivation}
|
||||||
|
<TextInput
|
||||||
|
data-testid="show-note-password"
|
||||||
|
type="password"
|
||||||
|
bind:value={password}
|
||||||
|
label={$t('common.password')}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Button disabled={!valid} data-testid="show-note-button" type="submit"
|
||||||
|
>{$t('show.show_note')}</Button
|
||||||
|
>
|
||||||
{#if error}
|
{#if error}
|
||||||
<br />
|
|
||||||
<p class="error-text">
|
<p class="error-text">
|
||||||
{error}
|
{error}
|
||||||
<br />
|
<br />
|
||||||
@ -97,4 +118,10 @@
|
|||||||
.loader {
|
.loader {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -10,7 +10,7 @@ proxy.on('error', function (err, req, res) {
|
|||||||
|
|
||||||
const server = http.createServer(function (req, res) {
|
const server = http.createServer(function (req, res) {
|
||||||
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
|
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
|
||||||
proxy.web(req, res, { target })
|
proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 })
|
||||||
})
|
})
|
||||||
server.listen(1234)
|
server.listen(1234)
|
||||||
console.log('Proxy on http://localhost:1234')
|
console.log('Proxy on http://localhost:1234')
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import type { TypedArray } from 'occulto'
|
import type { KeyData, TypedArray } from 'occulto'
|
||||||
|
|
||||||
export type NoteMeta = { type: 'text' | 'file' }
|
export type NoteMeta = {
|
||||||
|
type: 'text' | 'file'
|
||||||
|
derivation?: KeyData
|
||||||
|
}
|
||||||
|
|
||||||
export type Note = {
|
export type Note = {
|
||||||
contents: string
|
contents: string
|
||||||
@ -8,7 +11,7 @@ export type Note = {
|
|||||||
views?: number
|
views?: number
|
||||||
expiration?: number
|
expiration?: number
|
||||||
}
|
}
|
||||||
export type NoteInfo = {}
|
export type NoteInfo = Pick<Note, 'meta'>
|
||||||
export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
||||||
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
||||||
|
|
||||||
@ -71,10 +74,12 @@ export async function get(id: string): Promise<NotePublic> {
|
|||||||
method: 'delete',
|
method: 'delete',
|
||||||
})
|
})
|
||||||
const { contents, meta } = data
|
const { contents, meta } = data
|
||||||
return {
|
const note = {
|
||||||
contents,
|
contents,
|
||||||
meta: JSON.parse(meta) as NoteMeta,
|
meta: JSON.parse(meta),
|
||||||
}
|
} satisfies NotePublic
|
||||||
|
if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt))
|
||||||
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function info(id: string): Promise<NoteInfo> {
|
export async function info(id: string): Promise<NoteInfo> {
|
||||||
@ -82,7 +87,12 @@ export async function info(id: string): Promise<NoteInfo> {
|
|||||||
url: `notes/${id}`,
|
url: `notes/${id}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
})
|
})
|
||||||
return data
|
const { meta } = data
|
||||||
|
const note = {
|
||||||
|
meta: JSON.parse(meta),
|
||||||
|
} satisfies NoteInfo
|
||||||
|
if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt))
|
||||||
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Status = {
|
export type Status = {
|
||||||
|
Loading…
Reference in New Issue
Block a user