July 3rd - Review and optimize codes
This commit is contained in:
@@ -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 ─────────────────────────────────────────
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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})
|
||||
@@ -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
|
||||
@@ -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'))
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user