diff --git a/backend/app/notifications/constants.py b/backend/app/notifications/constants.py index a7f3de377..364c0c748 100644 --- a/backend/app/notifications/constants.py +++ b/backend/app/notifications/constants.py @@ -3,3 +3,4 @@ TYPE_NEW_ACTIVITY = 1 TYPE_DUPLICATE_ACTIVITY = 2 TYPE_NEW_FOLLOWER_REQUEST = 11 TYPE_NEW_FOLLOWER_REQUEST_ACCEPTED = 12 +TYPE_ADMIN_NEW_SIGN_UP_APPROVAL_REQUEST = 101 diff --git a/backend/app/notifications/utils.py b/backend/app/notifications/utils.py index f8ae18f74..29e1b5fa4 100644 --- a/backend/app/notifications/utils.py +++ b/backend/app/notifications/utils.py @@ -8,7 +8,9 @@ import notifications.constants as notifications_constants import notifications.crud as notifications_crud import notifications.schema as notifications_schema -import users.user.crud as user_crud +import users.user.crud as users_crud +import users.user.models as users_models +import users.user.utils as users_utils import websocket.utils as websocket_utils import websocket.schema as websocket_schema @@ -117,7 +119,7 @@ async def create_new_follower_request_notification( db: Session, ): try: - user = user_crud.get_user_by_id(user_id, db) + user = users_crud.get_user_by_id(user_id, db) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -172,7 +174,7 @@ async def create_accepted_follower_request_notification( db: Session, ): try: - user = user_crud.get_user_by_id(user_id, db) + user = users_crud.get_user_by_id(user_id, db) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -198,9 +200,7 @@ async def create_accepted_follower_request_notification( "message": "NEW_FOLLOWER_REQUEST_ACCEPTED_NOTIFICATION", "notification_id": notification.id, } - await websocket_utils.notify_frontend( - user_id, websocket_manager, json_data - ) + await websocket_utils.notify_frontend(user_id, websocket_manager, json_data) # Return the serialized notification return notification @@ -218,3 +218,49 @@ async def create_accepted_follower_request_notification( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal Server Error", ) from err + + +async def create_admin_new_sign_up_approval_request_notification( + user: users_models.User, + websocket_manager: websocket_schema.WebSocketManager, + db: Session, +): + try: + admins = users_utils.get_admin_users(db) + + # Send notification to all admin users + for admin in admins: + # Create a notification for the new sign up request + notification = notifications_crud.create_notification( + notifications_schema.Notification( + user_id=admin.id, + type=notifications_constants.TYPE_ADMIN_NEW_SIGN_UP_APPROVAL_REQUEST, + options={ + "user_id": user.id, + "user_name": user.name, + "user_username": user.username, + }, + ), + db, + ) + + # Notify the frontend about the new sign up request + json_data = { + "message": "ADMIN_NEW_SIGN_UP_APPROVAL_REQUEST_NOTIFICATION", + "notification_id": notification.id, + } + await websocket_utils.notify_frontend(admin.id, websocket_manager, json_data) + except HTTPException as http_err: + raise http_err + except Exception as err: + # Log the exception + core_logger.print_to_log( + f"Error in create_admin_new_sign_up_approval_request_notification: {err}", + "error", + exc=err, + ) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err diff --git a/backend/app/session/router.py b/backend/app/session/router.py index 9b48da11c..e9c6ce4d3 100644 --- a/backend/app/session/router.py +++ b/backend/app/session/router.py @@ -25,6 +25,8 @@ import users.user_integrations.crud as user_integrations_crud import users.user_default_gear.crud as user_default_gear_crud import users.user_privacy_settings.crud as users_privacy_settings_crud +import notifications.utils as notifications_utils + import health_targets.crud as health_targets_crud import profile.utils as profile_utils @@ -36,6 +38,8 @@ import server_settings.crud as server_settings_crud import core.database as core_database import core.apprise as core_apprise +import websocket.schema as websocket_schema + # Define the API router router = APIRouter() @@ -125,6 +129,10 @@ async def signup( core_apprise.AppriseService, Depends(core_apprise.get_email_service), ], + websocket_manager: Annotated[ + websocket_schema.WebSocketManager, + Depends(websocket_schema.get_websocket_manager), + ], db: Annotated[ Session, Depends(core_database.get_db), @@ -187,6 +195,9 @@ async def signup( response_data["message"] + " Account is pending admin approval." ) response_data["admin_approval_required"] = True + + await sign_up_tokens_utils.send_sign_up_admin_approval_email(created_user, email_service, db) + await notifications_utils.create_admin_new_sign_up_approval_request_notification(created_user, websocket_manager, db) if ( not server_settings.signup_require_email_verification and not server_settings.signup_require_admin_approval diff --git a/backend/app/sign_up_tokens/email_messages.py b/backend/app/sign_up_tokens/email_messages.py index d6bac892d..1c855cf26 100644 --- a/backend/app/sign_up_tokens/email_messages.py +++ b/backend/app/sign_up_tokens/email_messages.py @@ -1,5 +1,6 @@ import core.apprise as core_apprise + def get_signup_confirmation_email_en( user_name: str, signup_link: str, email_service: core_apprise.AppriseService ) -> tuple: @@ -97,4 +98,75 @@ def get_signup_confirmation_email_en( The Endurain team """.strip() - return subject, html_content, text_content \ No newline at end of file + return subject, html_content, text_content + + +def get_admin_signup_notification_email_en( + user_name: str, sign_up_user_name: str, email_service: core_apprise.AppriseService +) -> tuple: + subject = "Endurain - New user sign-up pending approval" + + html_content = f""" + + + + + + + {subject} + + + +
+
+
+ Endurain logo + Endurain +
+

New sign-up requires approval

+
+ +
+

Hello {user_name},

+ +

A new user has signed up and is awaiting approval:

+ +
+ User: {sign_up_user_name} +
+ +

Please log in to the Endurain admin panel to review and approve this request.

+ +
+ Go to Admin Panel +
+
+ +
+

Best regards,
The Endurain system

+

Visit Endurain at: {email_service.frontend_host} - + Source code at: GitHub

+
+
+ + + + """.strip() + + text_content = f""" + Hello {user_name}, + + A new user has signed up and is awaiting approval. + + User: {sign_up_user_name} + + Please log in to the Endurain admin panel to review and approve this request: + {email_service.frontend_host}/settings + + Best regards, + The Endurain system + """.strip() + + return subject, html_content, text_content diff --git a/backend/app/sign_up_tokens/utils.py b/backend/app/sign_up_tokens/utils.py index 9c04e7808..c88b95f58 100644 --- a/backend/app/sign_up_tokens/utils.py +++ b/backend/app/sign_up_tokens/utils.py @@ -13,7 +13,7 @@ import sign_up_tokens.schema as sign_up_tokens_schema import sign_up_tokens.crud as sign_up_tokens_crud import users.user.models as users_models -import users.user.crud as users_crud +import users.user.utils as users_utils import core.apprise as core_apprise import core.logger as core_logger @@ -31,7 +31,8 @@ def create_sign_up_token(user_id: int, db: Session) -> str: user_id=user_id, token_hash=token_hash, created_at=datetime.now(timezone.utc), - expires_at=datetime.now(timezone.utc) + timedelta(hours=1), # 1 hour expiration + expires_at=datetime.now(timezone.utc) + + timedelta(hours=24), # 24 hour expiration used=0, ) @@ -74,6 +75,36 @@ async def send_sign_up_email( ) +async def send_sign_up_admin_approval_email( + user: users_models.User, email_service: core_apprise.AppriseService, db: Session +) -> bool: + # Check if email service is configured + if not email_service.is_configured(): + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Email service is not configured", + ) + + admins = users_utils.get_admin_users(db) + + # Send email to all admin users + for admin in admins: + # use default email message in English + subject, html_content, text_content = ( + sign_up_tokens_email_messages.get_admin_signup_notification_email_en( + admin.name, user.name, email_service + ) + ) + + # Send email + await email_service.send_email( + to_emails=[admin.email], + subject=subject, + html_content=html_content, + text_content=text_content, + ) + + def use_sign_up_token(token: str, db: Session): # Hash the provided token to find the database record token_hash = hashlib.sha256(token.encode()).hexdigest() diff --git a/backend/app/users/user/crud.py b/backend/app/users/user/crud.py index 09cfc9207..a8b7bd1ed 100644 --- a/backend/app/users/user/crud.py +++ b/backend/app/users/user/crud.py @@ -85,7 +85,6 @@ def get_users_with_pagination(db: Session, page_number: int = 1, num_records: in # Return the users return users - except Exception as err: # Log the exception core_logger.print_to_log( @@ -254,6 +253,32 @@ def get_user_by_id_if_is_public(user_id: int, db: Session): ) from err +def get_users_admin(db: Session): + try: + # Get the users from the database and format the birthdate + users = [ + users_utils.format_user_birthdate(user) + for user in db.query(users_models.User) + .filter(users_models.User.access_type == 2) + .all() + ] + + # If the users were not found, return None + if not users: + return None + + # Return the users + return users + except Exception as err: + # Log the exception + core_logger.print_to_log(f"Error in get_users_admin: {err}", "error", exc=err) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + def create_user(user: users_schema.UserCreate, db: Session): try: # Create a new user @@ -599,7 +624,7 @@ def create_signup_user( users_models.User: The newly created user object. Raises: - HTTPException: + HTTPException: - 409 Conflict if the email or username is not unique. - 500 Internal Server Error for any other exceptions. """ diff --git a/backend/app/users/user/utils.py b/backend/app/users/user/utils.py index d98be9b28..a463f0bd0 100644 --- a/backend/app/users/user/utils.py +++ b/backend/app/users/user/utils.py @@ -1,14 +1,11 @@ import os import glob -import secrets from fastapi import HTTPException, status, UploadFile from sqlalchemy.orm import Session import shutil -import session.constants as session_constants - import users.user.crud as users_crud import users.user.schema as users_schema @@ -33,6 +30,18 @@ def check_user_is_active(user: users_schema.User) -> None: detail="Inactive user", headers={"WWW-Authenticate": "Bearer"}, ) + + +def get_admin_users(db: Session): + admins = users_crud.get_users_admin(db) + + if not admins: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No admin users found", + ) + + return admins def delete_user_photo_filesystem(user_id: int): diff --git a/frontend/app/src/components/Navbar/NavbarNotificationsComponent.vue b/frontend/app/src/components/Navbar/NavbarNotificationsComponent.vue index 1ad9f18ad..a57ee0fff 100644 --- a/frontend/app/src/components/Navbar/NavbarNotificationsComponent.vue +++ b/frontend/app/src/components/Navbar/NavbarNotificationsComponent.vue @@ -56,6 +56,12 @@ v-else-if="notification.type === 12" @notificationRead="markNotificationAsRead" /> +
  • Load more... @@ -75,6 +81,7 @@ import { notifications } from '@/services/notificationsService' import { useServerSettingsStore } from '@/stores/serverSettingsStore' import { useAuthStore } from '@/stores/authStore' +import AdminNewSignUpApprovalRequestNotificationComponent from '@/components/Notifications/AdminNewSignUpApprovalRequestNotificationComponent.vue' import NewAcceptedRequestNotificationComponent from '@/components/Notifications/NewAcceptedRequestNotificationComponent.vue' import NewActivityNotificationComponent from '@/components/Notifications/NewActivityNotificationComponent.vue' import NewActivityDuplicateStartTimeNotificationComponent from '@/components/Notifications/NewActivityDuplicateStartTimeNotificationComponent.vue' @@ -177,7 +184,8 @@ onMounted(async () => { (data.message === 'NEW_ACTIVITY_NOTIFICATION' || data.message === 'NEW_DUPLICATE_ACTIVITY_START_TIME_NOTIFICATION' || data.message === 'NEW_FOLLOWER_REQUEST_NOTIFICATION' || - data.message === 'NEW_FOLLOWER_REQUEST_ACCEPTED_NOTIFICATION') + data.message === 'NEW_FOLLOWER_REQUEST_ACCEPTED_NOTIFICATION' || + data.message === 'ADMIN_NEW_SIGN_UP_APPROVAL_REQUEST_NOTIFICATION') ) { await fetchNotificationById(data.notification_id) } diff --git a/frontend/app/src/components/Notifications/AdminNewSignUpApprovalRequestNotificationComponent.vue b/frontend/app/src/components/Notifications/AdminNewSignUpApprovalRequestNotificationComponent.vue new file mode 100644 index 000000000..790b75bb8 --- /dev/null +++ b/frontend/app/src/components/Notifications/AdminNewSignUpApprovalRequestNotificationComponent.vue @@ -0,0 +1,50 @@ + + + diff --git a/frontend/app/src/i18n/ca/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json b/frontend/app/src/i18n/ca/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json new file mode 100644 index 000000000..5a675a94b --- /dev/null +++ b/frontend/app/src/i18n/ca/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json @@ -0,0 +1,4 @@ +{ + "title": "New sign-up request", + "subTitle": " has requested to sign-up" +} \ No newline at end of file diff --git a/frontend/app/src/i18n/de/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json b/frontend/app/src/i18n/de/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json new file mode 100644 index 000000000..5a675a94b --- /dev/null +++ b/frontend/app/src/i18n/de/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json @@ -0,0 +1,4 @@ +{ + "title": "New sign-up request", + "subTitle": " has requested to sign-up" +} \ No newline at end of file diff --git a/frontend/app/src/i18n/es/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json b/frontend/app/src/i18n/es/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json new file mode 100644 index 000000000..5a675a94b --- /dev/null +++ b/frontend/app/src/i18n/es/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json @@ -0,0 +1,4 @@ +{ + "title": "New sign-up request", + "subTitle": " has requested to sign-up" +} \ No newline at end of file diff --git a/frontend/app/src/i18n/fr/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json b/frontend/app/src/i18n/fr/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json new file mode 100644 index 000000000..5a675a94b --- /dev/null +++ b/frontend/app/src/i18n/fr/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json @@ -0,0 +1,4 @@ +{ + "title": "New sign-up request", + "subTitle": " has requested to sign-up" +} \ No newline at end of file diff --git a/frontend/app/src/i18n/index.js b/frontend/app/src/i18n/index.js index 6be608e6a..91450baf0 100644 --- a/frontend/app/src/i18n/index.js +++ b/frontend/app/src/i18n/index.js @@ -40,6 +40,7 @@ const componentPaths = { navbarBottomMobileComponent: 'components/navbar/navbarBottomMobileComponent.json', navbarComponent: 'components/navbar/navbarComponent.json', // Notifications components + adminNewSignUpApprovalRequestNotificationComponent: 'components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json', navbarNotificationsComponent: 'components/notifications/navbarNotificationsComponent.json', newAcceptedRequestNotificationComponent: 'components/notifications/newAcceptedRequestNotificationComponent.json', diff --git a/frontend/app/src/i18n/nl/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json b/frontend/app/src/i18n/nl/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json new file mode 100644 index 000000000..5a675a94b --- /dev/null +++ b/frontend/app/src/i18n/nl/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json @@ -0,0 +1,4 @@ +{ + "title": "New sign-up request", + "subTitle": " has requested to sign-up" +} \ No newline at end of file diff --git a/frontend/app/src/i18n/pt/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json b/frontend/app/src/i18n/pt/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json new file mode 100644 index 000000000..5a675a94b --- /dev/null +++ b/frontend/app/src/i18n/pt/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json @@ -0,0 +1,4 @@ +{ + "title": "New sign-up request", + "subTitle": " has requested to sign-up" +} \ No newline at end of file diff --git a/frontend/app/src/i18n/us/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json b/frontend/app/src/i18n/us/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json new file mode 100644 index 000000000..5a675a94b --- /dev/null +++ b/frontend/app/src/i18n/us/components/notifications/adminNewSignUpApprovalRequestNotificationComponent.json @@ -0,0 +1,4 @@ +{ + "title": "New sign-up request", + "subTitle": " has requested to sign-up" +} \ No newline at end of file