mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-05-03 03:00:41 -04:00
Implement email verification for user signup
Added secure token generation and hashing for email verification, refactored backend signup flow to store token hashes, and send confirmation emails using a new email message utility. Updated frontend server settings UI to allow configuration of signup, admin approval, and email confirmation requirements. Adjusted related backend and frontend logic and translations for improved clarity and consistency.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import os
|
||||
import os, secrets, hashlib
|
||||
import apprise
|
||||
from typing import List
|
||||
|
||||
@@ -216,4 +216,20 @@ def get_email_service():
|
||||
return email_service
|
||||
|
||||
|
||||
def generate_token_and_hash() -> tuple[str, str]:
|
||||
"""
|
||||
Generates a secure random token and its SHA-256 hash.
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: A tuple containing the generated token and its SHA-256 hash.
|
||||
"""
|
||||
# Generate a random 32-byte token
|
||||
token = secrets.token_urlsafe(32)
|
||||
|
||||
# Create a hash of the token for database storage
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
return token, token_hash
|
||||
|
||||
|
||||
email_service = AppriseService()
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import secrets
|
||||
import hashlib
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import (
|
||||
HTTPException,
|
||||
@@ -22,34 +19,6 @@ import core.logger as core_logger
|
||||
from core.database import SessionLocal
|
||||
|
||||
|
||||
def generate_password_reset_token() -> tuple[str, str]:
|
||||
"""
|
||||
Generate a URL-safe password reset token and its SHA-256 hash for storage.
|
||||
Returns:
|
||||
tuple[str, str]: A tuple (token, token_hash) where:
|
||||
- token: a URL-safe, cryptographically secure random token suitable for
|
||||
inclusion in password reset links (this raw token is intended to be
|
||||
sent to the user).
|
||||
- token_hash: the hexadecimal SHA-256 hash of the token, suitable for
|
||||
storing in a database instead of the raw token.
|
||||
Notes:
|
||||
- Do not store or log the raw token; store only the hash (token_hash).
|
||||
- When validating a presented token, compute its SHA-256 hex digest and
|
||||
compare it to the stored token_hash using a constant-time comparison to
|
||||
mitigate timing attacks (e.g., secrets.compare_digest).
|
||||
- Consider associating an expiration timestamp and single-use semantics with
|
||||
the token to limit its validity window.
|
||||
- Token generation relies on the `secrets` module for cryptographic randomness.
|
||||
"""
|
||||
# Generate a random 32-byte token
|
||||
token = secrets.token_urlsafe(32)
|
||||
|
||||
# Create a hash of the token for database storage
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
return token, token_hash
|
||||
|
||||
|
||||
def create_password_reset_token(user_id: int, db: Session) -> str:
|
||||
"""
|
||||
Create and persist a password reset token for a user and return the plain token.
|
||||
@@ -93,7 +62,7 @@ def create_password_reset_token(user_id: int, db: Session) -> str:
|
||||
# Send `token` to the user's email. Do not store the plaintext token in persistent storage.
|
||||
"""
|
||||
# Generate token and hash
|
||||
token, token_hash = generate_password_reset_token()
|
||||
token, token_hash = core_apprise.generate_token_and_hash()
|
||||
|
||||
# Create token object
|
||||
reset_token = password_reset_tokens_schema.PasswordResetToken(
|
||||
|
||||
100
backend/app/session/email_messages.py
Normal file
100
backend/app/session/email_messages.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import core.apprise as core_apprise
|
||||
|
||||
def get_signup_confirmation_email_en(
|
||||
user_name: str, signup_link: str, email_service: core_apprise.AppriseService
|
||||
) -> tuple:
|
||||
"""
|
||||
Return the subject, HTML body, and plain-text body for an English sign-up confirmation email.
|
||||
|
||||
Args:
|
||||
user_name (str): The recipient's display name inserted into the greeting.
|
||||
signup_link (str): The URL the user will follow to confirm their sign-up; inserted into the CTA button
|
||||
and included as a plain link for clients that do not render the button.
|
||||
email_service (core_apprise.AppriseService): Notification service instance used to obtain service
|
||||
metadata (e.g., `frontend_host`) for the email footer.
|
||||
|
||||
Returns:
|
||||
tuple[str, str, str]: A 3-tuple containing:
|
||||
- subject: The email subject line.
|
||||
- html_content: The full HTML email content (string) including inline styles, logo, a prominent
|
||||
"Confirm Account" button linking to `signup_link`, a security notice about a 24-hour expiry, and
|
||||
a footer referencing `email_service.frontend_host`.
|
||||
- text_content: A plain-text alternative suitable for clients that do not render HTML, containing
|
||||
the greeting, confirmation instructions, the raw `signup_link`, expiry notice, and sign-off.
|
||||
|
||||
Notes:
|
||||
- The function only constructs and returns strings; it does not send emails or perform network I/O.
|
||||
- Calling code should ensure `signup_link` and `user_name` are properly validated/sanitized as needed.
|
||||
- The HTML is crafted with inline styles for broad email-client compatibility.
|
||||
"""
|
||||
subject = "Endurain - Confirm your account"
|
||||
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;">Confirm your account</h3>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 30px;">
|
||||
<p>Hi {user_name},</p>
|
||||
|
||||
<p>Thank you for signing up for Endurain! Please confirm your account by clicking the button below:</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{signup_link}" style="background-color: #198754; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: bold;">Confirm Account</a>
|
||||
</div>
|
||||
|
||||
<!--<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<strong>Security notice:</strong> This confirmation link will expire in 24 hours.
|
||||
</div>-->
|
||||
|
||||
<p>If you didn’t create an Endurain account, please ignore this email.</p>
|
||||
|
||||
<p>If the button above doesn’t work, you can copy and paste the following link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #198754;">{signup_link}</p>
|
||||
</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 team</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()
|
||||
|
||||
# Create text version
|
||||
text_content = f"""
|
||||
Hi {user_name},
|
||||
|
||||
Thank you for signing up for Endurain!
|
||||
|
||||
Please confirm your account by clicking the following link:
|
||||
{signup_link}
|
||||
|
||||
This confirmation link will expire in 24 hours.
|
||||
|
||||
If you didn’t create an Endurain account, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
The Endurain team
|
||||
""".strip()
|
||||
|
||||
return subject, html_content, text_content
|
||||
@@ -1,3 +1,4 @@
|
||||
import email
|
||||
from typing import Annotated, Callable
|
||||
|
||||
from fastapi import (
|
||||
@@ -16,7 +17,7 @@ import session.utils as session_utils
|
||||
import session.security as session_security
|
||||
import session.crud as session_crud
|
||||
import session.schema as session_schema
|
||||
import session.constants as session_constants
|
||||
import session.email_messages as session_email_messages
|
||||
|
||||
import users.user.crud as users_crud
|
||||
import users.user.utils as users_utils
|
||||
@@ -30,6 +31,7 @@ import profile.utils as profile_utils
|
||||
import server_settings.crud as server_settings_crud
|
||||
|
||||
import core.database as core_database
|
||||
import core.apprise as core_apprise
|
||||
|
||||
# Define the API router
|
||||
router = APIRouter()
|
||||
@@ -116,12 +118,15 @@ async def complete_login(
|
||||
@router.post("/signup", status_code=201)
|
||||
async def signup(
|
||||
user: users_schema.UserSignup,
|
||||
email_service: Annotated[
|
||||
core_apprise.AppriseService,
|
||||
Depends(core_apprise.get_email_service),
|
||||
],
|
||||
db: Annotated[
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
"""Public endpoint for user sign-up"""
|
||||
# Get server settings to check if signup is enabled
|
||||
server_settings = server_settings_crud.get_server_settings(db)
|
||||
if not server_settings:
|
||||
@@ -131,16 +136,24 @@ async def signup(
|
||||
)
|
||||
|
||||
# Check if signup is enabled
|
||||
users_utils.check_user_can_signup(server_settings)
|
||||
if not server_settings.signup_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User sign-up is not enabled on this server",
|
||||
)
|
||||
|
||||
# Generate email verification token if needed
|
||||
email_verification_token = None
|
||||
email_verification_token_hash = None
|
||||
if server_settings.signup_require_email_verification:
|
||||
email_verification_token = users_utils.generate_email_verification_token()
|
||||
# Generate token and hash
|
||||
email_verification_token, email_verification_token_hash = (
|
||||
core_apprise.generate_token_and_hash()
|
||||
)
|
||||
|
||||
# Create the user in the database
|
||||
created_user = users_crud.create_signup_user(
|
||||
user, email_verification_token, server_settings, db
|
||||
user, email_verification_token_hash, server_settings, db
|
||||
)
|
||||
|
||||
# Create the user integrations in the database
|
||||
@@ -156,27 +169,37 @@ async def signup(
|
||||
user_default_gear_crud.create_user_default_gear(created_user.id, db)
|
||||
|
||||
# Return appropriate response based on server configuration
|
||||
response_data = {"message": "User created successfully"}
|
||||
response_data = {"message": "User created successfully."}
|
||||
|
||||
if server_settings.signup_require_email_verification:
|
||||
# Generate sign-up link
|
||||
sign_up_link = f"{email_service.frontend_host}/verify-email?token={email_verification_token}"
|
||||
# use default email message in English
|
||||
subject, html_content, text_content = (
|
||||
session_email_messages.get_signup_confirmation_email_en(
|
||||
user.name, sign_up_link, email_service
|
||||
)
|
||||
)
|
||||
await email_service.send_email(
|
||||
to_emails=[user.email],
|
||||
subject=subject,
|
||||
html_content=html_content,
|
||||
text_content=text_content,
|
||||
)
|
||||
response_data["message"] = (
|
||||
"User created successfully. Please check your email for verification instructions."
|
||||
response_data["message"] + " Email sent with verification instructions."
|
||||
)
|
||||
response_data["email_verification_required"] = True
|
||||
# TODO: Send verification email here
|
||||
|
||||
if server_settings.signup_require_admin_approval:
|
||||
response_data["message"] = (
|
||||
"User created successfully. Account is pending admin approval."
|
||||
response_data["message"] + " Account is pending admin approval."
|
||||
)
|
||||
response_data["admin_approval_required"] = True
|
||||
|
||||
if (
|
||||
not server_settings.signup_require_email_verification
|
||||
and not server_settings.signup_require_admin_approval
|
||||
):
|
||||
response_data["message"] = "User created successfully. You can now log in."
|
||||
|
||||
response_data["message"] = response_data["message"] + " You can now log in."
|
||||
return response_data
|
||||
|
||||
|
||||
|
||||
@@ -548,7 +548,7 @@ def disable_user_mfa(user_id: int, db: Session):
|
||||
Returns:
|
||||
None
|
||||
Raises:
|
||||
HTTPException:
|
||||
HTTPException:
|
||||
- 404 Not Found if the user does not exist.
|
||||
- 500 Internal Server Error for any other failure; in this case the
|
||||
transaction is rolled back and the error is logged.
|
||||
@@ -568,7 +568,7 @@ def disable_user_mfa(user_id: int, db: Session):
|
||||
detail="User not found",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
db_user.mfa_enabled = False
|
||||
db_user.mfa_secret = None
|
||||
db.commit()
|
||||
@@ -582,14 +582,35 @@ def disable_user_mfa(user_id: int, db: Session):
|
||||
) from err
|
||||
|
||||
|
||||
def create_signup_user(user: users_schema.UserSignup, email_verification_token: str, server_settings, db: Session):
|
||||
"""Create a new user via sign-up with appropriate verification and approval settings"""
|
||||
def create_signup_user(
|
||||
user: users_schema.UserSignup,
|
||||
email_verification_token: str,
|
||||
server_settings,
|
||||
db: Session,
|
||||
):
|
||||
"""
|
||||
Creates a new user during the signup process, handling email verification and admin approval requirements.
|
||||
|
||||
Args:
|
||||
user (users_schema.UserSignup): The user signup data containing user details.
|
||||
email_verification_token (str): Token for email verification, if required.
|
||||
server_settings: Server configuration settings that determine signup requirements.
|
||||
db (Session): SQLAlchemy database session.
|
||||
|
||||
Returns:
|
||||
users_models.User: The newly created user object.
|
||||
|
||||
Raises:
|
||||
HTTPException:
|
||||
- 409 Conflict if the email or username is not unique.
|
||||
- 500 Internal Server Error for any other exceptions.
|
||||
"""
|
||||
try:
|
||||
# Determine user status based on server settings
|
||||
active = True
|
||||
email_verified = False
|
||||
pending_admin_approval = False
|
||||
|
||||
|
||||
if server_settings.signup_require_email_verification:
|
||||
email_verified = False
|
||||
active = False # Inactive until email verified
|
||||
@@ -599,10 +620,13 @@ def create_signup_user(user: users_schema.UserSignup, email_verification_token:
|
||||
active = False # Inactive until approved
|
||||
|
||||
# If both email verification and admin approval are disabled, user is immediately active
|
||||
if not server_settings.signup_require_email_verification and not server_settings.signup_require_admin_approval:
|
||||
if (
|
||||
not server_settings.signup_require_email_verification
|
||||
and not server_settings.signup_require_admin_approval
|
||||
):
|
||||
active = True
|
||||
email_verified = True
|
||||
|
||||
|
||||
# Create a new user
|
||||
db_user = users_models.User(
|
||||
name=user.name,
|
||||
@@ -619,7 +643,11 @@ def create_signup_user(user: users_schema.UserSignup, email_verification_token:
|
||||
first_day_of_week=user.first_day_of_week,
|
||||
currency=user.currency,
|
||||
email_verified=email_verified,
|
||||
email_verification_token=email_verification_token if server_settings.signup_require_email_verification else None,
|
||||
email_verification_token=(
|
||||
email_verification_token
|
||||
if server_settings.signup_require_email_verification
|
||||
else None
|
||||
),
|
||||
pending_admin_approval=pending_admin_approval,
|
||||
password=session_security.hash_password(user.password),
|
||||
)
|
||||
@@ -645,7 +673,9 @@ def create_signup_user(user: users_schema.UserSignup, email_verification_token:
|
||||
db.rollback()
|
||||
|
||||
# Log the exception
|
||||
core_logger.print_to_log(f"Error in create_signup_user: {err}", "error", exc=err)
|
||||
core_logger.print_to_log(
|
||||
f"Error in create_signup_user: {err}", "error", exc=err
|
||||
)
|
||||
|
||||
# Raise an HTTPException with a 500 Internal Server Error status code
|
||||
raise HTTPException(
|
||||
@@ -673,7 +703,7 @@ def verify_user_email(token: str, db: Session):
|
||||
# Mark email as verified and remove token
|
||||
db_user.email_verified = True
|
||||
db_user.email_verification_token = None
|
||||
|
||||
|
||||
# If not pending admin approval, activate the user
|
||||
if not db_user.pending_admin_approval:
|
||||
db_user.active = True
|
||||
|
||||
@@ -92,9 +92,9 @@ class User(Base):
|
||||
comment="Whether the user's email address has been verified (true - yes, false - no)",
|
||||
)
|
||||
email_verification_token = Column(
|
||||
String(length=255),
|
||||
String(length=128),
|
||||
nullable=True,
|
||||
comment="Token for email verification",
|
||||
comment="Token hash for email verification",
|
||||
)
|
||||
pending_admin_approval = Column(
|
||||
Boolean,
|
||||
|
||||
@@ -17,6 +17,16 @@ import core.config as core_config
|
||||
|
||||
|
||||
def check_user_is_active(user: users_schema.User) -> None:
|
||||
"""
|
||||
Checks if the given user is active.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the user is not active, raises an HTTP 403 Forbidden exception
|
||||
with a detail message "Inactive user" and a "WWW-Authenticate" header.
|
||||
|
||||
Args:
|
||||
user (users_schema.User): The user object to check.
|
||||
"""
|
||||
if not user.active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
@@ -26,6 +36,18 @@ def check_user_is_active(user: users_schema.User) -> None:
|
||||
|
||||
|
||||
def delete_user_photo_filesystem(user_id: int):
|
||||
"""
|
||||
Deletes all photo files associated with a user from the filesystem.
|
||||
|
||||
This function searches for files in the directory specified by `core_config.USER_IMAGES_DIR`
|
||||
that match the given `user_id` with any file extension, and removes them.
|
||||
|
||||
Args:
|
||||
user_id (int): The ID of the user whose photo files should be deleted.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Define the pattern to match files with the specified name regardless of the extension
|
||||
folder = core_config.USER_IMAGES_DIR
|
||||
file = f"{user_id}.*"
|
||||
@@ -40,11 +62,39 @@ def delete_user_photo_filesystem(user_id: int):
|
||||
|
||||
|
||||
def format_user_birthdate(user):
|
||||
user.birthdate = user.birthdate if isinstance(user.birthdate, str) else user.birthdate.isoformat() if user.birthdate else None
|
||||
"""
|
||||
Formats the birthdate attribute of a user object to an ISO 8601 string if it is a date/datetime object.
|
||||
If the birthdate is already a string or None, it remains unchanged.
|
||||
|
||||
Args:
|
||||
user: An object with a 'birthdate' attribute, which can be a string, date/datetime object, or None.
|
||||
|
||||
Returns:
|
||||
The user object with the 'birthdate' attribute formatted as an ISO 8601 string, string, or None.
|
||||
"""
|
||||
user.birthdate = (
|
||||
user.birthdate
|
||||
if isinstance(user.birthdate, str)
|
||||
else user.birthdate.isoformat() if user.birthdate else None
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
async def save_user_image(user_id: int, file: UploadFile, db: Session):
|
||||
"""
|
||||
Saves a user's image to the server and updates the user's photo path in the database.
|
||||
|
||||
Args:
|
||||
user_id (int): The ID of the user whose image is being saved.
|
||||
file (UploadFile): The uploaded image file.
|
||||
db (Session): The database session.
|
||||
|
||||
Returns:
|
||||
Any: The result of updating the user's photo path in the database.
|
||||
|
||||
Raises:
|
||||
HTTPException: If an error occurs during the image saving process, raises a 500 Internal Server Error.
|
||||
"""
|
||||
try:
|
||||
upload_dir = core_config.USER_IMAGES_DIR
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
@@ -73,17 +123,3 @@ async def save_user_image(user_id: int, file: UploadFile, db: Session):
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Internal Server Error",
|
||||
) from err
|
||||
|
||||
|
||||
def generate_email_verification_token():
|
||||
"""Generate a secure token for email verification"""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def check_user_can_signup(server_settings) -> None:
|
||||
"""Check if user signup is enabled on the server"""
|
||||
if not server_settings.signup_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User sign-up is not enabled on this server",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="col">
|
||||
<form class="bg-body-tertiary rounded p-3 shadow-sm">
|
||||
<!-- Defaults -->
|
||||
<h4>{{ $t('settingsServerSettingsZoneComponent.defaultsTitle') }}</h4>
|
||||
<!-- Units -->
|
||||
<label>{{ $t('settingsServerSettingsZoneComponent.unitsLabel') }}</label>
|
||||
@@ -17,12 +18,7 @@
|
||||
</select>
|
||||
<!-- Num records per list -->
|
||||
<label class="mt-1">{{ $t('settingsServerSettingsZoneComponent.numRecordsLabel') }}</label>
|
||||
<select
|
||||
class="form-select"
|
||||
name="serverSettingsNumRecordsPerPage"
|
||||
v-model="numRecordsPerPage"
|
||||
required
|
||||
>
|
||||
<select class="form-select" name="serverSettingsNumRecordsPerPage" v-model="numRecordsPerPage" required>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
@@ -30,24 +26,58 @@
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<hr />
|
||||
<!-- Sign-up -->
|
||||
<h4>{{ $t('settingsServerSettingsZoneComponent.signupTitle') }}</h4>
|
||||
<label class="form-label" for="serverSettingsSignUpEnabledSelect">{{
|
||||
$t('settingsServerSettingsZoneComponent.enabledLabel')
|
||||
}}</label>
|
||||
<select class="form-select" name="serverSettingsSignUpEnabledSelect" v-model="signUp" required>
|
||||
<option value="false">
|
||||
{{ $t('generalItems.false') }}
|
||||
</option>
|
||||
<option value="true">
|
||||
{{ $t('generalItems.true') }}
|
||||
</option>
|
||||
</select>
|
||||
<!-- requires admin approval -->
|
||||
<label class="form-label" for="serverSettingsAdminApprovalSelect">{{
|
||||
$t('settingsServerSettingsZoneComponent.adminApprovalLabel')
|
||||
}}</label>
|
||||
<select class="form-select" name="serverSettingsAdminApprovalSelect" v-model="adminApproval" required>
|
||||
<option value="false">
|
||||
{{ $t('generalItems.false') }}
|
||||
</option>
|
||||
<option value="true">
|
||||
{{ $t('generalItems.true') }}
|
||||
</option>
|
||||
</select>
|
||||
<!-- requires email confirmation -->
|
||||
<label class="form-label" for="serverSettingsEmailConfirmationSelect">{{
|
||||
$t('settingsServerSettingsZoneComponent.emailConfirmationLabel')
|
||||
}}</label>
|
||||
<select class="form-select" name="serverSettingsEmailConfirmationSelect" v-model="emailConfirmation" required>
|
||||
<option value="false">
|
||||
{{ $t('generalItems.false') }}
|
||||
</option>
|
||||
<option value="true">
|
||||
{{ $t('generalItems.true') }}
|
||||
</option>
|
||||
</select>
|
||||
<hr />
|
||||
<!-- Public shareable links -->
|
||||
<h4 class="mt-4">
|
||||
{{ $t('settingsServerSettingsZoneComponent.publicShareableLinksLabel') }}
|
||||
</h4>
|
||||
<label class="form-label" for="serverSettingsPublicShareableLinksEnabledSelect">{{
|
||||
$t('settingsServerSettingsZoneComponent.publicShareableLinksEnabledLabel')
|
||||
}}</label>
|
||||
<select
|
||||
class="form-select"
|
||||
name="serverSettingsPublicShareableLinksEnabledSelect"
|
||||
v-model="publicShareableLinks"
|
||||
required
|
||||
>
|
||||
$t('settingsServerSettingsZoneComponent.enabledLabel')
|
||||
}}</label>
|
||||
<select class="form-select" name="serverSettingsPublicShareableLinksEnabledSelect" v-model="publicShareableLinks"
|
||||
required>
|
||||
<option value="false">
|
||||
{{ $t('settingsServerSettingsZoneComponent.publicShareableLinksFalse') }}
|
||||
{{ $t('generalItems.false') }}
|
||||
</option>
|
||||
<option value="true">
|
||||
{{ $t('settingsServerSettingsZoneComponent.publicShareableLinksTrue') }}
|
||||
{{ $t('generalItems.true') }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="alert alert-warning mt-2" role="alert">
|
||||
@@ -61,18 +91,14 @@
|
||||
<!-- Public shareable user info -->
|
||||
<label class="form-label" for="serverSettingsPublicShareableLinksShowUserInfo">{{
|
||||
$t('settingsServerSettingsZoneComponent.publicShareableLinksShowUserInfoLabel')
|
||||
}}</label>
|
||||
<select
|
||||
class="form-select"
|
||||
name="serverSettingsPublicShareableLinksShowUserInfo"
|
||||
v-model="publicShareableLinksUserInfo"
|
||||
required
|
||||
>
|
||||
}}</label>
|
||||
<select class="form-select" name="serverSettingsPublicShareableLinksShowUserInfo"
|
||||
v-model="publicShareableLinksUserInfo" required>
|
||||
<option value="false">
|
||||
{{ $t('settingsServerSettingsZoneComponent.publicShareableLinksFalse') }}
|
||||
{{ $t('generalItems.false') }}
|
||||
</option>
|
||||
<option value="true">
|
||||
{{ $t('settingsServerSettingsZoneComponent.publicShareableLinksTrue') }}
|
||||
{{ $t('generalItems.true') }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="alert alert-warning mt-2" role="alert">
|
||||
@@ -90,50 +116,31 @@
|
||||
<div class="col">
|
||||
<label class="form-label" for="serverSettingsLoginPhotoLabel">{{
|
||||
$t('settingsServerSettingsZoneComponent.loginPhotoLabel')
|
||||
}}</label>
|
||||
}}</label>
|
||||
<!-- add login photo button -->
|
||||
<a
|
||||
class="w-100 btn btn-primary shadow-sm"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addLoginPhotoModal"
|
||||
v-if="!loginPhotoSet"
|
||||
>
|
||||
<a class="w-100 btn btn-primary shadow-sm" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#addLoginPhotoModal" v-if="!loginPhotoSet">
|
||||
{{ $t('settingsServerSettingsZoneComponent.buttonAddPhoto') }}
|
||||
</a>
|
||||
|
||||
<!-- Delete login photo section -->
|
||||
<a
|
||||
class="w-100 btn btn-danger"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteLoginPhotoModal"
|
||||
v-else
|
||||
>{{ $t('settingsServerSettingsZoneComponent.buttonDeleteLoginPhoto') }}</a
|
||||
>
|
||||
<a class="w-100 btn btn-danger" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#deleteLoginPhotoModal" v-else>{{
|
||||
$t('settingsServerSettingsZoneComponent.buttonDeleteLoginPhoto') }}</a>
|
||||
|
||||
<!-- Modal add login photo -->
|
||||
<ModalComponentUploadFile
|
||||
modalId="addLoginPhotoModal"
|
||||
<ModalComponentUploadFile modalId="addLoginPhotoModal"
|
||||
:title="$t('settingsServerSettingsZoneComponent.loginPhotoLabel')"
|
||||
:fileFieldLabel="$t('settingsServerSettingsZoneComponent.logonPhotoAddLabel')"
|
||||
filesAccepted=".png"
|
||||
actionButtonType="success"
|
||||
:actionButtonText="$t('settingsServerSettingsZoneComponent.loginPhotoLabel')"
|
||||
@fileToEmitAction="submitUploadFileForm"
|
||||
/>
|
||||
:fileFieldLabel="$t('settingsServerSettingsZoneComponent.logonPhotoAddLabel')" filesAccepted=".png"
|
||||
actionButtonType="success" :actionButtonText="$t('settingsServerSettingsZoneComponent.loginPhotoLabel')"
|
||||
@fileToEmitAction="submitUploadFileForm" />
|
||||
|
||||
<!-- Modal delete login photo -->
|
||||
<ModalComponent
|
||||
modalId="deleteLoginPhotoModal"
|
||||
<ModalComponent modalId="deleteLoginPhotoModal"
|
||||
:title="t('settingsServerSettingsZoneComponent.buttonDeleteLoginPhoto')"
|
||||
:body="`${t('settingsServerSettingsZoneComponent.modalDeleteLoginPhotoBody')}`"
|
||||
actionButtonType="danger"
|
||||
:body="`${t('settingsServerSettingsZoneComponent.modalDeleteLoginPhotoBody')}`" actionButtonType="danger"
|
||||
:actionButtonText="t('settingsServerSettingsZoneComponent.buttonDeleteLoginPhoto')"
|
||||
@submitAction="submitDeleteLoginPhoto"
|
||||
/>
|
||||
@submitAction="submitDeleteLoginPhoto" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -159,6 +166,9 @@ const publicShareableLinksUserInfo = ref(
|
||||
serverSettingsStore.serverSettings.public_shareable_links_user_info
|
||||
)
|
||||
const loginPhotoSet = ref(serverSettingsStore.serverSettings.login_photo_set)
|
||||
const signUp = ref(serverSettingsStore.serverSettings.signup_enabled)
|
||||
const adminApproval = ref(serverSettingsStore.serverSettings.signup_require_admin_approval)
|
||||
const emailConfirmation = ref(serverSettingsStore.serverSettings.signup_require_email_verification)
|
||||
|
||||
async function updateServerSettings() {
|
||||
const data = {
|
||||
@@ -168,7 +178,10 @@ async function updateServerSettings() {
|
||||
num_records_per_page: numRecordsPerPage.value,
|
||||
public_shareable_links: publicShareableLinks.value,
|
||||
public_shareable_links_user_info: publicShareableLinksUserInfo.value,
|
||||
login_photo_set: loginPhotoSet.value
|
||||
login_photo_set: loginPhotoSet.value,
|
||||
signup_enabled: signUp.value,
|
||||
signup_require_admin_approval: adminApproval.value,
|
||||
signup_require_email_verification: emailConfirmation.value
|
||||
}
|
||||
try {
|
||||
// Update the server settings in the DB
|
||||
@@ -229,7 +242,7 @@ const submitDeleteLoginPhoto = async () => {
|
||||
}
|
||||
|
||||
watch(
|
||||
[units, currency, numRecordsPerPage, publicShareableLinks, publicShareableLinksUserInfo],
|
||||
[units, currency, numRecordsPerPage, publicShareableLinks, publicShareableLinksUserInfo, signUp, adminApproval, emailConfirmation],
|
||||
async () => {
|
||||
await updateServerSettings()
|
||||
},
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
"unitsImperial": "Imperial",
|
||||
"currencyLabel": "Default currency",
|
||||
"numRecordsLabel": "Number of records per page",
|
||||
"signupTitle": "Sign-up",
|
||||
"adminApprovalLabel": "Admin approval",
|
||||
"emailConfirmationLabel": "Email confirmation",
|
||||
"publicShareableLinksLabel": "Public shareable links",
|
||||
"publicShareableLinksEnabledLabel": "Enabled",
|
||||
"publicShareableLinksTrue": "True",
|
||||
"publicShareableLinksFalse": "False",
|
||||
"enabledLabel": "Enabled",
|
||||
"serverSettingsPublicShareableLinksEnabledWarningAlert": "Enabling this will make all publicly posted activities viewable without authentication.",
|
||||
"publicShareableLinksShowUserInfoLabel": "Show user info",
|
||||
"serverSettingsPublicShareableLinksShowUserWarningAlert": "Enabling this will display user information on all public links",
|
||||
|
||||
@@ -99,10 +99,6 @@
|
||||
{{ $t('loginView.signUpLink') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<!--<div>
|
||||
<hr>
|
||||
<button class="w-100 btn btn-lg btn-warning disabled" type="submit">{{ $t("loginView.signUpButton") }}</button>
|
||||
</div>-->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user