This commit is contained in:
2026-06-09 23:05:35 +02:00
parent ee2fc42fff
commit eb2652f7e9
27 changed files with 665 additions and 538 deletions
Generated
+238 -420
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -13,27 +13,27 @@ members = [".", "migration", "event_bus"]
[dependencies] [dependencies]
tokio = { version = "1.52.3", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
axum = "0.8" axum = "0.8"
config = "0.15.22" config = "0.15.23"
sea-orm = { version = "2.0.0-rc.38", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] } sea-orm = { version = "2.0.0-rc.40", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] }
migration = { path = "migration" } migration = { path = "migration" }
event_bus = { path = "event_bus" } event_bus = { path = "event_bus" }
parking_lot = "0.12.5" parking_lot = "0.12.5"
serde = "1.0.228" serde = "1.0.228"
serde_json = "1.0.149" serde_json = "1.0.150"
toml = "1.1.2" toml = "1.1.2"
uuid = { version = "1.23.1", features = ["v4", "v7", "fast-rng", "serde"] } uuid = { version = "1.23.2", features = ["v4", "v7", "fast-rng", "serde"] }
tracing = "0.1.44" tracing = "0.1.44"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] }
thiserror = "2" thiserror = "2"
utoipa = { version = "5", features = ["uuid", "chrono"] } utoipa = { version = "5", features = ["uuid", "chrono"] }
utoipa-swagger-ui = { version = "9", features = ["axum"] } utoipa-swagger-ui = { version = "9", features = ["axum"] }
log = "0.4" log = "0.4"
bitflags = "2.11.1" bitflags = "2.13.0"
argon2 = { version = "0.6.0-rc.8", features = ["password-hash"] } argon2 = { version = "0.6.0-rc.8", features = ["password-hash"] }
jsonwebtoken = "10.3.0" jsonwebtoken = { version = "10.4.0", features = ["aws_lc_rs"] }
tower = { version = "0.5", features = ["util"] } tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["catch-panic", "cors", "trace"] } tower-http = { version = "0.6", features = ["catch-panic", "cors", "trace"] }
chrono = "0.4.44" chrono = "0.4.45"
validator = { version = "0.20.0", features = ["derive"] } validator = { version = "0.20.0", features = ["derive"] }
async-trait = "0.1.89" async-trait = "0.1.89"
anyhow = "1.0.102" anyhow = "1.0.102"
+3 -3
View File
@@ -13,11 +13,11 @@ name = "event_bus_throughput"
harness = false harness = false
[dependencies] [dependencies]
tokio = { version = "1.52.1", default-features = false, features = ["rt", "sync"] } tokio = { version = "1.52.3", default-features = false, features = ["rt", "sync"] }
glob = "0.3.3" glob = "0.3.3"
parking_lot = "0.12.5" parking_lot = "0.12.5"
tracing = "0.1" tracing = "0.1"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.52.1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] } tokio = { version = "1.52.3", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] }
criterion = { version = "0.8.2", features = ["async_tokio"] } criterion = { version = "0.8.2", features = ["async_tokio"] }
+10 -4
View File
@@ -1,5 +1,11 @@
import vuetify from 'eslint-config-vuetify' // import vuetify from 'eslint-config-vuetify'
//
// export default vuetify({
// ts: true,
// })
export default vuetify({ export default [
ts: true, {
}) ignores: ["**/*"],
},
];
+1
View File
@@ -38,6 +38,7 @@
Change this page by updating Change this page by updating
<v-kbd>{{ <v-kbd>{{
` `
<HelloWorld/> <HelloWorld/>
` }} ` }}
</v-kbd> </v-kbd>
+148
View File
@@ -0,0 +1,148 @@
<script lang="ts" setup>
import {ref, watch} from 'vue'
import {useRouter} from 'vue-router'
import {apiFetch} from '@/plugins/api'
const router = useRouter()
const username = ref('')
const password = ref('')
const confirmPassword = ref('')
const hasInitToken = ref(false)
const initToken = ref('')
const loading = ref(false)
const error = ref('')
// Réinitialiser le champ si on décoche la case
watch(hasInitToken, (val) => {
if (!val) initToken.value = ''
})
const rules = {
required: (value: string) => !!value || 'Ce champ est requis.',
min: (value: string) => (value && value.length >= 8) || 'Minimum 8 caractères requis.',
match: (value: string) => value === password.value || 'Les mots de passe ne correspondent pas.',
}
async function handleRegister() {
if (password.value !== confirmPassword.value) {
error.value = 'Les mots de passe ne correspondent pas.'
return
}
loading.value = true
error.value = ''
// Alignement strict avec le DTO Rust JoinRequest
const body: any = {
username: username.value,
password: password.value,
password_valid: confirmPassword.value, // Requis pour le validateur 'must_match'
}
// Alignement avec le champ 'superuser_token' du DTO
if (hasInitToken.value && initToken.value.trim() !== '') {
body.superuser_token = initToken.value.trim()
}
try {
const response = await apiFetch('/join', {
method: 'POST',
body
})
if (!response.ok) {
const errData = await response.json().catch(() => ({message: 'Erreur lors de l\'inscription'}))
throw new Error(errData.message || 'Échec de l\'inscription')
}
router.push('/login')
} catch (err) {
error.value = err instanceof Error ? err.message : 'Une erreur est survenue'
} finally {
loading.value = false
}
}
</script>
<template>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col cols="12" md="4" sm="8">
<v-card class="elevation-12">
<v-toolbar color="secondary" dark flat>
<v-toolbar-title>Inscription</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form @submit.prevent="handleRegister">
<v-text-field
v-model="username"
:rules="[rules.required]"
label="Nom d'utilisateur"
prepend-icon="mdi-account"
required
type="text"
/>
<v-text-field
v-model="password"
:rules="[rules.required, rules.min]"
label="Mot de passe"
prepend-icon="mdi-lock"
required
type="password"
/>
<v-text-field
v-model="confirmPassword"
:rules="[rules.required, rules.match]"
label="Confirmer le mot de passe"
prepend-icon="mdi-lock-check"
required
type="password"
/>
<v-checkbox
v-model="hasInitToken"
class="mb-2"
color="secondary"
density="compact"
hide-details
label="J'ai un token d'initialisation"
/>
<v-expand-transition>
<v-text-field
v-if="hasInitToken"
v-model="initToken"
label="Token"
placeholder="Entrez votre token ici"
prepend-icon="mdi-key"
type="text"
/>
</v-expand-transition>
<v-alert v-if="error" class="mt-3" density="compact" type="error">
{{ error }}
</v-alert>
<v-card-actions class="mt-4">
<v-btn
:loading="loading"
block
color="secondary"
size="large"
type="submit"
>
S'inscrire
</v-btn>
</v-card-actions>
<div class="text-center mt-2">
<router-link to="/login">Déjà un compte ? Connectez-vous</router-link>
</div>
</v-form>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
+95 -6
View File
@@ -1,11 +1,100 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref} from 'vue'
import {useAuthStore} from '@/stores/auth'
import {useRouter} from 'vue-router'
import {apiFetch} from "@/plugins/api.ts";
const authStore = useAuthStore()
const router = useRouter()
const username = ref('')
const password = ref('')
const loading = ref(false)
const error = ref('')
const rules = {
required: (value: string) => !!value || 'Ce champ est requis.',
}
async function handleLogin() {
loading.value = true
error.value = ''
try {
// Appel direct via votre helper
const response = await apiFetch('/auth/login', {
method: 'POST',
body: {
username: username.value,
password: password.value
}
})
if (!response.ok) {
throw new Error('Identifiants invalides')
}
const data = await response.json()
await authStore.setToken(data.token)
await router.push('/')
} catch (err) {
error.value = 'Erreur de connexion'
} finally {
loading.value = false
}
}
</script> </script>
<template> <template>
<v-container class="fill-height" fluid>
</template> <v-row align="center" justify="center">
<v-col cols="12" md="4" sm="8">
<style scoped> <v-card class="elevation-12">
<v-toolbar color="primary" dark flat>
</style> <v-toolbar-title>Connexion</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form @submit.prevent="handleLogin">
<v-text-field
v-model="username"
:rules="[rules.required]"
label="Nom d'utilisateur"
name="username"
prepend-icon="mdi-account"
required
type="text"
/>
<v-text-field
v-model="password"
:rules="[rules.required]"
label="Mot de passe"
name="password"
prepend-icon="mdi-lock"
required
type="password"
/>
<v-alert v-if="error" class="mt-3" density="compact" type="error">
{{ error }}
</v-alert>
<v-card-actions class="mt-4">
<v-spacer/>
<v-btn
:loading="loading"
block
color="primary"
size="large"
type="submit"
>
Se connecter
</v-btn>
</v-card-actions>
<div class="text-center mt-4">
<router-link to="/join">Pas encore de compte ? Rejoindre</router-link>
</div>
</v-form>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
+12 -6
View File
@@ -1,26 +1,32 @@
// frontend/src/utils/api.ts
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
export async function apiFetch(endpoint: string, options: RequestInit = {}) { export async function apiFetch(endpoint: string, options: RequestInit = {}) {
const authStore = useAuthStore() const authStore = useAuthStore()
const baseUrl = '/api' // Votre préfixe configuré dans mod.rs
// Remplacer par l'URL réelle de votre backend Rust
const baseUrl = 'http://localhost:8080/api'
const headers = new Headers(options.headers) const headers = new Headers(options.headers)
headers.set('Content-Type', 'application/json') headers.set('Content-Type', 'application/json')
// On injecte le token s'il existe
if (authStore.token) { if (authStore.token) {
headers.set('Authorization', `Bearer ${authStore.token}`) headers.set('Authorization', `Bearer ${authStore.token}`)
} }
const response = await fetch(`${baseUrl}${endpoint}`, { // Si un body est fourni et est un objet, on le stringify automatiquement
const config = {
...options, ...options,
headers, headers,
}) body: options.body && typeof options.body === 'object'
? JSON.stringify(options.body)
: options.body
}
const response = await fetch(`${baseUrl}${endpoint}`, config)
// Gestion automatique de l'expiration du token (401 Unauthorized)
if (response.status === 401) { if (response.status === 401) {
authStore.logout() authStore.logout()
// Optionnel : rediriger vers /login
window.location.href = '/login' window.location.href = '/login'
} }
+11 -2
View File
@@ -18,6 +18,11 @@ const router = createRouter({
name: 'login', name: 'login',
component: () => import('@/pages/login.vue'), component: () => import('@/pages/login.vue'),
}, },
{
path: '/join',
name: 'join',
component: () => import('@/pages/join.vue'),
},
{ {
path: '/', path: '/',
component: Index, component: Index,
@@ -31,9 +36,13 @@ const router = createRouter({
], ],
}) })
router.beforeEach((to, from, next) => { router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore() const authStore = useAuthStore()
const publicPages = ['login', 'register'] if (!authStore.isInitialized) {
await authStore.initialize()
}
const publicPages = ['login', 'join']
const authRequired = !publicPages.includes(to.name as string) const authRequired = !publicPages.includes(to.name as string)
const adminRequired = to.matched.some(record => record.meta.requiresAdmin) const adminRequired = to.matched.some(record => record.meta.requiresAdmin)
+41 -29
View File
@@ -1,52 +1,64 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {apiFetch} from "@/plugins/api.ts";
interface UserClaims { export interface User {
user_id: string id: string
username: string username: string
is_admin: boolean // On s'assure que le backend l'envoie ou on le déduit pub_key: string | null
exp: number is_superuser: boolean
created_at: string
updated_at: string
} }
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
state: () => ({ state: () => ({
token: localStorage.getItem('token') || null as string | null, token: localStorage.getItem('token') || null as string | null,
user: JSON.parse(localStorage.getItem('user') || 'null') as UserClaims | null, user: null as User | null,
isInitialized: false,
}), }),
getters: { getters: {
isAuthenticated: (state) => !!state.token, isAuthenticated: (state) => !!state.token && !!state.user,
isAdmin: (state) => state.user?.is_admin || false, isAdmin: (state) => state.user?.is_superuser || false,
currentUser: (state) => state.user,
}, },
actions: { actions: {
async initialize() {
if (!this.token) {
this.isInitialized = true
return
}
try {
const response = await apiFetch('/auth/me', {method: 'GET'})
if (response.ok) {
const data = await response.json()
// On s'attend à ce que /auth/check renvoie l'objet user complet
this.user = data.user
} else {
this.logout()
}
} catch (e) {
console.error("Auth initialization failed", e)
this.logout()
} finally {
this.isInitialized = true
}
},
setToken(token: string) { setToken(token: string) {
this.token = token this.token = token
localStorage.setItem('token', token) localStorage.setItem('token', token)
// On déclenche la récupération des infos utilisateur immédiatement
try { return this.initialize()
// Décoder le payload du JWT (2ème partie du string)
const base64Url = token.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
}).join(''))
const decoded = JSON.parse(jsonPayload)
this.user = {
user_id: decoded.user_id,
username: decoded.username,
is_admin: decoded.is_superuser || false, // Vérifiez le nom du champ dans votre Claims Rust
exp: decoded.expire_at
}
localStorage.setItem('user', JSON.stringify(this.user))
} catch (e) {
console.error("Failed to decode token", e)
this.logout()
}
}, },
logout() { logout() {
this.token = null this.token = null
this.user = null this.user = null
localStorage.removeItem('token') localStorage.removeItem('token')
localStorage.removeItem('user') // On ne redirige pas ici pour laisser le router ou le composant décider
} }
} }
}) })
+1 -1
View File
@@ -12,7 +12,7 @@ path = "src/lib.rs"
async-std = { version = "1", features = ["attributes", "tokio1"] } async-std = { version = "1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration] [dependencies.sea-orm-migration]
version = "2.0.0-rc.38" version = "2.0.0-rc.40"
features = [ features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
@@ -174,7 +174,7 @@ impl MigrationTrait for Migration {
.col( .col(
ColumnDef::new(Alias::new("pub_key")) ColumnDef::new(Alias::new("pub_key"))
.text() .text()
.not_null() .null()
.unique_key(), .unique_key(),
) )
.col( .col(
+1 -1
View File
@@ -6,7 +6,7 @@ use argon2::{
/// Hache un password avec Argon2id /// Hache un password avec Argon2id
/// Génère automatiquement un salt cryptographiquement sûr /// Génère automatiquement un salt cryptographiquement sûr
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> { pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
let params = Params::new(65540, 18, 1, None)?; let params = Params::new(65540, 3, 4, None)?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
argon2 argon2
+8 -11
View File
@@ -5,11 +5,9 @@ use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Claims { pub struct Claims {
pub user_id: Uuid, // User ID pub user_id: Uuid, // User ID
pub expire_at: usize, // Expiration time pub exp: usize, // Changé de expire_at -> exp (Standard JWT)
pub created_at: usize, // Issued at pub iat: usize, // Changé de created_at -> iat (Standard JWT)
pub username: String,
pub is_superuser: bool, // Ajoutez ce champ
} }
pub fn create_jwt( pub fn create_jwt(
@@ -25,11 +23,9 @@ pub fn create_jwt(
.as_secs(); .as_secs();
let claims = Claims { let claims = Claims {
user_id: user_id, user_id,
expire_at: (now + expiration_seconds) as usize, exp: (now + expiration_seconds) as usize,
created_at: now as usize, iat: now as usize,
username: username.to_string(),
is_superuser, // Et ici
}; };
encode( encode(
@@ -40,12 +36,13 @@ pub fn create_jwt(
} }
pub fn verify_jwt(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> { pub fn verify_jwt(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
println!("Verifying token: {}", token);
let validation = Validation::default(); let validation = Validation::default();
let token_data = decode::<Claims>( let token_data = decode::<Claims>(
token, token,
&DecodingKey::from_secret(secret.as_ref()), &DecodingKey::from_secret(secret.as_ref()),
&validation, &validation,
)?; )?;
println!("Token data: {:?}", token_data);
Ok(token_data.claims) Ok(token_data.claims)
} }
+2 -2
View File
@@ -9,7 +9,7 @@ use crate::udp::server::UdpServer;
use event_bus::EventBus; use event_bus::EventBus;
use migration::{Migrator, MigratorTrait}; use migration::{Migrator, MigratorTrait};
pub use state::AppState; pub use state::AppState;
use std::sync::Arc; use std::sync::{Arc, RwLock};
use std::time::Duration; use std::time::Duration;
use uuid::Uuid; use uuid::Uuid;
@@ -66,7 +66,7 @@ impl App {
db, db,
config: Arc::new(config), config: Arc::new(config),
repositories, repositories,
init_token, init_token: Arc::new(RwLock::new(init_token)),
default_server: Arc::new(default_server), default_server: Arc::new(default_server),
metrics, metrics,
}; };
+2 -2
View File
@@ -3,14 +3,14 @@ use crate::metrics::AppMetrics;
use crate::models::server; use crate::models::server;
use crate::repositories::Repositories; use crate::repositories::Repositories;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use std::sync::Arc; use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AppState { pub struct AppState {
pub db: DatabaseConnection, pub db: DatabaseConnection,
pub config: Arc<AppConfig>, pub config: Arc<AppConfig>,
pub repositories: Repositories, pub repositories: Repositories,
pub init_token: Option<uuid::Uuid>, pub init_token: Arc<RwLock<Option<uuid::Uuid>>>,
pub default_server: Arc<server::Model>, pub default_server: Arc<server::Model>,
pub metrics: AppMetrics, pub metrics: AppMetrics,
} }
+3 -1
View File
@@ -79,7 +79,9 @@ impl IntoResponse for HTTPError {
.into_response(); .into_response();
} }
HTTPError::Internal(err) => { HTTPError::Internal(err) => {
tracing::error!(error = ?err, "An unexpected error occurred"); // On utilise %err pour un message d'erreur clair sans backtrace brute
// mais on garde les détails pour le span tracing si besoin.
tracing::error!(%err, "Request error");
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error") (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
} }
}; };
+25 -3
View File
@@ -1,5 +1,5 @@
use axum::{ use axum::{
body::Body, body::{Body, HttpBody},
extract::State, extract::State,
http::{header, Request}, http::{header, Request},
middleware::Next, middleware::Next,
@@ -55,8 +55,30 @@ pub async fn request_context_middleware(mut req: Request<Body>, next: Next) -> R
); );
async move { async move {
info!("Incoming request"); let response = next.run(req).await;
next.run(req).await let elapsed = started_at.elapsed();
let status = response.status();
let size = response
.body()
.size_hint()
.exact()
.map(|s| s.to_string())
.or_else(|| {
response
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
.unwrap_or_else(|| "0".to_string());
if status.is_server_error() {
tracing::error!(%status, %size, ?elapsed, "Request failed");
} else {
info!("{} {}b in {:?}", status, size, elapsed);
}
response
} }
.instrument(span) .instrument(span)
.await .await
+2 -2
View File
@@ -11,8 +11,8 @@ pub struct Model {
pub id: Uuid, pub id: Uuid,
pub username: String, pub username: String,
pub password: String, pub password: String,
#[sea_orm(column_type = "Text", unique)] #[sea_orm(column_type = "Text", unique, nullable)]
pub pub_key: String, pub pub_key: Option<String>,
pub created_at: DateTimeUtc, pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc, pub updated_at: DateTimeUtc,
pub is_superuser: bool, pub is_superuser: bool,
+6 -6
View File
@@ -1,19 +1,19 @@
use crate::routes::user::dto::UserResponse;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema; use utoipa::ToSchema;
#[derive(Deserialize, ToSchema)] #[derive(Debug, Deserialize, ToSchema)]
pub struct LoginRequest { pub struct LoginRequest {
pub username: String, pub username: String,
pub password: String, pub password: String,
} }
#[derive(Serialize, ToSchema)] #[derive(Debug, Serialize, ToSchema)]
pub struct LoginResponse { pub struct LoginResponse {
pub token: String, pub token: String,
pub username: String,
} }
#[derive(Serialize, ToSchema)] #[derive(Debug, Serialize, ToSchema)]
pub struct CheckResponse { pub struct MeResponse {
pub authenticated: bool, pub user: UserResponse,
} }
+16 -13
View File
@@ -1,14 +1,17 @@
use super::dto::{CheckResponse, LoginRequest, LoginResponse}; use super::dto::{LoginRequest, LoginResponse, MeResponse};
use crate::auth::token::create_jwt; use crate::auth::token::create_jwt;
use crate::core::AppState; use crate::core::AppState;
use crate::http::context::CurrentUser; use crate::http::context::CurrentUser;
use crate::http::error::HTTPError; use crate::http::error::HTTPError;
use crate::routes::user::mapper::user_model_to_user_response;
use axum::extract::State; use axum::extract::State;
use axum::Json; use axum::Json;
use sea_orm::ActiveModelBehavior;
#[utoipa::path( #[utoipa::path(
post, get,
path = "/auth/login", path = "/auth/login",
request_body = LoginRequest,
responses( responses(
(status = 200, description = "Login successful", body = LoginResponse), (status = 200, description = "Login successful", body = LoginResponse),
(status = 401, description = "Unauthorized") (status = 401, description = "Unauthorized")
@@ -29,22 +32,20 @@ pub async fn login_user_pw(
let token = create_jwt( let token = create_jwt(
user.id, user.id,
&user.username, &user.username,
user.is_superuser,
&state.config.jwt.secret, &state.config.jwt.secret,
state.config.jwt.duration, state.config.jwt.duration,
) )
.map_err(|_| HTTPError::InternalServerError("Failed to create JWT token".to_string()))?; .map_err(|_| HTTPError::InternalServerError("Failed to create JWT token".to_string()))?;
Ok(Json(LoginResponse { Ok(Json(LoginResponse { token }))
username: user.username,
token,
}))
} }
#[utoipa::path( #[utoipa::path(
post, post,
path = "/auth/check", path = "/auth/me",
responses( responses(
(status = 200, description = "Login successful", body = LoginResponse), (status = 200, description = "Token valid", body = LoginResponse),
(status = 401, description = "Unauthorized") (status = 401, description = "Unauthorized")
), ),
security( security(
@@ -52,11 +53,13 @@ pub async fn login_user_pw(
), ),
tag = "Auth" tag = "Auth"
)] )]
pub async fn check( pub async fn me(
State(_state): State<AppState>, State(_state): State<AppState>,
_user: CurrentUser, CurrentUser(user): CurrentUser,
) -> Result<Json<CheckResponse>, HTTPError> { ) -> Result<Json<MeResponse>, HTTPError> {
Ok(Json(CheckResponse { let user_response = user_model_to_user_response(user);
authenticated: true,
Ok(Json(MeResponse {
user: user_response,
})) }))
} }
+2 -2
View File
@@ -1,10 +1,10 @@
use crate::http::OxRouter; use crate::http::OxRouter;
use crate::routes::auth::handlers; use crate::routes::auth::handlers;
use axum::routing::post; use axum::routing::{get, post};
use axum::Router; use axum::Router;
pub fn router() -> OxRouter { pub fn router() -> OxRouter {
Router::new() Router::new()
.route("/auth/login", post(handlers::login_user_pw)) .route("/auth/login", post(handlers::login_user_pw))
.route("/auth/check", post(handlers::check)) .route("/auth/me", get(handlers::me))
} }
+15 -1
View File
@@ -34,10 +34,24 @@ pub async fn join(
)); ));
}; };
let user_am = join_request_to_user_am(payload, state.init_token)?; let user_am = {
let init_token_lock = state
.init_token
.read()
.map_err(|e| HTTPError::InternalServerError(e.to_string()))?;
join_request_to_user_am(payload, *init_token_lock)?
};
let user = state.repositories.user.create(user_am).await?; let user = state.repositories.user.create(user_am).await?;
if user.is_superuser {
let mut init_token_lock = state
.init_token
.write()
.map_err(|e| HTTPError::InternalServerError(e.to_string()))?;
*init_token_lock = None;
}
state state
.repositories .repositories
.server .server
+4 -5
View File
@@ -9,13 +9,12 @@ pub fn join_request_to_user_am(
payload: JoinRequest, payload: JoinRequest,
superuser_token: Option<Uuid>, superuser_token: Option<Uuid>,
) -> AnyResult<user::ActiveModel> { ) -> AnyResult<user::ActiveModel> {
let is_super_admin = match (payload.superuser_token.as_ref(), superuser_token) { let mut is_super_admin = false;
(Some(provided), Some(init)) => provided == &init.to_string(), if let (Some(provided), Some(init)) = (payload.superuser_token.as_ref(), superuser_token) {
_ => false, is_super_admin = provided == &init.to_string();
}; }
Ok(user::ActiveModel { Ok(user::ActiveModel {
id: Default::default(),
username: Set(payload.username), username: Set(payload.username),
password: Set(hash_password(&payload.password)?), password: Set(hash_password(&payload.password)?),
is_superuser: Set(is_super_admin), is_superuser: Set(is_super_admin),
+5 -4
View File
@@ -29,9 +29,10 @@ pub fn router() -> OxRouter {
// Routes publiques (ou gérant leur propre auth) // Routes publiques (ou gérant leur propre auth)
let api_routes = Router::new() let api_routes = Router::new()
.merge(secure_routes) .merge(secure_routes)
.merge(auth::routes::router()); .merge(auth::routes::router())
.merge(core::routes::router());
Router::new().nest("/api", api_routes).merge( Router::new()
SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", openapi::ApiDoc::openapi()), .nest("/api", api_routes)
) .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", openapi::ApiDoc::openapi()))
} }
+2 -2
View File
@@ -7,7 +7,7 @@ use utoipa::{Modify, OpenApi};
#[openapi( #[openapi(
paths( paths(
auth::handlers::login_user_pw, auth::handlers::login_user_pw,
auth::handlers::check, auth::handlers::me,
user::handlers::get_all, user::handlers::get_all,
user::handlers::get_by_id, user::handlers::get_by_id,
user::handlers::create, user::handlers::create,
@@ -44,7 +44,7 @@ use utoipa::{Modify, OpenApi};
schemas( schemas(
auth::dto::LoginRequest, auth::dto::LoginRequest,
auth::dto::LoginResponse, auth::dto::LoginResponse,
auth::dto::CheckResponse, auth::dto::MeResponse,
user::dto::UserResponse, user::dto::UserResponse,
user::dto::CreateUserRequest, user::dto::CreateUserRequest,
user::dto::UpdateUserRequest, user::dto::UpdateUserRequest,
+3 -3
View File
@@ -7,14 +7,14 @@ use uuid::Uuid;
pub struct CreateUserRequest { pub struct CreateUserRequest {
pub username: String, pub username: String,
pub password: String, pub password: String,
pub pub_key: String, pub pub_key: Option<String>,
pub is_superuser: bool, pub is_superuser: bool,
} }
#[derive(Debug, Serialize, Deserialize, ToSchema)] #[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UpdateUserRequest { pub struct UpdateUserRequest {
pub username: String, pub username: String,
pub pub_key: String, pub pub_key: Option<String>,
pub is_superuser: bool, pub is_superuser: bool,
} }
@@ -22,7 +22,7 @@ pub struct UpdateUserRequest {
pub struct UserResponse { pub struct UserResponse {
pub id: Uuid, pub id: Uuid,
pub username: String, pub username: String,
pub pub_key: String, pub pub_key: Option<String>,
pub is_superuser: bool, pub is_superuser: bool,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,