mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-05-03 03:00:41 -04:00
Add admin notification for new sign-up approval
Introduces backend and frontend support for notifying admin users when a new user signs up and requires approval. Adds notification type, backend logic to send notifications and emails to admins, a new Vue component for displaying the notification, and i18n translations for the notification message.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
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"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{subject}</title>
|
||||
</head>
|
||||
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f4f4f4;">
|
||||
<div style="background-color: #ffffff; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<div style="font-size: 34px; font-weight: bold; margin-bottom: 10px; display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||
<img src="https://github.com/joaovitoriasilva/endurain/blob/0e17fafe450b66eda7982311e6f94cee44316684/frontend/app/public/logo/logo.svg?raw=true"
|
||||
alt="Endurain logo" style="height: 32px; width: auto;">
|
||||
<span>Endurain</span>
|
||||
</div>
|
||||
<h3 style="margin: 0;">New sign-up requires approval</h3>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 30px;">
|
||||
<p>Hello {user_name},</p>
|
||||
|
||||
<p>A new user has signed up and is awaiting approval:</p>
|
||||
|
||||
<div style="background-color: #e9ecef; border: 1px solid #ccc; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<strong>User:</strong> {sign_up_user_name}
|
||||
</div>
|
||||
|
||||
<p>Please log in to the Endurain admin panel to review and approve this request.</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{email_service.frontend_host}/settings" style="background-color: #198754; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: bold;">Go to Admin Panel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee;">
|
||||
<p>Best regards,<br>The Endurain system</p>
|
||||
<p>Visit Endurain at: <a style="color: #198754;" href="{email_service.frontend_host}">{email_service.frontend_host}</a> -
|
||||
Source code at: <a style="color: #198754;"
|
||||
href="https://github.com/joaovitoriasilva/endurain">GitHub</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
""".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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -56,6 +56,12 @@
|
||||
v-else-if="notification.type === 12"
|
||||
@notificationRead="markNotificationAsRead"
|
||||
/>
|
||||
<AdminNewSignUpApprovalRequestNotificationComponent
|
||||
:notification="notification"
|
||||
:showDropdown="showDropdown"
|
||||
v-else-if="notification.type === 101"
|
||||
@notificationRead="markNotificationAsRead"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="totalPages > 1 && totalPages > pageNumber">
|
||||
<a class="dropdown-item" @click="setPageNumber">Load more...</a>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<router-link
|
||||
class="dropdown-item link-body-emphasis text-wrap"
|
||||
:to="{ name: 'settings' }"
|
||||
>
|
||||
<span
|
||||
><b>{{ $t('adminNewSignUpApprovalRequestNotificationComponent.title') }}</b></span
|
||||
>
|
||||
<br />
|
||||
<span class="fw-lighter">
|
||||
{{ notification.options['user_name'] }} - @{{ notification.options['user_username']
|
||||
}}{{ $t('adminNewSignUpApprovalRequestNotificationComponent.subTitle') }}
|
||||
</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch } from 'vue'
|
||||
// Importing the i18n
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { notifications } from '@/services/notificationsService'
|
||||
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits(['notificationRead'])
|
||||
const props = defineProps({
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showDropdown: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const dropdownState = computed(() => {
|
||||
return props.showDropdown
|
||||
})
|
||||
|
||||
function markNotificationAsRead() {
|
||||
if (props.notification.read === false && props.showDropdown === true) {
|
||||
notifications.markNotificationAsRead(props.notification.id)
|
||||
emit('notificationRead', props.notification.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch the page number variable.
|
||||
watch(dropdownState, markNotificationAsRead, { immediate: false })
|
||||
</script>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "New sign-up request",
|
||||
"subTitle": " has requested to sign-up"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "New sign-up request",
|
||||
"subTitle": " has requested to sign-up"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "New sign-up request",
|
||||
"subTitle": " has requested to sign-up"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "New sign-up request",
|
||||
"subTitle": " has requested to sign-up"
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "New sign-up request",
|
||||
"subTitle": " has requested to sign-up"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "New sign-up request",
|
||||
"subTitle": " has requested to sign-up"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "New sign-up request",
|
||||
"subTitle": " has requested to sign-up"
|
||||
}
|
||||
Reference in New Issue
Block a user