diff --git a/trifid-api/config.toml b/trifid-api/config.toml index 77a5c77..655b64e 100644 --- a/trifid-api/config.toml +++ b/trifid-api/config.toml @@ -1,4 +1,5 @@ listen_port = 8000 db_url = "postgres://postgres@localhost/trifidapi" base = "http://localhost:8000" -magic_links_valid_for = 86400 \ No newline at end of file +magic_links_valid_for = 86400 +session_tokens_valid_for = 86400 \ No newline at end of file diff --git a/trifid-api/migrations/20230206012409_create_session_tokens.sql b/trifid-api/migrations/20230206012409_create_session_tokens.sql new file mode 100644 index 0000000..fbae208 --- /dev/null +++ b/trifid-api/migrations/20230206012409_create_session_tokens.sql @@ -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 +); \ No newline at end of file diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs index 6d0ccdc..1dd8cd1 100644 --- a/trifid-api/src/config.rs +++ b/trifid-api/src/config.rs @@ -6,5 +6,6 @@ pub struct TFConfig { pub listen_port: u16, pub db_url: String, pub base: Url, - pub magic_links_valid_for: i64 + pub magic_links_valid_for: i64, + pub session_tokens_valid_for: i64 } \ No newline at end of file diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index ec5e74f..c65f9cf 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -12,7 +12,7 @@ pub mod format; pub mod util; pub mod db; pub mod config; -pub mod email; +pub mod tokens; pub mod routes; static MIGRATOR: Migrator = sqlx::migrate!(); @@ -78,8 +78,9 @@ async fn main() -> Result<(), Box> { let _ = rocket::custom(figment) .mount("/", routes![ - //crate::routes::v1::auth::verify_magic_link::verify_magic_link - crate::routes::v1::signup::signup_request + crate::routes::v1::auth::magic_link::magiclink_request, + crate::routes::v1::signup::signup_request, + crate::routes::v1::auth::verify_magic_link::verify_magic_link ]) .register("/", catchers![ crate::routes::handler_400, diff --git a/trifid-api/src/routes/v1/auth/magic_link.rs b/trifid-api/src/routes/v1/auth/magic_link.rs new file mode 100644 index 0000000..aacc56e --- /dev/null +++ b/trifid-api/src/routes/v1/auth/magic_link.rs @@ -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, + pub metadata: MagicLinkResponseMetadata, +} + +#[post("/v1/auth/magic-link", data = "")] +pub async fn magiclink_request(req: Json, pool: &State, config: &State) -> Result<(ContentType, Json), (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 {}, + }))) +} \ No newline at end of file diff --git a/trifid-api/src/routes/v1/auth/mod.rs b/trifid-api/src/routes/v1/auth/mod.rs index 06675be..aa16fd9 100644 --- a/trifid-api/src/routes/v1/auth/mod.rs +++ b/trifid-api/src/routes/v1/auth/mod.rs @@ -1 +1,2 @@ -pub mod verify_magic_link; \ No newline at end of file +pub mod verify_magic_link; +pub mod magic_link; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/auth/verify_magic_link.rs b/trifid-api/src/routes/v1/auth/verify_magic_link.rs index 4f67255..3acab52 100644 --- a/trifid-api/src/routes/v1/auth/verify_magic_link.rs +++ b/trifid-api/src/routes/v1/auth/verify_magic_link.rs @@ -1,8 +1,11 @@ +use std::time::{SystemTime, UNIX_EPOCH}; use rocket::http::{ContentType, Status}; use rocket::serde::json::Json; use serde::{Serialize, Deserialize}; -use crate::routes::{APIError, APIErrorSingular, ERR_MSG_MALFORMED_REQUEST, ERR_MSG_MALFORMED_REQUEST_CODE}; -use rocket::post; +use rocket::{post, State}; +use sqlx::PgPool; +use crate::config::TFConfig; +use crate::tokens::generate_session_token; #[derive(Serialize, Deserialize)] #[serde(crate = "rocket::serde")] @@ -28,11 +31,30 @@ pub struct VerifyMagicLinkResponse { } #[post("/v1/auth/verify-magic-link", data = "")] -pub fn verify_magic_link(req: Json) -> Result<(ContentType, Json), (Status, Json)> { - // handle request +pub async fn verify_magic_link(req: Json, db: &State, config: &State) -> Result<(ContentType, Json), (Status, String)> { + // 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 { - data: VerifyMagicLinkResponseData { session_token: "sd[if0sf0dsfsdf".to_string() }, + data: VerifyMagicLinkResponseData { session_token: token }, metadata: VerifyMagicLinkResponseMetadata {}, }))) } \ No newline at end of file diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs index 6ce15fd..becd02a 100644 --- a/trifid-api/src/routes/v1/mod.rs +++ b/trifid-api/src/routes/v1/mod.rs @@ -1,2 +1,2 @@ -//pub mod auth; +pub mod auth; pub mod signup; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/signup.rs b/trifid-api/src/routes/v1/signup.rs index 5e46817..a12fccc 100644 --- a/trifid-api/src/routes/v1/signup.rs +++ b/trifid-api/src/routes/v1/signup.rs @@ -5,7 +5,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use rocket::http::{ContentType, Status}; use sqlx::PgPool; use crate::config::TFConfig; -use crate::email::send_magic_link; +use crate::tokens::send_magic_link; #[derive(Serialize, Deserialize)] #[serde(crate = "rocket::serde")] diff --git a/trifid-api/src/email.rs b/trifid-api/src/tokens.rs similarity index 69% rename from trifid-api/src/email.rs rename to trifid-api/src/tokens.rs index 47200d8..a27588a 100644 --- a/trifid-api/src/email.rs +++ b/trifid-api/src/tokens.rs @@ -14,4 +14,10 @@ pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFCon // TODO: send email info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for); Ok(()) +} + +pub async fn generate_session_token(id: i64, db: &PgPool, config: &TFConfig) -> Result> { + 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) } \ No newline at end of file