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:
388
internal/web/frontend/assets/css/style.css
Normal file
388
internal/web/frontend/assets/css/style.css
Normal 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 */
|
9
internal/web/frontend/assets/css/style.css.map
Normal file
9
internal/web/frontend/assets/css/style.css.map
Normal file
File diff suppressed because one or more lines are too long
333
internal/web/frontend/assets/css/style.scss
Normal file
333
internal/web/frontend/assets/css/style.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
5
internal/web/frontend/assets/js/app.js
Normal file
5
internal/web/frontend/assets/js/app.js
Normal 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);
|
12
internal/web/frontend/assets/js/modules/animation.js
Normal file
12
internal/web/frontend/assets/js/modules/animation.js
Normal 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});
|
||||
}
|
57
internal/web/frontend/assets/js/modules/api.js
Normal file
57
internal/web/frontend/assets/js/modules/api.js
Normal 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
|
||||
})
|
||||
});
|
||||
}
|
32
internal/web/frontend/assets/js/modules/duration.js
Normal file
32
internal/web/frontend/assets/js/modules/duration.js
Normal 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(", ");
|
||||
}
|
60
internal/web/frontend/assets/js/modules/encryption.js
Normal file
60
internal/web/frontend/assets/js/modules/encryption.js
Normal 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));
|
||||
}
|
25
internal/web/frontend/assets/js/modules/notifications.js
Normal file
25
internal/web/frontend/assets/js/modules/notifications.js
Normal 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);
|
||||
}
|
21
internal/web/frontend/assets/js/modules/spinner.js
Normal file
21
internal/web/frontend/assets/js/modules/spinner.js
Normal 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();
|
||||
}
|
438
internal/web/frontend/assets/js/modules/state.js
Normal file
438
internal/web/frontend/assets/js/modules/state.js
Normal 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();
|
||||
}
|
||||
}
|
1
internal/web/frontend/assets/libs/aesjs/aes.min.js
vendored
Normal file
1
internal/web/frontend/assets/libs/aesjs/aes.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
internal/web/frontend/assets/libs/animatecss/animate.min.css
vendored
Normal file
7
internal/web/frontend/assets/libs/animatecss/animate.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
44
internal/web/frontend/assets/libs/highlightjs/highlight.min.js
vendored
Normal file
44
internal/web/frontend/assets/libs/highlightjs/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
internal/web/frontend/assets/libs/highlightjs/solarized-dark.min.css
vendored
Normal file
1
internal/web/frontend/assets/libs/highlightjs/solarized-dark.min.css
vendored
Normal 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}
|
143
internal/web/frontend/index.html
Normal file
143
internal/web/frontend/index.html
Normal 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>
|
77
internal/web/frontend_server.go
Normal file
77
internal/web/frontend_server.go
Normal 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)
|
||||
}
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user