refactor(ui): Replace axios with fetch in all pages and extract auth layout

- Replace all $axios calls with native fetch across 8 page files
    - Add inline send-code button to login page code flow
    - Extract animated bubble background into shared auth layout
    - Restructure langs.json with zh-CN first, add new i18n keys
    - Add scoped langs.json for auth.send_code translation
    - Wire auth layout to login and callback routes
    - Fix mobile sign-up panel transform direction
    - Use onAuthSuccess for login/register code flows
master
veypi 3 weeks ago
parent 5c542daba0
commit 7022c2b52f

@ -1,4 +1,211 @@
{
"zh-CN": {
"auth.account": "用户名/邮箱",
"auth.back_to_login": "返回登录",
"auth.bind_account": "绑定账号",
"auth.bind_exist_desc": "将 {provider} 绑定到现有账号",
"auth.bind_existing": "绑定已有账号",
"auth.bind_login_tip": "请登录您的个人账号以保持连接",
"auth.bind_new_desc": "创建新账号并绑定 {provider}",
"auth.bind_register_tip": "输入您的个人信息,开启您的旅程",
"auth.bind_success": "绑定成功",
"auth.change_password": "修改密码",
"auth.code_login": "验证码",
"auth.code_sent": "验证码已发送",
"auth.confirm_password": "确认密码",
"auth.create_account": "创建账户",
"auth.email": "邮箱",
"auth.email_disabled": "邮箱登录未启用",
"auth.email_or_phone": "邮箱或手机号",
"auth.fill_all_fields": "请填写所有必填字段",
"auth.fill_required": "请填写所有必填项",
"auth.forgot_password": "忘记密码?",
"auth.have_account": "已有账户?",
"auth.hello_friend": "你好,朋友!",
"auth.keep_connected": "请使用您的个人信息登录,保持连接",
"auth.linked_accounts": "账号绑定",
"auth.login": "登录",
"auth.login_bind": "登录并绑定",
"auth.login_failed": "登录失败",
"auth.login_success": "登录成功",
"auth.logout": "登出",
"auth.new_password": "新密码",
"auth.no_account": "还没有账户?",
"auth.old_password": "当前密码",
"auth.password": "密码",
"auth.password_changed": "密码修改成功",
"auth.password_too_short": "密码至少8个字符",
"auth.passwords_not_match": "两次输入的密码不一致",
"auth.phone_placeholder": "请输入手机号",
"auth.register": "注册",
"auth.register_failed": "注册失败",
"auth.register_success": "注册成功",
"auth.security": "安全设置",
"auth.send_code": "发送验证码",
"auth.sign_up_bind": "注册并绑定",
"auth.sms_disabled": "手机号登录未启用",
"auth.start_journey": "输入您的个人信息,开始您的旅程",
"auth.target_and_code_required": "请输入邮箱/手机号和验证码",
"auth.target_required": "请输入邮箱或手机号",
"auth.unbind_confirm": "确定要解绑该账号吗?",
"auth.use_account": "或使用您的账户",
"auth.use_info_register": "或使用您的信息进行注册",
"auth.username": "用户名",
"auth.username_login": "用户名",
"auth.username_or_email": "用户名或邮箱",
"auth.username_required": "用户名必填",
"auth.username_too_short": "用户名至少3个字符",
"auth.verification_code": "验证码",
"auth.welcome_back": "欢迎回来!",
"common.actions": "操作",
"common.cancel": "取消",
"common.close": "关闭",
"common.create": "创建",
"common.delete": "删除",
"common.detail": "详情",
"common.edit": "编辑",
"common.forbidden": "禁止访问",
"common.not_found": "页面未找到",
"common.processing": "处理中...",
"common.remove": "移除",
"common.reset": "重置",
"common.save": "保存",
"common.save_success": "保存成功",
"common.saving": "保存中...",
"common.success": "成功",
"common.unbind": "解绑",
"nav.dashboard": "仪表盘",
"nav.home": "首页",
"nav.oauth": "OAuth应用",
"nav.oauth_providers": "身份源管理",
"nav.profile": "个人中心",
"nav.roles": "角色管理",
"nav.settings": "系统设置",
"nav.users": "用户管理",
"oauth.create": "创建",
"oauth.create_app": "创建应用",
"oauth.provider.client_id": "客户端ID",
"oauth.provider.client_secret": "客户端密钥",
"oauth.provider.code": "代码",
"oauth.provider.create": "添加身份源",
"oauth.provider.edit": "编辑身份源",
"oauth.provider.enabled": "启用",
"oauth.provider.name": "名称",
"oauth.provider.redirect_uri": "回调地址",
"org.created": "创建成功",
"org.deleted": "删除成功",
"org.required_fields": "请填写必填字段",
"org.updated": "更新成功",
"permission.level.admin": "管理",
"permission.level.create": "创建",
"permission.level.read": "读取",
"permission.level.rw": "读写",
"permission.level.write": "写入",
"role.add_permission": "添加权限",
"role.add_user": "添加用户",
"role.code": "角色代码",
"role.create": "创建角色",
"role.delete_confirm": "确定要删除该角色吗?",
"role.description": "描述",
"role.detail": "角色详情",
"role.edit": "编辑角色",
"role.name": "角色名称",
"role.no_available_permissions": "没有可用权限",
"role.no_available_users": "没有可用用户",
"role.no_permissions": "未分配权限",
"role.no_users": "未分配用户",
"role.permission_removed": "权限已移除",
"role.permissions": "权限管理",
"role.permissions_desc": "管理此角色的权限",
"role.permissions_updated": "权限已更新",
"role.remove_permission_confirm": "确定移除该权限?",
"role.remove_user_confirm": "确定将该用户从角色中移除?",
"role.search_permissions": "搜索权限...",
"role.search_placeholder": "搜索角色...",
"role.search_users": "搜索用户...",
"role.search_users_placeholder": "搜索用户名或昵称...",
"role.select_permissions": "选择权限",
"role.select_users": "选择用户",
"role.user_removed": "用户已移除",
"role.users": "用户管理",
"role.users_updated": "用户已更新",
"settings.app.id": "应用标识",
"settings.app.id_desc": "应用的唯一标识符",
"settings.app.name": "应用名称",
"settings.app.name_desc": "应用的显示名称",
"settings.auth.login_methods": "登录方式",
"settings.auth.login_methods_desc": "支持的登录方式JSON数组",
"settings.auth.password_fields": "密码登录字段",
"settings.auth.password_fields_desc": "密码登录支持的字段",
"settings.auth.reg_require_email": "注册需要邮箱",
"settings.auth.reg_require_email_desc": "注册时强制要求填写邮箱",
"settings.auth.reg_require_phone": "注册需要手机号",
"settings.auth.reg_require_phone_desc": "注册时强制要求填写手机号",
"settings.category.app": "应用配置",
"settings.category.auth": "认证配置",
"settings.category.code": "验证码配置",
"settings.category.email": "邮件配置",
"settings.category.security": "安全配置",
"settings.category.sms": "短信配置",
"settings.code.expiry": "验证码有效期",
"settings.code.expiry_desc": "验证码有效时间(分钟)",
"settings.code.length": "验证码长度",
"settings.code.length_desc": "验证码的位数",
"settings.code.max_attempt": "最大尝试次数",
"settings.code.max_attempt_desc": "验证码最大尝试次数",
"settings.code.max_daily_count": "单日最大发送次数",
"settings.code.max_daily_count_desc": "单日发送验证码最大次数0禁用-1不限制",
"settings.code.send_interval": "发送间隔",
"settings.code.send_interval_desc": "两次发送之间的最小间隔(秒)",
"settings.email.enabled": "启用邮件",
"settings.email.enabled_desc": "启用邮件服务用于通知",
"settings.email.from": "发件人邮箱",
"settings.email.from_desc": "发件人邮箱地址",
"settings.email.from_name": "发件人名称",
"settings.email.from_name_desc": "发件人显示名称",
"settings.email.provider": "邮件服务商",
"settings.email.provider_desc": "邮件服务提供商",
"settings.email.smtp_host": "SMTP服务器",
"settings.email.smtp_host_desc": "SMTP服务器地址",
"settings.email.smtp_pass": "SMTP密码",
"settings.email.smtp_pass_desc": "SMTP认证密码",
"settings.email.smtp_port": "SMTP端口",
"settings.email.smtp_port_desc": "SMTP服务器端口",
"settings.email.smtp_user": "SMTP用户名",
"settings.email.smtp_user_desc": "SMTP认证用户名",
"settings.load_failed": "加载设置失败",
"settings.no_changes": "没有需要保存的更改",
"settings.reset_done": "设置已重置",
"settings.save_failed": "保存设置失败",
"settings.save_success": "设置保存成功",
"settings.security.bcrypt_cost": "Bcrypt强度",
"settings.security.bcrypt_cost_desc": "密码哈希强度4-31",
"settings.security.captcha_enabled": "启用验证码",
"settings.security.captcha_enabled_desc": "登录时显示验证码验证",
"settings.security.max_login_attempts": "最大登录尝试次数",
"settings.security.max_login_attempts_desc": "失败登录尝试的最大次数",
"settings.sms.access_key": "Access Key",
"settings.sms.access_key_desc": "API访问密钥ID",
"settings.sms.access_secret": "Access Secret",
"settings.sms.access_secret_desc": "API访问密钥密文",
"settings.sms.enabled": "启用短信",
"settings.sms.enabled_desc": "启用短信服务用于验证码",
"settings.sms.provider": "短信服务商",
"settings.sms.provider_aliyun": "阿里云",
"settings.sms.provider_desc": "短信服务提供商",
"settings.sms.provider_tencent": "腾讯云",
"settings.sms.sign_name": "短信签名",
"settings.sms.sign_name_desc": "已注册的短信签名",
"settings.sms.template_code": "模板代码",
"settings.sms.template_code_desc": "验证码模板ID",
"user.avatar_url": "头像链接",
"user.email": "邮箱",
"user.info": "基本信息",
"user.nickname": "昵称",
"user.phone": "手机号",
"user.profile": "个人资料",
"user.username": "用户名"
},
"en-US": {
"auth.account": "Username/Email",
"auth.back_to_login": "Back to Login",
@ -12,6 +219,7 @@
"auth.change_password": "Change Password",
"auth.code_login": "Code",
"auth.code_sent": "Code sent",
"auth.send_code": "Send Code",
"auth.confirm_password": "Confirm Password",
"auth.create_account": "Create Account",
"auth.create_new": "Create New",
@ -61,13 +269,16 @@
"common.actions": "Actions",
"common.bind": "Bind",
"common.cancel": "Cancel",
"common.close": "Close",
"common.create": "Create",
"common.delete": "Delete",
"common.detail": "Detail",
"common.edit": "Edit",
"common.forbidden": "Forbidden",
"common.id": "ID",
"common.not_found": "Not Found",
"common.processing": "Processing...",
"common.remove": "Remove",
"common.reset": "Reset",
"common.save": "Save",
"common.save_success": "Saved successfully",
@ -90,9 +301,6 @@
"oauth.provider.enabled": "Enabled",
"oauth.provider.name": "Name",
"oauth.provider.redirect_uri": "Redirect URI",
"common.close": "Close",
"common.detail": "Detail",
"common.remove": "Remove",
"org.created": "Created successfully",
"org.deleted": "Deleted successfully",
"org.required_fields": "Please fill in required fields",
@ -209,211 +417,5 @@
"user.profile": "User Profile",
"user.search_placeholder": "Search users...",
"user.username": "Username"
},
"zh-CN": {
"auth.account": "用户名/邮箱",
"auth.back_to_login": "返回登录",
"auth.bind_account": "绑定账号",
"auth.bind_exist_desc": "将 {provider} 绑定到现有账号",
"auth.bind_existing": "绑定已有账号",
"auth.bind_login_tip": "请登录您的个人账号以保持连接",
"auth.bind_new_desc": "创建新账号并绑定 {provider}",
"auth.bind_register_tip": "输入您的个人信息,开启您的旅程",
"auth.bind_success": "绑定成功",
"auth.change_password": "修改密码",
"auth.code_login": "验证码",
"auth.code_sent": "验证码已发送",
"auth.confirm_password": "确认密码",
"auth.create_account": "创建账户",
"auth.email": "邮箱",
"auth.email_disabled": "邮箱登录未启用",
"auth.email_or_phone": "邮箱或手机号",
"auth.fill_all_fields": "请填写所有必填字段",
"auth.fill_required": "请填写所有必填项",
"auth.forgot_password": "忘记密码?",
"auth.have_account": "已有账户?",
"auth.hello_friend": "你好,朋友!",
"auth.keep_connected": "请使用您的个人信息登录,保持连接",
"auth.linked_accounts": "账号绑定",
"auth.login": "登录",
"auth.login_bind": "登录并绑定",
"auth.login_failed": "登录失败",
"auth.login_success": "登录成功",
"auth.logout": "登出",
"auth.new_password": "新密码",
"auth.no_account": "还没有账户?",
"auth.old_password": "当前密码",
"auth.password": "密码",
"auth.password_changed": "密码修改成功",
"auth.password_too_short": "密码至少8个字符",
"auth.passwords_not_match": "两次输入的密码不一致",
"auth.phone_placeholder": "请输入手机号",
"auth.register": "注册",
"auth.register_failed": "注册失败",
"auth.register_success": "注册成功",
"auth.security": "安全设置",
"auth.sign_up_bind": "注册并绑定",
"auth.sms_disabled": "手机号登录未启用",
"auth.start_journey": "输入您的个人信息,开始您的旅程",
"auth.target_and_code_required": "请输入邮箱/手机号和验证码",
"auth.target_required": "请输入邮箱或手机号",
"auth.unbind_confirm": "确定要解绑该账号吗?",
"auth.use_account": "或使用您的账户",
"auth.use_info_register": "或使用您的信息进行注册",
"auth.username": "用户名",
"auth.username_login": "用户名",
"auth.username_or_email": "用户名或邮箱",
"auth.username_required": "用户名必填",
"auth.username_too_short": "用户名至少3个字符",
"auth.verification_code": "验证码",
"auth.welcome_back": "欢迎回来!",
"common.actions": "操作",
"common.cancel": "取消",
"common.close": "关闭",
"common.create": "创建",
"common.delete": "删除",
"common.detail": "详情",
"common.edit": "编辑",
"common.remove": "移除",
"common.forbidden": "禁止访问",
"common.not_found": "页面未找到",
"common.processing": "处理中...",
"common.reset": "重置",
"common.save": "保存",
"common.save_success": "保存成功",
"common.saving": "保存中...",
"common.success": "成功",
"common.unbind": "解绑",
"nav.dashboard": "仪表盘",
"nav.home": "首页",
"nav.oauth": "OAuth应用",
"nav.oauth_providers": "身份源管理",
"nav.profile": "个人中心",
"nav.roles": "角色管理",
"nav.settings": "系统设置",
"nav.users": "用户管理",
"oauth.provider.client_id": "客户端ID",
"oauth.provider.client_secret": "客户端密钥",
"oauth.provider.code": "代码",
"oauth.provider.create": "添加身份源",
"oauth.provider.edit": "编辑身份源",
"oauth.provider.enabled": "启用",
"oauth.provider.name": "名称",
"oauth.provider.redirect_uri": "回调地址",
"oauth.create": "创建",
"oauth.create_app": "创建应用",
"org.created": "创建成功",
"org.deleted": "删除成功",
"org.required_fields": "请填写必填字段",
"org.updated": "更新成功",
"permission.level.admin": "管理",
"permission.level.create": "创建",
"permission.level.read": "读取",
"permission.level.rw": "读写",
"permission.level.write": "写入",
"role.add_permission": "添加权限",
"role.add_user": "添加用户",
"role.code": "角色代码",
"role.create": "创建角色",
"role.delete_confirm": "确定要删除该角色吗?",
"role.description": "描述",
"role.detail": "角色详情",
"role.edit": "编辑角色",
"role.name": "角色名称",
"role.no_available_permissions": "没有可用权限",
"role.no_available_users": "没有可用用户",
"role.no_permissions": "未分配权限",
"role.no_users": "未分配用户",
"role.permission_removed": "权限已移除",
"role.permissions": "权限管理",
"role.permissions_desc": "管理此角色的权限",
"role.permissions_updated": "权限已更新",
"role.remove_permission_confirm": "确定移除该权限?",
"role.remove_user_confirm": "确定将该用户从角色中移除?",
"role.search_permissions": "搜索权限...",
"role.search_placeholder": "搜索角色...",
"role.search_users": "搜索用户...",
"role.search_users_placeholder": "搜索用户名或昵称...",
"role.select_permissions": "选择权限",
"role.select_users": "选择用户",
"role.user_removed": "用户已移除",
"role.users": "用户管理",
"role.users_updated": "用户已更新",
"settings.app.id": "应用标识",
"settings.app.id_desc": "应用的唯一标识符",
"settings.app.name": "应用名称",
"settings.app.name_desc": "应用的显示名称",
"settings.auth.login_methods": "登录方式",
"settings.auth.login_methods_desc": "支持的登录方式JSON数组",
"settings.auth.password_fields": "密码登录字段",
"settings.auth.password_fields_desc": "密码登录支持的字段",
"settings.auth.reg_require_email": "注册需要邮箱",
"settings.auth.reg_require_email_desc": "注册时强制要求填写邮箱",
"settings.auth.reg_require_phone": "注册需要手机号",
"settings.auth.reg_require_phone_desc": "注册时强制要求填写手机号",
"settings.category.app": "应用配置",
"settings.category.auth": "认证配置",
"settings.category.code": "验证码配置",
"settings.category.email": "邮件配置",
"settings.category.security": "安全配置",
"settings.category.sms": "短信配置",
"settings.code.expiry": "验证码有效期",
"settings.code.expiry_desc": "验证码有效时间(分钟)",
"settings.code.length": "验证码长度",
"settings.code.length_desc": "验证码的位数",
"settings.code.max_attempt": "最大尝试次数",
"settings.code.max_attempt_desc": "验证码最大尝试次数",
"settings.code.max_daily_count": "单日最大发送次数",
"settings.code.max_daily_count_desc": "单日发送验证码最大次数0禁用-1不限制",
"settings.code.send_interval": "发送间隔",
"settings.code.send_interval_desc": "两次发送之间的最小间隔(秒)",
"settings.email.enabled": "启用邮件",
"settings.email.enabled_desc": "启用邮件服务用于通知",
"settings.email.from": "发件人邮箱",
"settings.email.from_desc": "发件人邮箱地址",
"settings.email.from_name": "发件人名称",
"settings.email.from_name_desc": "发件人显示名称",
"settings.email.provider": "邮件服务商",
"settings.email.provider_desc": "邮件服务提供商",
"settings.email.smtp_host": "SMTP服务器",
"settings.email.smtp_host_desc": "SMTP服务器地址",
"settings.email.smtp_pass": "SMTP密码",
"settings.email.smtp_pass_desc": "SMTP认证密码",
"settings.email.smtp_port": "SMTP端口",
"settings.email.smtp_port_desc": "SMTP服务器端口",
"settings.email.smtp_user": "SMTP用户名",
"settings.email.smtp_user_desc": "SMTP认证用户名",
"settings.load_failed": "加载设置失败",
"settings.no_changes": "没有需要保存的更改",
"settings.reset_done": "设置已重置",
"settings.save_failed": "保存设置失败",
"settings.save_success": "设置保存成功",
"settings.security.bcrypt_cost": "Bcrypt强度",
"settings.security.bcrypt_cost_desc": "密码哈希强度4-31",
"settings.security.captcha_enabled": "启用验证码",
"settings.security.captcha_enabled_desc": "登录时显示验证码验证",
"settings.security.max_login_attempts": "最大登录尝试次数",
"settings.security.max_login_attempts_desc": "失败登录尝试的最大次数",
"settings.sms.access_key": "Access Key",
"settings.sms.access_key_desc": "API访问密钥ID",
"settings.sms.access_secret": "Access Secret",
"settings.sms.access_secret_desc": "API访问密钥密文",
"settings.sms.enabled": "启用短信",
"settings.sms.enabled_desc": "启用短信服务用于验证码",
"settings.sms.provider": "短信服务商",
"settings.sms.provider_aliyun": "阿里云",
"settings.sms.provider_desc": "短信服务提供商",
"settings.sms.provider_tencent": "腾讯云",
"settings.sms.sign_name": "短信签名",
"settings.sms.sign_name_desc": "已注册的短信签名",
"settings.sms.template_code": "模板代码",
"settings.sms.template_code_desc": "验证码模板ID",
"user.avatar_url": "头像链接",
"user.email": "邮箱",
"user.info": "基本信息",
"user.nickname": "昵称",
"user.phone": "手机号",
"user.profile": "个人资料",
"user.username": "用户名"
}
}

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Auth Layout with animated background">
<style>
body {
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
position: relative;
background: linear-gradient(135deg, var(--color-primary-light, #e0e7ff) 0%, var(--color-primary-dark, #4338ca) 100%);
font-family: var(--font-family-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
}
.bg-layer {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
opacity: 0.3;
}
.bubble {
position: absolute;
bottom: -150px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
animation: rise linear infinite;
}
vslot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
z-index: 1;
}
@keyframes rise {
0% {
transform: translateY(0) rotate(0);
opacity: 0.5;
}
100% {
transform: translateY(-120vh) rotate(360deg);
opacity: 0;
}
}
</style>
</head>
<body>
<div class="bg-layer">
<div v-for="b in bubbles" class="bubble" :style="b"></div>
</div>
<vslot></vslot>
</body>
<script setup>
bubbles = [];
const n = 15;
for (let i = 0; i < n; i++) {
const size = Math.random() * 80 + 40;
bubbles.push({
width: size + 'px',
height: size + 'px',
left: (Math.random() * 100) + '%',
animationDelay: (Math.random() * 15) + 's',
animationDuration: (Math.random() * 15 + 10) + 's',
});
}
</script>
</html>

@ -13,41 +13,13 @@
display: flex;
justify-content: center;
align-items: center;
position: relative;
background: linear-gradient(135deg, var(--color-primary-light, #e0e7ff) 0%, var(--color-primary-dark, #4338ca) 100%);
font-family: var(--font-family-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
margin: 0;
}
.background {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
opacity: 0.3;
z-index: 0;
}
.bubble {
position: absolute;
bottom: -150px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
animation: rise linear infinite;
}
@keyframes rise {
0% {
transform: translateY(0) rotate(0);
opacity: 0.5;
}
100% {
transform: translateY(-120vh) rotate(360deg);
opacity: 0;
}
}
h1 {
@ -92,7 +64,6 @@
text-transform: uppercase;
transition: transform 80ms ease-in, background-color 0.2s;
cursor: pointer;
position: relative;
overflow: hidden;
margin-top: 10px;
}
@ -140,9 +111,6 @@
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.button-loading {
@ -183,10 +151,10 @@
}
.container {
position: relative;
background-color: var(--bg-color-secondary, #ffffff);
border-radius: 10px;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
width: 768px;
max-width: 100%;
@ -259,13 +227,13 @@
}
.overlay {
position: relative;
background: var(--color-primary, #4f46e5);
background: linear-gradient(to right, var(--color-secondary, #7c3aed), var(--color-primary, #4f46e5));
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
color: #ffffff;
position: relative;
left: -100%;
height: 100%;
width: 200%;
@ -341,9 +309,6 @@
</head>
<body>
<div class="background" id="background">
<!-- Bubbles will be injected here -->
</div>
<!-- Initial Loading State -->
<div v-if="loading && !error" class="initial-loading">
@ -443,27 +408,6 @@
email: ''
}
// Helper to generate bubbles
createBubbles = () => {
const bg = document.getElementById('background');
if (!bg) return;
const bubbleCount = 10;
for (let i = 0; i < bubbleCount; i++) {
const bubble = document.createElement('div');
bubble.classList.add('bubble');
const size = Math.random() * 60 + 20 + 'px';
bubble.style.width = size;
bubble.style.height = size;
bubble.style.left = Math.random() * 100 + '%';
bubble.style.animationDuration = Math.random() * 10 + 5 + 's';
bubble.style.animationDelay = Math.random() * 5 + 's';
bg.appendChild(bubble);
}
}
handleCallback = async () => {
try {
const urlParams = new URLSearchParams(window.location.search);
@ -494,20 +438,17 @@
const data = await $mod.$auth.oauthCallback(provider, code, state);
if (data.access_token || data.token) {
// Already bound, login success (token set by vbase)
$router.push('/');
} else if (data.need_bind) {
if (data.need_bind) {
// Need binding
needBind = true;
provider = data.provider;
providerId = data.provider_id;
tempToken = data.temp_token;
// Pre-fill username if available from provider (optional, depends on API)
// if (data.provider_username) regForm.username = data.provider_username;
loading = false;
} else if (data.user?.id) {
$router.push('/');
} else {
error = 'Unknown response state';
loading = false;
@ -560,7 +501,6 @@
<script>
$watch(() => {
createBubbles();
handleCallback();
});
</script>

@ -13,41 +13,9 @@
display: flex;
justify-content: center;
align-items: center;
position: relative;
background: linear-gradient(135deg, var(--color-primary-light, #e0e7ff) 0%, var(--color-primary-dark, #4338ca) 100%);
font-family: var(--font-family-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
}
.background {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
opacity: 0.3;
}
.bubble {
position: absolute;
bottom: -150px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
animation: rise linear infinite;
}
@keyframes rise {
0% {
transform: translateY(0) rotate(0);
opacity: 0.5;
}
100% {
transform: translateY(-120vh) rotate(360deg);
opacity: 0;
}
}
h1 {
font-weight: bold;
margin: 0;
@ -89,7 +57,6 @@
text-transform: uppercase;
transition: transform 80ms ease-in, background-color 0.2s;
cursor: pointer;
position: relative;
overflow: hidden;
}
@ -117,6 +84,29 @@
cursor: not-allowed;
}
/* Code input row */
.code-input-row {
display: flex;
gap: 8px;
align-items: center;
}
.code-input-row input {
flex: 1;
margin: 8px 0;
}
button.send-code-btn {
width: auto;
min-width: 90px;
padding: 10px 14px;
margin: 8px 0;
white-space: nowrap;
text-transform: none;
font-size: 13px;
letter-spacing: 0;
}
/* Loading spinner */
.loading-spinner {
position: absolute;
@ -179,10 +169,10 @@
}
.container {
position: relative;
background-color: var(--bg-color-secondary, #ffffff);
border-radius: 10px;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
width: 768px;
max-width: 100%;
@ -236,13 +226,13 @@
}
.overlay {
position: relative;
background: var(--color-primary, #4f46e5);
background: linear-gradient(to right, var(--color-secondary, #7c3aed), var(--color-primary, #4f46e5));
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
color: #ffffff;
position: relative;
left: -100%;
height: 100%;
width: 200%;
@ -353,7 +343,6 @@
.input-group {
width: 100%;
margin: 6px 0;
}
.forgot-link {
@ -379,11 +368,13 @@
}
.container.right-panel-active .sign-in-container {
transform: translateX(100%);
transform: translateX(-100%);
}
.container.right-panel-active .sign-up-container {
transform: translateX(100%);
transform: translateX(0);
opacity: 1;
z-index: 5;
}
.overlay-container {
@ -412,10 +403,6 @@
</head>
<body>
<div class="background">
<div v-for="bubble in bubbles" class="bubble" :style="bubble.style"></div>
</div>
<div class="container" :class="{ 'right-panel-active': isSignUp }">
<!-- Register Form -->
<div class="form-container sign-up-container">
@ -497,8 +484,11 @@
<!-- Code Login -->
<div v-if="loginType === 'code'" style="width: 100%;">
<div class="input-group">
<div class="input-group code-input-row">
<input type="text" :placeholder="codePlaceholder" v:value="signInForm.target" required />
<button type="button" class="send-code-btn" @click="sendCode" :disabled="countDown > 0 || sendCodeLoading">
{{ countDown > 0 ? countDown + 's' : sendCodeLoading ? '...' : $t('auth.send_code') }}
</button>
</div>
<div class="input-group">
<input type="text" :placeholder="$t('auth.verification_code')" v:value="signInForm.code" required />
@ -551,6 +541,7 @@
codePlaceholder = $t('auth.email_or_phone');
countDown = 0;
timer = null;
sendCodeLoading = false;
signInForm = {
username: '',
@ -571,30 +562,8 @@
signUpError = '';
signInLoading = false;
signUpLoading = false;
bubbles = [];
providers = [];
// Create background bubbles
createBubbles = () => {
const numberOfBubbles = 15;
for (let i = 0; i < numberOfBubbles; i++) {
const size = Math.random() * 80 + 40;
const left = Math.random() * 100;
const delay = Math.random() * 15;
const duration = Math.random() * 15 + 10;
bubbles.push({
style: {
width: size + 'px',
height: size + 'px',
left: left + '%',
animationDelay: delay + 's',
animationDuration: duration + 's',
},
});
}
};
// Switch to sign up panel
switchToSignUp = () => {
if (signInLoading || signUpLoading) return;
@ -625,14 +594,15 @@
// Handle OAuth login
handleOAuth = async (provider) => {
try {
const res = await $axios.get('/api/auth/authorize/thirdparty', {
params: {
provider: provider,
redirect: redirect
}
});
if (res && res.auth_url) {
window.location.href = res.auth_url;
const params = new URLSearchParams({provider, redirect});
const res = await fetch('/api/auth/authorize/thirdparty?' + params);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
if (data?.auth_url) {
window.location.href = data.auth_url;
}
} catch (e) {
$message.error(e.message || $t('auth.oauth_failed'));
@ -642,44 +612,44 @@
// Load configuration
loadConfig = async () => {
try {
const res = await $axios.get('/api/info');
if (res) {
if (res.oauth_providers) {
providers = res.oauth_providers;
}
const res = await fetch('/api/info');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const info = await res.json();
// Update feature flags
smsEnabled = res.sms_enabled;
emailEnabled = res.email_enabled;
regRequireEmail = res.reg_require_email;
regRequirePhone = res.reg_require_phone;
// Update placeholder
if (smsEnabled && emailEnabled) {
codePlaceholder = $t('auth.email_or_phone');
} else if (smsEnabled) {
codePlaceholder = $t('auth.phone_placeholder');
} else if (emailEnabled) {
codePlaceholder = $t('auth.email');
}
if (info.oauth_providers) {
providers = info.oauth_providers;
}
smsEnabled = info.sms_enabled;
emailEnabled = info.email_enabled;
regRequireEmail = info.reg_require_email;
regRequirePhone = info.reg_require_phone;
if (smsEnabled && emailEnabled) {
codePlaceholder = $t('auth.email_or_phone');
} else if (smsEnabled) {
codePlaceholder = $t('auth.phone_placeholder');
} else if (emailEnabled) {
codePlaceholder = $t('auth.email');
}
} catch (e) {
console.error("Failed to load config:", e);
console.error('Failed to load config:', e);
}
};
// Send verification code
sendCode = async () => {
if (countDown > 0) return;
if (countDown > 0 || sendCodeLoading) return;
if (!signInForm.target) {
signInError = $t('auth.target_required');
return;
}
// Determine type
const type = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(signInForm.target) ? 'email' : 'sms';
// Check if enabled
if (type === 'email' && !emailEnabled) {
signInError = $t('auth.email_disabled');
return;
@ -689,6 +659,7 @@
return;
}
sendCodeLoading = true;
try {
await $mod.$auth.request('POST', '/api/verification/send', {
type: type,
@ -707,9 +678,10 @@
timer = null;
}
}, 1000);
} catch (e) {
signInError = e.message || 'Failed to send code';
} finally {
sendCodeLoading = false;
}
};
@ -729,19 +701,24 @@
}
const type = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(signInForm.target) ? 'email' : 'phone';
const data = await $mod.$auth.request('POST', '/api/auth/login/code', {
type: type,
target: signInForm.target,
code: signInForm.code
const res = await fetch('/api/auth/login/code', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type, target: signInForm.target, code: signInForm.code})
});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
if (data && data.user) {
$mod.$auth.user = data.user;
if (data?.user) {
await $mod.$auth.onAuthSuccess(data.user);
$message.success($t('auth.login_success'));
window.location.href = redirect;
} else {
throw new Error('Login failed: no token received');
throw new Error('Login failed');
}
} else {
// Username/Password login
@ -750,7 +727,6 @@
}
const success = await $mod.$auth.login(signInForm.username, signInForm.password);
if (success) {
$message.success($t('auth.login_success'));
window.location.href = redirect;
@ -797,16 +773,24 @@
try {
// Use vbase.request for consistent API handling
const data = await $mod.$auth.request('POST', '/api/auth/register', {
username: signUpForm.username,
email: signUpForm.email || undefined,
phone: signUpForm.phone || undefined,
password: signUpForm.password
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: signUpForm.username,
email: signUpForm.email || undefined,
phone: signUpForm.phone || undefined,
password: signUpForm.password
})
});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
if (data && data.user) {
$mod.$auth.user = data.user;
if (data?.user) {
await $mod.$auth.onAuthSuccess(data.user);
$message.success($t('auth.register_success'));
window.location.href = redirect;
} else {
@ -821,7 +805,6 @@
};
// Initialize
createBubbles();
loadConfig();
</script>

@ -173,8 +173,13 @@
loadApps = async () => {
try {
const res = await $axios.get('/api/oauth/clients');
apps = res.items || [];
const res = await fetch('/api/oauth/clients');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
apps = data.items || [];
} catch (e) {
$message.error(e.message);
}
@ -204,17 +209,25 @@
try {
if (isEdit) {
await $axios.patch(`/api/oauth/clients/${formData.id}`, {
const res = await fetch(`/api/oauth/clients/${formData.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
name: formData.name,
redirect_uri: formData.redirect_uri
});
$message.success("App updated");
}) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success('App updated');
} else {
await $axios.post('/api/oauth/clients', {
const res = await fetch('/api/oauth/clients', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
name: formData.name,
redirect_uri: formData.redirect_uri
});
$message.success("App created");
}) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success('App created');
}
closeModal();
loadApps();
@ -226,7 +239,11 @@
deleteApp = async (app) => {
try {
await $message.confirm(`Delete app "${app.name}"?`);
await $axios.delete(`/api/oauth/clients/${app.id}`);
const res = await fetch(`/api/oauth/clients/${app.id}`, { method: 'DELETE' });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success("Deleted");
loadApps();
} catch (e) {

@ -227,7 +227,12 @@
openEditModal = async (p) => {
isEdit = true;
try {
const detail = await $axios.get(`/api/oauth/providers/${p.code}`);
const res = await fetch(`/api/oauth/providers/${p.code}`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const detail = await res.json();
formData = {...detail, client_secret: ""};
showModal = true;
} catch (e) {
@ -240,9 +245,17 @@
saveProvider = async () => {
try {
if (isEdit) {
await $axios.patch(`/api/oauth/providers/${formData.code}`, formData);
const res = await fetch(`/api/oauth/providers/${formData.code}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
} else {
await $axios.post('/api/oauth/providers', formData);
const res = await fetch('/api/oauth/providers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
}
$message.success("Saved");
closeModal();
@ -255,7 +268,11 @@
deleteProvider = async (p) => {
if (!confirm(`Delete ${p.name}?`)) return;
try {
await $axios.delete(`/api/oauth/providers/${p.code}`);
const res = await fetch(`/api/oauth/providers/${p.code}`, { method: 'DELETE' });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success("Deleted");
loadProviders();
} catch (e) {

@ -118,7 +118,8 @@
}
/* Tab Styles */
.tab, .tab-active {
.tab,
.tab-active {
padding: 12px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
@ -143,7 +144,10 @@
}
/* Permission List */
.permission-list, .user-list, .available-permission-list, .available-user-list {
.permission-list,
.user-list,
.available-permission-list,
.available-user-list {
max-height: 400px;
overflow-y: auto;
display: flex;
@ -151,7 +155,10 @@
gap: 8px;
}
.permission-item, .user-item, .available-permission-item, .available-user-item {
.permission-item,
.user-item,
.available-permission-item,
.available-user-item {
display: flex;
align-items: center;
gap: 12px;
@ -161,21 +168,25 @@
transition: background var(--transition-base);
}
.permission-item:hover, .user-item:hover {
.permission-item:hover,
.user-item:hover {
background: var(--border-color);
}
.permission-info, .user-info {
.permission-info,
.user-info {
flex: 1;
min-width: 0;
}
.permission-id, .user-name {
.permission-id,
.user-name {
font-weight: 500;
color: var(--text-color);
}
.permission-scope, .user-username {
.permission-scope,
.user-username {
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
}
@ -192,14 +203,34 @@
font-weight: 500;
}
.level-1 { background: color-mix(in srgb, var(--color-info), transparent 85%); color: var(--color-info); }
.level-2 { background: color-mix(in srgb, var(--color-success), transparent 85%); color: var(--color-success); }
.level-4 { background: color-mix(in srgb, var(--color-warning), transparent 85%); color: var(--color-warning); }
.level-6 { background: color-mix(in srgb, var(--color-warning), transparent 70%); color: var(--color-warning); }
.level-7 { background: color-mix(in srgb, var(--color-danger), transparent 85%); color: var(--color-danger); }
.level-1 {
background: color-mix(in srgb, var(--color-info), transparent 85%);
color: var(--color-info);
}
.level-2 {
background: color-mix(in srgb, var(--color-success), transparent 85%);
color: var(--color-success);
}
.level-4 {
background: color-mix(in srgb, var(--color-warning), transparent 85%);
color: var(--color-warning);
}
.level-6 {
background: color-mix(in srgb, var(--color-warning), transparent 70%);
color: var(--color-warning);
}
.level-7 {
background: color-mix(in srgb, var(--color-danger), transparent 85%);
color: var(--color-danger);
}
/* User Avatar */
.user-avatar, .user-avatar-sm {
.user-avatar,
.user-avatar-sm {
border-radius: var(--radius-full);
object-fit: cover;
}
@ -277,7 +308,7 @@
<td>{{ r.name }}</td>
<td>{{ r.description }}</td>
<td>
<span class="status-badge" :class="r.is_system ? 'status-active' : 'status-inactive'">
<span class="status-badge" :class="r.is_system ? 'status-active' : 'status-inactive'">
{{ r.is_system ? 'Yes' : 'No' }}
</span>
</td>
@ -291,10 +322,12 @@
<v-btn icon size="sm" variant="outline" :click="() => openDetailModal(r)" :title="$t('common.detail')">
<i class="fas fa-eye"></i>
</v-btn>
<v-btn icon size="sm" variant="outline" :click="() => openEditModal(r)" :title="$t('common.edit')" :disabled="r.is_system">
<v-btn icon size="sm" variant="outline" :click="() => openEditModal(r)" :title="$t('common.edit')"
:disabled="r.is_system">
<i class="fas fa-edit"></i>
</v-btn>
<v-btn icon size="sm" color="danger" variant="outline" :click="() => deleteRole(r)" :title="$t('common.delete')" :disabled="r.is_system">
<v-btn icon size="sm" color="danger" variant="outline" :click="() => deleteRole(r)"
:title="$t('common.delete')" :disabled="r.is_system">
<i class="fas fa-trash"></i>
</v-btn>
</div>
@ -305,10 +338,10 @@
</div>
<!-- Create/Edit Dialog -->
<v-dialog v:visible="showModal"
:title="isEdit ? $t('role.edit') : $t('role.create')">
<v-dialog v:visible="showModal" :title="isEdit ? $t('role.edit') : $t('role.create')">
<form @submit.prevent="saveRole" style="display: grid; gap: 16px;">
<v-input :label="$t('role.code')" required v:value="formData.code" :disabled="isEdit" placeholder="e.g. admin"></v-input>
<v-input :label="$t('role.code')" required v:value="formData.code" :disabled="isEdit"
placeholder="e.g. admin"></v-input>
<v-input :label="$t('role.name')" required v:value="formData.name" placeholder="e.g. Administrator"></v-input>
<v-input :label="$t('role.description')" v:value="formData.description" placeholder="Description..."></v-input>
</form>
@ -321,8 +354,7 @@
</v-dialog>
<!-- Role Detail Dialog - Permissions & Users -->
<v-dialog v:visible="showDetailModal" width="800px"
:title="$t('role.detail') + ' - ' + (currentRole?.name || '')">
<v-dialog v:visible="showDetailModal" width="800px" :title="$t('role.detail') + ' - ' + (currentRole?.name || '')">
<div style="display: flex; flex-direction: column; gap: 20px;">
<!-- Tabs -->
<div style="display: flex; gap: 8px; border-bottom: 1px solid var(--border-color);">
@ -401,8 +433,7 @@
</v-dialog>
<!-- Permission Selector Dialog -->
<v-dialog v:visible="showPermissionSelector" width="600px"
:title="$t('role.select_permissions')">
<v-dialog v:visible="showPermissionSelector" width="600px" :title="$t('role.select_permissions')">
<div style="display: flex; flex-direction: column; gap: 16px;">
<div class="search-box">
<i class="fas fa-search" style="color: var(--text-color-tertiary);"></i>
@ -428,8 +459,7 @@
</v-dialog>
<!-- User Selector Dialog -->
<v-dialog v:visible="showUserSelector" width="600px"
:title="$t('role.select_users')">
<v-dialog v:visible="showUserSelector" width="600px" :title="$t('role.select_users')">
<div style="display: flex; flex-direction: column; gap: 16px;">
<div class="search-box">
<i class="fas fa-search" style="color: var(--text-color-tertiary);"></i>
@ -491,8 +521,13 @@
loadRoles = async () => {
try {
const res = await $axios.get('/api/roles');
roles = res.items || [];
const res = await fetch('/api/roles');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
roles = data.items || [];
} catch (e) {
$message.error(e.message);
}
@ -510,13 +545,13 @@
openCreateModal = () => {
isEdit = false;
formData = { id: null, code: "", name: "", description: "" };
formData = {id: null, code: "", name: "", description: ""};
showModal = true;
};
openEditModal = (r) => {
isEdit = true;
formData = { ...r };
formData = {...r};
showModal = true;
};
@ -535,10 +570,18 @@
name: formData.name,
description: formData.description
};
await $axios.patch(`/api/roles/${formData.id}`, payload);
const res = await fetch(`/api/roles/${formData.id}`, {method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('org.updated'));
} else {
await $axios.post('/api/roles', formData);
const res = await fetch('/api/roles', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(formData)});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('org.created'));
}
closeModal();
@ -551,7 +594,11 @@
deleteRole = async (r) => {
try {
await $message.confirm($t('role.delete_confirm'));
await $axios.delete(`/api/roles/${r.id}`);
const res = await fetch(`/api/roles/${r.id}`, {method: 'DELETE'});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('org.deleted'));
loadRoles();
} catch (e) {
@ -583,7 +630,11 @@
loadRolePermissions = async () => {
if (!currentRole) return;
try {
const res = await $axios.get(`/api/roles/${currentRole.id}/permissions`);
const res = await fetch(`/api/roles/${currentRole.id}/permissions`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
rolePermissions = res || [];
} catch (e) {
$message.error(e.message);
@ -608,12 +659,17 @@
// you might need a /api/permissions endpoint to list all available permissions
try {
// This is a placeholder - adjust based on your actual API
const res = await $axios.get('/api/auth/me');
if (res.permissions) {
const res = await fetch('/api/auth/me');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const me = await res.json();
if (me.permissions) {
// Extract unique permission IDs
const uniquePerms = [];
const seen = new Set();
res.permissions.forEach(p => {
me.permissions.forEach(p => {
if (!seen.has(p.permission_id)) {
seen.add(p.permission_id);
uniquePerms.push({
@ -664,9 +720,15 @@
saveRolePermissions = async () => {
if (!currentRole) return;
try {
await $axios.put(`/api/roles/${currentRole.id}/permissions`, {
permission_ids: selectedPermissionIds
const res = await fetch(`/api/roles/${currentRole.id}/permissions`, {
method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({
permission_ids: selectedPermissionIds
})
});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('role.permissions_updated'));
closePermissionSelector();
loadRolePermissions();
@ -682,9 +744,15 @@
const newPermissions = rolePermissions
.filter(rp => rp.id !== p.id)
.map(rp => rp.id);
await $axios.put(`/api/roles/${currentRole.id}/permissions`, {
permission_ids: newPermissions
const res = await fetch(`/api/roles/${currentRole.id}/permissions`, {
method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({
permission_ids: newPermissions
})
});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('role.permission_removed'));
loadRolePermissions();
} catch (e) {
@ -709,13 +777,24 @@
try {
// Get users who have this role
// We need to fetch users and filter by role
const res = await $axios.get('/api/users', { params: { page_size: 1000 } });
const allUsers = res.items || [];
const params = new URLSearchParams({page_size: 1000});
let res = await fetch('/api/users?' + params);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
const allUsers = data.items || [];
// For each user, check if they have this role
const usersWithRole = [];
for (const user of allUsers) {
try {
const userRoles = await $axios.get(`/api/users/${user.id}/roles`);
res = await fetch(`/api/users/${user.id}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
if (userRoles.some(ur => ur.id === currentRole.id)) {
usersWithRole.push(user);
}
@ -753,13 +832,11 @@
searchAvailableUsers = async () => {
try {
const res = await $axios.get('/api/auth/users', {
params: {
keyword: availableUserSearchQuery,
limit: 50
}
});
availableUsers = res.items || [];
const params = new URLSearchParams({keyword: availableUserSearchQuery, limit: 50});
const res = await fetch('/api/auth/users?' + params);
const json_ = await res.json();
if (json_.code !== 200) throw new Error(json_.message);
availableUsers = json_.data?.items || [];
} catch (e) {
$message.error(e.message);
}
@ -791,16 +868,35 @@
// Users to remove
const toRemove = currentUserIds.filter(id => !selectedUserIds.includes(id));
let res = null
for (const userId of toAdd) {
const userRoles = await $axios.get(`/api/users/${userId}/roles`);
res = await fetch(`/api/users/${userId}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = [...userRoles.map(r => r.id), currentRole.id];
await $axios.put(`/api/users/${userId}/roles`, { role_ids: roleIds });
res = await fetch(`/api/users/${userId}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
}
for (const userId of toRemove) {
const userRoles = await $axios.get(`/api/users/${userId}/roles`);
res = await fetch(`/api/users/${userId}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = userRoles.filter(r => r.id !== currentRole.id).map(r => r.id);
await $axios.put(`/api/users/${userId}/roles`, { role_ids: roleIds });
res = await fetch(`/api/users/${userId}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
}
$message.success($t('role.users_updated'));
@ -815,9 +911,18 @@
if (!currentRole || currentRole.is_system) return;
try {
await $message.confirm($t('role.remove_user_confirm'));
const userRoles = await $axios.get(`/api/users/${u.id}/roles`);
let res = await fetch(`/api/users/${u.id}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = userRoles.filter(r => r.id !== currentRole.id).map(r => r.id);
await $axios.put(`/api/users/${u.id}/roles`, { role_ids: roleIds });
res = await fetch(`/api/users/${u.id}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('role.user_removed'));
loadRoleUsers();
} catch (e) {

@ -672,10 +672,15 @@
loadSettings = async () => {
loading = true;
try {
const response = await $axios.get('/api/settings');
if (response && response.items) {
const res = await fetch('/api/settings');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
if (data?.items) {
const settingsMap = {};
response.items.forEach(item => {
data.items.forEach(item => {
settingsMap[item.key] = item.value;
});
settings = settingsMap;
@ -719,7 +724,11 @@
return;
}
await $axios.put('/api/settings', { settings: settingsToUpdate });
const res = await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings: settingsToUpdate }) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
originalSettings = { ...settings };
$message.success($t('settings.save_success'));
} catch (error) {

@ -204,8 +204,13 @@
loadUsers = async () => {
try {
const res = await $axios.get('/api/users');
users = res.items || []; // Adjust based on actual API response structure (array or {items: []})
const res = await fetch('/api/users');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
users = data.items || []; // Adjust based on actual API response structure (array or {items: []})
} catch (e) {
$message.error(e.message);
}
@ -245,14 +250,22 @@
if (isEdit) {
const payload = { email: formData.email };
if (formData.password) payload.password = formData.password;
await $axios.patch(`/api/users/${formData.id}`, payload);
const res = await fetch(`/api/users/${formData.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success("User updated");
} else {
if (!formData.password) {
$message.error("Password is required for new users");
return;
}
await $axios.post('/api/users', formData);
const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success("User created");
}
closeModal();
@ -265,7 +278,11 @@
deleteUser = async (u) => {
try {
await $message.confirm(`Delete user "${u.username}"?`);
await $axios.delete(`/api/users/${u.id}`);
const res = await fetch(`/api/users/${u.id}`, { method: 'DELETE' });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success("Deleted");
loadUsers();
} catch (e) {

@ -260,14 +260,24 @@
loadBindings = async () => {
try {
// Get enabled providers
const providersRes = await $axios.get('/api/auth/login-methods');
providers = providersRes.methods.filter(p => p.type !== 'password' && p.type !== 'code' && p.type !== 'email_code' && p.type !== 'phone_code').map(p => ({
const res = await fetch('/api/auth/login-methods');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const providersData = await res.json();
providers = providersData.methods.filter(p => p.type !== 'password' && p.type !== 'code' && p.type !== 'email_code' && p.type !== 'phone_code').map(p => ({
code: p.type,
name: p.name
}));
// Get current bindings
bindings = await $axios.get('/api/auth/me/bindings');
const resBind = await fetch('/api/auth/me/bindings');
if (resBind.status !== 200) {
const err = await resBind.json();
throw new Error(err.message);
}
bindings = await resBind.json();
} catch (e) {
console.error(e);
}
@ -276,12 +286,16 @@
updateProfile = async () => {
loading = true;
try {
await $axios.patch('/api/auth/me', {
const res = await fetch('/api/auth/me', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
nickname: user.nickname,
email: user.email,
phone: user.phone,
avatar: user.avatar
});
}) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('common.save_success'));
await loadUser();
} catch (e) {
@ -295,10 +309,14 @@
if (!passwordForm.new_password || !passwordForm.old_password) return;
pwdLoading = true;
try {
await $axios.post('/api/auth/me/change-password', {
const res = await fetch('/api/auth/me/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
old_password: passwordForm.old_password,
new_password: passwordForm.new_password
});
}) });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('auth.password_changed'));
passwordForm.old_password = '';
passwordForm.new_password = '';
@ -325,17 +343,15 @@
bind = async (provider) => {
try {
// Fetch authorization URL
const res = await $axios.get('/api/auth/authorize/thirdparty', {
params: {
provider: provider,
redirect: window.location.origin + '/callback/' + provider,
bind_mode: true
}
});
// Redirect to provider auth page
if (res.auth_url) {
window.location.href = res.auth_url;
const params = new URLSearchParams({ provider, redirect: redirect || '', bind_mode: true });
const res = await fetch('/api/auth/authorize/thirdparty?' + params);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
if (data.auth_url) {
window.location.href = data.auth_url;
}
} catch (e) {
$message.error(e.message);
@ -345,7 +361,11 @@
unbind = async (provider) => {
try {
await $message.confirm($t('auth.unbind_confirm'));
await $axios.delete(`/api/auth/me/bindings/${provider}`);
const res = await fetch(`/api/auth/me/bindings/${provider}`, { method: 'DELETE' });
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('common.success'));
loadBindings();
} catch (e) {

@ -1,7 +1,7 @@
const routes = [
// Public
{ path: '/login', component: '/page/auth/login.html' },
{ path: '/callback/:provider', component: '/page/auth/callback.html' },
{ path: '/login', component: '/page/auth/login.html', layout: 'auth' },
{ path: '/callback/:provider', component: '/page/auth/callback.html', layout: 'auth' },
// Dashboard (Default Layout)
{

@ -0,0 +1,8 @@
{
"zh-CN": {
"auth.send_code": "发送验证码"
},
"en-US": {
"auth.send_code": "Send Code"
}
}
Loading…
Cancel
Save