diff --git a/backend/src/api/auth/controllers.rs b/backend/src/api/auth/controllers.rs index 0bf55b9..2fa76dc 100644 --- a/backend/src/api/auth/controllers.rs +++ b/backend/src/api/auth/controllers.rs @@ -1,5 +1,7 @@ use super::models::RegisterNewUserRequest; -use crate::api::auth::models::UserResponse; +use crate::api::auth::models::{RefreshTokenRequest, UserResponse}; +use crate::api::auth::utils::{find_refresh_token, find_user_by_id}; +use crate::api::error::ErrorType::Unauthorized; use crate::api::middleware::CurrentUser; use crate::api::utils::friendly_id::{ItemIdType, ToFriendlyId}; use crate::{ @@ -135,11 +137,27 @@ pub async fn logout() -> impl IntoResponse { content = RefreshTokenRequest ), responses( - (status = 501, description = "Not Implemented") + ) )] -pub async fn refresh_token() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +pub async fn refresh_token( + State(state): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), APIError> { + let mut conn = get_db_connection(&state.database_pool).await?; + + let token = find_refresh_token(&mut conn, &payload.refresh_token).await?; + let token = token.filter(|token| !token.is_expired()).ok_or( + APIErrorBuilder::new(Unauthorized) + .detail("The token you provided is expired.") + .build(), + )?; + + let user = find_user_by_id(&mut conn, &token.user_id).await?; + + let tokens = generate_auth_tokens(&mut conn, &user, &state.settings.auth.private_key).await?; + + Ok((StatusCode::OK, Json(tokens))) } /// Get the current user diff --git a/backend/src/api/auth/utils.rs b/backend/src/api/auth/utils.rs index fe3564b..9eb9e15 100644 --- a/backend/src/api/auth/utils.rs +++ b/backend/src/api/auth/utils.rs @@ -1,5 +1,5 @@ use crate::api::auth::models::AuthResponse; -use crate::api::error::ErrorType::UserAlreadyExists; +use crate::api::error::ErrorType::{Unauthorized, UserAlreadyExists}; use crate::api::error::{APIError, APIErrorBuilder}; use crate::db::database::Connection; use crate::db::refresh_token::{NewRefreshToken, RefreshToken}; @@ -138,6 +138,35 @@ pub async fn find_user_by_email( }) } +pub async fn find_user_by_id(conn: &mut Connection, id: &Uuid) -> Result { + User::all().find(id).first(conn).await.map_err(|e| { + error!(error = %e, "failed to find user by id"); + APIErrorBuilder::from_error(e).build() + }) +} + +pub async fn find_refresh_token( + conn: &mut Connection, + refresh_token: &str, +) -> Result, APIError> { + let uuid = Uuid::parse_str(refresh_token).map_err(|e| { + warn!(error = %e, "failed to parse refresh token"); + APIErrorBuilder::new(Unauthorized) + .cause(e) + .detail("The token you provided is invalid.") + .build() + })?; + + let token: Option = refresh_tokens::table + .filter(refresh_tokens::id.eq(uuid)) + .first::(conn) + .await + .optional() + .map_err(|e| APIErrorBuilder::from_error(e).build())?; + + Ok(token) +} + fn generate_auth_token(user: &User, private_key: &str) -> Result { let mut claims = Claims::new_expires_in(&TOKEN_EXPIRY_TIME)?; claims.issuer("domus-api.jacksonc.dev")?; diff --git a/backend/src/db/refresh_token.rs b/backend/src/db/refresh_token.rs index 384e49c..25d763e 100644 --- a/backend/src/db/refresh_token.rs +++ b/backend/src/db/refresh_token.rs @@ -12,6 +12,12 @@ pub struct RefreshToken { pub updated_at: Option, } +impl RefreshToken { + pub(crate) fn is_expired(&self) -> bool { + self.expires_at < chrono::Utc::now().naive_utc() + } +} + #[derive(Insertable)] #[diesel(table_name = crate::db::schema::refresh_tokens)] pub struct NewRefreshToken {