feat: Add PostgreSQL support and enhance admin dashboard

- Migrate all service files from hardcoded SQLite to dual SQLite/PostgreSQL support
- Add PostgreSQL service to docker-compose.yml
- Create database abstraction layer (database.py) with execute_query, row_to_dict helpers
- Update all 7 service files: account, rating, contact, ai_cost, quota, site_assistant, admin
- Add new admin endpoint /database-stats for table size and row count visualization
- Add database_type field to system health endpoint
- Update .env.example with proper PostgreSQL connection string
This commit is contained in:
Your Name
2026-03-31 21:51:45 +02:00
parent 42b1ad1250
commit 030418f6db
11 changed files with 1930 additions and 1006 deletions

View File

@@ -1,71 +1,86 @@
"""Contact form service — stores messages and sends notification emails."""
"""Contact form service — stores messages and sends notification emails.
Supports both SQLite (development) and PostgreSQL (production).
"""
import logging
import os
import sqlite3
from datetime import datetime, timezone
from flask import current_app
from app.services.email_service import send_email
from app.utils.database import db_connection, execute_query, is_postgres, row_to_dict
logger = logging.getLogger(__name__)
VALID_CATEGORIES = {"general", "bug", "feature"}
def _connect() -> sqlite3.Connection:
db_path = current_app.config["DATABASE_PATH"]
db_dir = os.path.dirname(db_path)
if db_dir:
os.makedirs(db_dir, exist_ok=True)
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
def init_contact_db() -> None:
"""Create the contact_messages table if it doesn't exist."""
conn = _connect()
try:
conn.execute("""
CREATE TABLE IF NOT EXISTS contact_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'general',
subject TEXT NOT NULL,
message TEXT NOT NULL,
created_at TEXT NOT NULL,
is_read INTEGER NOT NULL DEFAULT 0
)
""")
conn.commit()
finally:
conn.close()
with db_connection() as conn:
if is_postgres():
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS contact_messages (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'general',
subject TEXT NOT NULL,
message TEXT NOT NULL,
created_at TEXT NOT NULL,
is_read BOOLEAN NOT NULL DEFAULT FALSE
)
""")
else:
conn.execute("""
CREATE TABLE IF NOT EXISTS contact_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'general',
subject TEXT NOT NULL,
message TEXT NOT NULL,
created_at TEXT NOT NULL,
is_read INTEGER NOT NULL DEFAULT 0
)
""")
def save_message(name: str, email: str, category: str, subject: str, message: str) -> dict:
def save_message(
name: str, email: str, category: str, subject: str, message: str
) -> dict:
"""Persist a contact message and send a notification email."""
if category not in VALID_CATEGORIES:
category = "general"
now = datetime.now(timezone.utc).isoformat()
conn = _connect()
try:
cursor = conn.execute(
"""INSERT INTO contact_messages (name, email, category, subject, message, created_at)
VALUES (?, ?, ?, ?, ?, ?)""",
(name, email, category, subject, message, now),
)
conn.commit()
msg_id = cursor.lastrowid
finally:
conn.close()
# Send notification email to admin
with db_connection() as conn:
sql = (
"""INSERT INTO contact_messages (name, email, category, subject, message, created_at)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id"""
if is_postgres()
else """INSERT INTO contact_messages (name, email, category, subject, message, created_at)
VALUES (?, ?, ?, ?, ?, ?)"""
)
cursor = execute_query(
conn, sql, (name, email, category, subject, message, now)
)
if is_postgres():
result = cursor.fetchone()
msg_id = result["id"] if result else None
else:
msg_id = cursor.lastrowid
admin_emails = tuple(current_app.config.get("INTERNAL_ADMIN_EMAILS", ()))
admin_email = admin_emails[0] if admin_emails else current_app.config.get(
"SMTP_FROM", "noreply@dociva.io"
admin_email = (
admin_emails[0]
if admin_emails
else current_app.config.get("SMTP_FROM", "noreply@dociva.io")
)
try:
send_email(
@@ -89,16 +104,19 @@ def save_message(name: str, email: str, category: str, subject: str, message: st
def get_messages(page: int = 1, per_page: int = 20) -> dict:
"""Retrieve paginated contact messages (admin use)."""
offset = (page - 1) * per_page
conn = _connect()
try:
total = conn.execute("SELECT COUNT(*) FROM contact_messages").fetchone()[0]
rows = conn.execute(
"SELECT * FROM contact_messages ORDER BY created_at DESC LIMIT ? OFFSET ?",
(per_page, offset),
).fetchall()
messages = [dict(r) for r in rows]
finally:
conn.close()
with db_connection() as conn:
cursor = execute_query(conn, "SELECT COUNT(*) FROM contact_messages")
total = cursor.fetchone()[0]
sql = (
"""SELECT * FROM contact_messages ORDER BY created_at DESC LIMIT %s OFFSET %s"""
if is_postgres()
else """SELECT * FROM contact_messages ORDER BY created_at DESC LIMIT ? OFFSET ?"""
)
cursor2 = execute_query(conn, sql, (per_page, offset))
rows = cursor2.fetchall()
messages = [row_to_dict(r) for r in rows]
return {
"messages": messages,
@@ -110,13 +128,11 @@ def get_messages(page: int = 1, per_page: int = 20) -> dict:
def mark_read(message_id: int) -> bool:
"""Mark a contact message as read."""
conn = _connect()
try:
result = conn.execute(
"UPDATE contact_messages SET is_read = 1 WHERE id = ?",
(message_id,),
with db_connection() as conn:
sql = (
"UPDATE contact_messages SET is_read = TRUE WHERE id = %s"
if is_postgres()
else "UPDATE contact_messages SET is_read = 1 WHERE id = ?"
)
conn.commit()
return result.rowcount > 0
finally:
conn.close()
cursor = execute_query(conn, sql, (message_id,))
return cursor.rowcount > 0