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
+
+
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.
+
+
+
+
+
+
+
+
+
+ """.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 @@
+
+
+ {{ $t('adminNewSignUpApprovalRequestNotificationComponent.title') }}
+
+
+ {{ notification.options['user_name'] }} - @{{ notification.options['user_username']
+ }}{{ $t('adminNewSignUpApprovalRequestNotificationComponent.subTitle') }}
+
+
+
+
+
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