diff --git a/backend/databases/user_manager.py b/backend/databases/user_manager.py index 2ff741d..e5fd03b 100644 --- a/backend/databases/user_manager.py +++ b/backend/databases/user_manager.py @@ -175,3 +175,21 @@ def delete_user(self, user_id: int): self.db_session.delete(db_user) self.db_session.commit() return db_user + + def update_user_password(self, username: str, new_hashed_password: str): + """ + Update a user's password in the database. + + Args: + old_password (str): The user's current password. + new_password (str): The user's new password. + + Returns: + User: The updated User object if found, else None. + """ + db_user = self.db_session.query(User).filter(User.username == username).first() + if db_user: + db_user.hashed_password = new_hashed_password + self.db_session.commit() + self.db_session.refresh(db_user) + return db_user diff --git a/backend/models/user.py b/backend/models/user.py index a647934..21e2aca 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -1,9 +1,11 @@ from enum import Enum -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, Field, validator from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.sql import func from typing import Optional +import re + from .base import Base @@ -95,3 +97,18 @@ class UserUpdate(BaseModel): username: str organization_id: Optional[int] role: Optional[UserRole] + + +class ChangePassword(BaseModel): + old_password: str = Field(...) + new_password: str = Field(...) + + @validator("new_password") + def validate_new_password(cls, v): + if len(v) < 8: + raise ValueError("Password should have at least 8 characters") + if not re.search(r"\d", v): + raise ValueError("Password should contain at least one digit") + if not re.search(r"\W", v): + raise ValueError("Password should contain at least one symbol") + return v diff --git a/backend/routes/user_routes.py b/backend/routes/user_routes.py index cd9da2e..2b595cd 100644 --- a/backend/routes/user_routes.py +++ b/backend/routes/user_routes.py @@ -1,7 +1,12 @@ from fastapi import APIRouter, Depends, HTTPException -from security import get_current_admin_user, get_current_user +from security import ( + get_current_admin_user, + get_current_user, + get_password_hash, + verify_password, +) -from models.user import User, UserOut, UserRole, UserUpdate +from models.user import ChangePassword, User, UserOut, UserRole, UserUpdate from databases.database_manager import DatabaseManager from databases.user_manager import UserManager @@ -46,3 +51,31 @@ async def update_user( raise HTTPException(status_code=404, detail="User not found") return {"message": f"Successfully updated details for {user_data.username}."} + + +@user_router.put("/users/change-password/") +async def change_user_password( + change_password: ChangePassword, current_user: User = Depends(get_current_user) +): + print("change_password", change_password) + # Verify old password + if not verify_password(change_password.old_password, current_user.hashed_password): + print("HELLO") + raise HTTPException(status_code=400, detail="Invalid old password") + + with DatabaseManager() as session: + user_manager = UserManager(session) + + # Update user password + new_hashed_password = get_password_hash(change_password.new_password) + updated_user = user_manager.update_user_password( + username=current_user.username, + new_hashed_password=new_hashed_password, + ) + + if not updated_user: + raise HTTPException(status_code=404, detail="User not found") + + return { + "message": f"Successfully updated password for {updated_user.username}." + } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c58ecf1..479be4e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -27,7 +27,7 @@ import LoginPage from './pages/Login'; import PricingPage from './pages/Pricing'; import RegisterPage from './pages/Register'; import UploadPage from './pages/upload/Upload'; -import UserPage from './pages/User'; +import UserPage from './pages/user/UserPage'; import Logout from './pages/Logout'; function AppWrapper() { diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index b170e94..1c96213 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -22,7 +22,6 @@ function LoginPage({ onLogin }) { // Determine if usernameOrEmail should be sent as username or email const isEmail = validator.isEmail(usernameOrEmail); - console.log(isEmail) const data = isEmail ? { email: usernameOrEmail, password } : { username: usernameOrEmail, password }; try { diff --git a/frontend/src/pages/User.jsx b/frontend/src/pages/User.jsx deleted file mode 100644 index df4d1b6..0000000 --- a/frontend/src/pages/User.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import axios from 'axios'; -import Typography from '@mui/material/Typography'; -import Box from '@mui/material/Box'; -import Grid from '@mui/material/Grid'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CircularProgress from '@mui/material/CircularProgress'; -import Alert from '@mui/material/Alert'; -import EmailIcon from '@mui/icons-material/Email'; -import BusinessIcon from '@mui/icons-material/Business'; -import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; -import { API_URL } from '../utils/constants'; - -const UserPage = () => { - const [userData, setUserData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [organizationData, setOrganizationData] = useState(null); - - useEffect(() => { - setIsLoading(true); - const fetchData = async () => { - try { - const userResponse = await axios.get(`${API_URL}users/me/`); - const userData = userResponse.data; - setUserData(userData); - - // Fetch organization data if organization_id is present - if (userData.organization_id) { - const orgResponse = await axios.get(`${API_URL}organization/`, { - params: { org_id: userData.organization_id } - }); - setOrganizationData(orgResponse.data); - } - } catch (error) { - setError(error.response ? error.response.data.message : error.message); - } finally { - setIsLoading(false); - } - }; - - fetchData(); - }, []); - - return ( - - - 👤 User Panel - - - {isLoading ? ( - - ) : error ? ( - {error} - ) : userData ? ( - - {/* Username Card */} - - - - - Username - {userData.username} - - - - - {/* Email Card */} - - - - - Email - {userData.email} - - - - - {/* Organization Card */} - - - - - Organization - {organizationData.name} - - - - - {/* Role Card */} - - - - - Role - {userData.role} - - - - {/* Additional Cards or Components */} - - ) : ( - User details not available. - )} - - ); -}; - -export default UserPage; diff --git a/frontend/src/pages/user/ChangePassword.jsx b/frontend/src/pages/user/ChangePassword.jsx new file mode 100644 index 0000000..86a5418 --- /dev/null +++ b/frontend/src/pages/user/ChangePassword.jsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { Alert, Box, Button, Grid, Paper, Snackbar, TextField, Typography } from '@mui/material'; + +const ChangePassword = ({ handleChangePassword, errorMessage, successMessage }) => { + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + const handleSubmit = async (event) => { + event.preventDefault(); + const success = await handleChangePassword(oldPassword, newPassword, confirmPassword); + if (success) { + setOldPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } + }; + + return ( + + + + Change Password + setOldPassword(e.target.value)} + /> + setNewPassword(e.target.value)} + /> + setConfirmPassword(e.target.value)} + /> + {successMessage && + + {successMessage} + + } + {errorMessage && + + {errorMessage} + + } + + + + + ); +}; + +export default ChangePassword; \ No newline at end of file diff --git a/frontend/src/pages/user/InfoCard.jsx b/frontend/src/pages/user/InfoCard.jsx new file mode 100644 index 0000000..aebf828 --- /dev/null +++ b/frontend/src/pages/user/InfoCard.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; + +const InfoCard = ({ Icon, title, content }) => ( + + + + + {title} + {content} + + + +); + +export default InfoCard; \ No newline at end of file diff --git a/frontend/src/pages/user/UserPage.jsx b/frontend/src/pages/user/UserPage.jsx new file mode 100644 index 0000000..9188274 --- /dev/null +++ b/frontend/src/pages/user/UserPage.jsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import EmailIcon from '@mui/icons-material/Email'; +import BusinessIcon from '@mui/icons-material/Business'; +import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import { Alert, Box, CircularProgress, Grid, Snackbar, Typography } from '@mui/material'; +import InfoCard from './InfoCard'; +import ChangePassword from './ChangePassword'; +import { API_URL } from '../../utils/constants'; + +const UserPage = () => { + const [userData, setUserData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [organizationData, setOrganizationData] = useState(null); + const [errorMessage, setErrorMessage] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + + useEffect(() => { + setIsLoading(true); + const fetchData = async () => { + try { + const userResponse = await axios.get(`${API_URL}users/me/`); + const userData = userResponse.data; + setUserData(userData); + + // Fetch organization data if organization_id is present + if (userData.organization_id) { + const orgResponse = await axios.get(`${API_URL}organization/`, { + params: { org_id: userData.organization_id } + }); + setOrganizationData(orgResponse.data); + } + } catch (error) { + setError(error.response ? error.response.data.message : error.message); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, []); + + const handleChangePassword = async (oldPassword, newPassword, confirmPassword) => { + if (newPassword !== confirmPassword) { + setErrorMessage('Passwords do not match!'); + return; + } + + try { + const response = await axios.put(`${API_URL}users/change-password/`, { old_password: oldPassword, new_password: newPassword }); + + if (response.status === 200) { + setSuccessMessage('Password changed successfully'); + setErrorMessage(''); + return true; + } else { + setErrorMessage('Failed to change password!'); + return false; + } + } catch (error) { + if (error.response) { + if (error.response.status === 400) { + setErrorMessage(error.response.data.detail); + } else if (error.response.data && error.response.data.detail && error.response.data.detail[0]) { + let errorMessage = error.response.data.detail[0].msg; + if (typeof errorMessage === 'string') { + errorMessage = errorMessage.replace('Value error, ', ''); + setErrorMessage(errorMessage); + } + } + } else if (error.request) { + // The request was made but no response was received + console.log(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.log('Error', error.message) + } + return false; + } + }; + + return ( + + + 👤 User Panel + + + {isLoading ? ( + + ) : error ? ( + {error} + ) : userData ? ( + + + + + + + ) : ( + User details not available. + )} + + + + ); +}; + +export default UserPage;