feat: Add admin user, PIN-based password reset, and profile management
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Auto-create admin user on startup with configurable credentials - Force password change on first admin login - PIN-based password reset via email (6-digit code) - Remove demo account notice from login page - Add complete profile edit with email, phone, address fields - Add password change functionality in profile - Add database migration for new user fields - Update Helm values with admin and email config
This commit is contained in:
parent
548711b68d
commit
417b2ef877
@ -9,6 +9,18 @@ class Settings(BaseSettings):
|
||||
access_token_expire_minutes: int = 30
|
||||
frontend_url: str = "http://localhost:5173"
|
||||
|
||||
# Admin user credentials (created on first startup)
|
||||
admin_email: str = "admin@brand-master.com"
|
||||
admin_password: str = "admin123" # Change via ADMIN_PASSWORD env var
|
||||
admin_full_name: str = "System Administrator"
|
||||
|
||||
# Email configuration for password reset
|
||||
smtp_host: str = "smtp.gmail.com"
|
||||
smtp_port: int = 587
|
||||
smtp_username: str = ""
|
||||
smtp_password: str = ""
|
||||
smtp_from: str = "noreply@brand-master.com"
|
||||
|
||||
class Config:
|
||||
env_file = str(Path(__file__).parent.parent / ".env")
|
||||
|
||||
|
||||
@ -21,6 +21,38 @@ uploads_dir.mkdir(exist_ok=True)
|
||||
# Create tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Create admin user if doesn't exist
|
||||
def create_admin_user():
|
||||
from app.database.database import SessionLocal
|
||||
from app.models.user import User
|
||||
from app.services.auth import get_password_hash
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
admin = db.query(User).filter(User.email == settings.admin_email).first()
|
||||
if not admin:
|
||||
admin = User(
|
||||
email=settings.admin_email,
|
||||
full_name=settings.admin_full_name,
|
||||
hashed_password=get_password_hash(settings.admin_password),
|
||||
is_admin=True,
|
||||
is_active=True,
|
||||
must_change_password=True # Force password change on first login
|
||||
)
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
print(f"✅ Admin user created: {settings.admin_email}")
|
||||
print(f"⚠️ Default password: {settings.admin_password} (CHANGE THIS!)")
|
||||
else:
|
||||
print(f"ℹ️ Admin user already exists: {settings.admin_email}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating admin user: {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
create_admin_user()
|
||||
|
||||
app = FastAPI(
|
||||
title="E-commerce API",
|
||||
description="Full-featured e-commerce API for clothing and shoes",
|
||||
|
||||
@ -25,6 +25,9 @@ class User(Base):
|
||||
country = Column(String, nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
must_change_password = Column(Boolean, default=False)
|
||||
password_reset_pin = Column(String, nullable=True)
|
||||
pin_expires_at = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
cart = relationship("Cart", back_populates="user", uselist=False)
|
||||
|
||||
@ -2,14 +2,18 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import timedelta, datetime
|
||||
from pydantic import BaseModel, EmailStr
|
||||
import random
|
||||
import string
|
||||
from app.database.database import get_db
|
||||
from app.models import User
|
||||
from app.schemas.user import UserCreate, UserResponse
|
||||
from app.schemas.user import UserCreate, UserResponse, RequestPinRequest, ResetPasswordWithPinRequest, ChangePasswordRequest
|
||||
from app.services.auth import (
|
||||
authenticate_user,
|
||||
create_access_token,
|
||||
get_password_hash,
|
||||
verify_token,
|
||||
verify_password,
|
||||
get_current_user,
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
@ -130,3 +134,97 @@ def reset_password(request: ResetPasswordRequest, db: Session = Depends(get_db))
|
||||
db.commit()
|
||||
|
||||
return {"message": "Password reset successful"}
|
||||
|
||||
|
||||
@router.post("/request-reset-pin")
|
||||
def request_reset_pin(request: RequestPinRequest, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Send a 6-digit PIN to user's email for password reset.
|
||||
"""
|
||||
user = db.query(User).filter(User.email == request.email).first()
|
||||
if not user:
|
||||
# Don't reveal if email exists
|
||||
return {"message": "If the email exists, a PIN has been sent"}
|
||||
|
||||
# Generate 6-digit PIN
|
||||
pin = ''.join(random.choices(string.digits, k=6))
|
||||
|
||||
# Store PIN with 15 minute expiration
|
||||
user.password_reset_pin = pin
|
||||
user.pin_expires_at = datetime.utcnow() + timedelta(minutes=15)
|
||||
db.commit()
|
||||
|
||||
# TODO: Send PIN via email
|
||||
# For now, print it (REMOVE IN PRODUCTION)
|
||||
print(f"\n✅ Password Reset PIN for {request.email}: {pin}")
|
||||
print(f"Expires at: {user.pin_expires_at}\n")
|
||||
|
||||
return {
|
||||
"message": "If the email exists, a PIN has been sent",
|
||||
"pin": pin # REMOVE IN PRODUCTION - only for testing
|
||||
}
|
||||
|
||||
|
||||
@router.post("/reset-password-with-pin")
|
||||
def reset_password_with_pin(request: ResetPasswordWithPinRequest, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Reset password using the PIN sent to email.
|
||||
"""
|
||||
user = db.query(User).filter(User.email == request.email).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid email or PIN",
|
||||
)
|
||||
|
||||
# Check if PIN exists and not expired
|
||||
if not user.password_reset_pin or not user.pin_expires_at:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No PIN request found. Please request a new PIN.",
|
||||
)
|
||||
|
||||
if datetime.utcnow() > user.pin_expires_at:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="PIN has expired. Please request a new one.",
|
||||
)
|
||||
|
||||
if user.password_reset_pin != request.pin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid email or PIN",
|
||||
)
|
||||
|
||||
# Update password and clear PIN
|
||||
user.hashed_password = get_password_hash(request.new_password)
|
||||
user.password_reset_pin = None
|
||||
user.pin_expires_at = None
|
||||
user.must_change_password = False # Clear forced password change flag
|
||||
db.commit()
|
||||
|
||||
return {"message": "Password reset successful"}
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
def change_password(
|
||||
request: ChangePasswordRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Change password for logged-in user.
|
||||
"""
|
||||
# Verify old password
|
||||
if not verify_password(request.old_password, current_user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Current password is incorrect",
|
||||
)
|
||||
|
||||
# Update to new password
|
||||
current_user.hashed_password = get_password_hash(request.new_password)
|
||||
current_user.must_change_password = False # Clear forced password change flag
|
||||
db.commit()
|
||||
|
||||
return {"message": "Password changed successfully"}
|
||||
|
||||
@ -22,6 +22,16 @@ def update_user_profile(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
update_data = user_update.model_dump(exclude_unset=True) if hasattr(user_update, 'model_dump') else user_update.dict(exclude_unset=True)
|
||||
|
||||
# If email is being updated, check if it's already taken
|
||||
if 'email' in update_data and update_data['email'] != current_user.email:
|
||||
existing_user = db.query(User).filter(User.email == update_data['email']).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already in use"
|
||||
)
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(current_user, field, value)
|
||||
|
||||
@ -29,14 +39,6 @@ def update_user_profile(
|
||||
db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
update_data2 = user_update.model_dump(exclude_unset=True) if hasattr(user_update, 'model_dump') else user_update.dict(exclude_unset=True)
|
||||
for field, value in update_data2.items():
|
||||
setattr(user, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
def get_user_by_id(user_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
@ -19,6 +19,22 @@ class UserUpdate(BaseModel):
|
||||
city: Optional[str] = None
|
||||
postal_code: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
class RequestPinRequest(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
class ResetPasswordWithPinRequest(BaseModel):
|
||||
email: EmailStr
|
||||
pin: str
|
||||
new_password: str
|
||||
|
||||
|
||||
class UserResponse(UserBase):
|
||||
@ -30,6 +46,7 @@ class UserResponse(UserBase):
|
||||
country: Optional[str]
|
||||
is_active: bool
|
||||
is_admin: bool
|
||||
must_change_password: bool = False
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
|
||||
11
backend/migrations/005_add_password_reset_fields.sql
Normal file
11
backend/migrations/005_add_password_reset_fields.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- Add password reset fields to user table
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS password_reset_pin VARCHAR(6);
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS pin_expires_at TIMESTAMP;
|
||||
|
||||
-- Update existing admin users to require password change (if any exist)
|
||||
-- This is safe to run multiple times
|
||||
UPDATE "user"
|
||||
SET must_change_password = TRUE
|
||||
WHERE is_admin = TRUE
|
||||
AND must_change_password IS NULL;
|
||||
@ -30,7 +30,7 @@ spec:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: harbor.dvirlabs.com/dockerhub/busybox:1.35
|
||||
image: harbor.dvirlabs.com/base-images/busybox:1.35
|
||||
command: ['sh', '-c', 'until nc -z {{ include "brand-master.fullname" . }}-db-headless {{ .Values.postgres.port | default 5432 }}; do echo waiting for postgres; sleep 2; done;']
|
||||
containers:
|
||||
- name: backend
|
||||
|
||||
@ -28,6 +28,16 @@ backend:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
BACKEND_URL: "https://api-brand-master.dvirlabs.com"
|
||||
FRONTEND_URL: "https://brand-master.dvirlabs.com"
|
||||
# Admin user credentials (change in production!)
|
||||
ADMIN_EMAIL: "admin@brand-master.com"
|
||||
ADMIN_PASSWORD: "admin123" # CHANGE THIS!
|
||||
ADMIN_FULL_NAME: "System Administrator"
|
||||
# Email configuration for password reset (optional)
|
||||
SMTP_HOST: "smtp.gmail.com"
|
||||
SMTP_PORT: "587"
|
||||
SMTP_USERNAME: ""
|
||||
SMTP_PASSWORD: ""
|
||||
SMTP_FROM: "noreply@brand-master.com"
|
||||
|
||||
# JWT Secret Key (IMPORTANT: Change this in production!)
|
||||
jwtSecretKey: "your-secret-key-change-this-in-production"
|
||||
|
||||
@ -16,8 +16,13 @@ export default function Login() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [toast, setToast] = useState(null)
|
||||
const [showForgotPassword, setShowForgotPassword] = useState(false)
|
||||
const [showPinEntry, setShowPinEntry] = useState(false)
|
||||
const [showChangePassword, setShowChangePassword] = useState(false)
|
||||
const [resetEmail, setResetEmail] = useState('')
|
||||
const [resetPin, setResetPin] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [resetLoading, setResetLoading] = useState(false)
|
||||
const [currentUserData, setCurrentUserData] = useState(null)
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
@ -38,10 +43,17 @@ export default function Login() {
|
||||
},
|
||||
})
|
||||
|
||||
// Check if user must change password
|
||||
if (response.data.user.must_change_password) {
|
||||
setCurrentUserData({ token: response.data.access_token, user: response.data.user })
|
||||
setShowChangePassword(true)
|
||||
setToast({ type: 'warning', message: 'You must change your password before continuing' })
|
||||
} else {
|
||||
setToken(response.data.access_token)
|
||||
setUser(response.data.user)
|
||||
setToast({ type: 'success', message: 'Login successful!' })
|
||||
setTimeout(() => navigate('/'), 1000)
|
||||
}
|
||||
} catch (error) {
|
||||
setToast({ type: 'error', message: 'Invalid email or password' })
|
||||
console.error('Login error:', error)
|
||||
@ -55,18 +67,76 @@ export default function Login() {
|
||||
setResetLoading(true)
|
||||
|
||||
try {
|
||||
await api.post('/auth/forgot-password', { email: resetEmail })
|
||||
setToast({ type: 'success', message: 'Password reset link sent to your email!' })
|
||||
const response = await api.post('/auth/request-reset-pin', { email: resetEmail })
|
||||
setToast({ type: 'success', message: 'A PIN has been sent to your email!' })
|
||||
setShowForgotPassword(false)
|
||||
setResetEmail('')
|
||||
setShowPinEntry(true)
|
||||
// For testing - show the PIN from response (remove in production)
|
||||
if (response.data.pin) {
|
||||
console.log('Reset PIN:', response.data.pin)
|
||||
}
|
||||
} catch (error) {
|
||||
setToast({ type: 'error', message: 'Error sending reset link. Please try again.' })
|
||||
setToast({ type: 'error', message: 'Error sending PIN. Please try again.' })
|
||||
console.error('Forgot password error:', error)
|
||||
} finally {
|
||||
setResetLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetWithPin = async (e) => {
|
||||
e.preventDefault()
|
||||
setResetLoading(true)
|
||||
|
||||
try {
|
||||
await api.post('/auth/reset-password-with-pin', {
|
||||
email: resetEmail,
|
||||
pin: resetPin,
|
||||
new_password: newPassword
|
||||
})
|
||||
setToast({ type: 'success', message: 'Password reset successful! Please login.' })
|
||||
setShowPinEntry(false)
|
||||
setResetEmail('')
|
||||
setResetPin('')
|
||||
setNewPassword('')
|
||||
} catch (error) {
|
||||
setToast({ type: 'error', message: error.response?.data?.detail || 'Invalid PIN or request expired' })
|
||||
console.error('Reset password error:', error)
|
||||
} finally {
|
||||
setResetLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChangePassword = async (e) => {
|
||||
e.preventDefault()
|
||||
setResetLoading(true)
|
||||
|
||||
try {
|
||||
// Set token temporarily for this request
|
||||
const tempToken = currentUserData.token
|
||||
await api.post('/auth/change-password',
|
||||
{
|
||||
old_password: formData.password,
|
||||
new_password: newPassword
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${tempToken}` }
|
||||
}
|
||||
)
|
||||
|
||||
// Login with new credentials
|
||||
setToken(currentUserData.token)
|
||||
setUser(currentUserData.user)
|
||||
setToast({ type: 'success', message: 'Password changed successfully!' })
|
||||
setShowChangePassword(false)
|
||||
setTimeout(() => navigate('/'), 1000)
|
||||
} catch (error) {
|
||||
setToast({ type: 'error', message: error.response?.data?.detail || 'Error changing password' })
|
||||
console.error('Change password error:', error)
|
||||
} finally {
|
||||
setResetLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-container">
|
||||
@ -113,16 +183,9 @@ export default function Login() {
|
||||
<p className="auth-switch">
|
||||
Don't have an account? <Link to="/register">Sign up here</Link>
|
||||
</p>
|
||||
|
||||
<div className="demo-account">
|
||||
<p><strong>Demo Accounts:</strong></p>
|
||||
<p className="admin-demo">
|
||||
Admin: admin@example.com / password123
|
||||
</p>
|
||||
<p>User: user@example.com / password123</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Forgot Password Modal */}
|
||||
<Modal
|
||||
isOpen={showForgotPassword}
|
||||
onClose={() => setShowForgotPassword(false)}
|
||||
@ -130,7 +193,7 @@ export default function Login() {
|
||||
>
|
||||
<form onSubmit={handleForgotPassword}>
|
||||
<p className="modal-description">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
Enter your email address and we'll send you a 6-digit PIN to reset your password.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>Email</label>
|
||||
@ -143,7 +206,71 @@ export default function Login() {
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-full" disabled={resetLoading}>
|
||||
{resetLoading ? 'Sending...' : 'Send Reset Link'}
|
||||
{resetLoading ? 'Sending...' : 'Send PIN'}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* PIN Entry Modal */}
|
||||
<Modal
|
||||
isOpen={showPinEntry}
|
||||
onClose={() => setShowPinEntry(false)}
|
||||
title="Enter PIN"
|
||||
>
|
||||
<form onSubmit={handleResetWithPin}>
|
||||
<p className="modal-description">
|
||||
Enter the 6-digit PIN sent to {resetEmail} and your new password.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>6-Digit PIN</label>
|
||||
<input
|
||||
type="text"
|
||||
value={resetPin}
|
||||
onChange={(e) => setResetPin(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
placeholder="123456"
|
||||
maxLength="6"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Enter new password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-full" disabled={resetLoading}>
|
||||
{resetLoading ? 'Resetting...' : 'Reset Password'}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Force Password Change Modal */}
|
||||
<Modal
|
||||
isOpen={showChangePassword}
|
||||
onClose={() => {}} // Cannot close - must change password
|
||||
title="Change Password Required"
|
||||
>
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<p className="modal-description">
|
||||
For security reasons, you must change your password before continuing.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Enter new password"
|
||||
minLength="6"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-full" disabled={resetLoading}>
|
||||
{resetLoading ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@ -10,14 +10,21 @@ export default function Profile() {
|
||||
const { token, user, setUser } = useContext(AuthContext)
|
||||
const [formData, setFormData] = useState({
|
||||
full_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
city: '',
|
||||
postal_code: '',
|
||||
country: '',
|
||||
})
|
||||
const [passwordData, setPasswordData] = useState({
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [changingPassword, setChangingPassword] = useState(false)
|
||||
const [toast, setToast] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
@ -52,19 +59,46 @@ export default function Profile() {
|
||||
setSaving(true)
|
||||
|
||||
try {
|
||||
const response = await api.put('/users/me', formData, {
|
||||
params: { token },
|
||||
})
|
||||
const response = await api.put('/users/me', formData)
|
||||
setUser(response.data)
|
||||
setToast({ type: 'success', message: 'Profile updated successfully!' })
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error)
|
||||
setToast({ type: 'error', message: 'Error updating profile. Please try again.' })
|
||||
setToast({ type: 'error', message: error.response?.data?.detail || 'Error updating profile. Please try again.' })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasswordChange = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (passwordData.new_password !== passwordData.confirm_password) {
|
||||
setToast({ type: 'error', message: 'New passwords do not match!' })
|
||||
return
|
||||
}
|
||||
|
||||
if (passwordData.new_password.length < 6) {
|
||||
setToast({ type: 'error', message: 'Password must be at least 6 characters!' })
|
||||
return
|
||||
}
|
||||
|
||||
setChangingPassword(true)
|
||||
try {
|
||||
await api.post('/auth/change-password', {
|
||||
old_password: passwordData.old_password,
|
||||
new_password: passwordData.new_password,
|
||||
})
|
||||
setToast({ type: 'success', message: 'Password changed successfully!' })
|
||||
setPasswordData({ old_password: '', new_password: '', confirm_password: '' })
|
||||
} catch (error) {
|
||||
console.error('Error changing password:', error)
|
||||
setToast({ type: 'error', message: error.response?.data?.detail || 'Error changing password. Please try again.' })
|
||||
} finally {
|
||||
setChangingPassword(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="loading">Loading...</div>
|
||||
|
||||
return (
|
||||
@ -100,8 +134,14 @@ export default function Profile() {
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Email (Read-only)</label>
|
||||
<input type="email" value={formData.email} disabled />
|
||||
<label>Email</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
@ -162,6 +202,46 @@ export default function Profile() {
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handlePasswordChange} className="profile-form" style={{ marginTop: '2rem' }}>
|
||||
<h2>Change Password</h2>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Current Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordData.old_password}
|
||||
onChange={(e) => setPasswordData({ ...passwordData, old_password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordData.new_password}
|
||||
onChange={(e) => setPasswordData({ ...passwordData, new_password: e.target.value })}
|
||||
minLength="6"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordData.confirm_password}
|
||||
onChange={(e) => setPasswordData({ ...passwordData, confirm_password: e.target.value })}
|
||||
minLength="6"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn btn-full" disabled={changingPassword}>
|
||||
{changingPassword ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{toast && (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user