diff --git a/server/alembic/versions/2026_01_19_1200-add_password_reset_token_table.py b/server/alembic/versions/2026_01_19_1200-add_password_reset_token_table.py new file mode 100644 index 000000000..f0add6f08 --- /dev/null +++ b/server/alembic/versions/2026_01_19_1200-add_password_reset_token_table.py @@ -0,0 +1,45 @@ +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +"""add_password_reset_token_table + +Revision ID: add_password_reset_token +Revises: add_timestamp_to_chat_step +Create Date: 2026-01-19 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "add_password_reset_token" +down_revision: Union[str, None] = "add_timestamp_to_chat_step" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """No-op. + + The password_reset_token flow is no longer used. + """ + pass + + +def downgrade() -> None: + """No-op.""" + pass diff --git a/server/app/controller/user/password_reset_controller.py b/server/app/controller/user/password_reset_controller.py new file mode 100644 index 000000000..0a8eda067 --- /dev/null +++ b/server/app/controller/user/password_reset_controller.py @@ -0,0 +1,65 @@ +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import logging + +from fastapi import APIRouter, Depends +from fastapi_babel import _ +from sqlmodel import Session, col + +from app.component import code +from app.component.database import session +from app.component.encrypt import password_hash +from app.exception.exception import UserException +from app.model.user.password_reset import ( + DirectResetPasswordRequest, +) +from app.model.user.user import User + +logger = logging.getLogger("server_password_reset_controller") + +router = APIRouter(tags=["Password Reset"]) + + +@router.post("/reset-password-direct", name="reset password directly") +async def reset_password_direct( + data: DirectResetPasswordRequest, + session: Session = Depends(session), +): + """ + Reset password directly without token verification. + This endpoint is for Full Local Deployment only where email verification is not needed. + The password is updated directly in the local Docker database. + Password validation is handled by Pydantic model. + """ + # Find the user by email + user = User.by(User.email == data.email, col(User.deleted_at).is_(None), s=session).one_or_none() + + if not user: + logger.warning("Direct password reset failed: user not found") + raise UserException(code.error, _("User with this email not found")) + + # Update password + user.password = password_hash(data.new_password) + user.save(session) + + logger.info( + "Direct password reset successful", + extra={"user_id": user.id} + ) + + return { + "status": "success", + "message": "Password has been reset successfully. You can now log in with your new password.", + } diff --git a/server/app/model/user/password_reset.py b/server/app/model/user/password_reset.py new file mode 100644 index 000000000..bc5790db1 --- /dev/null +++ b/server/app/model/user/password_reset.py @@ -0,0 +1,46 @@ +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +from pydantic import BaseModel, EmailStr, field_validator, model_validator + + +class DirectResetPasswordRequest(BaseModel): + """Request model for direct password reset (local deployment only).""" + email: EmailStr + new_password: str + confirm_password: str + + @field_validator("new_password") + @classmethod + def validate_password_strength(cls, v: str) -> str: + """Validate password meets strength requirements.""" + if len(v) < 8: + raise ValueError("Password must be at least 8 characters long") + + has_letter = any(c.isalpha() for c in v) + has_number = any(c.isdigit() for c in v) + + if not has_letter: + raise ValueError("Password must contain at least one letter") + if not has_number: + raise ValueError("Password must contain at least one number") + + return v + + @model_validator(mode="after") + def validate_passwords_match(self): + """Validate that new_password and confirm_password match.""" + if self.new_password != self.confirm_password: + raise ValueError("Passwords do not match") + return self diff --git a/server/lang/zh_CN/LC_MESSAGES/messages.po b/server/lang/zh_CN/LC_MESSAGES/messages.po index 4b78de2bf..75be7ccff 100644 --- a/server/lang/zh_CN/LC_MESSAGES/messages.po +++ b/server/lang/zh_CN/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-08-06 09:56+0800\n" +"POT-Creation-Date: 2026-01-28 01:03+0800\n" "PO-Revision-Date: 2025-08-06 09:56+0800\n" "Last-Translator: FULL NAME \n" "Language: zh_Hans_CN\n" @@ -18,198 +18,222 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" -#: app/component/auth.py:41 +#: app/component/auth.py:55 msgid "Validate credentials expired" msgstr "" -#: app/component/auth.py:43 +#: app/component/auth.py:57 msgid "Could not validate credentials" msgstr "" -#: app/component/permission.py:12 +#: app/component/permission.py:26 msgid "User" msgstr "" -#: app/component/permission.py:13 +#: app/component/permission.py:27 msgid "User manager" msgstr "" -#: app/component/permission.py:17 +#: app/component/permission.py:31 msgid "User Manage" msgstr "" -#: app/component/permission.py:18 +#: app/component/permission.py:32 msgid "View users" msgstr "" -#: app/component/permission.py:22 +#: app/component/permission.py:36 msgid "User Edit" msgstr "" -#: app/component/permission.py:23 +#: app/component/permission.py:37 msgid "Manage users" msgstr "" -#: app/component/permission.py:28 +#: app/component/permission.py:42 msgid "Admin" msgstr "" -#: app/component/permission.py:29 +#: app/component/permission.py:43 msgid "Admin manager" msgstr "" -#: app/component/permission.py:33 +#: app/component/permission.py:47 msgid "Admin View" msgstr "" -#: app/component/permission.py:34 +#: app/component/permission.py:48 msgid "View admins" msgstr "" -#: app/component/permission.py:38 +#: app/component/permission.py:52 msgid "Admin Edit" msgstr "" -#: app/component/permission.py:39 +#: app/component/permission.py:53 msgid "Edit admins" msgstr "" -#: app/component/permission.py:44 +#: app/component/permission.py:58 msgid "Role" msgstr "" -#: app/component/permission.py:45 +#: app/component/permission.py:59 msgid "Role manager" msgstr "" -#: app/component/permission.py:49 +#: app/component/permission.py:63 msgid "Role View" msgstr "" -#: app/component/permission.py:50 +#: app/component/permission.py:64 msgid "View roles" msgstr "" -#: app/component/permission.py:54 +#: app/component/permission.py:68 msgid "Role Edit" msgstr "" -#: app/component/permission.py:55 +#: app/component/permission.py:69 msgid "Edit roles" msgstr "" -#: app/component/permission.py:60 +#: app/component/permission.py:74 msgid "Mcp" msgstr "" -#: app/component/permission.py:61 +#: app/component/permission.py:75 msgid "Mcp manager" msgstr "" -#: app/component/permission.py:65 +#: app/component/permission.py:79 msgid "Mcp Edit" msgstr "" -#: app/component/permission.py:66 +#: app/component/permission.py:80 msgid "Edit mcp service" msgstr "" -#: app/component/permission.py:70 +#: app/component/permission.py:84 msgid "Mcp Category Edit" msgstr "" -#: app/component/permission.py:71 +#: app/component/permission.py:85 msgid "Edit mcp category" msgstr "" -#: app/controller/chat/snapshot_controller.py:34 -#: app/controller/chat/snapshot_controller.py:68 -#: app/controller/chat/snapshot_controller.py:81 +#: app/controller/chat/snapshot_controller.py:58 +#: app/controller/chat/snapshot_controller.py:104 +#: app/controller/chat/snapshot_controller.py:133 msgid "Chat snapshot not found" msgstr "" -#: app/controller/chat/step_controller.py:65 -#: app/controller/chat/step_controller.py:89 -#: app/controller/chat/step_controller.py:102 +#: app/controller/chat/snapshot_controller.py:108 +msgid "You are not allowed to update this snapshot" +msgstr "" + +#: app/controller/chat/snapshot_controller.py:137 +msgid "You are not allowed to delete this snapshot" +msgstr "" + +#: app/controller/chat/step_controller.py:105 +#: app/controller/chat/step_controller.py:142 +#: app/controller/chat/step_controller.py:167 msgid "Chat step not found" msgstr "" -#: app/controller/config/config_controller.py:40 -#: app/controller/config/config_controller.py:76 -#: app/controller/config/config_controller.py:108 +#: app/controller/config/config_controller.py:60 +#: app/controller/config/config_controller.py:112 +#: app/controller/config/config_controller.py:155 msgid "Configuration not found" msgstr "" -#: app/controller/config/config_controller.py:47 -msgid "Config Name is valid" +#: app/controller/config/config_controller.py:73 +msgid "Invalid config name or group" msgstr "" -#: app/controller/config/config_controller.py:55 -#: app/controller/config/config_controller.py:92 +#: app/controller/config/config_controller.py:82 +#: app/controller/config/config_controller.py:130 msgid "Configuration already exists for this user" msgstr "" -#: app/controller/config/config_controller.py:80 +#: app/controller/config/config_controller.py:117 msgid "Invalid configuration group" msgstr "" -#: app/controller/mcp/mcp_controller.py:70 +#: app/controller/mcp/mcp_controller.py:132 +#: app/controller/mcp/mcp_controller.py:143 msgid "Mcp not found" msgstr "" -#: app/controller/mcp/mcp_controller.py:73 -#: app/controller/mcp/user_controller.py:44 +#: app/controller/mcp/mcp_controller.py:148 +#: app/controller/mcp/user_controller.py:113 msgid "mcp is installed" msgstr "" -#: app/controller/mcp/user_controller.py:34 +#: app/controller/mcp/user_controller.py:97 msgid "McpUser not found" msgstr "" -#: app/controller/mcp/user_controller.py:61 -#: app/controller/mcp/user_controller.py:75 +#: app/controller/mcp/user_controller.py:156 +#: app/controller/mcp/user_controller.py:180 msgid "Mcp Info not found" msgstr "" -#: app/controller/mcp/user_controller.py:63 +#: app/controller/mcp/user_controller.py:159 msgid "current user have no permission to modify" msgstr "" -#: app/controller/provider/provider_controller.py:41 -#: app/controller/provider/provider_controller.py:60 -#: app/controller/provider/provider_controller.py:79 +#: app/controller/provider/provider_controller.py:61 +#: app/controller/provider/provider_controller.py:89 +#: app/controller/provider/provider_controller.py:116 msgid "Provider not found" msgstr "" -#: app/controller/user/login_controller.py:25 +#: app/controller/user/login_controller.py:52 +#: app/controller/user/login_controller.py:58 msgid "Account or password error" msgstr "" -#: app/controller/user/login_controller.py:47 +#: app/controller/user/login_controller.py:110 +msgid "Authentication failed" +msgstr "" + +#: app/controller/user/login_controller.py:120 +#: app/controller/user/password_reset_controller.py:135 msgid "User not found" msgstr "" -#: app/controller/user/login_controller.py:64 -#: app/controller/user/login_controller.py:89 +#: app/controller/user/login_controller.py:152 +#: app/controller/user/login_controller.py:198 msgid "Failed to register" msgstr "" -#: app/controller/user/login_controller.py:67 +#: app/controller/user/login_controller.py:159 msgid "Your account has been blocked." msgstr "" -#: app/controller/user/login_controller.py:75 +#: app/controller/user/login_controller.py:176 msgid "Email already registered" msgstr "" -#: app/controller/user/user_password_controller.py:19 +#: app/controller/user/password_reset_controller.py:119 +#: app/controller/user/password_reset_controller.py:126 +msgid "Invalid or expired reset token" +msgstr "" + +#: app/controller/user/password_reset_controller.py:201 +msgid "User with this email not found" +msgstr "" + +#: app/controller/user/user_password_controller.py:40 msgid "Password is incorrect" msgstr "" -#: app/controller/user/user_password_controller.py:21 +#: app/controller/user/user_password_controller.py:44 msgid "The two passwords do not match" msgstr "" -#: app/model/abstract/model.py:66 +#: app/model/abstract/model.py:97 msgid "There is no data that meets the conditions" msgstr "" diff --git a/server/messages.pot b/server/messages.pot index 04c9844e1..04c605e65 100644 --- a/server/messages.pot +++ b/server/messages.pot @@ -1,14 +1,14 @@ # Translations template for PROJECT. -# Copyright (C) 2025 ORGANIZATION +# Copyright (C) 2026 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2025. +# FIRST AUTHOR , 2026. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-08-06 09:56+0800\n" +"POT-Creation-Date: 2026-01-28 01:03+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,198 +17,222 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" -#: app/component/auth.py:41 +#: app/component/auth.py:55 msgid "Validate credentials expired" msgstr "" -#: app/component/auth.py:43 +#: app/component/auth.py:57 msgid "Could not validate credentials" msgstr "" -#: app/component/permission.py:12 +#: app/component/permission.py:26 msgid "User" msgstr "" -#: app/component/permission.py:13 +#: app/component/permission.py:27 msgid "User manager" msgstr "" -#: app/component/permission.py:17 +#: app/component/permission.py:31 msgid "User Manage" msgstr "" -#: app/component/permission.py:18 +#: app/component/permission.py:32 msgid "View users" msgstr "" -#: app/component/permission.py:22 +#: app/component/permission.py:36 msgid "User Edit" msgstr "" -#: app/component/permission.py:23 +#: app/component/permission.py:37 msgid "Manage users" msgstr "" -#: app/component/permission.py:28 +#: app/component/permission.py:42 msgid "Admin" msgstr "" -#: app/component/permission.py:29 +#: app/component/permission.py:43 msgid "Admin manager" msgstr "" -#: app/component/permission.py:33 +#: app/component/permission.py:47 msgid "Admin View" msgstr "" -#: app/component/permission.py:34 +#: app/component/permission.py:48 msgid "View admins" msgstr "" -#: app/component/permission.py:38 +#: app/component/permission.py:52 msgid "Admin Edit" msgstr "" -#: app/component/permission.py:39 +#: app/component/permission.py:53 msgid "Edit admins" msgstr "" -#: app/component/permission.py:44 +#: app/component/permission.py:58 msgid "Role" msgstr "" -#: app/component/permission.py:45 +#: app/component/permission.py:59 msgid "Role manager" msgstr "" -#: app/component/permission.py:49 +#: app/component/permission.py:63 msgid "Role View" msgstr "" -#: app/component/permission.py:50 +#: app/component/permission.py:64 msgid "View roles" msgstr "" -#: app/component/permission.py:54 +#: app/component/permission.py:68 msgid "Role Edit" msgstr "" -#: app/component/permission.py:55 +#: app/component/permission.py:69 msgid "Edit roles" msgstr "" -#: app/component/permission.py:60 +#: app/component/permission.py:74 msgid "Mcp" msgstr "" -#: app/component/permission.py:61 +#: app/component/permission.py:75 msgid "Mcp manager" msgstr "" -#: app/component/permission.py:65 +#: app/component/permission.py:79 msgid "Mcp Edit" msgstr "" -#: app/component/permission.py:66 +#: app/component/permission.py:80 msgid "Edit mcp service" msgstr "" -#: app/component/permission.py:70 +#: app/component/permission.py:84 msgid "Mcp Category Edit" msgstr "" -#: app/component/permission.py:71 +#: app/component/permission.py:85 msgid "Edit mcp category" msgstr "" -#: app/controller/chat/snapshot_controller.py:34 -#: app/controller/chat/snapshot_controller.py:68 -#: app/controller/chat/snapshot_controller.py:81 +#: app/controller/chat/snapshot_controller.py:58 +#: app/controller/chat/snapshot_controller.py:104 +#: app/controller/chat/snapshot_controller.py:133 msgid "Chat snapshot not found" msgstr "" -#: app/controller/chat/step_controller.py:65 -#: app/controller/chat/step_controller.py:89 -#: app/controller/chat/step_controller.py:102 +#: app/controller/chat/snapshot_controller.py:108 +msgid "You are not allowed to update this snapshot" +msgstr "" + +#: app/controller/chat/snapshot_controller.py:137 +msgid "You are not allowed to delete this snapshot" +msgstr "" + +#: app/controller/chat/step_controller.py:105 +#: app/controller/chat/step_controller.py:142 +#: app/controller/chat/step_controller.py:167 msgid "Chat step not found" msgstr "" -#: app/controller/config/config_controller.py:40 -#: app/controller/config/config_controller.py:76 -#: app/controller/config/config_controller.py:108 +#: app/controller/config/config_controller.py:60 +#: app/controller/config/config_controller.py:112 +#: app/controller/config/config_controller.py:155 msgid "Configuration not found" msgstr "" -#: app/controller/config/config_controller.py:47 -msgid "Config Name is valid" +#: app/controller/config/config_controller.py:73 +msgid "Invalid config name or group" msgstr "" -#: app/controller/config/config_controller.py:55 -#: app/controller/config/config_controller.py:92 +#: app/controller/config/config_controller.py:82 +#: app/controller/config/config_controller.py:130 msgid "Configuration already exists for this user" msgstr "" -#: app/controller/config/config_controller.py:80 +#: app/controller/config/config_controller.py:117 msgid "Invalid configuration group" msgstr "" -#: app/controller/mcp/mcp_controller.py:70 +#: app/controller/mcp/mcp_controller.py:132 +#: app/controller/mcp/mcp_controller.py:143 msgid "Mcp not found" msgstr "" -#: app/controller/mcp/mcp_controller.py:73 -#: app/controller/mcp/user_controller.py:44 +#: app/controller/mcp/mcp_controller.py:148 +#: app/controller/mcp/user_controller.py:113 msgid "mcp is installed" msgstr "" -#: app/controller/mcp/user_controller.py:34 +#: app/controller/mcp/user_controller.py:97 msgid "McpUser not found" msgstr "" -#: app/controller/mcp/user_controller.py:61 -#: app/controller/mcp/user_controller.py:75 +#: app/controller/mcp/user_controller.py:156 +#: app/controller/mcp/user_controller.py:180 msgid "Mcp Info not found" msgstr "" -#: app/controller/mcp/user_controller.py:63 +#: app/controller/mcp/user_controller.py:159 msgid "current user have no permission to modify" msgstr "" -#: app/controller/provider/provider_controller.py:41 -#: app/controller/provider/provider_controller.py:60 -#: app/controller/provider/provider_controller.py:79 +#: app/controller/provider/provider_controller.py:61 +#: app/controller/provider/provider_controller.py:89 +#: app/controller/provider/provider_controller.py:116 msgid "Provider not found" msgstr "" -#: app/controller/user/login_controller.py:25 +#: app/controller/user/login_controller.py:52 +#: app/controller/user/login_controller.py:58 msgid "Account or password error" msgstr "" -#: app/controller/user/login_controller.py:47 +#: app/controller/user/login_controller.py:110 +msgid "Authentication failed" +msgstr "" + +#: app/controller/user/login_controller.py:120 +#: app/controller/user/password_reset_controller.py:135 msgid "User not found" msgstr "" -#: app/controller/user/login_controller.py:64 -#: app/controller/user/login_controller.py:89 +#: app/controller/user/login_controller.py:152 +#: app/controller/user/login_controller.py:198 msgid "Failed to register" msgstr "" -#: app/controller/user/login_controller.py:67 +#: app/controller/user/login_controller.py:159 msgid "Your account has been blocked." msgstr "" -#: app/controller/user/login_controller.py:75 +#: app/controller/user/login_controller.py:176 msgid "Email already registered" msgstr "" -#: app/controller/user/user_password_controller.py:19 +#: app/controller/user/password_reset_controller.py:119 +#: app/controller/user/password_reset_controller.py:126 +msgid "Invalid or expired reset token" +msgstr "" + +#: app/controller/user/password_reset_controller.py:201 +msgid "User with this email not found" +msgstr "" + +#: app/controller/user/user_password_controller.py:40 msgid "Password is incorrect" msgstr "" -#: app/controller/user/user_password_controller.py:21 +#: app/controller/user/user_password_controller.py:44 msgid "The two passwords do not match" msgstr "" -#: app/model/abstract/model.py:66 +#: app/model/abstract/model.py:97 msgid "There is no data that meets the conditions" msgstr "" diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index a78ff720d..1bbb9fd73 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -101,7 +101,7 @@ const SelectTrigger = React.forwardRef< return (
{title ? ( -
+
{title} {required && *} {tooltip && ( @@ -116,7 +116,7 @@ const SelectTrigger = React.forwardRef< disabled={disabled} className={cn( // Base styles - 'gap-2 rounded-lg px-3 text-text-body relative flex w-full items-center justify-between border border-solid transition-all outline-none', + 'relative flex w-full items-center justify-between gap-2 rounded-lg border border-solid px-3 text-text-body outline-none transition-all', sizeClasses[size], 'whitespace-nowrap [&>span]:line-clamp-1', // Default state (when no error/success) @@ -124,8 +124,8 @@ const SelectTrigger = React.forwardRef< // Interactive states (only when no error/success state) state !== 'error' && state !== 'success' && [ - 'hover:bg-input-bg-hover hover:ring-input-border-hover hover:ring-1 hover:ring-offset-0', - 'focus-visible:ring-input-border-focus data-[state=open]:bg-input-bg-input data-[state=open]:ring-input-border-focus focus-visible:ring-1 focus-visible:ring-offset-0 data-[state=open]:ring-1 data-[state=open]:ring-offset-0', + 'hover:bg-input-bg-hover hover:ring-1 hover:ring-input-border-hover hover:ring-offset-0', + 'focus-visible:ring-1 focus-visible:ring-input-border-focus focus-visible:ring-offset-0 data-[state=open]:bg-input-bg-input data-[state=open]:ring-1 data-[state=open]:ring-input-border-focus data-[state=open]:ring-offset-0', ], // Validation states (override defaults) stateCls.trigger, @@ -156,7 +156,7 @@ const SelectScrollUpButton = React.forwardRef< - + @@ -292,12 +292,12 @@ const SelectItemWithButton = React.forwardRef< value={value} disabled={!enabled} className={cn( - 'focus:bg-accent focus:text-accent-foreground group rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-menutabs-fill-hover relative flex w-full cursor-pointer items-center outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + 'focus:bg-accent focus:text-accent-foreground group relative flex w-full cursor-pointer select-none items-center rounded-lg py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-menutabs-fill-hover data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className )} {...props} > - + diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index f56d4ebc4..00112282e 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -245,5 +245,31 @@ "restart-required": "Restart Required", "restart-required-message": "Restart the application to enable your cookie domain changes.", "restart": "Restart", - "cookie-count": "{{count}} Cookies" + "cookie-count": "{{count}} Cookies", + "forgot-password": "Forgot Password?", + "forgot-password-description": "Enter your email address and we'll send you a link to reset your password.", + "forgot-password-failed": "Failed to send reset link. Please try again.", + "send-reset-link": "Send Reset Link", + "sending": "Sending...", + "check-your-email": "Check Your Email", + "password-reset-email-sent": "If an account with that email exists, we've sent you a link to reset your password. Please check your inbox.", + "back-to-login": "Back to Login", + "reset-password": "Reset Password", + "reset-password-description": "Enter your new password below.", + "reset-password-failed": "Failed to reset password. Please try again.", + "new-password": "New Password", + "enter-new-password": "Enter new password", + "confirm-password": "Confirm Password", + "confirm-new-password": "Confirm new password", + "please-confirm-password": "Please confirm your password", + "passwords-do-not-match": "Passwords do not match", + "password-must-contain-letters-and-numbers": "Password must contain both letters and numbers", + "resetting": "Resetting...", + "verifying": "Verifying", + "invalid-reset-link": "Invalid Reset Link", + "reset-link-expired-or-invalid": "This password reset link is invalid or has expired. Please request a new one.", + "request-new-link": "Request New Link", + "password-reset-success": "Password Reset Successfully", + "password-reset-success-description": "Your password has been reset. You can now log in with your new password.", + "go-to-login": "Go to Login" } diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index ce7bce0cb..93060480d 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -245,5 +245,31 @@ "restart-required": "需要重启", "restart-required-message": "重启应用程序以启用您的 Cookie 域名更改。", "restart": "重启", - "cookie-count": "{{count}} 个 Cookie" + "cookie-count": "{{count}} 个 Cookie", + "forgot-password": "忘记密码?", + "forgot-password-description": "输入您的电子邮件地址,我们将向您发送重置密码的链接。", + "forgot-password-failed": "发送重置链接失败,请重试。", + "send-reset-link": "发送重置链接", + "sending": "发送中...", + "check-your-email": "检查您的邮箱", + "password-reset-email-sent": "如果该邮箱存在账户,我们已向您发送了重置密码的链接。请检查您的收件箱。", + "back-to-login": "返回登录", + "reset-password": "重置密码", + "reset-password-description": "请在下方输入您的新密码。", + "reset-password-failed": "重置密码失败,请重试。", + "new-password": "新密码", + "enter-new-password": "输入新密码", + "confirm-password": "确认密码", + "confirm-new-password": "确认新密码", + "please-confirm-password": "请确认您的密码", + "passwords-do-not-match": "两次输入的密码不一致", + "password-must-contain-letters-and-numbers": "密码必须包含字母和数字", + "resetting": "重置中...", + "verifying": "验证中", + "invalid-reset-link": "无效的重置链接", + "reset-link-expired-or-invalid": "此密码重置链接无效或已过期。请重新申请。", + "request-new-link": "申请新链接", + "password-reset-success": "密码重置成功", + "password-reset-success-description": "您的密码已重置。现在可以使用新密码登录。", + "go-to-login": "前往登录" } diff --git a/src/pages/ForgotPassword.tsx b/src/pages/ForgotPassword.tsx new file mode 100644 index 000000000..4597905e5 --- /dev/null +++ b/src/pages/ForgotPassword.tsx @@ -0,0 +1,229 @@ +import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; +import loginGif from '@/assets/login.gif'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { proxyFetchPost } from '@/api/http'; +import { useTranslation } from 'react-i18next'; +import eye from '@/assets/eye.svg'; +import eyeOff from '@/assets/eye-off.svg'; + +export default function ForgotPassword() { + const navigate = useNavigate(); + const { t } = useTranslation(); + const [email, setEmail] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [emailError, setEmailError] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [confirmPasswordError, setConfirmPasswordError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [generalError, setGeneralError] = useState(''); + const [hidePassword, setHidePassword] = useState(true); + const [hideConfirmPassword, setHideConfirmPassword] = useState(true); + + const validateEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const validatePassword = (password: string) => { + const hasLetter = /[a-zA-Z]/.test(password); + const hasNumber = /[0-9]/.test(password); + return password.length >= 8 && hasLetter && hasNumber; + }; + + const validateForm = () => { + let isValid = true; + + if (!email) { + setEmailError(t('layout.please-enter-email-address')); + isValid = false; + } else if (!validateEmail(email)) { + setEmailError(t('layout.please-enter-a-valid-email-address')); + isValid = false; + } else { + setEmailError(''); + } + + if (!newPassword) { + setPasswordError(t('layout.please-enter-password')); + isValid = false; + } else if (!validatePassword(newPassword)) { + setPasswordError(t('layout.password-must-contain-letters-and-numbers')); + isValid = false; + } else { + setPasswordError(''); + } + + if (!confirmPassword) { + setConfirmPasswordError(t('layout.please-confirm-password')); + isValid = false; + } else if (newPassword !== confirmPassword) { + setConfirmPasswordError(t('layout.passwords-do-not-match')); + isValid = false; + } else { + setConfirmPasswordError(''); + } + + return isValid; + }; + + const handleSubmit = async () => { + if (!validateForm()) { + return; + } + + setGeneralError(''); + setIsLoading(true); + try { + const data = await proxyFetchPost('/api/reset-password-direct', { + email: email, + new_password: newPassword, + confirm_password: confirmPassword, + }); + + if (data.code && data.code !== 0) { + setGeneralError(data.text || t('layout.reset-password-failed')); + return; + } + + setIsSuccess(true); + } catch (error: any) { + console.error('Reset password request failed:', error); + setGeneralError(t('layout.reset-password-failed')); + } finally { + setIsLoading(false); + } + }; + + if (isSuccess) { + return ( +
+
+ +
+
+
+
+ {t('layout.password-reset-success')} +
+

+ {t('layout.password-reset-success-description')} +

+ +
+
+
+ ); + } + + return ( +
+
+ +
+
+
+
+
+ {t('layout.reset-password')} +
+ +
+

+ {t('layout.reset-password-description')} +

+
+ {generalError && ( +

+ {generalError} +

+ )} +
+ { + setEmail(e.target.value); + if (emailError) setEmailError(''); + if (generalError) setGeneralError(''); + }} + state={emailError ? 'error' : undefined} + note={emailError} + /> + { + setNewPassword(e.target.value); + if (passwordError) setPasswordError(''); + if (generalError) setGeneralError(''); + }} + state={passwordError ? 'error' : undefined} + note={passwordError} + backIcon={} + onBackIconClick={() => setHidePassword(!hidePassword)} + /> + { + setConfirmPassword(e.target.value); + if (confirmPasswordError) setConfirmPasswordError(''); + if (generalError) setGeneralError(''); + }} + state={confirmPasswordError ? 'error' : undefined} + note={confirmPasswordError} + backIcon={} + onBackIconClick={() => setHideConfirmPassword(!hideConfirmPassword)} + onEnter={handleSubmit} + /> +
+
+ +
+
+
+ ); +} diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx new file mode 100644 index 000000000..63735a5ce --- /dev/null +++ b/src/pages/ResetPassword.tsx @@ -0,0 +1,17 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +export default function ResetPassword() { + return null; +} diff --git a/src/routers/index.tsx b/src/routers/index.tsx index f7e283ead..f80a8266a 100644 --- a/src/routers/index.tsx +++ b/src/routers/index.tsx @@ -21,6 +21,7 @@ import Layout from '@/components/Layout'; // Lazy load page components const Login = lazy(() => import('@/pages/Login')); const Signup = lazy(() => import('@/pages/SignUp')); +const ForgotPassword = lazy(() => import('@/pages/ForgotPassword')); const Home = lazy(() => import('@/pages/Home')); const History = lazy(() => import('@/pages/History')); const NotFound = lazy(() => import('@/pages/NotFound')); @@ -137,6 +138,7 @@ const AppRoutes = () => ( } /> } /> + } /> }> }> } />