Init
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
Change this page by updating
|
||||
<v-kbd>{{
|
||||
`
|
||||
|
||||
<HelloWorld/>
|
||||
` }}
|
||||
</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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<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>
|
||||
@@ -1,26 +1,32 @@
|
||||
// frontend/src/utils/api.ts
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
export async function apiFetch(endpoint: string, options: RequestInit = {}) {
|
||||
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)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
|
||||
// On injecte le token s'il existe
|
||||
if (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,
|
||||
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) {
|
||||
authStore.logout()
|
||||
// Optionnel : rediriger vers /login
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ const router = createRouter({
|
||||
name: 'login',
|
||||
component: () => import('@/pages/login.vue'),
|
||||
},
|
||||
{
|
||||
path: '/join',
|
||||
name: 'join',
|
||||
component: () => import('@/pages/join.vue'),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: Index,
|
||||
@@ -31,9 +36,13 @@ const router = createRouter({
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
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 adminRequired = to.matched.some(record => record.meta.requiresAdmin)
|
||||
|
||||
|
||||
+41
-29
@@ -1,52 +1,64 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {apiFetch} from "@/plugins/api.ts";
|
||||
|
||||
interface UserClaims {
|
||||
user_id: string
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
is_admin: boolean // On s'assure que le backend l'envoie ou on le déduit
|
||||
exp: number
|
||||
pub_key: string | null
|
||||
is_superuser: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
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: {
|
||||
isAuthenticated: (state) => !!state.token,
|
||||
isAdmin: (state) => state.user?.is_admin || false,
|
||||
isAuthenticated: (state) => !!state.token && !!state.user,
|
||||
isAdmin: (state) => state.user?.is_superuser || false,
|
||||
currentUser: (state) => state.user,
|
||||
},
|
||||
|
||||
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) {
|
||||
this.token = token
|
||||
localStorage.setItem('token', token)
|
||||
|
||||
try {
|
||||
// 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()
|
||||
}
|
||||
// On déclenche la récupération des infos utilisateur immédiatement
|
||||
return this.initialize()
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.token = null
|
||||
this.user = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
// On ne redirige pas ici pour laisser le router ou le composant décider
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user