Init
This commit is contained in:
Generated
+35
@@ -583,6 +583,28 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-extra"
|
||||||
|
version = "0.12.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -935,6 +957,17 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -2346,6 +2379,7 @@ dependencies = [
|
|||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"chrono",
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
@@ -2360,6 +2394,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"tower",
|
"tower",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ members = [".", "migration", "event_bus"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.52.3", features = ["full"] }
|
tokio = { version = "1.52.3", features = ["full"] }
|
||||||
axum = { version = "0.8", features = ["ws"] }
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
|
axum-extra = { version = "0.12.6", features = ["cookie"] }
|
||||||
config = "0.15.24"
|
config = "0.15.24"
|
||||||
sea-orm = { version = "2.0.0-rc.41", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] }
|
sea-orm = { version = "2.0.0-rc.41", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] }
|
||||||
migration = { path = "migration" }
|
migration = { path = "migration" }
|
||||||
@@ -39,3 +40,4 @@ async-trait = "0.1.89"
|
|||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
form_urlencoded = "1.2.2"
|
form_urlencoded = "1.2.2"
|
||||||
|
time = "0.3.47"
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<router-view/>
|
||||||
<v-main>
|
|
||||||
<router-view />
|
|
||||||
</v-main>
|
|
||||||
</v-app>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
//
|
//
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {useAuthStore} from "@/stores/auth";
|
|||||||
export function useApi() {
|
export function useApi() {
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const baseUrl = `${appStore.baseurl}/api`
|
// const baseUrl = `${appStore.baseurl}/api`
|
||||||
|
const baseUrl = '/api'
|
||||||
|
|
||||||
const request = async (endpoint: string, options: RequestInit = {}) => {
|
const request = async (endpoint: string, options: RequestInit = {}) => {
|
||||||
const headers = new Headers(options.headers)
|
const headers = new Headers(options.headers)
|
||||||
@@ -13,9 +14,9 @@ export function useApi() {
|
|||||||
headers.set('Content-Type', 'application/json')
|
headers.set('Content-Type', 'application/json')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authStore.token) {
|
// if (authStore.token) {
|
||||||
headers.set('Authorization', `Bearer ${authStore.token}`)
|
// headers.set('Authorization', `Bearer ${authStore.token}`)
|
||||||
}
|
// }
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
<div class="text-center mt-2">
|
<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>
|
</div>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -93,7 +93,7 @@ async function handleLogin() {
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
<div class="text-center mt-4">
|
<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>
|
</div>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -6,48 +6,75 @@
|
|||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
import {createRouter, createWebHistory} from 'vue-router'
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
import Index from '@/pages/index.vue'
|
|
||||||
|
|
||||||
import {useAuthStore} from '@/stores/auth'
|
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({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
|
// ----------------- GROUPE AUTHENTIFICATION -----------------
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/auth',
|
||||||
name: 'login',
|
component: AuthLayout,
|
||||||
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,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'channel/:channelId',
|
path: 'login',
|
||||||
name: 'server-channel',
|
name: 'login',
|
||||||
component: () => import('@/pages/server/channel/index.vue'),
|
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,
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'channel/:channelId',
|
||||||
|
name: 'server-channel',
|
||||||
|
component: () => import('@/pages/server/channel/index.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ----------------- GROUPE ADMINISTRATION -----------------
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
name: 'admin-dashboard',
|
component: AdminLayout,
|
||||||
component: () => import('@/pages/admin/dashboard.vue'),
|
meta: {requiresAdmin: true},
|
||||||
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) {
|
if (authRequired && !authStore.isAuthenticated) {
|
||||||
// Non connecté -> Login
|
// Non connecté -> Login
|
||||||
next('/login')
|
next('/auth/login')
|
||||||
} else if (to.name === 'login' && authStore.isAuthenticated) {
|
} else if (to.name === 'login' && authStore.isAuthenticated) {
|
||||||
// Déjà connecté -> Accueil
|
// Déjà connecté -> Accueil
|
||||||
next('/')
|
next('/')
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ export const useAppStore = defineStore('app', {
|
|||||||
// On s'abonne à l'événement de connexion de la gateway
|
// On s'abonne à l'événement de connexion de la gateway
|
||||||
window.addEventListener('gateway:connected', loadAppData)
|
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)
|
// 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
|
// Sécurité : si la gateway s'est déjà connectée entre-temps
|
||||||
if (gatewayStore.status === 'connected') {
|
if (gatewayStore.status === 'connected') {
|
||||||
|
|||||||
+67
-25
@@ -1,6 +1,5 @@
|
|||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {useApi} from "@/composables/useApi";
|
import {useApi} from "@/composables/useApi";
|
||||||
import {useGatewayStore} from "@/stores/gateway.ts";
|
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string
|
id: string
|
||||||
@@ -13,57 +12,100 @@ export interface User {
|
|||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
token: localStorage.getItem('token') || null as string | null,
|
// Plus de token stocké dans le localStorage !
|
||||||
user: null as User | null,
|
user: null as User | null,
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
|
// Nous ne gardons ce token en mémoire que pour la connexion au WebSocket
|
||||||
|
// gatewayToken: null as string | null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
isAuthenticated: (state) => !!state.token && !!state.user,
|
isAuthenticated: (state) => !!state.user,
|
||||||
isAdmin: (state) => state.user?.is_superuser || false,
|
isAdmin: (state) => state.user?.is_superuser || false,
|
||||||
currentUser: (state) => state.user,
|
currentUser: (state) => state.user,
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async initialize() {
|
async initialize() {
|
||||||
if (!this.token) {
|
|
||||||
this.isInitialized = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/auth/me')
|
const response = await api.get('/auth/me')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
this.user = data.user
|
this.user = data.user
|
||||||
|
|
||||||
// Initialisation du gateway
|
|
||||||
const gatewayStore = useGatewayStore()
|
|
||||||
await gatewayStore.connect()
|
|
||||||
} else {
|
|
||||||
this.logout()
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Auth initialization failed", e)
|
console.error("Auth initialization failed", e)
|
||||||
this.logout()
|
// this.logout()
|
||||||
} finally {
|
} finally {
|
||||||
this.isInitialized = true
|
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() {
|
logout() {
|
||||||
this.token = null
|
const api = useApi()
|
||||||
this.user = null
|
this.user = null
|
||||||
localStorage.removeItem('token')
|
api.post('/auth/logout')
|
||||||
// On ne redirige pas ici pour laisser le router ou le composant décider
|
// 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
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
@@ -8,7 +8,7 @@ export const useCategoryStore = defineStore("category", {
|
|||||||
actions: {
|
actions: {
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
let api = useApi();
|
let api = useApi();
|
||||||
let response = await api.get("/api/categories");
|
let response = await api.get("/categories");
|
||||||
this.categories = await response.json();
|
this.categories = await response.json();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const useChannelStore = defineStore('channel', {
|
|||||||
actions: {
|
actions: {
|
||||||
async fetchChannels() {
|
async fetchChannels() {
|
||||||
let api = useApi();
|
let api = useApi();
|
||||||
let response = await api.get("/api/channels");
|
let response = await api.get("/channels");
|
||||||
this.channels = await response.json();
|
this.channels = await response.json();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {useAppStore} from "@/stores/app.ts";
|
import {useAppStore} from "@/stores/app.ts";
|
||||||
import {useAuthStore} from "@/stores/auth.ts";
|
|
||||||
|
|
||||||
type GatewayStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
|
type GatewayStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
|
||||||
|
|
||||||
@@ -18,20 +17,21 @@ export const useGatewayStore = defineStore('gateway', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const authStore = useAuthStore()
|
// const authStore = useAuthStore()
|
||||||
|
|
||||||
|
|
||||||
this.status = 'connecting'
|
this.status = 'connecting'
|
||||||
const token = authStore.token
|
// version token
|
||||||
if (!token) {
|
// const token = authStore.token
|
||||||
this.status = 'error'
|
// if (!token) {
|
||||||
return
|
// this.status = 'error'
|
||||||
}
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
const apiUri = appStore.baseurl ? new URL(appStore.baseurl) : new URL(window.location.href)
|
const apiUri = appStore.baseurl ? new URL(appStore.baseurl) : new URL(window.location.href)
|
||||||
const wsProtocol = apiUri.protocol === 'https:' ? 'wss:' : 'ws:'
|
const wsProtocol = apiUri.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
// version token
|
||||||
const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway?token=${encodeURIComponent(token)}`
|
// const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway?token=${encodeURIComponent(token)}`
|
||||||
|
const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway`
|
||||||
const socket = new WebSocket(wsUrl)
|
const socket = new WebSocket(wsUrl)
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const useServerStore = defineStore("server", {
|
|||||||
actions: {
|
actions: {
|
||||||
async fetchServers() {
|
async fetchServers() {
|
||||||
let api = useApi();
|
let api = useApi();
|
||||||
const response = await api.get("/api/servers");
|
const response = await api.get("/servers");
|
||||||
this.servers = await response.json();
|
this.servers = await response.json();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import {fileURLToPath, URL} from 'node:url'
|
||||||
import Vue from '@vitejs/plugin-vue'
|
import Vue from '@vitejs/plugin-vue'
|
||||||
import Fonts from 'unplugin-fonts/vite'
|
import Fonts from 'unplugin-fonts/vite'
|
||||||
import { defineConfig } from 'vite'
|
import {defineConfig} from 'vite'
|
||||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
import Vuetify, {transformAssetUrls} from 'vite-plugin-vuetify'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
Vue({
|
Vue({
|
||||||
template: { transformAssetUrls },
|
template: {transformAssetUrls},
|
||||||
}),
|
}),
|
||||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||||
Vuetify({
|
Vuetify({
|
||||||
@@ -29,7 +29,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
define: { 'process.env': {} },
|
define: {'process.env': {}},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('src', import.meta.url)),
|
'@': fileURLToPath(new URL('src', import.meta.url)),
|
||||||
@@ -46,5 +46,11 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ impl MigrationTrait for Migration {
|
|||||||
.integer()
|
.integer()
|
||||||
.not_null(),
|
.not_null(),
|
||||||
)
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("default_channel_permissions"))
|
||||||
|
.big_integer()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("default_voice_permissions"))
|
||||||
|
.big_integer()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
.col(ColumnDef::new(Alias::new("name")).string().null())
|
.col(ColumnDef::new(Alias::new("name")).string().null())
|
||||||
.col(
|
.col(
|
||||||
ColumnDef::new(Alias::new("created_at"))
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
|||||||
@@ -36,13 +36,11 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use axum::{
|
|||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::{info, Instrument};
|
use tracing::{info, Instrument};
|
||||||
@@ -103,6 +104,11 @@ pub async fn auth_middleware(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
// Grâce à CookieJar, on extrait proprement le cookie "token" ou "jwt"
|
||||||
|
let jar = CookieJar::from_headers(req.headers());
|
||||||
|
jar.get("token").map(|cookie| cookie.value().to_string())
|
||||||
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
req.uri().query().and_then(|q| {
|
req.uri().query().and_then(|q| {
|
||||||
form_urlencoded::parse(q.as_bytes())
|
form_urlencoded::parse(q.as_bytes())
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ pub struct Model {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
pub default_permissions: Option<u64>,
|
pub default_channel_permissions: Option<i64>,
|
||||||
|
pub default_voice_permissions: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ use crate::http::error::HTTPError;
|
|||||||
use crate::routes::user::mapper::user_model_to_user_response;
|
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 axum_extra::extract::cookie::{Cookie, SameSite};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
use sea_orm::ActiveModelBehavior;
|
use sea_orm::ActiveModelBehavior;
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
post,
|
||||||
path = "/auth/login",
|
path = "/auth/bearer-login",
|
||||||
request_body = LoginRequest,
|
request_body = LoginRequest,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Login successful", body = LoginResponse),
|
(status = 200, description = "Login successful", body = LoginResponse),
|
||||||
@@ -18,7 +20,7 @@ use sea_orm::ActiveModelBehavior;
|
|||||||
),
|
),
|
||||||
tag = "Auth"
|
tag = "Auth"
|
||||||
)]
|
)]
|
||||||
pub async fn login_user_pw(
|
pub async fn login_bearer(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<LoginRequest>,
|
Json(payload): Json<LoginRequest>,
|
||||||
) -> Result<Json<LoginResponse>, HTTPError> {
|
) -> Result<Json<LoginResponse>, HTTPError> {
|
||||||
@@ -41,6 +43,69 @@ pub async fn login_user_pw(
|
|||||||
Ok(Json(LoginResponse { token }))
|
Ok(Json(LoginResponse { token }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/auth/login",
|
||||||
|
request_body = LoginRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Login successful with cookie set", body = LoginResponse),
|
||||||
|
(status = 401, description = "Unauthorized")
|
||||||
|
),
|
||||||
|
tag = "Auth"
|
||||||
|
)]
|
||||||
|
pub async fn login_cookie(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
jar: CookieJar,
|
||||||
|
Json(payload): Json<LoginRequest>,
|
||||||
|
) -> Result<(CookieJar, Json<LoginResponse>), HTTPError> {
|
||||||
|
let user = state
|
||||||
|
.repositories
|
||||||
|
.user
|
||||||
|
.check_password(&payload.username, &payload.password)
|
||||||
|
.await
|
||||||
|
.map_err(|_| HTTPError::Unauthorized)?;
|
||||||
|
|
||||||
|
let token = create_jwt(
|
||||||
|
user.id,
|
||||||
|
&user.username,
|
||||||
|
user.is_superuser,
|
||||||
|
&state.config.jwt.secret,
|
||||||
|
state.config.jwt.duration,
|
||||||
|
)
|
||||||
|
.map_err(|_| HTTPError::InternalServerError("Failed to create JWT token".to_string()))?;
|
||||||
|
|
||||||
|
// Création du cookie sécurisé contenant le token JWT
|
||||||
|
let cookie = Cookie::build(("token", token.clone()))
|
||||||
|
.path("/")
|
||||||
|
.http_only(true)
|
||||||
|
.same_site(SameSite::Lax)
|
||||||
|
.secure(false) // Mettez à true si vous forcez le HTTPS en production
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let updated_jar = jar.add(cookie);
|
||||||
|
|
||||||
|
Ok((updated_jar, Json(LoginResponse { token })))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/auth/logout",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Logout successful")
|
||||||
|
),
|
||||||
|
tag = "Auth"
|
||||||
|
)]
|
||||||
|
pub async fn logout_cookie(jar: CookieJar) -> Result<CookieJar, HTTPError> {
|
||||||
|
// On crée un cookie expiré en lui donnant une durée négative de 1 seconde (ou Duration::ZERO)
|
||||||
|
let cookie = Cookie::build(("token", ""))
|
||||||
|
.path("/")
|
||||||
|
.max_age(time::Duration::seconds(-1))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let updated_jar = jar.add(cookie);
|
||||||
|
Ok(updated_jar)
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/auth/me",
|
path = "/auth/me",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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_cookie))
|
||||||
|
.route("/auth/bearer-login", post(handlers::login_bearer))
|
||||||
.route("/auth/me", get(handlers::me))
|
.route("/auth/me", get(handlers::me))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ pub struct CreateChannelRequest {
|
|||||||
pub channel_type: ChannelType,
|
pub channel_type: ChannelType,
|
||||||
#[schema(example = "général")]
|
#[schema(example = "général")]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub default_permissions: Option<u64>,
|
pub default_channel_permissions: Option<u64>,
|
||||||
|
pub default_voice_permissions: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
@@ -23,7 +24,8 @@ pub struct UpdateChannelRequest {
|
|||||||
pub position: i32,
|
pub position: i32,
|
||||||
pub channel_type: ChannelType,
|
pub channel_type: ChannelType,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub default_permissions: Option<u64>,
|
pub default_channel_permissions: Option<u64>,
|
||||||
|
pub default_voice_permissions: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
@@ -36,5 +38,6 @@ pub struct ChannelResponse {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub default_permissions: Option<u64>,
|
pub default_channel_permissions: Option<u64>,
|
||||||
|
pub default_voice_permissions: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ pub fn channel_model_to_channel_response(model: channel::Model) -> ChannelRespon
|
|||||||
name: model.name,
|
name: model.name,
|
||||||
created_at: model.created_at,
|
created_at: model.created_at,
|
||||||
updated_at: model.updated_at,
|
updated_at: model.updated_at,
|
||||||
default_permissions: model.default_permissions,
|
default_channel_permissions: model.default_channel_permissions.map(|p| p as u64),
|
||||||
|
default_voice_permissions: model.default_voice_permissions.map(|p| p as u64),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +26,8 @@ pub fn create_request_to_am(req: CreateChannelRequest) -> channel::ActiveModel {
|
|||||||
position: Set(req.position),
|
position: Set(req.position),
|
||||||
channel_type: Set(req.channel_type),
|
channel_type: Set(req.channel_type),
|
||||||
name: Set(req.name),
|
name: Set(req.name),
|
||||||
default_permissions: Set(req.default_permissions),
|
default_channel_permissions: Set(req.default_channel_permissions.map(|p| p as i64)),
|
||||||
|
default_voice_permissions: Set(req.default_voice_permissions.map(|p| p as i64)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,8 @@ pub fn update_request_to_am(id: Uuid, req: UpdateChannelRequest) -> channel::Act
|
|||||||
position: Set(req.position),
|
position: Set(req.position),
|
||||||
channel_type: Set(req.channel_type),
|
channel_type: Set(req.channel_type),
|
||||||
name: Set(req.name),
|
name: Set(req.name),
|
||||||
default_permissions: Set(req.default_permissions),
|
default_channel_permissions: Set(req.default_channel_permissions.map(|p| p as i64)),
|
||||||
|
default_voice_permissions: Set(req.default_voice_permissions.map(|p| p as i64)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::models::user::Model as User;
|
|||||||
use crate::routes::gateway::GatewayClient;
|
use crate::routes::gateway::GatewayClient;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{
|
extract::{
|
||||||
ws::{Message, WebSocket, WebSocketUpgrade}, Query,
|
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||||
State,
|
State,
|
||||||
},
|
},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
@@ -19,7 +19,6 @@ pub struct WsQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ws_handler(
|
pub async fn ws_handler(
|
||||||
Query(query): Query<WsQuery>,
|
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use utoipa::{Modify, OpenApi};
|
|||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
paths(
|
paths(
|
||||||
auth::handlers::login_user_pw,
|
auth::handlers::login_bearer,
|
||||||
|
auth::handlers::login_cookie,
|
||||||
|
auth::handlers::logout_cookie,
|
||||||
auth::handlers::me,
|
auth::handlers::me,
|
||||||
user::handlers::get_all,
|
user::handlers::get_all,
|
||||||
user::handlers::get_by_id,
|
user::handlers::get_by_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user