diff --git a/backend/src/xfd_django/xfd_api/api_methods/user.py b/backend/src/xfd_django/xfd_api/api_methods/user.py index fbaafc5e5..c30fb47f1 100644 --- a/backend/src/xfd_django/xfd_api/api_methods/user.py +++ b/backend/src/xfd_django/xfd_api/api_methods/user.py @@ -197,7 +197,6 @@ def get_users(current_user): raise HTTPException(status_code=401, detail="Unauthorized") users = User.objects.all().prefetch_related("roles__organization") - # Return the updated user details return [ { @@ -212,6 +211,14 @@ def get_users(current_user): "state": user.state, "user_type": user.user_type, "last_logged_in": user.last_logged_in, + "date_approved": user.date_approved, + "approved_by": { + "id": str(user.approved_by.id), + "full_name": str(user.approved_by.full_name), + "email": str(user.approved_by.email), + } + if user.approved_by + else None, "accepted_terms_version": user.accepted_terms_version, "date_accepted_terms": user.date_accepted_terms, "roles": [ @@ -232,9 +239,11 @@ def get_users(current_user): for user in users ] except HTTPException as http_exc: + print("HTTPException: ", http_exc) raise http_exc except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + print("Error retrieving users: {}".format(e)) + raise Exception(status_code=500, detail=str(e)) # GET: /users/region_id/{region_id} @@ -378,6 +387,7 @@ def get_users_v2(state, region_id, invite_pending, current_user): return [ { "id": str(user.id), + "cognito_use_case_description": user.cognito_use_case_description, "created_at": user.created_at.isoformat(), "updated_at": user.updated_at.isoformat(), "first_name": user.first_name, @@ -388,6 +398,14 @@ def get_users_v2(state, region_id, invite_pending, current_user): "state": user.state, "user_type": user.user_type, "last_logged_in": user.last_logged_in, + "date_approved": user.date_approved, + "approved_by": { + "id": str(user.approved_by.id), + "full_name": str(user.approved_by.full_name), + "email": str(user.approved_by.email), + } + if user.approved_by + else None, "accepted_terms_version": user.accepted_terms_version, "roles": [ { @@ -454,6 +472,10 @@ def update_user_v2(user_id, user_data, current_user): updated_user = User.objects.prefetch_related("roles__organization").get( id=user_id ) + # print("Full updated user: ", vars(updated_user)) + # print("Full user data: ", vars(user_data)) + # print("Full current user: ", vars(current_user)) + # print("Full user id: ", user_id) # Return the updated user details return { @@ -500,6 +522,11 @@ def approve_user_registration(user_id, current_user): try: # Retrieve the user by ID user = User.objects.get(id=user_id) + print("User found: ", vars(user)) + print("Current user: ", vars(current_user)) + user.date_approved = datetime.now() + user.approved_by = current_user + user.save() except ObjectDoesNotExist: raise HTTPException(status_code=404, detail="User not found.") @@ -525,7 +552,10 @@ def approve_user_registration(user_id, current_user): status_code=500, detail="Failed to send email: {}".format(str(e)) ) - return {"status_code": 200, "body": "User registration approved."} + return { + "status_code": 200, + "body": "User registration approved by {}.".format(user.approved_by.email), + } # PUT: /users/{user_id}/register/deny diff --git a/backend/src/xfd_django/xfd_api/schema_models/user.py b/backend/src/xfd_django/xfd_api/schema_models/user.py index 0e9f6e3f9..e6be92a52 100644 --- a/backend/src/xfd_django/xfd_api/schema_models/user.py +++ b/backend/src/xfd_django/xfd_api/schema_models/user.py @@ -37,6 +37,9 @@ class User(BaseModel): email: str invite_pending: bool login_blocked_by_maintenance: bool + cognito_use_case_description: Optional[str] = None + date_approved: Optional[datetime] = None + approved_by: Optional[Dict[str, str]] = None date_accepted_terms: Optional[datetime] accepted_terms_version: Optional[str] last_logged_in: Optional[datetime] @@ -161,6 +164,8 @@ class UpdateUserV2(BaseModel): state: Optional[str] = None user_type: Optional[str] = None invite_pending: Optional[bool] = None + date_approved: Optional[datetime] = None + approved_by: Optional[Any] = None class RegisterUserResponse(BaseModel): @@ -186,6 +191,9 @@ class UserResponseV2(BaseModel): last_name: str full_name: str email: str + cognito_use_case_description: Optional[str] = None + date_approved: Optional[datetime] = None + approved_by: Optional[Dict[str, str]] = None accepted_terms_version: Optional[str] = None date_accepted_terms: Optional[datetime] = None last_logged_in: Optional[datetime] = None diff --git a/backend/src/xfd_django/xfd_mini_dl/models.py b/backend/src/xfd_django/xfd_mini_dl/models.py index 69029aa0d..03be6e75a 100644 --- a/backend/src/xfd_django/xfd_mini_dl/models.py +++ b/backend/src/xfd_django/xfd_mini_dl/models.py @@ -1274,6 +1274,20 @@ class User(AutoLengthCheckModel): default=False, help_text="A boolean field flagging if the user's invite is pending.", ) + date_approved = models.DateTimeField( + db_column="date_approved", + blank=True, + null=True, + help_text="Date the user was approved to have access to the cyhy dashboard.", + ) + approved_by = models.ForeignKey( + "User", + models.DO_NOTHING, + db_column="approved_by_id", + blank=True, + null=True, + help_text="Foreign key to the user who approved the user.", + ) login_blocked_by_maintenance = models.BooleanField( db_column="login_blocked_by_maintenance", default=False, @@ -2740,66 +2754,6 @@ class Meta: unique_together = ["id"] -class CisaKevCatalog(models.Model): - """Define CISA KEV Catalog model.""" - - cisa_kev_uid = models.UUIDField( - primary_key=True, - default=uuid.uuid4, - help_text="PK: Unique identifier for each KEV record.", - ) - cve_id = models.CharField( - max_length=50, - db_index=True, - help_text="CVE ID of the known exploited vulnerability.", - ) - vendor_project = models.CharField( - max_length=255, - help_text="Name of the vendor or project associated with the vulnerability.", - ) - product = models.CharField( - max_length=255, help_text="Name of the affected product." - ) - vulnerability_name = models.CharField( - max_length=255, help_text="Name or brief title of the vulnerability." - ) - date_added = models.DateField( - help_text="Date the vulnerability was added to the KEV catalog." - ) - short_description = models.TextField( - help_text="Brief description of the vulnerability." - ) - required_action = models.TextField( - help_text="Recommended remediation or mitigation action." - ) - due_date = models.DateField( - null=True, blank=True, help_text="Due date for action if provided." - ) - notes = models.TextField( - null=True, blank=True, help_text="Additional notes or remarks." - ) - cwe_id = models.CharField( - max_length=50, - null=True, - blank=True, - help_text="Common Weakness Enumeration (CWE) ID related to the CVE.", - ) - known_ransomware_campaign_use = models.BooleanField( - default=False, - help_text="Flag indicating if the CVE is known to be used in ransomware campaigns.", - ) - vulnerability_publish_date = models.DateField( - null=True, blank=True, help_text="Original publish date of the vulnerability." - ) - - class Meta: - """Set model metadata for Mini Data Lake.""" - - app_label = app_label_name - managed = manage_db - db_table = "cisa_kev_catalog" - - class PortScan(AutoLengthCheckModel): """The PortScan model.""" diff --git a/frontend/src/pages/RegionUsers/RegionUsers.tsx b/frontend/src/pages/RegionUsers/RegionUsers.tsx index a840df43b..54ca8125e 100644 --- a/frontend/src/pages/RegionUsers/RegionUsers.tsx +++ b/frontend/src/pages/RegionUsers/RegionUsers.tsx @@ -52,6 +52,7 @@ export const RegionUsers: React.FC = () => { const { formattedUserType } = useUserLevel(); const getOrgsURL = `/organizations/region_id/`; const getUsersURL = `/v2/users?invite_pending=`; + const pendingCols: GridColDef[] = [ { field: 'full_name', headerName: 'Name', minWidth: 100, flex: 1 }, { field: 'email', headerName: 'Email', minWidth: 100, flex: 2 }, @@ -86,6 +87,12 @@ export const RegionUsers: React.FC = () => { ); } + }, + { + field: 'cognito_use_case_description', + headerName: 'Use Case', + minWidth: 100, + flex: 1.5 } ]; const memberCols: GridColDef[] = [ diff --git a/frontend/src/pages/Users/UserForm.tsx b/frontend/src/pages/Users/UserForm.tsx index 043d54aea..cd7c7fba8 100644 --- a/frontend/src/pages/Users/UserForm.tsx +++ b/frontend/src/pages/Users/UserForm.tsx @@ -42,6 +42,8 @@ interface UserType extends User { dateToUSigned?: string | null | undefined; orgs?: string | null | undefined; full_name: string; + date_approved?: string | null | undefined; + approved_by_id?: string | null | undefined; } type CloseReason = 'backdropClick' | 'escapeKeyDown' | 'closeButtonClick'; diff --git a/frontend/src/pages/Users/Users.tsx b/frontend/src/pages/Users/Users.tsx index da82db3e6..e5cc12cd6 100644 --- a/frontend/src/pages/Users/Users.tsx +++ b/frontend/src/pages/Users/Users.tsx @@ -37,10 +37,15 @@ type ApiErrorStates = { getOrgsError: string; }; -export interface ApiResponse { - result: User[]; - count: number; - url?: string; +interface ApprovedBy { + id: string; + full_name: string; + first_name: string; + last_name: string; + email: string; + user_type: string; + region_id: string; + state: string; } interface UserType extends User { @@ -48,6 +53,8 @@ interface UserType extends User { dateToUSigned?: string | null | undefined; orgs?: string | null | undefined; full_name: string; + approved_by?: ApprovedBy | null; + date_approved?: string | null; } export const Users: React.FC = () => { @@ -116,6 +123,28 @@ export const Users: React.FC = () => { flex: 1 }, { field: 'user_type', headerName: 'User Type', minWidth: 100, flex: 0.75 }, + { + field: 'date_approved', + headerName: 'Approval Date', + minWidth: 100, + flex: 1, + sortComparator: (v1, v2) => { + if (v1 === 'None') return -1; + if (v2 === 'None') return 1; + + const date1 = new Date(v1); + const date2 = new Date(v2); + return date1.getTime() - date2.getTime(); + } + }, + { + field: 'approved_by', + headerName: 'Approved By', + minWidth: 100, + flex: 0.75, + valueGetter: (params) => + params.row.approved_by ? params.row.approved_by.full_name : 'None' + }, { field: 'dateToUSigned', headerName: 'Date ToU Signed',