Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/password lock #3

Merged
merged 2 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading