Commit 51a7e2a5 authored by Your Name's avatar Your Name

Release version 0.99.20: Added user signup functionality with email verification and SMTP support

parent 73e5db11
...@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.99.20] - 2026-04-11
### Added
- **User Signup Functionality**: Complete user registration system with email verification
- Admin-controlled signup enable/disable toggle via dashboard settings
- SMTP server configuration interface in settings page
- Secure password hashing with SHA256
- Email verification with time-limited secure tokens
- Signup form with email and password fields
- Conditional signup link on login page when enabled
- Email verification landing page with status messages
- User accounts are disabled until email is verified
- **SMTP Email System**: Full SMTP email sending capabilities
- Supports plain, TLS, and SSL SMTP connections
- Configurable SMTP host, port, username, password, and sender address
- Proxy-aware verification link generation
- Verification email templates with proper formatting
- Connection timeout and error handling
- **User Management System**:
- Admin user management interface at /dashboard/users
- Create, edit, delete, and toggle user status
- Role-based access control (admin / regular user)
- Users have isolated private configurations
- Each user gets their own providers, rotations, autoselects
- User-specific API endpoints and authentication tokens
### Changed
- **Version Bump**: Updated version to 0.99.20 in setup.py, pyproject.toml, and aisbf/__init__.py
- **Authentication System**: Updated to support both config admin and database users
- **Login System**: Added email verification check before allowing login
- **Dashboard Templates**: Updated login page to show conditional signup link
### Fixed
- **Database Schema**: Added backwards compatible database migration for user tables
- **Session Management**: Added sliding session expiration for authenticated users
- **Proxy Awareness**: All verification links respect reverse proxy configurations
## [0.99.5] - 2026-04-09 ## [0.99.5] - 2026-04-09
### Fixed ### Fixed
......
...@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2 ...@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.19" __version__ = "0.99.20"
__all__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -213,6 +213,23 @@ class AdaptiveRateLimitingConfig(BaseModel): ...@@ -213,6 +213,23 @@ class AdaptiveRateLimitingConfig(BaseModel):
history_window: int = 3600 # History window in seconds (1 hour) history_window: int = 3600 # History window in seconds (1 hour)
consecutive_successes_for_recovery: int = 10 # Successes needed before recovery starts consecutive_successes_for_recovery: int = 10 # Successes needed before recovery starts
class SignupConfig(BaseModel):
"""Configuration for user signup functionality"""
enabled: bool = False
require_email_verification: bool = True
verification_token_expiry_hours: int = 24
class SMTPConfig(BaseModel):
"""Configuration for SMTP email sending"""
host: str = "localhost"
port: int = 587
username: str = ""
password: str = ""
use_tls: bool = True
use_ssl: bool = False
from_email: str = ""
from_name: str = "AISBF"
class AISBFConfig(BaseModel): class AISBFConfig(BaseModel):
"""Global AISBF configuration from aisbf.json""" """Global AISBF configuration from aisbf.json"""
classify_nsfw: bool = False classify_nsfw: bool = False
...@@ -229,6 +246,8 @@ class AISBFConfig(BaseModel): ...@@ -229,6 +246,8 @@ class AISBFConfig(BaseModel):
response_cache: Optional[ResponseCacheConfig] = None response_cache: Optional[ResponseCacheConfig] = None
batching: Optional[BatchingConfig] = None batching: Optional[BatchingConfig] = None
adaptive_rate_limiting: Optional[AdaptiveRateLimitingConfig] = None adaptive_rate_limiting: Optional[AdaptiveRateLimitingConfig] = None
signup: Optional[SignupConfig] = None
smtp: Optional[SMTPConfig] = None
class AppConfig(BaseModel): class AppConfig(BaseModel):
...@@ -701,6 +720,14 @@ class Config: ...@@ -701,6 +720,14 @@ class Config:
adaptive_data = data.get('adaptive_rate_limiting') adaptive_data = data.get('adaptive_rate_limiting')
if adaptive_data: if adaptive_data:
data['adaptive_rate_limiting'] = AdaptiveRateLimitingConfig(**adaptive_data) data['adaptive_rate_limiting'] = AdaptiveRateLimitingConfig(**adaptive_data)
# Parse signup separately if present
signup_data = data.get('signup')
if signup_data:
data['signup'] = SignupConfig(**signup_data)
# Parse smtp separately if present
smtp_data = data.get('smtp')
if smtp_data:
data['smtp'] = SMTPConfig(**smtp_data)
self.aisbf = AISBFConfig(**data) self.aisbf = AISBFConfig(**data)
self._loaded_files['aisbf'] = str(aisbf_path.absolute()) self._loaded_files['aisbf'] = str(aisbf_path.absolute())
logger.info(f"Loaded AISBF config: classify_nsfw={self.aisbf.classify_nsfw}, classify_privacy={self.aisbf.classify_privacy}") logger.info(f"Loaded AISBF config: classify_nsfw={self.aisbf.classify_nsfw}, classify_privacy={self.aisbf.classify_privacy}")
...@@ -710,6 +737,10 @@ class Config: ...@@ -710,6 +737,10 @@ class Config:
logger.info(f"Batching config: enabled={self.aisbf.batching.enabled}, window_ms={self.aisbf.batching.window_ms}, max_batch_size={self.aisbf.batching.max_batch_size}") logger.info(f"Batching config: enabled={self.aisbf.batching.enabled}, window_ms={self.aisbf.batching.window_ms}, max_batch_size={self.aisbf.batching.max_batch_size}")
if self.aisbf.adaptive_rate_limiting: if self.aisbf.adaptive_rate_limiting:
logger.info(f"Adaptive rate limiting: enabled={self.aisbf.adaptive_rate_limiting.enabled}, initial_rate_limit={self.aisbf.adaptive_rate_limiting.initial_rate_limit}") logger.info(f"Adaptive rate limiting: enabled={self.aisbf.adaptive_rate_limiting.enabled}, initial_rate_limit={self.aisbf.adaptive_rate_limiting.initial_rate_limit}")
if self.aisbf.signup:
logger.info(f"Signup config: enabled={self.aisbf.signup.enabled}, require_email_verification={self.aisbf.signup.require_email_verification}")
if self.aisbf.smtp:
logger.info(f"SMTP config: host={self.aisbf.smtp.host}, port={self.aisbf.smtp.port}, from_email={self.aisbf.smtp.from_email}")
logger.info(f"=== Config._load_aisbf_config END ===") logger.info(f"=== Config._load_aisbf_config END ===")
def _initialize_error_tracking(self): def _initialize_error_tracking(self):
......
...@@ -221,14 +221,70 @@ class DatabaseManager: ...@@ -221,14 +221,70 @@ class DatabaseManager:
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY {auto_increment}, id INTEGER PRIMARY KEY {auto_increment},
username VARCHAR(255) UNIQUE NOT NULL, username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user', role VARCHAR(50) DEFAULT 'user',
created_by VARCHAR(255), created_by VARCHAR(255),
created_at TIMESTAMP DEFAULT {timestamp_default}, created_at TIMESTAMP DEFAULT {timestamp_default},
last_login TIMESTAMP NULL, last_login TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1 is_active {boolean_type} DEFAULT 1,
email_verified {boolean_type} DEFAULT 0,
verification_token VARCHAR(255),
verification_token_expires TIMESTAMP NULL
) )
''') ''')
# Migration: Add email-related columns if they don't exist
try:
if self.db_type == 'sqlite':
cursor.execute("PRAGMA table_info(users)")
columns = [row[1] for row in cursor.fetchall()]
if 'email' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE')
logger.info("Migration: Added email column to users table")
if 'email_verified' not in columns:
cursor.execute(f'ALTER TABLE users ADD COLUMN email_verified {boolean_type} DEFAULT 0')
logger.info("Migration: Added email_verified column to users table")
if 'verification_token' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN verification_token VARCHAR(255)')
logger.info("Migration: Added verification_token column to users table")
if 'verification_token_expires' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN verification_token_expires TIMESTAMP NULL')
logger.info("Migration: Added verification_token_expires column to users table")
else: # mysql
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'email'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE')
logger.info("Migration: Added email column to users table")
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'email_verified'
""")
if not cursor.fetchone():
cursor.execute(f'ALTER TABLE users ADD COLUMN email_verified {boolean_type} DEFAULT 0')
logger.info("Migration: Added email_verified column to users table")
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'verification_token'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE users ADD COLUMN verification_token VARCHAR(255)')
logger.info("Migration: Added verification_token column to users table")
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'verification_token_expires'
""")
if not cursor.fetchone():
cursor.execute('ALTER TABLE users ADD COLUMN verification_token_expires TIMESTAMP NULL')
logger.info("Migration: Added verification_token_expires column to users table")
except Exception as e:
logger.warning(f"Migration check for users email fields: {e}")
# User-specific configuration tables for multi-user isolation # User-specific configuration tables for multi-user isolation
cursor.execute(f''' cursor.execute(f'''
...@@ -798,7 +854,8 @@ class DatabaseManager: ...@@ -798,7 +854,8 @@ class DatabaseManager:
} }
return None return None
def create_user(self, username: str, password_hash: str, role: str = 'user', created_by: str = None) -> int: def create_user(self, username: str, password_hash: str, role: str = 'user', created_by: str = None,
email: str = None, email_verified: bool = False) -> int:
""" """
Create a new user. Create a new user.
...@@ -807,6 +864,8 @@ class DatabaseManager: ...@@ -807,6 +864,8 @@ class DatabaseManager:
password_hash: SHA256 hash of the password password_hash: SHA256 hash of the password
role: User role ('admin' or 'user') role: User role ('admin' or 'user')
created_by: Username of the creator created_by: Username of the creator
email: Email address (optional)
email_verified: Whether email is verified (default: False)
Returns: Returns:
User ID of the created user User ID of the created user
...@@ -815,11 +874,108 @@ class DatabaseManager: ...@@ -815,11 +874,108 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s' placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f''' cursor.execute(f'''
INSERT INTO users (username, password_hash, role, created_by) INSERT INTO users (username, email, password_hash, role, created_by, email_verified)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}) VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder})
''', (username, password_hash, role, created_by)) ''', (username, email, password_hash, role, created_by, 1 if email_verified else 0))
conn.commit() conn.commit()
return cursor.lastrowid return cursor.lastrowid
def get_user_by_email(self, email: str) -> Optional[Dict]:
"""
Get a user by email address.
Args:
email: Email address to look up
Returns:
User dict if found, None otherwise
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT id, username, email, role, is_active, email_verified
FROM users
WHERE email = {placeholder}
''', (email,))
row = cursor.fetchone()
if row:
return {
'id': row[0],
'username': row[1],
'email': row[2],
'role': row[3],
'is_active': row[4],
'email_verified': row[5]
}
return None
def set_verification_token(self, user_id: int, token: str, expires_at: datetime):
"""
Set email verification token for a user.
Args:
user_id: User ID
token: Verification token
expires_at: Token expiration datetime
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
UPDATE users
SET verification_token = {placeholder}, verification_token_expires = {placeholder}
WHERE id = {placeholder}
''', (token, expires_at.isoformat(), user_id))
conn.commit()
def verify_email(self, token: str) -> Optional[Dict]:
"""
Verify a user's email using the verification token.
Args:
token: Verification token
Returns:
User dict if token is valid and not expired, None otherwise
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
# Find user with this token that hasn't expired
cursor.execute(f'''
SELECT id, username, email, verification_token_expires
FROM users
WHERE verification_token = {placeholder} AND is_active = 1
''', (token,))
row = cursor.fetchone()
if not row:
return None
user_id, username, email, expires_str = row
# Check if token has expired
if expires_str:
expires_at = datetime.fromisoformat(expires_str)
if datetime.now() > expires_at:
return None
# Mark email as verified and clear token
cursor.execute(f'''
UPDATE users
SET email_verified = 1, verification_token = NULL, verification_token_expires = NULL
WHERE id = {placeholder}
''', (user_id,))
conn.commit()
return {
'id': user_id,
'username': username,
'email': email
}
def get_users(self) -> List[Dict]: def get_users(self) -> List[Dict]:
""" """
......
"""
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
AISBF - AI Service Broker Framework || AI Should Be Free
Email utilities for sending verification emails and notifications.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Why did the programmer quit his job? Because he didn't get arrays!
"""
import smtplib
import hashlib
import secrets
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional
import logging
logger = logging.getLogger(__name__)
def hash_password(password: str) -> str:
"""
Hash a password using SHA256.
Args:
password: Plain text password
Returns:
SHA256 hash of the password
"""
return hashlib.sha256(password.encode()).hexdigest()
def generate_verification_token() -> str:
"""
Generate a secure random verification token.
Returns:
Random token string (32 bytes hex)
"""
return secrets.token_hex(32)
def send_verification_email(
to_email: str,
username: str,
verification_token: str,
base_url: str,
smtp_config: dict
) -> bool:
"""
Send email verification email to a user.
Args:
to_email: Recipient email address
username: Username of the user
verification_token: Verification token
base_url: Base URL of the application
smtp_config: SMTP configuration dictionary
Returns:
True if email sent successfully, False otherwise
"""
try:
# Create verification URL
verification_url = f"{base_url}/dashboard/verify-email?token={verification_token}"
# Create message
msg = MIMEMultipart('alternative')
msg['Subject'] = 'Verify your AISBF account'
msg['From'] = f"{smtp_config.get('from_name', 'AISBF')} <{smtp_config.get('from_email')}>"
msg['To'] = to_email
# Create plain text and HTML versions
text = f"""
Hello {username},
Thank you for signing up for AISBF!
Please verify your email address by clicking the link below:
{verification_url}
This link will expire in {smtp_config.get('verification_token_expiry_hours', 24)} hours.
If you did not create this account, please ignore this email.
Best regards,
AISBF Team
"""
html = f"""
<html>
<head></head>
<body>
<h2>Hello {username},</h2>
<p>Thank you for signing up for AISBF!</p>
<p>Please verify your email address by clicking the button below:</p>
<p style="margin: 30px 0;">
<a href="{verification_url}"
style="background-color: #4CAF50; color: white; padding: 14px 20px;
text-decoration: none; border-radius: 4px; display: inline-block;">
Verify Email Address
</a>
</p>
<p>Or copy and paste this link into your browser:</p>
<p><a href="{verification_url}">{verification_url}</a></p>
<p>This link will expire in {smtp_config.get('verification_token_expiry_hours', 24)} hours.</p>
<p>If you did not create this account, please ignore this email.</p>
<br>
<p>Best regards,<br>AISBF Team</p>
</body>
</html>
"""
# Attach parts
part1 = MIMEText(text, 'plain')
part2 = MIMEText(html, 'html')
msg.attach(part1)
msg.attach(part2)
# Send email
if smtp_config.get('use_ssl', False):
# Use SSL
with smtplib.SMTP_SSL(smtp_config['host'], smtp_config['port']) as server:
if smtp_config.get('username') and smtp_config.get('password'):
server.login(smtp_config['username'], smtp_config['password'])
server.send_message(msg)
else:
# Use TLS or no encryption
with smtplib.SMTP(smtp_config['host'], smtp_config['port']) as server:
if smtp_config.get('use_tls', True):
server.starttls()
if smtp_config.get('username') and smtp_config.get('password'):
server.login(smtp_config['username'], smtp_config['password'])
server.send_message(msg)
logger.info(f"Verification email sent to {to_email}")
return True
except Exception as e:
logger.error(f"Failed to send verification email to {to_email}: {e}")
return False
def send_password_reset_email(
to_email: str,
username: str,
reset_token: str,
base_url: str,
smtp_config: dict
) -> bool:
"""
Send password reset email to a user.
Args:
to_email: Recipient email address
username: Username of the user
reset_token: Password reset token
base_url: Base URL of the application
smtp_config: SMTP configuration dictionary
Returns:
True if email sent successfully, False otherwise
"""
try:
# Create reset URL
reset_url = f"{base_url}/dashboard/reset-password?token={reset_token}"
# Create message
msg = MIMEMultipart('alternative')
msg['Subject'] = 'Reset your AISBF password'
msg['From'] = f"{smtp_config.get('from_name', 'AISBF')} <{smtp_config.get('from_email')}>"
msg['To'] = to_email
# Create plain text and HTML versions
text = f"""
Hello {username},
You requested to reset your password for your AISBF account.
Please click the link below to reset your password:
{reset_url}
This link will expire in 1 hour.
If you did not request a password reset, please ignore this email.
Best regards,
AISBF Team
"""
html = f"""
<html>
<head></head>
<body>
<h2>Hello {username},</h2>
<p>You requested to reset your password for your AISBF account.</p>
<p>Please click the button below to reset your password:</p>
<p style="margin: 30px 0;">
<a href="{reset_url}"
style="background-color: #2196F3; color: white; padding: 14px 20px;
text-decoration: none; border-radius: 4px; display: inline-block;">
Reset Password
</a>
</p>
<p>Or copy and paste this link into your browser:</p>
<p><a href="{reset_url}">{reset_url}</a></p>
<p>This link will expire in 1 hour.</p>
<p>If you did not request a password reset, please ignore this email.</p>
<br>
<p>Best regards,<br>AISBF Team</p>
</body>
</html>
"""
# Attach parts
part1 = MIMEText(text, 'plain')
part2 = MIMEText(html, 'html')
msg.attach(part1)
msg.attach(part2)
# Send email
if smtp_config.get('use_ssl', False):
# Use SSL
with smtplib.SMTP_SSL(smtp_config['host'], smtp_config['port']) as server:
if smtp_config.get('username') and smtp_config.get('password'):
server.login(smtp_config['username'], smtp_config['password'])
server.send_message(msg)
else:
# Use TLS or no encryption
with smtplib.SMTP(smtp_config['host'], smtp_config['port']) as server:
if smtp_config.get('use_tls', True):
server.starttls()
if smtp_config.get('username') and smtp_config.get('password'):
server.login(smtp_config['username'], smtp_config['password'])
server.send_message(msg)
logger.info(f"Password reset email sent to {to_email}")
return True
except Exception as e:
logger.error(f"Failed to send password reset email to {to_email}: {e}")
return False
...@@ -116,5 +116,20 @@ ...@@ -116,5 +116,20 @@
"jitter_factor": 0.25, "jitter_factor": 0.25,
"history_window": 3600, "history_window": 3600,
"consecutive_successes_for_recovery": 10 "consecutive_successes_for_recovery": 10
},
"signup": {
"enabled": false,
"require_email_verification": true,
"verification_token_expiry_hours": 24
},
"smtp": {
"host": "localhost",
"port": 587,
"username": "",
"password": "",
"use_tls": true,
"use_ssl": false,
"from_email": "noreply@example.com",
"from_name": "AISBF"
} }
} }
This diff is collapsed.
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" name = "aisbf"
version = "0.99.19" version = "0.99.20"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations" description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -49,7 +49,7 @@ class InstallCommand(_install): ...@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.99.19", version="0.99.20",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations", description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
...@@ -167,6 +167,7 @@ setup( ...@@ -167,6 +167,7 @@ setup(
'templates/dashboard/user_tokens.html', 'templates/dashboard/user_tokens.html',
'templates/dashboard/rate_limits.html', 'templates/dashboard/rate_limits.html',
'templates/dashboard/users.html', 'templates/dashboard/users.html',
'templates/dashboard/signup.html',
]), ]),
# Install static files (extension and favicon) # Install static files (extension and favicon)
('share/aisbf/static', [ ('share/aisbf/static', [
......
...@@ -45,6 +45,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -45,6 +45,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
<button type="submit" class="btn" style="width: 100%;">Login</button> <button type="submit" class="btn" style="width: 100%;">Login</button>
{% if signup_enabled %}
<div style="text-align: center; margin-top: 20px;">
<p>Don't have an account? <a href="{{ url_for(request, '/dashboard/signup') }}" style="color: #4CAF50;">Sign up here</a></p>
</div>
{% endif %}
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
...@@ -422,12 +422,143 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -422,12 +422,143 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</div> </div>
<h3 style="margin: 30px 0 20px;">User Signup</h3>
<div class="form-group">
<label>
<input type="checkbox" name="signup_enabled" {% if config.signup and config.signup.enabled %}checked{% endif %}>
Enable User Signup
</label>
<small style="color: #666; display: block; margin-top: 5px;">Allow users to create accounts via signup form</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="signup_require_verification" {% if config.signup and config.signup.require_email_verification %}checked{% endif %}>
Require Email Verification
</label>
<small style="color: #666; display: block; margin-top: 5px;">Users must verify their email before they can login</small>
</div>
<div class="form-group">
<label for="verification_token_expiry">Verification Token Expiry (hours)</label>
<input type="number" id="verification_token_expiry" name="verification_token_expiry"
value="{{ config.signup.verification_token_expiry_hours if config.signup else 24 }}"
min="1" max="168">
<small style="color: #666; display: block; margin-top: 5px;">How long verification links remain valid (1-168 hours)</small>
</div>
<h3 style="margin: 30px 0 20px;">SMTP Email Configuration</h3>
<div class="form-group">
<label for="smtp_host">SMTP Host</label>
<input type="text" id="smtp_host" name="smtp_host"
value="{{ config.smtp.host if config.smtp else 'localhost' }}" required>
<small style="color: #666; display: block; margin-top: 5px;">SMTP server hostname (e.g., smtp.gmail.com)</small>
</div>
<div class="form-group">
<label for="smtp_port">SMTP Port</label>
<input type="number" id="smtp_port" name="smtp_port"
value="{{ config.smtp.port if config.smtp else 587 }}" required>
<small style="color: #666; display: block; margin-top: 5px;">SMTP server port (587 for TLS, 465 for SSL, 25 for no encryption)</small>
</div>
<div class="form-group">
<label for="smtp_username">SMTP Username</label>
<input type="text" id="smtp_username" name="smtp_username"
value="{{ config.smtp.username if config.smtp else '' }}">
<small style="color: #666; display: block; margin-top: 5px;">SMTP authentication username (leave blank if not required)</small>
</div>
<div class="form-group">
<label for="smtp_password">SMTP Password</label>
<input type="password" id="smtp_password" name="smtp_password" placeholder="Leave blank to keep current">
<small style="color: #666; display: block; margin-top: 5px;">SMTP authentication password</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="smtp_use_tls" {% if not config.smtp or config.smtp.use_tls %}checked{% endif %}>
Use TLS
</label>
<small style="color: #666; display: block; margin-top: 5px;">Use STARTTLS for secure connection (recommended for port 587)</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="smtp_use_ssl" {% if config.smtp and config.smtp.use_ssl %}checked{% endif %}>
Use SSL
</label>
<small style="color: #666; display: block; margin-top: 5px;">Use SSL/TLS from the start (for port 465)</small>
</div>
<div class="form-group">
<label for="smtp_from_email">From Email Address</label>
<input type="email" id="smtp_from_email" name="smtp_from_email"
value="{{ config.smtp.from_email if config.smtp else 'noreply@example.com' }}" required>
<small style="color: #666; display: block; margin-top: 5px;">Email address to send from</small>
</div>
<div class="form-group">
<label for="smtp_from_name">From Name</label>
<input type="text" id="smtp_from_name" name="smtp_from_name"
value="{{ config.smtp.from_name if config.smtp else 'AISBF' }}" required>
<small style="color: #666; display: block; margin-top: 5px;">Display name for sent emails</small>
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;"> <div style="display: flex; gap: 10px; margin-top: 30px;">
<button type="submit" class="btn">Save Settings</button> <button type="submit" class="btn">Save Settings</button>
<a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a>
<button type="button" onclick="testSMTP()" class="btn btn-secondary" style="margin-left: auto;">Test SMTP</button>
</div> </div>
</form> </form>
<script>
async function testSMTP() {
const host = document.getElementById('smtp_host').value;
const port = document.getElementById('smtp_port').value;
const username = document.getElementById('smtp_username').value;
const fromEmail = document.getElementById('smtp_from_email').value;
if (!host || !port || !fromEmail) {
alert('Please fill in SMTP host, port, and from email before testing');
return;
}
if (!confirm('This will send a test email to the admin email address. Continue?')) {
return;
}
try {
const response = await fetch('{{ url_for(request, "/dashboard/test-smtp") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
host: host,
port: parseInt(port),
username: username,
password: document.getElementById('smtp_password').value,
use_tls: document.querySelector('input[name="smtp_use_tls"]').checked,
use_ssl: document.querySelector('input[name="smtp_use_ssl"]').checked,
from_email: fromEmail,
from_name: document.getElementById('smtp_from_name').value
})
});
const data = await response.json();
if (data.success) {
alert('Test email sent successfully! Check your inbox.');
} else {
alert('Failed to send test email: ' + (data.error || 'Unknown error'));
}
} catch (error) {
alert('Error testing SMTP: ' + error.message);
}
}
</script>
<script> <script>
function toggleSSLFields() { function toggleSSLFields() {
const protocol = document.getElementById('protocol').value; const protocol = document.getElementById('protocol').value;
......
<!--
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{% extends "base.html" %}
{% block title %}Sign Up - AISBF{% endblock %}
{% block content %}
<div style="max-width: 500px; margin: 50px auto;">
<h2 style="margin-bottom: 30px; text-align: center;">Create Account</h2>
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% else %}
<form method="POST" action="{{ url_for(request, '/dashboard/signup') }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus
pattern="[a-zA-Z0-9_-]+"
title="Username can only contain letters, numbers, underscores, and hyphens"
minlength="3" maxlength="50">
<small style="color: #666; display: block; margin-top: 5px;">
3-50 characters, letters, numbers, underscores, and hyphens only
</small>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required>
<small style="color: #666; display: block; margin-top: 5px;">
You will receive a verification email at this address
</small>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required
minlength="8"
pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"
title="Password must be at least 8 characters and contain at least one uppercase letter, one lowercase letter, and one number">
<small style="color: #666; display: block; margin-top: 5px;">
At least 8 characters with uppercase, lowercase, and numbers
</small>
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="terms" name="terms" required style="width: auto; margin-right: 10px;">
I agree to the Terms of Service and Privacy Policy
</label>
</div>
<button type="submit" class="btn" style="width: 100%;">Create Account</button>
<div style="text-align: center; margin-top: 20px;">
<p>Already have an account? <a href="{{ url_for(request, '/dashboard/login') }}" style="color: #4CAF50;">Login here</a></p>
</div>
</form>
{% endif %}
</div>
<script>
// Client-side password validation
document.querySelector('form')?.addEventListener('submit', function(e) {
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (password !== confirmPassword) {
e.preventDefault();
alert('Passwords do not match!');
return false;
}
// Check password strength
if (password.length < 8) {
e.preventDefault();
alert('Password must be at least 8 characters long!');
return false;
}
if (!/[a-z]/.test(password)) {
e.preventDefault();
alert('Password must contain at least one lowercase letter!');
return false;
}
if (!/[A-Z]/.test(password)) {
e.preventDefault();
alert('Password must contain at least one uppercase letter!');
return false;
}
if (!/\d/.test(password)) {
e.preventDefault();
alert('Password must contain at least one number!');
return false;
}
return true;
});
</script>
{% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment