summaryrefslogtreecommitdiff
path: root/src/routes/v1/users.rs
blob: 10afc0c58ec20673a7e5c8cdece20973afca7c12 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use crate::db;
use crate::db::models::User;
use crate::extractors::{Json, Jwt};
use crate::routes::v1::requests;
use axum::extract::Query;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Extension;
use hmac::{Hmac, Mac};
use jwt::SignWithKey;
use lazy_static::lazy_static;
use serde_json::json;
use sha2::Sha256;
use sqlx::{Error, Pool, Postgres};
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::sync::Arc;

lazy_static! {
    static ref JWT_SECRET: String = env::var("JWT_SECRET").unwrap();
}

pub async fn create_user(
    Json(signup): Json<requests::Signup>,
    Extension(pool): Extension<Arc<Pool<Postgres>>>,
) -> impl IntoResponse {
    // TODO: Potentially more checks for password strength
    if signup.password.len() < 12 {
        return (
            StatusCode::BAD_REQUEST,
            Json(json!({"error": "Password must be at least 12 characters"})),
        );
    }
    let user =
        db::users::create_user(&pool, &signup.email, &signup.password, &signup.display_name).await;
    match user {
        Ok(user) => (StatusCode::OK, Json(json!({ "token": issue_jwt(user) }))),
        Err(err) => match err {
            Error::Database(e) if e.code().unwrap_or(std::borrow::Cow::Borrowed("")) == "23505" => {
                (
                    StatusCode::BAD_REQUEST,
                    Json(json!({"error": "A user with that email already exists"})),
                )
            }
            _ => (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(json!({ "error": format!("{:?}", err) })),
            ),
        },
    }
}

pub async fn get_all_users(
    Jwt(user): Jwt,
    Extension(pool): Extension<Arc<Pool<Postgres>>>,
) -> impl IntoResponse {
    if !user.admin {
        return (
            StatusCode::FORBIDDEN,
            Json(json!({"error": "You do not have permission to perform this action"})),
        );
    }

    let users = db::users::get_all_users(&pool).await;
    match users {
        Ok(users) => (StatusCode::OK, Json(json!(users))),
        Err(err) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({"error": err.to_string()})),
        ),
    }
}

pub async fn whoami(Jwt(user): Jwt) -> impl IntoResponse {
    (StatusCode::OK, Json(json!(user)))
}

pub async fn needs_totp(
    Query(params): Query<HashMap<String, String>>,
    Extension(pool): Extension<Arc<Pool<Postgres>>>,
) -> impl IntoResponse {
    match params.get("email") {
        Some(email) => match db::users::get_user(&pool, email).await {
            Ok(user) => (
                StatusCode::OK,
                Json(json!({"totp": user.totp_secret.is_some()})),
            ),
            Err(_) => (StatusCode::OK, Json(json!({"totp": false}))),
        },
        None => (
            StatusCode::BAD_REQUEST,
            Json(json!({"error": "Missing query parameter `email`"})),
        ),
    }
}

pub async fn login(
    Json(login_req): Json<requests::Login>,
    Extension(pool): Extension<Arc<Pool<Postgres>>>,
) -> impl IntoResponse {
    let user = db::users::get_user(&pool, &login_req.email).await;
    let user = match user {
        Ok(user) => user,
        Err(err) => match err {
            Error::RowNotFound => {
                return (
                    StatusCode::UNAUTHORIZED,
                    Json(json!({"error": "Invalid email or password"})),
                );
            }
            _ => {
                return (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    Json(json!({ "error": format!("{:?}", err) })),
                )
            }
        },
    };

    if !bcrypt::verify(&login_req.password, &user.password).unwrap_or(false) {
        return (
            StatusCode::UNAUTHORIZED,
            Json(json!({"error": "Invalid email or password"})),
        );
    }
    if !user.enabled {
        return (
            StatusCode::UNAUTHORIZED,
            Json(json!({"error": "Invalid email or password"})),
        );
    }

    let token = issue_jwt(user);
    (StatusCode::OK, Json(json!({ "token": token })))
}

fn issue_jwt(user: User) -> String {
    let key: Hmac<Sha256> = Hmac::new_from_slice((*JWT_SECRET).as_bytes()).unwrap();
    let mut claims = BTreeMap::new();

    let iat = chrono::Utc::now().timestamp().to_string();
    let exp = (chrono::Utc::now() + chrono::Duration::hours(24))
        .timestamp()
        .to_string();
    let dn = user.display_name.unwrap_or_else(|| user.email.clone());
    let admin = user.admin.to_string();
    let sub = user.id.to_string();

    // https://www.iana.org/assignments/jwt/jwt.xhtml
    claims.insert("iss", "fdns");
    claims.insert("sub", &sub);
    claims.insert("iat", &iat);
    claims.insert("exp", &exp);
    claims.insert("dn", &dn);
    claims.insert("email", &user.email);
    claims.insert("admin", &admin);

    claims.sign_with_key(&key).unwrap()
}