-
Notifications
You must be signed in to change notification settings - Fork 0
SOme frontend and couple of sql queries in the backend #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| "use client"; | ||
| import { useSearchParams } from "next/navigation"; | ||
|
|
||
| export default function VerifyPage() { | ||
| const searchParams = useSearchParams(); | ||
| const token = searchParams.get("token"); | ||
|
|
||
| // TODO: Implement actual verification logic by sending the token to the server for validation | ||
|
|
||
| return ( | ||
| <div className="auth-verify"> | ||
| <div className="container"> | ||
| <h1>Verify your email</h1> | ||
| <p> | ||
| A verification link has been sent to your email. Please check your | ||
| inbox and click the link to verify your account. | ||
| </p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Force single-threaded test execution to avoid database race conditions. | ||
| # Each test truncates the database, so concurrent execution causes conflicts. | ||
| RUST_TEST_THREADS = "1" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,17 @@ | ||
| use crate::db::queries::{find_user_by_email, find_user_by_username}; | ||
| use crate::features::auth::queries::delete_user; | ||
| use crate::db::queries::{find_token_by_value, find_user_by_email, find_user_by_username}; | ||
| use crate::features::auth::queries::{delete_user, verify_user}; | ||
| use crate::features::auth::service::new_user; | ||
| use crate::features::auth::types::{AvailabilityQuery, VerifyQuery}; | ||
| use crate::state::AppState; | ||
| use crate::utils::api_response::ApiResponse; | ||
| use crate::utils::password::hash_password; | ||
| use crate::{errors::AppError, features::auth::types::RegisterData}; | ||
| use axum::extract::rejection::JsonRejection; | ||
| use axum::extract::{Json, State}; | ||
| use axum::extract::{Json, Query, State}; | ||
| use chrono::{DateTime, Utc}; | ||
| use sqlx::Row; | ||
| use tokio::task::spawn_blocking; | ||
| use uuid::Uuid; | ||
| use validator::Validate; | ||
|
|
||
| // ---------------------- | ||
|
|
@@ -25,11 +29,6 @@ pub async fn register_user( | |
| }; | ||
|
|
||
| let cleaned_data = validate_registerdata(payload)?; | ||
|
|
||
| let hashed_password = spawn_blocking(move || hash_password(&cleaned_data.password)) | ||
| .await | ||
| .map_err(|_| AppError::internal("Threading error"))??; | ||
|
|
||
| let db = state.db()?; | ||
|
|
||
| if find_user_by_email(db, &cleaned_data.email).await?.is_some() { | ||
|
|
@@ -43,6 +42,10 @@ pub async fn register_user( | |
| return Err(AppError::conflict("Username already in use")); | ||
| } | ||
|
|
||
| let hashed_password = spawn_blocking(move || hash_password(&cleaned_data.password)) | ||
| .await | ||
| .map_err(|_| AppError::internal("Threading error"))??; | ||
|
|
||
| let (user_id, token) = new_user( | ||
| db, | ||
| &cleaned_data.email, | ||
|
|
@@ -76,6 +79,77 @@ pub async fn register_user( | |
| )) | ||
| } | ||
|
|
||
| // ---------------------- | ||
| // /api/auth/available - params: email, username | ||
| // GET | ||
| // Check if email or username is available | ||
| // ---------------------- | ||
| pub async fn check_availability( | ||
| State(state): State<AppState>, | ||
| Query(params): Query<AvailabilityQuery>, | ||
| ) -> Result<ApiResponse<()>, AppError> { | ||
| match (¶ms.email, ¶ms.username) { | ||
| (None, None) => return Err(AppError::bad_request("Invalid query")), | ||
| (Some(_), Some(_)) => return Err(AppError::bad_request("Too many params")), | ||
| _ => {} | ||
| } | ||
|
|
||
| let db = state.db()?; | ||
|
|
||
| if let Some(email) = ¶ms.email { | ||
| let email = email.trim().to_lowercase(); | ||
| if email.is_empty() { | ||
| return Err(AppError::bad_request("Invalid query")); | ||
| } | ||
| if find_user_by_email(db, &email).await?.is_some() { | ||
| return Err(AppError::conflict("Email already in use")); | ||
| } | ||
| } | ||
|
|
||
| if let Some(username) = ¶ms.username { | ||
| let username = username.trim().to_lowercase(); | ||
| if username.is_empty() { | ||
| return Err(AppError::bad_request("Invalid query")); | ||
| } | ||
| if find_user_by_username(db, &username).await?.is_some() { | ||
| return Err(AppError::conflict("Username already in use")); | ||
| } | ||
| } | ||
|
reijjo marked this conversation as resolved.
|
||
|
|
||
| Ok(ApiResponse::ok("No duplicates found", None)) | ||
| } | ||
|
|
||
| // ---------------------- | ||
| // /api/auth/verify - params: token | ||
| // GET | ||
| // Account verification | ||
| // ---------------------- | ||
| pub async fn verify_account( | ||
| State(state): State<AppState>, | ||
| Query(params): Query<VerifyQuery>, | ||
| ) -> Result<ApiResponse<()>, AppError> { | ||
| let token = match ¶ms.token { | ||
| Some(t) if !t.trim().is_empty() => t.trim().to_string(), | ||
| _ => return Err(AppError::bad_request("Invalid or missing token")), | ||
| }; | ||
|
|
||
| let db = state.db()?; | ||
| let row = find_token_by_value(db, &token) | ||
| .await? | ||
| .ok_or_else(|| AppError::not_found("Invalid token"))?; | ||
|
|
||
| let user_id: Uuid = row.get("user_id"); | ||
| let expires_at: DateTime<Utc> = row.get("expires_at"); | ||
|
|
||
| if Utc::now() > expires_at { | ||
| return Err(AppError::bad_request("Token has expired")); | ||
| } | ||
|
|
||
| verify_user(db, user_id).await?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent success on non-existent users. Looking at Consider checking the result: pub async fn verify_user<'e, E>(db: E, user_id: Uuid) -> Result<(), AppError>
where
E: Executor<'e, Database = Postgres>,
{
let result = sqlx::query("UPDATE users SET verified = true WHERE id = $1")
.bind(user_id)
.execute(db)
.await
.map_err(AppError::Sql)?;
if result.rows_affected() == 0 {
return Err(AppError::not_found("User not found"));
}
Ok(())
}This is edge-case defensive coding, but it prevents confusing scenarios in production. 🤖 Prompt for AI Agents |
||
|
|
||
| Ok(ApiResponse::ok("Email verified successfully", None)) | ||
| } | ||
|
|
||
| // ----------------- | ||
| // Validate data | ||
| // ----------------- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,16 @@ | ||
| use super::handlers::register_user; | ||
| use crate::state::AppState; | ||
| use axum::{Router, routing::post}; | ||
| use crate::{ | ||
| features::auth::handlers::{check_availability, verify_account}, | ||
| state::AppState, | ||
| }; | ||
| use axum::{ | ||
| Router, | ||
| routing::{get, post}, | ||
| }; | ||
|
|
||
| pub fn auth_router() -> Router<AppState> { | ||
| Router::new().route("/auth/register", post(register_user)) | ||
| Router::new() | ||
| .route("/register", post(register_user)) | ||
| .route("/available", get(check_availability)) | ||
| .route("/verify", get(verify_account)) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.