-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
184 lines (142 loc) · 5.84 KB
/
Copy pathapp.py
File metadata and controls
184 lines (142 loc) · 5.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# coding: utf-8
# Code By DaTi_Co
import os
import logging
from typing import Any, cast
from flask import Flask, send_from_directory
from flask_login import LoginManager
from firebase_admin import credentials, initialize_app, get_app
from sqlalchemy import inspect, text
from auth import auth
from models import User, db
from my_oauth import init_oauth
from notifications import mqtt
from routes import bp
# Module logger
logger = logging.getLogger(__name__)
def _is_production_environment() -> bool:
"""Return True when runtime environment is production-like."""
environment = os.getenv('APP_ENV', os.getenv('FLASK_ENV', 'development')).strip().lower()
return environment in {'production', 'prod'}
def _get_config_object() -> str:
"""Resolve config from explicit APP_ENV with FLASK_ENV fallback."""
if _is_production_environment():
return 'config.ProductionConfig'
return 'config.DevelopmentConfig'
def _init_firebase(flask_app: Flask) -> None:
"""Initialize Firebase only when required configuration is available."""
# Avoid duplicate default app initialization when module reload/import happens.
try:
get_app()
logger.info('Firebase already initialized; skipping re-initialization.')
return
except ValueError:
pass
service_account_data = flask_app.config.get('SERVICE_ACCOUNT_DATA')
database_url = flask_app.config.get('DATABASEURL')
if not service_account_data or not database_url:
logger.warning('Firebase config missing; skipping Firebase initialization.')
return
firebase_credentials = credentials.Certificate(service_account_data)
firebase_options = {'databaseURL': database_url}
initialize_app(firebase_credentials, firebase_options)
# Flask Application Configuration
app = Flask(__name__, template_folder='templates')
app.config.from_object(_get_config_object())
if not _is_production_environment():
# Authlib requires HTTPS by default; allow HTTP for local/dev/test only.
os.environ.setdefault('AUTHLIB_INSECURE_TRANSPORT', '1')
logger.info('ENV is set to: %s', app.config.get('ENV'))
app.register_blueprint(bp, url_prefix='')
app.register_blueprint(auth, url_prefix='')
# MQTT CONNECT
try:
logger.info(
'Initializing MQTT broker at %s:%s (TLS=%s)',
app.config.get('MQTT_BROKER_URL'),
app.config.get('MQTT_BROKER_PORT'),
app.config.get('MQTT_TLS_ENABLED'),
)
mqtt.init_app(app)
# New multi-tenant topic structure: {user_id}/{device_id}/{notification|status}
mqtt.subscribe('+/+/notification')
mqtt.subscribe('+/+/status')
logger.info('MQTT initialized; connection lifecycle will be reported by on_connect/on_disconnect callbacks')
except Exception as e:
logger.warning('MQTT initialization skipped: %s', e)
# SQLAlchemy DATABASE
db.init_app(app)
# OAuth2 Authorisation
init_oauth(app)
# Flask Login
login_manager = LoginManager()
cast(Any, login_manager).login_view = 'auth.login'
login_manager.init_app(app)
# FIREBASE_CONFIG environment variable can be added
_init_firebase(app)
# File Extensions for Upload Folder
ALLOWED_EXTENSIONS = {'txt', 'py'}
@login_manager.user_loader
def load_user(user_id):
"""Get User ID"""
session = cast(Any, db.session)
return session.get(User, int(user_id))
def allowed_file(filename):
"""File Uploading Function"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def init_database_schema(flask_app: Flask) -> None:
"""Create DB tables if they do not exist."""
db_uri = flask_app.config.get('SQLALCHEMY_DATABASE_URI', '')
logger.info('DB Engine: %s', db_uri.split(':')[0] if db_uri else 'unknown')
with flask_app.app_context():
db.create_all()
_ensure_password_column_capacity()
logger.info('Initialized the database.')
def _ensure_password_column_capacity() -> None:
"""Ensure legacy user.password columns can store modern Werkzeug hashes."""
inspector = inspect(db.engine)
table_names = set(inspector.get_table_names())
if 'user' not in table_names:
return
columns = {column['name']: column for column in inspector.get_columns('user')}
password_column = columns.get('password')
if not password_column:
return
current_length = getattr(password_column['type'], 'length', None)
if current_length is None or current_length >= 255:
return
dialect = db.engine.dialect.name
alter_sql = _password_column_migration_sql(dialect)
if not alter_sql:
message = (
'Detected legacy password column length=%s on unsupported dialect=%s; '
'manual migration to VARCHAR(255) required.'
)
if _is_production_environment():
raise RuntimeError(message % (current_length, dialect))
logger.warning(message, current_length, dialect)
return
logger.info('Expanding user.password column from %s to 255.', current_length)
db.session.execute(text(alter_sql))
db.session.commit()
def _password_column_migration_sql(dialect: str) -> Any:
"""Return migration SQL for supported dialects, or None if unsupported."""
normalized_dialect = (dialect or '').strip().lower()
if normalized_dialect == 'postgresql':
return 'ALTER TABLE "user" ALTER COLUMN password TYPE VARCHAR(255)'
if normalized_dialect in {'mysql', 'mariadb'}:
return 'ALTER TABLE `user` MODIFY COLUMN password VARCHAR(255)'
return None
@app.route('/uploads/<filename>')
def uploaded_file(filename):
"""File formats for upload folder"""
return send_from_directory(app.config['UPLOAD_FOLDER'],
filename)
@app.cli.command('init-db')
def create_db_command() -> None:
"""Initialize database tables."""
init_database_schema(app)
if __name__ == '__main__':
init_database_schema(app)
app.run(debug=app.config.get('DEBUG', False))