magic-link and verify-magic-link working

This commit is contained in:
c0repwn3r 2023-02-05 21:00:25 -05:00
parent e3c8819c08
commit 459dfb34ef
Signed by: core
GPG Key ID: FDBF740DADDCEECF
10 changed files with 105 additions and 13 deletions

View File

@ -2,3 +2,4 @@ listen_port = 8000
db_url = "postgres://postgres@localhost/trifidapi" db_url = "postgres://postgres@localhost/trifidapi"
base = "http://localhost:8000" base = "http://localhost:8000"
magic_links_valid_for = 86400 magic_links_valid_for = 86400
session_tokens_valid_for = 86400

View File

@ -0,0 +1,5 @@
CREATE TABLE session_tokens (
id VARCHAR(36) NOT NULL PRIMARY KEY,
user_id SERIAL NOT NULL REFERENCES users(id),
expires_on INTEGER NOT NULL
);

View File

@ -6,5 +6,6 @@ pub struct TFConfig {
pub listen_port: u16, pub listen_port: u16,
pub db_url: String, pub db_url: String,
pub base: Url, pub base: Url,
pub magic_links_valid_for: i64 pub magic_links_valid_for: i64,
pub session_tokens_valid_for: i64
} }

View File

@ -12,7 +12,7 @@ pub mod format;
pub mod util; pub mod util;
pub mod db; pub mod db;
pub mod config; pub mod config;
pub mod email; pub mod tokens;
pub mod routes; pub mod routes;
static MIGRATOR: Migrator = sqlx::migrate!(); static MIGRATOR: Migrator = sqlx::migrate!();
@ -78,8 +78,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
let _ = rocket::custom(figment) let _ = rocket::custom(figment)
.mount("/", routes![ .mount("/", routes![
//crate::routes::v1::auth::verify_magic_link::verify_magic_link crate::routes::v1::auth::magic_link::magiclink_request,
crate::routes::v1::signup::signup_request crate::routes::v1::signup::signup_request,
crate::routes::v1::auth::verify_magic_link::verify_magic_link
]) ])
.register("/", catchers![ .register("/", catchers![
crate::routes::handler_400, crate::routes::handler_400,

View File

@ -0,0 +1,55 @@
use rocket::{post, State};
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use rocket::http::{ContentType, Status};
use sqlx::PgPool;
use crate::config::TFConfig;
use crate::tokens::send_magic_link;
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkRequest {
pub email: String,
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkResponseMetadata {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkResponse {
pub data: Option<String>,
pub metadata: MagicLinkResponseMetadata,
}
#[post("/v1/auth/magic-link", data = "<req>")]
pub async fn magiclink_request(req: Json<MagicLinkRequest>, pool: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<MagicLinkResponse>), (Status, String)> {
// figure out if the user already exists
let mut id = -1;
match sqlx::query!("SELECT id FROM users WHERE email = $1", req.email.clone()).fetch_optional(pool.inner()).await {
Ok(res) => if let Some(r) = res { id = r.id as i64 },
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
}
}
if id == -1 {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_UNAUTHORIZED", "authorization was provided but it is expired or invalid")))
}
// send magic link to email
match send_magic_link(id, req.email.clone(), pool.inner(), config.inner()).await {
Ok(_) => (),
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
}
};
// this endpoint doesn't actually ever return an error? it will send you the magic link no matter what
// this appears to do the exact same thing as /v1/auth/magic-link, but it doesn't check if you have an account (magic-link does)
Ok((ContentType::JSON, Json(MagicLinkResponse {
data: None,
metadata: MagicLinkResponseMetadata {},
})))
}

View File

@ -1 +1,2 @@
pub mod verify_magic_link; pub mod verify_magic_link;
pub mod magic_link;

View File

@ -1,8 +1,11 @@
use std::time::{SystemTime, UNIX_EPOCH};
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::routes::{APIError, APIErrorSingular, ERR_MSG_MALFORMED_REQUEST, ERR_MSG_MALFORMED_REQUEST_CODE}; use rocket::{post, State};
use rocket::post; use sqlx::PgPool;
use crate::config::TFConfig;
use crate::tokens::generate_session_token;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
@ -28,11 +31,30 @@ pub struct VerifyMagicLinkResponse {
} }
#[post("/v1/auth/verify-magic-link", data = "<req>")] #[post("/v1/auth/verify-magic-link", data = "<req>")]
pub fn verify_magic_link(req: Json<VerifyMagicLinkRequest>) -> Result<(ContentType, Json<VerifyMagicLinkResponse>), (Status, Json<APIError>)> { pub async fn verify_magic_link(req: Json<VerifyMagicLinkRequest>, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<VerifyMagicLinkResponse>), (Status, String)> {
// handle request // get the current time to check if the token is expired
let (user_id, expired_at) = match sqlx::query!("SELECT user_id, expires_on FROM magic_links WHERE id = $1", req.0.magic_link_token).fetch_one(db.inner()).await {
Ok(row) => (row.user_id, row.expires_on),
Err(e) => {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNAUTHORIZED", "this token is invalid", e)))
}
};
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32;
println!("expired on {}, currently {}", expired_at, current_time);
if expired_at < current_time {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_UNAUTHORIZED", "valid authorization was provided but it is expired")))
}
// generate session token
let token = match generate_session_token(user_id as i64, db.inner(), config.inner()).await {
Ok(t) => t,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
}
};
Ok((ContentType::JSON, Json(VerifyMagicLinkResponse { Ok((ContentType::JSON, Json(VerifyMagicLinkResponse {
data: VerifyMagicLinkResponseData { session_token: "sd[if0sf0dsfsdf".to_string() }, data: VerifyMagicLinkResponseData { session_token: token },
metadata: VerifyMagicLinkResponseMetadata {}, metadata: VerifyMagicLinkResponseMetadata {},
}))) })))
} }

View File

@ -1,2 +1,2 @@
//pub mod auth; pub mod auth;
pub mod signup; pub mod signup;

View File

@ -5,7 +5,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use sqlx::PgPool; use sqlx::PgPool;
use crate::config::TFConfig; use crate::config::TFConfig;
use crate::email::send_magic_link; use crate::tokens::send_magic_link;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]

View File

@ -15,3 +15,9 @@ pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFCon
info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for); info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for);
Ok(()) Ok(())
} }
pub async fn generate_session_token(id: i64, db: &PgPool, config: &TFConfig) -> Result<String, Box<dyn Error>> {
let token = Uuid::new_v4().to_string();
sqlx::query!("INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?;
Ok(token)
}