Open source Nest implementation
use crate::database::Database;
use rocket::serde::{Deserialize, Serialize};
use rocket::State;
use sqlx::query_as;

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,

    #[serde(skip_deserializing)]
    pub password: String,
}

impl User {
    pub async fn create(
        db: &Database,
        name: String,
        password: String,
        email: String,
    ) -> Result<Self, sqlx::Error> {
        let result = query_as!( User,
            "INSERT INTO users (name, email, password) VALUES ($1, $2, crypt($3, gen_salt('bf'))) RETURNING *",
            &name,
            &email.to_lowercase(),
            &password
        )
        .fetch_one(db)
        .await?;

        Ok(result)
    }

    pub async fn find(db: &Database, id: i32) -> Result<Self, sqlx::Error> {
        query_as!(User, "SELECT * FROM users WHERE id = $1", id)
            .fetch_one(db)
            .await
    }

    /// Validates a user and password combination, returns a User struct when
    /// valid.
    pub async fn authenticate(
        db: &Database,
        name: String,
        password: String,
    ) -> Result<Option<User>, sqlx::Error> {
        let result = query_as!(
            User,
            "SELECT * FROM users WHERE name = $1 AND password = crypt($2, password)",
            name,
            password
        )
        .fetch_optional(db)
        .await?;

        Ok(result)
    }
}

static COOKIE_USER_ID_KEY: &str = "user_id";

use rocket::http::{CookieJar, Status};
use rocket::request::{FromRequest, Outcome};
use rocket::Request;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for User {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> Outcome<User, ()> {
        let db: &State<Database> = match request.guard().await {
            Outcome::Success(db) => db,
            _ => return Outcome::Failure((Status::Unauthorized, ())),
        };

        if let Some(user) = user_from_basic_auth(db, &*request).await {
            return Outcome::Success(user);
        }
        // Try getting the user from the cookie
        let cookies = request.cookies();

        let some_id = match get_user_id_from_cookie(cookies) {
            Some(id) => id,
            None => return Outcome::Forward(()),
        };

        match User::find(db, some_id).await {
            Ok(u) => Outcome::Success(u),
            Err(_e) => Outcome::Forward(()), // TODO this is a failure
        }
    }
}

async fn user_from_basic_auth(db: &State<Database>, req: &Request<'_>) -> Option<User> {
    use rocket_basicauth::BasicAuth;

    info!("trying basic auth");
    let credentials = match BasicAuth::from_request(req).await {
        Outcome::Success(ba) => ba,
        _ => return None,
    };
    info!("extracted basic auth");

    match User::authenticate(db, credentials.username, credentials.password).await {
        Ok(some) => some,
        _ => None,
    }
}

fn get_user_id_from_cookie(jar: &CookieJar) -> Option<i32> {
    match jar.get_private(COOKIE_USER_ID_KEY) {
        Some(id) => Some(id.value().parse::<i32>().ok()?),
        None => None,
    }
}

impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}
impl Eq for User {}