From d599021dd5e4f1def324c39294d7fce7b7bb1b52 Mon Sep 17 00:00:00 2001 From: jingrow Date: Sun, 23 Nov 2025 00:33:06 +0800 Subject: [PATCH] Add user registration with jingrow cloud integration --- apps/jingrow/frontend/src/app/router/index.ts | 8 +- apps/jingrow/frontend/src/locales/zh-CN.json | 16 + apps/jingrow/frontend/src/shared/api/auth.ts | 78 ++++ .../jingrow/frontend/src/views/auth/Login.vue | 74 +++- .../frontend/src/views/auth/Signup.vue | 333 ++++++++++++++++++ apps/jingrow/jingrow/api/auth_api.py | 95 ++++- 6 files changed, 593 insertions(+), 11 deletions(-) create mode 100644 apps/jingrow/frontend/src/views/auth/Signup.vue diff --git a/apps/jingrow/frontend/src/app/router/index.ts b/apps/jingrow/frontend/src/app/router/index.ts index e5edb8f..c686531 100644 --- a/apps/jingrow/frontend/src/app/router/index.ts +++ b/apps/jingrow/frontend/src/app/router/index.ts @@ -10,6 +10,12 @@ const router = createRouter({ component: () => import('../../views/auth/Login.vue'), meta: { requiresAuth: false } }, + { + path: '/signup', + name: 'Signup', + component: () => import('../../views/auth/Signup.vue'), + meta: { requiresAuth: false } + }, { path: '/', name: 'AppLayout', @@ -250,7 +256,7 @@ router.beforeEach(async (to, _from, next) => { if (to.meta.requiresAuth && !authStore.isLoggedIn) { next('/login') - } else if (to.path === '/login' && authStore.isLoggedIn) { + } else if ((to.path === '/login' || to.path === '/signup') && authStore.isLoggedIn) { next('/') } else { next() diff --git a/apps/jingrow/frontend/src/locales/zh-CN.json b/apps/jingrow/frontend/src/locales/zh-CN.json index 313b065..0224793 100644 --- a/apps/jingrow/frontend/src/locales/zh-CN.json +++ b/apps/jingrow/frontend/src/locales/zh-CN.json @@ -28,13 +28,29 @@ "Username": "用户名", "Password": "密码", "Login": "登录", + "Sign up": "注册", "Connect to Jingrow SaaS platform": "连接到 Jingrow SaaS 平台", "Please enter username": "请输入用户名", "Please enter password": "请输入密码", "Password must be at least 6 characters": "密码长度不能少于6位", + "Username must be at least 3 characters": "用户名至少需要3个字符", "Login successful": "登录成功", "Login failed": "登录失败", "Login failed, please check username and password": "登录失败,请检查用户名和密码", + "Create your account": "创建您的账户", + "Confirm Password": "确认密码", + "Please confirm password": "请确认密码", + "Passwords do not match": "两次输入的密码不一致", + "Email (Optional)": "邮箱(可选)", + "Phone Number": "手机号", + "Phone Number (Optional)": "手机号(可选)", + "Please enter a valid email address": "请输入有效的邮箱地址", + "Please enter phone number": "请输入手机号", + "Please enter a valid phone number": "请输入有效的手机号码", + "Sign up successful": "注册成功", + "Sign up failed": "注册失败", + "Sign up failed, please try again": "注册失败,请重试", + "Already have an account?": "已有账户?", "System and personal settings": "系统配置和个人设置", "Personal Settings": "个人设置", "Email": "邮箱", diff --git a/apps/jingrow/frontend/src/shared/api/auth.ts b/apps/jingrow/frontend/src/shared/api/auth.ts index d17405f..813db66 100644 --- a/apps/jingrow/frontend/src/shared/api/auth.ts +++ b/apps/jingrow/frontend/src/shared/api/auth.ts @@ -110,6 +110,84 @@ export const logoutApi = async (): Promise => { } } +// 注册接口 +export interface SignupRequest { + username: string + password: string + email?: string + phone_number?: string +} + +export interface SignupResponse { + success: boolean + message?: string + error?: string + user?: UserInfo +} + +export const signupApi = async (data: SignupRequest): Promise => { + try { + const response = await fetch(`/jingrow/signup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(data) + }) + + // 先克隆响应,以便可以多次读取(如果需要) + const responseClone = response.clone() + + let result: any = {} + + // 尝试解析响应体 + try { + result = await response.json() + } catch (e) { + // 如果JSON解析失败,尝试读取文本 + try { + const text = await responseClone.text() + return { + success: false, + error: text || `注册请求失败 (HTTP ${response.status})` + } + } catch (textError) { + return { + success: false, + error: `注册请求失败 (HTTP ${response.status})` + } + } + } + + // 如果响应不成功,提取错误消息 + if (!response.ok) { + // FastAPI HTTPException 返回格式: {"detail": "错误消息"} + const errorMsg = result.detail || `注册请求失败 (HTTP ${response.status})` + return { success: false, error: errorMsg } + } + + // 检查业务逻辑错误(success 为 false) + if (result.success === false) { + const errorMsg = result.error || result.message || '注册失败' + return { success: false, error: errorMsg } + } + + return { + success: true, + message: result.message || '注册成功', + user: result.user || undefined + } + } catch (error: any) { + // 处理网络错误或其他异常 + return { + success: false, + error: error.message || '网络错误,请检查网络连接后重试' + } + } +} + // 仅使用会话Cookie的最小鉴权头部(不影响现有API Key逻辑) export function get_session_api_headers() { return { diff --git a/apps/jingrow/frontend/src/views/auth/Login.vue b/apps/jingrow/frontend/src/views/auth/Login.vue index 210ea3e..edf8db9 100644 --- a/apps/jingrow/frontend/src/views/auth/Login.vue +++ b/apps/jingrow/frontend/src/views/auth/Login.vue @@ -12,7 +12,8 @@ ref="formRef" :model="formData" :rules="rules" - size="large" + size="medium" + :show-label="false" @keyup.enter="handleLogin" > @@ -48,14 +49,18 @@ block :loading="loading" @click="handleLogin" + class="brand-button" > {{ t('Login') }} - @@ -76,6 +81,7 @@ const authStore = useAuthStore() const formRef = ref() const loading = ref(false) +const showSignupLink = ref(false) const formData = reactive({ username: '', @@ -123,6 +129,20 @@ onMounted(async () => { // 如果已经登录,直接跳转 if (authStore.isLoggedIn) { router.push('/') + return + } + + // 检查服务器配置,判断是否显示注册链接 + try { + const response = await fetch('/jingrow/server-config') + if (response.ok) { + const data = await response.json() + if (data.success && data.jingrow_server_url === 'https://cloud.jingrow.com') { + showSignupLink.value = true + } + } + } catch (error) { + console.error('Failed to get server config:', error) } }) @@ -139,7 +159,7 @@ onMounted(async () => { .login-card { width: 100%; - max-width: 400px; + max-width: 480px; background: white; border-radius: 16px; padding: 40px; @@ -148,7 +168,26 @@ onMounted(async () => { .login-header { text-align: center; - margin-bottom: 32px; + margin-bottom: 24px; +} + +:deep(.n-form-item) { + margin-bottom: 6px !important; +} + +:deep(.n-form-item:last-child) { + margin-bottom: 0 !important; +} + +:deep(.n-form-item:not(.n-form-item--error) .n-form-item__feedback-wrapper) { + min-height: 0 !important; + margin-top: 0 !important; + padding-top: 0 !important; +} + +:deep(.n-form-item--error .n-form-item__feedback-wrapper) { + margin-top: 4px !important; + min-height: auto !important; } .logo { @@ -172,4 +211,31 @@ onMounted(async () => { text-align: center; margin-top: 24px; } + +.signup-link { + color: #18a058; + text-decoration: none; + font-weight: 500; + transition: color 0.2s; +} + +.signup-link:hover { + color: #36ad6a; + text-decoration: underline; +} + +:deep(.brand-button) { + background-color: #18a058 !important; + border-color: #18a058 !important; +} + +:deep(.brand-button:hover) { + background-color: #36ad6a !important; + border-color: #36ad6a !important; +} + +:deep(.brand-button:focus) { + background-color: #18a058 !important; + border-color: #18a058 !important; +} diff --git a/apps/jingrow/frontend/src/views/auth/Signup.vue b/apps/jingrow/frontend/src/views/auth/Signup.vue new file mode 100644 index 0000000..514c057 --- /dev/null +++ b/apps/jingrow/frontend/src/views/auth/Signup.vue @@ -0,0 +1,333 @@ + + + + + + diff --git a/apps/jingrow/jingrow/api/auth_api.py b/apps/jingrow/jingrow/api/auth_api.py index a077559..2bc9f77 100644 --- a/apps/jingrow/jingrow/api/auth_api.py +++ b/apps/jingrow/jingrow/api/auth_api.py @@ -4,7 +4,8 @@ from pydantic import BaseModel from typing import Optional import logging -from jingrow.utils.auth import login, logout, get_user_info, set_context +from jingrow.utils.auth import login, logout, get_user_info, set_context, get_jingrow_cloud_url, get_jingrow_cloud_api_headers +from jingrow.config import Config logger = logging.getLogger(__name__) router = APIRouter() @@ -13,6 +14,18 @@ class LoginRequest(BaseModel): username: str password: str +class SignupRequest(BaseModel): + username: str + password: str + email: Optional[str] = None + phone_number: Optional[str] = None + +class SignupResponse(BaseModel): + success: bool + message: str + user: Optional[dict] = None + session_cookie: Optional[str] = None + class LoginResponse(BaseModel): success: bool message: str @@ -28,16 +41,14 @@ class UserInfoResponse(BaseModel): user_info: Optional[dict] = None message: Optional[str] = None -# Cookie配置常量 COOKIE_CONFIG = { "httponly": True, "samesite": "lax", - "secure": False, # 开发环境可以设为False,生产环境建议设为True + "secure": False, "path": "/", - "max_age": 7 * 24 * 60 * 60 # 7天过期时间(秒) + "max_age": 7 * 24 * 60 * 60 } -# 需要清除的cookie列表 COOKIES_TO_CLEAR = ["sid", "user_id", "user_image", "full_name", "system_user"] def get_session_cookie(request: Request) -> Optional[str]: @@ -69,7 +80,7 @@ def create_response_clear_cookies(data: dict) -> JSONResponse: for cookie_name in COOKIES_TO_CLEAR: cookie_kwargs = COOKIE_CONFIG.copy() if cookie_name == "sid": - cookie_kwargs.pop("secure", None) # delete_cookie不需要secure参数 + cookie_kwargs.pop("secure", None) response.delete_cookie(key=cookie_name, **cookie_kwargs) return response @@ -111,6 +122,78 @@ async def logout_route(session_cookie: Optional[str] = Depends(get_session_cooki logger.error(f"登出异常: {str(e)}", exc_info=True) raise HTTPException(status_code=500, detail=f"登出异常: {str(e)}") +@router.post("/jingrow/signup", response_model=SignupResponse) +async def signup_route(signup_data: SignupRequest): + """注册路由""" + import requests + + try: + url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.account.signup_with_username" + data = {"username": signup_data.username, "password": signup_data.password} + if signup_data.email: + data["email"] = signup_data.email + if signup_data.phone_number: + data["phone_number"] = signup_data.phone_number + + response = requests.post(url, headers=get_jingrow_cloud_api_headers(), json=data, timeout=30) + + if response.status_code != 200: + raise HTTPException(status_code=400, detail=f"注册请求失败 (HTTP {response.status_code})") + + try: + result = response.json() + except Exception as e: + raise HTTPException(status_code=400, detail="注册失败:无法解析服务器响应") + + message_obj = result.get("message") + if not isinstance(message_obj, dict): + raise HTTPException(status_code=400, detail="注册失败:服务器响应格式错误") + + success_value = message_obj.get("success") + success_message = message_obj.get("message", "注册成功") + + if success_value is False: + raise HTTPException(status_code=400, detail=success_message) + + session_cookie = None + user_info = None + + try: + login_result = login(signup_data.username, signup_data.password) + if login_result.get("success"): + session_cookie = login_result.get("session_cookie") + if session_cookie: + user_info_result = get_user_info(session_cookie) + if user_info_result.get("success"): + user_info = user_info_result.get("user_info") + else: + pass + except Exception as e: + pass + + response_data = { + "success": True, + "message": success_message, + "user": user_info + } + + if session_cookie: + return create_response_with_cookie(response_data, session_cookie) + else: + return JSONResponse(content=response_data) + except HTTPException: + raise + except Exception as e: + logger.error(f"注册异常: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"注册异常: {str(e)}") + +@router.get("/jingrow/server-config") +async def get_server_config(): + return { + "success": True, + "jingrow_server_url": Config.jingrow_server_url + } + @router.get("/jingrow/user-info", response_model=UserInfoResponse) async def get_user_info_route(session_cookie: Optional[str] = Depends(get_session_cookie)): """获取用户信息路由"""