Working on email templating, updating with swagger
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use super::{SESSION_COOKIE_NAME, Session};
|
||||
use crate::{error::Error, users::User};
|
||||
use super::{Session, SESSION_COOKIE_NAME};
|
||||
use actix_web::{Error as ActixError, FromRequest, HttpRequest, dev::Payload, http};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Auth {
|
||||
@@ -34,13 +34,13 @@ impl FromRequest for Auth {
|
||||
return Err(Error::new(401, "API Key does not exist".to_string()).into());
|
||||
}
|
||||
};
|
||||
match User::select(&api_key.id).await {
|
||||
match User::select(&api_key.user_id).await {
|
||||
Some(user) => Ok(Auth {
|
||||
session_id: None,
|
||||
api_key: Some(key_id),
|
||||
user,
|
||||
}),
|
||||
None => Err(Error::new(404, format!("User {} not found", api_key.id)).into()),
|
||||
None => Err(Error::new(404, format!("User {} not found", api_key.user_id)).into()),
|
||||
}
|
||||
};
|
||||
return Box::pin(fut);
|
||||
@@ -79,13 +79,13 @@ impl FromRequest for Auth {
|
||||
// Verify the session
|
||||
let fut = async move {
|
||||
match Session::verify(&session_id, &ip_address).await {
|
||||
Ok(session) => match User::select(&session.id).await {
|
||||
Ok(session) => match User::select(&session.user_id).await {
|
||||
Some(user) => Ok(Auth {
|
||||
session_id: Some(session_id),
|
||||
api_key: None,
|
||||
user,
|
||||
}),
|
||||
None => Err(Error::new(404, format!("User {} not found", session.id)).into()),
|
||||
None => Err(Error::new(404, format!("User {} not found", session.user_id)).into()),
|
||||
},
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, SaltString},
|
||||
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||
password_hash::{SaltString, rand_core::OsRng},
|
||||
};
|
||||
use rand::distr::Alphanumeric;
|
||||
use rand::prelude::*;
|
||||
@@ -11,10 +11,10 @@ mod routes;
|
||||
mod session;
|
||||
|
||||
pub use auth::*;
|
||||
pub use session::*;
|
||||
pub use routes::init_routes;
|
||||
pub use session::*;
|
||||
|
||||
use crate::error::{Error, ApiResult};
|
||||
use crate::error::{ApiResult, Error};
|
||||
|
||||
pub fn csprng(take: usize) -> String {
|
||||
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
use actix_web::{post, web, HttpResponse, ResponseError, HttpRequest, put, get};
|
||||
use crate::{
|
||||
account::{verify_hash, Session, SESSION_COOKIE_NAME},
|
||||
account::{SESSION_COOKIE_NAME, Session, verify_hash},
|
||||
error::Error,
|
||||
smtp,
|
||||
users::{LoginRequest, RegisterRequest, User, UserResponse},
|
||||
};
|
||||
use actix_web::{HttpRequest, HttpResponse, ResponseError, get, post, put, web};
|
||||
use utoipa_actix_web::scope;
|
||||
use utoipa_actix_web::service_config::ServiceConfig;
|
||||
|
||||
use crate::account::Auth;
|
||||
use crate::account::{Auth, csprng};
|
||||
use crate::users::UpdateUser;
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
request_body(
|
||||
content = RegisterRequest, content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "", body = UserResponse),
|
||||
(status = 409, description = ""),
|
||||
)
|
||||
)]
|
||||
#[post("/register")]
|
||||
async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpResponse {
|
||||
let register_user = user.into_inner();
|
||||
@@ -38,13 +51,22 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
|
||||
);
|
||||
HttpResponse::Conflict().finish()
|
||||
} else {
|
||||
log::error!("attemptFailed to register user [Email: {}]: {}", email, err);
|
||||
log::error!("Failed to register user [Email: {}]: {}", email, err);
|
||||
ResponseError::error_response(&err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
request_body(
|
||||
content = LoginRequest, content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "", body = UserResponse),
|
||||
),
|
||||
)]
|
||||
#[post("/login")]
|
||||
async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpResponse {
|
||||
let email = &request.email;
|
||||
@@ -93,6 +115,17 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
responses(
|
||||
(status = 200, description = ""),
|
||||
(status = 401, description = ""),
|
||||
(status = 500, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[post("/logout")]
|
||||
async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
let email = auth.user.email;
|
||||
@@ -132,6 +165,16 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
responses(
|
||||
(status = 200, description = "", body = UserResponse),
|
||||
(status = 401, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[get("/profile")]
|
||||
async fn get_profile(req: HttpRequest) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
@@ -154,7 +197,7 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
|
||||
.finish();
|
||||
}
|
||||
};
|
||||
let id = &session.id;
|
||||
let id = &session.user_id;
|
||||
let query_user = match User::select(&id).await {
|
||||
Some(query_user) => query_user,
|
||||
None => {
|
||||
@@ -186,6 +229,16 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
responses(
|
||||
(status = 200, description = ""),
|
||||
(status = 401, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[get("/session")]
|
||||
async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
@@ -208,7 +261,7 @@ async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
||||
.finish();
|
||||
}
|
||||
};
|
||||
let id = &session.id;
|
||||
let id = &session.user_id;
|
||||
let session_cookie = session.cookie();
|
||||
let session_exp_cookie = session.expiration_cookie();
|
||||
|
||||
@@ -229,6 +282,19 @@ async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
request_body(
|
||||
content = String, content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "", body = UserResponse),
|
||||
(status = 401, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[put("/password")]
|
||||
async fn change_password(
|
||||
password: web::Json<String>,
|
||||
@@ -269,25 +335,52 @@ async fn change_password(
|
||||
ip_address,
|
||||
err
|
||||
);
|
||||
ResponseError::error_response(&Error::new(500, err.to_string()))
|
||||
ResponseError::error_response(&err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/password-reset")]
|
||||
async fn password_reset(req: HttpRequest, _auth: Auth) -> HttpResponse {
|
||||
let _ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
responses(
|
||||
(status = 200, description = ""),
|
||||
(status = 401, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[post("/password/reset")]
|
||||
async fn reset_password(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let id = auth.user.id;
|
||||
let email = auth.user.email;
|
||||
let token = csprng(128);
|
||||
|
||||
match smtp::send_password_reset(&email, &token) {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Invalid password reset attempt [ID: {}] [IP Address: {}]: {}",
|
||||
&id,
|
||||
ip_address,
|
||||
err
|
||||
);
|
||||
ResponseError::error_response(&err)
|
||||
}
|
||||
};
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
pub fn init_routes(config: &mut ServiceConfig) {
|
||||
config.service(
|
||||
web::scope("account")
|
||||
scope::scope("/account")
|
||||
.service(register)
|
||||
.service(login)
|
||||
.service(logout)
|
||||
.service(change_password)
|
||||
.service(get_profile)
|
||||
.service(session_refresh),
|
||||
.service(session_refresh)
|
||||
.service(change_password)
|
||||
.service(reset_password),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use actix_web::cookie::{time::Duration, Cookie};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use tokio::task;
|
||||
use uuid::Uuid;
|
||||
use super::{csprng, hash, verify_hash};
|
||||
use crate::{
|
||||
db::redis_async_connection,
|
||||
error::{Error, ApiResult},
|
||||
error::{ApiResult, Error},
|
||||
};
|
||||
use super::{csprng, hash, verify_hash};
|
||||
use actix_web::cookie::{Cookie, time::Duration};
|
||||
use chrono::{DateTime, Utc};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
|
||||
pub const SESSION_COOKIE_NAME: &str = "session";
|
||||
@@ -17,22 +17,22 @@ pub const SESSION_EXPIRATION_COOKIE_NAME: &str = "session_expiration";
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
pub session_id: String,
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub ip_address: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn default(id: &Uuid, ip_address: &str) -> Self {
|
||||
Self::new(64, id, ip_address, Some(DEFAULT_SESSION_TTL))
|
||||
pub fn default(user_id: &Uuid, ip_address: &str) -> Self {
|
||||
Self::new(64, user_id, ip_address, Some(DEFAULT_SESSION_TTL))
|
||||
}
|
||||
|
||||
pub fn new(take: usize, id: &Uuid, ip_address: &str, ttl: Option<i64>) -> Self {
|
||||
pub fn new(take: usize, user_id: &Uuid, ip_address: &str, ttl: Option<i64>) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
session_id: csprng(take),
|
||||
id: id.clone(),
|
||||
user_id: user_id.clone(),
|
||||
ip_address: hash(&ip_address).unwrap(),
|
||||
expires_at: match ttl {
|
||||
Some(ttl) => Some(now + chrono::Duration::seconds(ttl)),
|
||||
@@ -79,7 +79,7 @@ impl Session {
|
||||
);
|
||||
};
|
||||
});
|
||||
session = Session::default(&session.id, ip_address);
|
||||
session = Session::default(&session.user_id, ip_address);
|
||||
session.store().await?;
|
||||
Ok(session)
|
||||
}
|
||||
@@ -120,8 +120,8 @@ impl Session {
|
||||
if let Ok(environment) = std::env::var("ENVIRONMENT") {
|
||||
if environment == "development" || environment == "dev" {
|
||||
log::trace!(
|
||||
"Session cookie [ID: {}]: {}",
|
||||
self.id,
|
||||
"Session cookie [User ID: {}]: {}",
|
||||
self.user_id,
|
||||
self.session_id
|
||||
);
|
||||
cookie.set_secure(false);
|
||||
@@ -148,8 +148,8 @@ impl Session {
|
||||
if let Ok(environment) = std::env::var("ENVIRONMENT") {
|
||||
if environment == "development" || environment == "dev" {
|
||||
log::trace!(
|
||||
"Session expiration cookie [ID: {}]: {}",
|
||||
self.id,
|
||||
"Session expiration cookie [User ID: {}]: {}",
|
||||
self.user_id,
|
||||
self.session_id
|
||||
);
|
||||
cookie.set_secure(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod model;
|
||||
mod routes;
|
||||
pub mod model;
|
||||
pub mod routes;
|
||||
|
||||
pub use model::*;
|
||||
pub use routes::init_routes;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use crate::airports::{
|
||||
AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication,
|
||||
UpdateRunway,
|
||||
};
|
||||
use crate::db;
|
||||
use crate::error::{ApiResult, Error};
|
||||
use crate::metars::Metar;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures_util::try_join;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use crate::airports::{
|
||||
AirportCategory, Communication, CommunicationRow, Runway, RunwayRow, UpdateCommunication, UpdateRunway,
|
||||
};
|
||||
use crate::db;
|
||||
use crate::error::{ApiResult, Error};
|
||||
use crate::metars::Metar;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
const TABLE_NAME: &str = "airports";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Airport {
|
||||
pub icao: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -40,7 +42,8 @@ pub struct Airport {
|
||||
pub latest_metar: Option<Metar>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct AirportQuery {
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
@@ -75,7 +78,7 @@ impl Default for AirportQuery {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct Bounds {
|
||||
pub north_east_lat: f32,
|
||||
pub north_east_lon: f32,
|
||||
@@ -125,7 +128,7 @@ struct AirportRow {
|
||||
pub metar_observation_time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateAirport {
|
||||
pub icao: Option<String>,
|
||||
pub iata: Option<String>,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub enum AirportCategory {
|
||||
#[serde(rename = "small_airport")]
|
||||
Small,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use uuid::Uuid;
|
||||
use crate::db;
|
||||
use crate::error::ApiResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
const TABLE_NAME: &str = "communications";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Communication {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -27,7 +28,7 @@ pub struct CommunicationRow {
|
||||
pub phone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateCommunication {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icao: Option<String>,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use uuid::Uuid;
|
||||
use crate::db;
|
||||
use crate::error::ApiResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
const TABLE_NAME: &str = "runways";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Runway {
|
||||
#[serde(rename = "id")]
|
||||
pub runway_id: String,
|
||||
@@ -26,7 +27,7 @@ pub struct RunwayRow {
|
||||
pub surface: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateRunway {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icao: Option<String>,
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
use futures_util::stream::StreamExt as _;
|
||||
|
||||
use crate::{
|
||||
airports::Airport,
|
||||
db::Paged,
|
||||
account::{Auth, verify_role},
|
||||
AppState,
|
||||
};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
||||
use crate::airports::{AirportQuery, UpdateAirport};
|
||||
use crate::users::ADMIN_ROLE;
|
||||
use crate::{
|
||||
AppState,
|
||||
account::{Auth, verify_role},
|
||||
airports::Airport,
|
||||
db::Paged,
|
||||
};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{HttpRequest, HttpResponse, ResponseError, delete, get, post, put, web};
|
||||
use utoipa::ToSchema;
|
||||
use utoipa_actix_web::scope;
|
||||
use utoipa_actix_web::service_config::ServiceConfig;
|
||||
|
||||
#[derive(ToSchema)]
|
||||
#[allow(unused)]
|
||||
struct UploadedFile {
|
||||
#[schema(value_type = String, format = Binary)]
|
||||
file: Vec<u8>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Airports",
|
||||
request_body(
|
||||
content = UploadedFile, content_type = "multipart/form-data"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Successful import"),
|
||||
(status = 401, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[post("/import")]
|
||||
async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
if let Err(err) = verify_role(&auth, ADMIN_ROLE) {
|
||||
@@ -53,6 +76,15 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Airports",
|
||||
params(
|
||||
AirportQuery
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "", body = [Airport]),
|
||||
),
|
||||
)]
|
||||
#[get("")]
|
||||
async fn get_airports(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
||||
@@ -87,6 +119,13 @@ async fn get_airports(data: web::Data<AppState>, req: HttpRequest) -> HttpRespon
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Airports",
|
||||
responses(
|
||||
(status = 200, description = "", body = Airport),
|
||||
(status = 404, description = ""),
|
||||
),
|
||||
)]
|
||||
#[get("/{icao}")]
|
||||
async fn get_airport(
|
||||
data: web::Data<AppState>,
|
||||
@@ -108,6 +147,17 @@ async fn get_airport(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Airports",
|
||||
responses(
|
||||
(status = 200, description = "", body = Airport),
|
||||
(status = 401, description = ""),
|
||||
(status = 409, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[post("")]
|
||||
async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||
@@ -123,6 +173,16 @@ async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Airports",
|
||||
responses(
|
||||
(status = 200, description = "", body = Airport),
|
||||
(status = 401, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[put("/{icao}")]
|
||||
async fn update_airport(
|
||||
icao: web::Path<String>,
|
||||
@@ -142,6 +202,16 @@ async fn update_airport(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Airports",
|
||||
responses(
|
||||
(status = 201, description = ""),
|
||||
(status = 401, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[delete("")]
|
||||
async fn delete_airports(auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||
@@ -157,6 +227,16 @@ async fn delete_airports(auth: Auth) -> HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Airports",
|
||||
responses(
|
||||
(status = 201, description = ""),
|
||||
(status = 401, description = ""),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[delete("/{icao}")]
|
||||
async fn delete_airport(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
let _ = match verify_role(&auth, ADMIN_ROLE) {
|
||||
@@ -172,9 +252,9 @@ async fn delete_airport(icao: web::Path<String>, auth: Auth) -> HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
pub fn init_routes(config: &mut ServiceConfig) {
|
||||
config.service(
|
||||
web::scope("airports")
|
||||
scope::scope("/airports")
|
||||
.service(import_airports)
|
||||
.service(get_airports)
|
||||
.service(get_airport)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::error::ApiResult;
|
||||
use redis::{Client as RedisClient, aio::MultiplexedConnection as RedisConnection, RedisResult};
|
||||
use s3::{Bucket, Region, creds::Credentials, BucketConfiguration, request::ResponseData};
|
||||
use redis::{Client as RedisClient, RedisResult, aio::MultiplexedConnection as RedisConnection};
|
||||
use s3::{Bucket, BucketConfiguration, Region, creds::Credentials, request::ResponseData};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
|
||||
static POOL: OnceLock<Pool<Postgres>> = OnceLock::new();
|
||||
static REDIS: OnceLock<RedisClient> = OnceLock::new();
|
||||
@@ -169,9 +169,3 @@ pub struct Paged<T> {
|
||||
pub limit: u32,
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Coordinate {
|
||||
pub lon: f64,
|
||||
pub lat: f64,
|
||||
}
|
||||
|
||||
@@ -204,3 +204,27 @@ impl From<sqlx::migrate::MigrateError> for Error {
|
||||
Error::new(500, error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre::address::AddressError> for Error {
|
||||
fn from(error: lettre::address::AddressError) -> Self {
|
||||
Error::new(500, error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre::error::Error> for Error {
|
||||
fn from(error: lettre::error::Error) -> Self {
|
||||
Error::new(500, error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre::transport::smtp::Error> for Error {
|
||||
fn from(error: lettre::transport::smtp::Error) -> Self {
|
||||
Error::new(500, error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(error: String) -> Self {
|
||||
Self::new(500, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
use crate::account::hash;
|
||||
use crate::users::{ADMIN_ROLE, User};
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, HttpServer, middleware::Logger, web};
|
||||
use dotenv::from_filename;
|
||||
use reqwest::Certificate;
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
|
||||
use utoipa::openapi::{Contact, SecurityRequirement};
|
||||
use utoipa_actix_web::{AppExt, scope};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
use uuid::Uuid;
|
||||
use crate::account::hash;
|
||||
use crate::users::{User, ADMIN_ROLE};
|
||||
|
||||
mod account;
|
||||
mod airports;
|
||||
@@ -14,6 +18,7 @@ mod db;
|
||||
mod error;
|
||||
mod metars;
|
||||
mod scheduler;
|
||||
mod smtp;
|
||||
mod system;
|
||||
mod users;
|
||||
|
||||
@@ -91,18 +96,49 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.allow_any_header()
|
||||
.supports_credentials()
|
||||
.max_age(3600);
|
||||
App::new()
|
||||
let (app, mut api) = App::new()
|
||||
.wrap(cors)
|
||||
.wrap(Logger::default())
|
||||
.app_data(web::Data::new(state.clone()))
|
||||
.into_utoipa_app()
|
||||
.service(
|
||||
web::scope("api")
|
||||
scope::scope("/api")
|
||||
.configure(airports::init_routes)
|
||||
.configure(metars::init_routes)
|
||||
.configure(account::init_routes)
|
||||
.configure(users::init_routes)
|
||||
.configure(system::init_routes),
|
||||
)
|
||||
.split_for_parts();
|
||||
|
||||
let contact_name = env::var("API_CONTACT_NAME").unwrap();
|
||||
let contact_url = env::var("EXTERNAL_URL").unwrap();
|
||||
let contact_email = env::var("API_CONTACT_EMAIL").unwrap();
|
||||
let version = env::var("CARGO_PKG_VERSION").unwrap();
|
||||
|
||||
api.info.title = "Aviation Data".to_string();
|
||||
api.info.description = None;
|
||||
api.info.terms_of_service = None;
|
||||
api.info.contact = Some(
|
||||
Contact::builder()
|
||||
.name(Some(contact_name))
|
||||
.url(Some(format!("{}/support", contact_url)))
|
||||
.email(Some(contact_email))
|
||||
.build(),
|
||||
);
|
||||
api.info.license = None;
|
||||
api.info.version = version;
|
||||
|
||||
let session_scheme = SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new("session")));
|
||||
let mut components = api.components.take().unwrap_or_default();
|
||||
components
|
||||
.security_schemes
|
||||
.insert("session_auth".to_string(), session_scheme);
|
||||
api.components = Some(components);
|
||||
// api.security = Some(vec![SecurityRequirement::new("session_auth", [""])]);
|
||||
api.security = Some(vec![SecurityRequirement::default()]);
|
||||
|
||||
app.service(SwaggerUi::new("/swagger/{_:.*}").url("/api-docs/openapi.json", api))
|
||||
})
|
||||
.bind(format!("{}:{}", host, port))
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::redis_async_connection;
|
||||
use crate::error::ApiResult;
|
||||
use crate::metars::Metar;
|
||||
use chrono::{DateTime, Utc};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MetarCheck {
|
||||
|
||||
@@ -2,6 +2,6 @@ mod metar_check;
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
pub use model::*;
|
||||
pub use metar_check::*;
|
||||
pub use model::*;
|
||||
pub use routes::init_routes;
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
use crate::airports::{Airport, UpdateAirport};
|
||||
use crate::db::redis_async_connection;
|
||||
use crate::error::Error;
|
||||
use crate::{error::ApiResult, db};
|
||||
use crate::metars::MetarCheck;
|
||||
use crate::{db, error::ApiResult};
|
||||
use chrono::{DateTime, Datelike, NaiveDate, Utc};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::airports::{Airport, UpdateAirport};
|
||||
use crate::db::redis_async_connection;
|
||||
use crate::metars::MetarCheck;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
const TABLE_NAME: &str = "metars";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Metar {
|
||||
pub icao: String,
|
||||
pub raw_text: String,
|
||||
@@ -60,7 +61,7 @@ pub struct Metar {
|
||||
pub density_altitude: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub enum ReportModifier {
|
||||
#[serde(rename = "AUTO")]
|
||||
Auto,
|
||||
@@ -88,7 +89,7 @@ impl Display for ReportModifier {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RunwayVisualRange {
|
||||
pub runway: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -110,7 +111,7 @@ impl Default for RunwayVisualRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub enum AutomatedStationType {
|
||||
#[serde(rename = "AO1")]
|
||||
WithoutPrecipitationDiscriminator,
|
||||
@@ -141,7 +142,7 @@ impl Display for AutomatedStationType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Remarks {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub peak_wind: Option<PeakWind>,
|
||||
@@ -165,7 +166,7 @@ pub struct Remarks {
|
||||
pub sky_condition_at_secondary_location_not_available: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct PeakWind {
|
||||
pub degrees: i32,
|
||||
pub speed: i32,
|
||||
@@ -190,7 +191,7 @@ impl Default for Remarks {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SkyCondition {
|
||||
pub sky_cover: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -209,7 +210,7 @@ impl Default for SkyCondition {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub enum FlightCategory {
|
||||
VFR,
|
||||
MVFR,
|
||||
@@ -1134,8 +1135,8 @@ impl Metar {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::NaiveDateTime;
|
||||
use super::*;
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
#[test]
|
||||
fn test_parse_time() {
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
use crate::AppState;
|
||||
use crate::metars::Metar;
|
||||
use actix_web::{get, web, HttpResponse, HttpRequest};
|
||||
use actix_web::{HttpRequest, HttpResponse, get, web};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::AppState;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use utoipa_actix_web::service_config::ServiceConfig;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct FindAllParameters {
|
||||
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
struct MetarQuery {
|
||||
icaos: Option<String>,
|
||||
}
|
||||
|
||||
#[get("metars")]
|
||||
#[utoipa::path(
|
||||
tag = "METARs",
|
||||
params(
|
||||
MetarQuery,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "", body = [Metar]),
|
||||
),
|
||||
)]
|
||||
#[get("/metars")]
|
||||
async fn find_all(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let parameters = web::Query::<FindAllParameters>::from_query(req.query_string()).unwrap();
|
||||
let parameters = web::Query::<MetarQuery>::from_query(req.query_string()).unwrap();
|
||||
let icao_option = ¶meters.icaos;
|
||||
if let None = icao_option {
|
||||
let empty_metars: Vec<Metar> = vec![];
|
||||
return HttpResponse::Ok().json(empty_metars);
|
||||
}
|
||||
let icao_string = match icao_option {
|
||||
Some(i) => i,
|
||||
None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter"),
|
||||
@@ -30,6 +46,6 @@ async fn find_all(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok().json(metars)
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
pub fn init_routes(config: &mut ServiceConfig) {
|
||||
config.service(find_all);
|
||||
}
|
||||
|
||||
103
api/src/smtp/mod.rs
Normal file
103
api/src/smtp/mod.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use crate::error::ApiResult;
|
||||
use chrono::{Datelike, Utc};
|
||||
use handlebars::Handlebars;
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::message::{Mailbox, MultiPart, SinglePart};
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Address, Message, SmtpTransport, Transport};
|
||||
use serde::Serialize;
|
||||
use std::env;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static MAILER: OnceLock<SmtpTransport> = OnceLock::new();
|
||||
static FROM_ADDRESS: OnceLock<Mailbox> = OnceLock::new();
|
||||
static REGISTRY: OnceLock<Handlebars> = OnceLock::new();
|
||||
|
||||
fn mailer() -> &'static SmtpTransport {
|
||||
MAILER.get_or_init(|| {
|
||||
let server = env::var("SMTP_SERVER").expect("SMTP_SERVER missing");
|
||||
let username = env::var("SMTP_USERNAME").expect("SMTP_USERNAME missing");
|
||||
let password = env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD missing");
|
||||
let creds = Credentials::new(username, password);
|
||||
SmtpTransport::relay(&server)
|
||||
.expect("invalid SMTP_SERVER")
|
||||
.credentials(creds)
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
fn from_address() -> &'static Mailbox {
|
||||
FROM_ADDRESS.get_or_init(|| {
|
||||
let raw = env::var("SMTP_FROM").expect("SMTP_FROM missing");
|
||||
let addr = raw.parse().expect("SMTP_FROM invalid");
|
||||
Mailbox::new(Some("Aviation Data".into()), addr)
|
||||
})
|
||||
}
|
||||
|
||||
fn registry() -> &'static Handlebars<'static> {
|
||||
REGISTRY.get_or_init(|| Handlebars::new())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PasswordResetCtx {
|
||||
logo_url: String,
|
||||
link: String,
|
||||
domain: String,
|
||||
year: i32,
|
||||
}
|
||||
|
||||
pub fn send_password_reset(to: &str, token: &str) -> ApiResult<()> {
|
||||
let base_url = env::var("EXTERNAL_URL")?.trim_end_matches('/').to_string();
|
||||
let link = format!("{base_url}/profile/reset?token={token}");
|
||||
let subject = "Reset your password";
|
||||
|
||||
let plain = format!(
|
||||
"Hello,\n\n\
|
||||
We received a password reset request. Click the link below:\n\n\
|
||||
{link}\n\n\
|
||||
This link expires in 24 hours. If you didn't request this, please ignore.\n\n\
|
||||
Cheers,\n\
|
||||
\tAviation Data",
|
||||
link = link
|
||||
);
|
||||
|
||||
let ctx = PasswordResetCtx {
|
||||
logo_url: format!("{}/logo.svg", base_url),
|
||||
link: link.clone(),
|
||||
domain: base_url,
|
||||
year: Utc::now().year(),
|
||||
};
|
||||
|
||||
let template_html = include_str!("../.././templates/password_reset.html");
|
||||
let html = registry().render_template(template_html, &ctx).unwrap();
|
||||
|
||||
send_email(to, subject, plain, html)
|
||||
}
|
||||
|
||||
pub fn send_email(to: &str, subject: &str, header: String, html: String) -> ApiResult<()> {
|
||||
let to_address = to.parse::<Address>()?;
|
||||
let to_mailbox = Mailbox::new(None, to_address);
|
||||
|
||||
// Build the email
|
||||
let email = Message::builder()
|
||||
.from(from_address().clone())
|
||||
.to(to_mailbox)
|
||||
.subject(subject)
|
||||
.multipart(
|
||||
MultiPart::alternative()
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(header),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html),
|
||||
),
|
||||
)?;
|
||||
|
||||
// Send the email
|
||||
mailer().send(&email)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
use std::env;
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use actix_web::{HttpResponse, get};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use utoipa::ToSchema;
|
||||
use utoipa_actix_web::scope;
|
||||
use utoipa_actix_web::service_config::ServiceConfig;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SystemInfo {
|
||||
version: String,
|
||||
healthy: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "System",
|
||||
responses(
|
||||
(status = 200, description = "Successful system info"),
|
||||
)
|
||||
)]
|
||||
#[get("/info")]
|
||||
async fn info() -> HttpResponse {
|
||||
let mut healthy = true;
|
||||
@@ -24,6 +33,6 @@ async fn info() -> HttpResponse {
|
||||
HttpResponse::Ok().json(info)
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
config.service(web::scope("/system").service(info));
|
||||
pub fn init_routes(config: &mut ServiceConfig) {
|
||||
config.service(scope::scope("/system").service(info));
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use crate::db;
|
||||
use crate::{account::hash, error::ApiResult};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use crate::{account::hash, error::ApiResult};
|
||||
use crate::db;
|
||||
|
||||
pub const ADMIN_ROLE: &str = "ADMIN";
|
||||
pub const USER_ROLE: &str = "USER";
|
||||
const TABLE_NAME: &str = "users";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct RegisterRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
@@ -35,13 +37,21 @@ impl RegisterRequest {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[schema(
|
||||
example = json!(
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "changeme"
|
||||
}
|
||||
)
|
||||
)]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct UserResponse {
|
||||
pub id: Uuid,
|
||||
pub role: String,
|
||||
@@ -65,7 +75,7 @@ impl From<User> for UserResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, sqlx::FromRow)]
|
||||
#[derive(Debug, Deserialize, sqlx::FromRow, ToSchema)]
|
||||
pub struct UpdateUser {
|
||||
pub email: Option<String>,
|
||||
pub email_verified: Option<bool>,
|
||||
@@ -167,13 +177,13 @@ impl User {
|
||||
"#,
|
||||
TABLE_NAME
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Unable to find user by id '{}': {}", id, err);
|
||||
None
|
||||
});
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Unable to find user by id '{}': {}", id, err);
|
||||
None
|
||||
});
|
||||
|
||||
user
|
||||
}
|
||||
|
||||
@@ -152,7 +152,9 @@
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn init_routes(_config: &mut actix_web::web::ServiceConfig) {
|
||||
use utoipa_actix_web::service_config::ServiceConfig;
|
||||
|
||||
pub fn init_routes(_config: &mut ServiceConfig) {
|
||||
// config.service(
|
||||
// web::scope("users")
|
||||
// .service(get_favorites)
|
||||
|
||||
Reference in New Issue
Block a user