Init
This commit is contained in:
Generated
+238
-420
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -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"
|
||||||
@@ -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"] }
|
||||||
@@ -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: ["**/*"],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
Change this page by updating
|
Change this page by updating
|
||||||
<v-kbd>{{
|
<v-kbd>{{
|
||||||
`
|
`
|
||||||
|
|
||||||
<HelloWorld/>
|
<HelloWorld/>
|
||||||
` }}
|
` }}
|
||||||
</v-kbd>
|
</v-kbd>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
<v-row align="center" justify="center">
|
||||||
|
<v-col cols="12" md="4" sm="8">
|
||||||
|
<v-card class="elevation-12">
|
||||||
|
<v-toolbar color="primary" dark flat>
|
||||||
|
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
Reference in New Issue
Block a user