Working on fixing metars, airport layout, etc
This commit is contained in:
281
api/src/account/user.rs
Normal file
281
api/src/account/user.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use crate::db;
|
||||
use crate::{account::hash, error::ApiResult};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[allow(unused_imports)] // Import is used in schema examples
|
||||
use serde_json::json;
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const ADMIN_ROLE: &str = "ADMIN";
|
||||
pub const USER_ROLE: &str = "USER";
|
||||
const TABLE_NAME: &str = "users";
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[schema(
|
||||
example = json!(
|
||||
{
|
||||
"email": "user",
|
||||
"email": "user@example.com",
|
||||
"password": "changeme",
|
||||
"firstName": "firstname",
|
||||
"lastName": "lastname"
|
||||
}
|
||||
)
|
||||
)]
|
||||
pub struct RegisterRequest {
|
||||
pub username: String,
|
||||
pub email: Option<String>,
|
||||
pub password: String,
|
||||
#[serde(rename = "firstName")]
|
||||
pub first_name: String,
|
||||
#[serde(rename = "lastName")]
|
||||
pub last_name: String,
|
||||
}
|
||||
|
||||
impl RegisterRequest {
|
||||
pub fn to_user(self) -> ApiResult<User> {
|
||||
let password_hash = hash(&self.password)?;
|
||||
Ok(User {
|
||||
username: self.username,
|
||||
email: match self.email {
|
||||
Some(email) => Some(email.to_lowercase()),
|
||||
None => None,
|
||||
},
|
||||
email_verified: false,
|
||||
password_hash,
|
||||
role: USER_ROLE.to_string(),
|
||||
first_name: self.first_name,
|
||||
last_name: self.last_name,
|
||||
avatar: None,
|
||||
updated_at: Utc::now(),
|
||||
created_at: Utc::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[schema(
|
||||
example = json!(
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "changeme"
|
||||
}
|
||||
)
|
||||
)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct UserResponse {
|
||||
pub username: String,
|
||||
pub role: String,
|
||||
#[serde(rename = "firstName")]
|
||||
pub first_name: String,
|
||||
#[serde(rename = "lastName")]
|
||||
pub last_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<String>,
|
||||
#[serde(rename = "emailVerified")]
|
||||
pub email_verified: bool,
|
||||
}
|
||||
|
||||
impl From<User> for UserResponse {
|
||||
fn from(user: User) -> Self {
|
||||
UserResponse {
|
||||
username: user.username,
|
||||
email_verified: user.email_verified,
|
||||
role: user.role,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
avatar: user.avatar,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, sqlx::FromRow, ToSchema)]
|
||||
pub struct UpdateUser {
|
||||
pub email: Option<String>,
|
||||
pub email_verified: Option<bool>,
|
||||
pub password: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateUser {
|
||||
pub async fn update(&self, username: &str) -> ApiResult<User> {
|
||||
let pool = db::pool();
|
||||
|
||||
let mut query_builder: QueryBuilder<Postgres> =
|
||||
QueryBuilder::new(&format!("UPDATE {} SET ", TABLE_NAME));
|
||||
|
||||
let mut first_clause = true;
|
||||
|
||||
let mut push_comma = |query_builder: &mut QueryBuilder<Postgres>| {
|
||||
if !first_clause {
|
||||
query_builder.push(", ");
|
||||
} else {
|
||||
first_clause = false;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref email) = self.email {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("email = ");
|
||||
query_builder.push_bind(email);
|
||||
}
|
||||
if let Some(ref email_verified) = self.email_verified {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("email_verified = ");
|
||||
query_builder.push_bind(email_verified);
|
||||
}
|
||||
if let Some(ref password) = self.password {
|
||||
push_comma(&mut query_builder);
|
||||
let password_hash = hash(password)?;
|
||||
query_builder.push("password_hash = ");
|
||||
query_builder.push_bind(password_hash);
|
||||
}
|
||||
if let Some(ref role) = self.role {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("role = ");
|
||||
query_builder.push_bind(role);
|
||||
}
|
||||
if let Some(ref first_name) = self.first_name {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("first_name = ");
|
||||
query_builder.push_bind(first_name);
|
||||
}
|
||||
if let Some(ref last_name) = self.last_name {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("last_name = ");
|
||||
query_builder.push_bind(last_name);
|
||||
}
|
||||
if let Some(ref avatar) = self.avatar {
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("avatar = ");
|
||||
query_builder.push_bind(avatar);
|
||||
}
|
||||
push_comma(&mut query_builder);
|
||||
query_builder.push("updated_at = ");
|
||||
query_builder.push_bind(Utc::now());
|
||||
|
||||
query_builder.push(" WHERE username = ");
|
||||
query_builder.push_bind(username);
|
||||
query_builder.push(" RETURNING *");
|
||||
|
||||
let query = query_builder.build_query_as::<User>();
|
||||
let user = query.fetch_one(pool).await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub email: Option<String>,
|
||||
pub email_verified: bool,
|
||||
pub password_hash: String,
|
||||
pub role: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn select(username: &str) -> Option<Self> {
|
||||
let pool = db::pool();
|
||||
let user: Option<Self> = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
SELECT * FROM {} WHERE username = $1
|
||||
"#,
|
||||
TABLE_NAME
|
||||
))
|
||||
.bind(username)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Unable to find user '{}': {}", username, err);
|
||||
None
|
||||
});
|
||||
|
||||
user
|
||||
}
|
||||
|
||||
pub async fn select_by_email(email: &str) -> Option<Self> {
|
||||
let pool = db::pool();
|
||||
let user: Option<Self> = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
SELECT * FROM {} WHERE email = $1
|
||||
"#,
|
||||
TABLE_NAME
|
||||
))
|
||||
.bind(email.to_lowercase())
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Unable to find user by email '{}': {}", email, err);
|
||||
None
|
||||
});
|
||||
|
||||
user
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn count() -> i64 {
|
||||
let pool = db::pool();
|
||||
|
||||
sqlx::query_scalar(&format!(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM {}
|
||||
"#,
|
||||
TABLE_NAME
|
||||
))
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
pub async fn insert(&self) -> ApiResult<User> {
|
||||
let pool = db::pool();
|
||||
let user: User = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
INSERT INTO {} (
|
||||
username,
|
||||
email,
|
||||
email_verified,
|
||||
password_hash,
|
||||
role,
|
||||
first_name,
|
||||
last_name,
|
||||
avatar,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *
|
||||
"#,
|
||||
TABLE_NAME,
|
||||
))
|
||||
.bind(&self.username)
|
||||
.bind(&self.email)
|
||||
.bind(&self.email_verified)
|
||||
.bind(&self.password_hash)
|
||||
.bind(&self.role)
|
||||
.bind(&self.first_name)
|
||||
.bind(&self.last_name)
|
||||
.bind(&self.avatar)
|
||||
.bind(self.created_at)
|
||||
.bind(self.updated_at)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user