feat: tracing system

fix: 🚑 user_contact is not containing value
feat:  html mail system
refactor: ♻️ axum 0.8 routing compatibility
fix: 🚑 custom claim isn't working in jwt
feat:  token header extended compatibility
This commit is contained in:
Ahmet Kaan GÜMÜŞ 2025-01-23 21:34:54 +03:00
parent 3f2aa572a6
commit 0bb5a0b753
25 changed files with 182 additions and 99 deletions

View file

@ -18,10 +18,13 @@ strip = "symbols"
tokio = { version = "1.43.0", default-features = false,features = ["macros", "rt-multi-thread", "time"] } tokio = { version = "1.43.0", default-features = false,features = ["macros", "rt-multi-thread", "time"] }
serde = { version = "1.0.217", default-features = false, features = ["derive"] } serde = { version = "1.0.217", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.135" , default-features = false} serde_json = { version = "1.0.135" , default-features = false}
axum = { version = "0.8.1", default-features = false, features = ["http2", "json", "tokio"]} axum = { version = "0.8.1", default-features = false, features = ["http1", "json", "tokio"]}
chrono = { version = "0.4.39", default-features = false, features = ["serde"] } chrono = { version = "0.4.39", default-features = false, features = ["serde"] }
jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] } jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] }
lettre = { version = "0.11.11", default-features = false, features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"] } lettre = { version = "0.11.11", default-features = false, features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"] }
sqlx = { version = "0.8.3", default-features = false, features = ["chrono", "macros", "migrate", "postgres", "runtime-tokio-rustls"] } sqlx = { version = "0.8.3", default-features = false, features = ["chrono", "macros", "migrate", "postgres", "runtime-tokio-rustls"] }
tower = { version = "0.5.2", default-features = false, features = ["limit"] } tower = { version = "0.5.2", default-features = false, features = ["limit"] }
tower-http = { version = "0.6.2", default-features = false, features = ["cors"] } tower-http = { version = "0.6.2", default-features = false, features = ["cors", "trace"] }
axum-macros = "0.5.0"
tracing-subscriber = "0.3.19"
tracing = "0.1.41"

View file

@ -4,4 +4,5 @@ username = "root"
password = "root" password = "root"
database = "rust_forum" database = "rust_forum"
backend = "postgres" backend = "postgres"
connection_pool_size = "100" connection_pool_size = 100
default_user_role_id = 10

View file

@ -1,3 +1,3 @@
[one_time_password] [one_time_password]
subject = "Your One Time Password" subject = "Your One Time Password"
body = "Dear *, \nHere is your One Time Password = * \n Best Wishes" body = "Dear *, <br><br>Here is your One Time Password = * <br><br>Best Wishes"

View file

@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS "user"(
user_id BIGSERIAL PRIMARY KEY NOT NULL UNIQUE, user_id BIGSERIAL PRIMARY KEY NOT NULL UNIQUE,
name VARCHAR(256) NOT NULL, name VARCHAR(256) NOT NULL,
surname VARCHAR(256) NOT NULL, surname VARCHAR(256) NOT NULL,
gender boolean NOT NULL, gender BOOLEAN NOT NULL,
birth_date DATE NOT NULL, birth_date DATE NOT NULL,
role_id BIGSERIAL NOT NULL REFERENCES "role"(id), role_id BIGINT NOT NULL REFERENCES "role" DEFAULT 10,
creation_time TIMESTAMPTZ NOT NULL DEFAULT NOW() creation_time TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );

View file

@ -2,4 +2,6 @@
CREATE TABLE IF NOT EXISTS "contact"( CREATE TABLE IF NOT EXISTS "contact"(
id BIGSERIAL PRIMARY KEY NOT NULL UNIQUE, id BIGSERIAL PRIMARY KEY NOT NULL UNIQUE,
name VARCHAR(32) NOT NULL UNIQUE name VARCHAR(32) NOT NULL UNIQUE
); );
INSERT INTO "contact"(id, name) VALUES (0, 'Email') ON CONFLICT(id) DO UPDATE SET "name" = 'Email';

View file

@ -2,5 +2,6 @@
CREATE TABLE IF NOT EXISTS "user_contact"( CREATE TABLE IF NOT EXISTS "user_contact"(
user_id BIGSERIAL NOT NULL REFERENCES "user"(user_id), user_id BIGSERIAL NOT NULL REFERENCES "user"(user_id),
contact_id BIGSERIAL NOT NULL REFERENCES "contact"(id), contact_id BIGSERIAL NOT NULL REFERENCES "contact"(id),
contact_value VARCHAR(256) NOT NULL,
PRIMARY KEY (user_id, contact_id) PRIMARY KEY (user_id, contact_id)
); );

View file

@ -13,15 +13,14 @@ pub async fn create(
sqlx::query_as!( sqlx::query_as!(
User, User,
r#" r#"
INSERT INTO "user"(name, surname, gender, birth_date, role_id) INSERT INTO "user"(name, surname, gender, birth_date)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4)
RETURNING * RETURNING *
"#, "#,
name, name,
surname, surname,
gender, gender,
birth_date, birth_date,
2
) )
.fetch_one(&*DATABASE_CONNECTIONS) .fetch_one(&*DATABASE_CONNECTIONS)
.await .await

View file

@ -2,16 +2,21 @@ use crate::feature::user_contact::UserContact;
use super::DATABASE_CONNECTIONS; use super::DATABASE_CONNECTIONS;
pub async fn create(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> { pub async fn create(
user_id: &i64,
contact_id: &i64,
contact_value: &String,
) -> Result<UserContact, sqlx::Error> {
sqlx::query_as!( sqlx::query_as!(
UserContact, UserContact,
r#" r#"
INSERT INTO "user_contact"(user_id, contact_id) INSERT INTO "user_contact"(user_id, contact_id, contact_value)
VALUES ($1, $2) VALUES ($1, $2, $3)
RETURNING * RETURNING *
"#, "#,
user_id, user_id,
contact_id, contact_id,
contact_value,
) )
.fetch_one(&*DATABASE_CONNECTIONS) .fetch_one(&*DATABASE_CONNECTIONS)
.await .await
@ -24,21 +29,25 @@ pub async fn read(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::
SELECT * FROM "user_contact" WHERE "user_id" = $1 AND "contact_id" = $2 SELECT * FROM "user_contact" WHERE "user_id" = $1 AND "contact_id" = $2
"#, "#,
user_id, user_id,
contact_id contact_id,
) )
.fetch_one(&*DATABASE_CONNECTIONS) .fetch_one(&*DATABASE_CONNECTIONS)
.await .await
} }
pub async fn update(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> { pub async fn update(
user_id: &i64,
contact_id: &i64,
contact_value: &String,
) -> Result<UserContact, sqlx::Error> {
sqlx::query_as!( sqlx::query_as!(
UserContact, UserContact,
r#" r#"
UPDATE "user_contact" SET "contact_id" = $2 WHERE "user_id" = $1 UPDATE "user_contact" SET "contact_value" = $3 WHERE "user_id" = $1 AND "contact_id" = $2 RETURNING *
RETURNING *
"#, "#,
user_id, user_id,
contact_id, contact_id,
contact_value,
) )
.fetch_one(&*DATABASE_CONNECTIONS) .fetch_one(&*DATABASE_CONNECTIONS)
.await .await

View file

@ -14,6 +14,11 @@ use super::user::User;
static TOKEN_META: LazyLock<TokenMeta> = LazyLock::new(TokenMeta::init); static TOKEN_META: LazyLock<TokenMeta> = LazyLock::new(TokenMeta::init);
#[derive(Debug, Serialize, Deserialize)]
pub struct CustomClaim {
pub user_id: i64,
}
pub struct TokenMeta { pub struct TokenMeta {
token_key: HS256Key, token_key: HS256Key,
token_verification_options: Option<VerificationOptions>, token_verification_options: Option<VerificationOptions>,
@ -33,9 +38,9 @@ impl TokenMeta {
async fn create_token(user_id: &i64) -> Option<String> { async fn create_token(user_id: &i64) -> Option<String> {
let key = &TOKEN_META.token_key; let key = &TOKEN_META.token_key;
let custom_claim = CustomClaim { user_id: *user_id };
let claims = Claims::with_custom_claims( let claims = Claims::with_custom_claims(
*user_id, custom_claim,
jwt_simple::prelude::Duration::from_mins( jwt_simple::prelude::Duration::from_mins(
SERVER_CONFIG.login_token_expiration_time_limit as u64, SERVER_CONFIG.login_token_expiration_time_limit as u64,
), ),
@ -48,11 +53,10 @@ impl TokenMeta {
} }
} }
pub async fn verify_token(token: &String) -> Result<JWTClaims<i64>, jwt_simple::Error> { pub async fn verify_token(token: &String) -> Result<JWTClaims<CustomClaim>, jwt_simple::Error> {
let token_meta = &TOKEN_META; TOKEN_META
token_meta
.token_key .token_key
.verify_token::<i64>(token, token_meta.token_verification_options.clone()) .verify_token::<CustomClaim>(token, TOKEN_META.token_verification_options.clone())
} }
} }

View file

@ -6,19 +6,28 @@ use crate::database::user_contact;
pub struct UserContact { pub struct UserContact {
pub user_id: i64, pub user_id: i64,
pub contact_id: i64, pub contact_id: i64,
pub contact_value: String,
} }
impl UserContact { impl UserContact {
pub async fn create(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> { pub async fn create(
user_contact::create(user_id, contact_id).await user_id: &i64,
contact_id: &i64,
contact_value: &String,
) -> Result<UserContact, sqlx::Error> {
user_contact::create(user_id, contact_id, contact_value).await
} }
pub async fn read(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> { pub async fn read(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> {
user_contact::read(user_id, contact_id).await user_contact::read(user_id, contact_id).await
} }
pub async fn update(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> { pub async fn update(
user_contact::update(user_id, contact_id).await user_id: &i64,
contact_id: &i64,
contact_value: &String,
) -> Result<UserContact, sqlx::Error> {
user_contact::update(user_id, contact_id, contact_value).await
} }
pub async fn delete(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> { pub async fn delete(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> {

View file

@ -57,6 +57,11 @@ impl Default for ServerConfig {
fn default() -> Self { fn default() -> Self {
let (header, mut server_configs) = naive_toml_parser(SERVER_CONFIG_FILE_LOCATION); let (header, mut server_configs) = naive_toml_parser(SERVER_CONFIG_FILE_LOCATION);
let value_or_max = |value: String| value.parse().map_or(usize::MAX, |value| value); let value_or_max = |value: String| value.parse().map_or(usize::MAX, |value| value);
let value_or_semaphore_max = |value: String| {
value
.parse()
.map_or(tokio::sync::Semaphore::MAX_PERMITS, |value| value)
};
if header == "[server_config]" { if header == "[server_config]" {
Self { Self {
@ -66,7 +71,7 @@ impl Default for ServerConfig {
server_configs.pop_front().unwrap(), server_configs.pop_front().unwrap(),
), ),
login_token_refresh_time_limit: value_or_max(server_configs.pop_front().unwrap()), login_token_refresh_time_limit: value_or_max(server_configs.pop_front().unwrap()),
concurrency_limit: value_or_max(server_configs.pop_front().unwrap()), concurrency_limit: value_or_semaphore_max(server_configs.pop_front().unwrap()),
} }
} else { } else {
panic!("Server Config File Must Include [server_config] at the First Line") panic!("Server Config File Must Include [server_config] at the First Line")

View file

@ -66,7 +66,7 @@ impl MailTemplate {
MailTemplate::OneTimePassword(mail_fields) => { MailTemplate::OneTimePassword(mail_fields) => {
let mut mail_template_from_file = let mut mail_template_from_file =
naive_toml_parser(ONE_TIME_PASSWORD_MAIL_TEMPLATE_FILE_LOCATION); naive_toml_parser(ONE_TIME_PASSWORD_MAIL_TEMPLATE_FILE_LOCATION);
if mail_template_from_file.0 == "one_time_password" { if mail_template_from_file.0 == "[one_time_password]" {
let subject = match mail_template_from_file.1.pop_front() { let subject = match mail_template_from_file.1.pop_front() {
Some(subject) => subject, Some(subject) => subject,
None => return Err(ForumMailError::TemplateLackOfParameter), None => return Err(ForumMailError::TemplateLackOfParameter),
@ -106,7 +106,7 @@ async fn send_mail(
) )
.to(format!("<{}>", receiver).parse().unwrap()) .to(format!("<{}>", receiver).parse().unwrap())
.subject(subject) .subject(subject)
.header(ContentType::TEXT_PLAIN) .header(ContentType::TEXT_HTML)
.body(body.to_owned()) .body(body.to_owned())
.unwrap(); .unwrap();

View file

@ -1,8 +1,11 @@
use rust_forum::server::start_server; use rust_forum::server::start_server;
use tracing::Level;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
println!("Hello, world!"); println!("Hello, world!");
tracing_subscriber::fmt()
.with_max_level(Level::TRACE)
.init();
start_server().await; start_server().await;
} }

View file

@ -12,13 +12,14 @@ pub mod user_contact;
use axum::{http::StatusCode, response::IntoResponse, routing::get, Router}; use axum::{http::StatusCode, response::IntoResponse, routing::get, Router};
use tower::limit::ConcurrencyLimitLayer; use tower::limit::ConcurrencyLimitLayer;
use tower_http::cors::CorsLayer; use tower_http::{cors::CorsLayer, trace::TraceLayer};
use crate::database; use crate::database;
pub async fn route(concurrency_limit: &usize) -> Router { pub async fn route(concurrency_limit: &usize) -> Router {
Router::new() Router::new()
.route("/", get(alive)) .route("/", get(alive))
.nest("/logins", login::route())
.nest("/roles", role::route()) .nest("/roles", role::route())
.nest("/users", user::route()) .nest("/users", user::route())
.nest("/posts", post::route()) .nest("/posts", post::route())
@ -30,6 +31,7 @@ pub async fn route(concurrency_limit: &usize) -> Router {
.nest("/user_contacts", user_contact::route()) .nest("/user_contacts", user_contact::route())
.layer(CorsLayer::permissive()) .layer(CorsLayer::permissive())
.layer(ConcurrencyLimitLayer::new(*concurrency_limit)) .layer(ConcurrencyLimitLayer::new(*concurrency_limit))
.layer(TraceLayer::new_for_http())
} }
pub async fn alive() -> impl IntoResponse { pub async fn alive() -> impl IntoResponse {

View file

@ -26,10 +26,10 @@ struct UpdateComment {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route("/:creation_time", get(read)) .route("/{creation_time}", get(read))
.route("/", patch(update)) .route("/", patch(update))
.route("/:creation_time", delete(delete_)) .route("/{creation_time}", delete(delete_))
.route("/posts/:post_creation_time", get(read_all_for_post)) .route("/posts/{post_creation_time}", get(read_all_for_post))
} }
async fn create(Json(create_comment): Json<CreateComment>) -> impl IntoResponse { async fn create(Json(create_comment): Json<CreateComment>) -> impl IntoResponse {

View file

@ -28,11 +28,11 @@ struct UpdateCommentInteraction {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route("/:interaction_time", get(read)) .route("/{interaction_time}", get(read))
.route("/", patch(update)) .route("/", patch(update))
.route("/:interaction_time", delete(delete_)) .route("/{interaction_time}", delete(delete_))
.route( .route(
"/comments/:comment_creation_time", "/comments/{comment_creation_time}",
get(read_all_for_comment), get(read_all_for_comment),
) )
} }

View file

@ -23,9 +23,9 @@ struct UpdateContact {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route("/:id", get(read)) .route("/{id}", get(read))
.route("/", patch(update)) .route("/", patch(update))
.route("/:id", delete(delete_)) .route("/{id}", delete(delete_))
.route("/", get(read_all)) .route("/", get(read_all))
} }

View file

@ -23,9 +23,9 @@ struct UpdateInteraction {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route("/:id", get(read)) .route("/{id}", get(read))
.route("/", patch(update)) .route("/", patch(update))
.route("/:id", delete(delete_)) .route("/{id}", delete(delete_))
.route("/", get(read_all)) .route("/", get(read_all))
} }

View file

@ -7,7 +7,14 @@ use axum::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::feature::{auth::OneTimePassword, login::Login}; use crate::feature::{auth::OneTimePassword, login::Login, user::User, user_contact::UserContact};
const CONTACT_EMAIL_DEFAULT_ID: i64 = 0;
#[derive(Debug, Serialize, Deserialize)]
struct CreateOneTimePassword {
pub user_id: i64,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct CreateLogin { struct CreateLogin {
@ -22,15 +29,34 @@ struct UpdateLogin {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/one_time_password", post(create_one_time_password))
.route("/", post(create)) .route("/", post(create))
.route("/users/:user_id/token/:token", get(read)) .route("/users/{user_id}/tokens/{token}", get(read))
.route("/", patch(update)) .route("/", patch(update))
.route("/users/:user_id/token/:token", delete(delete_)) .route("/users/{user_id}/tokens/{token}", delete(delete_))
.route("/users/:user_id", get(read_all_for_user)) .route("/users/{user_id}", get(read_all_for_user))
.route("/users/:user_id", delete(delete_all_for_user)) .route("/users/{user_id}", delete(delete_all_for_user))
.route("/count/users/:user_id", get(count_all_for_user)) .route("/count/users/{user_id}", get(count_all_for_user))
}
async fn create_one_time_password(
Json(create_one_time_password): Json<CreateOneTimePassword>,
) -> impl IntoResponse {
//todo get user from middleware or something
let user = User::read(&create_one_time_password.user_id).await.unwrap();
match UserContact::read(&user.user_id, &CONTACT_EMAIL_DEFAULT_ID).await {
Ok(user_email) => match OneTimePassword::new(&user, &user_email.contact_value).await {
Ok(_) => (StatusCode::CREATED, Json(serde_json::json!(""))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
},
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
} }
async fn create(Json(create_login): Json<CreateLogin>) -> impl IntoResponse { async fn create(Json(create_login): Json<CreateLogin>) -> impl IntoResponse {
match OneTimePassword::verify(&create_login.one_time_password).await { match OneTimePassword::verify(&create_login.one_time_password).await {
true => match Login::create(&create_login.one_time_password.user_id).await { true => match Login::create(&create_login.one_time_password.user_id).await {
@ -59,8 +85,8 @@ async fn read(Path((user_id, token)): Path<(i64, String)>) -> impl IntoResponse
} }
} }
async fn update(Json(update_role): Json<UpdateLogin>) -> impl IntoResponse { async fn update(Json(update_login): Json<UpdateLogin>) -> impl IntoResponse {
match Login::update(&update_role.user_id, &update_role.token).await { match Login::update(&update_login.user_id, &update_login.token).await {
Ok(login) => (StatusCode::ACCEPTED, Json(serde_json::json!(login))), Ok(login) => (StatusCode::ACCEPTED, Json(serde_json::json!(login))),
Err(err_val) => ( Err(err_val) => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,

View file

@ -33,14 +33,17 @@ async fn user_extraction(request: Request) -> Option<UserAndRequest> {
if let Some(authorization_header) = request.headers().get(http::header::AUTHORIZATION) { if let Some(authorization_header) = request.headers().get(http::header::AUTHORIZATION) {
if let Ok(authorization_header) = authorization_header.to_str() { if let Ok(authorization_header) = authorization_header.to_str() {
if let Some((bearer, authorization_header)) = authorization_header.split_once(' ') { if let Some((bearer, authorization_header)) = authorization_header.split_once(' ') {
if bearer == "bearer" { if bearer.to_lowercase() == "bearer" {
if let Ok(claims) = match TokenMeta::verify_token(&authorization_header.to_string()).await {
TokenMeta::verify_token(&authorization_header.to_string()).await Ok(claims) => {
{ return Some(UserAndRequest {
return Some(UserAndRequest { user: User::read(&claims.custom.user_id).await.ok()?,
user: User::read(&claims.custom).await.ok()?, request,
request, });
}); }
Err(err_val) => {
eprintln!("Verify Token | {}", err_val);
}
} }
} }
} }

View file

@ -26,9 +26,9 @@ struct UpdatePost {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route("/:creation_time", get(read)) .route("/{creation_time}", get(read))
.route("/", patch(update)) .route("/", patch(update))
.route("/:creation_time", delete(delete_)) .route("/{creation_time}", delete(delete_))
.route("/", get(read_all)) .route("/", get(read_all))
} }

View file

@ -28,10 +28,10 @@ struct UpdatePostInteraction {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route("/:interaction_time", get(read)) .route("/{interaction_time}", get(read))
.route("/", patch(update)) .route("/", patch(update))
.route("/:interaction_time", delete(delete_)) .route("/{interaction_time}", delete(delete_))
.route("/posts/:post_creation_time", get(read_all_for_post)) .route("/posts/{post_creation_time}", get(read_all_for_post))
} }
async fn create(Json(create_post_interaction): Json<CreatePostInteraction>) -> impl IntoResponse { async fn create(Json(create_post_interaction): Json<CreatePostInteraction>) -> impl IntoResponse {

View file

@ -9,8 +9,6 @@ use serde::{Deserialize, Serialize};
use crate::feature::role::Role; use crate::feature::role::Role;
use super::middleware;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct CreateRole { struct CreateRole {
name: String, name: String,
@ -25,15 +23,10 @@ struct UpdateRole {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route_layer(axum::middleware::from_fn(middleware::pass_builder_or_admin)) .route("/{id}", get(read))
.route("/:id", get(read))
.route_layer(axum::middleware::from_fn(middleware::pass))
.route("/", patch(update)) .route("/", patch(update))
.route_layer(axum::middleware::from_fn(middleware::pass_builder_or_admin)) .route("/{id}", delete(delete_))
.route("/:id", delete(delete_))
.route_layer(axum::middleware::from_fn(middleware::pass_builder_or_admin))
.route("/", get(read_all)) .route("/", get(read_all))
.route_layer(axum::middleware::from_fn(middleware::pass))
} }
async fn create(Json(create_role): Json<CreateRole>) -> impl IntoResponse { async fn create(Json(create_role): Json<CreateRole>) -> impl IntoResponse {

View file

@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
use crate::feature::user::User; use crate::feature::user::User;
use super::middleware;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct CreateUser { struct CreateUser {
name: String, name: String,
@ -31,33 +33,45 @@ struct UpdateUser {
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route("/:id", get(read))
.route("/", patch(update))
.route("/:id", delete(delete_))
.route("/", get(read_all))
.route("/names/:name", get(read_all_for_name))
.route("/surnames/:surname", get(read_all_for_surname))
.route("/birth_dates/:birth_date", get(read_all_for_birth_date))
.route("/roles/:role", get(read_all_for_role))
.route("/genders/:gender", get(read_all_for_gender))
.route("/ids", get(read_all_id))
.route("/ids/names/:name", get(read_all_id_for_name))
.route("/ids/surnames/:surname", get(read_all_id_for_surname))
.route( .route(
"/ids/birth_dates/:birth_date", "/{id}",
get(read).route_layer(axum::middleware::from_fn(middleware::pass)),
)
.route(
"/",
patch(update).route_layer(axum::middleware::from_fn(middleware::pass_higher_or_self)),
)
.route(
"/{id}",
delete(delete_).route_layer(axum::middleware::from_fn(middleware::pass_higher_or_self)),
)
.route(
"/",
get(read_all).route_layer(axum::middleware::from_fn(middleware::pass_builder_or_admin)),
)
.route("/names/{name}", get(read_all_for_name))
.route("/surnames/{surname}", get(read_all_for_surname))
.route("/birth_dates/{birth_date}", get(read_all_for_birth_date))
.route("/roles/{role}", get(read_all_for_role))
.route("/genders/{gender}", get(read_all_for_gender))
.route("/ids", get(read_all_id))
.route("/ids/names/{name}", get(read_all_id_for_name))
.route("/ids/surnames/{surname}", get(read_all_id_for_surname))
.route(
"/ids/birth_dates/{birth_date}",
get(read_all_id_for_birth_date), get(read_all_id_for_birth_date),
) )
.route("/ids/roles/:role", get(read_all_id_for_role)) .route("/ids/roles/{role}", get(read_all_id_for_role))
.route("/ids/genders/:gender", get(read_all_id_for_gender)) .route("/ids/genders/{gender}", get(read_all_id_for_gender))
.route("/count", get(count_all)) .route("/count", get(count_all))
.route("/count/names/:name", get(count_all_for_name)) .route("/count/names/{name}", get(count_all_for_name))
.route("/count/surnames/:surname", get(count_all_for_surname)) .route("/count/surnames/{surname}", get(count_all_for_surname))
.route( .route(
"/count/birth_dates/:birth_date", "/count/birth_dates/{birth_date}",
get(count_all_for_birth_date), get(count_all_for_birth_date),
) )
.route("/count/roles/:role", get(count_all_for_role)) .route("/count/roles/{role}", get(count_all_for_role))
.route("/count/genders/:gender", get(count_all_for_gender)) .route("/count/genders/{gender}", get(count_all_for_gender))
} }
async fn create(Json(create_user): Json<CreateUser>) -> impl IntoResponse { async fn create(Json(create_user): Json<CreateUser>) -> impl IntoResponse {

View file

@ -13,28 +13,31 @@ use crate::feature::user_contact::UserContact;
struct CreateUserContact { struct CreateUserContact {
pub user_id: i64, pub user_id: i64,
pub contact_id: i64, pub contact_id: i64,
pub contact_value: String,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct UpdateUserContact { struct UpdateUserContact {
pub user_id: i64, pub user_id: i64,
pub contact_id: i64, pub contact_id: i64,
pub contact_value: String,
} }
pub fn route() -> Router { pub fn route() -> Router {
Router::new() Router::new()
.route("/", post(create)) .route("/", post(create))
.route("/roles/:user_id/contacts/:contact_id", get(read)) .route("/roles/{user_id}/contacts/{contact_id}", get(read))
.route("/", patch(update)) .route("/", patch(update))
.route("/roles/:user_id/contacts/:contact_id", delete(delete_)) .route("/roles/{user_id}/contacts/{contact_id}", delete(delete_))
.route("/users/:user_id", get(read_all_for_user)) .route("/users/{user_id}", get(read_all_for_user))
.route("/users/:user_id", delete(delete_all_for_user)) .route("/users/{user_id}", delete(delete_all_for_user))
} }
async fn create(Json(create_user_contact): Json<CreateUserContact>) -> impl IntoResponse { async fn create(Json(create_user_contact): Json<CreateUserContact>) -> impl IntoResponse {
match UserContact::create( match UserContact::create(
&create_user_contact.user_id, &create_user_contact.user_id,
&create_user_contact.contact_id, &create_user_contact.contact_id,
&create_user_contact.contact_value,
) )
.await .await
{ {
@ -56,8 +59,14 @@ async fn read(Path((user_id, contact_id)): Path<(i64, i64)>) -> impl IntoRespons
} }
} }
async fn update(Json(update_role): Json<UpdateUserContact>) -> impl IntoResponse { async fn update(Json(update_user_contact): Json<UpdateUserContact>) -> impl IntoResponse {
match UserContact::update(&update_role.user_id, &update_role.contact_id).await { match UserContact::update(
&update_user_contact.user_id,
&update_user_contact.contact_id,
&update_user_contact.contact_value,
)
.await
{
Ok(user_contact) => (StatusCode::ACCEPTED, Json(serde_json::json!(user_contact))), Ok(user_contact) => (StatusCode::ACCEPTED, Json(serde_json::json!(user_contact))),
Err(err_val) => ( Err(err_val) => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,