Skip to content

Commit

Permalink
Merge pull request #3 from BRAVO68WEB/feat/password-lock
Browse files Browse the repository at this point in the history
feat/password lock
  • Loading branch information
BRAVO68WEB authored May 6, 2024
2 parents 53446a6 + c12a46b commit 2290e5d
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 16 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/frontend/index.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion apps/frontend/src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -23,6 +24,7 @@ const saveButton = <HTMLButtonElement>document.getElementById("save-button")
const newButton = <HTMLButtonElement>document.getElementById("new-button")
const copyButton = <HTMLButtonElement>document.getElementById("copy-button")
const hideButton = <HTMLButtonElement>document.getElementById("hide-button")
const lockButton = <HTMLButtonElement>document.getElementById("lock-button")
const githubButton = <HTMLButtonElement>document.getElementById("github-button")
const shareButton = <HTMLButtonElement>document.getElementById("share-button")
const markdownButton = <HTMLButtonElement>(
Expand Down Expand Up @@ -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<br><span class='keybind'>Ctrl + X</span>",
Expand Down Expand Up @@ -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() {
Expand Down
72 changes: 65 additions & 7 deletions apps/frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ let rawContent = ""
let buttonPaneHidden = false
let isMarkdown = false
let singleView = false
let locked = false
let lockPassword = ""

const jsConfetti = new JSConfetti()

Expand Down Expand Up @@ -52,6 +54,7 @@ const markdownButton = <HTMLButtonElement>(
const singleViewButton = <HTMLButtonElement>(
document.getElementById("single-view-button")
)
const lockButton = <HTMLButtonElement>document.getElementById("lock-button")

function hide(element: HTMLElement) {
element.style.visibility = "hidden"
Expand All @@ -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: {
Expand Down Expand Up @@ -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({
Expand All @@ -130,6 +174,7 @@ function newPaste() {
disable(shareButton)
disable(rawButton)
enable(singleViewButton)
enable(lockButton)

editor.value = ""
rawContent = ""
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -331,8 +377,6 @@ copyButton.addEventListener("click", async function () {
await duplicatePaste()
})



newButton.addEventListener("click", function () {
window.location.href = "/"
})
Expand All @@ -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()
})
Expand All @@ -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)
Expand All @@ -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]

Expand Down Expand Up @@ -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!")
}
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ COPY . .
RUN apk add musl-dev \
&& apk cache clean

RUN cargo build
RUN cargo build --release

EXPOSE 8000

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/migrations/001_password_lock.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE pastes ADD column is_locked BOOLEAN DEFAULT false;
ALTER TABLE pastes ADD column password TEXT;
7 changes: 6 additions & 1 deletion packages/backend/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ pub struct Paste {
pub views: i64,
pub single_view: bool,
pub expires_at: Option<NaiveDateTime>,
pub is_locked: bool,
pub password: Option<String>
}

#[derive(Deserialize)]
pub struct PartialPaste {
pub content: String,
pub single_view: bool
pub single_view: bool,
pub password: Option<String>,
}

#[derive(Serialize)]
Expand All @@ -37,6 +40,8 @@ pub struct GetPasteResponse {
pub views: i64,
pub single_view: bool,
pub expires_at: Option<NaiveDateTime>,
pub is_locked: bool,
pub password: Option<String>,
}

#[derive(Serialize)]
Expand Down
64 changes: 60 additions & 4 deletions packages/backend/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -18,11 +19,17 @@ use crate::{
AppState,
};

#[derive(Deserialize)]
pub struct Info {
pub pass: Option<String>
}

// Pastes

#[get("/{id}")]
pub async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> impl Responder {
pub async fn get_paste(state: web::Data<AppState>, info: web::Query<Info>,id: web::Path<String>) -> impl Responder {
let id = id.into_inner();
let info = &info.pass;

let res: Result<Paste, sqlx::Error> =
sqlx::query_as::<_, Paste>(r#"SELECT * FROM pastes WHERE "id" = $1"#)
Expand All @@ -49,6 +56,25 @@ pub async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> 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 {
Expand All @@ -57,7 +83,9 @@ pub async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> 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 {
Expand All @@ -84,8 +112,10 @@ pub async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> imp
}

#[get("/r/{id}")]
pub async fn get_raw_paste(state: web::Data<AppState>, id: web::Path<String>) -> impl Responder {
pub async fn get_raw_paste(state: web::Data<AppState>, info: web::Query<Info>, id: web::Path<String>) -> impl Responder {
let id = id.into_inner();

let info = &info.pass;

let res: Result<Paste, sqlx::Error> =
sqlx::query_as::<_, Paste>(r#"SELECT * FROM pastes WHERE "id" = $1"#)
Expand All @@ -111,6 +141,25 @@ pub async fn get_raw_paste(state: web::Data<AppState>, id: web::Path<String>) ->
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)
Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit 2290e5d

Please sign in to comment.