From 73f1fe105d4ba57485dd0fe549dd5dc3e59647a3 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 5 Mar 2026 18:55:36 +0800 Subject: [PATCH] refactor(auth): remove OTP flows from login/register/reset --- api/register_otp_test.go | 252 ------------ api/server.go | 142 +------ auth/auth.go | 35 -- go.mod | 1 - go.sum | 2 - store/user.go | 24 -- web/src/components/LoginPage.tsx | 198 +--------- web/src/components/RegisterPage.tsx | 467 +++++++---------------- web/src/components/ResetPasswordPage.tsx | 38 +- web/src/contexts/AuthContext.tsx | 194 +++------- web/src/i18n/translations.ts | 64 ---- 11 files changed, 210 insertions(+), 1207 deletions(-) delete mode 100644 api/register_otp_test.go diff --git a/api/register_otp_test.go b/api/register_otp_test.go deleted file mode 100644 index f03a9bfb..00000000 --- a/api/register_otp_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package api - -import ( - "testing" -) - -// MockUser Mock user structure -type MockUser struct { - ID int - Email string - OTPSecret string - OTPVerified bool -} - -// TestOTPRefetchLogic Test OTP refetch logic -func TestOTPRefetchLogic(t *testing.T) { - tests := []struct { - name string - existingUser *MockUser - userExists bool - expectedAction string // "allow_refetch", "reject_duplicate", "create_new" - expectedMessage string - }{ - { - name: "New user registration - email does not exist", - existingUser: nil, - userExists: false, - expectedAction: "create_new", - expectedMessage: "Create new user", - }, - { - name: "Incomplete OTP verification - allow refetch", - existingUser: &MockUser{ - ID: 1, - Email: "test@example.com", - OTPSecret: "SECRET123", - OTPVerified: false, - }, - userExists: true, - expectedAction: "allow_refetch", - expectedMessage: "Incomplete registration detected, please continue OTP setup", - }, - { - name: "Completed OTP verification - reject duplicate registration", - existingUser: &MockUser{ - ID: 2, - Email: "verified@example.com", - OTPSecret: "SECRET456", - OTPVerified: true, - }, - userExists: true, - expectedAction: "reject_duplicate", - expectedMessage: "Email already registered", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Simulate logic processing flow - var actualAction string - var actualMessage string - - if !tt.userExists { - // User does not exist, create new user - actualAction = "create_new" - actualMessage = "Create new user" - } else { - // User exists, check OTP verification status - if !tt.existingUser.OTPVerified { - // OTP verification incomplete, allow refetch - actualAction = "allow_refetch" - actualMessage = "Incomplete registration detected, please continue OTP setup" - } else { - // Verification completed, reject duplicate registration - actualAction = "reject_duplicate" - actualMessage = "Email already registered" - } - } - - // Verify results - if actualAction != tt.expectedAction { - t.Errorf("Action mismatch: got %s, want %s", actualAction, tt.expectedAction) - } - if actualMessage != tt.expectedMessage { - t.Errorf("Message mismatch: got %s, want %s", actualMessage, tt.expectedMessage) - } - }) - } -} - -// TestOTPVerificationStates Test OTP verification state determination -func TestOTPVerificationStates(t *testing.T) { - tests := []struct { - name string - otpVerified bool - shouldAllowRefetch bool - }{ - { - name: "OTP verified - disallow refetch", - otpVerified: true, - shouldAllowRefetch: false, - }, - { - name: "OTP not verified - allow refetch", - otpVerified: false, - shouldAllowRefetch: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Simulate verification logic - allowRefetch := !tt.otpVerified - - if allowRefetch != tt.shouldAllowRefetch { - t.Errorf("Refetch logic error: OTPVerified=%v, allowRefetch=%v, expected=%v", - tt.otpVerified, allowRefetch, tt.shouldAllowRefetch) - } - }) - } -} - -// TestRegistrationFlow Test complete registration flow logic branches -func TestRegistrationFlow(t *testing.T) { - tests := []struct { - name string - scenario string - userExists bool - otpVerified bool - expectHTTPCode int // Simulated HTTP status code - expectResponse string - }{ - { - name: "Scenario 1: New user first registration", - scenario: "New user first accesses registration endpoint", - userExists: false, - otpVerified: false, - expectHTTPCode: 200, - expectResponse: "Create user and return OTP setup information", - }, - { - name: "Scenario 2: User re-accesses after interrupting registration", - scenario: "User registered previously but did not complete OTP setup, now re-accessing", - userExists: true, - otpVerified: false, - expectHTTPCode: 200, - expectResponse: "Return existing user's OTP information, allow continuation", - }, - { - name: "Scenario 3: Registered user attempts duplicate registration", - scenario: "User already completed registration, attempts to register again with same email", - userExists: true, - otpVerified: true, - expectHTTPCode: 409, // Conflict - expectResponse: "Email already registered", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Simulate registration flow logic - var actualHTTPCode int - var actualResponse string - - if !tt.userExists { - // New user, create and return OTP information - actualHTTPCode = 200 - actualResponse = "Create user and return OTP setup information" - } else { - // User exists - if !tt.otpVerified { - // OTP verification incomplete, allow refetch - actualHTTPCode = 200 - actualResponse = "Return existing user's OTP information, allow continuation" - } else { - // Verification completed, reject duplicate registration - actualHTTPCode = 409 - actualResponse = "Email already registered" - } - } - - // Verify - if actualHTTPCode != tt.expectHTTPCode { - t.Errorf("HTTP code mismatch: got %d, want %d (scenario: %s)", - actualHTTPCode, tt.expectHTTPCode, tt.scenario) - } - if actualResponse != tt.expectResponse { - t.Errorf("Response mismatch: got %s, want %s (scenario: %s)", - actualResponse, tt.expectResponse, tt.scenario) - } - - t.Logf("✓ %s: HTTP %d, %s", tt.scenario, actualHTTPCode, actualResponse) - }) - } -} - -// TestEdgeCases Test edge cases -func TestEdgeCases(t *testing.T) { - tests := []struct { - name string - user *MockUser - expectAllow bool - description string - }{ - { - name: "User ID is 0 - treated as new user", - user: &MockUser{ - ID: 0, - Email: "new@example.com", - OTPVerified: false, - }, - expectAllow: true, - description: "ID of 0 usually indicates user has not been created yet", - }, - { - name: "OTPSecret is empty - still can refetch", - user: &MockUser{ - ID: 1, - Email: "test@example.com", - OTPSecret: "", - OTPVerified: false, - }, - expectAllow: true, - description: "Even if OTPSecret is empty, as long as not verified, refetch is allowed", - }, - { - name: "OTPSecret exists but already verified - not allowed", - user: &MockUser{ - ID: 2, - Email: "verified@example.com", - OTPSecret: "SECRET789", - OTPVerified: true, - }, - expectAllow: false, - description: "Users with verified OTP cannot refetch", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Core logic: as long as OTPVerified is false, refetch is allowed - allowRefetch := !tt.user.OTPVerified - - if allowRefetch != tt.expectAllow { - t.Errorf("Edge case failed: %s\nUser: ID=%d, OTPVerified=%v\nExpected allow=%v, got=%v", - tt.description, tt.user.ID, tt.user.OTPVerified, tt.expectAllow, allowRefetch) - } - - t.Logf("✓ %s", tt.description) - }) - } -} diff --git a/api/server.go b/api/server.go index 45c5dda4..fc545a54 100644 --- a/api/server.go +++ b/api/server.go @@ -142,8 +142,6 @@ func (s *Server) setupRoutes() { // Authentication related routes (no authentication required) api.POST("/register", s.handleRegister) api.POST("/login", s.handleLogin) - api.POST("/verify-otp", s.handleVerifyOTP) - api.POST("/complete-registration", s.handleCompleteRegistration) // Routes requiring authentication protected := api.Group("/", s.authMiddleware()) @@ -3095,29 +3093,9 @@ func (s *Server) handleRegister(c *gin.Context) { return } - // Check if email already exists (must check before maxUsers to allow incomplete OTP users) - existingUser, err := s.store.User().GetByEmail(req.Email) + // Check if email already exists + _, err := s.store.User().GetByEmail(req.Email) if err == nil { - // User exists, check OTP verification status - if !existingUser.OTPVerified { - // OTP not verified, verify password first for security - if !auth.CheckPassword(req.Password, existingUser.PasswordHash) { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Email or password incorrect"}) - return - } - // Password correct, allow user to continue OTP setup - // Return existing OTP information - qrCodeURL := auth.GetOTPQRCodeURL(existingUser.OTPSecret, req.Email) - c.JSON(http.StatusOK, gin.H{ - "user_id": existingUser.ID, - "email": existingUser.Email, - "otp_secret": existingUser.OTPSecret, - "qr_code_url": qrCodeURL, - "message": "Incomplete registration detected, please continue OTP setup", - }) - return - } - // OTP already verified, reject duplicate registration c.JSON(http.StatusConflict, gin.H{"error": "Email already registered"}) return } @@ -3143,21 +3121,12 @@ func (s *Server) handleRegister(c *gin.Context) { return } - // Generate OTP secret - otpSecret, err := auth.GenerateOTPSecret() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "OTP secret generation failed"}) - return - } - - // Create user (unverified OTP status) + // Create user userID := uuid.New().String() user := &store.User{ ID: userID, Email: req.Email, PasswordHash: passwordHash, - OTPSecret: otpSecret, - OTPVerified: false, } err = s.store.User().Create(user) @@ -3166,49 +3135,6 @@ func (s *Server) handleRegister(c *gin.Context) { return } - // Return OTP setup information - qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email) - c.JSON(http.StatusOK, gin.H{ - "user_id": userID, - "email": req.Email, - "otp_secret": otpSecret, - "qr_code_url": qrCodeURL, - "message": "Please scan the QR code with Google Authenticator and verify OTP", - }) -} - -// handleCompleteRegistration Complete registration (verify OTP) -func (s *Server) handleCompleteRegistration(c *gin.Context) { - var req struct { - UserID string `json:"user_id" binding:"required"` - OTPCode string `json:"otp_code" binding:"required"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - SafeBadRequest(c, "Invalid request parameters") - return - } - - // Get user information - user, err := s.store.User().GetByID(req.UserID) - if err != nil { - SafeNotFound(c, "User") - return - } - - // Verify OTP - if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": "OTP code error"}) - return - } - - // Update user OTP verified status - err = s.store.User().UpdateOTPVerified(req.UserID, true) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user status"}) - return - } - // Generate JWT token token, err := auth.GenerateJWT(user.ID, user.Email) if err != nil { @@ -3226,7 +3152,7 @@ func (s *Server) handleCompleteRegistration(c *gin.Context) { "token": token, "user_id": user.ID, "email": user.Email, - "message": "Registration completed", + "message": "Registration successful", }) } @@ -3255,56 +3181,7 @@ func (s *Server) handleLogin(c *gin.Context) { return } - // Check if OTP is verified - if !user.OTPVerified { - // Return OTP info so user can complete setup - qrCodeURL := auth.GetOTPQRCodeURL(user.OTPSecret, user.Email) - c.JSON(http.StatusOK, gin.H{ - "user_id": user.ID, - "email": user.Email, - "otp_secret": user.OTPSecret, - "qr_code_url": qrCodeURL, - "requires_otp_setup": true, - "message": "Please complete OTP setup first", - }) - return - } - - // Return status requiring OTP verification - c.JSON(http.StatusOK, gin.H{ - "user_id": user.ID, - "email": user.Email, - "message": "Please enter Google Authenticator code", - "requires_otp": true, - }) -} - -// handleVerifyOTP Verify OTP and complete login -func (s *Server) handleVerifyOTP(c *gin.Context) { - var req struct { - UserID string `json:"user_id" binding:"required"` - OTPCode string `json:"otp_code" binding:"required"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - SafeBadRequest(c, "Invalid request parameters") - return - } - - // Get user information - user, err := s.store.User().GetByID(req.UserID) - if err != nil { - SafeNotFound(c, "User") - return - } - - // Verify OTP - if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": "Verification code error"}) - return - } - - // Generate JWT token + // Issue token directly after password verification. token, err := auth.GenerateJWT(user.ID, user.Email) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) @@ -3319,12 +3196,11 @@ func (s *Server) handleVerifyOTP(c *gin.Context) { }) } -// handleResetPassword Reset password (via email + OTP verification) +// handleResetPassword Reset password via email and new password func (s *Server) handleResetPassword(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` NewPassword string `json:"new_password" binding:"required,min=6"` - OTPCode string `json:"otp_code" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -3339,12 +3215,6 @@ func (s *Server) handleResetPassword(c *gin.Context) { return } - // Verify OTP - if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": "Google Authenticator code error"}) - return - } - // Generate new password hash newPasswordHash, err := auth.HashPassword(req.NewPassword) if err != nil { diff --git a/auth/auth.go b/auth/auth.go index a6bbe736..4861fee7 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,15 +1,12 @@ package auth import ( - "crypto/rand" "fmt" "log" "sync" "time" "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" - "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" ) @@ -25,9 +22,6 @@ var tokenBlacklist = struct { // maxBlacklistEntries is the maximum capacity threshold for blacklist const maxBlacklistEntries = 100_000 -// OTPIssuer is the OTP issuer name -const OTPIssuer = "nofxAI" - // SetJWTSecret sets the JWT secret key func SetJWTSecret(secret string) { JWTSecret = []byte(secret) @@ -87,30 +81,6 @@ func CheckPassword(password, hash string) bool { return err == nil } -// GenerateOTPSecret generates OTP secret -func GenerateOTPSecret() (string, error) { - secret := make([]byte, 20) - _, err := rand.Read(secret) - if err != nil { - return "", err - } - - key, err := totp.Generate(totp.GenerateOpts{ - Issuer: OTPIssuer, - AccountName: uuid.New().String(), - }) - if err != nil { - return "", err - } - - return key.Secret(), nil -} - -// VerifyOTP verifies OTP code -func VerifyOTP(secret, code string) bool { - return totp.Validate(code, secret) -} - // GenerateJWT generates JWT token func GenerateJWT(userID, email string) (string, error) { claims := Claims{ @@ -147,8 +117,3 @@ func ValidateJWT(tokenString string) (*Claims, error) { return nil, fmt.Errorf("invalid token") } - -// GetOTPQRCodeURL gets OTP QR code URL -func GetOTPQRCodeURL(secret, email string) string { - return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", OTPIssuer, email, secret, OTPIssuer) -} diff --git a/go.mod b/go.mod index d6d0127f..35e958df 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 - github.com/pquerna/otp v1.4.0 github.com/rs/zerolog v1.34.0 github.com/sirupsen/logrus v1.9.3 github.com/sonirico/go-hyperliquid v0.26.0 diff --git a/go.sum b/go.sum index 732fc62b..bf4a9098 100644 --- a/go.sum +++ b/go.sum @@ -186,8 +186,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= -github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= diff --git a/store/user.go b/store/user.go index 9c878757..73c9a671 100644 --- a/store/user.go +++ b/store/user.go @@ -1,8 +1,6 @@ package store import ( - "crypto/rand" - "encoding/base32" "time" "gorm.io/gorm" @@ -18,24 +16,12 @@ type User struct { ID string `gorm:"primaryKey" json:"id"` Email string `gorm:"uniqueIndex:idx_users_email;not null" json:"email"` PasswordHash string `gorm:"column:password_hash;not null" json:"-"` - OTPSecret string `gorm:"column:otp_secret" json:"-"` - OTPVerified bool `gorm:"column:otp_verified;default:false" json:"otp_verified"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (User) TableName() string { return "users" } -// GenerateOTPSecret generates OTP secret -func GenerateOTPSecret() (string, error) { - secret := make([]byte, 20) - _, err := rand.Read(secret) - if err != nil { - return "", err - } - return base32.StdEncoding.EncodeToString(secret), nil -} - // NewUserStore creates a new UserStore func NewUserStore(db *gorm.DB) *UserStore { return &UserStore{db: db} @@ -54,9 +40,6 @@ func (s *UserStore) initTables() error { s.db.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT NOT NULL DEFAULT ''`) s.db.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`) s.db.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`) - // OTP columns (added later) - s.db.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS otp_secret TEXT DEFAULT ''`) - s.db.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS otp_verified BOOLEAN DEFAULT FALSE`) // Ensure unique index exists on email (don't care about the name) var indexExists int64 @@ -114,11 +97,6 @@ func (s *UserStore) GetAllIDs() ([]string, error) { return userIDs, err } -// UpdateOTPVerified updates OTP verification status -func (s *UserStore) UpdateOTPVerified(userID string, verified bool) error { - return s.db.Model(&User{}).Where("id = ?", userID).Update("otp_verified", verified).Error -} - // UpdatePassword updates password func (s *UserStore) UpdatePassword(userID, passwordHash string) error { return s.db.Model(&User{}).Where("id = ?", userID).Updates(map[string]interface{}{ @@ -138,7 +116,5 @@ func (s *UserStore) EnsureAdmin() error { ID: "admin", Email: "admin@localhost", PasswordHash: "", - OTPSecret: "", - OTPVerified: true, }) } diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index 115c1def..af246723 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -10,15 +10,10 @@ import { useSystemConfig } from '../hooks/useSystemConfig' export function LoginPage() { const { language } = useLanguage() - const { login, loginAdmin, verifyOTP, completeRegistration } = useAuth() - const [step, setStep] = useState<'login' | 'otp' | 'setup-otp'>('login') + const { login, loginAdmin } = useAuth() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [showPassword, setShowPassword] = useState(false) - const [otpCode, setOtpCode] = useState('') - const [userID, setUserID] = useState('') - const [qrCodeURL, setQrCodeURL] = useState('') // New state for recovery - const [otpSecret, setOtpSecret] = useState('') // New state for recovery const [error, setError] = useState('') const [loading, setLoading] = useState(false) const [adminPassword, setAdminPassword] = useState('') @@ -64,83 +59,19 @@ export function LoginPage() { const result = await login(email, password) if (result.success) { - // Check for incomplete OTP setup (user registered but didn't complete 2FA) - if (result.requiresOTPSetup && result.userID) { - setUserID(result.userID) - setQrCodeURL(result.qrCodeURL || '') - setOtpSecret(result.otpSecret || '') - setStep('setup-otp') - toast.info("Pending 2FA setup detected. Please complete configuration.") - } else if (result.requiresOTP && result.userID) { - setUserID(result.userID) - - // Check if backend provided recovery data (meaning 2FA is pending setup) - if (result.qrCodeURL) { - setQrCodeURL(result.qrCodeURL) - setOtpSecret(result.otpSecret || '') - setStep('setup-otp') - toast.info("Pending 2FA setup detected. Please complete configuration.") - } else { - setStep('otp') - } - } else { - // Dismiss the "login expired" toast on successful login (no OTP required) - if (expiredToastId) { - toast.dismiss(expiredToastId) - } - } - } else { - // Check if we have recovery data despite the error (e.g. "Account has not completed OTP setup") - if (result.qrCodeURL) { - setUserID(result.userID || '') // We might need to ensure userID is returned in error case too, or derived - setQrCodeURL(result.qrCodeURL) - setOtpSecret(result.otpSecret || '') - setStep('setup-otp') - toast.warning(t('completeGapSetup', language) || "Incomplete setup detected. Please configure 2FA.") - } else { - const msg = result.message || t('loginFailed', language) - setError(msg) - toast.error(msg) - } - } - - setLoading(false) - } - - const handleOTPVerify = async (e: React.FormEvent) => { - e.preventDefault() - setError('') - setLoading(true) - - // If we have qrCodeURL, it means user needs to complete registration (first time OTP setup) - // Otherwise, it's a normal login OTP verification - const result = qrCodeURL - ? await completeRegistration(userID, otpCode) - : await verifyOTP(userID, otpCode) - - if (!result.success) { - const msg = result.message || t('verificationFailed', language) - setError(msg) - toast.error(msg) - } else { - // Dismiss the "login expired" toast on successful OTP verification + // Dismiss the "login expired" toast on successful login. if (expiredToastId) { toast.dismiss(expiredToastId) } - // Clear qrCodeURL after successful completion - setQrCodeURL('') - setOtpSecret('') + } else { + const msg = result.message || t('loginFailed', language) + setError(msg) + toast.error(msg) } - // 成功的话AuthContext会自动处理登录状态 setLoading(false) } - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) - toast.success('Copied to clipboard') - } - return ( @@ -172,7 +103,7 @@ export function LoginPage() { SYSTEM ACCESS

- {step === 'login' ? 'Authentication Protocol v3.0' : 'Multi-Factor Verification'} + Authentication Protocol v3.0

@@ -241,67 +172,7 @@ export function LoginPage() { {loading ? '> VERIFYING...' : '> EXECUTE_LOGIN'} - ) : step === 'setup-otp' ? ( -
-
-
COMPLETE 2FA CONFIGURATION
- {qrCodeURL ? ( -
- QR Code -
- ) : ( -
- )} -
-

Backup Secret Key

-
- {otpSecret} - -
-
-
- -
-
- 01 -
-

Install Authenticator App

-

Recommended: Google Authenticator.

-
- iOS - Android -
-
-
- -
- -
- 02 -
-

Scan & Verify

-

Scan code above, then enter the 6-digit token below to activate your account.

-
-
-
- - -
- ) : step === 'login' ? ( + ) : (
@@ -371,59 +242,6 @@ export function LoginPage() { )} - ) : ( -
-
-
- 🔐 -
-

- {t('scanQRCodeInstructions', language)}
- {t('enterOTPCode', language)} -

-
- -
- - - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) - } - className="w-full bg-black border border-zinc-700 rounded px-4 py-4 text-center text-2xl tracking-[0.5em] font-mono text-white focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800" - placeholder="000000" - maxLength={6} - required - autoFocus - /> -
- - {error && ( -
- [ACCESS DENIED]: {error} -
- )} - -
- - -
-
)}
diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index cf92819c..55182bac 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -1,33 +1,25 @@ -import React, { useState, useEffect } from 'react' +import React, { useEffect, useState } from 'react' +import { Eye, EyeOff } from 'lucide-react' +import PasswordChecklist from 'react-password-checklist' +import { toast } from 'sonner' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' import { getSystemConfig } from '../lib/config' -import { toast } from 'sonner' -import { copyWithToast } from '../lib/clipboard' -import { Eye, EyeOff } from 'lucide-react' import { DeepVoidBackground } from './DeepVoidBackground' -// import { Input } from './ui/input' // Removed unused import -import PasswordChecklist from 'react-password-checklist' import { RegistrationDisabled } from './RegistrationDisabled' import { WhitelistFullPage } from './WhitelistFullPage' export function RegisterPage() { const { language } = useLanguage() - const { register, completeRegistration } = useAuth() - const [step, setStep] = useState<'register' | 'setup-otp' | 'verify-otp' | 'whitelist-full'>( - 'register' - ) + const { register } = useAuth() + const [view, setView] = useState<'register' | 'whitelist-full'>('register') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [betaCode, setBetaCode] = useState('') const [betaMode, setBetaMode] = useState(false) const [registrationEnabled, setRegistrationEnabled] = useState(true) - const [otpCode, setOtpCode] = useState('') - const [userID, setUserID] = useState('') - const [otpSecret, setOtpSecret] = useState('') - const [qrCodeURL, setQrCodeURL] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) const [passwordValid, setPasswordValid] = useState(false) @@ -35,7 +27,6 @@ export function RegisterPage() { const [showConfirmPassword, setShowConfirmPassword] = useState(false) useEffect(() => { - // 获取系统配置,检查是否开启内测模式和注册功能 getSystemConfig() .then((config) => { setBetaMode(config.beta_mode || false) @@ -46,21 +37,18 @@ export function RegisterPage() { }) }, []) - // 如果注册功能被禁用,显示注册已关闭页面 if (!registrationEnabled) { return } - // 如果白名单已满,显示容量已满页面 - if (step === 'whitelist-full') { - return setStep('register')} /> + if (view === 'whitelist-full') { + return setView('register')} /> } const handleRegister = async (e: React.FormEvent) => { e.preventDefault() setError('') - // 使用 PasswordChecklist 的校验结果 if (!passwordValid) { setError(t('passwordNotMeetRequirements', language)) return @@ -72,50 +60,44 @@ export function RegisterPage() { } setLoading(true) - try { const result = await register(email, password, betaCode.trim() || undefined) - // Helper to check for whitelist errors const isWhitelistError = (msg: string) => { const lowerMsg = msg.toLowerCase() - return lowerMsg.includes('whitelist') || + return ( + lowerMsg.includes('whitelist') || lowerMsg.includes('capacity') || lowerMsg.includes('limit') || lowerMsg.includes('permission denied') || lowerMsg.includes('not on whitelist') + ) } - if (result.success && result.userID) { - setUserID(result.userID) - setOtpSecret(result.otpSecret || '') - setQrCodeURL(result.qrCodeURL || '') - setStep('setup-otp') - } else { - // Check for whitelist/capacity limit error + if (!result.success) { const msg = result.message || t('registrationFailed', language) if (isWhitelistError(msg)) { - setStep('whitelist-full') + setView('whitelist-full') return } setError(msg) toast.error(msg) } + // success path is handled in AuthContext (auto login + navigation) } catch (e) { console.error('Registration error:', e) const errorMsg = e instanceof Error ? e.message : 'Registration failed due to server error' - - // Check for whitelist error in catch block too const lowerMsg = errorMsg.toLowerCase() - if (lowerMsg.includes('whitelist') || + if ( + lowerMsg.includes('whitelist') || lowerMsg.includes('capacity') || lowerMsg.includes('limit') || lowerMsg.includes('permission denied') || - lowerMsg.includes('not on whitelist')) { - setStep('whitelist-full') + lowerMsg.includes('not on whitelist') + ) { + setView('whitelist-full') return } - setError(errorMsg) toast.error(errorMsg) } finally { @@ -123,39 +105,12 @@ export function RegisterPage() { } } - const handleSetupComplete = () => { - setStep('verify-otp') - } - - const handleOTPVerify = async (e: React.FormEvent) => { - e.preventDefault() - setError('') - setLoading(true) - - const result = await completeRegistration(userID, otpCode) - - if (!result.success) { - const msg = result.message || t('registrationFailed', language) - setError(msg) - toast.error(msg) - } - // 成功的话AuthContext会自动处理登录状态 - - setLoading(false) - } - - const copyToClipboard = (text: string) => { - copyWithToast(text) - } - return ( -
- {/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
- {/* Terminal Header */}
- NoFx Logo + NoFx Logo

NEW_USER ONBOARDING

- {step === 'register' && 'Initializing Registration Sequence...'} - {step === 'setup-otp' && 'Configuring Security Protocols...'} - {step === 'verify-otp' && 'Finalizing Authentication...'} + Initializing Registration Sequence...

- {/* Terminal Output / Form Container */}
- {/* Window Bar */}
window.location.href = '/'} + onClick={() => (window.location.href = '/')} title="Close / Return Home" >
@@ -206,7 +152,6 @@ export function RegisterPage() {
- {/* Status Output */}
@@ -218,275 +163,151 @@ export function RegisterPage() {
- {step === 'register' && ( -
+ +
+ + setEmail(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono" + placeholder="user@nofx.os" + required + /> +
+ +
- - setEmail(e.target.value)} - className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono" - placeholder="user@nofx.os" - required + +
+ setPassword(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10" + placeholder="••••••••" + required + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10" + placeholder="••••••••" + required + /> + +
+
+
+ +
+
+
+ Password Strength Protocol +
+
+ setPasswordValid(isValid)} + iconSize={10} />
- -
-
- -
- setPassword(e.target.value)} - className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10" - placeholder="••••••••" - required - /> - -
-
- -
- -
- setConfirmPassword(e.target.value)} - className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10" - placeholder="••••••••" - required - /> - -
-
-
- -
-
-
- Password Strength Protocol -
-
- setPasswordValid(isValid)} - iconSize={10} - /> -
-
- - {betaMode && ( -
- - setBetaCode(e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase())} - className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono tracking-widest" - placeholder="XXXXXX" - maxLength={6} - required={betaMode} - /> -

* CASE SENSITIVE ALPHANUMERIC

-
- )} - - {error && ( -
- [REGISTRATION_ERROR]: {error} -
- )} - - - - )} - - {step === 'setup-otp' && ( -
-
-
SCAN_QR_CODE_SEQUENCE
- {qrCodeURL ? ( -
- QR Code -
- ) : ( -
- )} -
-

Backup Secret Key

-
- {otpSecret} - -
-
-
- -
-
- 01 -
-

Install Authenticator App

-

We highly recommend Google Authenticator for compatibility.

-
- iOS - Android -
-
-
- -
- -
- 02 -
-

Scan QR Code

-

Open Google Authenticator, tap the + button, and scan the code above.

-

Protocol: Time-Based OTP (TOTP)

-
-
- -
- -
- 03 -
-

Verify Token

-

Enter the 6-digit code generated by the app.

-
- ⚠️ - Stuck? Ensure your phone's time is set to "Automatic". Time drift causes codes to fail. -
-
-
-
- -
- )} - - {step === 'verify-otp' && ( -
-
-

- ENTER 6-DIGIT SECURITY TOKEN TO FINALIZE ONBOARDING -

-
+ {betaMode && (
+ - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) - } - className="w-full bg-black border border-zinc-700 rounded px-4 py-4 text-center text-3xl tracking-[0.5em] font-mono text-white focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800" - placeholder="000000" + value={betaCode} + onChange={(e) => setBetaCode(e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase())} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono tracking-widest" + placeholder="XXXXXX" maxLength={6} - required - autoFocus + required={betaMode} /> +

* CASE SENSITIVE ALPHANUMERIC

+ )} - {error && ( -
- [VERIFICATION_FAILED]: {error} -
+ {error && ( +
+ [REGISTRATION_ERROR]: {error} +
+ )} + + -
- )} - + +
- {/* Terminal Footer Info */}
ENCRYPTION: AES-256
SECURE_REGISTRY
- {/* Login Link */} - {step === 'register' && ( -
-

- EXISTING_OPERATOR?{' '} - -

+
+

+ EXISTING_OPERATOR?{' '} -

- )} - +

+ +
) diff --git a/web/src/components/ResetPasswordPage.tsx b/web/src/components/ResetPasswordPage.tsx index 2504c9c8..1eb569fe 100644 --- a/web/src/components/ResetPasswordPage.tsx +++ b/web/src/components/ResetPasswordPage.tsx @@ -14,7 +14,6 @@ export function ResetPasswordPage() { const [email, setEmail] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') - const [otpCode, setOtpCode] = useState('') const [error, setError] = useState('') const [success, setSuccess] = useState(false) const [loading, setLoading] = useState(false) @@ -35,7 +34,7 @@ export function ResetPasswordPage() { setLoading(true) - const result = await resetPassword(email, newPassword, otpCode) + const result = await resetPassword(email, newPassword) if (result.success) { setSuccess(true) @@ -88,7 +87,7 @@ export function ResetPasswordPage() { {t('resetPasswordTitle', language)}

- 使用邮箱和 Google Authenticator 重置密码 + 使用邮箱和新密码重置账户密码

@@ -230,37 +229,6 @@ export function ResetPasswordPage() { />
-
- -
-
📱
-

- 打开 Google Authenticator 获取6位验证码 -

-
- - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) - } - className="w-full px-3 py-2 rounded text-center text-2xl font-mono" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - placeholder={t('otpPlaceholder', language)} - maxLength={6} - required - /> -
- {error && (
diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 38c9c8ec..b387a884 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -16,12 +16,6 @@ interface AuthContextType { ) => Promise<{ success: boolean message?: string - userID?: string - requiresOTP?: boolean - requiresOTPSetup?: boolean - qrCodeURL?: string - otpSecret?: string - email?: string }> loginAdmin: (password: string) => Promise<{ success: boolean @@ -34,22 +28,10 @@ interface AuthContextType { ) => Promise<{ success: boolean message?: string - userID?: string - otpSecret?: string - qrCodeURL?: string - }> - verifyOTP: ( - userID: string, - otpCode: string - ) => Promise<{ success: boolean; message?: string }> - completeRegistration: ( - userID: string, - otpCode: string ) => Promise<{ success: boolean; message?: string }> resetPassword: ( email: string, - newPassword: string, - otpCode: string + newPassword: string ) => Promise<{ success: boolean; message?: string }> logout: () => void isLoading: boolean @@ -123,38 +105,37 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const data = await response.json() if (response.ok) { - // Check for OTP setup required (incomplete registration) - if (data.requires_otp_setup) { - return { - success: true, - userID: data.user_id, - requiresOTPSetup: true, - message: data.message, - qrCodeURL: data.qr_code_url, - otpSecret: data.otp_secret, - email: data.email - } - } - // Check for OTP verification required (normal login flow) - if (data.requires_otp) { - return { - success: true, - userID: data.user_id, - requiresOTP: true, - message: data.message, - qrCodeURL: data.qr_code_url, - otpSecret: data.otp_secret + if (data.token) { + // Reset 401 flag on successful login + reset401Flag() + + const userInfo = { id: data.user_id, email: data.email } + setToken(data.token) + setUser(userInfo) + localStorage.setItem('auth_token', data.token) + localStorage.setItem('auth_user', JSON.stringify(userInfo)) + + // Check and redirect to returnUrl if exists + const returnUrl = sessionStorage.getItem('returnUrl') + if (returnUrl) { + sessionStorage.removeItem('returnUrl') + window.history.pushState({}, '', returnUrl) + window.dispatchEvent(new PopStateEvent('popstate')) + } else { + // 跳转到配置页面 + window.history.pushState({}, '', '/traders') + window.dispatchEvent(new PopStateEvent('popstate')) } + + return { success: true, message: data.message } } + // Unexpected success response - return { success: false, message: '登录响应异常' } + return { success: false, message: data.message || '登录响应异常' } } else { return { success: false, message: data.error, - qrCodeURL: data.qr_code_url, - otpSecret: data.otp_secret, - userID: data.user_id } } } catch (error) { @@ -219,18 +200,36 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { try { const result = await httpClient.post<{ + token: string user_id: string - otp_secret: string - qr_code_url: string + email: string message: string }>('/api/register', requestBody) if (result.success && result.data) { + // Reset 401 flag on successful login + reset401Flag() + + const userInfo = { id: result.data.user_id, email: result.data.email } + setToken(result.data.token) + setUser(userInfo) + localStorage.setItem('auth_token', result.data.token) + localStorage.setItem('auth_user', JSON.stringify(userInfo)) + + // Check and redirect to returnUrl if exists + const returnUrl = sessionStorage.getItem('returnUrl') + if (returnUrl) { + sessionStorage.removeItem('returnUrl') + window.history.pushState({}, '', returnUrl) + window.dispatchEvent(new PopStateEvent('popstate')) + } else { + // 跳转到配置页面 + window.history.pushState({}, '', '/traders') + window.dispatchEvent(new PopStateEvent('popstate')) + } + return { success: true, - userID: result.data.user_id, - otpSecret: result.data.otp_secret, - qrCodeURL: result.data.qr_code_url, message: result.message || result.data.message, } } @@ -252,99 +251,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } } - const verifyOTP = async (userID: string, otpCode: string) => { - try { - const response = await fetch('/api/verify-otp', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ user_id: userID, otp_code: otpCode }), - }) - - const data = await response.json() - - if (response.ok) { - // Reset 401 flag on successful login - reset401Flag() - - // 登录成功,保存token和用户信息 - const userInfo = { id: data.user_id, email: data.email } - setToken(data.token) - setUser(userInfo) - localStorage.setItem('auth_token', data.token) - localStorage.setItem('auth_user', JSON.stringify(userInfo)) - - // Check and redirect to returnUrl if exists - const returnUrl = sessionStorage.getItem('returnUrl') - if (returnUrl) { - sessionStorage.removeItem('returnUrl') - window.history.pushState({}, '', returnUrl) - window.dispatchEvent(new PopStateEvent('popstate')) - } else { - // 跳转到配置页面 - window.history.pushState({}, '', '/traders') - window.dispatchEvent(new PopStateEvent('popstate')) - } - - return { success: true, message: data.message } - } else { - return { success: false, message: data.error } - } - } catch (error) { - return { success: false, message: 'OTP验证失败,请重试' } - } - } - - const completeRegistration = async (userID: string, otpCode: string) => { - try { - const response = await fetch('/api/complete-registration', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ user_id: userID, otp_code: otpCode }), - }) - - const data = await response.json() - - if (response.ok) { - // Reset 401 flag on successful login - reset401Flag() - - // 注册完成,自动登录 - const userInfo = { id: data.user_id, email: data.email } - setToken(data.token) - setUser(userInfo) - localStorage.setItem('auth_token', data.token) - localStorage.setItem('auth_user', JSON.stringify(userInfo)) - - // Check and redirect to returnUrl if exists - const returnUrl = sessionStorage.getItem('returnUrl') - if (returnUrl) { - sessionStorage.removeItem('returnUrl') - window.history.pushState({}, '', returnUrl) - window.dispatchEvent(new PopStateEvent('popstate')) - } else { - // 跳转到配置页面 - window.history.pushState({}, '', '/traders') - window.dispatchEvent(new PopStateEvent('popstate')) - } - - return { success: true, message: data.message } - } else { - return { success: false, message: data.error } - } - } catch (error) { - return { success: false, message: '注册完成失败,请重试' } - } - } - - const resetPassword = async ( - email: string, - newPassword: string, - otpCode: string - ) => { + const resetPassword = async (email: string, newPassword: string) => { try { const response = await fetch('/api/reset-password', { method: 'POST', @@ -354,7 +261,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { body: JSON.stringify({ email, new_password: newPassword, - otp_code: otpCode, }), }) @@ -394,8 +300,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { login, loginAdmin, register, - verifyOTP, - completeRegistration, resetPassword, logout, isLoading, diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 5ed8f262..85433077 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -649,7 +649,6 @@ export const translations = { passwordRuleMatch: 'Passwords match', passwordNotMeetRequirements: 'Password does not meet the security requirements', - otpPlaceholder: '000000', loginTitle: 'Sign in to your account', registerTitle: 'Create a new account', loginButton: 'Sign In', @@ -661,7 +660,6 @@ export const translations = { loginNow: 'Sign in now', forgotPassword: 'Forgot password?', rememberMe: 'Remember me', - otpCode: 'OTP Code', resetPassword: 'Reset Password', resetPasswordTitle: 'Reset your password', newPassword: 'New Password', @@ -671,33 +669,11 @@ export const translations = { 'Password reset successful! Please login with your new password', resetPasswordFailed: 'Password reset failed', backToLogin: 'Back to Login', - scanQRCode: 'Scan QR Code', - enterOTPCode: 'Enter 6-digit OTP code', - verifyOTP: 'Verify OTP', - setupTwoFactor: 'Set up two-factor authentication', - setupTwoFactorDesc: - 'Follow the steps below to secure your account with Google Authenticator', - scanQRCodeInstructions: - 'Scan this QR code with Google Authenticator or Authy', - otpSecret: 'Or enter this secret manually:', - qrCodeHint: 'QR code (if scanning fails, use the secret below):', - authStep1Title: 'Step 1: Install Google Authenticator', - authStep1Desc: - 'Download and install Google Authenticator from your app store', - authStep2Title: 'Step 2: Add account', - authStep2Desc: 'Tap "+", then choose "Scan QR code" or "Enter a setup key"', - authStep3Title: 'Step 3: Verify setup', - authStep3Desc: 'After setup, continue to enter the 6-digit code', - setupCompleteContinue: 'I have completed setup, continue', copy: 'Copy', - completeRegistration: 'Complete Registration', - completeRegistrationSubtitle: 'to complete registration', loginSuccess: 'Login successful', registrationSuccess: 'Registration successful', loginFailed: 'Login failed. Please check your email and password.', registrationFailed: 'Registration failed. Please try again.', - verificationFailed: - 'OTP verification failed. Please check the code and try again.', sessionExpired: 'Session expired, please login again', invalidCredentials: 'Invalid email or password', weak: 'Weak', @@ -1866,7 +1842,6 @@ export const translations = { passwordRuleSpecial: '至少 1 个特殊字符(@#$%!&*?)', passwordRuleMatch: '两次密码一致', passwordNotMeetRequirements: '密码不符合安全要求', - otpPlaceholder: '000000', loginTitle: '登录到您的账户', registerTitle: '创建新账户', loginButton: '登录', @@ -1886,30 +1861,11 @@ export const translations = { resetPasswordSuccess: '密码重置成功!请使用新密码登录', resetPasswordFailed: '密码重置失败', backToLogin: '返回登录', - otpCode: 'OTP验证码', - scanQRCode: '扫描二维码', - enterOTPCode: '输入6位OTP验证码', - verifyOTP: '验证OTP', - setupTwoFactor: '设置双因素认证', - setupTwoFactorDesc: '请按以下步骤设置Google验证器以保护您的账户安全', - scanQRCodeInstructions: '使用Google Authenticator或Authy扫描此二维码', - otpSecret: '或手动输入此密钥:', - qrCodeHint: '二维码(如果无法扫描,请使用下方密钥):', - authStep1Title: '步骤1:下载Google Authenticator', - authStep1Desc: '在手机应用商店下载并安装Google Authenticator应用', - authStep2Title: '步骤2:添加账户', - authStep2Desc: '在应用中点击“+”,选择“扫描二维码”或“手动输入密钥”', - authStep3Title: '步骤3:验证设置', - authStep3Desc: '设置完成后,点击下方按钮输入6位验证码', - setupCompleteContinue: '我已完成设置,继续', copy: '复制', - completeRegistration: '完成注册', - completeRegistrationSubtitle: '以完成注册', loginSuccess: '登录成功', registrationSuccess: '注册成功', loginFailed: '登录失败,请检查您的邮箱和密码。', registrationFailed: '注册失败,请重试。', - verificationFailed: 'OTP 验证失败,请检查验证码后重试。', sessionExpired: '登录已过期,请重新登录', invalidCredentials: '邮箱或密码错误', weak: '弱', @@ -3020,7 +2976,6 @@ export const translations = { passwordRuleSpecial: 'Minimal 1 karakter khusus (@#$%!&*?)', passwordRuleMatch: 'Kata sandi cocok', passwordNotMeetRequirements: 'Kata sandi tidak memenuhi persyaratan keamanan', - otpPlaceholder: '000000', loginTitle: 'Masuk ke akun Anda', registerTitle: 'Buat akun baru', loginButton: 'Masuk', @@ -3032,7 +2987,6 @@ export const translations = { loginNow: 'Masuk sekarang', forgotPassword: 'Lupa kata sandi?', rememberMe: 'Ingat saya', - otpCode: 'Kode OTP', resetPassword: 'Reset Kata Sandi', resetPasswordTitle: 'Reset kata sandi Anda', newPassword: 'Kata Sandi Baru', @@ -3041,29 +2995,11 @@ export const translations = { resetPasswordSuccess: 'Kata sandi berhasil direset! Silakan masuk dengan kata sandi baru', resetPasswordFailed: 'Gagal mereset kata sandi', backToLogin: 'Kembali ke Login', - scanQRCode: 'Pindai Kode QR', - enterOTPCode: 'Masukkan kode OTP 6 digit', - verifyOTP: 'Verifikasi OTP', - setupTwoFactor: 'Atur autentikasi dua faktor', - setupTwoFactorDesc: 'Ikuti langkah-langkah di bawah untuk mengamankan akun Anda dengan Google Authenticator', - scanQRCodeInstructions: 'Pindai kode QR ini dengan Google Authenticator atau Authy', - otpSecret: 'Atau masukkan kunci ini secara manual:', - qrCodeHint: 'Kode QR (jika pemindaian gagal, gunakan kunci di bawah):', - authStep1Title: 'Langkah 1: Instal Google Authenticator', - authStep1Desc: 'Unduh dan instal Google Authenticator dari toko aplikasi', - authStep2Title: 'Langkah 2: Tambahkan akun', - authStep2Desc: 'Ketuk "+", lalu pilih "Pindai kode QR" atau "Masukkan kunci pengaturan"', - authStep3Title: 'Langkah 3: Verifikasi pengaturan', - authStep3Desc: 'Setelah pengaturan, lanjutkan untuk memasukkan kode 6 digit', - setupCompleteContinue: 'Saya telah menyelesaikan pengaturan, lanjutkan', copy: 'Salin', - completeRegistration: 'Selesaikan Pendaftaran', - completeRegistrationSubtitle: 'untuk menyelesaikan pendaftaran', loginSuccess: 'Berhasil masuk', registrationSuccess: 'Berhasil mendaftar', loginFailed: 'Gagal masuk. Periksa email dan kata sandi Anda.', registrationFailed: 'Gagal mendaftar. Silakan coba lagi.', - verificationFailed: 'Verifikasi OTP gagal. Periksa kode dan coba lagi.', sessionExpired: 'Sesi berakhir, silakan masuk kembali', invalidCredentials: 'Email atau kata sandi salah', weak: 'Lemah',