webdev_guide 0.6.0

Learn how to build a webservice in Rust!
use crate::db;
use crate::dtypes::structs::{Auth, Id, Status};
use crate::utils::handle_sql_error;
use actix_web::http::{header, StatusCode};
use actix_web::web::Json;
use actix_web::{post, HttpResponse};
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Argon2,
};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use sqlx::Error;
use std::env;
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Serialize, Deserialize)]
struct LoginMessage {
    message: String,
    redirect_url: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    iss: String,
    aud: String,
    email: Option<String>,
    security_level: Option<i16>,
    status: Option<Status>,
    last_login: Option<String>,
    failed_login_attempts: Option<i32>,
    exp: u64,
}

// payload of JWT
#[derive(Debug, Serialize, Deserialize)]
struct Claims2 {
    sub: String,
    iss: String,
    aud: String,
    email: Option<String>,
    security_level: Option<i16>,
    exp: u64,
}

#[post("/auth/create_user")]
async fn create_user(auth: Json<Auth>) -> HttpResponse {
    match db::connect().await {
        Ok(pg) => {
            dotenv::dotenv().ok();

            let start = SystemTime::now();
            let since_the_epoch = start
                .duration_since(UNIX_EPOCH)
                .expect("Time went backwards");

            let jwt_hours_active_var =
                env::var("JWT_HOURS_ACTIVE").expect("JWT_HOURS_ACTIVE is not set");
            let jwt_hours_active: u64 = jwt_hours_active_var
                .parse()
                .expect("Failed to convert JWT_HOURS_ACTIVE env var to u64");

            // Add (1 hour (3600 seconds) * however many hours) to the current Unix timestamp
            let exp: u64 = since_the_epoch.as_secs() + (3600 * jwt_hours_active as u64);

            let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET is not set");

            let encoding_key = EncodingKey::from_base64_secret(&jwt_secret).unwrap_or_else(|err| {
                eprintln!("Failed to decode base64 secret: {}", err);
                std::process::exit(1);
            });

            let user_claims = Claims2 {
                sub: auth.username.clone(),
                iss: "webservice_tutorial".to_string(),
                aud: "user".to_string(),
                email: auth.email.clone(),
                security_level: Some(10),
                exp,
            };

            let token =
                encode(&Header::new(Algorithm::HS256), &user_claims, &encoding_key).unwrap();

            let salt = SaltString::generate(&mut OsRng);

            let password_hash_str: String = Argon2::default()
                .hash_password(&auth.password.as_bytes(), &salt)
                .unwrap()
                .to_string();

            let result = sqlx::query_as!(
                Id,
                r#"
                    INSERT INTO auth
                        (
                            email,
                            username,
                            password,
                            security_level
                        )
                    VALUES (
                        $1,
                        $2,
                        $3,
                        $4
                    )
                    RETURNING id
                "#,
                auth.email,
                auth.username,
                password_hash_str,
                auth.security_level
            )
            .fetch_one(&pg)
            .await;

            match result {
                Ok(id) => HttpResponse::Created()
                    .status(StatusCode::CREATED)
                    .content_type("application/json")
                    .append_header((header::AUTHORIZATION, format!("Bearer {}", token)))
                    .body(
                        serde_json::to_string(&Json(id))
                            .unwrap_or_else(|e| format!("JSON serialization error: {}", e)),
                    ),

                Err(e) => handle_sql_error(e),
            }
        }
        Err(e) => HttpResponse::InternalServerError()
            .status(StatusCode::INTERNAL_SERVER_ERROR)
            .content_type("application/json")
            .body(e.message),
    }
}

#[post("/auth/login")]
async fn login(auth: Json<Auth>) -> HttpResponse {
    match db::connect().await {
        Ok(pg) => {
            let user: Result<Auth, Error> = sqlx::query_as!(
                Auth,
                r#"
                    SELECT
                        id,
                        email,
                        username,
                        password,
                        security_level,
                        status as "status: _",
                        to_char(last_login, 'DD Month YYYY HH12:MI AM') as last_login,
                        failed_login_attempts,
                        to_char(created, 'DD Month YYYY HH12:MI AM') as created,
                        to_char(edited, 'DD Month YYYY HH12:MI AM') as edited
                    FROM auth
                    WHERE username = $1
                    LIMIT 1;
                "#,
                &auth.username
            )
            .fetch_one(&pg)
            .await;

            match user {
                Ok(record) => {
                    dotenv::dotenv().ok();

                    let start = SystemTime::now();
                    let since_the_epoch = start
                        .duration_since(UNIX_EPOCH)
                        .expect("Time went backwards");

                    let jwt_hours_active_var =
                        env::var("JWT_HOURS_ACTIVE").expect("JWT_HOURS_ACTIVE is not set");
                    let jwt_hours_active: u64 = jwt_hours_active_var
                        .parse()
                        .expect("Failed to convert JWT_HOURS_ACTIVE env var to u64");

                    // Add (1 hour (3600 seconds) * however many hours) to the current Unix timestamp
                    let exp: u64 = since_the_epoch.as_secs() + (3600 * jwt_hours_active as u64);

                    let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET is not set");

                    let encoding_key =
                        EncodingKey::from_base64_secret(&jwt_secret).unwrap_or_else(|err| {
                            eprintln!("Failed to decode base64 secret: {}", err);
                            std::process::exit(1);
                        });
                    let stored_hash: String = record.password;
                    let parsed_hash = match PasswordHash::new(&stored_hash) {
                        Ok(hash) => hash,
                        Err(_) => return HttpResponse::Unauthorized().finish(),
                    };

                    let user_claims = Claims {
                        sub: record.username,
                        iss: "webservice_tutorial".to_string(),
                        aud: "user".to_string(),
                        email: record.email,
                        security_level: record.security_level,
                        status: record.status,
                        last_login: record.last_login,
                        failed_login_attempts: record.failed_login_attempts,
                        exp,
                    };

                    let token = encode(&Header::new(Algorithm::HS256), &user_claims, &encoding_key)
                        .unwrap();

                    if Argon2::default()
                        .verify_password(auth.password.as_bytes(), &parsed_hash)
                        .is_ok()
                    {
                        HttpResponse::Ok()
                            .status(StatusCode::OK)
                            .content_type("application/json")
                            .append_header((header::AUTHORIZATION, format!("Bearer {}", token)))
                            .json(LoginMessage {
                                message: "Logged in successfully".to_owned(),
                                redirect_url: "/dashboard".to_owned(),
                            })
                    } else {
                        return HttpResponse::Unauthorized().finish();
                    }
                }
                Err(_) => {
                    return HttpResponse::InternalServerError().finish();
                }
            }
        }
        Err(e) => HttpResponse::InternalServerError()
            .status(StatusCode::INTERNAL_SERVER_ERROR)
            .content_type("application/json")
            .body(e.message),
    }
}