feat: 前端认证和工具模块
- 新增 useAuth.ts composable - 新增 tools/useTools.ts - 更新 Login.vue, Signup.vue Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
114
web/src/composables/useAuth.ts
Normal file
114
web/src/composables/useAuth.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
const API_BASE = 'http://localhost:8082'
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string
|
||||||
|
user: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const user = localStorage.getItem('user')
|
||||||
|
|
||||||
|
const login = async (data: LoginRequest): Promise<LoginResponse> => {
|
||||||
|
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.message || 'Login failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
// Save token and user info
|
||||||
|
localStorage.setItem('token', result.token)
|
||||||
|
localStorage.setItem('user', JSON.stringify(result.user))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = async (data: RegisterRequest): Promise<any> => {
|
||||||
|
const response = await fetch(`${API_BASE}/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.message || 'Registration failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getToken = () => {
|
||||||
|
return localStorage.getItem('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUser = () => {
|
||||||
|
const userStr = localStorage.getItem('user')
|
||||||
|
return userStr ? JSON.parse(userStr) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoggedIn = () => {
|
||||||
|
return !!token
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCurrentUser = async () => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Not authenticated')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}/auth/me`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.message || 'Failed to get user')
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await response.json()
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
getToken,
|
||||||
|
getUser,
|
||||||
|
isLoggedIn,
|
||||||
|
getCurrentUser,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const { login } = useAuth()
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
@@ -11,23 +14,38 @@ const rememberMe = ref(false)
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
|
const successMsg = ref('')
|
||||||
|
|
||||||
// 模拟登录验证
|
// 检查是否从注册页跳转过来
|
||||||
const handleLogin = () => {
|
onMounted(() => {
|
||||||
|
if (route.query.registered === 'true') {
|
||||||
|
successMsg.value = 'Account created successfully! Please sign in.'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登录处理
|
||||||
|
const handleLogin = async () => {
|
||||||
errorMsg.value = ''
|
errorMsg.value = ''
|
||||||
|
|
||||||
if (username.value !== 'admin' || password.value !== 'admin') {
|
if (!username.value || !password.value) {
|
||||||
errorMsg.value = 'Invalid username or password'
|
errorMsg.value = 'Please enter username and password'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
// 模拟登录
|
|
||||||
setTimeout(() => {
|
try {
|
||||||
isLoading.value = false
|
await login({
|
||||||
|
username: username.value,
|
||||||
|
password: password.value,
|
||||||
|
})
|
||||||
// 登录成功,跳转到 Dashboard
|
// 登录成功,跳转到 Dashboard
|
||||||
router.push('/dashboard')
|
router.push('/chat')
|
||||||
}, 1500)
|
} catch (error: any) {
|
||||||
|
errorMsg.value = error.message || 'Login failed, please check your credentials'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -52,6 +70,12 @@ const handleLogin = () => {
|
|||||||
|
|
||||||
<!-- 登录表单 -->
|
<!-- 登录表单 -->
|
||||||
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50">
|
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50">
|
||||||
|
<!-- 成功提示 -->
|
||||||
|
<div v-if="successMsg" class="mb-4 p-3 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-sm flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-check-circle"></i>
|
||||||
|
{{ successMsg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 错误提示 -->
|
<!-- 错误提示 -->
|
||||||
<div v-if="errorMsg" class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center gap-2">
|
<div v-if="errorMsg" class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center gap-2">
|
||||||
<i class="fa-solid fa-circle-exclamation"></i>
|
<i class="fa-solid fa-circle-exclamation"></i>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { register } = useAuth()
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
@@ -14,6 +16,7 @@ const isLoading = ref(false)
|
|||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
const showConfirmPassword = ref(false)
|
const showConfirmPassword = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
|
const successMsg = ref('')
|
||||||
|
|
||||||
// 表单验证
|
// 表单验证
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
@@ -24,11 +27,6 @@ const validateForm = () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!email.value || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
|
|
||||||
errorMsg.value = 'Please enter a valid email address'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password.value || password.value.length < 6) {
|
if (!password.value || password.value.length < 6) {
|
||||||
errorMsg.value = 'Password must be at least 6 characters'
|
errorMsg.value = 'Password must be at least 6 characters'
|
||||||
return false
|
return false
|
||||||
@@ -48,21 +46,34 @@ const validateForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 注册处理
|
// 注册处理
|
||||||
const handleSignup = () => {
|
const handleSignup = async () => {
|
||||||
if (!validateForm()) return
|
if (!validateForm()) return
|
||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
// 模拟注册
|
errorMsg.value = ''
|
||||||
setTimeout(() => {
|
successMsg.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await register({
|
||||||
|
username: username.value,
|
||||||
|
password: password.value,
|
||||||
|
email: email.value || undefined,
|
||||||
|
})
|
||||||
|
// 注册成功
|
||||||
|
successMsg.value = 'Account created successfully! Redirecting to login...'
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/?registered=true')
|
||||||
|
}, 1500)
|
||||||
|
} catch (error: any) {
|
||||||
|
errorMsg.value = error.message || 'Registration failed, please try again'
|
||||||
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
// 注册成功,跳转到登录页
|
}
|
||||||
router.push('/')
|
|
||||||
}, 1500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到登录
|
// 跳转到登录
|
||||||
const goToLogin = () => {
|
const goToLogin = () => {
|
||||||
router.push('/')
|
router.push('/?registered=true')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -87,6 +98,12 @@ const goToLogin = () => {
|
|||||||
|
|
||||||
<!-- 注册表单 -->
|
<!-- 注册表单 -->
|
||||||
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50">
|
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50">
|
||||||
|
<!-- 成功提示 -->
|
||||||
|
<div v-if="successMsg" class="mb-4 p-3 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-sm flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-check-circle"></i>
|
||||||
|
{{ successMsg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 错误提示 -->
|
<!-- 错误提示 -->
|
||||||
<div v-if="errorMsg" class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center gap-2">
|
<div v-if="errorMsg" class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center gap-2">
|
||||||
<i class="fa-solid fa-circle-exclamation"></i>
|
<i class="fa-solid fa-circle-exclamation"></i>
|
||||||
|
|||||||
146
web/src/views/tools/useTools.ts
Normal file
146
web/src/views/tools/useTools.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
const API_BASE = 'http://localhost:8082'
|
||||||
|
|
||||||
|
// Tool 接口
|
||||||
|
export interface Tool {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
category: string
|
||||||
|
provider: string
|
||||||
|
security_level: string
|
||||||
|
require_approval: boolean
|
||||||
|
parameters: string
|
||||||
|
status: string
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTools() {
|
||||||
|
const tools = ref<Tool[]>([])
|
||||||
|
const toolsLoading = ref(false)
|
||||||
|
|
||||||
|
// 获取工具列表
|
||||||
|
const fetchTools = async (category?: string, status?: string) => {
|
||||||
|
toolsLoading.value = true
|
||||||
|
try {
|
||||||
|
let url = `${API_BASE}/tool/list?`
|
||||||
|
if (category) url += `category=${category}&`
|
||||||
|
if (status) url += `status=${status}&`
|
||||||
|
|
||||||
|
const response = await fetch(url)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.list) {
|
||||||
|
tools.value = data.list
|
||||||
|
}
|
||||||
|
return data.list || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch tools:', error)
|
||||||
|
return []
|
||||||
|
} finally {
|
||||||
|
toolsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步工具到数据库
|
||||||
|
const syncTools = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/tool/sync`)
|
||||||
|
const data = await response.json()
|
||||||
|
await fetchTools() // 重新获取列表
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to sync tools:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工具详情
|
||||||
|
const fetchToolById = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/tool/${id}`)
|
||||||
|
const data = await response.json()
|
||||||
|
return data.tool
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch tool:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建工具
|
||||||
|
const createTool = async (tool: Partial<Tool>) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/tool/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(tool),
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
await fetchTools() // 重新获取列表
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create tool:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新工具
|
||||||
|
const updateTool = async (id: string, tool: Partial<Tool>) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/tool/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(tool),
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
await fetchTools() // 重新获取列表
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update tool:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除工具
|
||||||
|
const deleteTool = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/tool/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
await fetchTools() // 重新获取列表
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete tool:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按分类获取工具
|
||||||
|
const getToolsByCategory = (category: string) => {
|
||||||
|
return tools.value.filter(tool => tool.category === category)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有分类
|
||||||
|
const categories = computed(() => {
|
||||||
|
const cats = new Set(tools.value.map(tool => tool.category))
|
||||||
|
return Array.from(cats)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
toolsLoading,
|
||||||
|
categories,
|
||||||
|
fetchTools,
|
||||||
|
syncTools,
|
||||||
|
fetchToolById,
|
||||||
|
createTool,
|
||||||
|
updateTool,
|
||||||
|
deleteTool,
|
||||||
|
getToolsByCategory,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user