Init
This commit is contained in:
@@ -85,4 +85,8 @@ impl App {
|
||||
|
||||
println!("Nettoyage et fermeture de l'application.");
|
||||
}
|
||||
|
||||
async fn shutdown(&self) {
|
||||
|
||||
}
|
||||
}
|
||||
35
src/interfaces/http/dto/category.rs
Normal file
35
src/interfaces/http/dto/category.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::models::category;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CategoryResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<category::Model> for CategoryResponse {
|
||||
fn from(model: category::Model) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateCategoryRequest {
|
||||
pub server_id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<CreateCategoryRequest> for category::ActiveModel {
|
||||
fn from(request: CreateCategoryRequest) -> Self {
|
||||
Self {
|
||||
server_id: Set(request.server_id),
|
||||
name: Set(request.name),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/interfaces/http/dto/channel.rs
Normal file
50
src/interfaces/http/dto/channel.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::models::channel;
|
||||
use crate::models::channel::ChannelType;
|
||||
use sea_orm::Set;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ChannelResponse {
|
||||
pub id: Uuid,
|
||||
pub server_id: Option<Uuid>,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub position: i32,
|
||||
pub channel_type: ChannelType,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<channel::Model> for ChannelResponse {
|
||||
fn from(model: channel::Model) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
server_id: model.server_id,
|
||||
category_id: model.category_id,
|
||||
position: model.position,
|
||||
channel_type: model.channel_type,
|
||||
name: model.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CreateChannelRequest {
|
||||
pub server_id: Option<Uuid>,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub position: i32,
|
||||
pub channel_type: ChannelType,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<CreateChannelRequest> for channel::ActiveModel {
|
||||
fn from(request: CreateChannelRequest) -> Self {
|
||||
Self {
|
||||
server_id: Set(request.server_id),
|
||||
category_id: Set(request.category_id),
|
||||
position: Set(request.position),
|
||||
channel_type: Set(request.channel_type),
|
||||
name: Set(request.name),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/interfaces/http/dto/message.rs
Normal file
40
src/interfaces/http/dto/message.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use crate::models::message;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MessageResponse {
|
||||
pub id: Uuid,
|
||||
pub channel_id: Uuid,
|
||||
pub author_id: Uuid,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl From<message::Model> for MessageResponse {
|
||||
fn from(model: message::Model) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
channel_id: model.channel_id,
|
||||
author_id: model.user_id,
|
||||
content: model.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateMessageRequest {
|
||||
pub channel_id: Uuid,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl From<CreateMessageRequest> for message::ActiveModel {
|
||||
fn from(request: CreateMessageRequest) -> Self {
|
||||
Self {
|
||||
channel_id: Set(request.channel_id),
|
||||
user_id: Set(Uuid::new_v4()),
|
||||
content: Set(request.content),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/interfaces/http/dto/mod.rs
Normal file
6
src/interfaces/http/dto/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod auth;
|
||||
pub mod category;
|
||||
pub mod channel;
|
||||
pub mod message;
|
||||
pub mod server;
|
||||
pub mod user;
|
||||
52
src/interfaces/http/dto/server.rs
Normal file
52
src/interfaces/http/dto/server.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
// On importe le modèle pour pouvoir mapper
|
||||
use crate::models::server;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ServerResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<server::Model> for ServerResponse {
|
||||
fn from(model: server::Model) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
created_at: model.created_at.to_rfc3339(),
|
||||
updated_at: model.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateServerRequest {
|
||||
pub name: String,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl From<CreateServerRequest> for server::ActiveModel {
|
||||
fn from(request: CreateServerRequest) -> Self {
|
||||
Self {
|
||||
name: Set(request.name),
|
||||
password: Set(request.password),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum TreeItemType {
|
||||
// todo : faire le CategoryResponse et ChannelResponse
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ServerTreeResponse {
|
||||
items: Vec<TreeItemType>,
|
||||
}
|
||||
33
src/interfaces/http/dto/user.rs
Normal file
33
src/interfaces/http/dto/user.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use crate::models::user;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserResponse {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl From<user::Model> for UserResponse {
|
||||
fn from(model: user::Model) -> Self {
|
||||
Self {
|
||||
id: model.id,
|
||||
username: model.username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateUserRequest {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl From<CreateUserRequest> for user::ActiveModel {
|
||||
fn from(request: CreateUserRequest) -> Self {
|
||||
Self {
|
||||
username: Set(request.username),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/interfaces/http/mod.rs
Normal file
1
src/interfaces/http/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod dto;
|
||||
1
src/interfaces/mod.rs
Normal file
1
src/interfaces/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod http;
|
||||
12
src/lib.rs
12
src/lib.rs
@@ -1,11 +1,13 @@
|
||||
pub mod config;
|
||||
pub mod app;
|
||||
pub mod config;
|
||||
pub mod network;
|
||||
pub mod utils;
|
||||
|
||||
pub mod database;
|
||||
pub mod models;
|
||||
pub mod serializers;
|
||||
pub mod repositories;
|
||||
pub mod event_bus;
|
||||
pub mod hub;
|
||||
pub mod hub;
|
||||
pub mod models;
|
||||
pub mod repositories;
|
||||
pub mod serializers;
|
||||
|
||||
pub mod interfaces;
|
||||
|
||||
@@ -13,7 +13,7 @@ pub struct Model {
|
||||
pub filename: String,
|
||||
pub file_size: i32,
|
||||
pub mime_type: String,
|
||||
pub created_at: DateTime,
|
||||
pub created_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -12,8 +12,8 @@ pub struct Model {
|
||||
pub server_id: Uuid,
|
||||
pub name: String,
|
||||
pub position: i32,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -50,4 +50,4 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ pub struct Model {
|
||||
pub position: i32,
|
||||
pub channel_type: ChannelType,
|
||||
pub name: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -87,4 +87,4 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct Model {
|
||||
pub channel_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub role: String,
|
||||
pub joined_at: DateTime,
|
||||
pub joined_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -55,4 +55,4 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ pub struct Model {
|
||||
pub user_id: Uuid,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub content: String,
|
||||
pub created_at: DateTime,
|
||||
pub edited_at: Option<DateTime>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: Option<DateTimeUtc>,
|
||||
pub reply_to_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
@@ -74,4 +74,4 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ pub struct Model {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub password: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -51,4 +51,4 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ pub struct Model {
|
||||
pub server_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub username: Option<String>,
|
||||
pub joined_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub joined_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -56,4 +56,4 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[sea_orm(column_type = "Text", unique)]
|
||||
pub pub_key: String,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -52,4 +53,4 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::network::http::{web, AppRouter};
|
||||
pub fn setup_route(app_state: AppState) -> Router {
|
||||
Router::new()
|
||||
.merge(web::setup_route())
|
||||
.layer(CorsLayer::permissive())
|
||||
.layer(middleware::from_fn_with_state(app_state.clone(), context_middleware))
|
||||
.with_state(app_state)
|
||||
.layer(CorsLayer::permissive())
|
||||
}
|
||||
105
src/network/http/web/api/auth.rs
Normal file
105
src/network/http/web/api/auth.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use crate::app::AppState;
|
||||
use crate::network::http::{AppRouter, HTTPError};
|
||||
use crate::utils::toolbox::ssh_generate_challenge;
|
||||
use axum::extract::State;
|
||||
use axum::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssh_key::{Algorithm as SshAlgorithm, PublicKey, Signature};
|
||||
|
||||
fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SshChallengeRequest {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SshChallengeResponse {
|
||||
challenge: String,
|
||||
}
|
||||
|
||||
pub async fn ssh_challenge(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SshChallengeRequest>,
|
||||
) -> Result<Json<SshChallengeResponse>, HTTPError> {
|
||||
log::info!(
|
||||
"POST /auth/ssh-challenge - Challenge request for user: {}",
|
||||
payload.username
|
||||
);
|
||||
|
||||
let user = state
|
||||
.repositories
|
||||
.user
|
||||
.get_by_username(payload.username.clone())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
log::warn!(
|
||||
"POST /auth/ssh-challenge - User not found: {}",
|
||||
payload.username
|
||||
);
|
||||
HTTPError::NotFound
|
||||
})?;
|
||||
|
||||
let challenge = ssh_generate_challenge(32);
|
||||
log::info!(
|
||||
"POST /auth/ssh-challenge - Challenge generated for user: {}",
|
||||
payload.username
|
||||
);
|
||||
|
||||
// todo : stocker le challenge dans AppState
|
||||
// bien penser à ajouter un délai d'expiration
|
||||
// songer à mettre une session id ?
|
||||
// Peut être que l'utilisateur se connectera depuis différent device
|
||||
// state.store_challenge(&payload.username, challenge.clone())
|
||||
|
||||
Ok(Json(SshChallengeResponse { challenge }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
enum SignatureAlgorithm {
|
||||
#[serde(rename = "rsa")]
|
||||
Rsa,
|
||||
#[serde(rename = "ed25519")]
|
||||
Ed25519,
|
||||
#[serde(rename = "ecdsa")]
|
||||
Ecdsa,
|
||||
}
|
||||
|
||||
impl SignatureAlgorithm {
|
||||
fn to_ssh_algorithm(&self) -> SshAlgorithm {
|
||||
match self {
|
||||
SignatureAlgorithm::Rsa => SshAlgorithm::Rsa { hash: None },
|
||||
SignatureAlgorithm::Ed25519 => SshAlgorithm::Ed25519,
|
||||
SignatureAlgorithm::Ecdsa => SshAlgorithm::Ecdsa {
|
||||
curve: ssh_key::EcdsaCurve::NistP256,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SshVerifyRequest {
|
||||
username: String,
|
||||
signature: String,
|
||||
algorithm: SignatureAlgorithm,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SshVerifyResponse {
|
||||
// todo : remplir avec la réponse jwt - à établir après la construction jwt
|
||||
}
|
||||
|
||||
pub async fn ssh_verify() {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SshChallengeResponse {
|
||||
challenge: String,
|
||||
}
|
||||
@@ -30,6 +30,8 @@ pub async fn category_list(
|
||||
Extension(_ctx): Extension<RequestContext>,
|
||||
Query(query): Query<CategoryQuery>
|
||||
) -> Result<Json<Vec<CategorySerializer>>, HTTPError> {
|
||||
log::info!("GET /categories/ - Query: server_id={:?}", query.server_id);
|
||||
|
||||
let categories = if let Some(server_id) = query.server_id {
|
||||
app_state.repositories.category
|
||||
.get_by_server_id(server_id)
|
||||
@@ -40,7 +42,7 @@ pub async fn category_list(
|
||||
.await?
|
||||
};
|
||||
|
||||
|
||||
log::info!("GET /categories/ - Found {} categories", categories.len());
|
||||
Ok(Json(categories.into_iter().map(CategorySerializer::from).collect()))
|
||||
}
|
||||
|
||||
@@ -48,11 +50,17 @@ pub async fn category_detail(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||
let category = category::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
log::info!("GET /categories/{id}/ - Fetching category: {}", id);
|
||||
|
||||
let category = app_state.repositories.category
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
log::warn!("GET /categories/{id}/ - Category not found: {}", id);
|
||||
HTTPError::NotFound
|
||||
})?;
|
||||
|
||||
log::info!("GET /categories/{id}/ - Category found: {}", id);
|
||||
Ok(Json(CategorySerializer::from(category)))
|
||||
}
|
||||
|
||||
@@ -60,9 +68,12 @@ pub async fn category_create(
|
||||
State(app_state): State<AppState>,
|
||||
Json(serializer): Json<CategorySerializer>
|
||||
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||
log::info!("POST /categories/ - Creating category: {:?}", serializer.name);
|
||||
|
||||
let active = serializer.into_active_model();
|
||||
let category: category::Model = active.insert(app_state.db.get_connection()).await?;
|
||||
|
||||
log::info!("POST /categories/ - Category created with id: {}", category.id);
|
||||
Ok(Json(CategorySerializer::from(category)))
|
||||
}
|
||||
|
||||
@@ -71,10 +82,15 @@ pub async fn category_update(
|
||||
Path(id): Path<Uuid>,
|
||||
Json(serializer): Json<CategorySerializer>,
|
||||
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||
log::info!("PUT /categories/{id}/ - Updating category: {}", id);
|
||||
|
||||
let category = category::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
.ok_or_else(|| {
|
||||
log::warn!("PUT /categories/{id}/ - Category not found: {}", id);
|
||||
HTTPError::NotFound
|
||||
})?;
|
||||
|
||||
let active = category.into_active_model();
|
||||
|
||||
@@ -82,6 +98,7 @@ pub async fn category_update(
|
||||
.update(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
log::info!("PUT /categories/{id}/ - Category updated: {}", id);
|
||||
Ok(Json(CategorySerializer::from(category)))
|
||||
}
|
||||
|
||||
@@ -89,13 +106,17 @@ pub async fn category_delete(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
log::info!("DELETE /categories/{id}/ - Deleting category: {}", id);
|
||||
|
||||
let result = category::Entity::delete_by_id(id)
|
||||
.exec(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
log::warn!("DELETE /categories/{id}/ - Category not found: {}", id);
|
||||
Err(HTTPError::NotFound)
|
||||
} else {
|
||||
log::info!("DELETE /categories/{id}/ - Category deleted: {}", id);
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ mod category;
|
||||
mod channel;
|
||||
mod message;
|
||||
mod server;
|
||||
mod user;
|
||||
mod auth;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
|
||||
@@ -12,4 +14,5 @@ pub fn setup_route() -> AppRouter {
|
||||
.nest("/channel", channel::setup_route())
|
||||
.nest("/message", message::setup_route())
|
||||
.nest("/server", server::setup_route())
|
||||
.nest("/user", user::setup_route())
|
||||
}
|
||||
@@ -1,75 +1,84 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use sea_orm::{IntoActiveModel};
|
||||
use uuid::Uuid;
|
||||
use crate::app::AppState;
|
||||
use crate::models::server;
|
||||
use crate::interfaces::http::dto::server::ServerResponse;
|
||||
use crate::network::http::{AppRouter, HTTPError};
|
||||
use crate::serializers::{ServerSerializer, ServerTreeSerializer};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::get;
|
||||
use axum::Json;
|
||||
use sea_orm::IntoActiveModel;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
.route("/servers/", get(server_list))
|
||||
.route("/servers/{id}/", get(server_detail))
|
||||
.route("/servers/", post(server_create))
|
||||
.route("/servers/{id}/", put(server_update))
|
||||
.route("/servers/{id}/", delete(server_delete))
|
||||
.route("/servers/", get(server_list).post(server_create))
|
||||
.route(
|
||||
"/servers/{id}/",
|
||||
get(server_detail).put(server_update).delete(server_delete),
|
||||
)
|
||||
.route("/servers/{id}/password/", get(server_password))
|
||||
.route("/servers/{id}/tree/", get(tree))
|
||||
}
|
||||
|
||||
pub async fn server_list(
|
||||
State(state): State<AppState>
|
||||
) -> Result<Json<Vec<ServerSerializer>>, HTTPError> {
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<ServerResponse>>, HTTPError> {
|
||||
let servers = state.repositories.server.get_all().await?;
|
||||
|
||||
Ok(Json(servers.into_iter().map(ServerSerializer::from).collect()))
|
||||
Ok(Json(
|
||||
servers.into_iter().map(ServerResponse::from).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn server_detail(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
let server = state.repositories.server.get_by_id(id)
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<ServerResponse>, HTTPError> {
|
||||
let server = state
|
||||
.repositories
|
||||
.server
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
Ok(Json(ServerSerializer::from(server)))
|
||||
Ok(Json(ServerResponse::from(server)))
|
||||
}
|
||||
|
||||
pub async fn server_create(
|
||||
State(state): State<AppState>,
|
||||
Json(serializer): Json<ServerSerializer>
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
Json(serializer): Json<ServerSerializer>,
|
||||
) -> Result<Json<ServerResponse>, HTTPError> {
|
||||
let active = serializer.into_active_model();
|
||||
let server = state.repositories.server.create(active).await?;
|
||||
|
||||
Ok(Json(ServerSerializer::from(server)))
|
||||
Ok(Json(ServerResponse::from(server)))
|
||||
}
|
||||
|
||||
pub async fn server_update(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(serializer): Json<ServerSerializer>,
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
let am_server = state.repositories.server
|
||||
) -> Result<Json<ServerResponse>, HTTPError> {
|
||||
let am_server = state
|
||||
.repositories
|
||||
.server
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?
|
||||
.into_active_model();
|
||||
|
||||
let server = state.repositories.server
|
||||
let server = state
|
||||
.repositories
|
||||
.server
|
||||
.update(serializer.apply_to_active_model(am_server))
|
||||
.await?;
|
||||
|
||||
Ok(Json(ServerSerializer::from(server)))
|
||||
Ok(Json(ServerResponse::from(server)))
|
||||
}
|
||||
|
||||
pub async fn server_delete(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
if state.repositories.server.delete(id).await? {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
@@ -80,9 +89,12 @@ pub async fn server_delete(
|
||||
|
||||
pub async fn server_password(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<Option<String>>, HTTPError> {
|
||||
let server = state.repositories.server.get_by_id(id)
|
||||
let server = state
|
||||
.repositories
|
||||
.server
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
@@ -91,9 +103,9 @@ pub async fn server_password(
|
||||
|
||||
pub async fn tree(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<ServerTreeSerializer>, HTTPError> {
|
||||
let layout = state.repositories.server.get_tree(id).await?;
|
||||
|
||||
Ok(Json(ServerTreeSerializer::from(layout)))
|
||||
}
|
||||
}
|
||||
|
||||
42
src/network/http/web/api/user.rs
Normal file
42
src/network/http/web/api/user.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use axum::{Extension, Json};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::routing::get;
|
||||
use uuid::Uuid;
|
||||
use crate::app::AppState;
|
||||
use crate::network::http::{AppRouter, HTTPError};
|
||||
use crate::serializers::UserSerializer;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
.route("/users/", get(user_list))
|
||||
.route("/users/{id}/", get(user_detail))
|
||||
}
|
||||
|
||||
pub async fn user_list(
|
||||
State(app_state): State<AppState>,
|
||||
Extension(_ctx): Extension<crate::network::http::RequestContext>
|
||||
) -> Result<Json<Vec<UserSerializer>>, HTTPError> {
|
||||
log::info!("GET /users/ - Fetching user list");
|
||||
|
||||
let users = app_state.repositories.user.get_all().await?;
|
||||
log::info!("GET /users/ - Found {} users", users.len());
|
||||
|
||||
Ok(Json(users.into_iter().map(UserSerializer::from).collect()))
|
||||
}
|
||||
|
||||
pub async fn user_detail(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<Json<UserSerializer>, HTTPError> {
|
||||
log::info!("GET /users/{id}/ - Fetching user with id: {}", id);
|
||||
|
||||
let user = app_state.repositories.user
|
||||
.get_by_id(id).await?
|
||||
.ok_or_else(|| {
|
||||
log::warn!("GET /users/{id}/ - User not found: {}", id);
|
||||
HTTPError::NotFound
|
||||
})?;
|
||||
|
||||
log::info!("GET /users/{id}/ - User found: {}", id);
|
||||
Ok(Json(UserSerializer::from(user)))
|
||||
}
|
||||
@@ -10,7 +10,9 @@ use crate::utils::toolbox::number_of_cpus;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UDPServer {
|
||||
// todo : passer sur du arcswap, rwlock rajoute trop de contention.
|
||||
table_router: Arc<RwLock<HashMap<String, SocketAddr>>>,
|
||||
|
||||
bind_addr: SocketAddr
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,83 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||
use crate::models::user;
|
||||
use crate::repositories::RepositoryContext;
|
||||
use crate::utils::password;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, IntoActiveModel, QueryFilter, Set,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
pub context: Arc<RepositoryContext>,
|
||||
}
|
||||
|
||||
impl UserRepository {
|
||||
pub async fn get_all(&self) -> Result<Vec<user::Model>, DbErr> {
|
||||
user::Entity::find().all(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<user::Model>, DbErr> {
|
||||
user::Entity::find_by_id(id).one(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn get_by_username(&self, username: String) -> Result<Option<user::Model>, DbErr> {
|
||||
user::Entity::find()
|
||||
.filter(user::Column::Username.eq(username))
|
||||
.one(&self.context.db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn check_password(
|
||||
&self,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<user::Model, DbErr> {
|
||||
let user = self.get_by_username(username).await?;
|
||||
if let Some(user) = user {
|
||||
let password_ok = password::verify_password(password.as_str(), user.password.as_str())
|
||||
.map_err(|_| DbErr::Custom("Password hashing failed".to_string()))?;
|
||||
|
||||
if password_ok {
|
||||
return Ok(user);
|
||||
}
|
||||
}
|
||||
Err(DbErr::Custom("Invalid username or password".to_string()))
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: user::ActiveModel) -> Result<user::Model, DbErr> {
|
||||
let user = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("user_updated", user.clone());
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: user::ActiveModel) -> Result<user::Model, DbErr> {
|
||||
pub async fn create(&self, mut active: user::ActiveModel) -> Result<user::Model, DbErr> {
|
||||
let user = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("user_created", user.clone());
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn set_password(&self, id: uuid::Uuid, password: String) -> Result<(), DbErr> {
|
||||
let user = self
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or_else(|| DbErr::Custom("User not found".to_string()))?;
|
||||
|
||||
let mut active = user.into_active_model();
|
||||
let password = password::hash_password(&password)
|
||||
.map_err(|_| DbErr::Custom("Password hashing failed".to_string()))?;
|
||||
active.password = Set(password);
|
||||
|
||||
let user = self.update(active).await?;
|
||||
|
||||
self.context.events.emit("user_changed", user);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
user::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
user::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
self.context.events.emit("user_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,20 @@ mod server;
|
||||
mod category;
|
||||
mod channel;
|
||||
mod message;
|
||||
mod user;
|
||||
|
||||
trait BaseSerializer<M>
|
||||
where
|
||||
M: ActiveModelTrait,
|
||||
{
|
||||
fn into_active_model(self) -> M;
|
||||
|
||||
fn apply_to_active_model(self, active_model: M) -> M;
|
||||
}
|
||||
|
||||
use sea_orm::ActiveModelTrait;
|
||||
pub use server::*;
|
||||
pub use category::*;
|
||||
pub use channel::*;
|
||||
pub use message::*;
|
||||
pub use message::*;
|
||||
pub use user::*;
|
||||
44
src/serializers/user.rs
Normal file
44
src/serializers/user.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use crate::models::{category, user};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct UserSerializer {
|
||||
#[serde(skip_deserializing)]
|
||||
pub id: Option<Uuid>,
|
||||
pub username: String,
|
||||
pub pub_key: String,
|
||||
|
||||
#[serde(skip_deserializing)]
|
||||
pub created_at: String,
|
||||
#[serde(skip_deserializing)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<user::Model> for UserSerializer {
|
||||
fn from(model: user::Model) -> Self {
|
||||
Self {
|
||||
id: Some(model.id),
|
||||
username: model.username,
|
||||
pub_key: model.pub_key,
|
||||
created_at: model.created_at.to_string(),
|
||||
updated_at: model.updated_at.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserSerializer {
|
||||
pub fn into_active_model(self) -> user::ActiveModel {
|
||||
user::ActiveModel {
|
||||
username: Set(self.username),
|
||||
pub_key: Set(self.pub_key),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to_active_model(self, mut active_model: user::ActiveModel) -> user::ActiveModel { active_model.username = Set(self.username);
|
||||
active_model.pub_key = Set(self.pub_key);
|
||||
active_model
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pub mod toolbox;
|
||||
pub mod password;
|
||||
pub mod ssh_auth;
|
||||
pub mod toolbox;
|
||||
|
||||
28
src/utils/password.rs
Normal file
28
src/utils/password.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
// src/utils/password.rs
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Algorithm, Argon2, Params,
|
||||
Version,
|
||||
};
|
||||
|
||||
/// Hache un password avec Argon2id
|
||||
/// Génère automatiquement un salt cryptographiquement sûr
|
||||
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
||||
let salt = SaltString::generate(OsRng);
|
||||
let params = Params::new(65540, 18, 1, None)?;
|
||||
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||
|
||||
argon2
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map(|hash| hash.to_string())
|
||||
}
|
||||
|
||||
/// Vérifie un password contre son hash
|
||||
pub fn verify_password(password: &str, hash: &str) -> Result<bool, argon2::password_hash::Error> {
|
||||
let parsed_hash = PasswordHash::new(hash)?;
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
Ok(argon2
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok())
|
||||
}
|
||||
61
src/utils/ssh_auth.rs
Normal file
61
src/utils/ssh_auth.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use chrono::{DateTime, Utc};
|
||||
use parking_lot::Mutex;
|
||||
use crate::utils::toolbox::ssh_generate_challenge;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SshAuthManager {
|
||||
// session_id: String,
|
||||
challenges: Arc<Mutex<HashMap<String, SshAuthChallenge>>>,
|
||||
}
|
||||
|
||||
impl SshAuthManager {
|
||||
pub fn new() -> Self {
|
||||
let manager = Self {
|
||||
challenges: Arc::new(Mutex::new(HashMap::new()))
|
||||
};
|
||||
manager.start_cleanup_task();
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
fn start_cleanup_task(&self) {
|
||||
let challenges = self.challenges.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let mut map = challenges.lock();
|
||||
let now = Utc::now();
|
||||
map.retain(|_, challenge| {
|
||||
challenge.expires_at > now
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_auth_challenge(&self, session_id: String) -> SshAuthChallenge {
|
||||
let challenge = ssh_generate_challenge(1024);
|
||||
let auth_challenge = SshAuthChallenge {
|
||||
session_id: session_id.clone(),
|
||||
challenge,
|
||||
expires_at: Utc::now() + chrono::Duration::minutes(1)
|
||||
};
|
||||
|
||||
self.challenges.lock().insert(
|
||||
session_id,
|
||||
auth_challenge.clone()
|
||||
);
|
||||
|
||||
auth_challenge
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SshAuthChallenge {
|
||||
session_id: String,
|
||||
challenge: String,
|
||||
expires_at: DateTime<Utc>
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use rand::Rng;
|
||||
|
||||
pub fn number_of_cpus() -> usize {
|
||||
match std::thread::available_parallelism() {
|
||||
Ok(n) => n.get(),
|
||||
@@ -6,4 +8,31 @@ pub fn number_of_cpus() -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_random(size: usize) -> Vec<u8> {
|
||||
let mut rng = rand::rng();
|
||||
let value = (0..size).map(|_| rng.random()).collect::<Vec<u8>>();
|
||||
value
|
||||
}
|
||||
|
||||
pub fn b64_encode(value: &[u8]) -> String {
|
||||
base64::Engine::encode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
value
|
||||
)
|
||||
}
|
||||
|
||||
pub fn b64_decode(value: &str) -> Result<Vec<u8>, std::io::Error> {
|
||||
base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
value
|
||||
).map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ssh_generate_challenge(size: usize) -> String {
|
||||
let challenge = generate_random(size);
|
||||
b64_encode(&challenge)
|
||||
}
|
||||
Reference in New Issue
Block a user