refactor(stage-ui): move auth lib to stage-ui
This commit is contained in:
@@ -55,7 +55,8 @@
|
||||
"unspeech": "catalog:xsai",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-router": "^4.6.4"
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-sonner": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nekopaw/tempora": "catalog:",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { listSessions, signOut } from '@proj-airi/stage-ui/libs/auth'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { listSessions, signOut } from '../../libs/auth'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { isAuthenticated, user } = storeToRefs(authStore)
|
||||
|
||||
|
||||
@@ -102,11 +102,13 @@
|
||||
"@xsai/tool": "catalog:",
|
||||
"@xsai/utils-chat": "catalog:",
|
||||
"animejs": "^4.2.2",
|
||||
"better-auth": "catalog:",
|
||||
"culori": "^4.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"es-toolkit": "catalog:",
|
||||
"gpuu": "^1.0.6",
|
||||
"hono": "catalog:",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jszip": "^3.10.1",
|
||||
"localforage": "^1.10.0",
|
||||
@@ -132,6 +134,7 @@
|
||||
"unist-builder": "^4.0.0",
|
||||
"unspeech": "catalog:xsai",
|
||||
"uuid": "^13.0.0",
|
||||
"valibot": "catalog:",
|
||||
"vaul-vue": "^0.4.1",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-router": "^4.6.4",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AppType } from '../../../../apps/server/src/app'
|
||||
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
import { hc } from 'hono/client'
|
||||
|
||||
import { SERVER_URL } from '../libs/auth'
|
||||
|
||||
export const client = hc<AppType>(SERVER_URL, {
|
||||
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const authStore = useAuthStore()
|
||||
const headers = new Headers(init?.headers)
|
||||
if (authStore.authToken) {
|
||||
headers.set('Authorization', `Bearer ${authStore.authToken}`)
|
||||
}
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers,
|
||||
credentials: 'include', // Send cookies with request (for sessions, etc)
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createAuthClient } from 'better-auth/vue'
|
||||
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
export const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'https://airi-api.moeru.ai'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: SERVER_URL,
|
||||
|
||||
credentials: 'include',
|
||||
fetchOptions: {
|
||||
auth: {
|
||||
type: 'Bearer',
|
||||
token: () => authStore.authToken,
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
const newToken = ctx.response.headers.get('set-auth-token')
|
||||
if (newToken) {
|
||||
authStore.authToken = newToken
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export async function fetchSession() {
|
||||
const { data } = await authClient.getSession()
|
||||
if (data) {
|
||||
authStore.user = data.user
|
||||
authStore.session = data.session
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export async function listSessions() {
|
||||
return await authClient.listSessions()
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
await authClient.signOut()
|
||||
|
||||
authStore.user = undefined
|
||||
authStore.session = undefined
|
||||
authStore.authToken = ''
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Session, User } from 'better-auth'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const authToken = useLocalStorage('auth/token', '')
|
||||
const user = ref<User>()
|
||||
const session = ref<Session>()
|
||||
const isAuthenticated = computed(() => !!user.value && !!session.value)
|
||||
|
||||
// TODO: include fetchSession here for pulling and updating better-auth session with initialize(...) action
|
||||
|
||||
return {
|
||||
authToken,
|
||||
user,
|
||||
session,
|
||||
isAuthenticated,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { Character, CreateCharacterPayload, UpdateCharacterPayload } from '../types/character'
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { client } from '../composables/api'
|
||||
|
||||
export const useCharacterStore = defineStore('characters', () => {
|
||||
const characters = ref<Map<string, Character>>(new Map())
|
||||
const isLoading = ref(false)
|
||||
const error = ref<unknown>(null)
|
||||
|
||||
async function fetchList() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await client.api.characters.$get()
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch characters')
|
||||
}
|
||||
const data = await res.json()
|
||||
|
||||
characters.value.clear()
|
||||
for (const char of data) {
|
||||
characters.value.set(char.id, char as Character)
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchById(id: string) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await client.api.characters[':id'].$get({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch character')
|
||||
}
|
||||
const data = await res.json()
|
||||
|
||||
characters.value.set(data.id, data as Character)
|
||||
return data
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function create(payload: CreateCharacterPayload) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await client.api.characters.$post({
|
||||
json: payload,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to create character')
|
||||
}
|
||||
const data = await res.json()
|
||||
|
||||
characters.value.set(data.id, data as Character)
|
||||
return data
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function update(id: string, payload: UpdateCharacterPayload) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await client.api.characters[':id'].$patch({
|
||||
param: { id },
|
||||
json: payload,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to update character')
|
||||
}
|
||||
const data = await res.json()
|
||||
|
||||
characters.value.set(data.id, data as Character)
|
||||
return data
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await client.api.characters[':id'].$delete({
|
||||
param: { id },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to delete character')
|
||||
}
|
||||
|
||||
characters.value.delete(id)
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getCharacter(id: string) {
|
||||
return characters.value.get(id)
|
||||
}
|
||||
|
||||
return {
|
||||
characters,
|
||||
isLoading,
|
||||
error,
|
||||
fetchList,
|
||||
fetchById,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
getCharacter,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { InferOutput } from 'valibot'
|
||||
|
||||
import { array, date, literal, number, object, optional, pipe, string, transform, union } from 'valibot'
|
||||
|
||||
// --- Enums & Configs ---
|
||||
|
||||
export const AvatarModelConfigSchema = object({
|
||||
vrm: optional(object({
|
||||
urls: array(string()),
|
||||
})),
|
||||
live2d: optional(object({
|
||||
urls: array(string()),
|
||||
})),
|
||||
})
|
||||
|
||||
export const CharacterCapabilityConfigSchema = object({
|
||||
apiKey: string(),
|
||||
apiBaseUrl: string(),
|
||||
llm: optional(object({
|
||||
temperature: number(),
|
||||
model: string(),
|
||||
})),
|
||||
tts: optional(object({
|
||||
ssml: string(),
|
||||
voiceId: string(),
|
||||
speed: number(),
|
||||
pitch: number(),
|
||||
})),
|
||||
vlm: optional(object({
|
||||
image: string(),
|
||||
})),
|
||||
asr: optional(object({
|
||||
audio: string(),
|
||||
})),
|
||||
})
|
||||
|
||||
const CharacterCapabilityTypeSchema = union([
|
||||
literal('llm'),
|
||||
literal('tts'),
|
||||
literal('vlm'),
|
||||
literal('asr'),
|
||||
])
|
||||
|
||||
const AvatarModelTypeSchema = union([
|
||||
literal('vrm'),
|
||||
literal('live2d'),
|
||||
])
|
||||
|
||||
const PromptTypeSchema = union([
|
||||
literal('system'),
|
||||
literal('personality'),
|
||||
literal('greetings'),
|
||||
])
|
||||
|
||||
const DateSchema = pipe(
|
||||
union([string(), date()]),
|
||||
transform(v => new Date(v)),
|
||||
)
|
||||
|
||||
// --- Base Entities (mimicking database tables) ---
|
||||
|
||||
export const CharacterBaseSchema = object({
|
||||
id: string(),
|
||||
version: string(),
|
||||
coverUrl: string(),
|
||||
creatorId: string(),
|
||||
ownerId: string(),
|
||||
characterId: string(),
|
||||
createdAt: DateSchema,
|
||||
updatedAt: DateSchema,
|
||||
})
|
||||
|
||||
export const CharacterCapabilitySchema = object({
|
||||
id: string(),
|
||||
characterId: string(),
|
||||
type: CharacterCapabilityTypeSchema,
|
||||
config: CharacterCapabilityConfigSchema,
|
||||
})
|
||||
|
||||
export const AvatarModelSchema = object({
|
||||
id: string(),
|
||||
characterId: string(),
|
||||
name: string(),
|
||||
type: AvatarModelTypeSchema,
|
||||
description: string(),
|
||||
config: AvatarModelConfigSchema,
|
||||
createdAt: DateSchema,
|
||||
updatedAt: DateSchema,
|
||||
})
|
||||
|
||||
export const CharacterI18nSchema = object({
|
||||
id: string(),
|
||||
characterId: string(),
|
||||
language: string(),
|
||||
name: string(),
|
||||
description: string(),
|
||||
tags: array(string()),
|
||||
createdAt: DateSchema,
|
||||
updatedAt: DateSchema,
|
||||
})
|
||||
|
||||
export const CharacterPromptSchema = object({
|
||||
id: string(),
|
||||
characterId: string(),
|
||||
language: string(),
|
||||
type: PromptTypeSchema,
|
||||
content: string(),
|
||||
})
|
||||
|
||||
// --- Aggregated Character (with relations) ---
|
||||
|
||||
export const CharacterWithRelationsSchema = object({
|
||||
...CharacterBaseSchema.entries,
|
||||
capabilities: array(CharacterCapabilitySchema),
|
||||
avatarModels: array(AvatarModelSchema),
|
||||
i18n: array(CharacterI18nSchema),
|
||||
prompts: array(CharacterPromptSchema),
|
||||
})
|
||||
|
||||
// --- API Request Schemas ---
|
||||
|
||||
export const CreateCharacterSchema = object({
|
||||
character: object({
|
||||
version: string(),
|
||||
coverUrl: string(),
|
||||
characterId: string(),
|
||||
// creatorId & ownerId are handled by server
|
||||
}),
|
||||
capabilities: optional(array(object({
|
||||
type: CharacterCapabilityTypeSchema,
|
||||
config: CharacterCapabilityConfigSchema,
|
||||
}))),
|
||||
avatarModels: optional(array(object({
|
||||
name: string(),
|
||||
type: AvatarModelTypeSchema,
|
||||
description: string(),
|
||||
config: AvatarModelConfigSchema,
|
||||
}))),
|
||||
i18n: optional(array(object({
|
||||
language: string(),
|
||||
name: string(),
|
||||
description: string(),
|
||||
tags: array(string()),
|
||||
}))),
|
||||
prompts: optional(array(object({
|
||||
language: string(),
|
||||
type: PromptTypeSchema,
|
||||
content: string(),
|
||||
}))),
|
||||
})
|
||||
|
||||
export const UpdateCharacterSchema = object({
|
||||
version: optional(string()),
|
||||
coverUrl: optional(string()),
|
||||
characterId: optional(string()),
|
||||
})
|
||||
|
||||
// --- Type Exports ---
|
||||
|
||||
export type Character = InferOutput<typeof CharacterWithRelationsSchema>
|
||||
export type CharacterBase = InferOutput<typeof CharacterBaseSchema>
|
||||
export type CharacterCapability = InferOutput<typeof CharacterCapabilitySchema>
|
||||
export type AvatarModel = InferOutput<typeof AvatarModelSchema>
|
||||
export type CharacterI18n = InferOutput<typeof CharacterI18nSchema>
|
||||
export type CharacterPrompt = InferOutput<typeof CharacterPromptSchema>
|
||||
|
||||
export type CreateCharacterPayload = InferOutput<typeof CreateCharacterSchema>
|
||||
export type UpdateCharacterPayload = InferOutput<typeof UpdateCharacterSchema>
|
||||
Reference in New Issue
Block a user