feat: 添加用户注册功能,新增数据工厂专用鉴权接口

master
钟良源 1 month ago
parent 110b621375
commit c0f6f75e49

@ -2,94 +2,101 @@
import axios from 'axios' import axios from 'axios'
export interface LoginParams { export interface LoginParams {
account: string username: string
password: string password: string
}
export interface RegisterParams {
username: string
password: string
} }
export interface LoginResponse { export interface LoginResponse {
access_token: string token: string
refresh_token: string
expires_in: number
token_type: string
} }
export interface UserInfo { export interface UserInfo {
userId: string userId: string
userAccount: string userAccount: string
userFullName: string userFullName: string
userImageUrl?: string userImageUrl?: string
deptId?: string deptId?: string
deptName?: string deptName?: string
orgId?: string orgId?: string
orgName?: string orgName?: string
roles: string[] roles: string[]
} }
// 创建认证服务的 axios 实例 // 创建认证服务的 axios 实例
const authService = axios.create({ const authService = axios.create({
baseURL: '/api', baseURL: '/api',
timeout: 30000 timeout: 30000
}) })
// 请求拦截器 // 请求拦截器
authService.interceptors.request.use( authService.interceptors.request.use(
config => { config => {
const token = localStorage.getItem('Access-Token') const token = localStorage.getItem('Access-Token')
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${token}`
} }
return config return config
}, },
error => Promise.reject(error) error => Promise.reject(error)
) )
// 响应拦截器 // 响应拦截器
authService.interceptors.response.use( authService.interceptors.response.use(
response => response, response => response,
error => { error => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
// 清除 token 并跳转登录 // 清除 token 并跳转登录
localStorage.removeItem('Access-Token') localStorage.removeItem('Access-Token')
localStorage.removeItem('Refresh-Token') localStorage.removeItem('Refresh-Token')
window.location.href = '/login' window.location.href = '/login'
}
return Promise.reject(error)
} }
return Promise.reject(error)
}
) )
// 登录接口 // 登录接口
export async function login(params: LoginParams): Promise<LoginResponse> { export async function login(params: LoginParams): Promise<LoginResponse> {
const response = await authService.post<{ data: LoginResponse }>('/auth/login', params) const response = await authService.post<{ data: LoginResponse }>('/auth/login', params)
return response.data.data return response.data.data
}
// 注册接口
export async function register(params: RegisterParams): Promise<void> {
await authService.post('/auth/register', params)
} }
// 登出接口 // 登出接口
export async function logout(): Promise<void> { export async function logout(): Promise<void> {
await authService.post('/auth/logout') await authService.post('/auth/logout')
} }
// 获取用户信息 // 获取用户信息
export async function getUserInfo(): Promise<UserInfo> { export async function getUserInfo(): Promise<UserInfo> {
const response = await authService.get<{ data: UserInfo }>('/auth/user-info') const response = await authService.get<{ data: UserInfo }>('/auth/user-info')
return response.data.data return response.data.data
} }
// 刷新 token // 刷新 token
export async function refreshToken(): Promise<LoginResponse> { export async function refreshToken(): Promise<LoginResponse> {
const refreshToken = localStorage.getItem('Refresh-Token') const refreshToken = localStorage.getItem('Refresh-Token')
const response = await authService.post<{ data: LoginResponse }>('/auth/refresh-token', { const response = await authService.post<{ data: LoginResponse }>('/auth/refresh-token', {
refresh_token: refreshToken refresh_token: refreshToken
}) })
return response.data.data return response.data.data
} }
// 交换 dify token (用于 agents 模块) // 交换 dify token (用于 agents 模块)
export async function exchangeDifyToken(): Promise<{ access_token: string; refresh_token: string }> { export async function exchangeDifyToken(): Promise<{ access_token: string; refresh_token: string }> {
const token = localStorage.getItem('Access-Token') const token = localStorage.getItem('Access-Token')
const response = await fetch('/api/console/api/oauth/tokenExchange/cyberwing', { const response = await fetch('/api/console/api/oauth/tokenExchange/cyberwing', {
headers: { Authorization: `Bearer ${token}` } headers: {Authorization: `Bearer ${token}`}
}) })
return response.json() return response.json()
} }
export default authService export default authService

@ -0,0 +1,67 @@
// 用户认证相关 API使用 databuilder 服务)
import axios from 'axios'
export interface LoginParams {
username: string
password: string
}
export interface RegisterParams {
username: string
password: string
}
export interface LoginResponse {
access_token: string
token_type: string
}
// 创建认证服务的 axios 实例
const userAuthService = axios.create({
baseURL: '/api/databuilder/v1',
timeout: 30000
})
// 请求拦截器
userAuthService.interceptors.request.use(
config => {
const token = localStorage.getItem('Access-Token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
userAuthService.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 清除 token 并跳转登录
localStorage.removeItem('Access-Token')
localStorage.removeItem('Refresh-Token')
window.location.href = '/#/login'
}
return Promise.reject(error)
}
)
// 注册接口 POST /auth/register
export async function register(params: RegisterParams): Promise<void> {
await userAuthService.post('/auth/register', params)
}
// 登录接口 POST /auth/login
export async function login(params: LoginParams): Promise<LoginResponse> {
const response = await userAuthService.post<{ data: LoginResponse }>('/auth/login', params)
return response.data.data
}
// 登出接口 POST /auth/logout
export async function logout(): Promise<void> {
await userAuthService.post('/auth/logout')
}
export default userAuthService

@ -60,6 +60,24 @@
<span v-else>{{ item.title }}</span> <span v-else>{{ item.title }}</span>
</el-breadcrumb-item> </el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
<div class="header-actions">
<el-dropdown @command="handleCommand">
<span class="user-dropdown">
<el-icon><User /></el-icon>
<span class="username">{{ userStore.userInfo.userFullName || userStore.userInfo.userAccount || '用户' }}</span>
<el-icon class="arrow"><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="logout">
<el-icon><SwitchButton /></el-icon>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header> </header>
<main class="main-content"> <main class="main-content">
<router-view /> <router-view />
@ -81,10 +99,33 @@ import {
List, List,
Clock, Clock,
Tickets, Tickets,
Document Document,
User,
SwitchButton
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/modules/user'
import { ElMessageBox } from 'element-plus'
const route = useRoute() const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
//
const handleCommand = async (command: string) => {
if (command === 'logout') {
try {
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await userStore.logout()
router.push('/login')
} catch {
//
}
}
}
// //
const expandedMenus = ref<string[]>(['/data-factory/process']) const expandedMenus = ref<string[]>(['/data-factory/process'])
@ -313,6 +354,9 @@ const breadcrumbList = computed(() => {
padding: 16px 24px; padding: 16px 24px;
border-bottom: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8;
flex-shrink: 0; flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
:deep(.el-breadcrumb) { :deep(.el-breadcrumb) {
font-size: 14px; font-size: 14px;
@ -339,6 +383,36 @@ const breadcrumbList = computed(() => {
} }
} }
.header-actions {
.user-dropdown {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
color: #666;
font-size: 14px;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.2s;
&:hover {
background: #f5f5f5;
color: #409eff;
}
.username {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow {
font-size: 12px;
}
}
}
.main-content { .main-content {
flex: 1; flex: 1;
overflow: auto; overflow: auto;

@ -15,6 +15,15 @@ const routes: RouteRecordRaw[] = [
requiresAuth: false requiresAuth: false
} }
}, },
{
path: '/register',
name: 'Register',
component: () => import('@/views/register/index.vue'),
meta: {
title: '注册',
requiresAuth: false
}
},
...dataRoutes ...dataRoutes
] ]
@ -24,7 +33,7 @@ const router = createRouter({
}) })
// 白名单路由(不需要登录) // 白名单路由(不需要登录)
const whiteList = ['/login'] const whiteList = ['/login', '/register']
// 路由守卫 // 路由守卫
router.beforeEach((to, _from, next) => { router.beforeEach((to, _from, next) => {

@ -1,118 +1,131 @@
import { defineStore } from 'pinia' import {defineStore} from 'pinia'
import { ref, computed } from 'vue' import {ref, computed} from 'vue'
import { login as loginApi, logout as logoutApi, getUserInfo, exchangeDifyToken, LoginParams, UserInfo } from '@/api/auth' import {getUserInfo, exchangeDifyToken, UserInfo} from '@/api/auth'
import { removeDifyToken } from '@/micro-app/agents/request/service' import {
login as loginApi,
logout as logoutApi,
register as registerApi,
LoginParams,
RegisterParams
} from '@/api/user-auth'
import {removeDifyToken} from '@/micro-app/agents/request/service'
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('Access-Token') || '') const token = ref(localStorage.getItem('Access-Token') || '')
const refreshToken = ref(localStorage.getItem('Refresh-Token') || '') const refreshToken = ref(localStorage.getItem('Refresh-Token') || '')
const userInfo = ref<UserInfo>({ const userInfo = ref<UserInfo>({
userId: '', userId: '',
userAccount: '', userAccount: '',
userFullName: '', userFullName: '',
userImageUrl: '', userImageUrl: '',
deptId: '', deptId: '',
deptName: '', deptName: '',
orgId: '', orgId: '',
orgName: '', orgName: '',
roles: [] roles: []
}) })
// 是否已登录 // 是否已登录
const isLoggedIn = computed(() => !!token.value) const isLoggedIn = computed(() => !!token.value)
// 设置 token // 设置 token
const setToken = (accessToken: string, refToken?: string) => { const setToken = (accessToken: string, refToken?: string) => {
token.value = accessToken token.value = accessToken
localStorage.setItem('Access-Token', accessToken) localStorage.setItem('Access-Token', accessToken)
if (refToken) { if (refToken) {
refreshToken.value = refToken refreshToken.value = refToken
localStorage.setItem('Refresh-Token', refToken) localStorage.setItem('Refresh-Token', refToken)
}
} }
}
// 设置用户信息 // 设置用户信息
const setUserInfo = (info: UserInfo) => { const setUserInfo = (info: UserInfo) => {
userInfo.value = info userInfo.value = info
} }
// 登录
const login = async (params: LoginParams) => {
try {
const res = await loginApi(params)
setToken(res.access_token)
// 登录 // 获取用户信息
const login = async (params: LoginParams) => { // await fetchUserInfo()
try {
const res = await loginApi(params)
setToken(res.access_token, res.refresh_token)
// 获取用户信息 // 交换 dify token (用于 agents 模块)
await fetchUserInfo() try {
const difyTokenRes = await exchangeDifyToken()
localStorage.setItem('dify_access_token', difyTokenRes.access_token)
localStorage.setItem('dify_refresh_token', difyTokenRes.refresh_token)
} catch (e) {
console.warn('交换 dify token 失败', e)
}
// 交换 dify token (用于 agents 模块) return res
try { } catch (error) {
const difyTokenRes = await exchangeDifyToken() throw error
localStorage.setItem('dify_access_token', difyTokenRes.access_token) }
localStorage.setItem('dify_refresh_token', difyTokenRes.refresh_token) }
} catch (e) {
console.warn('交换 dify token 失败', e)
}
return res // 注册
} catch (error) { const register = async (params: RegisterParams) => {
throw error await registerApi(params)
} }
}
// 获取用户信息 // 获取用户信息
const fetchUserInfo = async () => { const fetchUserInfo = async () => {
try { try {
const info = await getUserInfo() const info = await getUserInfo()
setUserInfo(info) setUserInfo(info)
return info return info
} catch (error) { } catch (error) {
throw error throw error
}
} }
}
// 登出 // 登出
const logout = async () => { const logout = async () => {
try { try {
await logoutApi() await logoutApi()
} catch (e) { } catch (e) {
console.warn('登出接口调用失败', e) console.warn('登出接口调用失败', e)
} finally { } finally {
clearAuth() clearAuth()
}
} }
}
// 清除认证信息 // 清除认证信息
const clearAuth = () => { const clearAuth = () => {
token.value = '' token.value = ''
refreshToken.value = '' refreshToken.value = ''
userInfo.value = { userInfo.value = {
userId: '', userId: '',
userAccount: '', userAccount: '',
userFullName: '', userFullName: '',
userImageUrl: '', userImageUrl: '',
deptId: '', deptId: '',
deptName: '', deptName: '',
orgId: '', orgId: '',
orgName: '', orgName: '',
roles: [] roles: []
}
localStorage.removeItem('Access-Token')
localStorage.removeItem('Refresh-Token')
// 清除 dify token
removeDifyToken()
} }
localStorage.removeItem('Access-Token')
localStorage.removeItem('Refresh-Token')
// 清除 dify token
removeDifyToken()
}
return { return {
token, token,
refreshToken, refreshToken,
userInfo, userInfo,
isLoggedIn, isLoggedIn,
setToken, setToken,
setUserInfo, setUserInfo,
login, login,
logout, register,
fetchUserInfo, logout,
clearAuth fetchUserInfo,
} clearAuth
}
}) })

@ -12,9 +12,9 @@
class="login-form" class="login-form"
size="large" size="large"
> >
<el-form-item prop="account"> <el-form-item prop="username">
<el-input <el-input
v-model="loginForm.account" v-model="loginForm.username"
placeholder="请输入用户名" placeholder="请输入用户名"
:prefix-icon="User" :prefix-icon="User"
:disabled="loading" :disabled="loading"
@ -44,6 +44,11 @@
{{ loading ? '登录中...' : '登录' }} {{ loading ? '登录中...' : '登录' }}
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item>
<div class="register-link">
没有账号<router-link to="/register">立即注册</router-link>
</div>
</el-form-item>
</el-form> </el-form>
</div> </div>
</div> </div>
@ -65,12 +70,12 @@ const loading = ref(false)
const rememberPassword = ref(true) const rememberPassword = ref(true)
const loginForm = reactive({ const loginForm = reactive({
account: '', username: '',
password: '' password: ''
}) })
const loginRules: FormRules = { const loginRules: FormRules = {
account: [ username: [
{required: true, message: '请输入用户名', trigger: 'blur'}, {required: true, message: '请输入用户名', trigger: 'blur'},
{min: 1, max: 50, message: '用户名长度不超过50字符', trigger: 'blur'} {min: 1, max: 50, message: '用户名长度不超过50字符', trigger: 'blur'}
], ],
@ -84,8 +89,8 @@ onMounted(() => {
const savedCredentials = localStorage.getItem('login-credentials') const savedCredentials = localStorage.getItem('login-credentials')
if (savedCredentials) { if (savedCredentials) {
try { try {
const {account, password} = JSON.parse(atob(savedCredentials)) const {username, password} = JSON.parse(atob(savedCredentials))
loginForm.account = account || '' loginForm.username = username || ''
loginForm.password = password || '' loginForm.password = password || ''
} catch (e) { } catch (e) {
console.error('Failed to parse saved credentials') console.error('Failed to parse saved credentials')
@ -101,19 +106,16 @@ const handleLogin = async () => {
loading.value = true loading.value = true
try { try {
// await userStore.login({ await userStore.login({
// account: loginForm.account, username: loginForm.username,
// password: loginForm.password password: loginForm.password
// }) })
// token
localStorage.setItem('Access-Token', 'eyJraWQiOiJmMGQ4YjkyZC0yZjIzLTQzNmUtOGIzMS1hZWQzOGEwZmY5MDYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbnVzZXJkZXB0aWQiLCJhdWQiOiJvYXV0aDJzZXJ2ZXIiLCJuYmYiOjE3NTYwODM3MjUsImNsaWVudElkIjoibGxtYWlwIiwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiaXNzIjoiaHR0cDpcL1wvYXV0aC1zZXJ2ZXI6ODA4NiIsImV4cCI6MTc1NjA4NzMyNSwibG9jYWxlIjoiemgtY24iLCJpYXQiOjE3NTYwODM3MjUsInVzZXJkZXB0SWQiOiJhZG1pbnVzZXJkZXB0aWQifQ.IGB4FZ1uYErluF0q7Owutw7GGqxVGCknSQNcOlgo1tmU-lLF0YVQ3eONX-iNQo4C4Um-q7FVkk5UZtuz190HJRIE7RenaFzYa4UjAGrGfgN7ZjjlXTHGy2T2Z_tKzrQlsF4i56SegXhKZGfHXGmBDTc9i3bhu9Frs-pWCahpP2Ehg7tP9q4pnzePr0wqIkMhgdFsq2sLBSwYzWwXHVVexgbySLbWCJlI0c-IdH0dYBHowMT_tVfUqqpg5IQysUokLspRjM_mxlIahMNRkiwvx8X4LjASGJMzWj0VsQJf-IBJKi13O-_f9gaR8Sq4eAar6i-e7X9BFoV9lRXNXp_2DA')
// //
if (rememberPassword.value) { if (rememberPassword.value) {
localStorage.setItem( localStorage.setItem(
'login-credentials', 'login-credentials',
btoa(JSON.stringify({account: loginForm.account, password: loginForm.password})) btoa(JSON.stringify({username: loginForm.username, password: loginForm.password}))
) )
} else { } else {
localStorage.removeItem('login-credentials') localStorage.removeItem('login-credentials')
@ -136,7 +138,7 @@ const handleLogin = async () => {
.login-container { .login-container {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #1e3a5f 0%, #0d2137 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -166,7 +168,7 @@ const handleLogin = async () => {
color: #333; color: #333;
margin: 0; margin: 0;
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 3px solid #667eea; border-bottom: 3px solid #1e3a5f;
display: inline-block; display: inline-block;
} }
@ -182,4 +184,20 @@ const handleLogin = async () => {
font-size: 16px; font-size: 16px;
border-radius: 8px; border-radius: 8px;
} }
.register-link {
width: 100%;
text-align: center;
color: #666;
a {
color: #1e3a5f;
text-decoration: none;
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
}
</style> </style>

@ -0,0 +1,195 @@
<template>
<div class="register-container">
<div class="register-wrapper">
<div class="register-card">
<div class="register-header">
<h2 class="register-title">用户注册</h2>
</div>
<el-form
ref="registerFormRef"
:model="registerForm"
:rules="registerRules"
class="register-form"
size="large"
>
<el-form-item prop="username">
<el-input
v-model="registerForm.username"
placeholder="请输入用户名"
:prefix-icon="User"
:disabled="loading"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="请输入密码"
:prefix-icon="Lock"
:disabled="loading"
show-password
/>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
placeholder="请确认密码"
:prefix-icon="Lock"
:disabled="loading"
show-password
@keyup.enter="handleRegister"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="register-btn"
:loading="loading"
@click="handleRegister"
>
{{ loading ? '注册中...' : '注册' }}
</el-button>
</el-form-item>
<el-form-item>
<div class="login-link">
已有账号<router-link to="/login">立即登录</router-link>
</div>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { User, Lock } from '@element-plus/icons-vue'
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import { useUserStore } from '@/stores/modules/user'
import { useRouter } from 'vue-router'
const router = useRouter()
const userStore = useUserStore()
const registerFormRef = ref<FormInstance>()
const loading = ref(false)
const registerForm = reactive({
username: '',
password: '',
confirmPassword: ''
})
const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
if (value !== registerForm.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const registerRules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 1, max: 50, message: '用户名长度不超过50字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}
const handleRegister = async () => {
if (!registerFormRef.value) return
const valid = await registerFormRef.value.validate().catch(() => false)
if (!valid) return
loading.value = true
try {
await userStore.register({
username: registerForm.username,
password: registerForm.password
})
ElMessage.success('注册成功,请登录')
router.push('/login')
} catch (error: any) {
ElMessage.error(error?.response?.data?.message || error?.message || '注册失败')
} finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
.register-container {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #1e3a5f 0%, #0d2137 100%);
display: flex;
align-items: center;
justify-content: center;
}
.register-wrapper {
width: 100%;
max-width: 400px;
padding: 20px;
}
.register-card {
background: #fff;
border-radius: 12px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.register-header {
text-align: center;
margin-bottom: 30px;
}
.register-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin: 0;
padding-bottom: 10px;
border-bottom: 3px solid #1e3a5f;
display: inline-block;
}
.register-form {
.el-form-item {
margin-bottom: 20px;
}
}
.register-btn {
width: 100%;
height: 44px;
font-size: 16px;
border-radius: 8px;
}
.login-link {
width: 100%;
text-align: center;
color: #666;
a {
color: #1e3a5f;
text-decoration: none;
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
}
</style>

@ -58,6 +58,12 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/api\/databuilder/, '') rewrite: path => path.replace(/^\/api\/databuilder/, '')
}, },
// 数据工厂专用鉴权接口
'/api/databuilder/v1/': {
target: 'http://192.168.8.131:8084',
changeOrigin: true,
rewrite: path => path.replace(/^\/api\/databuilder\/v1/, '')
},
// console API (dify agents 服务) // console API (dify agents 服务)
'/api/console': { '/api/console': {
target: 'http://192.168.8.122:12800/console', target: 'http://192.168.8.122:12800/console',

Loading…
Cancel
Save