init
This commit is contained in:
@@ -2,11 +2,6 @@
|
||||
import {onMounted, onUnmounted, ref} from 'vue';
|
||||
import {NavigationMenuItem} from "@nuxt/ui";
|
||||
import {createApiClient} from "./api";
|
||||
import {useConfigStore, useServerStore} from "./stores";
|
||||
|
||||
// stores
|
||||
const configStore = useConfigStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
// vars
|
||||
let unlisten: (() => void) | null = null;
|
||||
@@ -48,9 +43,6 @@ async function fetchServers() {
|
||||
// ])
|
||||
|
||||
onMounted(async () => {
|
||||
// config
|
||||
await configStore.init();
|
||||
|
||||
// fetch servers
|
||||
await fetchServers()
|
||||
})
|
||||
|
||||
+5
-10
@@ -1,19 +1,14 @@
|
||||
export * from './types';
|
||||
export * from './client';
|
||||
export * from './tauri-client';
|
||||
export * from './web-client';
|
||||
export * from './api-client';
|
||||
|
||||
import {TauriApiClient} from './tauri-client';
|
||||
import {WebApiClient} from './web-client';
|
||||
import {ApiClient} from './api-client';
|
||||
import type {IApiClient} from './client';
|
||||
|
||||
/**
|
||||
* Usine pour créer des clients API.
|
||||
* Elle retourne une implémentation Web ou Tauri selon l'environnement.
|
||||
* Factory for creating API clients.
|
||||
* Always returns a unified ApiClient that works in both Web and Tauri.
|
||||
*/
|
||||
export function createApiClient(baseUrl: string): IApiClient {
|
||||
if ((window as any).__TAURI_INTERNALS__) {
|
||||
return new TauriApiClient(baseUrl);
|
||||
}
|
||||
return new WebApiClient(baseUrl);
|
||||
return new ApiClient(baseUrl);
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import {invoke} from '@tauri-apps/api/core';
|
||||
import type {IApiClient} from './client';
|
||||
import type {
|
||||
CategoryResponse,
|
||||
ChannelResponse,
|
||||
ClaimAdminRequest,
|
||||
CreateCategoryRequest,
|
||||
CreateChannelRequest,
|
||||
CreateMessageRequest,
|
||||
CreateServerRequest,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
MessageResponse,
|
||||
ServerResponse,
|
||||
SshChallengeResponse,
|
||||
UserResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Implémentation Tauri de l'interface API.
|
||||
* Cette version utilise 'invoke' pour communiquer avec le backend Rust.
|
||||
*/
|
||||
export class TauriApiClient implements IApiClient {
|
||||
constructor(public readonly baseUrl: string) {
|
||||
}
|
||||
|
||||
// AUTH
|
||||
async login(req: LoginRequest): Promise<LoginResponse> {
|
||||
return await invoke<LoginResponse>('api_login', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
async verifyToken(): Promise<UserResponse> {
|
||||
return await invoke<UserResponse>('api_verify_token', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
||||
return await invoke<void>('api_claim_admin', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
async sshChallenge(): Promise<SshChallengeResponse> {
|
||||
return await invoke<SshChallengeResponse>('api_ssh_challenge', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
// SERVER
|
||||
async getServerList(): Promise<ServerResponse[]> {
|
||||
return await invoke<ServerResponse[]>('api_server_list', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
||||
return await invoke<ServerResponse>('api_server_create', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
async getServerTree(serverId: string): Promise<any> {
|
||||
return await invoke<any>('api_server_tree', {baseUrl: this.baseUrl, serverId});
|
||||
}
|
||||
|
||||
// CATEGORY
|
||||
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
||||
return await invoke<CategoryResponse[]>('api_category_list', {
|
||||
baseUrl: this.baseUrl,
|
||||
serverId: serverId ?? null
|
||||
});
|
||||
}
|
||||
|
||||
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
||||
return await invoke<CategoryResponse>('api_category_create', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
// CHANNEL
|
||||
async getChannelList(): Promise<ChannelResponse[]> {
|
||||
return await invoke<ChannelResponse[]>('api_channel_list', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
||||
return await invoke<ChannelResponse>('api_channel_create', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
// MESSAGE
|
||||
async getMessageList(): Promise<MessageResponse[]> {
|
||||
return await invoke<MessageResponse[]>('api_message_list', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
||||
return await invoke<MessageResponse>('api_message_create', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
// USER
|
||||
async getUserList(): Promise<UserResponse[]> {
|
||||
return await invoke<UserResponse[]>('api_user_list', {baseUrl: this.baseUrl});
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import type {IApiClient} from './client';
|
||||
import type {
|
||||
CategoryResponse,
|
||||
ChannelResponse,
|
||||
ClaimAdminRequest,
|
||||
CreateCategoryRequest,
|
||||
CreateChannelRequest,
|
||||
CreateMessageRequest,
|
||||
CreateServerRequest,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
MessageResponse,
|
||||
ServerResponse,
|
||||
SshChallengeResponse,
|
||||
UserResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Implémentation Web (standard) de l'interface API.
|
||||
* Elle utilise fetch() pour communiquer directement avec le serveur.
|
||||
*/
|
||||
export class WebApiClient implements IApiClient {
|
||||
private token: string | null = null;
|
||||
|
||||
constructor(public readonly baseUrl: string) {
|
||||
this.token = localStorage.getItem(`token_${baseUrl}`);
|
||||
}
|
||||
|
||||
private async request<T>(method: string, path: string, body?: any): Promise<T> {
|
||||
// Nettoyer l'URL
|
||||
const cleanBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
const url = `${cleanBaseUrl}/${cleanPath}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch (e) {
|
||||
errorData = {message: await response.text()};
|
||||
}
|
||||
throw new Error(errorData.message || `Erreur HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private setToken(token: string) {
|
||||
this.token = token;
|
||||
localStorage.setItem(`token_${this.baseUrl}`, token);
|
||||
}
|
||||
|
||||
// AUTH
|
||||
async login(req: LoginRequest): Promise<LoginResponse> {
|
||||
const res = await this.request<LoginResponse>('POST', 'api/auth/login', req);
|
||||
this.setToken(res.token);
|
||||
return res;
|
||||
}
|
||||
|
||||
async verifyToken(): Promise<UserResponse> {
|
||||
return await this.request<UserResponse>('GET', 'api/auth/me');
|
||||
}
|
||||
|
||||
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
||||
return await this.request<void>('POST', 'api/auth/claim-admin', req);
|
||||
}
|
||||
|
||||
async sshChallenge(): Promise<SshChallengeResponse> {
|
||||
return await this.request<SshChallengeResponse>('POST', 'api/auth/ssh-challenge');
|
||||
}
|
||||
|
||||
// SERVER
|
||||
async getServerList(): Promise<ServerResponse[]> {
|
||||
return await this.request<ServerResponse[]>('GET', 'api/server');
|
||||
}
|
||||
|
||||
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
||||
return await this.request<ServerResponse>('POST', 'api/server', req);
|
||||
}
|
||||
|
||||
async getServerTree(serverId: string): Promise<any> {
|
||||
return await this.request<any>('GET', `api/server/servers/${serverId}/tree/`);
|
||||
}
|
||||
|
||||
// CATEGORY
|
||||
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
||||
const path = serverId ? `api/category?server_id=${serverId}` : 'api/category';
|
||||
return await this.request<CategoryResponse[]>('GET', path);
|
||||
}
|
||||
|
||||
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
||||
return await this.request<CategoryResponse>('POST', 'api/category', req);
|
||||
}
|
||||
|
||||
// CHANNEL
|
||||
async getChannelList(): Promise<ChannelResponse[]> {
|
||||
return await this.request<ChannelResponse[]>('GET', 'api/channel');
|
||||
}
|
||||
|
||||
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
||||
return await this.request<ChannelResponse>('POST', 'api/channel', req);
|
||||
}
|
||||
|
||||
// MESSAGE
|
||||
async getMessageList(): Promise<MessageResponse[]> {
|
||||
return await this.request<MessageResponse[]>('GET', 'api/message');
|
||||
}
|
||||
|
||||
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
||||
return await this.request<MessageResponse>('POST', 'api/message', req);
|
||||
}
|
||||
|
||||
// USER
|
||||
async getUserList(): Promise<UserResponse[]> {
|
||||
return await this.request<UserResponse[]>('GET', 'api/user');
|
||||
}
|
||||
}
|
||||
+66
-22
@@ -1,34 +1,78 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import {createApp} from 'vue'
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import router from './router'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import App from './App.vue'
|
||||
import {createPinia} from "pinia";
|
||||
import {debug, error, info, trace, warn} from '@tauri-apps/plugin-log'
|
||||
import {useGlobalStore} from "@/stores";
|
||||
|
||||
function getCallerLocation(): string {
|
||||
const stack = new Error().stack ?? '';
|
||||
const lines = stack.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('node_modules') || line.includes('main.ts')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Chromium/Firefox : " at fetchServers (http://localhost:1420/src/App.vue:39:18)"
|
||||
// ou sans nom : " at http://localhost:1420/src/App.vue:39:18"
|
||||
const chromiumMatch = line.match(/^\s+at\s+(?:\S+\s+)?\(?https?:\/\/[^/]+([^?]+?)(?:\?[^:]*)?:(\d+):\d+\)?$/);
|
||||
if (chromiumMatch) {
|
||||
return `${chromiumMatch[1]}:${chromiumMatch[2]}`;
|
||||
}
|
||||
|
||||
// WebKit : "fetchServers@http://localhost:1420/src/App.vue:39:18"
|
||||
const webkitMatch = line.match(/@https?:\/\/[^/]+([^?]+?)(?:\?[^:]*)?:(\d+):\d+$/);
|
||||
if (webkitMatch) {
|
||||
return `${webkitMatch[1]}:${webkitMatch[2]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function forwardConsole(
|
||||
fnName: 'log' | 'debug' | 'info' | 'warn' | 'error',
|
||||
logger: (message: string) => Promise<void>
|
||||
) {
|
||||
const original = console[fnName].bind(console);
|
||||
console[fnName] = (...args: unknown[]) => {
|
||||
original(...args);
|
||||
|
||||
const location = getCallerLocation();
|
||||
const message = `[${location}] ` + args
|
||||
.map(a => {
|
||||
if (a instanceof Error) return `${a.message}`;
|
||||
if (typeof a === 'object' && a !== null) return JSON.stringify(a);
|
||||
return String(a);
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
logger(message);
|
||||
};
|
||||
}
|
||||
|
||||
forwardConsole('log', trace);
|
||||
forwardConsole('debug', debug);
|
||||
forwardConsole('info', info);
|
||||
forwardConsole('warn', warn);
|
||||
forwardConsole('error', error);
|
||||
|
||||
console.log('--- JS Logs Initialized (Forwarding enabled) ---');
|
||||
info('--- Direct Plugin Info: JS side ready ---');
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
const router = createRouter({
|
||||
routes: [
|
||||
{path: '/', component: () => import('./pages/index.vue')},
|
||||
{path: '/config', component: () => import('./pages/config.vue')},
|
||||
{
|
||||
path: '/server/:server_id',
|
||||
component: () => import('./pages/server_detail.vue'),
|
||||
props: true,
|
||||
children: [
|
||||
{
|
||||
path: 'channel/:channel_id',
|
||||
component: () => import('./pages/channel_detail.vue'),
|
||||
props: true
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
history: createWebHistory()
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
app.use(ui)
|
||||
app.use(createPinia())
|
||||
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
await globalStore.init()
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
+26
-203
@@ -1,97 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {createConfigClient} from '../config'
|
||||
import {computed, onMounted} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useConfigStore} from '../stores/config'
|
||||
|
||||
// --- Types ---
|
||||
// Ils sont maintenant importés ou définis via l'interface du client
|
||||
|
||||
const configClient = createConfigClient()
|
||||
const config = ref<any>({
|
||||
servers: [],
|
||||
identities: []
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const configStore = useConfigStore()
|
||||
const toast = useToast()
|
||||
|
||||
const items = [{
|
||||
label: 'Serveurs',
|
||||
icon: 'i-lucide-server',
|
||||
slot: 'servers'
|
||||
}, {
|
||||
label: 'Identités',
|
||||
icon: 'i-lucide-user',
|
||||
slot: 'identities'
|
||||
}]
|
||||
const items = [
|
||||
{label: 'Serveurs', icon: 'i-lucide-server', value: 'servers'},
|
||||
{label: 'Identités', icon: 'i-lucide-user', value: 'identities'},
|
||||
]
|
||||
|
||||
// --- Methods ---
|
||||
const activeTab = computed(() =>
|
||||
route.path.endsWith('identities') ? 'identities' : 'servers'
|
||||
)
|
||||
|
||||
async function fetchConfig() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await configClient.get()
|
||||
config.value = res
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération de la config:', error)
|
||||
toast.add({title: 'Erreur', description: 'Impossible de charger la configuration.', color: 'red'})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
function onTabChange(value: string) {
|
||||
router.push(`/config/${value}`)
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
loading.value = true
|
||||
try {
|
||||
await configClient.update(config.value)
|
||||
// Re-fetch config to get any auto-generated keys
|
||||
await fetchConfig()
|
||||
await configStore.saveConfig()
|
||||
toast.add({title: 'Succès', description: 'Configuration sauvegardée.', color: 'green'})
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde de la config:', error)
|
||||
toast.add({title: 'Erreur', description: 'Impossible de sauvegarder la configuration.', color: 'red'})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// async function generateKey(index: number) {
|
||||
// try {
|
||||
// // Note: generate_ssh_key n'est pas encore dans IConfigClient, on peut l'ajouter si besoin
|
||||
// // ou garder un invoke direct si c'est spécifique à Tauri, mais ici on veut de l'abstrait.
|
||||
// // Pour le moment on utilise invoke car c'est un utilitaire.
|
||||
// const {invoke} from '@tauri-apps/api/core'
|
||||
// const key = await invoke<string>('generate_ssh_key')
|
||||
// config.value.identities[index].private_key = key
|
||||
// toast.add({title: 'Succès', description: 'Clé SSH générée.', color: 'green'})
|
||||
// } catch (error) {
|
||||
// console.error('Erreur lors de la génération de la clé:', error)
|
||||
// toast.add({title: 'Erreur', description: 'Impossible de générer la clé.', color: 'red'})
|
||||
// }
|
||||
// }
|
||||
|
||||
function addServer() {
|
||||
config.value.servers.push({adresse: '', identity: ''})
|
||||
}
|
||||
|
||||
function removeServer(index: number) {
|
||||
config.value.servers.splice(index, 1)
|
||||
}
|
||||
|
||||
function addIdentity() {
|
||||
config.value.identities.push({
|
||||
id: crypto.randomUUID(),
|
||||
username: '',
|
||||
private_key: '',
|
||||
mode: 'private_key_path'
|
||||
})
|
||||
}
|
||||
|
||||
function removeIdentity(index: number) {
|
||||
config.value.identities.splice(index, 1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchConfig()
|
||||
configStore.loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -102,139 +43,21 @@ onMounted(() => {
|
||||
<UButton
|
||||
icon="i-lucide-save"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:loading="configStore.saving"
|
||||
@click="saveConfig"
|
||||
>
|
||||
Sauvegarder
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UTabs :items="items" class="w-full">
|
||||
<template #servers>
|
||||
<div class="space-y-4 py-4">
|
||||
<div v-for="(server, index) in config.servers" :key="index"
|
||||
class="p-4 border border-gray-200 dark:border-gray-800 rounded-lg relative bg-white dark:bg-gray-900 shadow-sm">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<UFormField label="Adresse du serveur (IP:Port)">
|
||||
<UInput v-model="server.adresse" placeholder="ex: 127.0.0.1:50051" class="w-full"/>
|
||||
</UFormField>
|
||||
<UTabs
|
||||
:items="items"
|
||||
:content="false"
|
||||
:model-value="activeTab"
|
||||
class="w-full"
|
||||
@update:model-value="onTabChange"
|
||||
/>
|
||||
|
||||
<UFormField label="Identité à utiliser">
|
||||
<USelect
|
||||
v-model="server.identity"
|
||||
:items="config.identities.map(i => ({ label: i.username || i.id, value: i.id }))"
|
||||
placeholder="Sélectionner une identité"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
icon="i-lucide-trash"
|
||||
color="red"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="absolute top-2 right-2"
|
||||
@click="removeServer(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
icon="i-lucide-plus"
|
||||
variant="dashed"
|
||||
block
|
||||
@click="addServer"
|
||||
>
|
||||
Ajouter un serveur
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #identities>
|
||||
<div class="space-y-4 py-4">
|
||||
<div v-for="(identity, index) in config.identities" :key="identity.id"
|
||||
class="p-4 border border-gray-200 dark:border-gray-800 rounded-lg relative bg-white dark:bg-gray-900 shadow-sm">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<UFormField label="ID d'identité (unique)">
|
||||
<UInput v-model="identity.id" placeholder="ex: mon-id" class="w-full"/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Nom d'utilisateur">
|
||||
<UInput v-model="identity.username" placeholder="ex: MonPseudo" class="w-full"/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Mode d'authentification" class="md:col-span-2">
|
||||
<USelect
|
||||
v-model="identity.mode"
|
||||
:items="[
|
||||
{ label: 'Clé (Fichier)', value: 'private_key_path' },
|
||||
{ label: 'Clé (Base64)', value: 'private_key_base64' },
|
||||
{ label: 'Login uniquement', value: 'login' }
|
||||
]"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<template v-if="identity.mode === 'private_key_path' || identity.mode === 'private_key_base64'">
|
||||
<UFormField :label="identity.mode === 'private_key_path' ? 'Chemin de la clé' : 'Clé privée (Base64)'"
|
||||
class="md:col-span-2">
|
||||
<div class="space-y-2">
|
||||
<UTextarea
|
||||
v-model="identity.private_key"
|
||||
:placeholder="identity.mode === 'private_key_path' ? 'ex: ~/.ssh/id_rsa' : 'Entrez la clé encodée en base64 (Si vide, une clé sera générée au besoin)'"
|
||||
class="w-full"
|
||||
/>
|
||||
<!-- <UButton-->
|
||||
<!-- v-if="identity.mode === 'private_key_base64'"-->
|
||||
<!-- icon="i-lucide-key"-->
|
||||
<!-- variant="subtle"-->
|
||||
<!-- size="xs"-->
|
||||
<!-- label="Générer une clé SSH"-->
|
||||
<!-- @click="generateKey(index)"-->
|
||||
<!-- />-->
|
||||
</div>
|
||||
</UFormField>
|
||||
</template>
|
||||
|
||||
|
||||
<template v-if="identity.token">
|
||||
<UFormField label="Jeton JWT (Persistant)" class="md:col-span-2">
|
||||
<div class="flex gap-2">
|
||||
<UInput v-model="identity.token" readonly class="flex-1 font-mono text-xs"/>
|
||||
<UButton
|
||||
icon="i-lucide-copy"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="() => {
|
||||
navigator.clipboard.writeText(identity.token || '')
|
||||
toast.add({ title: 'Copié', description: 'Jeton JWT copié dans le presse-papier', color: 'green' })
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</UFormField>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
icon="i-lucide-trash"
|
||||
color="red"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="absolute top-2 right-2"
|
||||
@click="removeIdentity(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
icon="i-lucide-plus"
|
||||
variant="dashed"
|
||||
block
|
||||
@click="addIdentity"
|
||||
>
|
||||
Ajouter une identité
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UTabs>
|
||||
<RouterView/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+36
-4
@@ -1,11 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {useConfigStore} from '../stores/config'
|
||||
import JoinServerModal from '../components/JoinServerModal.vue'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const isModalOpen = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await configStore.init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>Hello world - index</p>
|
||||
</template>
|
||||
<div class="p-6 flex flex-col items-center justify-center h-full bg-gray-50 dark:bg-gray-950">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight mb-2 text-gray-900 dark:text-white">Ox-Speak</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400">Votre plateforme de communication décentralisée.</p>
|
||||
</div>
|
||||
|
||||
<style scoped>
|
||||
<div class="grid grid-cols-1 gap-6 w-full max-w-sm">
|
||||
<UButton
|
||||
icon="i-lucide-plus"
|
||||
size="xl"
|
||||
block
|
||||
label="Joindre un serveur"
|
||||
@click="isModalOpen = true"
|
||||
/>
|
||||
|
||||
</style>
|
||||
<UButton
|
||||
to="/config"
|
||||
icon="i-lucide-settings"
|
||||
size="xl"
|
||||
variant="outline"
|
||||
block
|
||||
label="Configuration"
|
||||
color="neutral"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<JoinServerModal v-model:open="isModalOpen"/>
|
||||
</div>
|
||||
</template>
|
||||
+46
-1
@@ -1,11 +1,12 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {ref} from 'vue';
|
||||
import {type ConfigTree, createConfigClient} from '../config';
|
||||
import {type ConfigTree, createConfigClient} from '@/config';
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const client = createConfigClient();
|
||||
const config = ref<ConfigTree | null>(null);
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
|
||||
async function loadConfig() {
|
||||
loading.value = true;
|
||||
@@ -21,19 +22,63 @@ export const useConfigStore = defineStore('config', () => {
|
||||
config.value = newConfig;
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
if (!config.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await client.update(config.value);
|
||||
// Re-fetch pour récupérer les clés auto-générées
|
||||
await loadConfig();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function addServer() {
|
||||
if (!config.value) return;
|
||||
config.value.servers.push({adresse: '', identity: ''} as any);
|
||||
}
|
||||
|
||||
function removeServer(index: number) {
|
||||
if (!config.value) return;
|
||||
config.value.servers.splice(index, 1);
|
||||
}
|
||||
|
||||
function addIdentity() {
|
||||
if (!config.value) return;
|
||||
config.value.identities.push({
|
||||
id: crypto.randomUUID(),
|
||||
username: '',
|
||||
private_key: '',
|
||||
mode: 'private_key_path'
|
||||
} as any);
|
||||
}
|
||||
|
||||
function removeIdentity(index: number) {
|
||||
if (!config.value) return;
|
||||
config.value.identities.splice(index, 1);
|
||||
}
|
||||
|
||||
// Initialiser la configuration et écouter les changements
|
||||
async function init() {
|
||||
await loadConfig();
|
||||
await client.onChanged((updatedConfig) => {
|
||||
config.value = updatedConfig;
|
||||
});
|
||||
console.debug('Config store initialized');
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
loading,
|
||||
saving,
|
||||
loadConfig,
|
||||
updateConfig,
|
||||
saveConfig,
|
||||
addServer,
|
||||
removeServer,
|
||||
addIdentity,
|
||||
removeIdentity,
|
||||
init,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {ref} from 'vue';
|
||||
import {useConfigStore} from './config';
|
||||
|
||||
export const useGlobalStore = defineStore('global', () => {
|
||||
const loading = ref(false);
|
||||
const theme = ref('dark');
|
||||
const isInitialized = ref(false);
|
||||
|
||||
async function init() {
|
||||
console.debug('Initializing global store...');
|
||||
if (isInitialized.value) return;
|
||||
|
||||
// Initialisation des stores requis au boot
|
||||
const configStore = useConfigStore();
|
||||
await configStore.init();
|
||||
|
||||
// Autres initialisations peuvent être ajoutées ici plus tard
|
||||
|
||||
isInitialized.value = true;
|
||||
console.debug('Global store initialized');
|
||||
}
|
||||
|
||||
function setLoading(value: boolean) {
|
||||
loading.value = value;
|
||||
@@ -16,7 +32,9 @@ export const useGlobalStore = defineStore('global', () => {
|
||||
return {
|
||||
loading,
|
||||
theme,
|
||||
isInitialized,
|
||||
setLoading,
|
||||
toggleTheme,
|
||||
init,
|
||||
};
|
||||
});
|
||||
|
||||
+36
-65
@@ -1,54 +1,10 @@
|
||||
import {invoke} from '@tauri-apps/api/core';
|
||||
import {listen} from '@tauri-apps/api/event';
|
||||
import type {IWsClient, WsEvent} from './types';
|
||||
import type {IWsClient} from './types';
|
||||
|
||||
/**
|
||||
* Implémentation Tauri du client WebSocket.
|
||||
* Utilise le backend Rust pour maintenir la connexion.
|
||||
* Unified WebSocket Client for OxSpeak.
|
||||
* Implemented ONLY in TypeScript.
|
||||
*/
|
||||
export class TauriWsClient implements IWsClient {
|
||||
private serverUrl: string | null = null;
|
||||
|
||||
async connect(serverUrl: string, token?: string): Promise<void> {
|
||||
this.serverUrl = serverUrl;
|
||||
await invoke('ws_connect', {serverUrl, token});
|
||||
}
|
||||
|
||||
async send(message: any): Promise<void> {
|
||||
if (!this.serverUrl) throw new Error('Non connecté');
|
||||
const msgString = typeof message === 'string' ? message : JSON.stringify(message);
|
||||
await invoke('ws_send', {serverUrl: this.serverUrl, message: msgString});
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.serverUrl) {
|
||||
await invoke('ws_disconnect', {serverUrl: this.serverUrl});
|
||||
this.serverUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(callback: (payload: any) => void): () => void {
|
||||
let unlisten: (() => void) | null = null;
|
||||
|
||||
listen<WsEvent>('ws-message', (event) => {
|
||||
// Filtrer par serverUrl si nécessaire
|
||||
if (this.serverUrl && event.payload.server_url === this.serverUrl) {
|
||||
callback(event.payload.payload);
|
||||
}
|
||||
}).then((fn) => {
|
||||
unlisten = fn;
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (unlisten) unlisten();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implémentation Web (standard) du client WebSocket.
|
||||
*/
|
||||
export class WebWsClient implements IWsClient {
|
||||
export class WsClient implements IWsClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private messageCallbacks: Set<(payload: any) => void> = new Set();
|
||||
private serverUrl: string | null = null;
|
||||
@@ -65,19 +21,36 @@ export class WebWsClient implements IWsClient {
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => resolve();
|
||||
this.ws.onerror = (err) => reject(err);
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event.data);
|
||||
this.messageCallbacks.forEach((cb) => cb(payload));
|
||||
} catch (e) {
|
||||
// Si ce n'est pas du JSON, on renvoie la data brute
|
||||
this.messageCallbacks.forEach((cb) => cb(event.data));
|
||||
}
|
||||
};
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connected to', wsUrl);
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.ws.onerror = (err) => {
|
||||
console.error('WebSocket error:', err);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event.data);
|
||||
this.messageCallbacks.forEach((cb) => cb(payload));
|
||||
} catch (e) {
|
||||
// Si ce n'est pas du JSON, on renvoie la data brute
|
||||
this.messageCallbacks.forEach((cb) => cb(event.data));
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log('WebSocket closed:', event.code, event.reason);
|
||||
this.ws = null;
|
||||
};
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,11 +76,9 @@ export class WebWsClient implements IWsClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory pour créer le client approprié.
|
||||
* Factory for creating the WebSocket client.
|
||||
* Always returns the unified TS implementation.
|
||||
*/
|
||||
export function createWsClient(): IWsClient {
|
||||
if ((window as any).__TAURI_INTERNALS__) {
|
||||
return new TauriWsClient();
|
||||
}
|
||||
return new WebWsClient();
|
||||
return new WsClient();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user