This commit is contained in:
2026-06-28 18:12:00 +02:00
parent 5152ec0f7e
commit 7a593fc204
27 changed files with 413 additions and 100 deletions
+2 -6
View File
@@ -1,11 +1,7 @@
<template>
<v-app>
<v-main>
<router-view />
</v-main>
</v-app>
<router-view/>
</template>
<script lang="ts" setup>
//
//
</script>
+5 -4
View File
@@ -4,7 +4,8 @@ import {useAuthStore} from "@/stores/auth";
export function useApi() {
const appStore = useAppStore()
const authStore = useAuthStore()
const baseUrl = `${appStore.baseurl}/api`
// const baseUrl = `${appStore.baseurl}/api`
const baseUrl = '/api'
const request = async (endpoint: string, options: RequestInit = {}) => {
const headers = new Headers(options.headers)
@@ -13,9 +14,9 @@ export function useApi() {
headers.set('Content-Type', 'application/json')
}
if (authStore.token) {
headers.set('Authorization', `Bearer ${authStore.token}`)
}
// if (authStore.token) {
// headers.set('Authorization', `Bearer ${authStore.token}`)
// }
const config = {
...options,
+32
View File
@@ -0,0 +1,32 @@
<script lang="ts" setup>
</script>
<template>
<v-app class="admin-layout">
<v-app-bar color="error" dark>
<v-app-bar-title>Console d'Administration</v-app-bar-title>
<v-spacer></v-spacer>
<v-btn prepend-icon="mdi-arrow-left" to="/">Retour à l'App</v-btn>
</v-app-bar>
<v-navigation-drawer permanent>
<v-list>
<v-list-item subtitle="Contrôle" title="Admin Panel"></v-list-item>
<v-divider></v-divider>
<v-list-item prepend-icon="mdi-view-dashboard" title="Dashboard" to="/admin"></v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<v-container fluid>
<!-- Les pages d'administration s'injecteront ici -->
<router-view/>
</v-container>
</v-main>
</v-app>
</template>
<style scoped>
</style>
+65
View File
@@ -0,0 +1,65 @@
<script lang="ts" setup>
</script>
<template>
<v-app>
<!--Top bar-->
<v-system-bar>
<v-spacer></v-spacer>
<v-icon>mdi-square</v-icon>
<v-icon>mdi-circle</v-icon>
<v-icon>mdi-triangle</v-icon>
</v-system-bar>
<v-navigation-drawer
color="grey-lighten-3"
rail
>
<v-avatar
class="d-block text-center mx-auto mt-4"
color="grey-darken-1"
size="36"
></v-avatar>
<v-divider class="mx-3 my-5"></v-divider>
<v-avatar
v-for="n in 6"
:key="n"
class="d-block text-center mx-auto mb-9"
color="grey-lighten-1"
size="28"
></v-avatar>
</v-navigation-drawer>
<v-navigation-drawer width="244">
<v-sheet
color="grey-lighten-5"
height="128"
width="100%"
></v-sheet>
<v-list>
<v-list-item
v-for="n in 5"
:key="n"
:title="`Item ${ n }`"
link
></v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<!-- Les pages de l'application s'injecteront ici -->
<router-view/>
</v-main>
</v-app>
</template>
<style scoped>
</style>
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts" setup>
</script>
<template>
<v-app class="auth-layout">
<v-main>
<!-- On injecte directement les pages qui gèrent déjà leur propre centrage et v-card -->
<router-view/>
</v-main>
</v-app>
</template>
<style scoped>
</style>
@@ -137,7 +137,7 @@ async function handleRegister() {
</v-btn>
</v-card-actions>
<div class="text-center mt-2">
<router-link to="/login">Déjà un compte ? Connectez-vous</router-link>
<router-link to="/auth/login">Déjà un compte ? Connectez-vous</router-link>
</div>
</v-form>
</v-card-text>
@@ -93,7 +93,7 @@ async function handleLogin() {
</v-btn>
</v-card-actions>
<div class="text-center mt-4">
<router-link to="/join">Pas encore de compte ? Rejoindre</router-link>
<router-link to="/auth/join">Pas encore de compte ? Rejoindre</router-link>
</div>
</v-form>
</v-card-text>
+54 -27
View File
@@ -6,48 +6,75 @@
// Composables
import {createRouter, createWebHistory} from 'vue-router'
import Index from '@/pages/index.vue'
import {useAuthStore} from '@/stores/auth'
import AuthLayout from "@/layouts/AuthLayout.vue";
import AppLayout from "@/layouts/AppLayout.vue";
import AdminLayout from "@/layouts/AdminLayout.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// ----------------- GROUPE AUTHENTIFICATION -----------------
{
path: '/login',
name: 'login',
component: () => import('@/pages/login.vue'),
},
{
path: '/join',
name: 'join',
component: () => import('@/pages/join.vue'),
},
{
path: '/',
component: Index,
},
{
// Cette regex capture soit un UUID, soit le mot "default"
path: '/server/:id(default|[0-9a-fA-F-]{36})',
name: 'server-dashboard',
component: () => import('@/pages/server/index.vue'),
props: true,
path: '/auth',
component: AuthLayout,
children: [
{
path: 'channel/:channelId',
name: 'server-channel',
component: () => import('@/pages/server/channel/index.vue'),
path: 'login',
name: 'login',
component: () => import('@/pages/auth/login.vue'),
},
{
path: 'join',
name: 'join',
component: () => import('@/pages/auth/join.vue'),
},
// Vous pouvez ajouter ici 'reset-password', etc.
],
},
// ----------------- GROUPE APPLICATION GÉNÉRALE -----------------
{
path: '/',
component: AppLayout,
children: [
{
path: '',
name: 'home',
component: () => import('@/pages/index.vue'),
},
{
path: 'server/:id(default|[0-9a-fA-F-]{36})',
name: 'server-dashboard',
component: () => import('@/pages/server/index.vue'),
props: true,
children: [
{
path: 'channel/:channelId',
name: 'server-channel',
component: () => import('@/pages/server/channel/index.vue'),
props: true,
},
],
},
],
},
// ----------------- GROUPE ADMINISTRATION -----------------
{
path: '/admin',
name: 'admin-dashboard',
component: () => import('@/pages/admin/dashboard.vue'),
meta: {requiresAdmin: true}
component: AdminLayout,
meta: {requiresAdmin: true},
children: [
{
path: '',
name: 'admin-dashboard',
component: () => import('@/pages/admin/dashboard.vue'),
},
// Ajoutez d'autres pages admin ici
],
},
],
})
@@ -64,7 +91,7 @@ router.beforeEach(async (to, from, next) => {
if (authRequired && !authStore.isAuthenticated) {
// Non connecté -> Login
next('/login')
next('/auth/login')
} else if (to.name === 'login' && authStore.isAuthenticated) {
// Déjà connecté -> Accueil
next('/')
+4 -1
View File
@@ -34,8 +34,11 @@ export const useAppStore = defineStore('app', {
// On s'abonne à l'événement de connexion de la gateway
window.addEventListener('gateway:connected', loadAppData)
// On connecte la gateway (qui déclenchera 'gateway:connected')
await gatewayStore.connect()
// Initialisation de l'authentification (qui lancera la connexion à la gateway)
await authStore.initialize()
// await authStore.initialize()
// Sécurité : si la gateway s'est déjà connectée entre-temps
if (gatewayStore.status === 'connected') {
+68 -26
View File
@@ -1,6 +1,5 @@
import {defineStore} from 'pinia'
import {useApi} from "@/composables/useApi";
import {useGatewayStore} from "@/stores/gateway.ts";
export interface User {
id: string
@@ -13,57 +12,100 @@ export interface User {
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('token') || null as string | null,
// Plus de token stocké dans le localStorage !
user: null as User | null,
isInitialized: false,
// Nous ne gardons ce token en mémoire que pour la connexion au WebSocket
// gatewayToken: null as string | null,
}),
getters: {
isAuthenticated: (state) => !!state.token && !!state.user,
isAuthenticated: (state) => !!state.user,
isAdmin: (state) => state.user?.is_superuser || false,
currentUser: (state) => state.user,
},
actions: {
async initialize() {
if (!this.token) {
this.isInitialized = true
return
}
const api = useApi()
try {
const response = await api.get('/auth/me')
if (response.ok) {
const data = await response.json()
this.user = data.user
// Initialisation du gateway
const gatewayStore = useGatewayStore()
await gatewayStore.connect()
} else {
this.logout()
}
} catch (e) {
console.error("Auth initialization failed", e)
this.logout()
// this.logout()
} finally {
this.isInitialized = true
}
},
setToken(token: string) {
this.token = token
localStorage.setItem('token', token)
// On déclenche la récupération des infos utilisateur immédiatement
return this.initialize()
},
logout() {
this.token = null
const api = useApi()
this.user = null
localStorage.removeItem('token')
// On ne redirige pas ici pour laisser le router ou le composant décider
api.post('/auth/logout')
// Note : si vous implémentez une route /auth/logout côté Axum,
// elle devra nettoyer le cookie en renvoyant un Set-Cookie expiré.
}
}
})
})
////// Version with token + local storage
// export const useAuthStore = defineStore('auth', {
// state: () => ({
// token: localStorage.getItem('token') || null as string | null,
// user: null as User | null,
// isInitialized: false,
// }),
//
// getters: {
// 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
// }
//
// const api = useApi()
// try {
// const response = await api.get('/auth/me')
// if (response.ok) {
// const data = await response.json()
// this.user = data.user
//
// // Initialisation du gateway
// const gatewayStore = useGatewayStore()
// await gatewayStore.connect()
// } 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)
// // 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')
// // On ne redirige pas ici pour laisser le router ou le composant décider
// }
// }
// })
+1 -1
View File
@@ -8,7 +8,7 @@ export const useCategoryStore = defineStore("category", {
actions: {
async fetchCategories() {
let api = useApi();
let response = await api.get("/api/categories");
let response = await api.get("/categories");
this.categories = await response.json();
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ export const useChannelStore = defineStore('channel', {
actions: {
async fetchChannels() {
let api = useApi();
let response = await api.get("/api/channels");
let response = await api.get("/channels");
this.channels = await response.json();
}
}
+10 -10
View File
@@ -1,6 +1,5 @@
import {defineStore} from 'pinia';
import {useAppStore} from "@/stores/app.ts";
import {useAuthStore} from "@/stores/auth.ts";
type GatewayStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
@@ -18,20 +17,21 @@ export const useGatewayStore = defineStore('gateway', {
}
const appStore = useAppStore()
const authStore = useAuthStore()
// const authStore = useAuthStore()
this.status = 'connecting'
const token = authStore.token
if (!token) {
this.status = 'error'
return
}
// version token
// const token = authStore.token
// if (!token) {
// this.status = 'error'
// return
// }
const apiUri = appStore.baseurl ? new URL(appStore.baseurl) : new URL(window.location.href)
const wsProtocol = apiUri.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway?token=${encodeURIComponent(token)}`
// version token
// const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway?token=${encodeURIComponent(token)}`
const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway`
const socket = new WebSocket(wsUrl)
socket.onopen = () => {
+1 -1
View File
@@ -8,7 +8,7 @@ export const useServerStore = defineStore("server", {
actions: {
async fetchServers() {
let api = useApi();
const response = await api.get("/api/servers");
const response = await api.get("/servers");
this.servers = await response.json();
}
}
+11 -5
View File
@@ -1,14 +1,14 @@
import { fileURLToPath, URL } from 'node:url'
import {fileURLToPath, URL} from 'node:url'
import Vue from '@vitejs/plugin-vue'
import Fonts from 'unplugin-fonts/vite'
import { defineConfig } from 'vite'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import {defineConfig} from 'vite'
import Vuetify, {transformAssetUrls} from 'vite-plugin-vuetify'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
Vue({
template: { transformAssetUrls },
template: {transformAssetUrls},
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
Vuetify({
@@ -29,7 +29,7 @@ export default defineConfig({
},
}),
],
define: { 'process.env': {} },
define: {'process.env': {}},
resolve: {
alias: {
'@': fileURLToPath(new URL('src', import.meta.url)),
@@ -46,5 +46,11 @@ export default defineConfig({
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})