refactor(auth): remove OTP flows from login/register/reset

This commit is contained in:
tinkle-community
2026-03-05 18:55:36 +08:00
parent fa664ccae3
commit 73f1fe105d
11 changed files with 210 additions and 1207 deletions
-252
View File
@@ -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)
})
}
}
+6 -136
View File
@@ -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 {
-35
View File
@@ -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)
}
-1
View File
@@ -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
-2
View File
@@ -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=
-24
View File
@@ -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,
})
}
+3 -185
View File
@@ -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)
// Dismiss the "login expired" toast on successful login.
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
if (expiredToastId) {
toast.dismiss(expiredToastId)
}
// Clear qrCodeURL after successful completion
setQrCodeURL('')
setOtpSecret('')
}
// 成功的话AuthContext会自动处理登录状态
setLoading(false)
}
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
toast.success('Copied to clipboard')
}
return (
<DeepVoidBackground className="min-h-screen flex items-center justify-center py-12 font-mono" disableAnimation>
@@ -172,7 +103,7 @@ export function LoginPage() {
<span className="text-nofx-gold">SYSTEM</span> ACCESS
</h1>
<p className="text-zinc-500 text-xs tracking-[0.2em] uppercase">
{step === 'login' ? 'Authentication Protocol v3.0' : 'Multi-Factor Verification'}
Authentication Protocol v3.0
</p>
</div>
@@ -241,67 +172,7 @@ export function LoginPage() {
{loading ? '> VERIFYING...' : '> EXECUTE_LOGIN'}
</button>
</form>
) : step === 'setup-otp' ? (
<div className="space-y-6">
<div className="text-center bg-zinc-900/50 p-4 rounded border border-zinc-800">
<div className="text-xs font-mono text-zinc-400 mb-2">COMPLETE 2FA CONFIGURATION</div>
{qrCodeURL ? (
<div className="bg-white p-2 rounded inline-block shadow-[0_0_30px_rgba(255,255,255,0.1)]">
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(`otpauth://totp/NoFX:${email}?secret=${otpSecret}&issuer=NoFX`)}`}
alt="QR Code"
className="w-32 h-32"
/>
</div>
) : (
<div className="w-32 h-32 bg-zinc-800 animate-pulse rounded inline-block"></div>
)}
<div className="mt-4">
<p className="text-[10px] text-zinc-500 uppercase tracking-widest mb-1">Backup Secret Key</p>
<div className="flex items-center gap-2 justify-center bg-black/50 p-2 rounded border border-zinc-700/50 max-w-[200px] mx-auto">
<code className="text-xs font-mono text-nofx-gold">{otpSecret}</code>
<button
onClick={() => copyToClipboard(otpSecret)}
className="text-zinc-500 hover:text-white transition-colors"
>
<span className="text-[10px] uppercase border border-zinc-700 px-1 rounded">Copy</span>
</button>
</div>
</div>
</div>
<div className="space-y-4 font-mono text-xs text-zinc-400 bg-black/20 p-4 rounded border border-zinc-800/50">
<div className="flex gap-3 items-start">
<span className="text-nofx-gold font-bold mt-0.5">01</span>
<div>
<p className="font-bold text-white mb-1">Install Authenticator App</p>
<p className="mb-2">Recommended: <span className="text-nofx-gold">Google Authenticator</span>.</p>
<div className="flex gap-2">
<span className="px-1.5 py-0.5 bg-zinc-800 rounded text-[10px] text-zinc-300 border border-zinc-700">iOS</span>
<span className="px-1.5 py-0.5 bg-zinc-800 rounded text-[10px] text-zinc-300 border border-zinc-700">Android</span>
</div>
</div>
</div>
<div className="w-full h-px bg-zinc-800/50"></div>
<div className="flex gap-3 items-start">
<span className="text-nofx-gold font-bold mt-0.5">02</span>
<div>
<p className="font-bold text-white mb-1">Scan & Verify</p>
<p>Scan code above, then enter the 6-digit token below to activate your account.</p>
</div>
</div>
</div>
<button
onClick={() => setStep('otp')}
className="w-full bg-nofx-gold text-black font-bold py-3 px-4 rounded text-sm tracking-wide uppercase hover:bg-yellow-400 transition-colors font-mono shadow-lg"
>
I HAVE SCANNED THE CODE
</button>
</div>
) : step === 'login' ? (
<form onSubmit={handleLogin} className="space-y-5">
<div className="space-y-4">
<div>
@@ -371,59 +242,6 @@ export function LoginPage() {
)}
</button>
</form>
) : (
<form onSubmit={handleOTPVerify} className="space-y-6">
<div className="text-center py-2">
<div className="w-12 h-12 bg-zinc-900 rounded-full flex items-center justify-center mx-auto mb-4 border border-zinc-700 text-2xl">
🔐
</div>
<p className="text-xs text-zinc-400 font-mono leading-relaxed">
{t('scanQRCodeInstructions', language)}<br />
{t('enterOTPCode', language)}
</p>
</div>
<div>
<label className="block text-xs uppercase tracking-wider text-nofx-gold mb-2 text-center font-bold">
{t('otpCode', language)}
</label>
<input
type="text"
value={otpCode}
onChange={(e) =>
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
/>
</div>
{error && (
<div className="text-xs bg-red-500/10 border border-red-500/30 text-red-500 px-3 py-2 rounded font-mono text-center">
[ACCESS DENIED]: {error}
</div>
)}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={() => setStep('login')}
className="flex-1 bg-zinc-900 border border-zinc-700 text-zinc-400 py-3 rounded text-xs font-mono uppercase hover:bg-zinc-800 transition-colors"
>
&lt; ABORT
</button>
<button
type="submit"
disabled={loading || otpCode.length !== 6}
className="flex-1 bg-nofx-gold text-black font-bold py-3 rounded text-xs font-mono uppercase hover:bg-yellow-400 transition-colors disabled:opacity-50"
>
{loading ? 'VERIFYING...' : 'CONFIRM IDENTITY'}
</button>
</div>
</form>
)}
</div>
+25 -204
View File
@@ -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 <RegistrationDisabled />
}
// 如果白名单已满,显示容量已满页面
if (step === 'whitelist-full') {
return <WhitelistFullPage onBack={() => setStep('register')} />
if (view === 'whitelist-full') {
return <WhitelistFullPage onBack={() => 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 (
<DeepVoidBackground className="min-h-screen flex items-center justify-center py-12 font-mono" disableAnimation>
<div className="w-full max-w-lg relative z-10 px-6">
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
<div className="flex justify-between items-center mb-8">
<button
onClick={() => window.location.href = '/'}
onClick={() => (window.location.href = '/')}
className="flex items-center gap-2 text-zinc-500 hover:text-white transition-colors group px-3 py-1.5 rounded border border-transparent hover:border-zinc-700 bg-black/20 backdrop-blur-sm"
>
<div className="w-2 h-2 rounded-full bg-red-500 group-hover:animate-pulse"></div>
@@ -163,38 +118,29 @@ export function RegisterPage() {
</button>
</div>
{/* Terminal Header */}
<div className="mb-8 text-center">
<div className="flex justify-center mb-6">
<div className="relative">
<div className="absolute -inset-2 bg-nofx-gold/20 rounded-full blur-xl animate-pulse"></div>
<img
src="/icons/nofx.svg"
alt="NoFx Logo"
className="w-16 h-16 object-contain relative z-10 opacity-90"
/>
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 object-contain relative z-10 opacity-90" />
</div>
</div>
<h1 className="text-3xl font-bold tracking-tighter text-white uppercase mb-2">
<span className="text-nofx-gold">NEW_USER</span> ONBOARDING
</h1>
<p className="text-zinc-500 text-xs tracking-[0.2em] uppercase">
{step === 'register' && 'Initializing Registration Sequence...'}
{step === 'setup-otp' && 'Configuring Security Protocols...'}
{step === 'verify-otp' && 'Finalizing Authentication...'}
Initializing Registration Sequence...
</p>
</div>
{/* Terminal Output / Form Container */}
<div className="bg-zinc-900/40 backdrop-blur-md border border-zinc-800 rounded-lg overflow-hidden shadow-2xl relative group">
<div className="absolute inset-0 bg-zinc-900/50 opacity-0 group-hover:opacity-100 transition duration-700 pointer-events-none"></div>
{/* Window Bar */}
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900/80 border-b border-zinc-800">
<div className="flex gap-1.5">
<div
className="w-2.5 h-2.5 rounded-full bg-red-500/50 hover:bg-red-500 cursor-pointer transition-colors"
onClick={() => window.location.href = '/'}
onClick={() => (window.location.href = '/')}
title="Close / Return Home"
></div>
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/50"></div>
@@ -206,7 +152,6 @@ export function RegisterPage() {
</div>
<div className="p-6 md:p-8 relative">
{/* Status Output */}
<div className="mb-6 font-mono text-xs space-y-1 text-zinc-500 border-b border-zinc-800/50 pb-4">
<div className="flex gap-2">
<span className="text-emerald-500"></span>
@@ -218,7 +163,6 @@ export function RegisterPage() {
</div>
</div>
{step === 'register' && (
<form onSubmit={handleRegister} className="space-y-5">
<div>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">{t('email', language)}</label>
@@ -339,154 +283,31 @@ export function RegisterPage() {
)}
</button>
</form>
)}
{step === 'setup-otp' && (
<div className="space-y-6">
<div className="text-center bg-zinc-900/50 p-4 rounded border border-zinc-800">
<div className="text-xs font-mono text-zinc-400 mb-2">SCAN_QR_CODE_SEQUENCE</div>
{qrCodeURL ? (
<div className="bg-white p-2 rounded inline-block shadow-[0_0_30px_rgba(255,255,255,0.1)]">
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(`otpauth://totp/NoFX:${email}?secret=${otpSecret}&issuer=NoFX`)}`}
alt="QR Code"
className="w-32 h-32"
/>
</div>
) : (
<div className="w-32 h-32 bg-zinc-800 animate-pulse rounded inline-block"></div>
)}
<div className="mt-4">
<p className="text-[10px] text-zinc-500 uppercase tracking-widest mb-1">Backup Secret Key</p>
<div className="flex items-center gap-2 justify-center bg-black/50 p-2 rounded border border-zinc-700/50 max-w-[200px] mx-auto">
<code className="text-xs font-mono text-nofx-gold">{otpSecret}</code>
<button
onClick={() => copyToClipboard(otpSecret)}
className="text-zinc-500 hover:text-white transition-colors"
>
<span className="text-[10px] uppercase border border-zinc-700 px-1 rounded">Copy</span>
</button>
</div>
</div>
</div>
<div className="space-y-4 font-mono text-xs text-zinc-400 bg-black/20 p-4 rounded border border-zinc-800/50">
<div className="flex gap-3 items-start">
<span className="text-nofx-gold font-bold mt-0.5">01</span>
<div>
<p className="font-bold text-white mb-1">Install Authenticator App</p>
<p className="mb-2">We highly recommend <span className="text-nofx-gold">Google Authenticator</span> for compatibility.</p>
<div className="flex gap-2">
<span className="px-1.5 py-0.5 bg-zinc-800 rounded text-[10px] text-zinc-300 border border-zinc-700">iOS</span>
<span className="px-1.5 py-0.5 bg-zinc-800 rounded text-[10px] text-zinc-300 border border-zinc-700">Android</span>
</div>
</div>
</div>
<div className="w-full h-px bg-zinc-800/50"></div>
<div className="flex gap-3 items-start">
<span className="text-nofx-gold font-bold mt-0.5">02</span>
<div>
<p className="font-bold text-white mb-1">Scan QR Code</p>
<p>Open Google Authenticator, tap the <span className="text-white">+</span> button, and scan the code above.</p>
<p className="text-[10px] text-zinc-500 mt-1 italic">Protocol: Time-Based OTP (TOTP)</p>
</div>
</div>
<div className="w-full h-px bg-zinc-800/50"></div>
<div className="flex gap-3 items-start">
<span className="text-nofx-gold font-bold mt-0.5">03</span>
<div>
<p className="font-bold text-white mb-1">Verify Token</p>
<p>Enter the 6-digit code generated by the app.</p>
<div className="mt-2 p-2 bg-yellow-500/10 border border-yellow-500/20 rounded text-[10px] text-yellow-500/80 flex gap-2 items-start">
<span className="mt-px"></span>
<span>Stuck? Ensure your phone's time is set to "Automatic". Time drift causes codes to fail.</span>
</div>
</div>
</div>
</div>
<button
onClick={handleSetupComplete}
className="w-full bg-nofx-gold text-black font-bold py-3 px-4 rounded text-sm tracking-wide uppercase hover:bg-yellow-400 transition-colors font-mono shadow-lg"
>
PROCEED TO VERIFICATION
</button>
</div>
)}
{step === 'verify-otp' && (
<form onSubmit={handleOTPVerify} className="space-y-6">
<div className="text-center">
<p className="text-xs text-zinc-400 font-mono mb-6">
ENTER 6-DIGIT SECURITY TOKEN TO FINALIZE ONBOARDING
</p>
</div>
<div>
<input
type="text"
value={otpCode}
onChange={(e) =>
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"
maxLength={6}
required
autoFocus
/>
</div>
{error && (
<div className="text-xs bg-red-500/10 border border-red-500/30 text-red-500 px-3 py-2 rounded font-mono text-center">
[VERIFICATION_FAILED]: {error}
</div>
)}
<button
type="submit"
disabled={loading || otpCode.length !== 6}
className="w-full bg-nofx-gold text-black font-bold py-3 px-4 rounded text-sm tracking-wide uppercase hover:bg-yellow-400 transition-colors font-mono shadow-lg disabled:opacity-50"
>
{loading ? 'VALIDATING...' : 'ACTIVATE ACCOUNT'}
</button>
</form>
)}
</div>
{/* Terminal Footer Info */}
<div className="bg-zinc-900/50 p-3 flex justify-between items-center text-[10px] font-mono text-zinc-600 border-t border-zinc-800">
<div>ENCRYPTION: AES-256</div>
<div>SECURE_REGISTRY</div>
</div>
</div>
{/* Login Link */}
{step === 'register' && (
<div className="text-center mt-8 space-y-4">
<p className="text-xs font-mono text-zinc-500">
EXISTING_OPERATOR?{' '}
<button
onClick={() => window.location.href = '/login'}
onClick={() => (window.location.href = '/login')}
className="text-nofx-gold hover:underline hover:text-yellow-300 transition-colors ml-1 uppercase"
>
ACCESS TERMINAL
</button>
</p>
<button
onClick={() => window.location.href = '/'}
onClick={() => (window.location.href = '/')}
className="text-[10px] text-zinc-600 hover:text-red-500 transition-colors uppercase tracking-widest hover:underline decoration-red-500/30 font-mono"
>
[ ABORT_REGISTRATION_RETURN_HOME ]
</button>
</div>
)}
</div>
</DeepVoidBackground>
)
+3 -35
View File
@@ -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)}
</h1>
<p className="text-sm mt-2" style={{ color: '#848E9C' }}>
使 Google Authenticator
使
</p>
</div>
@@ -230,37 +229,6 @@ export function ResetPasswordPage() {
/>
</div>
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('otpCode', language)}
</label>
<div className="text-center mb-3">
<div className="text-3xl">📱</div>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
Google Authenticator 6
</p>
</div>
<input
type="text"
value={otpCode}
onChange={(e) =>
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
/>
</div>
{error && (
<div
className="text-sm px-3 py-2 rounded"
@@ -275,7 +243,7 @@ export function ResetPasswordPage() {
<button
type="submit"
disabled={loading || otpCode.length !== 6 || !passwordValid}
disabled={loading || !passwordValid}
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }}
>
+49 -145
View File
@@ -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,
-64
View File
@@ -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',