Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
aisbf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexlab
aisbf
Commits
51a7e2a5
Commit
51a7e2a5
authored
Apr 11, 2026
by
Your Name
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Release version 0.99.20: Added user signup functionality with email verification and SMTP support
parent
73e5db11
Changes
12
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1025 additions
and
58 deletions
+1025
-58
CHANGELOG.md
CHANGELOG.md
+39
-0
__init__.py
aisbf/__init__.py
+1
-1
config.py
aisbf/config.py
+31
-0
database.py
aisbf/database.py
+161
-5
email_utils.py
aisbf/email_utils.py
+258
-0
aisbf.json
config/aisbf.json
+15
-0
main.py
main.py
+256
-50
pyproject.toml
pyproject.toml
+1
-1
setup.py
setup.py
+2
-1
login.html
templates/dashboard/login.html
+6
-0
settings.html
templates/dashboard/settings.html
+131
-0
signup.html
templates/dashboard/signup.html
+124
-0
No files found.
CHANGELOG.md
View file @
51a7e2a5
...
@@ -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
...
...
aisbf/__init__.py
View file @
51a7e2a5
...
@@ -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"
,
...
...
aisbf/config.py
View file @
51a7e2a5
...
@@ -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
):
...
...
aisbf/database.py
View file @
51a7e2a5
...
@@ -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
]:
"""
"""
...
...
aisbf/email_utils.py
0 → 100644
View file @
51a7e2a5
"""
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
config/aisbf.json
View file @
51a7e2a5
...
@@ -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"
}
}
}
}
main.py
View file @
51a7e2a5
This diff is collapsed.
Click to expand it.
pyproject.toml
View file @
51a7e2a5
...
@@ -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"
...
...
setup.py
View file @
51a7e2a5
...
@@ -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'
,
[
...
...
templates/dashboard/login.html
View file @
51a7e2a5
...
@@ -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 %}
templates/dashboard/settings.html
View file @
51a7e2a5
...
@@ -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
;
...
...
templates/dashboard/signup.html
0 → 100644
View file @
51a7e2a5
<!--
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 %}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment