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

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

@ -2,94 +2,101 @@
import axios from 'axios'
export interface LoginParams {
account: string
password: string
username: string
password: string
}
export interface RegisterParams {
username: string
password: string
}
export interface LoginResponse {
access_token: string
refresh_token: string
expires_in: number
token_type: string
token: string
}
export interface UserInfo {
userId: string
userAccount: string
userFullName: string
userImageUrl?: string
deptId?: string
deptName?: string
orgId?: string
orgName?: string
roles: string[]
userId: string
userAccount: string
userFullName: string
userImageUrl?: string
deptId?: string
deptName?: string
orgId?: string
orgName?: string
roles: string[]
}
// 创建认证服务的 axios 实例
const authService = axios.create({
baseURL: '/api',
timeout: 30000
baseURL: '/api',
timeout: 30000
})
// 请求拦截器
authService.interceptors.request.use(
config => {
const token = localStorage.getItem('Access-Token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
config => {
const token = localStorage.getItem('Access-Token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
authService.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 清除 token 并跳转登录
localStorage.removeItem('Access-Token')
localStorage.removeItem('Refresh-Token')
window.location.href = '/login'
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)
}
return Promise.reject(error)
}
)
// 登录接口
export async function login(params: LoginParams): Promise<LoginResponse> {
const response = await authService.post<{ data: LoginResponse }>('/auth/login', params)
return response.data.data
const response = await authService.post<{ data: LoginResponse }>('/auth/login', params)
return response.data.data
}
// 注册接口
export async function register(params: RegisterParams): Promise<void> {
await authService.post('/auth/register', params)
}
// 登出接口
export async function logout(): Promise<void> {
await authService.post('/auth/logout')
await authService.post('/auth/logout')
}
// 获取用户信息
export async function getUserInfo(): Promise<UserInfo> {
const response = await authService.get<{ data: UserInfo }>('/auth/user-info')
return response.data.data
const response = await authService.get<{ data: UserInfo }>('/auth/user-info')
return response.data.data
}
// 刷新 token
export async function refreshToken(): Promise<LoginResponse> {
const refreshToken = localStorage.getItem('Refresh-Token')
const response = await authService.post<{ data: LoginResponse }>('/auth/refresh-token', {
refresh_token: refreshToken
})
return response.data.data
const refreshToken = localStorage.getItem('Refresh-Token')
const response = await authService.post<{ data: LoginResponse }>('/auth/refresh-token', {
refresh_token: refreshToken
})
return response.data.data
}
// 交换 dify token (用于 agents 模块)
export async function exchangeDifyToken(): Promise<{ access_token: string; refresh_token: string }> {
const token = localStorage.getItem('Access-Token')
const response = await fetch('/api/console/api/oauth/tokenExchange/cyberwing', {
headers: { Authorization: `Bearer ${token}` }
})
return response.json()
const token = localStorage.getItem('Access-Token')
const response = await fetch('/api/console/api/oauth/tokenExchange/cyberwing', {
headers: {Authorization: `Bearer ${token}`}
})
return response.json()
}
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>
</el-breadcrumb-item>
</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>
<main class="main-content">
<router-view />
@ -81,10 +99,33 @@ import {
List,
Clock,
Tickets,
Document
Document,
User,
SwitchButton
} from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/modules/user'
import { ElMessageBox } from 'element-plus'
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'])
@ -313,6 +354,9 @@ const breadcrumbList = computed(() => {
padding: 16px 24px;
border-bottom: 1px solid #e8e8e8;
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
:deep(.el-breadcrumb) {
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 {
flex: 1;
overflow: auto;

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

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

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

@ -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,
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 服务)
'/api/console': {
target: 'http://192.168.8.122:12800/console',

Loading…
Cancel
Save