diff --git a/backend/routes/user.py b/backend/routes/user.py index 4ee7bcb..4dc0fbc 100755 --- a/backend/routes/user.py +++ b/backend/routes/user.py @@ -20,13 +20,7 @@ def get_profile(): user = User.query.get(user_id) if not user: return jsonify({"msg": "User not found"}), 404 - return jsonify({ - "id": user.id, - "username": user.username, - "email": user.email, - "created_at": user.created_at.isoformat(), - "is_verified": user.is_verified - }), 200 + return jsonify(user.to_dict()), 200 # UPDATE USER PROFILE @user_bp.route('/profile', methods=['PUT']) @@ -99,13 +93,7 @@ def get_user_profile(username): user = User.query.filter_by(username=username).first() if not user: return jsonify({"msg": "User not found"}), 404 - return jsonify({ - "id": user.id, - "username": user.username, - "email": user.email, - "created_at": user.created_at.isoformat(), - "is_verified": user.is_verified - }), 200 + return jsonify(user.to_dict()), 200 # GET ALL USERS (with pagination and search) @user_bp.route('/users', methods=['GET']) diff --git a/frontend/src/components/features/auth/components/RegisterModal.tsx b/frontend/src/components/features/auth/components/RegisterModal.tsx index 5a79354..bc84d1c 100644 --- a/frontend/src/components/features/auth/components/RegisterModal.tsx +++ b/frontend/src/components/features/auth/components/RegisterModal.tsx @@ -10,6 +10,7 @@ import { colors, shadows, transitions } from '../../../../theme/colors' import { StyledModal } from '../../../common/StyledModal' import { PrimaryButton, SecondaryButton } from '../../../common/StyledButton' import StyledAlert from '../../../common/StyledAlert' +import PasswordStrengthMeter from '../../../common/PasswordStrengthMeter' import { useLocalStorage } from '../../../../hooks/useLocalStorage' import logger from '../../../../utils/logger' import { getErrorMessage, isErrorStatus } from '../../../../utils/errors' @@ -75,67 +76,6 @@ const SwitchLink = styled.button` } ` -const PasswordStrengthMeter = styled.div` - margin-top: 0.5rem; - margin-bottom: 1rem; -` - -const StrengthBar = styled.div<{ strength: number }>` - height: 4px; - background: ${props => { - if (props.strength === 0) return 'rgba(255, 255, 255, 0.1)'; - if (props.strength === 1) return colors.danger; // Weak - red - if (props.strength === 2) return '#ffa500'; // Fair - orange - if (props.strength === 3) return colors.success; // Good - green - if (props.strength === 4) return colors.success; // Strong - green - return 'rgba(255, 255, 255, 0.1)'; - }}; - width: ${props => (props.strength / 4) * 100}%; - transition: ${transitions.default}; - border-radius: 2px; -` - -const StrengthBarContainer = styled.div` - width: 100%; - height: 4px; - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; - overflow: hidden; -` - -const StrengthText = styled.div<{ strength: number }>` - font-size: 0.875rem; - margin-top: 0.25rem; - color: ${props => { - if (props.strength === 0) return colors.text.muted; - if (props.strength === 1) return colors.danger; - if (props.strength === 2) return '#ffa500'; - if (props.strength === 3) return colors.success; - if (props.strength === 4) return colors.success; - return colors.text.muted; - }}; -` - -const PasswordRequirements = styled.ul` - list-style: none; - padding: 0; - margin: 0.5rem 0 0 0; - font-size: 0.75rem; - - li { - color: ${colors.text.muted}; - margin-bottom: 0.25rem; - - &.met { - color: ${colors.success}; - } - - &::before { - content: '• '; - margin-right: 0.25rem; - } - } -` const RegisterModal = () => { const [searchParams, setSearchParams] = useSearchParams() @@ -163,46 +103,6 @@ const RegisterModal = () => { const show = searchParams.get('register') === 'true' - // Calculate password strength (matches cpta_app logic) - const calculatePasswordStrength = (pwd: string) => { - const requirements = { - minLength8: pwd.length >= 8, - minLength12: pwd.length >= 12, - uppercase: /[A-Z]/.test(pwd), - lowercase: /[a-z]/.test(pwd), - number: /\d/.test(pwd), - special: /[!@#$%^&*(),.?":{}|<>]/.test(pwd), - } - - // If less than 8 chars, weak - if (pwd.length < 8) { - return { strength: 1, label: 'Weak', requirements, isValid: false } - } - - // If 12+ characters, automatically strong - if (requirements.minLength12) { - return { strength: 4, label: 'Strong', requirements, isValid: true } - } - - // Between 8-11 characters - check complexity - const complexityCount = [ - requirements.uppercase, - requirements.lowercase, - requirements.number, - requirements.special - ].filter(Boolean).length - - if (complexityCount === 4) { - return { strength: 3, label: 'Good', requirements, isValid: true } - } else if (complexityCount >= 2) { - return { strength: 2, label: 'Fair', requirements, isValid: false } - } else { - return { strength: 1, label: 'Weak', requirements, isValid: false } - } - } - - const { strength: passwordStrength, label, requirements } = calculatePasswordStrength(password) - // Countdown timer for rate limiting useEffect(() => { if (!rateLimitedUntil) { @@ -468,32 +368,7 @@ const RegisterModal = () => { - {password && ( - - - - - - Password Strength: {label} - - {passwordStrength < 3 && ( - -
  • - 12+ characters (recommended) -
  • - {!requirements.minLength12 && ( - <> -
  • At least 8 characters
  • -
  • One uppercase letter
  • -
  • One lowercase letter
  • -
  • One number
  • -
  • One special character (!@#$%^&*)
  • - - )} -
    - )} -
    - )} + diff --git a/frontend/src/components/features/auth/pages/ResetPasswordPage.tsx b/frontend/src/components/features/auth/pages/ResetPasswordPage.tsx index 4cf6f52..5e2a5aa 100644 --- a/frontend/src/components/features/auth/pages/ResetPasswordPage.tsx +++ b/frontend/src/components/features/auth/pages/ResetPasswordPage.tsx @@ -1,39 +1,30 @@ -import { useState } from 'react' -import { useSearchParams, useNavigate, Link } from 'react-router-dom' +import React, { useState } from 'react' import { Container, Form, Card } from 'react-bootstrap' +import { Link, useSearchParams, useNavigate } from 'react-router-dom' import styled from 'styled-components' import { authAPI } from '../../../../services/api' -import { colors, gradients, shadows } from '../../../../theme/colors' import logger from '../../../../utils/logger' import { getErrorMessage } from '../../../../utils/errors' import StyledAlert from '../../../common/StyledAlert' import PasswordStrengthMeter from '../../../common/PasswordStrengthMeter' +import { SubmitButton } from '../../../common/StyledButton' import { PasswordInput } from '../../../common/PasswordInput' -import { PrimaryButton } from '../../../common/StyledButton' -import Footer from '../../../layout/Footer' +import { colors, gradients } from '../../../../theme/colors' const PageWrapper = styled.div` min-height: 100vh; background: linear-gradient(135deg, ${colors.backgroundDark} 0%, ${colors.background} 100%); display: flex; - flex-direction: column; - padding-top: 70px; /* Account for navbar */ -` - -const ContentWrapper = styled.div` - flex: 1; - display: flex; align-items: center; padding: 40px 0; ` const StyledCard = styled(Card)` - background: ${colors.backgroundAlt}; - border: 1px solid ${colors.borderLight}; - box-shadow: ${shadows.large}; + background: ${colors.background}; + border: 1px solid ${colors.primary}; + box-shadow: 0 8px 32px rgba(40, 167, 69, 0.15); max-width: 500px; margin: 0 auto; - border-radius: 12px; .card-header { background: ${gradients.primary}; @@ -41,20 +32,17 @@ const StyledCard = styled(Card)` color: ${colors.text.primary}; padding: 1.5rem; text-align: center; - border-radius: 12px 12px 0 0; h2 { margin: 0; font-size: 1.75rem; font-weight: 600; - color: #000; } .subtitle { margin-top: 0.5rem; font-size: 0.95rem; opacity: 0.9; - color: #000; } } @@ -62,6 +50,31 @@ const StyledCard = styled(Card)` padding: 2rem; } + .form-label { + color: ${colors.text.primary}; + font-weight: 500; + margin-bottom: 0.5rem; + } + + .form-control { + background: ${colors.backgroundLight}; + border: 1px solid ${colors.borderInput}; + color: ${colors.text.primary}; + padding: 0.75rem; + border-radius: 8px; + + &:focus { + background: ${colors.backgroundLight}; + border-color: ${colors.primary}; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); + color: ${colors.text.primary}; + } + + &::placeholder { + color: ${colors.text.muted}; + } + } + .back-link { color: ${colors.primary}; text-decoration: none; @@ -76,9 +89,15 @@ const StyledCard = styled(Card)` text-decoration: underline; } } + + .password-requirements { + color: ${colors.text.muted}; + font-size: 0.85rem; + margin-top: 0.5rem; + } ` -const ResetPasswordPage = () => { +const ResetPasswordPage: React.FC = () => { const [searchParams] = useSearchParams() const navigate = useNavigate() const token = searchParams.get('token') @@ -92,13 +111,6 @@ const ResetPasswordPage = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError('') - setSuccess(false) - - // Validate token exists - if (!token) { - setError('Invalid or missing reset token') - return - } // Validate passwords match if (password !== confirmPassword) { @@ -108,7 +120,12 @@ const ResetPasswordPage = () => { // Validate password length if (password.length < 8) { - setError('Password must be at least 8 characters') + setError('Password must be at least 8 characters long') + return + } + + if (!token) { + setError('Invalid reset link. Please request a new password reset.') return } @@ -118,140 +135,129 @@ const ResetPasswordPage = () => { await authAPI.resetPassword(token, password) setSuccess(true) - // Redirect to login after 2 seconds + // Redirect to home after 3 seconds setTimeout(() => { - navigate('/?login=true') - }, 2000) + navigate('/') + }, 3000) } catch (error: unknown) { logger.error('Reset password error:', error) - setError(getErrorMessage(error, 'Failed to reset password. The link may have expired.')) + setError(getErrorMessage(error, 'An error occurred. The link may have expired. Please request a new password reset.')) } finally { setLoading(false) } } - // Show error if no token if (!token) { return ( - <> - - - - - -

    Invalid Link

    -
    - - - Invalid Reset Link -
    This password reset link is invalid or has expired. Please request a new one.
    -
    -
    - - - Request New Reset Link - -
    -
    -
    -
    -
    -
    -