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"
|
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
|
|
@ -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 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
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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 verify_magic_link;
|
||||||
|
pub mod magic_link;
|
|
@ -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 {},
|
||||||
})))
|
})))
|
||||||
}
|
}
|
|
@ -1,2 +1,2 @@
|
||||||
//pub mod auth;
|
pub mod auth;
|
||||||
pub mod signup;
|
pub mod signup;
|
|
@ -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")]
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue