mirror of
https://github.com/eliasrenman/url-shortener.git
synced 2026-03-16 20:16:06 +01:00
feat: basic creation and deletion of urls
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2607,6 +2607,7 @@ dependencies = [
|
||||
"rocket",
|
||||
"rocket_oauth2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
@@ -18,4 +18,5 @@ reqwest = { version = "0.12.14", default-features = false, features = [
|
||||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
rocket_oauth2 = "0.5.0"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
uuid = { version = "1.16.0", features = ["v3", "v5"] }
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::env;
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::{Builder, Uuid};
|
||||
use uuid::Builder;
|
||||
|
||||
use crate::User;
|
||||
|
||||
@@ -39,7 +39,7 @@ pub fn create_user_token(user: User) -> Result<std::string::String, jsonwebtoken
|
||||
}
|
||||
|
||||
pub fn jwts_decode(token: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
|
||||
decode::<Claims>(token, &get_decoding_key(), &Validation::default())
|
||||
decode::<Claims>(token, &get_decoding_key(), &get_validation())
|
||||
}
|
||||
|
||||
pub fn generate_id(namespace: &str, id: &str) -> String {
|
||||
@@ -54,3 +54,22 @@ fn get_encoding_key() -> EncodingKey {
|
||||
fn get_decoding_key() -> DecodingKey {
|
||||
DecodingKey::from_secret(env::var("JWT_SECRET").unwrap().as_ref())
|
||||
}
|
||||
|
||||
fn get_validation() -> Validation {
|
||||
let address = env::var("BASE_URL").unwrap();
|
||||
let mut validation = Validation::default();
|
||||
validation.set_audience(&[address.clone()]);
|
||||
validation.set_issuer(&[address]);
|
||||
validation.set_required_spec_claims(&["aud", "exp", "iat", "iss", "nbf", "sub", "name"]);
|
||||
validation
|
||||
}
|
||||
// aud: address.clone(),
|
||||
// exp: Utc::now()
|
||||
// .checked_add_signed(TimeDelta::new(week_in_seconds, 0).unwrap())
|
||||
// .unwrap()
|
||||
// .timestamp() as usize,
|
||||
// iat: Utc::now().timestamp() as usize,
|
||||
// iss: address,
|
||||
// nbf: Utc::now().timestamp() as usize,
|
||||
// sub: user.id,
|
||||
// name: user.username,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use super::schema::urls;
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Queryable, Selectable, AsChangeset)]
|
||||
#[derive(Serialize, Queryable, Selectable, AsChangeset)]
|
||||
#[serde(rename_all(serialize = "camelCase"))]
|
||||
#[diesel(table_name = urls)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Urls {
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::schema::urls::dsl::*;
|
||||
use super::schema::urls::*;
|
||||
use crate::*;
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{delete, dsl::insert_into, prelude::*, result::Error};
|
||||
use diesel::{associations::HasTable, delete, dsl::insert_into, prelude::*, result::Error, select};
|
||||
|
||||
pub fn get_entry(path: &str) -> Result<Urls, Error> {
|
||||
let connection = &mut establish_connection();
|
||||
@@ -11,7 +11,7 @@ pub fn get_entry(path: &str) -> Result<Urls, Error> {
|
||||
}
|
||||
|
||||
pub fn upsert_entry(
|
||||
username: &str,
|
||||
owned_by_id: &str,
|
||||
path: &str,
|
||||
dest: &str,
|
||||
time_to_live: Option<NaiveDateTime>,
|
||||
@@ -22,19 +22,27 @@ pub fn upsert_entry(
|
||||
url.eq(path),
|
||||
destination_url.eq(dest),
|
||||
ttl.eq(time_to_live),
|
||||
owned_by.eq(username),
|
||||
owned_by.eq(owned_by_id),
|
||||
))
|
||||
.on_conflict(url)
|
||||
.do_update()
|
||||
.set((
|
||||
destination_url.eq(dest),
|
||||
ttl.eq(time_to_live),
|
||||
owned_by.eq(username),
|
||||
owned_by.eq(owned_by_id),
|
||||
))
|
||||
.execute(connection)
|
||||
}
|
||||
|
||||
pub fn delete_entry(id: &str, path: &str) -> Result<usize, Error> {
|
||||
pub fn delete_entry(owned_by_id: &str, path: &str) -> Result<usize, Error> {
|
||||
let connection = &mut establish_connection();
|
||||
delete(urls.filter(url.eq(path)).filter(owned_by.eq(id))).execute(connection)
|
||||
delete(urls.filter(url.eq(path)).filter(owned_by.eq(owned_by_id))).execute(connection)
|
||||
}
|
||||
|
||||
pub fn list(owned_by_id: &str) -> Vec<Urls> {
|
||||
let connection = &mut establish_connection();
|
||||
urls.filter(owned_by.eq(owned_by_id))
|
||||
.select(Urls::as_select())
|
||||
.load(connection)
|
||||
.expect("Error loading urls")
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpsertUrlDto<'r> {
|
||||
pub url: &'r str,
|
||||
pub destination_url: &'r str,
|
||||
pub ttl: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use rocket::{http::Status, response::Redirect};
|
||||
|
||||
use crate::{
|
||||
db::url::{delete_entry, get_entry, upsert_entry},
|
||||
db::url::{delete_entry, get_entry, list, upsert_entry},
|
||||
dto::UpsertUrlDto,
|
||||
};
|
||||
|
||||
@@ -10,18 +10,23 @@ pub fn handle_redirect(url: &str) -> Result<Redirect, (Status, &'static str)> {
|
||||
Ok(Redirect::to(row.destination_url))
|
||||
}
|
||||
|
||||
pub fn handle_upsert(id: &str, dto: UpsertUrlDto<'_>) -> (Status, &'static str) {
|
||||
let row = upsert_entry(id, dto.url, dto.destination_url, dto.ttl);
|
||||
pub fn handle_upsert(owned_by: &str, url: &str, dto: UpsertUrlDto<'_>) -> (Status, &'static str) {
|
||||
let row = upsert_entry(owned_by, url, dto.destination_url, dto.ttl);
|
||||
if row.is_err() {
|
||||
return (Status::BadRequest, "Failed to upsert redirect");
|
||||
}
|
||||
(Status::Ok, "Successfully upserted redirect")
|
||||
}
|
||||
|
||||
pub fn handle_delete(id: &str, url: &str) -> (Status, &'static str) {
|
||||
let row = delete_entry(id, url);
|
||||
pub fn handle_delete(owned_by: &str, url: &str) -> (Status, &'static str) {
|
||||
let row = delete_entry(owned_by, url);
|
||||
if row.is_err() {
|
||||
return (Status::BadRequest, "Failed to delete redirect");
|
||||
}
|
||||
(Status::Ok, "Successfully deleted redirect")
|
||||
}
|
||||
|
||||
pub fn handle_list(owned_by: &str) -> String {
|
||||
let urls = list(owned_by);
|
||||
serde_json::to_string(&urls).unwrap()
|
||||
}
|
||||
|
||||
21
src/main.rs
21
src/main.rs
@@ -7,12 +7,13 @@ use auth::github::github_fairing;
|
||||
use auth::jwts::jwts_decode;
|
||||
use db::establish_connection;
|
||||
use dotenvy::dotenv;
|
||||
use handlers::url::{handle_delete, handle_redirect, handle_upsert};
|
||||
use handlers::url::{handle_delete, handle_list, handle_redirect, handle_upsert};
|
||||
use rocket::fs::{FileServer, relative};
|
||||
use rocket::http::{Cookie, CookieJar};
|
||||
use rocket::request;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{http::Status, response::Redirect};
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
@@ -32,6 +33,7 @@ impl<'r> request::FromRequest<'r> for User {
|
||||
.expect("request cookies");
|
||||
if let Some(cookie) = cookies.get("access_token") {
|
||||
let decoded = jwts_decode(cookie.value());
|
||||
|
||||
if decoded.is_ok() {
|
||||
let unwrapped = decoded.unwrap();
|
||||
return request::Outcome::Success(User {
|
||||
@@ -41,7 +43,7 @@ impl<'r> request::FromRequest<'r> for User {
|
||||
}
|
||||
}
|
||||
|
||||
request::Outcome::Forward(Status::Unauthorized)
|
||||
request::Outcome::Error((Status::Unauthorized, ()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,14 +52,14 @@ fn rocket() -> _ {
|
||||
dotenv().ok();
|
||||
rocket::build()
|
||||
.mount("/", routes![shortner, logout])
|
||||
.mount("/api", routes![upsert, delete])
|
||||
.mount("/api/url", routes![upsert, delete, list])
|
||||
.mount("/", FileServer::from(relative!("web/dist")))
|
||||
.attach(github_fairing())
|
||||
}
|
||||
|
||||
#[get("/logout")]
|
||||
fn logout(cookies: &CookieJar<'_>) -> Redirect {
|
||||
cookies.remove(Cookie::from("username"));
|
||||
cookies.remove(Cookie::from("access_token"));
|
||||
Redirect::to("/")
|
||||
}
|
||||
|
||||
@@ -66,9 +68,14 @@ fn shortner(url: &str) -> Result<Redirect, (Status, &'static str)> {
|
||||
handle_redirect(url)
|
||||
}
|
||||
|
||||
#[post("/write", format = "json", data = "<upsert>")]
|
||||
fn upsert(user: User, upsert: Json<dto::UpsertUrlDto<'_>>) -> (Status, &'static str) {
|
||||
handle_upsert(&user.id.as_str(), upsert.0)
|
||||
#[patch("/<url>", format = "json", data = "<upsert>")]
|
||||
fn upsert(user: User, url: &str, upsert: Json<dto::UpsertUrlDto<'_>>) -> (Status, &'static str) {
|
||||
handle_upsert(&user.id.as_str(), url, upsert.0)
|
||||
}
|
||||
|
||||
#[get("/list")]
|
||||
fn list(user: User) -> String {
|
||||
handle_list(&user.id.as_str())
|
||||
}
|
||||
|
||||
#[delete("/<url>")]
|
||||
|
||||
60
web/bun.lock
60
web/bun.lock
@@ -5,11 +5,15 @@
|
||||
"name": "web",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.14",
|
||||
"@tanstack/svelte-query": "^5.69.0",
|
||||
"axios": "^1.8.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"tailwindcss": "^4.0.14",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/node": "^22.13.11",
|
||||
"svelte": "^5.20.2",
|
||||
"svelte-check": "^4.1.4",
|
||||
"typescript": "~5.7.2",
|
||||
@@ -152,28 +156,54 @@
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.0.14", "", { "dependencies": { "@tailwindcss/node": "4.0.14", "@tailwindcss/oxide": "4.0.14", "lightningcss": "1.29.2", "tailwindcss": "4.0.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-y69ztPTRFy+13EPS/7dEFVl7q2Goh1pQueVO8IfGeyqSpcx/joNJXFk0lLhMgUbF0VFJotwRSb9ZY7Xoq3r26Q=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.69.0", "", {}, "sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ=="],
|
||||
|
||||
"@tanstack/svelte-query": ["@tanstack/svelte-query@5.69.0", "", { "dependencies": { "@tanstack/query-core": "5.69.0" }, "peerDependencies": { "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" } }, "sha512-4zn8uW76PQCIvFHS/JbvxtdjlglF1BaH9wZiiEWRp5lnlcwr+4NHYlvzXI6z7Ff4ZuJvPjVBNhrwFYfJjL+gOQ=="],
|
||||
|
||||
"@tsconfig/svelte": ["@tsconfig/svelte@5.0.4", "", {}, "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.11", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.8.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
@@ -182,14 +212,34 @@
|
||||
|
||||
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
|
||||
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="],
|
||||
@@ -218,6 +268,12 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
@@ -228,6 +284,8 @@
|
||||
|
||||
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
||||
@@ -246,6 +304,8 @@
|
||||
|
||||
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"vite": ["vite@6.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ=="],
|
||||
|
||||
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/node": "^22.13.11",
|
||||
"svelte": "^5.20.2",
|
||||
"svelte-check": "^4.1.4",
|
||||
"typescript": "~5.7.2",
|
||||
@@ -19,6 +20,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.14",
|
||||
"@tanstack/svelte-query": "^5.69.0",
|
||||
"axios": "^1.8.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"tailwindcss": "^4.0.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { QueryClientProvider } from "@tanstack/svelte-query";
|
||||
import "./app.css";
|
||||
import Home from "./pages/Home/Home.svelte";
|
||||
import Login from "./pages/Login/Login.svelte";
|
||||
import { queryClient } from "@/api";
|
||||
const cookies = document.cookie;
|
||||
</script>
|
||||
|
||||
<main></main>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<main class="p-4">
|
||||
{#if cookies.includes("access_token")}
|
||||
<Home />
|
||||
{:else}
|
||||
<Login />
|
||||
{/if}
|
||||
</main>
|
||||
</QueryClientProvider>
|
||||
|
||||
3
web/src/api/client.ts
Normal file
3
web/src/api/client.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { QueryClient } from "@tanstack/svelte-query";
|
||||
|
||||
export const queryClient = new QueryClient();
|
||||
2
web/src/api/index.ts
Normal file
2
web/src/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./client";
|
||||
export * from "./url";
|
||||
55
web/src/api/url.ts
Normal file
55
web/src/api/url.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { createMutation, createQuery } from "@tanstack/svelte-query";
|
||||
import axios from "axios";
|
||||
import { queryClient } from "./client";
|
||||
|
||||
export type UrlUpsertDto = {
|
||||
url: string;
|
||||
destinationUrl: string;
|
||||
ttl: Date;
|
||||
};
|
||||
export type Url = {
|
||||
url: string;
|
||||
destinationUrl: string;
|
||||
ttl: Date;
|
||||
ownedBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export function writeUrl({ url, ...rest }: UrlUpsertDto) {
|
||||
return axios.patch(`/api/url/${url}`, rest, { withCredentials: true });
|
||||
}
|
||||
|
||||
export function listUrls() {
|
||||
return axios.get<Url[]>("/api/url/list", { withCredentials: true });
|
||||
}
|
||||
|
||||
export function deleteUrl(url: string) {
|
||||
return axios.delete(`/api/url/${url}`, { withCredentials: true });
|
||||
}
|
||||
|
||||
export const queryUrls = () =>
|
||||
createQuery({
|
||||
queryKey: ["urls"],
|
||||
queryFn: () => listUrls(),
|
||||
});
|
||||
|
||||
export const mutateUrl = () =>
|
||||
createMutation({
|
||||
mutationFn: (data: UrlUpsertDto) => writeUrl(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["urls"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const mutateDeleteUrl = () =>
|
||||
createMutation({
|
||||
mutationFn: (data: string) => deleteUrl(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["urls"],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
html {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
41
web/src/common/Icon.svelte
Normal file
41
web/src/common/Icon.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
export let name: keyof typeof icons;
|
||||
export let width = "1rem";
|
||||
export let height = "1rem";
|
||||
export let focusable: string | number | null | undefined = undefined;
|
||||
let icons = {
|
||||
logout: {
|
||||
box: 32,
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15" />
|
||||
</svg>`,
|
||||
},
|
||||
add: {
|
||||
box: 32,
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>`,
|
||||
},
|
||||
edit: {
|
||||
box: 32,
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
|
||||
</svg>`,
|
||||
},
|
||||
delete: {
|
||||
box: 32,
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>`,
|
||||
},
|
||||
} as const;
|
||||
let displayIcon = icons[name];
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={$$props.class}
|
||||
{focusable}
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 {displayIcon.box} {displayIcon.box}">{@html displayIcon.svg}</svg
|
||||
>
|
||||
0
web/src/index.ts
Normal file
0
web/src/index.ts
Normal file
36
web/src/pages/Home/Home.svelte
Normal file
36
web/src/pages/Home/Home.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import Create from "./components/create.svelte";
|
||||
import Navbar from "./components/navbar.svelte";
|
||||
import { queryUrls } from "@/api/url";
|
||||
import Row from "./components/row.svelte";
|
||||
const urls = queryUrls();
|
||||
</script>
|
||||
|
||||
<Navbar />
|
||||
|
||||
<div class="h-screen flex items-center justify-center flex-col">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Shortform</th>
|
||||
<th>Destination</th>
|
||||
<th>Time to live</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{#if $urls.isLoading}
|
||||
<tr>
|
||||
<td><p>Loading...</p></td>
|
||||
</tr>
|
||||
{:else if $urls.isError}
|
||||
<tr>
|
||||
<td><p>Error: {$urls.error.message}</p></td>
|
||||
</tr>
|
||||
{:else if $urls.isSuccess}
|
||||
{#each $urls.data.data as row}
|
||||
<Row {row} />
|
||||
{/each}
|
||||
{/if}
|
||||
</table>
|
||||
<Create />
|
||||
</div>
|
||||
24
web/src/pages/Home/components/create.svelte
Normal file
24
web/src/pages/Home/components/create.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { mutateUrl, type UrlUpsertDto } from "@/api/url";
|
||||
import Icon from "@/common/Icon.svelte";
|
||||
|
||||
const mutation = mutateUrl();
|
||||
const handleSubmit = (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const data = new FormData(e.target as HTMLFormElement);
|
||||
|
||||
const formObject = Object.fromEntries(
|
||||
data.entries(),
|
||||
) as unknown as UrlUpsertDto;
|
||||
|
||||
console.log("Form Data:", formObject);
|
||||
$mutation.mutate(formObject);
|
||||
};
|
||||
</script>
|
||||
|
||||
<form method="POST" on:submit={handleSubmit}>
|
||||
<input name="url" type="text" class="border-b-1" />
|
||||
<input name="destinationUrl" type="url" class="border-b-1" />
|
||||
<button type="submit"><Icon name="add" /></button>
|
||||
</form>
|
||||
27
web/src/pages/Home/components/jwt.ts
Normal file
27
web/src/pages/Home/components/jwt.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { jwtDecode, type JwtPayload } from "jwt-decode";
|
||||
|
||||
interface JwtExtendedPayload extends JwtPayload {
|
||||
name: string;
|
||||
}
|
||||
|
||||
function parseCookies(): Record<string, string> {
|
||||
if (!document.cookie) return {};
|
||||
|
||||
return document.cookie.split("; ").reduce(
|
||||
(prev, current) => {
|
||||
const [name, ...value] = current.split("=");
|
||||
prev[name] = value.join("=");
|
||||
return prev;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
export function decodeJwt(): JwtExtendedPayload | null {
|
||||
const cookies = parseCookies();
|
||||
|
||||
if (Object.keys(cookies).length === 0 || !("access_token" in cookies)) {
|
||||
return null;
|
||||
}
|
||||
return jwtDecode<JwtExtendedPayload>(cookies.access_token);
|
||||
}
|
||||
13
web/src/pages/Home/components/navbar.svelte
Normal file
13
web/src/pages/Home/components/navbar.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@common/Icon.svelte";
|
||||
import { decodeJwt } from "./jwt";
|
||||
|
||||
const payload = decodeJwt();
|
||||
</script>
|
||||
|
||||
<div class="flex flexbox justify-between">
|
||||
<p>{payload?.name}</p>
|
||||
<a href="/logout">
|
||||
<Icon name="logout" class="h-5 w-5" />
|
||||
</a>
|
||||
</div>
|
||||
26
web/src/pages/Home/components/row.svelte
Normal file
26
web/src/pages/Home/components/row.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { mutateDeleteUrl, type Url } from "@/api/url";
|
||||
import Icon from "@/common/icon.svelte";
|
||||
|
||||
let { row }: { row: Url } = $props();
|
||||
const mutation = mutateDeleteUrl();
|
||||
function verifyDelete() {
|
||||
if (confirm("Are you sure you want to delete this redirect?")) {
|
||||
$mutation.mutate(row.url);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr class="border-b-1">
|
||||
<td><a href={"/" + row.url} aria-label="Redirect url">{row.url}</a></td>
|
||||
<td
|
||||
><a href={row.destinationUrl} aria-label="Destination url"
|
||||
>{row.destinationUrl}</a
|
||||
></td
|
||||
>
|
||||
<td></td>
|
||||
<td class="flex flex-row justify-around"
|
||||
><Icon name="edit" />
|
||||
<button onclick={verifyDelete}><Icon name="delete" /></button>
|
||||
</td>
|
||||
</tr>
|
||||
38
web/src/pages/Login/Login.css
Normal file
38
web/src/pages/Login/Login.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.github-signin-container {
|
||||
background-color: #1b1f23;
|
||||
transition: background-color 0.3s ease;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.github-signin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #1b1f23;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.github-signin-btn:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.github-signin-btn .github-icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.github-signin-btn span {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
16
web/src/pages/Login/Login.svelte
Normal file
16
web/src/pages/Login/Login.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import "./Login.css";
|
||||
</script>
|
||||
|
||||
<div class="h-screen flex items-center justify-center">
|
||||
<div class="github-signin-container">
|
||||
<a class="github-signin-btn" href="/login/github">
|
||||
<img
|
||||
src="https://cdn.pixabay.com/photo/2022/01/30/13/33/github-6980894_1280.png"
|
||||
alt="GitHub Icon"
|
||||
class="github-icon"
|
||||
/>
|
||||
Sign in with GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +14,11 @@
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force"
|
||||
"moduleDetection": "force",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@common/*": ["src/common/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@common/*": ["src/common/*"]
|
||||
}
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@common": path.resolve(__dirname, "./src/common"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user