diff --git a/Cargo.lock b/Cargo.lock index c7345c5..17abb9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2607,6 +2607,7 @@ dependencies = [ "rocket", "rocket_oauth2", "serde", + "serde_json", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index 7758bb5..fe0d19d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/auth/jwts.rs b/src/auth/jwts.rs index 43efb99..b25b3c6 100644 --- a/src/auth/jwts.rs +++ b/src/auth/jwts.rs @@ -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 Result, jsonwebtoken::errors::Error> { - decode::(token, &get_decoding_key(), &Validation::default()) + decode::(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, diff --git a/src/db/models.rs b/src/db/models.rs index 98ca0b2..b76683e 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -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 { diff --git a/src/db/url.rs b/src/db/url.rs index 0d6e118..7057dc1 100644 --- a/src/db/url.rs +++ b/src/db/url.rs @@ -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 { let connection = &mut establish_connection(); @@ -11,7 +11,7 @@ pub fn get_entry(path: &str) -> Result { } pub fn upsert_entry( - username: &str, + owned_by_id: &str, path: &str, dest: &str, time_to_live: Option, @@ -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 { +pub fn delete_entry(owned_by_id: &str, path: &str) -> Result { 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 { + let connection = &mut establish_connection(); + urls.filter(owned_by.eq(owned_by_id)) + .select(Urls::as_select()) + .load(connection) + .expect("Error loading urls") } diff --git a/src/dto/mod.rs b/src/dto/mod.rs index 3f2c34c..1c1381d 100644 --- a/src/dto/mod.rs +++ b/src/dto/mod.rs @@ -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, } diff --git a/src/handlers/url.rs b/src/handlers/url.rs index 7762b90..66309a5 100644 --- a/src/handlers/url.rs +++ b/src/handlers/url.rs @@ -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 { 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() +} diff --git a/src/main.rs b/src/main.rs index d1bac28..5b4d031 100644 --- a/src/main.rs +++ b/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 { handle_redirect(url) } -#[post("/write", format = "json", data = "")] -fn upsert(user: User, upsert: Json>) -> (Status, &'static str) { - handle_upsert(&user.id.as_str(), upsert.0) +#[patch("/", format = "json", data = "")] +fn upsert(user: User, url: &str, upsert: Json>) -> (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("/")] diff --git a/web/bun.lock b/web/bun.lock index a0f21ba..4b497ba 100644 --- a/web/bun.lock +++ b/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=="], diff --git a/web/package.json b/web/package.json index 8b2f177..f6839b0 100644 --- a/web/package.json +++ b/web/package.json @@ -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" } } diff --git a/web/src/App.svelte b/web/src/App.svelte index 56ae2f7..bcac3e9 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -1,5 +1,18 @@ -
+ +
+ {#if cookies.includes("access_token")} + + {:else} + + {/if} +
+
diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 0000000..58ffa6f --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,3 @@ +import { QueryClient } from "@tanstack/svelte-query"; + +export const queryClient = new QueryClient(); diff --git a/web/src/api/index.ts b/web/src/api/index.ts new file mode 100644 index 0000000..ca93960 --- /dev/null +++ b/web/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./client"; +export * from "./url"; diff --git a/web/src/api/url.ts b/web/src/api/url.ts new file mode 100644 index 0000000..3e1b3e9 --- /dev/null +++ b/web/src/api/url.ts @@ -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("/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"], + }); + }, + }); diff --git a/web/src/app.css b/web/src/app.css index f1d8c73..ac454f4 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1 +1,6 @@ @import "tailwindcss"; + +html { + background-color: black; + color: white; +} diff --git a/web/src/common/Icon.svelte b/web/src/common/Icon.svelte new file mode 100644 index 0000000..2b9c004 --- /dev/null +++ b/web/src/common/Icon.svelte @@ -0,0 +1,41 @@ + + +{@html displayIcon.svg} diff --git a/web/src/index.ts b/web/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/web/src/pages/Home/Home.svelte b/web/src/pages/Home/Home.svelte new file mode 100644 index 0000000..0c60475 --- /dev/null +++ b/web/src/pages/Home/Home.svelte @@ -0,0 +1,36 @@ + + + + +
+ + + + + + + + + + {#if $urls.isLoading} + + + + {:else if $urls.isError} + + + + {:else if $urls.isSuccess} + {#each $urls.data.data as row} + + {/each} + {/if} +
ShortformDestinationTime to liveAction

Loading...

Error: {$urls.error.message}

+ +
diff --git a/web/src/pages/Home/components/create.svelte b/web/src/pages/Home/components/create.svelte new file mode 100644 index 0000000..cfd5642 --- /dev/null +++ b/web/src/pages/Home/components/create.svelte @@ -0,0 +1,24 @@ + + +
+ + + +
diff --git a/web/src/pages/Home/components/jwt.ts b/web/src/pages/Home/components/jwt.ts new file mode 100644 index 0000000..c882eca --- /dev/null +++ b/web/src/pages/Home/components/jwt.ts @@ -0,0 +1,27 @@ +import { jwtDecode, type JwtPayload } from "jwt-decode"; + +interface JwtExtendedPayload extends JwtPayload { + name: string; +} + +function parseCookies(): Record { + if (!document.cookie) return {}; + + return document.cookie.split("; ").reduce( + (prev, current) => { + const [name, ...value] = current.split("="); + prev[name] = value.join("="); + return prev; + }, + {} as Record, + ); +} + +export function decodeJwt(): JwtExtendedPayload | null { + const cookies = parseCookies(); + + if (Object.keys(cookies).length === 0 || !("access_token" in cookies)) { + return null; + } + return jwtDecode(cookies.access_token); +} diff --git a/web/src/pages/Home/components/navbar.svelte b/web/src/pages/Home/components/navbar.svelte new file mode 100644 index 0000000..7dfa381 --- /dev/null +++ b/web/src/pages/Home/components/navbar.svelte @@ -0,0 +1,13 @@ + + +
+

{payload?.name}

+ + + +
diff --git a/web/src/pages/Home/components/row.svelte b/web/src/pages/Home/components/row.svelte new file mode 100644 index 0000000..f22f3fb --- /dev/null +++ b/web/src/pages/Home/components/row.svelte @@ -0,0 +1,26 @@ + + + + {row.url} + {row.destinationUrl} + + + + + diff --git a/web/src/pages/Login/Login.css b/web/src/pages/Login/Login.css new file mode 100644 index 0000000..e0f2b91 --- /dev/null +++ b/web/src/pages/Login/Login.css @@ -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; +} diff --git a/web/src/pages/Login/Login.svelte b/web/src/pages/Login/Login.svelte new file mode 100644 index 0000000..9632a7f --- /dev/null +++ b/web/src/pages/Login/Login.svelte @@ -0,0 +1,16 @@ + + + diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json index 55a2f9b..8549988 100644 --- a/web/tsconfig.app.json +++ b/web/tsconfig.app.json @@ -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"] } diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json index db0becc..4852ff7 100644 --- a/web/tsconfig.node.json +++ b/web/tsconfig.node.json @@ -18,7 +18,11 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["src/*"], + "@common/*": ["src/common/*"] + } }, "include": ["vite.config.ts"] } diff --git a/web/vite.config.ts b/web/vite.config.ts index 40be189..68a2608 100644 --- a/web/vite.config.ts +++ b/web/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"), + }, + }, });