This commit is contained in:
2026-02-21 10:39:52 +01:00
parent f7c975a3f0
commit 66c1fe0025
38 changed files with 1543 additions and 616 deletions

View File

@@ -85,4 +85,8 @@ impl App {
println!("Nettoyage et fermeture de l'application.");
}
async fn shutdown(&self) {
}
}

View 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()
}
}
}

View 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()
}
}
}

View 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()
}
}
}

View File

@@ -0,0 +1,6 @@
pub mod auth;
pub mod category;
pub mod channel;
pub mod message;
pub mod server;
pub mod user;

View 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>,
}

View 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()
}
}
}

View File

@@ -0,0 +1 @@
pub mod dto;

1
src/interfaces/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod http;

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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())
}

View 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,
}

View File

@@ -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)
}
}

View File

@@ -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())
}

View File

@@ -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)))
}
}

View 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)))
}

View File

@@ -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
}

View File

@@ -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(())
}
}
}

View File

@@ -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
View 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
}
}

View File

@@ -1 +1,3 @@
pub mod toolbox;
pub mod password;
pub mod ssh_auth;
pub mod toolbox;

28
src/utils/password.rs Normal file
View 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
View 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>
}

View File

@@ -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)
}