init
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
use tauri::command;
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import type {IApiClient} from './client';
|
||||||
|
import {getPlatform} from '../platform';
|
||||||
|
import {useAuthStore} from '../stores/auth';
|
||||||
|
import type {
|
||||||
|
CategoryResponse,
|
||||||
|
ChannelResponse,
|
||||||
|
ClaimAdminRequest,
|
||||||
|
CreateCategoryRequest,
|
||||||
|
CreateChannelRequest,
|
||||||
|
CreateMessageRequest,
|
||||||
|
CreateServerRequest,
|
||||||
|
LoginRequest,
|
||||||
|
LoginResponse,
|
||||||
|
MessageResponse,
|
||||||
|
ServerResponse,
|
||||||
|
SshChallengeResponse,
|
||||||
|
UserResponse,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified API Client for OxSpeak.
|
||||||
|
* Uses fetch() directly in all environments.
|
||||||
|
* Uses the Auth store for token persistence and state.
|
||||||
|
*/
|
||||||
|
export class ApiClient implements IApiClient {
|
||||||
|
private token: string | null = null;
|
||||||
|
private tokenLoaded = false;
|
||||||
|
|
||||||
|
constructor(public readonly baseUrl: string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureTokenLoaded() {
|
||||||
|
if (!this.tokenLoaded) {
|
||||||
|
try {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
this.token = await authStore.loadToken(this.baseUrl);
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to platform directly if store is not available
|
||||||
|
this.token = await getPlatform().loadToken(this.baseUrl);
|
||||||
|
}
|
||||||
|
this.tokenLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(method: string, path: string, body?: any): Promise<T> {
|
||||||
|
await this.ensureTokenLoaded();
|
||||||
|
|
||||||
|
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 async setToken(token: string) {
|
||||||
|
this.token = token;
|
||||||
|
this.tokenLoaded = true;
|
||||||
|
try {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
await authStore.saveToken(this.baseUrl, token);
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to platform directly
|
||||||
|
await getPlatform().saveToken(this.baseUrl, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUTH
|
||||||
|
async login(req: LoginRequest): Promise<LoginResponse> {
|
||||||
|
const res = await this.request<LoginResponse>('POST', 'api/auth/login', req);
|
||||||
|
await 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref} from 'vue'
|
||||||
|
import {useConfigStore} from '@/stores/config'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
open?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:open'])
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const serverAddress = ref('')
|
||||||
|
const selectedIdentity = ref('')
|
||||||
|
|
||||||
|
const identitiesOptions = computed(() => {
|
||||||
|
if (!configStore.config) return []
|
||||||
|
return configStore.config.identities.map(i => ({
|
||||||
|
label: i.username || i.id,
|
||||||
|
value: i.id
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
async function joinServer() {
|
||||||
|
if (!serverAddress.value || !selectedIdentity.value) return
|
||||||
|
|
||||||
|
if (configStore.config) {
|
||||||
|
configStore.config.servers.push({
|
||||||
|
adresse: serverAddress.value,
|
||||||
|
identity: selectedIdentity.value
|
||||||
|
})
|
||||||
|
await configStore.saveConfig()
|
||||||
|
|
||||||
|
emit('update:open', false)
|
||||||
|
router.push('/config/servers')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => !!props.open,
|
||||||
|
set: (val) => emit('update:open', val)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal
|
||||||
|
v-model:open="isOpen"
|
||||||
|
title="Joindre un nouveau serveur"
|
||||||
|
description="Entrez l'adresse d'un serveur et choisissez une identité pour vous connecter."
|
||||||
|
>
|
||||||
|
<!-- <template #header>-->
|
||||||
|
<!-- <div class="p-4">-->
|
||||||
|
<!-- <h3 class="text-xl font-bold">Joindre un nouveau serveur</h3>-->
|
||||||
|
<!-- <p class="text-gray-500 text-sm">Entrez l'adresse d'un serveur et choisissez une identité pour vous-->
|
||||||
|
<!-- connecter.</p>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </template>-->
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4 px-4 pb-4">
|
||||||
|
<UFormField label="Adresse du serveur (IP:Port)">
|
||||||
|
<UInput
|
||||||
|
v-model="serverAddress"
|
||||||
|
placeholder="ex: 127.0.0.1:50051"
|
||||||
|
icon="i-lucide-server"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Identité à utiliser">
|
||||||
|
<USelect
|
||||||
|
v-model="selectedIdentity"
|
||||||
|
:items="identitiesOptions"
|
||||||
|
placeholder="Sélectionner une identité"
|
||||||
|
icon="i-lucide-user"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<p v-if="identitiesOptions.length === 0" class="text-xs text-red-500">
|
||||||
|
Vous devez d'abord créer une identité dans la configuration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-3 w-full px-4 py-3">
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
label="Annuler"
|
||||||
|
@click="isOpen = false"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
label="Joindre"
|
||||||
|
:disabled="!serverAddress || !selectedIdentity"
|
||||||
|
@click="joinServer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useConfigStore} from '../../stores/config'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const toast = useToast()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<template v-if="configStore.config">
|
||||||
|
<div v-for="(identity, index) in configStore.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"
|
||||||
|
/>
|
||||||
|
</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="configStore.removeIdentity(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
variant="dashed"
|
||||||
|
block
|
||||||
|
@click="configStore.addIdentity"
|
||||||
|
>
|
||||||
|
Ajouter une identité
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useConfigStore} from '../../stores/config'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<template v-if="configStore.config">
|
||||||
|
<div v-for="(server, index) in configStore.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>
|
||||||
|
|
||||||
|
<UFormField label="Identité à utiliser">
|
||||||
|
<USelect
|
||||||
|
v-model="server.identity"
|
||||||
|
:items="configStore.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="configStore.removeServer(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
variant="dashed"
|
||||||
|
block
|
||||||
|
@click="configStore.addServer"
|
||||||
|
>
|
||||||
|
Ajouter un serveur
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {invoke} from '@tauri-apps/api/core';
|
||||||
|
import type {IPlatform} from './types';
|
||||||
|
|
||||||
|
export class WebPlatform implements IPlatform {
|
||||||
|
readonly isTauri = false;
|
||||||
|
|
||||||
|
async saveToken(baseUrl: string, token: string): Promise<void> {
|
||||||
|
localStorage.setItem(`token_${baseUrl}`, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadToken(baseUrl: string): Promise<string | null> {
|
||||||
|
return localStorage.getItem(`token_${baseUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearToken(baseUrl: string): Promise<void> {
|
||||||
|
localStorage.removeItem(`token_${baseUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async startAudio(): Promise<void> {
|
||||||
|
console.warn('Audio is not supported in Web mode yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopAudio(): Promise<void> {
|
||||||
|
console.warn('Audio is not supported in Web mode yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendAudioPacket(data: ArrayBuffer | Uint8Array): Promise<void> {
|
||||||
|
console.warn('Audio is not supported in Web mode yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TauriPlatform implements IPlatform {
|
||||||
|
readonly isTauri = true;
|
||||||
|
|
||||||
|
async saveToken(baseUrl: string, token: string): Promise<void> {
|
||||||
|
await invoke('save_token', {baseUrl, token});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadToken(baseUrl: string): Promise<string | null> {
|
||||||
|
return await invoke<string | null>('load_token', {baseUrl});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearToken(baseUrl: string): Promise<void> {
|
||||||
|
await invoke('clear_token', {baseUrl});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startAudio(): Promise<void> {
|
||||||
|
await invoke('start_audio');
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopAudio(): Promise<void> {
|
||||||
|
await invoke('stop_audio');
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendAudioPacket(data: ArrayBuffer | Uint8Array): Promise<void> {
|
||||||
|
await invoke('send_audio_packet', {data: Array.from(new Uint8Array(data))});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {TauriPlatform, WebPlatform} from './implementations';
|
||||||
|
import type {IPlatform} from './types';
|
||||||
|
|
||||||
|
let platformInstance: IPlatform | null = null;
|
||||||
|
|
||||||
|
export function getPlatform(): IPlatform {
|
||||||
|
if (platformInstance) return platformInstance;
|
||||||
|
|
||||||
|
const isTauri = !!(window as any).__TAURI_INTERNALS__;
|
||||||
|
platformInstance = isTauri ? new TauriPlatform() : new WebPlatform();
|
||||||
|
return platformInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export interface IPlatform {
|
||||||
|
// Token handling
|
||||||
|
saveToken(baseUrl: string, token: string): Promise<void>;
|
||||||
|
|
||||||
|
loadToken(baseUrl: string): Promise<string | null>;
|
||||||
|
|
||||||
|
clearToken(baseUrl: string): Promise<void>;
|
||||||
|
|
||||||
|
// Audio control
|
||||||
|
startAudio(): Promise<void>;
|
||||||
|
|
||||||
|
stopAudio(): Promise<void>;
|
||||||
|
|
||||||
|
sendAudioPacket(data: ArrayBuffer | Uint8Array): Promise<void>;
|
||||||
|
|
||||||
|
// Environment
|
||||||
|
readonly isTauri: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
|
import IndexPage from './pages/index.vue'
|
||||||
|
import ConfigPage from './pages/config.vue'
|
||||||
|
import ServersPage from './pages/config/servers.vue'
|
||||||
|
import IdentitiesPage from './pages/config/identities.vue'
|
||||||
|
import ServerDetailPage from './pages/server_detail.vue'
|
||||||
|
import ChannelDetailPage from './pages/channel_detail.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
routes: [
|
||||||
|
{path: '/', component: IndexPage},
|
||||||
|
{
|
||||||
|
path: '/config',
|
||||||
|
component: ConfigPage,
|
||||||
|
children: [
|
||||||
|
{path: '', redirect: '/config/servers'},
|
||||||
|
{path: 'servers', component: ServersPage},
|
||||||
|
{path: 'identities', component: IdentitiesPage},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/server/:server_id',
|
||||||
|
component: ServerDetailPage,
|
||||||
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'channel/:channel_id',
|
||||||
|
component: ChannelDetailPage,
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
history: createWebHistory()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {getPlatform} from '../platform';
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const tokens = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
async function loadToken(baseUrl: string): Promise<string | null> {
|
||||||
|
if (tokens.value[baseUrl]) {
|
||||||
|
return tokens.value[baseUrl];
|
||||||
|
}
|
||||||
|
const token = await getPlatform().loadToken(baseUrl);
|
||||||
|
if (token) {
|
||||||
|
tokens.value[baseUrl] = token;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveToken(baseUrl: string, token: string): Promise<void> {
|
||||||
|
tokens.value[baseUrl] = token;
|
||||||
|
await getPlatform().saveToken(baseUrl, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearToken(baseUrl: string): Promise<void> {
|
||||||
|
delete tokens.value[baseUrl];
|
||||||
|
await getPlatform().clearToken(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokens,
|
||||||
|
loadToken,
|
||||||
|
saveToken,
|
||||||
|
clearToken,
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user