From 828bf008b3d62b52e990c5a5b519b20c71454766 Mon Sep 17 00:00:00 2001 From: liberty-rising Date: Tue, 26 Dec 2023 21:56:59 +0100 Subject: [PATCH] create admin user on startup in prod, create change password page that is used whenever a users requires a password update, fix bug where fields are not reset upon successfuly password change --- .../initialization/setup_prod_environment.py | 1 + backend/models/user.py | 6 ++- backend/routes/user_routes.py | 12 ++++-- frontend/src/App.jsx | 2 + .../change-password}/ChangePassword.jsx | 1 + .../change-password/ChangePasswordPage.jsx | 40 ++++++++++++++++++ frontend/src/pages/login/LoginPage.jsx | 11 ++++- frontend/src/pages/user/UserPage.jsx | 42 ++----------------- frontend/src/utils/updateUserPassword.jsx | 40 ++++++++++++++++++ 9 files changed, 112 insertions(+), 43 deletions(-) rename frontend/src/{pages/user => components/change-password}/ChangePassword.jsx (97%) create mode 100644 frontend/src/pages/change-password/ChangePasswordPage.jsx create mode 100644 frontend/src/utils/updateUserPassword.jsx diff --git a/backend/envs/prod/initialization/setup_prod_environment.py b/backend/envs/prod/initialization/setup_prod_environment.py index b1eba04..a4c3059 100644 --- a/backend/envs/prod/initialization/setup_prod_environment.py +++ b/backend/envs/prod/initialization/setup_prod_environment.py @@ -15,6 +15,7 @@ def create_admin_user(): email="admin@docshow.ai", organization_id=1, role="admin", + requires_password_update=True, ) with DatabaseManager() as session: diff --git a/backend/models/user.py b/backend/models/user.py index 74143f5..786aeaf 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -1,6 +1,6 @@ from enum import Enum from pydantic import BaseModel, EmailStr, Field, validator -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.sql import func from typing import Optional @@ -29,6 +29,7 @@ class User(Base): hashed_password = Column(String) organization_id = Column(Integer, ForeignKey("organizations.id"), nullable=True) role = Column(String, nullable=True) + requires_password_update = Column(Boolean, default=False) created_at = Column(DateTime(timezone=True), default=func.now()) refresh_token = Column(Text, nullable=True) @@ -50,6 +51,7 @@ class UserCreate(BaseModel): password: str organization_id: Optional[int] = None role: Optional[str] = None + requires_password_update: Optional[bool] = False @validator("password") def validate_password(cls, v): @@ -85,6 +87,7 @@ class UserOut(BaseModel): email: EmailStr organization_id: Optional[int] role: Optional[str] + requires_password_update: bool class UserRole(str, Enum): @@ -109,6 +112,7 @@ class UserUpdate(BaseModel): username: str organization_id: Optional[int] role: Optional[UserRole] + requires_password_update: Optional[bool] = False class ChangePassword(BaseModel): diff --git a/backend/routes/user_routes.py b/backend/routes/user_routes.py index 2b595cd..70eea99 100644 --- a/backend/routes/user_routes.py +++ b/backend/routes/user_routes.py @@ -25,7 +25,15 @@ async def get_users(current_admin_user: User = Depends(get_current_admin_user)): @user_router.get("/users/me/", response_model=UserOut) async def read_users_me(current_user: User = Depends(get_current_user)): - return current_user + user_out = UserOut( + id=current_user.id, + username=current_user.username, + email=current_user.email, + organization_id=current_user.organization_id, + role=current_user.role, + requires_password_update=current_user.requires_password_update, + ) + return user_out @user_router.get("/users/roles/") @@ -57,10 +65,8 @@ async def update_user( 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: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 42745c0..507186f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -15,6 +15,7 @@ import AboutPage from './pages/about/AboutPage'; import AdminPage from './pages/admin/AdminPage'; import AnalyticsPage from './pages/analytics/AnalyticsPage'; import BlogPage from './pages/blog/BlogPage'; +import ChangePasswordPage from './pages/change-password/ChangePasswordPage'; import CreateChartPage from './pages/charts/CreateChartPage'; import CreateDashboardPage from './pages/dashboards/CreateDashboard'; import DashboardMenuPage from './pages/dashboards/DashboardsMenuPage'; @@ -60,6 +61,7 @@ function App() { element={isAuthenticated ? : } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/user/ChangePassword.jsx b/frontend/src/components/change-password/ChangePassword.jsx similarity index 97% rename from frontend/src/pages/user/ChangePassword.jsx rename to frontend/src/components/change-password/ChangePassword.jsx index 86a5418..91b1a24 100644 --- a/frontend/src/pages/user/ChangePassword.jsx +++ b/frontend/src/components/change-password/ChangePassword.jsx @@ -9,6 +9,7 @@ const ChangePassword = ({ handleChangePassword, errorMessage, successMessage }) const handleSubmit = async (event) => { event.preventDefault(); const success = await handleChangePassword(oldPassword, newPassword, confirmPassword); + console.log('handleChangePassword result:', success); if (success) { setOldPassword(''); setNewPassword(''); diff --git a/frontend/src/pages/change-password/ChangePasswordPage.jsx b/frontend/src/pages/change-password/ChangePasswordPage.jsx new file mode 100644 index 0000000..6858164 --- /dev/null +++ b/frontend/src/pages/change-password/ChangePasswordPage.jsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react' +import { useNavigate } from 'react-router-dom'; +import { Box, Container, Typography } from '@mui/material' +import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; +import ChangePassword from '../../components/change-password/ChangePassword'; +import { updateUserPassword } from '../../utils/updateUserPassword'; + +function ChangePasswordPage() { + const navigate = useNavigate(); + const [errorMessage, setErrorMessage] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + + const handleChangePassword = async (oldPassword, newPassword, confirmPassword) => { + const success = await updateUserPassword(oldPassword, newPassword, confirmPassword, setErrorMessage, setSuccessMessage); + if (success) { + navigate('/dashboards'); + } + }; + + return ( + + + + {/* + Change Password + */} + + + + ) +} + +export default ChangePasswordPage \ No newline at end of file diff --git a/frontend/src/pages/login/LoginPage.jsx b/frontend/src/pages/login/LoginPage.jsx index 4e31375..e7faecf 100644 --- a/frontend/src/pages/login/LoginPage.jsx +++ b/frontend/src/pages/login/LoginPage.jsx @@ -33,7 +33,16 @@ function LoginPage({ onLogin }) { if (response.status === 200) { updateAuth(true); - navigate('/dashboards'); + const userResponse = await axios.get(`${API_URL}users/me/`, { + headers: { + 'Authorization': `Bearer ${response.data.access_token}` + } + }); + if (userResponse.data.requires_password_update) { + navigate('/change-password'); + } else { + navigate('/dashboards'); + } } } catch (error) { if (error.response && error.response.status === 401) { diff --git a/frontend/src/pages/user/UserPage.jsx b/frontend/src/pages/user/UserPage.jsx index 9188274..dab0f49 100644 --- a/frontend/src/pages/user/UserPage.jsx +++ b/frontend/src/pages/user/UserPage.jsx @@ -4,9 +4,10 @@ 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 { Alert, Box, CircularProgress, Grid, Typography } from '@mui/material'; import InfoCard from './InfoCard'; -import ChangePassword from './ChangePassword'; +import ChangePassword from '../../components/change-password/ChangePassword'; +import { updateUserPassword } from '../../utils/updateUserPassword'; import { API_URL } from '../../utils/constants'; const UserPage = () => { @@ -43,42 +44,7 @@ const UserPage = () => { }, []); 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 await updateUserPassword(oldPassword, newPassword, confirmPassword, setErrorMessage, setSuccessMessage); }; return ( diff --git a/frontend/src/utils/updateUserPassword.jsx b/frontend/src/utils/updateUserPassword.jsx new file mode 100644 index 0000000..964b293 --- /dev/null +++ b/frontend/src/utils/updateUserPassword.jsx @@ -0,0 +1,40 @@ +import axios from 'axios'; +import { API_URL } from './constants'; + +export const updateUserPassword = async (oldPassword, newPassword, confirmPassword, setErrorMessage, setSuccessMessage) => { + if (newPassword !== confirmPassword) { + setErrorMessage('Passwords do not match!'); + return false; + } + + try { + const response = await axios.put(`${API_URL}users/change-password/`, { old_password: oldPassword, new_password: newPassword }); + + console.log('Server response status:', response.status); // Add this line + + if (response.status === 200) { + console.log('Password changed successfully') + setSuccessMessage('Password changed successfully'); + setErrorMessage(''); + return true; + } else { + setErrorMessage('Failed to change password!'); + setSuccessMessage(''); + 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') { + setErrorMessage(errorMessage); + } + } + } + } + setErrorMessage(errorMessage); + setSuccessMessage(''); + return false; +}; \ No newline at end of file