magic-link and verify-magic-link working
This commit is contained in:
parent
e3c8819c08
commit
459dfb34ef
|
@ -2,3 +2,4 @@ listen_port = 8000
|
|||
db_url = "postgres://postgres@localhost/trifidapi"
|
||||
base = "http://localhost:8000"
|
||||
magic_links_valid_for = 86400
|
||||
session_tokens_valid_for = 86400
|
|
@ -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
|
||||
);
|
|
@ -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
|
||||
}
|
|
@ -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<dyn Error>> {
|
|||
|
||||
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,
|
||||
|
|
|
@ -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 {},
|
||||
})))
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod verify_magic_link;
|
||||
pub mod magic_link;
|
|
@ -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 = "<req>")]
|
||||
pub fn verify_magic_link(req: Json<VerifyMagicLinkRequest>) -> Result<(ContentType, Json<VerifyMagicLinkResponse>), (Status, Json<APIError>)> {
|
||||
// handle request
|
||||
pub async fn verify_magic_link(req: Json<VerifyMagicLinkRequest>, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<VerifyMagicLinkResponse>), (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 {},
|
||||
})))
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
//pub mod auth;
|
||||
pub mod auth;
|
||||
pub mod signup;
|
|
@ -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")]
|
||||
|
|
|
@ -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);
|
||||
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)
|
||||
}
|
Loading…
Reference in New Issue