1
0
mirror of https://github.com/lus/pasty.git synced 2023-08-10 21:13:09 +03:00

implement frontend routing

This commit is contained in:
Lukas Schulte Pelkum
2023-06-08 19:17:34 +02:00
parent 941da057ae
commit a53bd39dbd
19 changed files with 82 additions and 1 deletions

View File

@@ -0,0 +1,388 @@
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap");
html, body {
margin: 0;
padding: 0;
background-color: #000000;
color: #ffffff;
font-family: 'Source Code Pro', monospace;
}
html.embedded .navigation, body.embedded .navigation {
display: none;
}
html.embedded .container, body.embedded .container {
margin: 0;
}
html.embedded #content, html.embedded #linenos, body.embedded #content, body.embedded #linenos {
padding-top: 10px;
min-height: calc(100vh - 50px);
}
html.embedded #footer, body.embedded #footer {
font-size: 0.8em;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #000000;
}
::-webkit-scrollbar-thumb {
background: #444444;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #333333;
}
::-webkit-scrollbar-thumb:active {
background: #222222;
}
.hidden {
display: none;
}
@-webkit-keyframes spinner {
0% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg);
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg);
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
@keyframes spinner {
0% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg);
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg);
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
#spinner-container {
position: fixed;
top: 130px;
right: 20px;
height: 50px;
width: 50px;
}
#spinner-container .spinner {
-webkit-animation: .75s linear infinite spinner;
animation: .75s linear infinite spinner;
-webkit-animation-play-state: inherit;
animation-play-state: inherit;
border: solid 5px #ffffff;
border-bottom-color: transparent;
border-radius: 50%;
height: 100%;
width: 100%;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
will-change: transform;
}
#btn_report {
position: fixed;
bottom: 60px;
right: 30px;
}
#btn_report svg {
-webkit-transition: all 250ms;
transition: all 250ms;
}
#btn_report:hover {
cursor: pointer;
}
#btn_report:hover svg {
stroke: #2daa57;
}
.navigation {
position: fixed;
top: 0;
width: calc(100vw - 80px);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
padding: 0 40px;
background-color: #222222;
}
.navigation .button {
padding: 10px 20px;
background-color: transparent;
border: none;
outline: none;
}
.navigation .button svg {
-webkit-transition: all 250ms;
transition: all 250ms;
}
.navigation .button.active svg {
stroke: #2daa57;
}
.navigation .button:hover {
cursor: pointer;
}
.navigation .button:hover svg {
stroke: #2daa57;
}
.navigation .button:disabled svg {
stroke: #5a5a5a;
}
.navigation .button:disabled:hover {
cursor: initial;
color: initial;
}
.container {
margin-top: 60px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
}
.container #linenos {
padding: 20px 0;
width: 50px;
min-height: calc(100vh - 100px);
background-color: #111111;
color: #bebebe;
}
.container #linenos span {
display: block;
width: 100%;
height: 20px;
text-align: center;
}
.container #linenos span:last-child {
margin-bottom: 25px;
}
.container #content {
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 20px;
width: calc(100vw - 50px);
}
.container #content #code {
white-space: pre;
line-height: 20px;
overflow-x: auto;
}
.container #content #input {
height: 100%;
width: 100%;
padding: 0;
background-color: transparent;
border: none;
outline: none;
color: inherit;
resize: none;
font: inherit;
line-height: 20px;
}
.container #notifications {
position: fixed;
bottom: 30px;
right: 0;
padding: 20px;
z-index: 1;
}
.container #notifications div {
border-radius: 10px;
width: 500px;
margin-top: 20px;
padding: 20px 30px;
}
.container #notifications div.error {
background-color: #ff4d4d;
}
.container #notifications div.success {
background-color: #389b38;
}
.container #notifications div:first-child {
margin-top: 0;
}
.container #lifetime_container {
position: fixed;
right: 30px;
top: 90px;
padding: 10px 15px;
background-color: #222222;
border-radius: 10px;
}
.container #lifetime_container #lifetime {
background-color: #111111;
margin-left: 10px;
padding: 5px 10px;
border-radius: 10px;
}
.container #content_length_container {
position: fixed;
right: 30px;
bottom: 60px;
padding: 10px 15px;
background-color: #222222;
border-radius: 10px;
}
.container #content_length_container span {
background-color: #111111;
padding: 5px 10px;
border-radius: 10px;
}
#footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #222222;
}
#footer #flex {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
margin: 0 80px 0 60px;
}
#footer div {
display: inline-block;
}
#footer a {
display: inline-block;
text-decoration: none;
color: #ffffff;
padding: 5px 20px;
height: 100%;
-webkit-transition: all 200ms;
transition: all 200ms;
}
#footer a:hover {
background-color: #333333;
color: #2daa57;
}
#footer #version {
display: inline-block;
margin-left: 10px;
padding: 5px 30px;
background-color: #111111;
}
@media only screen and (max-width: 650px) {
.navigation {
padding: 0 20px;
width: calc(100vw - 40px);
}
.navigation .button {
padding: 15px 10px;
}
.navigation .button svg {
width: 30px;
height: 30px;
}
.navigation .meta #version {
display: none;
}
.container #notifications {
padding: 0;
}
.container #notifications div {
margin: 0;
border-radius: 0;
width: 100vw;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
#footer #flex {
margin: 0 0 0 25px;
}
#footer .version-container span {
display: none;
}
#footer a {
padding: 5px 15px;
}
}
@media only screen and (max-width: 400px) {
#btn_copy, #lifetime_container, #content_length_container {
display: none;
}
}
@media only screen and (max-width: 500px) {
#footer #flex {
margin: 0;
-ms-flex-pack: distribute;
justify-content: space-around;
}
#footer .version-container {
display: none;
}
}
/*# sourceMappingURL=style.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,333 @@
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap');
html, body {
margin: 0;
padding: 0;
background-color: #000000;
color: #ffffff;
font-family: 'Source Code Pro', monospace;
&.embedded {
.navigation {
display: none;
}
.container {
margin: 0;
}
#content, #linenos {
padding-top: 10px;
min-height: calc(100vh - 50px);
}
#footer {
font-size: 0.8em;
}
}
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #000000;
}
::-webkit-scrollbar-thumb {
background: #444444;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #333333;
}
::-webkit-scrollbar-thumb:active {
background: #222222;
}
.hidden {
display: none;
}
@-webkit-keyframes spinner {
0% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg);
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg);
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
@keyframes spinner {
0% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg);
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg);
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
#spinner-container {
position: fixed;
top: 130px;
right: 20px;
height: 50px;
width: 50px;
& .spinner {
-webkit-animation: .75s linear infinite spinner;
animation: .75s linear infinite spinner;
-webkit-animation-play-state: inherit;
animation-play-state: inherit;
border: solid 5px #ffffff;
border-bottom-color: transparent;
border-radius: 50%;
height: 100%;
width: 100%;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
will-change: transform;
}
}
#btn_report {
position: fixed;
bottom: 60px;
right: 30px;
& svg {
transition: all 250ms;
}
&:hover {
cursor: pointer;
& svg {
stroke: #2daa57;
}
}
}
.navigation {
position: fixed;
top: 0;
width: calc(100vw - 80px);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 40px;
background-color: #222222;
& .button {
padding: 10px 20px;
background-color: transparent;
border: none;
outline: none;
& svg {
transition: all 250ms;
}
&.active svg {
stroke: #2daa57;
}
&:hover {
cursor: pointer;
& svg {
stroke: #2daa57;
}
}
&:disabled {
& svg {
stroke: #5a5a5a;
}
&:hover {
cursor: initial;
color: initial;
}
}
}
}
.container {
margin-top: 60px;
display: flex;
flex-direction: row;
& #linenos {
padding: 20px 0;
width: 50px;
min-height: calc(100vh - 100px);
background-color: #111111;
color: #bebebe;
& span {
display: block;
width: 100%;
height: 20px;
text-align: center;
&:last-child {
margin-bottom: 25px;
}
}
}
& #content {
box-sizing: border-box;
padding: 20px;
width: calc(100vw - 50px);
& #code {
white-space: pre;
line-height: 20px;
overflow-x: auto;
}
& #input {
height: 100%;
width: 100%;
padding: 0;
background-color: transparent;
border: none;
outline: none;
color: inherit;
resize: none;
font: inherit;
line-height: 20px;
}
}
& #notifications {
position: fixed;
bottom: 30px;
right: 0;
padding: 20px;
z-index: 1;
& div {
border-radius: 10px;
width: 500px;
margin-top: 20px;
padding: 20px 30px;
&.error {
background-color: #ff4d4d;
}
&.success {
background-color: #389b38;
}
&:first-child {
margin-top: 0;
}
}
}
& #lifetime_container {
position: fixed;
right: 30px;
top: 90px;
padding: 10px 15px;
background-color: #222222;
border-radius: 10px;
& #lifetime {
background-color: #111111;
margin-left: 10px;
padding: 5px 10px;
border-radius: 10px;
}
}
& #content_length_container {
position: fixed;
right: 30px;
bottom: 60px;
padding: 10px 15px;
background-color: #222222;
border-radius: 10px;
& span {
background-color: #111111;
padding: 5px 10px;
border-radius: 10px;
}
}
}
#footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #222222;
& #flex {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0 80px 0 60px;
}
& div {
display: inline-block;
}
& a {
display: inline-block;
text-decoration: none;
color: #ffffff;
padding: 5px 20px;
height: 100%;
transition: all 200ms;
&:hover {
background-color: #333333;
color: #2daa57;
}
}
& #version {
display: inline-block;
margin-left: 10px;
padding: 5px 30px;
background-color: #111111;
}
}
@media only screen and (max-width: 650px) {
.navigation {
padding: 0 20px;
width: calc(100vw - 40px);
& .button {
padding: 15px 10px;
& svg {
width: 30px;
height: 30px;
}
}
& .meta #version {
display: none;
}
}
.container #notifications {
padding: 0;
& div {
margin: 0;
border-radius: 0;
width: 100vw;
box-sizing: border-box;
}
}
#footer {
& #flex {
margin: 0 0 0 25px;
}
& .version-container span {
display: none;
}
& a {
padding: 5px 15px;
}
}
}
@media only screen and (max-width: 400px) {
#btn_copy, #lifetime_container, #content_length_container {
display: none;
}
}
@media only screen and (max-width: 500px) {
#footer {
& #flex {
margin: 0;
justify-content: space-around;
}
& .version-container {
display: none;
}
}
}

View File

@@ -0,0 +1,5 @@
import * as Spinner from "./modules/spinner.js";
import * as State from "./modules/state.js";
// Initialize the application state
Spinner.surround(State.initialize);

View File

@@ -0,0 +1,12 @@
// Properly animates an element
export function animate(element, animation, duration, after) {
element.style.setProperty("--animate-duration", duration);
element.classList.add("animate__animated", animation);
element.addEventListener("animationend", () => {
element.style.removeProperty("--animate-duration");
element.classList.remove("animate__animated", animation);
if (after) {
after();
}
}, {once: true});
}

View File

@@ -0,0 +1,57 @@
const API_BASE_URL = location.protocol + "//" + location.host + "/web/v2";
export async function getAPIInformation() {
return fetch(API_BASE_URL + "/info");
}
export async function getPaste(pasteID) {
return fetch(API_BASE_URL + "/pastes/" + pasteID);
}
export async function createPaste(content, metadata) {
return fetch(API_BASE_URL + "/pastes", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
content,
metadata
})
});
}
export async function editPaste(pasteID, modificationToken, content, metadata) {
return fetch(API_BASE_URL + "/pastes/" + pasteID, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + modificationToken,
},
body: JSON.stringify({
content,
metadata
})
});
}
export async function deletePaste(pasteID, modificationToken) {
return fetch(API_BASE_URL + "/pastes/" + pasteID, {
method: "DELETE",
headers: {
"Authorization": "Bearer " + modificationToken,
}
});
}
export async function reportPaste(pasteID, reason) {
return fetch(API_BASE_URL + "/pastes/" + pasteID + "/report", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
reason
})
});
}

View File

@@ -0,0 +1,32 @@
export function format(milliseconds) {
if (milliseconds < 0) {
return "forever";
}
let parts = new Array();
let days = Math.floor(milliseconds / 86400000);
if (days > 0) {
parts.push(`${days} ${days > 1 ? "days" : "day"}`);
milliseconds -= days * 86400000;
}
let hours = Math.floor(milliseconds / 3600000);
if (hours > 0) {
parts.push(`${hours} ${hours > 1 ? "hours" : "hour"}`);
milliseconds -= hours * 3600000;
}
let minutes = Math.floor(milliseconds / 60000);
if (minutes > 0) {
parts.push(`${minutes} ${minutes > 1 ? "minutes" : "minute"}`);
milliseconds -= minutes * 60000;
}
let seconds = Math.ceil(milliseconds / 1000);
if (seconds > 0) {
parts.push(`${seconds} ${seconds > 1 ? "seconds" : "second"}`);
}
return parts.join(", ");
}

View File

@@ -0,0 +1,60 @@
// Encrypts a piece of text using AES-CBC and returns the HEX-encoded key, initialization vector and encrypted text
export async function encrypt(encryptionData, text) {
const key = encryptionData.key;
const iv = encryptionData.iv;
const textBytes = aesjs.padding.pkcs7.pad(aesjs.utils.utf8.toBytes(text));
const aes = new aesjs.ModeOfOperation.cbc(key, iv);
const encrypted = aes.encrypt(textBytes);
return {
key: aesjs.utils.hex.fromBytes(key),
iv: aesjs.utils.hex.fromBytes(iv),
result: aesjs.utils.hex.fromBytes(encrypted)
};
}
// Decrypts an encrypted piece of AES-CBC encrypted text
export async function decrypt(keyHex, ivHex, inputHex) {
const key = aesjs.utils.hex.toBytes(keyHex);
const iv = aesjs.utils.hex.toBytes(ivHex);
const input = aesjs.utils.hex.toBytes(inputHex);
const aes = new aesjs.ModeOfOperation.cbc(key, iv);
const decrypted = aesjs.padding.pkcs7.strip(aes.decrypt(input));
return aesjs.utils.utf8.fromBytes(decrypted);
}
// Creates encryption data from hex key and IV
export async function encryptionDataFromHex(keyHex, ivHex) {
return {
key: aesjs.utils.hex.toBytes(keyHex),
iv: aesjs.utils.hex.toBytes(ivHex)
};
}
// Generates encryption data to pass into the encrypt function
export async function generateEncryptionData() {
return {
key: await generateKey(),
iv: generateIV()
};
}
// Generates a new 256-bit AES-CBC key
async function generateKey() {
const key = await crypto.subtle.generateKey({
name: "AES-CBC",
length: 256
}, true, ["encrypt", "decrypt"]);
const extracted = await crypto.subtle.exportKey("raw", key);
return new Uint8Array(extracted);
}
// Generates a new cryptographically secure 16-byte array which is used as the initialization vector (IV) for AES-CBC
function generateIV() {
return crypto.getRandomValues(new Uint8Array(16));
}

View File

@@ -0,0 +1,25 @@
import * as Animation from "./animation.js";
const ELEMENT = document.getElementById("notifications");
// Shows a success notification
export function success(message) {
create("success", message, 3000);
}
// Shows an error notification
export function error(message) {
create("error", message, 3000);
}
// Creates a new custom notification
function create(type, message, duration) {
const node = document.createElement("div");
node.classList.add(type);
Animation.animate(node, "animate__fadeInUp", "0.2s");
node.innerHTML = message;
ELEMENT.childNodes.forEach(child => Animation.animate(child, "animate__slideInUp", "0.2s"));
ELEMENT.appendChild(node);
setTimeout(() => Animation.animate(node, "animate__fadeOutUp", "0.2s", () => ELEMENT.removeChild(node)), duration);
}

View File

@@ -0,0 +1,21 @@
import * as Animation from "./animation.js";
const ELEMENT = document.getElementById("spinner-container");
// SHows the spinner
export function show() {
ELEMENT.classList.remove("hidden");
Animation.animate(ELEMENT, "animate__zoomIn", "0.2s");
}
// Hides the spinner
export function hide() {
Animation.animate(ELEMENT, "animate__zoomOut", "0.2s", () => ELEMENT.classList.add("hidden"));
}
// Surrounds an async action with a spinner
export async function surround(innerFunction) {
show();
await innerFunction();
hide();
}

View File

@@ -0,0 +1,438 @@
import * as API from "./api.js";
import * as Notifications from "./notifications.js";
import * as Spinner from "./spinner.js";
import * as Animation from "./animation.js";
import * as Encryption from "./encryption.js";
import * as Duration from "./duration.js";
const CODE_ELEMENT = document.getElementById("code");
const LINE_NUMBERS_ELEMENT = document.getElementById("linenos");
const INPUT_ELEMENT = document.getElementById("input");
const LIFETIME_CONTAINER_ELEMENT = document.getElementById("lifetime_container");
const CHARACTER_AMOUNT_ELEMENT = document.getElementById("characters");
const LINES_AMOUNT_ELEMENT = document.getElementById("lines");
const BUTTONS_DEFAULT_ELEMENT = document.getElementById("buttons_default");
const BUTTON_NEW_ELEMENT = document.getElementById("btn_new");
const BUTTON_SAVE_ELEMENT = document.getElementById("btn_save");
const BUTTON_EDIT_ELEMENT = document.getElementById("btn_edit");
const BUTTON_DELETE_ELEMENT = document.getElementById("btn_delete");
const BUTTON_COPY_ELEMENT = document.getElementById("btn_copy");
const BUTTON_REPORT_ELEMENT = document.getElementById("btn_report");
const BUTTONS_EDIT_ELEMENT = document.getElementById("buttons_edit");
const BUTTON_EDIT_CANCEL_ELEMENT = document.getElementById("btn_edit_cancel");
const BUTTON_EDIT_APPLY_ELEMENT = document.getElementById("btn_edit_apply");
const BUTTON_TOGGLE_ENCRYPTION_ELEMENT = document.getElementById("btn_toggle_encryption");
let PASTE_ID;
let LANGUAGE;
let CODE;
let ENCRYPTION_KEY;
let ENCRYPTION_IV;
let EDIT_MODE = false;
let API_INFORMATION = {
version: "error",
pasteLifetime: -1,
modificationTokens: false,
reports: false
};
// Initializes the state system
export async function initialize() {
loadAPIInformation();
setupButtonFunctionality();
setupKeybinds();
// When embedded inside an iframe, add "embedded"
// class to body element.
if (window != window.parent) {
document.body.classList += " embedded";
}
// Enable encryption if enabled from last session
if (localStorage.getItem("encryption") === "true") {
BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.add("active");
}
if (location.pathname !== "/") {
// Extract the pastes data (ID and language)
const split = location.pathname.replace("/", "").split(".");
const pasteID = split[0];
const language = split[1];
// Try to retrieve the pastes data from the API
const response = await API.getPaste(pasteID);
if (!response.ok) {
Notifications.error("Could not load pastes: <b>" + await response.text() + "</b>");
setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000);
return;
}
// Set the persistent pastes data
PASTE_ID = pasteID;
LANGUAGE = language;
// Decode the response and decrypt the content if needed
const json = await response.json();
CODE = json.content;
if (json.metadata.pf_encryption) {
ENCRYPTION_KEY = location.hash.replace("#", "");
while (ENCRYPTION_KEY.length == 0) {
ENCRYPTION_KEY = prompt("Your decryption key:");
}
try {
CODE = await Encryption.decrypt(ENCRYPTION_KEY, json.metadata.pf_encryption.iv, CODE);
ENCRYPTION_IV = json.metadata.pf_encryption.iv;
} catch (error) {
console.log(error);
Notifications.error("Could not decrypt pastes; make sure the decryption key is correct.");
setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000);
return;
}
}
// Fill the code block with the just received data
updateCode();
} else {
// Give the user the opportunity to pastes his code
INPUT_ELEMENT.classList.remove("hidden");
INPUT_ELEMENT.focus();
LIFETIME_CONTAINER_ELEMENT.classList.remove("hidden");
}
// Update the state of the buttons to match the current state
updateButtonState();
INPUT_ELEMENT.addEventListener("input", () => {
updateLineNumbers(INPUT_ELEMENT.value);
if (BUTTON_SAVE_ELEMENT.hasAttribute("disabled") && INPUT_ELEMENT.value.length > 0) {
BUTTON_SAVE_ELEMENT.removeAttribute("disabled");
}
if (!BUTTON_SAVE_ELEMENT.hasAttribute("disabled") && INPUT_ELEMENT.value.length == 0) {
BUTTON_SAVE_ELEMENT.setAttribute("disabled", true);
}
});
}
// Loads the API information
async function loadAPIInformation() {
// try to retrieve the API information
const response = await API.getAPIInformation();
if (response.ok) {
API_INFORMATION = await response.json();
} else {
Notifications.error("Failed loading API information: <b>" + await response.text() + "</b>");
}
// Display the API version
document.getElementById("version").innerText = API_INFORMATION.version;
// Display the pastes lifetime
document.getElementById("lifetime").innerText = Duration.format(API_INFORMATION.pasteLifetime);
}
// Sets the current persistent code to the code block, highlights it and updates the line numbers
function updateCode() {
CODE_ELEMENT.innerHTML = LANGUAGE
? hljs.highlight(LANGUAGE, CODE).value
: hljs.highlightAuto(CODE).value;
updateLineNumbers(CODE);
}
function updateLineNumbers(content) {
CHARACTER_AMOUNT_ELEMENT.innerText = content.length;
LINES_AMOUNT_ELEMENT.innerText = content.split(/\n/).length;
if (content == "") {
LINE_NUMBERS_ELEMENT.innerHTML = "<span>></span>";
return;
}
LINE_NUMBERS_ELEMENT.innerHTML = content.split(/\n/).map((_, index) => `<span>${index + 1}</span>`).join("");
}
// Updates the button state according to the current state
function updateButtonState() {
if (PASTE_ID) {
BUTTON_SAVE_ELEMENT.setAttribute("disabled", true);
BUTTON_EDIT_ELEMENT.removeAttribute("disabled");
BUTTON_DELETE_ELEMENT.removeAttribute("disabled");
BUTTON_COPY_ELEMENT.removeAttribute("disabled");
if (API_INFORMATION.reports) {
BUTTON_REPORT_ELEMENT.classList.remove("hidden");
}
} else {
BUTTON_EDIT_ELEMENT.setAttribute("disabled", true);
BUTTON_DELETE_ELEMENT.setAttribute("disabled", true);
BUTTON_COPY_ELEMENT.setAttribute("disabled", true);
if (API_INFORMATION.reports) {
BUTTON_REPORT_ELEMENT.classList.add("hidden");
}
}
}
// Toggles the edit mode
function toggleEditMode() {
if (EDIT_MODE) {
EDIT_MODE = false;
INPUT_ELEMENT.classList.add("hidden");
LIFETIME_CONTAINER_ELEMENT.classList.add("hidden");
CODE_ELEMENT.classList.remove("hidden");
updateLineNumbers(CODE);
Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeOutDown", "0.3s", () => {
BUTTONS_EDIT_ELEMENT.classList.add("hidden");
BUTTONS_DEFAULT_ELEMENT.classList.remove("hidden");
Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeInDown", "0.3s");
});
} else {
EDIT_MODE = true;
CODE_ELEMENT.classList.add("hidden");
LIFETIME_CONTAINER_ELEMENT.classList.remove("hidden");
INPUT_ELEMENT.classList.remove("hidden");
INPUT_ELEMENT.value = CODE;
INPUT_ELEMENT.focus();
Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeOutUp", "0.3s", () => {
BUTTONS_DEFAULT_ELEMENT.classList.add("hidden");
BUTTONS_EDIT_ELEMENT.classList.remove("hidden");
Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeInUp", "0.3s");
});
}
}
// Sets up the keybinds for the buttons
function setupKeybinds() {
window.addEventListener("keydown", (event) => {
// All keybinds in the default button set include the CTRL key
if ((EDIT_MODE && !event.ctrlKey && event.code !== "Escape") || (!EDIT_MODE && !event.ctrlKey)) {
return;
}
// Find the DOM element of the button to trigger
let element;
if (EDIT_MODE) {
switch (event.code) {
case "Escape": {
element = BUTTON_EDIT_CANCEL_ELEMENT;
break
}
case "KeyS": {
element = BUTTON_EDIT_APPLY_ELEMENT;
break;
}
}
} else {
switch (event.code) {
case "KeyQ": {
element = BUTTON_NEW_ELEMENT;
break;
}
case "KeyS": {
element = BUTTON_SAVE_ELEMENT;
break;
}
case "KeyO": {
element = BUTTON_EDIT_ELEMENT;
break;
}
case "KeyX": {
element = BUTTON_DELETE_ELEMENT;
break;
}
case "KeyB": {
element = BUTTON_COPY_ELEMENT;
break;
}
}
}
// Trigger the found button
if (element) {
event.preventDefault();
if (element.hasAttribute("disabled")) {
return;
}
element.click();
}
});
// Additionally fix the behaviour of the Tab key
window.addEventListener("keydown", (event) => {
if (event.code != "Tab") {
return;
}
event.preventDefault();
insertTextAtCursor(inputElement, " ");
});
}
// Sets up the different button functionalities
function setupButtonFunctionality() {
BUTTON_NEW_ELEMENT.addEventListener("click", () => location.replace(location.protocol + "//" + location.host));
BUTTON_SAVE_ELEMENT.addEventListener("click", () => {
Spinner.surround(async () => {
// Only proceed if the input is not empty
if (!INPUT_ELEMENT.value) {
return;
}
// Encrypt the pastes if needed
let value = INPUT_ELEMENT.value;
let metadata;
let key;
if (BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.contains("active")) {
const encrypted = await Encryption.encrypt(await Encryption.generateEncryptionData(), value);
value = encrypted.result;
metadata = {
pf_encryption: {
alg: "AES-CBC",
iv: encrypted.iv
}
};
key = encrypted.key;
}
// Try to create the pastes
const response = await API.createPaste(value, metadata);
if (!response.ok) {
Notifications.error("Error while creating pastes: <b>" + await response.text() + "</b>");
return;
}
const data = await response.json();
// Display the modification token if provided
if (data.modificationToken) {
prompt("The modification token for your pastes is:", data.modificationToken);
}
// Redirect the user to his newly created pastes
location.replace(location.protocol + "//" + location.host + "/" + data.id + (key ? "#" + key : ""));
});
});
BUTTON_EDIT_ELEMENT.addEventListener("click", toggleEditMode);
BUTTON_DELETE_ELEMENT.addEventListener("click", () => {
Spinner.surround(async () => {
// Ask for the modification token
const modificationToken = prompt("Modification token:");
if (!modificationToken) {
return;
}
// Try to delete the pastes
const response = await API.deletePaste(PASTE_ID, modificationToken);
if (!response.ok) {
Notifications.error("Error while deleting pastes: <b>" + await response.text() + "</b>");
return;
}
// Redirect the user to the start page
location.replace(location.protocol + "//" + location.host);
});
});
BUTTON_COPY_ELEMENT.addEventListener("click", async () => {
if (!navigator.clipboard) {
Notifications.error("Clipboard API not supported by your browser.");
return;
}
await navigator.clipboard.writeText(CODE);
Notifications.success("Successfully copied the code.");
});
BUTTON_EDIT_CANCEL_ELEMENT.addEventListener("click", toggleEditMode);
BUTTON_EDIT_APPLY_ELEMENT.addEventListener("click", async () => {
// Only proceed if the input is not empty
if (!INPUT_ELEMENT.value) {
return;
}
// Ask for the modification token
const modificationToken = prompt("Modification token:");
if (!modificationToken) {
return;
}
// Re-encrypt the pastes data if needed
let value = INPUT_ELEMENT.value;
if (ENCRYPTION_KEY && ENCRYPTION_IV) {
const encrypted = await Encryption.encrypt(await Encryption.encryptionDataFromHex(ENCRYPTION_KEY, ENCRYPTION_IV), value);
value = encrypted.result;
}
// Try to edit the pastes
const response = await API.editPaste(PASTE_ID, modificationToken, value);
if (!response.ok) {
Notifications.error("Error while editing pastes: <b>" + await response.text() + "</b>");
return;
}
// Update the code and leave the edit mode
CODE = INPUT_ELEMENT.value;
updateCode();
toggleEditMode();
Notifications.success("Successfully edited pastes.");
});
BUTTON_TOGGLE_ENCRYPTION_ELEMENT.addEventListener("click", () => {
const active = BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.toggle("active");
localStorage.setItem("encryption", active);
Notifications.success((active ? "Enabled" : "Disabled") + " automatic pastes encryption.");
});
BUTTON_REPORT_ELEMENT.addEventListener("click", async () => {
// Ask the user for a reason
const reason = prompt("Reason:");
if (!reason) {
return;
}
// Try to report the pastes
const response = await API.reportPaste(PASTE_ID, reason);
if (!response.ok) {
Notifications.error("Error while reporting pastes: <b>" + await response.text() + "</b>");
return;
}
// Show the response message
const data = await response.json();
if (!data.success) {
Notifications.error("Error while reporting pastes: <b>" + data.message + "</b>");
return;
}
Notifications.success(data.message);
});
}
// 1:1 skid from https://stackoverflow.com/questions/7404366/how-do-i-insert-some-text-where-the-cursor-is
function insertTextAtCursor(element, text) {
let value = element.value, endIndex, range, doc = element.ownerDocument;
if (typeof element.selectionStart == "number"
&& typeof element.selectionEnd == "number") {
endIndex = element.selectionEnd;
element.value = value.slice(0, endIndex) + text + value.slice(endIndex);
element.selectionStart = element.selectionEnd = endIndex + text.length;
} else if (doc.selection != "undefined" && doc.selection.createRange) {
element.focus();
range = doc.selection.createRange();
range.collapse(false);
range.text = text;
range.select();
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.hljs{display:block;overflow-x:auto;padding:.5em;background:#002b36;color:#839496}.hljs-comment,.hljs-quote{color:#586e75}.hljs-addition,.hljs-keyword,.hljs-selector-tag{color:#859900}.hljs-doctag,.hljs-literal,.hljs-meta .hljs-meta-string,.hljs-number,.hljs-regexp,.hljs-string{color:#2aa198}.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-title{color:#268bd2}.hljs-attr,.hljs-attribute,.hljs-class .hljs-title,.hljs-template-variable,.hljs-type,.hljs-variable{color:#b58900}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-subst,.hljs-symbol{color:#cb4b16}.hljs-built_in,.hljs-deletion{color:#dc322f}.hljs-formula{background:#073642}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

View File

@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pasty</title>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="assets/libs/highlightjs/solarized-dark.min.css">
<link rel="stylesheet" href="assets/libs/animatecss/animate.min.css">
</head>
<body>
<div id="spinner-container" class="hidden"><div class="spinner"></div></div>
<div id="btn_report" class="hidden" title="Report paste">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-flag" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="5" y1="5" x2="5" y2="21" />
<line x1="19" y1="5" x2="19" y2="14" />
<path d="M5 5a5 5 0 0 1 7 0a5 5 0 0 0 7 0" />
<path d="M5 14a5 5 0 0 1 7 0a5 5 0 0 0 7 0" />
</svg>
</div>
<div class="navigation">
<div class="buttons" id="buttons_default">
<button class="button" id="btn_new" title="Create new paste (Ctrl + Q)">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-circle-plus" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" />
<circle cx="12" cy="12" r="9" />
<line x1="9" y1="12" x2="15" y2="12" />
<line x1="12" y1="9" x2="12" y2="15" />
</svg>
</button>
<button class="button" id="btn_save" title="Save paste (Ctrl + S)" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-device-floppy" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" />
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" />
<circle cx="12" cy="14" r="2" />
<polyline points="14 4 14 8 8 8 8 4" />
</svg>
</button>
<button class="button" id="btn_edit" title="Edit paste (Ctrl + O)" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-edit" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 7h-3a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-3" />
<path d="M9 15h3l8.5 -8.5a1.5 1.5 0 0 0 -3 -3l-8.5 8.5v3" />
<line x1="16" y1="5" x2="19" y2="8" />
</svg>
</button>
<button class="button" id="btn_delete" title="Delete paste (Ctrl + X)" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" />
<line x1="4" y1="7" x2="20" y2="7" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</svg>
</button>
<button class="button" id="btn_copy" title="Copy paste to clipboard (Ctrl + B)" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" />
<path d="M9 5H7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2V7a2 2 0 0 0 -2 -2h-2" />
<rect x="9" y="3" width="6" height="4" rx="2" />
</svg>
</button>
</div>
<div class="buttons hidden" id="buttons_edit">
<button class="button" id="btn_edit_cancel" title="Cancel (Escape)">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-back-up" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 13l-4 -4l4 -4m-4 4h11a4 4 0 0 1 0 8h-1" />
</svg>
</button>
<button class="button" id="btn_edit_apply" title="Apply changes (Ctrl + S)">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10" />
</svg>
</button>
</div>
<div class="buttons">
<button class="button" id="btn_toggle_encryption" title="Toggle encryption">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="5" y="11" width="14" height="10" rx="2" />
<circle cx="12" cy="16" r="1" />
<path d="M8 11v-4a4 4 0 0 1 8 0v4" />
</svg>
</button>
</div>
</div>
<div class="container">
<div id="notifications"></div>
<div id="lifetime_container" class="hidden">
Lifetime: <span id="lifetime">loading...</span>
</div>
<div id="content_length_container">
<span id="characters">0</span> characters, <span id="lines">0</span> lines
</div>
<div id="linenos"><span>></span></div>
<div id="content">
<div id="code"></div>
<textarea id="input" class="hidden"></textarea>
</div>
</div>
<div id="footer">
<div id="flex">
<div>
<a href="https://github.com/lus/pasty" target="_blank">GitHub</a>
<a href="https://go.lus.pm/discord" target="_blank">Discord</a>
<a href="https://github.com/lus/pasty/blob/develop/CREDITS.md" target="_blank">Credits</a>
</div>
<div class="version-container">
<span>Version:</span>
<div id="version">loading...</div>
</div>
</div>
</div>
<script src="assets/libs/highlightjs/highlight.min.js"></script>
<script type="text/javascript" src="assets/libs/aesjs/aes.min.js"></script>
<script src="assets/js/app.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,77 @@
package web
import (
"embed"
"errors"
"io"
"io/fs"
"mime"
"net/http"
"path/filepath"
"strconv"
"strings"
)
//go:embed frontend/*
var frontend embed.FS
func frontendHandler(notFoundHandler http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
path := strings.TrimSpace(strings.TrimSuffix(request.URL.Path, "/"))
isFirstLevel := strings.Count(path, "/") <= 1
file, err := frontend.Open(filepath.Join("frontend", path))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
if isFirstLevel {
serveIndexFile(writer, request)
} else {
notFoundHandler(writer, request)
}
return
}
writeErr(writer, err)
return
}
defer func() {
_ = file.Close()
}()
fileInfo, err := file.Stat()
if err != nil {
writeErr(writer, err)
return
}
if fileInfo.IsDir() {
if isFirstLevel {
serveIndexFile(writer, request)
} else {
notFoundHandler(writer, request)
}
return
}
content, err := io.ReadAll(file)
if err != nil {
writeErr(writer, err)
return
}
writer.Header().Set("Content-Type", mime.TypeByExtension(fileInfo.Name()))
writer.Header().Set("Content-Length", strconv.Itoa(len(content)))
_, _ = writer.Write(content)
}
}
func serveIndexFile(writer http.ResponseWriter, _ *http.Request) {
indexFile, err := frontend.ReadFile("frontend/index.html")
if err != nil {
writeErr(writer, err)
return
}
writer.Header().Set("Content-Type", "text/html")
writer.Header().Set("Content-Length", strconv.Itoa(len(indexFile)))
_, _ = writer.Write(indexFile)
}

View File

@@ -39,7 +39,11 @@ type Server struct {
func (server *Server) Start() error {
router := chi.NewRouter()
// Serve the web frontend
router.Get("/*", frontendHandler(router.NotFoundHandler()))
// Register the paste API endpoints
router.Get("/api/*", router.NotFoundHandler())
router.With(server.v2MiddlewareInjectPaste).Get("/api/v2/pastes/{paste_id}", server.v2EndpointGetPaste)
router.Post("/api/v2/pastes", server.v2EndpointCreatePaste)
router.With(server.v2MiddlewareInjectPaste, server.v2MiddlewareAuthorize).Patch("/api/v2/pastes/{paste_id}", server.v2EndpointModifyPaste)