Oviya commited on
Commit
c7da6fa
·
1 Parent(s): cef089a

Deploy: backend updates

Browse files
Files changed (3) hide show
  1. .env +8 -0
  2. requirements.txt +2 -1
  3. verification.py +155 -167
.env ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # .env — local development only (do NOT commit to git)
2
+ # Place this file next to verification.py
3
+
4
+ DB_USER=admin
5
+ DB_PASSWORD=Pykara123
6
+
7
+ # Run once to create tables, then change to 0 or remove.
8
+ RUN_INIT_DB=0
requirements.txt CHANGED
@@ -3,4 +3,5 @@ flask-cors
3
  pyodbc
4
  bcrypt
5
  PyJWT
6
- gunicorn
 
 
3
  pyodbc
4
  bcrypt
5
  PyJWT
6
+ gunicorn
7
+ python-dotenv
verification.py CHANGED
@@ -1,57 +1,63 @@
1
- import pyodbc
2
- from flask import Flask, request, jsonify, make_response
3
- from flask_cors import CORS
4
- import jwt
5
- import datetime
6
- import bcrypt
7
- from functools import wraps # Import wraps for decorators
8
- import pdb
9
- import os # add this
10
-
11
- app = Flask(__name__)
12
- # CORS(app,
13
- # supports_credentials=True,
14
- # resources={r"/*": {"origins": "http://localhost:4200"}})
15
 
 
 
16
 
17
 
18
- CORS(app, supports_credentials=True, origins=["http://localhost:4200"])
 
 
 
 
 
 
 
 
 
19
 
20
- # CORS(app, supports_credentials=True, origins=["http://localhost:4200"])
21
- # CORS(app, supports_credentials=True) # Enable CORS with credentials
 
 
 
22
  app.config['SECRET_KEY'] = '96c63da06374c1bde332516f3acbd23c84f35f90d8a6321a25d790a0a451af32'
23
 
24
- # Configure SQL Server Connection (Windows Authentication)
25
- DB_SERVER = "pykara-sqlserver.c5aosm6ie5j3.eu-north-1.rds.amazonaws.com,1433"
 
 
 
 
 
26
  DB_DATABASE = "AuthenticationDB1"
27
 
28
- # ✅ Create Connection String for Windows Authentication
29
- # conn_str = f"DRIVER={{SQL Server}};SERVER={DB_SERVER};DATABASE={DB_DATABASE};Trusted_Connection=yes"
30
- # Use Windows Authentication only for local SQLEXPRESS; otherwise use SQL login from environment
31
  if DB_SERVER.lower().startswith("localhost") or "\\" in DB_SERVER:
32
- # Local dev: Windows Authentication
33
- conn_str = f"DRIVER={{SQL Server}};SERVER={DB_SERVER};DATABASE={DB_DATABASE};Trusted_Connection=yes"
34
  else:
35
- # RDS: SQL authentication (no hard-coding; read from env)
36
- conn_str = (
37
  "DRIVER={ODBC Driver 17 for SQL Server};"
38
  f"SERVER={DB_SERVER};DATABASE={DB_DATABASE};"
39
  f"UID={os.getenv('DB_USER')};PWD={os.getenv('DB_PASSWORD')};"
40
  "Encrypt=yes;TrustServerCertificate=yes"
41
  )
42
 
43
-
44
- # ✅ Function to Connect to SQL Server
45
  def get_db_connection():
46
- return pyodbc.connect(conn_str)
 
 
 
 
47
 
48
- # ✅ Initialize the database (Create required tables if they do not exist)
49
  def init_db():
 
50
  conn = get_db_connection()
51
- cursor = conn.cursor()
52
 
53
- # ✅ Create Users Table
54
- cursor.execute("""
55
  IF OBJECT_ID('Users', 'U') IS NULL
56
  CREATE TABLE Users (
57
  id INT IDENTITY(1,1) PRIMARY KEY,
@@ -61,8 +67,7 @@ def init_db():
61
  )
62
  """)
63
 
64
- # ✅ Create Blacklisted Tokens Table (For Logout)
65
- cursor.execute("""
66
  IF OBJECT_ID('BlacklistedTokens', 'U') IS NULL
67
  CREATE TABLE BlacklistedTokens (
68
  id INT IDENTITY(1,1) PRIMARY KEY,
@@ -71,8 +76,7 @@ def init_db():
71
  )
72
  """)
73
 
74
- # ✅ Create Refresh Tokens Table (For Secure Token Refresh)
75
- cursor.execute("""
76
  IF OBJECT_ID('RefreshTokens', 'U') IS NULL
77
  CREATE TABLE RefreshTokens (
78
  id INT IDENTITY(1,1) PRIMARY KEY,
@@ -86,30 +90,53 @@ def init_db():
86
  conn.commit()
87
  conn.close()
88
 
89
- # ✅ Initialize Database (Call once when Flask starts)
90
- init_db()
91
-
92
- # Implement token verification function
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  def token_required(f):
94
  @wraps(f)
95
  def decorated(*args, **kwargs):
96
- print("🔐 Accessing protected route:", request.path)
97
- print("🍪 Cookies received:", request.cookies)
98
- token = request.cookies.get('access_token') # Access token from cookies
99
-
100
  if not token:
101
  return jsonify({"message": "Token is missing"}), 401
102
 
103
  try:
104
- # Check if token is blacklisted in SQL Server
105
  conn = get_db_connection()
106
- cursor = conn.cursor()
107
- cursor.execute("SELECT token FROM BlacklistedTokens WHERE token = ?", (token,))
108
- result = cursor.fetchone()
109
- conn.close()
110
-
111
- if result:
112
  return jsonify({"message": "Token has been revoked. Please log in again."}), 401
 
113
 
114
  data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
115
  return f(data['username'], *args, **kwargs)
@@ -118,85 +145,74 @@ def token_required(f):
118
  return jsonify({"message": "Token has expired"}), 401
119
  except jwt.InvalidTokenError:
120
  return jsonify({"message": "Invalid token"}), 401
121
-
 
 
122
  return decorated
123
 
124
- # ✅ Protected route: Dashboard
125
- @app.route('/dashboard', methods=['GET'])
 
 
126
  @token_required
127
  def dashboard(username):
128
  return jsonify({"message": f"Welcome {username} to your dashboard!"})
129
 
130
-
131
-
132
- # ✅ Login: Generate Access & Refresh Tokens (Uses SQL Server for authentication)
133
- @app.route('/login', methods=['POST'])
134
  def login():
135
- print("Login Request works")
136
- data = request.json
137
  username = data.get('username')
138
  password = data.get('password')
139
 
140
- conn = get_db_connection()
141
- cursor = conn.cursor()
142
-
143
- # Retrieve user from SQL Server
144
- cursor.execute("SELECT password_hash FROM Users WHERE username = ?", (username,))
145
- user = cursor.fetchone()
146
-
147
- conn.close()
 
148
 
149
- if not user:
150
  return jsonify({"message": "Invalid credentials"}), 401
151
 
152
- stored_hashed_password = user[0]
153
-
154
- if bcrypt.checkpw(password.encode('utf-8'), stored_hashed_password.encode('utf-8')):
155
- # ✅ Generate Access Token (expires in 15 minutes)
156
- access_token = jwt.encode(
157
- {'username': username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15)},
158
- app.config['SECRET_KEY'],
159
- algorithm="HS256"
160
- )
161
- print(f"Generated Access Token: {access_token}")
162
- # ✅ Generate Refresh Token (expires in 7 days)
163
- refresh_token = jwt.encode(
164
- {'username': username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7)},
165
- app.config['SECRET_KEY'],
166
- algorithm="HS256"
167
- )
168
 
169
-
170
- print(f"Generated Refresh Token: {refresh_token}")
 
 
 
 
 
 
 
 
171
 
172
- # ✅ Store refresh token in SQL Server
173
  conn = get_db_connection()
174
- cursor = conn.cursor()
175
- cursor.execute("INSERT INTO RefreshTokens (username, token) VALUES (?, ?)", (username, refresh_token))
176
  conn.commit()
177
  conn.close()
 
 
 
178
 
 
 
 
 
179
 
180
- # Set the tokens in cookies
181
- resp = make_response(jsonify({"message": "Login successful"}))
182
-
183
- resp.set_cookie('access_token', access_token, httponly=True, secure=False, samesite='Lax', max_age=900) # Adjusted secure flag for development (set to True for production)
184
- resp.set_cookie('refresh_token', refresh_token, httponly=True, secure=False, samesite='Lax', max_age=7*24*60*60 ) # Same adjustment for refresh token
185
- print(f"Set Cookies: access_token={access_token}, refresh_token={refresh_token}")
186
-
187
-
188
- return resp
189
- return jsonify({"message": "Invalid credentials"}), 401
190
-
191
- # ✅ Refresh Token: Get New Access Token
192
- @app.route('/refresh', methods=['POST'])
193
  def refresh():
194
  refresh_token = request.cookies.get("refresh_token")
195
-
196
  if not refresh_token:
197
  return jsonify({'message': 'Refresh token is missing'}), 400
198
 
199
- # Step 1: Decode and verify expiration
200
  try:
201
  payload = jwt.decode(refresh_token, app.config['SECRET_KEY'], algorithms=["HS256"])
202
  except jwt.ExpiredSignatureError:
@@ -204,99 +220,71 @@ def refresh():
204
  except jwt.InvalidTokenError:
205
  return jsonify({'message': 'Invalid refresh token'}), 401
206
 
207
- # Step 2: Check if the token exists in DB
208
- conn = get_db_connection()
209
- cursor = conn.cursor()
210
- cursor.execute("SELECT username FROM RefreshTokens WHERE token = ?", (refresh_token,))
211
- result = cursor.fetchone()
212
- conn.close()
 
 
 
213
 
214
- if not result:
215
  return jsonify({'message': 'Invalid refresh token'}), 401
216
 
217
- username = result[0]
218
-
219
- # Step 3: Generate new access token
220
- new_access_token = jwt.encode(
221
  {'username': username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15)},
222
  app.config['SECRET_KEY'],
223
  algorithm="HS256"
224
  )
225
 
226
- resp = make_response(jsonify({'access_token': new_access_token}))
227
- resp.set_cookie(
228
- 'access_token',
229
- new_access_token,
230
- httponly=True,
231
- secure=False,
232
- samesite='Lax',
233
- max_age=900 # 15 minutes
234
- )
235
-
236
  return resp
237
 
238
-
239
-
240
-
241
- @app.route('/logout', methods=['POST'])
242
  @token_required
243
  def logout(username):
244
- print("Logout Request works")
245
- print("Incoming Cookies:", request.cookies)
246
-
247
- # ✅ Get the access token from cookies
248
  token = request.cookies.get('access_token')
249
- print("Logout Request - Token received:", token)
250
-
251
  if not token:
252
  return jsonify({"message": "Invalid token format"}), 401
253
 
254
  try:
255
- print("Decoding token...")
256
- # ✅ Decode the token manually to extract username
257
  data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
258
  username = data['username']
259
- print(f"Token decoded successfully. Username: {username}, Expiry: {data['exp']}")
260
-
261
  except jwt.ExpiredSignatureError:
262
  return jsonify({"message": "Token has expired"}), 401
263
  except jwt.InvalidTokenError:
264
  return jsonify({"message": "Invalid token"}), 401
265
 
266
- print(f"Blacklisting token: {token}")
267
-
268
- # Store the revoked token in SQL Server **only if it's not already blacklisted**
269
- conn = get_db_connection()
270
- cursor = conn.cursor()
271
-
272
- # Check if the token is already blacklisted
273
- cursor.execute("SELECT token FROM BlacklistedTokens WHERE token = ?", (token,))
274
- existing_token = cursor.fetchone()
275
-
276
- if not existing_token:
277
- cursor.execute("INSERT INTO BlacklistedTokens (token) VALUES (?)", (token,))
278
-
279
- # ✅ Remove refresh token for the user
280
- cursor.execute("DELETE FROM RefreshTokens WHERE username = ?", (username,))
281
-
282
- conn.commit()
283
- conn.close()
284
 
285
- # ✅ Clear cookies
286
  resp = make_response(jsonify({"message": "Logged out successfully!"}))
287
  resp.delete_cookie('access_token', path='/')
288
  resp.delete_cookie('refresh_token', path='/')
289
-
290
  return resp
291
 
292
- @app.route('/check-auth', methods=['GET'])
293
  @token_required
294
  def check_auth(username):
295
  return jsonify({"message": "Authenticated", "username": username}), 200
296
 
297
-
298
-
299
-
300
-
301
  if __name__ == '__main__':
302
- app.run(debug=True)
 
 
1
+ # --- load .env FIRST ---
2
+ import os
3
+ from dotenv import load_dotenv
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ BASEDIR = os.path.abspath(os.path.dirname(__file__))
6
+ load_dotenv(os.path.join(BASEDIR, ".env")) # loads DB_USER, DB_PASSWORD, RUN_INIT_DB
7
 
8
 
9
+ # import os
10
+ import logging
11
+ from threading import Lock
12
+ from functools import wraps
13
+ import datetime
14
+ import bcrypt
15
+ import jwt
16
+ import pyodbc
17
+ from flask import Flask, request, jsonify, make_response
18
+ from flask_cors import CORS
19
 
20
+ # ------------------------------------------------------------------------------
21
+ # App & CORS
22
+ # ------------------------------------------------------------------------------
23
+ app = Flask(__name__)
24
+ CORS(app, supports_credentials=True, origins=["http://localhost:4200"]) # add your prod origins later
25
  app.config['SECRET_KEY'] = '96c63da06374c1bde332516f3acbd23c84f35f90d8a6321a25d790a0a451af32'
26
 
27
+ # Optional: cleaner logs on Spaces / local
28
+ logging.basicConfig(level=logging.INFO)
29
+
30
+ # ------------------------------------------------------------------------------
31
+ # SQL Server configuration
32
+ # ------------------------------------------------------------------------------
33
+ DB_SERVER = "pykara-sqlserver.c5aosm6ie5j3.eu-north-1.rds.amazonaws.com,1433"
34
  DB_DATABASE = "AuthenticationDB1"
35
 
 
 
 
36
  if DB_SERVER.lower().startswith("localhost") or "\\" in DB_SERVER:
37
+ # Local Windows SQL Express with integrated auth
38
+ CONN_STR = f"DRIVER={{SQL Server}};SERVER={DB_SERVER};DATABASE={DB_DATABASE};Trusted_Connection=yes"
39
  else:
40
+ # RDS / SQL login via env secrets
41
+ CONN_STR = (
42
  "DRIVER={ODBC Driver 17 for SQL Server};"
43
  f"SERVER={DB_SERVER};DATABASE={DB_DATABASE};"
44
  f"UID={os.getenv('DB_USER')};PWD={os.getenv('DB_PASSWORD')};"
45
  "Encrypt=yes;TrustServerCertificate=yes"
46
  )
47
 
 
 
48
  def get_db_connection():
49
+ """Create a short-timeout connection. Fail clearly if secrets are missing."""
50
+ if "Trusted_Connection=yes" not in CONN_STR:
51
+ if not os.getenv("DB_USER") or not os.getenv("DB_PASSWORD"):
52
+ raise RuntimeError("DB_USER/DB_PASSWORD are not set in the environment.")
53
+ return pyodbc.connect(CONN_STR, timeout=5)
54
 
 
55
  def init_db():
56
+ """Create tables if they do not exist."""
57
  conn = get_db_connection()
58
+ cur = conn.cursor()
59
 
60
+ cur.execute("""
 
61
  IF OBJECT_ID('Users', 'U') IS NULL
62
  CREATE TABLE Users (
63
  id INT IDENTITY(1,1) PRIMARY KEY,
 
67
  )
68
  """)
69
 
70
+ cur.execute("""
 
71
  IF OBJECT_ID('BlacklistedTokens', 'U') IS NULL
72
  CREATE TABLE BlacklistedTokens (
73
  id INT IDENTITY(1,1) PRIMARY KEY,
 
76
  )
77
  """)
78
 
79
+ cur.execute("""
 
80
  IF OBJECT_ID('RefreshTokens', 'U') IS NULL
81
  CREATE TABLE RefreshTokens (
82
  id INT IDENTITY(1,1) PRIMARY KEY,
 
90
  conn.commit()
91
  conn.close()
92
 
93
+ # ------------------------------------------------------------------------------
94
+ # One-time DB initialisation (Flask 3.x safe)
95
+ # ------------------------------------------------------------------------------
96
+ _db_init_done = False
97
+ _db_init_lock = Lock()
98
+ _should_init = os.getenv("RUN_INIT_DB", "0") == "1"
99
+
100
+ @app.before_request
101
+ def maybe_init_db():
102
+ global _db_init_done
103
+ if _should_init and not _db_init_done:
104
+ with _db_init_lock:
105
+ if not _db_init_done:
106
+ try:
107
+ init_db()
108
+ app.logger.info("Database initialised.")
109
+ except Exception as e:
110
+ app.logger.exception("DB init failed: %s", e)
111
+ finally:
112
+ _db_init_done = True
113
+
114
+ # ------------------------------------------------------------------------------
115
+ # Health endpoint (helps confirm worker booted)
116
+ # ------------------------------------------------------------------------------
117
+ @app.get("/")
118
+ def health():
119
+ return {"status": "ok"}, 200
120
+
121
+ # ------------------------------------------------------------------------------
122
+ # Auth utilities
123
+ # ------------------------------------------------------------------------------
124
  def token_required(f):
125
  @wraps(f)
126
  def decorated(*args, **kwargs):
127
+ token = request.cookies.get('access_token')
 
 
 
128
  if not token:
129
  return jsonify({"message": "Token is missing"}), 401
130
 
131
  try:
132
+ # Check blacklist
133
  conn = get_db_connection()
134
+ cur = conn.cursor()
135
+ cur.execute("SELECT token FROM BlacklistedTokens WHERE token = ?", (token,))
136
+ if cur.fetchone():
137
+ conn.close()
 
 
138
  return jsonify({"message": "Token has been revoked. Please log in again."}), 401
139
+ conn.close()
140
 
141
  data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
142
  return f(data['username'], *args, **kwargs)
 
145
  return jsonify({"message": "Token has expired"}), 401
146
  except jwt.InvalidTokenError:
147
  return jsonify({"message": "Invalid token"}), 401
148
+ except Exception as e:
149
+ app.logger.exception("Auth error: %s", e)
150
+ return jsonify({"message": "Server error"}), 500
151
  return decorated
152
 
153
+ # ------------------------------------------------------------------------------
154
+ # Routes
155
+ # ------------------------------------------------------------------------------
156
+ @app.get("/dashboard")
157
  @token_required
158
  def dashboard(username):
159
  return jsonify({"message": f"Welcome {username} to your dashboard!"})
160
 
161
+ @app.post("/login")
 
 
 
162
  def login():
163
+ data = request.json or {}
 
164
  username = data.get('username')
165
  password = data.get('password')
166
 
167
+ try:
168
+ conn = get_db_connection()
169
+ cur = conn.cursor()
170
+ cur.execute("SELECT password_hash FROM Users WHERE username = ?", (username,))
171
+ row = cur.fetchone()
172
+ conn.close()
173
+ except Exception as e:
174
+ app.logger.exception("DB access error on login: %s", e)
175
+ return jsonify({"message": "Database is unavailable"}), 503
176
 
177
+ if not row:
178
  return jsonify({"message": "Invalid credentials"}), 401
179
 
180
+ stored_hash = row[0]
181
+ if not bcrypt.checkpw(password.encode('utf-8'), stored_hash.encode('utf-8')):
182
+ return jsonify({"message": "Invalid credentials"}), 401
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ access_token = jwt.encode(
185
+ {'username': username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15)},
186
+ app.config['SECRET_KEY'],
187
+ algorithm="HS256"
188
+ )
189
+ refresh_token = jwt.encode(
190
+ {'username': username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7)},
191
+ app.config['SECRET_KEY'],
192
+ algorithm="HS256"
193
+ )
194
 
195
+ try:
196
  conn = get_db_connection()
197
+ cur = conn.cursor()
198
+ cur.execute("INSERT INTO RefreshTokens (username, token) VALUES (?, ?)", (username, refresh_token))
199
  conn.commit()
200
  conn.close()
201
+ except Exception as e:
202
+ app.logger.exception("DB write error on login: %s", e)
203
+ return jsonify({"message": "Database is unavailable"}), 503
204
 
205
+ resp = make_response(jsonify({"message": "Login successful"}))
206
+ resp.set_cookie('access_token', access_token, httponly=True, secure=False, samesite='Lax', max_age=900)
207
+ resp.set_cookie('refresh_token', refresh_token, httponly=True, secure=False, samesite='Lax', max_age=7*24*60*60)
208
+ return resp
209
 
210
+ @app.post("/refresh")
 
 
 
 
 
 
 
 
 
 
 
 
211
  def refresh():
212
  refresh_token = request.cookies.get("refresh_token")
 
213
  if not refresh_token:
214
  return jsonify({'message': 'Refresh token is missing'}), 400
215
 
 
216
  try:
217
  payload = jwt.decode(refresh_token, app.config['SECRET_KEY'], algorithms=["HS256"])
218
  except jwt.ExpiredSignatureError:
 
220
  except jwt.InvalidTokenError:
221
  return jsonify({'message': 'Invalid refresh token'}), 401
222
 
223
+ try:
224
+ conn = get_db_connection()
225
+ cur = conn.cursor()
226
+ cur.execute("SELECT username FROM RefreshTokens WHERE token = ?", (refresh_token,))
227
+ row = cur.fetchone()
228
+ conn.close()
229
+ except Exception as e:
230
+ app.logger.exception("DB access error on refresh: %s", e)
231
+ return jsonify({"message": "Database is unavailable"}), 503
232
 
233
+ if not row:
234
  return jsonify({'message': 'Invalid refresh token'}), 401
235
 
236
+ username = row[0]
237
+ new_access = jwt.encode(
 
 
238
  {'username': username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15)},
239
  app.config['SECRET_KEY'],
240
  algorithm="HS256"
241
  )
242
 
243
+ resp = make_response(jsonify({'access_token': new_access}))
244
+ resp.set_cookie('access_token', new_access, httponly=True, secure=False, samesite='Lax', max_age=900)
 
 
 
 
 
 
 
 
245
  return resp
246
 
247
+ @app.post("/logout")
 
 
 
248
  @token_required
249
  def logout(username):
 
 
 
 
250
  token = request.cookies.get('access_token')
 
 
251
  if not token:
252
  return jsonify({"message": "Invalid token format"}), 401
253
 
254
  try:
 
 
255
  data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
256
  username = data['username']
 
 
257
  except jwt.ExpiredSignatureError:
258
  return jsonify({"message": "Token has expired"}), 401
259
  except jwt.InvalidTokenError:
260
  return jsonify({"message": "Invalid token"}), 401
261
 
262
+ try:
263
+ conn = get_db_connection()
264
+ cur = conn.cursor()
265
+ cur.execute("SELECT token FROM BlacklistedTokens WHERE token = ?", (token,))
266
+ if not cur.fetchone():
267
+ cur.execute("INSERT INTO BlacklistedTokens (token) VALUES (?)", (token,))
268
+ cur.execute("DELETE FROM RefreshTokens WHERE username = ?", (username,))
269
+ conn.commit()
270
+ conn.close()
271
+ except Exception as e:
272
+ app.logger.exception("DB write error on logout: %s", e)
273
+ return jsonify({"message": "Database is unavailable"}), 503
 
 
 
 
 
 
274
 
 
275
  resp = make_response(jsonify({"message": "Logged out successfully!"}))
276
  resp.delete_cookie('access_token', path='/')
277
  resp.delete_cookie('refresh_token', path='/')
 
278
  return resp
279
 
280
+ @app.get("/check-auth")
281
  @token_required
282
  def check_auth(username):
283
  return jsonify({"message": "Authenticated", "username": username}), 200
284
 
285
+ # ------------------------------------------------------------------------------
286
+ # Local run (Gunicorn will import `verification:app` on Spaces)
287
+ # ------------------------------------------------------------------------------
 
288
  if __name__ == '__main__':
289
+ port = int(os.getenv("PORT", "5000"))
290
+ app.run(host="0.0.0.0", port=port, debug=True)