mirror of
https://github.com/eliasrenman/url-shortener.git
synced 2026-03-16 20:16:06 +01:00
feat: github & google login
This commit is contained in:
@@ -1 +1,3 @@
|
|||||||
DATABASE_URL=./sqlite.db
|
DATABASE_URL=./sqlite.db
|
||||||
|
BASE_URL=https://url.renman.dev
|
||||||
|
JWT_SECRET=secret
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
*.db
|
*.db
|
||||||
.env
|
.env
|
||||||
|
Rocket.toml
|
||||||
|
|||||||
1332
Cargo.lock
generated
1332
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -5,8 +5,17 @@ edition = "2024"
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.40", features = ["serde"] }
|
anyhow = "1.0.97"
|
||||||
|
chrono = { version = "0.4.40", features = ["serde", "now"] }
|
||||||
diesel = { version = "2.2.8", features = ["sqlite", "chrono"] }
|
diesel = { version = "2.2.8", features = ["sqlite", "chrono"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
jsonwebtoken = "9.3.1"
|
||||||
|
md5 = "0.7.0"
|
||||||
|
reqwest = { version = "0.12.14", default-features = false, features = [
|
||||||
|
"json",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
|
rocket_oauth2 = "0.5.0"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
uuid = { version = "1.16.0", features = ["v3", "v5"] }
|
||||||
|
|||||||
14
Rocket.toml.example
Normal file
14
Rocket.toml.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[default.oauth.github]
|
||||||
|
provider = "GitHub"
|
||||||
|
client_id = "<client_id>"
|
||||||
|
client_secret = "<client_secret>"
|
||||||
|
|
||||||
|
# redirect_uri is optional for this library. If left out, it must still be set
|
||||||
|
# correctly on the provider's configuration page for the OAuth2 application.
|
||||||
|
#redirect_uri = "http://localhost:8000/auth/github"
|
||||||
|
|
||||||
|
[default.oauth.google]
|
||||||
|
provider = "Google"
|
||||||
|
client_id = "<client_id>"
|
||||||
|
client_secret = "<client_secret>"
|
||||||
|
redirect_uri = "http://localhost:8000/auth/google"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
-- This file should undo anything in `up.sql`
|
||||||
DROP TRIGGER urls_update_at_trigger;
|
DROP TRIGGER IF EXISTS urls_update_at_trigger;
|
||||||
|
|
||||||
DROP TABLE urls;
|
DROP TABLE urls;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ CREATE TABLE urls (
|
|||||||
url TEXT PRIMARY KEY NOT NULL,
|
url TEXT PRIMARY KEY NOT NULL,
|
||||||
destination_url TEXT NOT NULL,
|
destination_url TEXT NOT NULL,
|
||||||
ttl DATETIME,
|
ttl DATETIME,
|
||||||
owned_by TEXT,
|
owned_by TEXT NOT NULL CHECK (owned_by = owned_by),
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|||||||
2
migrations/2025-03-17-125909_magic_email_link/down.sql
Normal file
2
migrations/2025-03-17-125909_magic_email_link/down.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE magic_links;
|
||||||
7
migrations/2025-03-17-125909_magic_email_link/up.sql
Normal file
7
migrations/2025-03-17-125909_magic_email_link/up.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
CREATE TABLE magiclinks (
|
||||||
|
url TEXT PRIMARY KEY NOT NULL,
|
||||||
|
ttl DATETIME NOT NULL,
|
||||||
|
owned_by TEXT NOT NULL CHECK (owned_by = owned_by),
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
);
|
||||||
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
src/auth/.DS_Store
vendored
Normal file
BIN
src/auth/.DS_Store
vendored
Normal file
Binary file not shown.
48
src/auth/email.rs
Normal file
48
src/auth/email.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use rocket::fairing::{AdHoc, Fairing};
|
||||||
|
use rocket::get;
|
||||||
|
use rocket::http::{CookieJar, Status};
|
||||||
|
use rocket::response::Debug;
|
||||||
|
use rocket::response::Redirect;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::User;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct EmailUserInfo {
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
impl From<EmailUserInfo> for User {
|
||||||
|
fn from(info: EmailUserInfo) -> Self {
|
||||||
|
User {
|
||||||
|
id: info.email.clone(),
|
||||||
|
username: info.email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[get("/login/email")]
|
||||||
|
fn email_login<'a>(cookies: &'a CookieJar<'a>) -> (Status, &'a str) {
|
||||||
|
// Create magic link
|
||||||
|
// Send email containing magic link
|
||||||
|
(Status::Ok, "Email link sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/auth/email")]
|
||||||
|
async fn email_callback(cookies: &CookieJar<'_>) -> Result<Redirect, Debug<Error>> {
|
||||||
|
// Validate magic link
|
||||||
|
// create token
|
||||||
|
// Set a cookie with the user's name, and redirect to the home page.
|
||||||
|
// cookies.add(
|
||||||
|
// Cookie::build(("access_token", create_user_token(user_info.into()).unwrap()))
|
||||||
|
// .same_site(SameSite::Lax)
|
||||||
|
// .build(),
|
||||||
|
// );
|
||||||
|
Ok(Redirect::to("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn email_fairing() -> impl Fairing {
|
||||||
|
AdHoc::on_ignite("Email Auth", |rocket| async {
|
||||||
|
rocket.mount("/api", rocket::routes![email_login, email_callback])
|
||||||
|
})
|
||||||
|
}
|
||||||
72
src/auth/github.rs
Normal file
72
src/auth/github.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use super::jwts::{create_user_token, generate_id};
|
||||||
|
use crate::User;
|
||||||
|
use anyhow::{Context, Error};
|
||||||
|
use reqwest::header::{ACCEPT, AUTHORIZATION, USER_AGENT};
|
||||||
|
use rocket::fairing::{AdHoc, Fairing};
|
||||||
|
use rocket::get;
|
||||||
|
use rocket::http::{Cookie, CookieJar, SameSite};
|
||||||
|
use rocket::response::Debug;
|
||||||
|
use rocket::response::Redirect;
|
||||||
|
use rocket_oauth2::{OAuth2, TokenResponse};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// User information to be retrieved from the GitHub API.
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct GitHubUserInfo {
|
||||||
|
name: String,
|
||||||
|
id: i64,
|
||||||
|
}
|
||||||
|
impl From<GitHubUserInfo> for User {
|
||||||
|
fn from(info: GitHubUserInfo) -> Self {
|
||||||
|
User {
|
||||||
|
username: info.name,
|
||||||
|
id: generate_id("github", format!("{}", info.id).as_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// NB: Here we are using the same struct as a type parameter to OAuth2 and
|
||||||
|
// TokenResponse as we use for the user's GitHub login details. For
|
||||||
|
// `TokenResponse` and `OAuth2` the actual type does not matter; only that they
|
||||||
|
// are matched up.
|
||||||
|
#[get("/login/github")]
|
||||||
|
fn github_login(oauth2: OAuth2<GitHubUserInfo>, cookies: &CookieJar<'_>) -> Redirect {
|
||||||
|
oauth2
|
||||||
|
.get_redirect(cookies, &["user:read", "user:email"])
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/auth/github")]
|
||||||
|
async fn github_callback(
|
||||||
|
token: TokenResponse<GitHubUserInfo>,
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
) -> Result<Redirect, Debug<Error>> {
|
||||||
|
// Use the token to retrieve the user's GitHub account information.
|
||||||
|
let user: GitHubUserInfo = reqwest::Client::builder()
|
||||||
|
.build()
|
||||||
|
.context("failed to build reqwest client")?
|
||||||
|
.get("https://api.github.com/user")
|
||||||
|
.header(AUTHORIZATION, format!("token {}", token.access_token()))
|
||||||
|
.header(ACCEPT, "application/vnd.github.v3+json")
|
||||||
|
.header(USER_AGENT, "rocket_oauth2 demo application")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to complete request")?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("failed to deserialize response")?;
|
||||||
|
|
||||||
|
// Set a cookie with the user's name, and redirect to the home page.
|
||||||
|
cookies.add(
|
||||||
|
Cookie::build(("access_token", create_user_token(user.into()).unwrap()))
|
||||||
|
.same_site(SameSite::Lax)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
Ok(Redirect::to("/"))
|
||||||
|
}
|
||||||
|
pub fn github_fairing() -> impl Fairing {
|
||||||
|
AdHoc::on_ignite("Github OAuth2", |rocket| async {
|
||||||
|
rocket
|
||||||
|
.mount("/", rocket::routes![github_login, github_callback])
|
||||||
|
.attach(OAuth2::<GitHubUserInfo>::fairing("github"))
|
||||||
|
})
|
||||||
|
}
|
||||||
80
src/auth/google.rs
Normal file
80
src/auth/google.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use anyhow::{Context, Error};
|
||||||
|
use reqwest::header::AUTHORIZATION;
|
||||||
|
use rocket::fairing::{AdHoc, Fairing};
|
||||||
|
use rocket::get;
|
||||||
|
use rocket::http::{Cookie, CookieJar, SameSite};
|
||||||
|
use rocket::response::{Debug, Redirect};
|
||||||
|
use rocket::serde::json::Value;
|
||||||
|
use rocket_oauth2::{OAuth2, TokenResponse};
|
||||||
|
|
||||||
|
use crate::User;
|
||||||
|
|
||||||
|
use super::jwts::{create_user_token, generate_id};
|
||||||
|
|
||||||
|
/// User information to be retrieved from the Google People API.
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
struct GoogleUserInfo {
|
||||||
|
names: Vec<Value>,
|
||||||
|
resource_name: String,
|
||||||
|
}
|
||||||
|
impl From<GoogleUserInfo> for User {
|
||||||
|
fn from(info: GoogleUserInfo) -> Self {
|
||||||
|
User {
|
||||||
|
username: info
|
||||||
|
.names
|
||||||
|
.iter()
|
||||||
|
.find_map(|name| {
|
||||||
|
let metadata = name.get("metadata")?;
|
||||||
|
let primary = metadata.get("primary")?.as_bool()?;
|
||||||
|
|
||||||
|
if primary {
|
||||||
|
name.get("displayName")?.as_str().map(|s| s.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
id: generate_id("github", format!("{}", info.resource_name).as_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[get("/login/google")]
|
||||||
|
fn google_login(oauth2: OAuth2<GoogleUserInfo>, cookies: &CookieJar<'_>) -> Redirect {
|
||||||
|
oauth2.get_redirect(cookies, &["profile"]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/auth/google")]
|
||||||
|
async fn google_callback(
|
||||||
|
token: TokenResponse<GoogleUserInfo>,
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
) -> Result<Redirect, Debug<Error>> {
|
||||||
|
// Use the token to retrieve the user's Google account information.
|
||||||
|
let user_info: GoogleUserInfo = reqwest::Client::builder()
|
||||||
|
.build()
|
||||||
|
.context("failed to build reqwest client")?
|
||||||
|
.get("https://people.googleapis.com/v1/people/me?personFields=names")
|
||||||
|
.header(AUTHORIZATION, format!("Bearer {}", token.access_token()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to complete request")?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("failed to deserialize response")?;
|
||||||
|
|
||||||
|
// Set a cookie with the user's name, and redirect to the home page.
|
||||||
|
cookies.add(
|
||||||
|
Cookie::build(("access_token", create_user_token(user_info.into()).unwrap()))
|
||||||
|
.same_site(SameSite::Lax)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Redirect::to("/"))
|
||||||
|
}
|
||||||
|
pub fn google_fairing() -> impl Fairing {
|
||||||
|
AdHoc::on_ignite("Google OAuth2", |rocket| async {
|
||||||
|
rocket
|
||||||
|
.mount("/", rocket::routes![google_login, google_callback])
|
||||||
|
.attach(OAuth2::<GoogleUserInfo>::fairing("google"))
|
||||||
|
})
|
||||||
|
}
|
||||||
56
src/auth/jwts.rs
Normal file
56
src/auth/jwts.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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 crate::User;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Claims {
|
||||||
|
aud: String,
|
||||||
|
exp: usize,
|
||||||
|
iat: usize,
|
||||||
|
iss: String,
|
||||||
|
nbf: usize,
|
||||||
|
pub sub: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jwts_encode(my_claims: Claims) -> Result<std::string::String, jsonwebtoken::errors::Error> {
|
||||||
|
encode(&Header::default(), &my_claims, &get_encoding_key())
|
||||||
|
}
|
||||||
|
pub fn create_user_token(user: User) -> Result<std::string::String, jsonwebtoken::errors::Error> {
|
||||||
|
let week_in_seconds = 3600 * 24 * 7;
|
||||||
|
let address = env::var("BASE_URL").unwrap();
|
||||||
|
jwts_encode(Claims {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jwts_decode(token: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
|
||||||
|
decode::<Claims>(token, &get_decoding_key(), &Validation::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_id(namespace: &str, id: &str) -> String {
|
||||||
|
let digest = md5::compute(format!("{}-{}", namespace, id));
|
||||||
|
let md5_bytes = digest.0;
|
||||||
|
Builder::from_md5_bytes(md5_bytes).into_uuid().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_encoding_key() -> EncodingKey {
|
||||||
|
EncodingKey::from_secret(env::var("JWT_SECRET").unwrap().as_ref())
|
||||||
|
}
|
||||||
|
fn get_decoding_key() -> DecodingKey {
|
||||||
|
DecodingKey::from_secret(env::var("JWT_SECRET").unwrap().as_ref())
|
||||||
|
}
|
||||||
4
src/auth/mod.rs
Normal file
4
src/auth/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod email;
|
||||||
|
pub mod github;
|
||||||
|
pub mod google;
|
||||||
|
pub mod jwts;
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
use diesel::{Connection, sqlite::SqliteConnection};
|
use diesel::{Connection, sqlite::SqliteConnection};
|
||||||
|
|
||||||
use dotenvy::dotenv;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
pub mod url;
|
pub mod url;
|
||||||
|
|
||||||
pub fn establish_connection() -> SqliteConnection {
|
pub fn establish_connection() -> SqliteConnection {
|
||||||
dotenv().ok();
|
|
||||||
|
|
||||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
SqliteConnection::establish(&database_url)
|
SqliteConnection::establish(&database_url)
|
||||||
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub struct Urls {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
pub destination_url: String,
|
pub destination_url: String,
|
||||||
pub ttl: Option<NaiveDateTime>,
|
pub ttl: Option<NaiveDateTime>,
|
||||||
|
pub owned_by: String,
|
||||||
pub created_at: Option<NaiveDateTime>,
|
pub created_at: Option<NaiveDateTime>,
|
||||||
pub updated_at: Option<NaiveDateTime>,
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ diesel::table! {
|
|||||||
url -> Text,
|
url -> Text,
|
||||||
destination_url -> Text,
|
destination_url -> Text,
|
||||||
ttl -> Nullable<Timestamp>,
|
ttl -> Nullable<Timestamp>,
|
||||||
owned_by -> Nullable<Text>,
|
owned_by -> Text,
|
||||||
created_at -> Nullable<Timestamp>,
|
created_at -> Nullable<Timestamp>,
|
||||||
updated_at -> Nullable<Timestamp>,
|
updated_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,26 @@ pub fn get_entry(path: &str) -> Result<Urls, Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_entry(
|
pub fn upsert_entry(
|
||||||
|
username: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
dest: &str,
|
dest: &str,
|
||||||
time_to_live: Option<NaiveDateTime>,
|
time_to_live: Option<NaiveDateTime>,
|
||||||
) -> Result<usize, Error> {
|
) -> Result<usize, Error> {
|
||||||
let connection = &mut establish_connection();
|
let connection = &mut establish_connection();
|
||||||
insert_into(table)
|
insert_into(table)
|
||||||
.values((url.eq(path), destination_url.eq(dest), ttl.eq(time_to_live)))
|
.values((
|
||||||
|
url.eq(path),
|
||||||
|
destination_url.eq(dest),
|
||||||
|
ttl.eq(time_to_live),
|
||||||
|
owned_by.eq(username),
|
||||||
|
))
|
||||||
.on_conflict(url)
|
.on_conflict(url)
|
||||||
.do_update()
|
.do_update()
|
||||||
.set((destination_url.eq(dest), ttl.eq(time_to_live)))
|
.set((
|
||||||
|
destination_url.eq(dest),
|
||||||
|
ttl.eq(time_to_live),
|
||||||
|
owned_by.eq(username),
|
||||||
|
))
|
||||||
.execute(connection)
|
.execute(connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ pub fn handle_redirect(url: &str) -> Result<Redirect, (Status, &'static str)> {
|
|||||||
Ok(Redirect::to(row.destination_url))
|
Ok(Redirect::to(row.destination_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_upsert(dto: UpsertUrlDto<'_>) -> (Status, &'static str) {
|
pub fn handle_upsert(username: &str, dto: UpsertUrlDto<'_>) -> (Status, &'static str) {
|
||||||
let row = upsert_entry(dto.url, dto.destination_url, dto.ttl);
|
let row = upsert_entry(username, dto.url, dto.destination_url, dto.ttl);
|
||||||
if row.is_err() {
|
if row.is_err() {
|
||||||
return (Status::BadRequest, "Failed to upsert redirect");
|
return (Status::BadRequest, "Failed to upsert redirect");
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/main.rs
51
src/main.rs
@@ -1,31 +1,74 @@
|
|||||||
|
mod auth;
|
||||||
mod db;
|
mod db;
|
||||||
mod dto;
|
mod dto;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
|
|
||||||
|
use auth::github::github_fairing;
|
||||||
|
use auth::jwts::jwts_decode;
|
||||||
use db::establish_connection;
|
use db::establish_connection;
|
||||||
|
use dotenvy::dotenv;
|
||||||
use handlers::url::{handle_delete, handle_redirect, handle_upsert};
|
use handlers::url::{handle_delete, handle_redirect, handle_upsert};
|
||||||
use rocket::fs::{FileServer, relative};
|
use rocket::fs::{FileServer, relative};
|
||||||
|
use rocket::http::{Cookie, CookieJar};
|
||||||
|
use rocket::request;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{http::Status, response::Redirect};
|
use rocket::{http::Status, response::Redirect};
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
pub id: String,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> request::FromRequest<'r> for User {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn from_request(request: &'r request::Request<'_>) -> request::Outcome<User, ()> {
|
||||||
|
let cookies = request
|
||||||
|
.guard::<&CookieJar<'_>>()
|
||||||
|
.await
|
||||||
|
.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 {
|
||||||
|
username: unwrapped.claims.name,
|
||||||
|
id: unwrapped.claims.sub,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request::Outcome::Forward(Status::Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
|
dotenv().ok();
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", routes![shortner,])
|
.mount("/", routes![shortner, logout])
|
||||||
.mount("/api", routes![upsert, delete])
|
.mount("/api", routes![upsert, delete])
|
||||||
.mount("/", FileServer::from(relative!("web/dist")))
|
.mount("/", FileServer::from(relative!("web/dist")))
|
||||||
|
.attach(github_fairing())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/logout")]
|
||||||
|
fn logout(cookies: &CookieJar<'_>) -> Redirect {
|
||||||
|
cookies.remove(Cookie::from("username"));
|
||||||
|
Redirect::to("/")
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/<url>")]
|
#[get("/<url>")]
|
||||||
fn shortner(url: &str) -> Result<Redirect, (Status, &'static str)> {
|
fn shortner(url: &str) -> Result<Redirect, (Status, &'static str)> {
|
||||||
handle_redirect(url)
|
handle_redirect(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/write", format = "json", data = "<upsert>")]
|
#[post("/write", format = "json", data = "<upsert>")]
|
||||||
fn upsert(upsert: Json<dto::UpsertUrlDto<'_>>) -> (Status, &'static str) {
|
fn upsert(user: User, upsert: Json<dto::UpsertUrlDto<'_>>) -> (Status, &'static str) {
|
||||||
handle_upsert(upsert.0)
|
handle_upsert(&user.id.as_str(), upsert.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/<url>")]
|
#[delete("/<url>")]
|
||||||
|
|||||||
Reference in New Issue
Block a user