mirror of https://github.com/zer0bin-dev/zer0bin
first
This commit is contained in:
commit
656b561073
|
@ -0,0 +1,2 @@
|
|||
**/target/
|
||||
config.json
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 Domterion
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,15 @@
|
|||
<div align="center">
|
||||
<h1>zer0b.in</h1>
|
||||
zer0b.in, or zer0, is a hastebin like service
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
# API
|
||||
|
||||
[**GET**] `/p/:id` - Get a paste
|
||||
[**POST**] `/p/n` - Post a new paste
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
actix-rt = "2.6.0"
|
||||
actix-web = "4.0.0-rc.1"
|
||||
actix-cors = "0.6.0"
|
||||
|
||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "postgres", "chrono"] }
|
||||
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
|
||||
nanoid = "0.4.0"
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
CREATE TABLE IF NOT EXISTS pastes (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"content" TEXT NOT NULL,
|
||||
"views" BIGINT DEFAULT 0,
|
||||
"expires_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
"created_at" TIMESTAMP WITHOUT TIME ZONE DEFAULT(NOW() AT TIME ZONE 'utc')
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION deleteExpiredPastes() RETURNS trigger AS $pastes_expire$
|
||||
BEGIN
|
||||
DELETE FROM pastes WHERE "expires_at" < now() AT TIME ZONE 'utc';
|
||||
RETURN NEW;
|
||||
END;
|
||||
$pastes_expire$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER checkPastes BEFORE INSERT OR UPDATE ON pastes
|
||||
FOR STATEMENT EXECUTE PROCEDURE deleteExpiredPastes();
|
|
@ -0,0 +1,27 @@
|
|||
use std::{fs::File, path::PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub pastes: PastesConfig,
|
||||
pub databases: DatabasesConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PastesConfig {
|
||||
pub character_limit: usize,
|
||||
pub days_til_expiration: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DatabasesConfig {
|
||||
pub postgres_uri: String,
|
||||
}
|
||||
|
||||
pub fn load(path: PathBuf) -> Config {
|
||||
let file = File::open(path).expect("Failed to load config.json");
|
||||
let config = serde_json::from_reader(file).unwrap();
|
||||
|
||||
config
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
mod config;
|
||||
mod models;
|
||||
|
||||
use std::{io, path::PathBuf};
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{
|
||||
get, post,
|
||||
web::{self, Data},
|
||||
App, HttpResponse, HttpServer, Responder,
|
||||
};
|
||||
use config::Config;
|
||||
|
||||
use chrono::Duration;
|
||||
use models::{ApiError, ApiResponse, GetPasteResponse, NewPasteResponse, PartialPaste, Paste};
|
||||
use nanoid::nanoid;
|
||||
use sqlx::{postgres::PgPoolOptions, types::chrono::Utc, PgPool};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
config: Config,
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> impl Responder {
|
||||
let id = id.into_inner();
|
||||
|
||||
let res: Result<Paste, sqlx::Error> =
|
||||
sqlx::query_as::<_, Paste>(r#"SELECT * FROM pastes WHERE "id" = $1"#)
|
||||
.bind(id.clone())
|
||||
.fetch_one(&state.pool)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(p) => {
|
||||
if let Err(_) =
|
||||
sqlx::query(r#"UPDATE pastes SET "views" = "views" + 1 WHERE "id" = $1"#)
|
||||
.bind(id.clone())
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
{
|
||||
// we should probably handle this but eh
|
||||
};
|
||||
|
||||
return HttpResponse::Ok().json(ApiResponse {
|
||||
success: true,
|
||||
data: GetPasteResponse {
|
||||
id: p.id,
|
||||
content: p.content,
|
||||
views: p.views + 1,
|
||||
expires_at: p.expires_at,
|
||||
},
|
||||
});
|
||||
}
|
||||
Err(e) => match e {
|
||||
sqlx::Error::RowNotFound => {
|
||||
return HttpResponse::InternalServerError().json(ApiResponse {
|
||||
success: false,
|
||||
data: ApiError {
|
||||
message: format!("Paste {id} wasnt found."),
|
||||
},
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
return HttpResponse::InternalServerError().json(ApiResponse {
|
||||
success: false,
|
||||
data: ApiError {
|
||||
message: "Unknown error occurred, please try again.".to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/n")]
|
||||
async fn new_paste(state: web::Data<AppState>, data: web::Json<PartialPaste>) -> impl Responder {
|
||||
if data.content.is_empty() || data.content.len() > state.config.pastes.character_limit {
|
||||
let character_limit = state.config.pastes.character_limit;
|
||||
|
||||
return HttpResponse::BadRequest().json(ApiResponse {
|
||||
success: false,
|
||||
data: ApiError {
|
||||
message: format!("Maximum file length exceeded, maximum is {character_limit} characters. Or the content is blank.."),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let id = nanoid!(10);
|
||||
let expires_at = Utc::now() + Duration::days(state.config.pastes.days_til_expiration);
|
||||
|
||||
let res =
|
||||
sqlx::query(r#"INSERT INTO pastes("id", "content", "expires_at") VALUES ($1, $2, $3)"#)
|
||||
.bind(id.clone())
|
||||
.bind(data.content.clone())
|
||||
.bind(expires_at)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
return HttpResponse::Ok().json(ApiResponse {
|
||||
success: true,
|
||||
data: NewPasteResponse {
|
||||
id,
|
||||
content: data.content.clone(),
|
||||
},
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().json(ApiResponse {
|
||||
success: false,
|
||||
data: ApiError {
|
||||
message: "Unknown error occurred, please try again.".to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
let config = config::load(PathBuf::from("../config.json"));
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(100)
|
||||
.connect(&config.databases.postgres_uri)
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
|
||||
let state = AppState { config, pool };
|
||||
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default()
|
||||
.allow_any_header()
|
||||
.allow_any_method()
|
||||
.allow_any_origin()
|
||||
.send_wildcard()
|
||||
.max_age(3600);
|
||||
|
||||
App::new()
|
||||
.wrap(cors)
|
||||
.app_data(Data::new(state.clone()))
|
||||
.service(web::scope("/p").service(get_paste).service(new_paste))
|
||||
})
|
||||
.bind("localhost:8000")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct Paste {
|
||||
pub id: String,
|
||||
pub content: String,
|
||||
pub views: i64,
|
||||
pub expires_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PartialPaste {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub success: bool,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NewPasteResponse {
|
||||
pub id: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetPasteResponse {
|
||||
pub id: String,
|
||||
pub content: String,
|
||||
pub views: i64,
|
||||
pub expires_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ApiError {
|
||||
pub message: String,
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>zer0b.in</title>
|
||||
|
||||
<link href="styles.css" rel="stylesheet" />
|
||||
|
||||
<script
|
||||
src="https://kit.fontawesome.com/87d6c5dd5a.js"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/base16/solarized-dark.min.css"
|
||||
integrity="sha512-kBHeOXtsKtA97/1O3ebZzWRIwiWEOmdrylPrOo3D2+pGhq1m+1CroSOVErIlsqn1xmYowKfQNVDhsczIzeLpmg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/highlight.min.js"
|
||||
integrity="sha512-IaaKO80nPNs5j+VLxd42eK/7sYuXQmr+fyywCNA0e+C6gtQnuCXNtORe9xR4LqGPz5U9VpH+ff41wKs/ZmC3iA=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<script>
|
||||
hljs.highlightAll();
|
||||
</script>
|
||||
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"
|
||||
integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ul id="messages"></ul>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="line-numbers"></div>
|
||||
|
||||
<!-- We only show this if they are viewing a paste-->
|
||||
<pre
|
||||
id="code-view-pre"
|
||||
style="display: none"
|
||||
><code id="code-view"></code></pre>
|
||||
|
||||
<!-- We only show the textarea if they are creating a new paste-->
|
||||
<textarea
|
||||
spellcheck="false"
|
||||
autofocus
|
||||
name="value"
|
||||
id="text-area"
|
||||
style="display: none"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="button-wrapper">
|
||||
<a href="/" class="logo">zer0b.in</a>
|
||||
<div class="buttons">
|
||||
<button id="save-button" class="btn">
|
||||
<i class="fas fa-save fa-xl"></i>
|
||||
</button>
|
||||
<button id="new-button" class="btn">
|
||||
<i class="fas fa-file-medical fa-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,129 @@
|
|||
//var lineNumbers, editor, codeView, apiUrl, saveButton;
|
||||
let lineNumbers = $(".line-numbers");
|
||||
let editor = $("#text-area");
|
||||
let codeViewPre = $("#code-view-pre");
|
||||
let codeView = $("#code-view");
|
||||
let messages = $("#messages");
|
||||
|
||||
let saveButton = $("#save-button");
|
||||
let newButton = $("#new-button");
|
||||
|
||||
let apiUrl = "http://localhost:8000";
|
||||
|
||||
function postPaste(content, callback) {
|
||||
var data = {
|
||||
content,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `${apiUrl}/p/n`,
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
success: function (res) {
|
||||
callback(null, res);
|
||||
},
|
||||
error: function (xhr) {
|
||||
callback(
|
||||
JSON.parse(
|
||||
xhr.responseText ||
|
||||
`{"data": { "message": "Unknown error occurred.." } }`
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getPaste(id, callback) {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: `${apiUrl}/p/${id}`,
|
||||
contentType: "application/json",
|
||||
success: function (res) {
|
||||
callback(null, res);
|
||||
},
|
||||
error: function (xhr) {
|
||||
callback(
|
||||
JSON.parse(
|
||||
xhr.responseText ||
|
||||
`{"data": { "message": "Unknown error occurred.." } }`
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function newPaste() {
|
||||
lineNumbers.html("<div>></div>");
|
||||
|
||||
saveButton.prop("disabled", false);
|
||||
newButton.prop("disabled", true);
|
||||
|
||||
editor.val("");
|
||||
|
||||
editor.show();
|
||||
codeViewPre.hide();
|
||||
}
|
||||
|
||||
function addMessage(message) {
|
||||
let msg = $(`<li>${message || "Unknown error occurred.."}</li>`);
|
||||
messages.prepend(msg);
|
||||
|
||||
setTimeout(function () {
|
||||
msg.slideUp("fast", function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function viewPaste(content) {
|
||||
lineNumbers.html("");
|
||||
for (let i = 1; i <= content.split("\n").length; i++) {
|
||||
lineNumbers.append(`<div>${i}</div>`);
|
||||
}
|
||||
|
||||
codeView.html(content);
|
||||
|
||||
editor.hide();
|
||||
codeViewPre.show();
|
||||
}
|
||||
|
||||
saveButton.click(function () {
|
||||
if (editor.val() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
postPaste(editor.val(), function (err, res) {
|
||||
if (err) {
|
||||
addMessage(err["data"]["message"]);
|
||||
} else {
|
||||
window.location.href = `/?id=${res["data"]["id"]}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
newButton.click(function () {
|
||||
window.location.href = "/";
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
let id = new URLSearchParams(window.location.search).get("id");
|
||||
|
||||
if (id == null) {
|
||||
newPaste();
|
||||
return;
|
||||
}
|
||||
|
||||
getPaste(id, function (err, res) {
|
||||
if (err) {
|
||||
newPaste();
|
||||
} else {
|
||||
let content = res["data"]["content"];
|
||||
|
||||
viewPaste(hljs.highlightAuto(content).value);
|
||||
|
||||
saveButton.prop("disabled", true);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,118 @@
|
|||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #002b36;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
padding: 1rem 0.5rem;
|
||||
font-size: 1rem;
|
||||
height: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
color: #7d7d7d;
|
||||
font-family: monospace;
|
||||
text-align: end;
|
||||
user-select: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#code-view {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: transparent;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
padding-left: 1rem;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
background-color: #00222b;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.buttons > * + * {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
color: #8fbcc8;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
transition: background-color 200ms ease-in-out;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:disabled,
|
||||
.btn[disabled] {
|
||||
color: #255662;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: #8fbcc8;
|
||||
width: 100%;
|
||||
font-family: monospace;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#messages {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 168px;
|
||||
z-index: 1000;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
#messages li {
|
||||
background-color: rgba(37, 86, 98, 0.8);
|
||||
font-family: monospace;
|
||||
color: white;
|
||||
padding: 7px;
|
||||
}
|
Loading…
Reference in New Issue