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"] }
serde = { version = "1.0.217", default-features = false, features = ["derive"] }
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"] }
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"] }
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-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"
database = "rust_forum"
backend = "postgres"
connection_pool_size = "100"
connection_pool_size = 100
default_user_role_id = 10

View file

@ -1,3 +1,3 @@
[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,
name VARCHAR(256) NOT NULL,
surname VARCHAR(256) NOT NULL,
gender boolean NOT NULL,
gender BOOLEAN 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()
);

View file

@ -3,3 +3,5 @@ CREATE TABLE IF NOT EXISTS "contact"(
id BIGSERIAL PRIMARY KEY 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"(
user_id BIGSERIAL NOT NULL REFERENCES "user"(user_id),
contact_id BIGSERIAL NOT NULL REFERENCES "contact"(id),
contact_value VARCHAR(256) NOT NULL,
PRIMARY KEY (user_id, contact_id)
);

View file

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

View file

@ -2,16 +2,21 @@ use crate::feature::user_contact::UserContact;
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!(
UserContact,
r#"
INSERT INTO "user_contact"(user_id, contact_id)
VALUES ($1, $2)
INSERT INTO "user_contact"(user_id, contact_id, contact_value)
VALUES ($1, $2, $3)
RETURNING *
"#,
user_id,
contact_id,
contact_value,
)
.fetch_one(&*DATABASE_CONNECTIONS)
.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
"#,
user_id,
contact_id
contact_id,
)
.fetch_one(&*DATABASE_CONNECTIONS)
.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!(
UserContact,
r#"
UPDATE "user_contact" SET "contact_id" = $2 WHERE "user_id" = $1
RETURNING *
UPDATE "user_contact" SET "contact_value" = $3 WHERE "user_id" = $1 AND "contact_id" = $2 RETURNING *
"#,
user_id,
contact_id,
contact_value,
)
.fetch_one(&*DATABASE_CONNECTIONS)
.await

View file

@ -14,6 +14,11 @@ use super::user::User;
static TOKEN_META: LazyLock<TokenMeta> = LazyLock::new(TokenMeta::init);
#[derive(Debug, Serialize, Deserialize)]
pub struct CustomClaim {
pub user_id: i64,
}
pub struct TokenMeta {
token_key: HS256Key,
token_verification_options: Option<VerificationOptions>,
@ -33,9 +38,9 @@ impl TokenMeta {
async fn create_token(user_id: &i64) -> Option<String> {
let key = &TOKEN_META.token_key;
let custom_claim = CustomClaim { user_id: *user_id };
let claims = Claims::with_custom_claims(
*user_id,
custom_claim,
jwt_simple::prelude::Duration::from_mins(
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> {
let token_meta = &TOKEN_META;
token_meta
pub async fn verify_token(token: &String) -> Result<JWTClaims<CustomClaim>, jwt_simple::Error> {
TOKEN_META
.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 user_id: i64,
pub contact_id: i64,
pub contact_value: String,
}
impl UserContact {
pub async fn create(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> {
user_contact::create(user_id, contact_id).await
pub async fn create(
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> {
user_contact::read(user_id, contact_id).await
}
pub async fn update(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx::Error> {
user_contact::update(user_id, contact_id).await
pub async fn update(
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> {

View file

@ -57,6 +57,11 @@ impl Default for ServerConfig {
fn default() -> Self {
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_semaphore_max = |value: String| {
value
.parse()
.map_or(tokio::sync::Semaphore::MAX_PERMITS, |value| value)
};
if header == "[server_config]" {
Self {
@ -66,7 +71,7 @@ impl Default for ServerConfig {
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 {
panic!("Server Config File Must Include [server_config] at the First Line")

View file

@ -66,7 +66,7 @@ impl MailTemplate {
MailTemplate::OneTimePassword(mail_fields) => {
let mut mail_template_from_file =
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() {
Some(subject) => subject,
None => return Err(ForumMailError::TemplateLackOfParameter),
@ -106,7 +106,7 @@ async fn send_mail(
)
.to(format!("<{}>", receiver).parse().unwrap())
.subject(subject)
.header(ContentType::TEXT_PLAIN)
.header(ContentType::TEXT_HTML)
.body(body.to_owned())
.unwrap();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,14 @@ use axum::{
};
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)]
struct CreateLogin {
@ -22,15 +29,34 @@ struct UpdateLogin {
pub fn route() -> Router {
Router::new()
.route("/one_time_password", post(create_one_time_password))
.route("/", post(create))
.route("/users/:user_id/token/:token", get(read))
.route("/users/{user_id}/tokens/{token}", get(read))
.route("/", patch(update))
.route("/users/:user_id/token/:token", delete(delete_))
.route("/users/:user_id", get(read_all_for_user))
.route("/users/:user_id", delete(delete_all_for_user))
.route("/count/users/:user_id", get(count_all_for_user))
.route("/users/{user_id}/tokens/{token}", delete(delete_))
.route("/users/{user_id}", get(read_all_for_user))
.route("/users/{user_id}", delete(delete_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 {
match OneTimePassword::verify(&create_login.one_time_password).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 {
match Login::update(&update_role.user_id, &update_role.token).await {
async fn update(Json(update_login): Json<UpdateLogin>) -> impl IntoResponse {
match Login::update(&update_login.user_id, &update_login.token).await {
Ok(login) => (StatusCode::ACCEPTED, Json(serde_json::json!(login))),
Err(err_val) => (
StatusCode::BAD_REQUEST,

View file

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

View file

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

View file

@ -28,10 +28,10 @@ struct UpdatePostInteraction {
pub fn route() -> Router {
Router::new()
.route("/", post(create))
.route("/:interaction_time", get(read))
.route("/{interaction_time}", get(read))
.route("/", patch(update))
.route("/:interaction_time", delete(delete_))
.route("/posts/:post_creation_time", get(read_all_for_post))
.route("/{interaction_time}", delete(delete_))
.route("/posts/{post_creation_time}", get(read_all_for_post))
}
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 super::middleware;
#[derive(Debug, Serialize, Deserialize)]
struct CreateRole {
name: String,
@ -25,15 +23,10 @@ struct UpdateRole {
pub fn route() -> Router {
Router::new()
.route("/", post(create))
.route_layer(axum::middleware::from_fn(middleware::pass_builder_or_admin))
.route("/:id", get(read))
.route_layer(axum::middleware::from_fn(middleware::pass))
.route("/{id}", get(read))
.route("/", patch(update))
.route_layer(axum::middleware::from_fn(middleware::pass_builder_or_admin))
.route("/:id", delete(delete_))
.route_layer(axum::middleware::from_fn(middleware::pass_builder_or_admin))
.route("/{id}", delete(delete_))
.route("/", get(read_all))
.route_layer(axum::middleware::from_fn(middleware::pass))
}
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 super::middleware;
#[derive(Debug, Serialize, Deserialize)]
struct CreateUser {
name: String,
@ -31,33 +33,45 @@ struct UpdateUser {
pub fn route() -> Router {
Router::new()
.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(
"/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),
)
.route("/ids/roles/:role", get(read_all_id_for_role))
.route("/ids/genders/:gender", get(read_all_id_for_gender))
.route("/ids/roles/{role}", get(read_all_id_for_role))
.route("/ids/genders/{gender}", get(read_all_id_for_gender))
.route("/count", get(count_all))
.route("/count/names/:name", get(count_all_for_name))
.route("/count/surnames/:surname", get(count_all_for_surname))
.route("/count/names/{name}", get(count_all_for_name))
.route("/count/surnames/{surname}", get(count_all_for_surname))
.route(
"/count/birth_dates/:birth_date",
"/count/birth_dates/{birth_date}",
get(count_all_for_birth_date),
)
.route("/count/roles/:role", get(count_all_for_role))
.route("/count/genders/:gender", get(count_all_for_gender))
.route("/count/roles/{role}", get(count_all_for_role))
.route("/count/genders/{gender}", get(count_all_for_gender))
}
async fn create(Json(create_user): Json<CreateUser>) -> impl IntoResponse {

View file

@ -13,28 +13,31 @@ use crate::feature::user_contact::UserContact;
struct CreateUserContact {
pub user_id: i64,
pub contact_id: i64,
pub contact_value: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct UpdateUserContact {
pub user_id: i64,
pub contact_id: i64,
pub contact_value: String,
}
pub fn route() -> Router {
Router::new()
.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("/roles/:user_id/contacts/:contact_id", delete(delete_))
.route("/users/:user_id", get(read_all_for_user))
.route("/users/:user_id", delete(delete_all_for_user))
.route("/roles/{user_id}/contacts/{contact_id}", delete(delete_))
.route("/users/{user_id}", get(read_all_for_user))
.route("/users/{user_id}", delete(delete_all_for_user))
}
async fn create(Json(create_user_contact): Json<CreateUserContact>) -> impl IntoResponse {
match UserContact::create(
&create_user_contact.user_id,
&create_user_contact.contact_id,
&create_user_contact.contact_value,
)
.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 {
match UserContact::update(&update_role.user_id, &update_role.contact_id).await {
async fn update(Json(update_user_contact): Json<UpdateUserContact>) -> impl IntoResponse {
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))),
Err(err_val) => (
StatusCode::BAD_REQUEST,