July 3rd - Review and optimize codes

This commit is contained in:
2026-07-03 14:38:49 -04:00
parent d0b1b7ae23
commit 4c275fc0e5
16 changed files with 353 additions and 136 deletions
-2
View File
@@ -247,7 +247,6 @@ def create_app(config_name='default'):
from app.api.notifications import bp as _api_notifications_bp
from app.api.stats import bp as _api_stats_bp
from app.api.comments import bp as _api_comments_bp
from app.api.devices import bp as _api_devices_bp
csrf.exempt(_api_auth_bp)
csrf.exempt(_api_facilities_bp)
csrf.exempt(_api_templates_bp)
@@ -257,7 +256,6 @@ def create_app(config_name='default'):
csrf.exempt(_api_notifications_bp)
csrf.exempt(_api_stats_bp)
csrf.exempt(_api_comments_bp)
csrf.exempt(_api_devices_bp)
register_api(app)
# ── Security response headers ─────────────────────────────────────────
-40
View File
@@ -1,40 +0,0 @@
"""Add form_schema column to inspection_templates
Run this once on your server:
python add_form_schema.py
Or via Flask-Migrate:
flask db migrate -m "add form_schema to inspection_templates"
flask db upgrade
"""
# If you prefer to run this as a standalone script:
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import create_app, db
from sqlalchemy import text
app = create_app()
with app.app_context():
with db.engine.connect() as conn:
# Check if column already exists
result = conn.execute(text("""
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'inspection_templates'
AND column_name = 'form_schema'
"""))
exists = result.scalar()
if not exists:
conn.execute(text("""
ALTER TABLE inspection_templates
ADD COLUMN form_schema JSON NULL COMMENT 'JSON schema for the dynamic form builder'
"""))
conn.commit()
print("✓ Column 'form_schema' added to inspection_templates.")
else:
print("✓ Column 'form_schema' already exists — no changes made.")
+5 -2
View File
@@ -50,7 +50,10 @@ def register_api(app):
from app.api.comments import bp as comments_bp
api_bp.register_blueprint(comments_bp)
from app.api.devices import bp as devices_bp
api_bp.register_blueprint(devices_bp)
# NOTE: device registration lives on the auth blueprint
# (POST /api/v1/devices/register in app/api/auth.py) and writes to the
# canonical api_device_tokens table (model DeviceToken). A former duplicate
# `api_devices` blueprint wrote to the orphaned device_registrations table
# and was removed — see CLAUDE.md rule 84.
app.register_blueprint(api_bp)
-95
View File
@@ -1,95 +0,0 @@
"""
app/api/devices.py
------------------
Mobile API endpoint for device registration.
POST /api/v1/devices/register
Upserts a device record for the authenticated user.
Called on every app foreground (active scenePhase) so last_seen_at
stays current and the admin can identify stale / outdated installs.
Request JSON
------------
{
"device_id": "stable-uuid-from-keychain", // required
"device_name": "Nguyen's iPad", // UIDevice.current.name
"app_version": "1.2.0", // CFBundleShortVersionString
"ios_version": "18.3.1" // UIDevice.current.systemVersion
}
Response 200
------------
{ "ok": true, "data": { "registered": true } }
"""
import logging
from flask import Blueprint, request, g
from app import db
from app.models.device_registration import DeviceRegistration
from app.api.errors import api_ok, api_error
from app.api.decorators import jwt_required
from app.utils.audit import log_action, ACTION_UPDATE, ACTION_CREATE
from app.utils.time_utils import now_eastern
logger = logging.getLogger(__name__)
bp = Blueprint('api_devices', __name__)
_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'}
@bp.route('/devices/register', methods=['POST'])
@jwt_required
def register_device():
user = g.api_user
if user.role not in _ALLOWED_ROLES:
return api_error('Access denied', 403)
data = request.get_json(silent=True) or {}
device_id = (data.get('device_id') or '').strip()
device_name = (data.get('device_name') or '').strip()[:255]
app_version = (data.get('app_version') or '').strip()[:32]
ios_version = (data.get('ios_version') or '').strip()[:32]
if not device_id:
return api_error('device_id is required', 400)
if len(device_id) > 64:
return api_error('device_id too long', 400)
now = now_eastern()
existing = DeviceRegistration.query.filter_by(device_id=device_id).first()
if existing:
# Update — always refresh last_seen_at and app/ios version
existing.user_id = user.id # re-bind if different user logs in same device
existing.device_name = device_name or existing.device_name
existing.app_version = app_version or existing.app_version
existing.ios_version = ios_version or existing.ios_version
existing.last_seen_at = now
db.session.commit()
log_action(ACTION_UPDATE, 'DeviceRegistration', existing.id,
f'{device_name} v{app_version}',
f'user={user.username}; ios={ios_version}')
logger.info('API DEVICES | updated | device_id=%s | user=%s | app=%s',
device_id[:8], user.username, app_version)
else:
reg = DeviceRegistration(
device_id = device_id,
user_id = user.id,
device_name = device_name,
app_version = app_version,
ios_version = ios_version,
registered_at = now,
last_seen_at = now,
)
db.session.add(reg)
db.session.commit()
log_action(ACTION_CREATE, 'DeviceRegistration', reg.id,
f'{device_name} v{app_version}',
f'user={user.username}; ios={ios_version}')
logger.info('API DEVICES | registered | device_id=%s | user=%s | app=%s',
device_id[:8], user.username, app_version)
return api_ok({'registered': True})
+1 -2
View File
@@ -5,5 +5,4 @@ from app.models.inspection import (InspectionTemplate, ChecklistItem,
from app.models.issue import Issue
from app.models.project import Project, CustomerAssignment
from app.models.api_token import RefreshToken, DeviceToken
from app.models.notification_matrix import NotificationMatrix
from app.models.device_registration import DeviceRegistration
from app.models.notification_matrix import NotificationMatrix
-24
View File
@@ -1,24 +0,0 @@
# app/models/device_registration.py
# -----------------------------------
# Tracks iOS devices that have registered with the server.
# One row per physical device — upserted on every app foreground.
from app import db
from app.utils.time_utils import now_eastern
class DeviceRegistration(db.Model):
__tablename__ = 'device_registrations'
id = db.Column(db.Integer, primary_key=True)
# Stable UUID generated on first launch and stored in iOS Keychain.
# Unique across all devices; survives app restarts but not device wipes.
device_id = db.Column(db.String(64), nullable=False, unique=True, index=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
device_name = db.Column(db.String(255), nullable=False, default='')
app_version = db.Column(db.String(32), nullable=False, default='')
ios_version = db.Column(db.String(32), nullable=False, default='')
registered_at = db.Column(db.DateTime, nullable=False, default=now_eastern)
last_seen_at = db.Column(db.DateTime, nullable=False, default=now_eastern)
user = db.relationship('User', backref=db.backref('devices', lazy='dynamic'))
-206
View File
@@ -1,206 +0,0 @@
#!/usr/bin/env python3
"""
scratch_bootstrap_test.py
=========================
One-shot, self-cleaning test that proves the squashed baseline DDL executes on
real MySQL and that bootstrap_tenant() builds a complete, head-stamped schema.
It mirrors what MT-3 provisioning will do:
1. (as a MySQL admin) CREATE a throwaway database + user + grant
2. insert a temporary tenant row in the control DB (encrypted creds)
3. run control.tenant_migrate.bootstrap_tenant() against it
4. verify: all expected tables built + alembic_version stamped at chain head
5. tear everything down (tenant row, provisioning jobs, database, user)
Nothing touches your real LT database or any real tenant. Run from the REPO ROOT.
Prerequisites in the environment (same as the runner):
export CONTROL_DATABASE_URL='mysql+pymysql://jqc_control:pw@127.0.0.1/jqc_control'
export CONTROL_FERNET_KEY='...'
Example:
python scratch_bootstrap_test.py \
--admin-user root --admin-password 'ROOTPW' \
--host 127.0.0.1 --port 3306
Optional overrides: --test-db, --test-user, --test-password, --user-host, --keep
"""
import argparse
import sys
import pymysql
from sqlalchemy import create_engine, inspect, text
from control.base import control_session
from control.models import Plan, Tenant, ProvisioningJob
from control.seed import seed_plans
from control.tenant_migrate import bootstrap_tenant, chain_head
EXPECTED_TABLES = {
'api_device_tokens', 'api_refresh_tokens', 'areas', 'audit_logs', 'broadcasts',
'checklist_items', 'customer_assignments', 'device_registrations', 'facilities',
'facility_score_alerts', 'inspection_results', 'inspection_templates',
'inspections', 'inspector_assignments', 'issue_comments', 'issue_followers',
'issues', 'notification_matrix', 'notification_preferences', 'notifications',
'projects', 'scheduled_reports', 'support_ticket_replies', 'support_tickets',
'users',
}
def _admin_conn(args):
return pymysql.connect(
host=args.host, port=args.port,
user=args.admin_user, password=args.admin_password,
autocommit=True,
)
def _admin_exec(conn, sql):
with conn.cursor() as cur:
cur.execute(sql)
def create_test_db(args):
conn = _admin_conn(args)
try:
_admin_exec(conn, f"DROP DATABASE IF EXISTS `{args.test_db}`")
_admin_exec(conn, f"CREATE DATABASE `{args.test_db}` "
f"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
_admin_exec(conn, f"DROP USER IF EXISTS '{args.test_user}'@'{args.user_host}'")
_admin_exec(conn, f"CREATE USER '{args.test_user}'@'{args.user_host}' "
f"IDENTIFIED BY '{args.test_password}'")
_admin_exec(conn, f"GRANT ALL PRIVILEGES ON `{args.test_db}`.* "
f"TO '{args.test_user}'@'{args.user_host}'")
_admin_exec(conn, "FLUSH PRIVILEGES")
finally:
conn.close()
print(f" [setup] created database `{args.test_db}` + user "
f"'{args.test_user}'@'{args.user_host}'")
def drop_test_db(args):
conn = _admin_conn(args)
try:
_admin_exec(conn, f"DROP DATABASE IF EXISTS `{args.test_db}`")
_admin_exec(conn, f"DROP USER IF EXISTS '{args.test_user}'@'{args.user_host}'")
_admin_exec(conn, "FLUSH PRIVILEGES")
finally:
conn.close()
print(f" [cleanup] dropped database + user")
def insert_temp_tenant(args):
seed_plans() # idempotent — ensures at least one plan exists
with control_session() as s:
plan = s.query(Plan).order_by(Plan.id).first()
t = Tenant(
slug=args.test_slug, name='SCRATCH TEST (temporary)',
plan_id=plan.id, status='provisioning',
db_host=args.host, db_port=args.port,
db_name=args.test_db, db_user=args.test_user,
)
t.set_db_password(args.test_password)
s.add(t)
s.flush()
tid = t.id
print(f" [setup] inserted temp tenant id={tid} slug='{args.test_slug}'")
return tid
def delete_temp_tenant(tid):
with control_session() as s:
s.query(ProvisioningJob).filter_by(tenant_id=tid).delete()
t = s.get(Tenant, tid)
if t is not None:
s.delete(t)
print(f" [cleanup] removed temp tenant id={tid} + its provisioning jobs")
def verify(args):
uri = (f"mysql+pymysql://{args.test_user}:{args.test_password}"
f"@{args.host}:{args.port}/{args.test_db}")
engine = create_engine(uri, future=True)
ok = True
try:
insp = inspect(engine)
built = set(insp.get_table_names())
built.discard('alembic_version')
missing = EXPECTED_TABLES - built
extra = built - EXPECTED_TABLES
print(f" tables built: {len(built)} (expected {len(EXPECTED_TABLES)})")
if missing:
print(f" !! MISSING tables: {sorted(missing)}"); ok = False
if extra:
print(f" ?? unexpected extra tables: {sorted(extra)}")
with engine.connect() as c:
ver = c.execute(text("SELECT version_num FROM alembic_version")).scalar()
head = chain_head()
print(f" alembic_version: {ver} (chain head: {head})")
if ver != head:
print(" !! alembic_version is not at chain head"); ok = False
# spot-check a couple of tables have their columns
ucols = {c['name'] for c in insp.get_columns('users')}
for need in ('id', 'username', 'email', 'password_hash', 'role'):
if need not in ucols:
print(f" !! users.{need} missing"); ok = False
print(f" users columns: {len(ucols)} (incl id/username/email/role)")
finally:
engine.dispose()
return ok
def main():
ap = argparse.ArgumentParser(description='Scratch MySQL bootstrap test')
ap.add_argument('--admin-user', required=True, help='MySQL admin user (can CREATE DB/USER)')
ap.add_argument('--admin-password', required=True)
ap.add_argument('--host', default='127.0.0.1')
ap.add_argument('--port', type=int, default=3306)
ap.add_argument('--test-db', default='jqc_scratch_test')
ap.add_argument('--test-user', default='jqc_scratch_user')
ap.add_argument('--test-password', default='Scratch_pw_123!')
ap.add_argument('--test-slug', default='scratchtest')
ap.add_argument('--user-host', default='%', help="MySQL host part for the test user")
ap.add_argument('--keep', action='store_true', help='Skip cleanup (inspect manually)')
args = ap.parse_args()
print("== JQC scratch bootstrap test ==")
tid = None
passed = False
try:
create_test_db(args)
tid = insert_temp_tenant(args)
print(" [run] bootstrap_tenant() ...")
with control_session() as s:
t = s.get(Tenant, tid)
ref = type('R', (), {'id': t.id, 'db_uri': t.db_uri})()
applied = bootstrap_tenant(ref)
print(f" [run] bootstrapped -> {applied}")
passed = verify(args)
except Exception as e:
print(f" !! ERROR: {type(e).__name__}: {e}")
passed = False
finally:
if not args.keep:
if tid is not None:
try:
delete_temp_tenant(tid)
except Exception as e:
print(f" !! tenant cleanup failed: {e}")
try:
drop_test_db(args)
except Exception as e:
print(f" !! db cleanup failed: {e}")
else:
print(" [keep] left test DB + tenant in place for inspection")
print()
print("RESULT:", "PASS ✅" if passed else "FAIL ❌")
sys.exit(0 if passed else 1)
if __name__ == '__main__':
main()