feat: admin routing part 3

This commit is contained in:
Ahmet Kaan GÜMÜŞ 2025-01-27 17:17:13 +03:00
parent bf2b0a439c
commit 8fb39f27ca
14 changed files with 305 additions and 139 deletions

View file

@ -1,7 +1,7 @@
-- Add up migration script here
CREATE TABLE IF NOT EXISTS "login" (
user_id BIGSERIAL NOT NULL REFERENCES "user" (user_id),
token VARCHAR(1024) NOT NULL,
authorization_token VARCHAR(1024) NOT NULL,
token_creation_time TIMESTAMPTZ NOT NULL DEFAULT NOW (),
PRIMARY KEY (user_id, token)
PRIMARY KEY (user_id, authorization_token)
);

View file

@ -2,43 +2,43 @@ use crate::feature::login::Login;
use super::DATABASE_CONNECTIONS;
pub async fn create(user_id: &i64, token: &String) -> Result<Login, sqlx::Error> {
pub async fn create(user_id: &i64, authorization_token: &String) -> Result<Login, sqlx::Error> {
sqlx::query_as!(
Login,
r#"
INSERT INTO "login"(user_id, token)
INSERT INTO "login"(user_id, authorization_token)
VALUES ($1, $2)
RETURNING *
"#,
user_id,
token,
authorization_token,
)
.fetch_one(&*DATABASE_CONNECTIONS)
.await
}
pub async fn read(user_id: &i64, token: &String) -> Result<Login, sqlx::Error> {
pub async fn read(user_id: &i64, authorization_token: &String) -> Result<Login, sqlx::Error> {
sqlx::query_as!(
Login,
r#"
SELECT * FROM "login" WHERE "user_id" = $1 AND "token" = $2
SELECT * FROM "login" WHERE "user_id" = $1 AND "authorization_token" = $2
"#,
user_id,
token
authorization_token
)
.fetch_one(&*DATABASE_CONNECTIONS)
.await
}
pub async fn delete(user_id: &i64, token: &String) -> Result<Login, sqlx::Error> {
pub async fn delete(user_id: &i64, authorization_token: &String) -> Result<Login, sqlx::Error> {
sqlx::query_as!(
Login,
r#"
DELETE FROM "login" WHERE "user_id" = $1 AND "token" = $2
DELETE FROM "login" WHERE "user_id" = $1 AND "authorization_token" = $2
RETURNING *
"#,
user_id,
token,
authorization_token,
)
.fetch_one(&*DATABASE_CONNECTIONS)
.await

View file

@ -67,6 +67,22 @@ pub async fn delete(user_id: &i64, contact_id: &i64) -> Result<UserContact, sqlx
.await
}
pub async fn read_for_value(
contact_id: &i64,
contact_value: &String,
) -> Result<UserContact, sqlx::Error> {
sqlx::query_as!(
UserContact,
r#"
SELECT * FROM "user_contact" WHERE "contact_id" = $1 AND "contact_value" = $2
"#,
contact_id,
contact_value,
)
.fetch_one(&*DATABASE_CONNECTIONS)
.await
}
pub async fn read_all_for_user(user_id: &i64) -> Result<Vec<UserContact>, sqlx::Error> {
sqlx::query_as!(
UserContact,

View file

@ -15,7 +15,7 @@ static ONE_TIME_PASSWORDS: LazyLock<RwLock<Vec<OneTimePassword>>> =
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OneTimePassword {
pub user_id: i64,
user_id: i64,
pub one_time_password: String,
}
@ -24,6 +24,13 @@ impl OneTimePassword {
RwLock::new(vec![])
}
pub async fn from_string(user: &User, one_time_password: &String) -> Self {
OneTimePassword {
user_id: user.user_id,
one_time_password: one_time_password.to_owned(),
}
}
pub async fn new(user: &User, user_email: &String) -> Result<(), ForumMailError> {
let one_time_password = "123".to_owned();
let new_self = Self {

View file

@ -12,23 +12,23 @@ use crate::{database::login, error::ForumAuthError, SERVER_CONFIG};
use super::user::User;
static TOKEN_META: LazyLock<TokenMeta> = LazyLock::new(TokenMeta::init);
static TOKEN_META: LazyLock<AuthorizationTokenMeta> = LazyLock::new(AuthorizationTokenMeta::init);
#[derive(Debug, Serialize, Deserialize)]
pub struct CustomClaim {
pub user_id: i64,
}
pub struct TokenMeta {
token_key: HS256Key,
token_verification_options: Option<VerificationOptions>,
pub struct AuthorizationTokenMeta {
authorization_token: HS256Key,
authorization_token_verification_options: Option<VerificationOptions>,
}
impl TokenMeta {
impl AuthorizationTokenMeta {
fn init() -> Self {
Self {
token_key: HS256Key::generate(),
token_verification_options: {
authorization_token: HS256Key::generate(),
authorization_token_verification_options: {
let mut verification_options = VerificationOptions::default();
verification_options.time_tolerance = Some(jwt_simple::prelude::Duration::from(0));
Some(verification_options)
@ -37,7 +37,7 @@ impl TokenMeta {
}
async fn create_token(user_id: &i64) -> Option<String> {
let key = &TOKEN_META.token_key;
let key = &TOKEN_META.authorization_token;
let custom_claim = CustomClaim { user_id: *user_id };
let claims = Claims::with_custom_claims(
custom_claim,
@ -47,23 +47,24 @@ impl TokenMeta {
);
let token = key.authenticate(claims).unwrap();
match TokenMeta::verify_token(&token).await {
match AuthorizationTokenMeta::verify_token(&token).await {
Ok(_) => Some(token),
Err(_) => None,
}
}
pub async fn verify_token(token: &String) -> Result<JWTClaims<CustomClaim>, jwt_simple::Error> {
TOKEN_META
.token_key
.verify_token::<CustomClaim>(token, TOKEN_META.token_verification_options.clone())
TOKEN_META.authorization_token.verify_token::<CustomClaim>(
token,
TOKEN_META.authorization_token_verification_options.clone(),
)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Login {
pub user_id: i64,
pub token: String,
pub authorization_token: String,
pub token_creation_time: DateTime<Utc>,
}
@ -71,24 +72,24 @@ impl Login {
pub async fn create(user_id: &i64) -> Result<Login, sqlx::Error> {
User::read(user_id).await?;
let token = TokenMeta::create_token(user_id)
let token = AuthorizationTokenMeta::create_token(user_id)
.await
.expect("Should not panic if it isn't configured wrong");
login::create(user_id, &token).await
}
pub async fn read(user_id: &i64, token: &String) -> Result<Login, sqlx::Error> {
pub async fn read(user_id: &i64, authorization_token: &String) -> Result<Login, sqlx::Error> {
User::read(user_id).await?;
login::read(user_id, token).await
login::read(user_id, authorization_token).await
}
pub async fn update(
user_id: &i64,
token: &String,
authorization_token: &String,
) -> Result<Login, Box<dyn std::error::Error>> {
let login = Login::read(user_id, token).await?;
match TokenMeta::verify_token(token).await {
let login = Login::read(user_id, authorization_token).await?;
match AuthorizationTokenMeta::verify_token(authorization_token).await {
Ok(_) => Ok(login),
Err(_) => {
if DateTime::<Utc>::default()
@ -96,7 +97,7 @@ impl Login {
.num_minutes()
<= SERVER_CONFIG.login_token_refresh_time_limit as i64
{
Login::delete(user_id, token).await?;
Login::delete(user_id, authorization_token).await?;
let login = Login::create(user_id).await?;
Ok(login)
} else {
@ -105,8 +106,8 @@ impl Login {
}
}
}
pub async fn delete(user_id: &i64, token: &String) -> Result<Login, sqlx::Error> {
login::delete(user_id, token).await
pub async fn delete(user_id: &i64, authorization_token: &String) -> Result<Login, sqlx::Error> {
login::delete(user_id, authorization_token).await
}
pub async fn read_all_for_user(user_id: &i64) -> Result<Vec<Login>, sqlx::Error> {

View file

@ -24,6 +24,13 @@ impl UserContact {
user_contact::read(&user.user_id, contact_id).await
}
pub async fn read_for_value(
contact_id: &i64,
contact_value: &String,
) -> Result<UserContact, sqlx::Error> {
user_contact::read_for_value(contact_id, contact_value).await
}
pub async fn update(
user: &User,
contact_id: &i64,

View file

@ -1,4 +1,6 @@
pub mod contact;
pub mod interaction;
pub mod login;
pub mod role;
pub mod user;
pub mod user_contact;
@ -9,10 +11,12 @@ use super::middleware::builder_or_admin_by_authorization_token;
pub fn route() -> Router {
Router::new()
.nest("/logins", login::route())
.nest("/users", user::route())
.nest("/roles", role::route())
.nest("/contacts", contact::route())
.nest("/user_contacts", user_contact::route())
.nest("/interactions", interaction::route())
.route_layer(axum::middleware::from_fn(
builder_or_admin_by_authorization_token,
))

View file

@ -0,0 +1,58 @@
use axum::{
extract::Path,
http::StatusCode,
response::IntoResponse,
routing::{delete, patch, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use crate::feature::interaction::Interaction;
#[derive(Debug, Serialize, Deserialize)]
struct CreateInteraction {
name: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct UpdateInteraction {
id: i64,
name: String,
}
pub fn route() -> Router {
Router::new()
.route("/", post(create))
.route("/", patch(update))
.route("/{id}", delete(delete_))
}
async fn create(Json(create_interaction): Json<CreateInteraction>) -> impl IntoResponse {
match Interaction::create(&create_interaction.name).await {
Ok(interaction) => (StatusCode::CREATED, Json(serde_json::json!(interaction))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn update(Json(update_interaction): Json<UpdateInteraction>) -> impl IntoResponse {
match Interaction::update(&update_interaction.id, &update_interaction.name).await {
Ok(interaction) => (StatusCode::ACCEPTED, Json(serde_json::json!(interaction))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn delete_(Path(id): Path<i64>) -> impl IntoResponse {
match Interaction::delete(&id).await {
Ok(interaction) => (StatusCode::NO_CONTENT, Json(serde_json::json!(interaction))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}

View file

@ -0,0 +1,45 @@
use std::sync::Arc;
use axum::{
http::StatusCode,
response::IntoResponse,
routing::{delete, get},
Extension, Json, Router,
};
use crate::{
feature::{login::Login, user::User},
routing::middleware::by_uri_then_insert,
};
pub fn route() -> Router {
Router::new()
.route(
"/users/{user_id}",
delete(delete_all_for_user).route_layer(axum::middleware::from_fn(by_uri_then_insert)),
)
.route(
"/count/users/{user_id}",
get(count_all_for_user).route_layer(axum::middleware::from_fn(by_uri_then_insert)),
)
}
async fn delete_all_for_user(Extension(user): Extension<Arc<User>>) -> impl IntoResponse {
match Login::delete_all_for_user(&user.user_id).await {
Ok(logins) => (StatusCode::OK, Json(serde_json::json!(logins))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn count_all_for_user(Extension(user): Extension<Arc<User>>) -> impl IntoResponse {
match Login::count_all_for_user(&user.user_id).await {
Ok(login_count) => (StatusCode::OK, Json(serde_json::json!(login_count))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}

View file

@ -90,21 +90,25 @@ async fn update(
Extension(target_user): Extension<Arc<User>>,
Json(update_user): Json<UpdateUser>,
) -> impl IntoResponse {
match User::update(
&target_user.user_id,
&update_user.name,
&update_user.surname,
&update_user.gender,
&update_user.birth_date,
&update_user.role_id,
)
.await
{
Ok(user) => (StatusCode::ACCEPTED, Json(serde_json::json!(user))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
if update_user.role_id == 0 {
(StatusCode::FORBIDDEN, Json(serde_json::json!({})))
} else {
match User::update(
&target_user.user_id,
&update_user.name,
&update_user.surname,
&update_user.gender,
&update_user.birth_date,
&update_user.role_id,
)
.await
{
Ok(user) => (StatusCode::ACCEPTED, Json(serde_json::json!(user))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
}

View file

@ -1,10 +1,4 @@
use axum::{
extract::Path,
http::StatusCode,
response::IntoResponse,
routing::{delete, get, patch, post},
Json, Router,
};
use axum::{extract::Path, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::{Deserialize, Serialize};
use crate::feature::interaction::Interaction;
@ -22,23 +16,10 @@ struct UpdateInteraction {
pub fn route() -> Router {
Router::new()
.route("/", post(create))
.route("/{id}", get(read))
.route("/", patch(update))
.route("/{id}", delete(delete_))
.route("/", get(read_all))
}
async fn create(Json(create_interaction): Json<CreateInteraction>) -> impl IntoResponse {
match Interaction::create(&create_interaction.name).await {
Ok(interaction) => (StatusCode::CREATED, Json(serde_json::json!(interaction))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn read(Path(id): Path<i64>) -> impl IntoResponse {
match Interaction::read(&id).await {
Ok(interaction) => (StatusCode::OK, Json(serde_json::json!(interaction))),
@ -49,26 +30,6 @@ async fn read(Path(id): Path<i64>) -> impl IntoResponse {
}
}
async fn update(Json(update_interaction): Json<UpdateInteraction>) -> impl IntoResponse {
match Interaction::update(&update_interaction.id, &update_interaction.name).await {
Ok(interaction) => (StatusCode::ACCEPTED, Json(serde_json::json!(interaction))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn delete_(Path(id): Path<i64>) -> impl IntoResponse {
match Interaction::delete(&id).await {
Ok(interaction) => (StatusCode::NO_CONTENT, Json(serde_json::json!(interaction))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn read_all() -> impl IntoResponse {
match Interaction::read_all().await {
Ok(interactions) => (StatusCode::OK, Json(serde_json::json!(interactions))),

View file

@ -1,7 +1,6 @@
use std::sync::Arc;
use axum::{
extract::Path,
http::StatusCode,
response::IntoResponse,
routing::{delete, get, patch, post},
@ -11,18 +10,20 @@ use serde::{Deserialize, Serialize};
use crate::feature::{auth::OneTimePassword, login::Login, user::User, user_contact::UserContact};
use super::middleware::{user_and_token_then_insert, UserAndAuthorizationToken};
use super::middleware::{
by_authorization_token_then_insert, user_and_token_then_insert, UserAndAuthorizationToken,
};
const CONTACT_EMAIL_DEFAULT_ID: i64 = 0;
#[derive(Debug, Serialize, Deserialize)]
struct CreateOneTimePassword {
pub user_id: i64,
pub user_email: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct CreateLogin {
user_id: i64,
user_email: String,
one_time_password: String,
}
@ -30,26 +31,48 @@ pub fn route() -> Router {
Router::new()
.route("/one_time_password", post(create_one_time_password))
.route("/", post(create))
.route("/users/{user_id}/tokens/{token}", get(read))
.route(
"/",
get(read).route_layer(axum::middleware::from_fn(user_and_token_then_insert)),
)
.route(
"/",
patch(update).route_layer(axum::middleware::from_fn(user_and_token_then_insert)),
)
.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))
.route(
"/",
delete(delete_).route_layer(axum::middleware::from_fn(user_and_token_then_insert)),
)
.route(
"/users",
delete(delete_all_for_user).route_layer(axum::middleware::from_fn(
by_authorization_token_then_insert,
)),
)
.route("/count/users", 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, &CONTACT_EMAIL_DEFAULT_ID).await {
Ok(user_email) => match OneTimePassword::new(&user, &user_email.contact_value).await {
Ok(_) => (StatusCode::CREATED, Json(serde_json::json!(""))),
match UserContact::read_for_value(
&CONTACT_EMAIL_DEFAULT_ID,
&create_one_time_password.user_email,
)
.await
{
Ok(user_contact) => match User::read(&user_contact.user_id).await {
Ok(user) => {
match OneTimePassword::new(&user, &create_one_time_password.user_email).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,
// this must be impossible that's why I send 500
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!(err_val.to_string())),
),
},
@ -60,30 +83,50 @@ async fn create_one_time_password(
}
}
async fn create(Json(create_login): Json<CreateLogin>) -> impl IntoResponse {
let one_time_password = OneTimePassword {
user_id: create_login.user_id,
one_time_password: create_login.one_time_password,
};
match UserContact::read_for_value(&CONTACT_EMAIL_DEFAULT_ID, &create_login.user_email).await {
Ok(user_contact) => match User::read(&user_contact.user_id).await {
Ok(user) => {
let one_time_password =
OneTimePassword::from_string(&user, &create_login.one_time_password).await;
match OneTimePassword::verify(&one_time_password).await {
true => match Login::create(&one_time_password.user_id).await {
Ok(login) => (StatusCode::CREATED, Json(serde_json::json!(login))),
match OneTimePassword::verify(&one_time_password).await {
true => match Login::create(&user.user_id).await {
Ok(login) => (StatusCode::CREATED, Json(serde_json::json!(login))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
},
false => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(
"One Time Password Authentication Failed".to_string()
)),
),
}
}
Err(err_val) => (
StatusCode::BAD_REQUEST,
// this must be impossible that's why I send 500
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!(err_val.to_string())),
),
},
false => (
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(
"One Time Password Authentication Failed".to_string()
)),
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn read(Path((user_id, token)): Path<(i64, String)>) -> impl IntoResponse {
match Login::read(&user_id, &token).await {
async fn read(
Extension(user_and_authorization_token): Extension<Arc<UserAndAuthorizationToken>>,
) -> impl IntoResponse {
match Login::read(
&user_and_authorization_token.user.user_id,
&user_and_authorization_token.authorization_token,
)
.await
{
Ok(login) => (StatusCode::OK, Json(serde_json::json!(login))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
@ -109,8 +152,15 @@ async fn update(
}
}
async fn delete_(Path((user_id, token)): Path<(i64, String)>) -> impl IntoResponse {
match Login::delete(&user_id, &token).await {
async fn delete_(
Extension(user_and_authorization_token): Extension<Arc<UserAndAuthorizationToken>>,
) -> impl IntoResponse {
match Login::delete(
&user_and_authorization_token.user.user_id,
&user_and_authorization_token.authorization_token,
)
.await
{
Ok(login) => (StatusCode::NO_CONTENT, Json(serde_json::json!(login))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
@ -119,8 +169,8 @@ async fn delete_(Path((user_id, token)): Path<(i64, String)>) -> impl IntoRespon
}
}
async fn read_all_for_user(Path(user_id): Path<i64>) -> impl IntoResponse {
match Login::read_all_for_user(&user_id).await {
async fn delete_all_for_user(Extension(user): Extension<Arc<User>>) -> impl IntoResponse {
match Login::delete_all_for_user(&user.user_id).await {
Ok(logins) => (StatusCode::OK, Json(serde_json::json!(logins))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
@ -129,18 +179,8 @@ async fn read_all_for_user(Path(user_id): Path<i64>) -> impl IntoResponse {
}
}
async fn delete_all_for_user(Path(user_id): Path<i64>) -> impl IntoResponse {
match Login::delete_all_for_user(&user_id).await {
Ok(logins) => (StatusCode::OK, Json(serde_json::json!(logins))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn count_all_for_user(Path(user_id): Path<i64>) -> impl IntoResponse {
match Login::count_all_for_user(&user_id).await {
async fn count_all_for_user(Extension(user): Extension<Arc<User>>) -> impl IntoResponse {
match Login::count_all_for_user(&user.user_id).await {
Ok(login_count) => (StatusCode::OK, Json(serde_json::json!(login_count))),
Err(err_val) => (
StatusCode::BAD_REQUEST,

View file

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use crate::{
error::ForumAuthError,
feature::{login::TokenMeta, user::User},
feature::{login::AuthorizationTokenMeta, user::User},
};
#[derive(Debug, Serialize, Deserialize)]
@ -43,7 +43,7 @@ async fn authorization_token_extraction(
async fn user_extraction_from_authorization_token(
authorization_token: &String,
) -> Result<User, ForumAuthError> {
match TokenMeta::verify_token(&authorization_token.to_string()).await {
match AuthorizationTokenMeta::verify_token(&authorization_token.to_string()).await {
Ok(claims) => User::read(&claims.custom.user_id)
.await
.map_err(|err_val| ForumAuthError::AuthenticationFailed(err_val.to_string())),

View file

@ -1,12 +1,35 @@
use axum::{extract::Path, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use std::sync::Arc;
use axum::{
extract::Path, http::StatusCode, response::IntoResponse, routing::get, Extension, Json, Router,
};
use crate::feature::user::User;
use super::middleware::by_authorization_token_then_insert;
pub fn route() -> Router {
Router::new().route("/{user_id}", get(read))
Router::new()
.route(
"/",
get(read).route_layer(axum::middleware::from_fn(
by_authorization_token_then_insert,
)),
)
.route("/{user_id}", get(read_anybody))
}
async fn read(Path(user_id): Path<i64>) -> impl IntoResponse {
async fn read(Extension(user): Extension<Arc<User>>) -> impl IntoResponse {
match User::read(&user.user_id).await {
Ok(user) => (StatusCode::OK, Json(serde_json::json!(user))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
}
}
async fn read_anybody(Path(user_id): Path<i64>) -> impl IntoResponse {
match User::read(&user_id).await {
Ok(user) => (StatusCode::OK, Json(serde_json::json!(user))),
Err(err_val) => (