From f2672ff7c5cf62db3289007411da74b5309ed46e Mon Sep 17 00:00:00 2001 From: "Jyotirmoy Bandyopadhyaya [Bravo68]" Date: Mon, 6 May 2024 20:15:46 +0530 Subject: [PATCH 1/2] web: added lock func --- apps/frontend/index.pug | 1 + apps/frontend/src/icons.ts | 13 ++++++- apps/frontend/src/index.ts | 72 ++++++++++++++++++++++++++++++++++---- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/apps/frontend/index.pug b/apps/frontend/index.pug index 3de99cf..2ba6f32 100755 --- a/apps/frontend/index.pug +++ b/apps/frontend/index.pug @@ -49,6 +49,7 @@ html(lang='en') .bottom-button-wrapper button#hide-button.btn(aria-label='Hide') + button#lock-button.btn(aria-label='Lock') .scrollbar-container .wrapper diff --git a/apps/frontend/src/icons.ts b/apps/frontend/src/icons.ts index 5003601..35cb7fc 100644 --- a/apps/frontend/src/icons.ts +++ b/apps/frontend/src/icons.ts @@ -11,7 +11,8 @@ import { FireOutlined, FileMarkdownOutlined, ShareAltOutlined, - VerticalLeftOutlined + VerticalLeftOutlined, + LockOutlined } from "@ant-design/icons-svg" import { renderIconDefinitionToSVGElement } from "@ant-design/icons-svg/es/helpers" import tippy from "tippy.js" @@ -23,6 +24,7 @@ const saveButton = document.getElementById("save-button") const newButton = document.getElementById("new-button") const copyButton = document.getElementById("copy-button") const hideButton = document.getElementById("hide-button") +const lockButton = document.getElementById("lock-button") const githubButton = document.getElementById("github-button") const shareButton = document.getElementById("share-button") const markdownButton = ( @@ -54,6 +56,7 @@ renderIcon(markdownButton, FileMarkdownOutlined) renderIcon(singleViewButton, FireOutlined) renderIcon(shareButton, ShareAltOutlined) renderIcon(rawButton, VerticalLeftOutlined) +renderIcon(lockButton, LockOutlined) tippy("#open-raw-button", { content: "Copy raw url to clipboard
Ctrl + X", @@ -135,6 +138,14 @@ tippy("#hide-button", { allowHTML: true, }) +tippy("#lock-button", { + content: "Add a password to your paste", + placement: "top", + animation: "scale", + theme: "rosepine", + allowHTML: true, +}) + const observer = new MutationObserver(callback) function callback() { diff --git a/apps/frontend/src/index.ts b/apps/frontend/src/index.ts index 88f9b2b..7f8ec6f 100755 --- a/apps/frontend/src/index.ts +++ b/apps/frontend/src/index.ts @@ -24,6 +24,8 @@ let rawContent = "" let buttonPaneHidden = false let isMarkdown = false let singleView = false +let locked = false +let lockPassword = "" const jsConfetti = new JSConfetti() @@ -52,6 +54,7 @@ const markdownButton = ( const singleViewButton = ( document.getElementById("single-view-button") ) +const lockButton = document.getElementById("lock-button") function hide(element: HTMLElement) { element.style.visibility = "hidden" @@ -73,6 +76,9 @@ function enable(element: HTMLButtonElement) { async function postPaste(content: string, callback: Function) { const payload = { content, single_view: singleView } + if (locked && lockPassword) { + payload["password"] = lockPassword + } await fetch(`${API_URL}/p/n`, { method: "POST", headers: { @@ -110,7 +116,45 @@ async function getPaste(id: string, callback: Function) { callback(null, data) return } - callback(data || { data: { message: "An unkown error occured!" } }) + else if (data["data"]["message"] == "This paste is locked, please provide a password."){ + console.log("This paste is locked, please provide a password.") + locked = true + lockPassword = prompt("Password :", ""); + if (lockPassword) { + getPasteWithPass(id, lockPassword, callback) + } + } + else { + callback(data || { data: { message: "An unkown error occured!" } }) + } + }) + .catch(() => { + callback({ + data: { message: "An API error occurred, please try again." }, + }) + }) +} + +async function getPasteWithPass(id: string, pass: string, callback: Function) { + await fetch(`${API_URL}/p/${id}?pass=${pass}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + referrerPolicy: "no-referrer", + }) + .then((response) => response.json()) + .then((data) => { + if (data["success"]) { + callback(null, data) + return + } + else if (data["data"]["message"] == "Incorrect password."){ + alert("Incorrect password.") + } + else { + callback(data || { data: { message: "An unkown error occured!" } }) + } }) .catch(() => { callback({ @@ -130,6 +174,7 @@ function newPaste() { disable(shareButton) disable(rawButton) enable(singleViewButton) + enable(lockButton) editor.value = "" rawContent = "" @@ -206,6 +251,7 @@ function viewPaste(content: string, views: string, singleView: boolean) { viewCounter.style.display = null viewCounter.textContent = views + disable(lockButton) try { wrapper.classList.remove("text-area-proper") @@ -331,8 +377,6 @@ copyButton.addEventListener("click", async function () { await duplicatePaste() }) - - newButton.addEventListener("click", function () { window.location.href = "/" }) @@ -349,6 +393,17 @@ hideButton.addEventListener("click", function () { toggleHiddenIcon(buttonPaneHidden) }) +lockButton.addEventListener("click", function () { + lockButton.lastElementChild.classList.toggle("fire") + if (locked) { + locked = false + } else { + locked = true + lockPassword = prompt("Password :", "Pa55%W0rd"); + } + show(lockButton.firstElementChild as HTMLElement) +}) + markdownButton.addEventListener("click", function () { toggleMarkdown() }) @@ -357,7 +412,7 @@ singleViewButton.addEventListener("click", function () { singleViewButton.lastElementChild.classList.toggle("fire") if (singleView) { singleView = false - hide(singleViewButton.firstElementChild as HTMLElement) + show(singleViewButton.firstElementChild as HTMLElement) } else { singleView = true show(singleViewButton.firstElementChild as HTMLElement) @@ -366,10 +421,10 @@ singleViewButton.addEventListener("click", function () { async function handlePopstate() { const path = window.location.pathname - if (path == "/") { newPaste() - } else { + } + else { const split = path.split("/") const id = split[split.length - 1] @@ -402,7 +457,10 @@ document.addEventListener( ) function rawUrlCopyToClipboard(){ - const rawUrl = `${API_URL}/p/r/${window.location.pathname.split("/")[1]}` + let rawUrl = `${API_URL}/p/r/${window.location.pathname.split("/")[1]}` + if(locked){ + rawUrl = `${API_URL}/p/r/${window.location.pathname.split("/")[1]}?pass=${lockPassword}` + } navigator.clipboard.writeText(rawUrl) addMessage("Copied raw URL to clipboard!") } From c12a46b21f5827b560496cd175e258326d152138 Mon Sep 17 00:00:00 2001 From: "Jyotirmoy Bandyopadhyaya [Bravo68]" Date: Mon, 6 May 2024 20:16:01 +0530 Subject: [PATCH 2/2] api: added lock func --- Cargo.lock | 2 +- packages/backend/Cargo.toml | 2 +- packages/backend/Dockerfile | 2 +- .../backend/migrations/001_password_lock.sql | 2 + packages/backend/src/models.rs | 7 +- packages/backend/src/routes.rs | 64 +++++++++++++++++-- 6 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 packages/backend/migrations/001_password_lock.sql diff --git a/Cargo.lock b/Cargo.lock index cb5753e..d2957bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,7 +308,7 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backend" -version = "1.1.2" +version = "1.2.0" dependencies = [ "actix-cors", "actix-governor", diff --git a/packages/backend/Cargo.toml b/packages/backend/Cargo.toml index 9945598..7d9d8d4 100755 --- a/packages/backend/Cargo.toml +++ b/packages/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "backend" -version = "1.1.2" +version = "1.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile index c30d798..f0fe9b1 100644 --- a/packages/backend/Dockerfile +++ b/packages/backend/Dockerfile @@ -7,7 +7,7 @@ COPY . . RUN apk add musl-dev \ && apk cache clean -RUN cargo build +RUN cargo build --release EXPOSE 8000 diff --git a/packages/backend/migrations/001_password_lock.sql b/packages/backend/migrations/001_password_lock.sql new file mode 100644 index 0000000..ee619fc --- /dev/null +++ b/packages/backend/migrations/001_password_lock.sql @@ -0,0 +1,2 @@ +ALTER TABLE pastes ADD column is_locked BOOLEAN DEFAULT false; +ALTER TABLE pastes ADD column password TEXT; \ No newline at end of file diff --git a/packages/backend/src/models.rs b/packages/backend/src/models.rs index fad7ff7..5b5a17f 100755 --- a/packages/backend/src/models.rs +++ b/packages/backend/src/models.rs @@ -9,12 +9,15 @@ pub struct Paste { pub views: i64, pub single_view: bool, pub expires_at: Option, + pub is_locked: bool, + pub password: Option } #[derive(Deserialize)] pub struct PartialPaste { pub content: String, - pub single_view: bool + pub single_view: bool, + pub password: Option, } #[derive(Serialize)] @@ -37,6 +40,8 @@ pub struct GetPasteResponse { pub views: i64, pub single_view: bool, pub expires_at: Option, + pub is_locked: bool, + pub password: Option, } #[derive(Serialize)] diff --git a/packages/backend/src/routes.rs b/packages/backend/src/routes.rs index f260e2b..dae056c 100644 --- a/packages/backend/src/routes.rs +++ b/packages/backend/src/routes.rs @@ -9,6 +9,7 @@ use badge_maker::{BadgeBuilder, Style}; use chrono::Duration; use nanoid::nanoid; use sqlx::{postgres::PgRow, types::chrono::Utc, Row}; +use serde::Deserialize; use crate::{ models::{ @@ -18,11 +19,17 @@ use crate::{ AppState, }; +#[derive(Deserialize)] +pub struct Info { + pub pass: Option +} + // Pastes #[get("/{id}")] -pub async fn get_paste(state: web::Data, id: web::Path) -> impl Responder { +pub async fn get_paste(state: web::Data, info: web::Query,id: web::Path) -> impl Responder { let id = id.into_inner(); + let info = &info.pass; let res: Result = sqlx::query_as::<_, Paste>(r#"SELECT * FROM pastes WHERE "id" = $1"#) @@ -49,6 +56,25 @@ pub async fn get_paste(state: web::Data, id: web::Path) -> imp println!("[GET] id={} views={} single_view={}", id, p.views + 1, p.single_view); } + if p.is_locked { + if info.is_none() { + return HttpResponse::Ok().json(ApiResponse { + success: false, + data: ApiError { + message: "This paste is locked, please provide a password.".to_string(), + }, + }); + } + else if info != &p.password { + return HttpResponse::Ok().json(ApiResponse { + success: false, + data: ApiError { + message: "Incorrect password.".to_string(), + }, + }); + } + } + HttpResponse::Ok().json(ApiResponse { success: true, data: GetPasteResponse { @@ -57,7 +83,9 @@ pub async fn get_paste(state: web::Data, id: web::Path) -> imp views: p.views + 1, single_view: p.single_view, expires_at: p.expires_at, - }, + is_locked: p.is_locked, + password: p.password + } }) } Err(e) => match e { @@ -84,8 +112,10 @@ pub async fn get_paste(state: web::Data, id: web::Path) -> imp } #[get("/r/{id}")] -pub async fn get_raw_paste(state: web::Data, id: web::Path) -> impl Responder { +pub async fn get_raw_paste(state: web::Data, info: web::Query, id: web::Path) -> impl Responder { let id = id.into_inner(); + + let info = &info.pass; let res: Result = sqlx::query_as::<_, Paste>(r#"SELECT * FROM pastes WHERE "id" = $1"#) @@ -111,6 +141,25 @@ pub async fn get_raw_paste(state: web::Data, id: web::Path) -> println!("[GET] raw id={} views={} single_view={}", id, p.views + 1, p.single_view); } + if p.is_locked { + if info.is_none() { + return HttpResponse::Ok().json(ApiResponse { + success: false, + data: ApiError { + message: "This paste is locked, please provide a password.".to_string(), + }, + }); + } + else if info != &p.password { + return HttpResponse::Ok().json(ApiResponse { + success: false, + data: ApiError { + message: "Incorrect password.".to_string(), + }, + }); + } + } + HttpResponse::Ok() .content_type("text/plain") .body(p.content) @@ -165,13 +214,20 @@ pub async fn new_paste( let content = data.content.clone(); let single_view = data.single_view; + let password = if Option::is_some(&data.password) { + &data.password + } else { &{ + None + } }; let res = - sqlx::query(r#"INSERT INTO pastes("id", "content", "single_view", "expires_at") VALUES ($1, $2, $3, $4)"#) + sqlx::query(r#"INSERT INTO pastes("id", "content", "single_view", "expires_at", "is_locked", "password") VALUES ($1, $2, $3, $4, $5, $6)"#) .bind(id.clone()) .bind(content.clone()) .bind(single_view) .bind(expires_at) + .bind(data.password.is_some()) + .bind(password) .execute(&state.pool) .await;