basyx commited on
Commit
5ab4e62
·
verified ·
1 Parent(s): 5ed7b3d

Update auth/security.py

Browse files
Files changed (1) hide show
  1. auth/security.py +274 -134
auth/security.py CHANGED
@@ -1,186 +1,326 @@
1
- """
2
- auth/security.py
3
- Enterprise Authentication Security Layer
4
- Basyx Whisper V10.1
5
- """
 
 
6
 
7
- from datetime import datetime, timedelta, timezone
8
- from typing import Optional, Dict, Any
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- from jose import JWTError, jwt
11
- from passlib.context import CryptContext
 
 
 
12
 
13
- from utils.logger import logger
 
14
 
15
- # ==========================================================
16
- # SECURITY CONSTANTS
17
- # ==========================================================
 
18
 
19
- # IMPORTANT:
20
- # Replace ONLY via environment variable in production.
21
- # No fallback assumptions allowed.
22
- import os
23
 
24
- JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
25
- if not JWT_SECRET_KEY:
26
- raise RuntimeError("JWT_SECRET_KEY environment variable is required")
27
 
28
- JWT_ALGORITHM = "HS256"
29
- ACCESS_TOKEN_EXPIRE_MINUTES = int(
30
- os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", "60")
31
- )
32
 
33
- # bcrypt safe configuration
34
- pwd_context = CryptContext(
35
- schemes=["bcrypt"],
36
- deprecated="auto",
37
  )
38
 
39
- # ==========================================================
40
- # PASSWORD UTILITIES
41
- # ==========================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
 
43
 
44
- def _sanitize_password(password: str) -> bytes:
45
- """
46
- bcrypt only supports 72 bytes.
 
 
47
 
48
- Enterprise rule:
49
- - Never silently fail
50
- - Deterministically truncate
51
- """
52
 
53
- if not isinstance(password, str):
54
- raise TypeError("Password must be string")
55
 
56
- encoded = password.encode("utf-8")
 
 
 
57
 
58
- if len(encoded) > 72:
59
- logger.warning("Password exceeded bcrypt limit — truncated safely")
60
- encoded = encoded[:72]
61
 
62
- return encoded
 
63
 
 
 
64
 
65
- def hash_password(password: str) -> str:
66
- """
67
- Secure password hashing
68
- """
69
 
70
- try:
71
- safe_password = _sanitize_password(password)
72
- return pwd_context.hash(safe_password)
73
- except Exception as e:
74
- logger.exception("Password hashing failed")
75
- raise RuntimeError("Password hashing failure") from e
76
 
 
 
77
 
78
- def verify_password(plain_password: str, hashed_password: str) -> bool:
79
- """
80
- Verify password against stored hash
81
- """
82
 
83
- try:
84
- safe_password = _sanitize_password(plain_password)
85
- return pwd_context.verify(safe_password, hashed_password)
86
- except Exception:
87
- logger.warning("Password verification failed")
88
- return False
89
 
 
 
 
90
 
91
- # ==========================================================
92
- # JWT TOKEN MANAGEMENT
93
- # ==========================================================
94
 
 
 
 
95
 
96
- def create_access_token(
97
- subject: str,
98
- additional_claims: Optional[Dict[str, Any]] = None,
99
- ) -> str:
100
- """
101
- Create signed JWT access token
102
- """
103
 
104
- if not subject:
105
- raise ValueError("Token subject required")
106
 
107
- expire = datetime.now(timezone.utc) + timedelta(
108
- minutes=ACCESS_TOKEN_EXPIRE_MINUTES
109
- )
110
 
111
- payload: Dict[str, Any] = {
112
- "sub": subject,
113
- "exp": expire,
114
- "iat": datetime.now(timezone.utc),
115
- "type": "access",
116
- }
117
 
118
- if additional_claims:
119
- payload.update(additional_claims)
 
 
120
 
121
- token = jwt.encode(
122
- payload,
123
- JWT_SECRET_KEY,
124
- algorithm=JWT_ALGORITHM,
125
- )
 
 
 
 
 
 
126
 
127
- return token
 
128
 
 
 
 
129
 
130
- def decode_access_token(token: str) -> Optional[Dict[str, Any]]:
131
- """
132
- Decode and validate JWT token
133
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  try:
136
- payload = jwt.decode(
137
- token,
138
- JWT_SECRET_KEY,
139
- algorithms=[JWT_ALGORITHM],
140
- )
141
 
142
- if payload.get("type") != "access":
143
- logger.warning("Invalid token type")
144
- return None
145
 
146
- return payload
147
 
148
- except JWTError:
149
- logger.warning("JWT decode failed")
150
- return None
151
 
 
 
152
 
153
- # ==========================================================
154
- # AUTH HELPER FUNCTIONS
155
- # ==========================================================
156
 
 
 
 
157
 
158
- def get_subject_from_token(token: str) -> Optional[str]:
159
- """
160
- Extract user identity from token
161
- """
162
 
163
- payload = decode_access_token(token)
 
 
 
 
 
 
 
 
 
 
164
 
165
- if not payload:
166
- return None
167
 
168
- return payload.get("sub")
 
 
 
169
 
 
 
170
 
171
- # ==========================================================
172
- # HEALTH CHECK (DEBUG SAFE)
173
- # ==========================================================
 
 
 
 
 
 
 
 
 
 
 
 
174
 
 
 
 
 
 
 
 
 
175
 
176
- def security_status() -> dict:
177
- """
178
- Internal verification helper.
179
- Safe for health diagnostics.
180
- """
 
 
 
181
 
182
- return {
183
- "jwt_algorithm": JWT_ALGORITHM,
184
- "token_expire_minutes": ACCESS_TOKEN_EXPIRE_MINUTES,
185
- "bcrypt_scheme": pwd_context.schemes(),
186
- }
 
1
+ from fastapi import FastAPI, UploadFile, File, Form, Request
2
+ from fastapi.responses import FileResponse, JSONResponse
3
+ from contextlib import asynccontextmanager
4
+ import os
5
+ import uuid
6
+ import asyncio
7
+ import gradio as gr
8
 
9
+ # ==============================
10
+ # LOGGER + QUEUE
11
+ # ==============================
12
+ from utils.logger import logger
13
+ from utils.job_queue import start_worker, create_job, get_job
14
+ from ingestion.resolver import resolve_input
15
+
16
+ # ==============================
17
+ # AUTH SYSTEM
18
+ # ==============================
19
+ from auth.routes import router as auth_router
20
+ from auth.database import Base, engine
21
+
22
+ # ==============================
23
+ # CORE PIPELINE
24
+ # ==============================
25
+ from utils.transcription import transcribe_video
26
+ from utils.srt import generate_srt
27
+ from utils.render import render_subtitles
28
+ from utils.highlights import detect_highlights
29
+ from utils.viral_scorer import score_clip
30
+ from utils.director import rewrite_script, viral_score
31
+ from utils.engagement import simulate_retention
32
+ from utils.platform import adapt_platform
33
+ from utils.persona import predict_audience
34
+ from utils.clipper import create_clips
35
+ from utils.autonomous_engine import run_autonomous_engine
36
+
37
+ # ==============================
38
+ # PUBLISHER
39
+ # ==============================
40
+ from publisher.publisher_ai import autonomous_loop
41
+ from publisher.scheduler_engine import init_scheduler
42
+ from publisher.platform_dispatcher import dispatch_publish
43
+ from publisher.bulk import execute as bulk_execute
44
+ from publisher.metadata_engine import generate_metadata
45
+ from publisher.thumbnail_engine import generate_thumbnail
46
+
47
+ # ==============================
48
+ # SAFETY PATCH: bcrypt/passlib crash shield
49
+ # ==============================
50
+ import passlib
51
+ try:
52
+ import bcrypt # noqa
53
+ except Exception as e:
54
+ logger.warning(f"bcrypt import issue detected (ignored at runtime startup): {e}")
55
+
56
+ # ==============================
57
+ # INIT
58
+ # ==============================
59
+ UPLOAD_DIR = "jobs"
60
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
61
+
62
+
63
+ # ==============================
64
+ # LIFECYCLE (HARDENED)
65
+ # ==============================
66
+ @asynccontextmanager
67
+ async def lifespan(app: FastAPI):
68
+ logger.info("Starting Basyx Whisper V10.1")
69
 
70
+ try:
71
+ Base.metadata.create_all(bind=engine)
72
+ except Exception as e:
73
+ logger.error(f"DB init failed: {e}")
74
+ raise
75
 
76
+ start_worker()
77
+ init_scheduler()
78
 
79
+ try:
80
+ asyncio.create_task(autonomous_loop())
81
+ except Exception as e:
82
+ logger.error(f"autonomous_loop failed to start: {e}")
83
 
84
+ yield
 
 
 
85
 
86
+ logger.info("Shutdown complete")
 
 
87
 
 
 
 
 
88
 
89
+ app = FastAPI(
90
+ title="Basyx Whisper V10.1 Autonomous Operator",
91
+ lifespan=lifespan,
 
92
  )
93
 
94
+ app.include_router(auth_router)
95
+
96
+ # ==============================
97
+ # TASK REGISTRY
98
+ # ==============================
99
+ VALID_TASKS = {
100
+ "autonomous",
101
+ "auto-publish",
102
+ "publish",
103
+ "bulk-publish",
104
+ "generate-metadata",
105
+ "generate-thumbnail",
106
+ "schedule-post",
107
+ "transcribe",
108
+ "subtitles",
109
+ "render",
110
+ "highlights",
111
+ "viral-score",
112
+ "strategy",
113
+ "batch",
114
+ "clips",
115
+ }
116
+
117
+
118
+ def normalize_task(task: str):
119
+ task = task.lower().replace("_", "-")
120
+ if task not in VALID_TASKS:
121
+ raise Exception(f"Unknown task: {task}")
122
+ return task
123
+
124
+
125
+ # ==============================
126
+ # SAFE INPUT RESOLVER (FIXED)
127
+ # ==============================
128
+ async def safe_resolve(file, source):
129
+ try:
130
+ if not file and not source:
131
+ return None
132
 
133
+ upload_file = file if isinstance(file, UploadFile) else None
134
 
135
+ return await asyncio.to_thread(
136
+ resolve_input,
137
+ source,
138
+ upload_file,
139
+ )
140
 
141
+ except Exception as e:
142
+ logger.error(f"resolve_input failed: {str(e)}")
143
+ return None
 
144
 
 
 
145
 
146
+ # ==============================
147
+ # EXECUTION ENGINE
148
+ # ==============================
149
+ async def execute_task(video_path, task, payload=None, webhook=None):
150
 
151
+ payload = payload or {}
152
+ logger.info(f"[TASK] {task}")
 
153
 
154
+ if task == "bulk-publish":
155
+ return await bulk_execute(payload), None
156
 
157
+ if task not in ["bulk-publish", "schedule-post"] and not video_path:
158
+ return {"error": "No valid input resolved"}, None
159
 
160
+ if task == "autonomous":
161
+ return await asyncio.to_thread(run_autonomous_engine, video_path), None
 
 
162
 
163
+ if task == "auto-publish":
164
+ auto = await asyncio.to_thread(run_autonomous_engine, video_path)
165
+ return await dispatch_publish(variants=auto.get("all_variants", [])), None
 
 
 
166
 
167
+ if task == "publish":
168
+ return await dispatch_publish(video_path=video_path, payload=payload), None
169
 
170
+ if task == "generate-metadata":
171
+ return generate_metadata(video_path), None
 
 
172
 
173
+ if task == "generate-thumbnail":
174
+ output_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}.jpg")
175
+ thumb = generate_thumbnail(video_path, output=output_path)
176
+ return {"thumbnail": thumb}, output_path
 
 
177
 
178
+ if task == "batch":
179
+ job_id = create_job(video_path, webhook=webhook)
180
+ return {"status": "queued", "job_id": job_id}, None
181
 
182
+ if task == "transcribe":
183
+ words = await asyncio.to_thread(transcribe_video, video_path)
184
+ return {"words": words}, None
185
 
186
+ if task == "subtitles":
187
+ words = await asyncio.to_thread(transcribe_video, video_path)
188
+ return {"srt": generate_srt(words)}, None
189
 
190
+ if task == "render":
191
+ words = await asyncio.to_thread(transcribe_video, video_path)
192
+ srt = generate_srt(words)
 
 
 
 
193
 
194
+ output = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}_render.mp4")
 
195
 
196
+ await asyncio.to_thread(render_subtitles, video_path, srt, output)
 
 
197
 
198
+ return {"status": "render_complete"}, output
 
 
 
 
 
199
 
200
+ if task == "highlights":
201
+ words = await asyncio.to_thread(transcribe_video, video_path)
202
+ highlights = detect_highlights(words) or []
203
+ clips = create_clips(video_path, highlights)
204
 
205
+ return {"clips_created": len(clips)}, (clips[0] if clips else None)
206
+
207
+ if task == "clips":
208
+ words = await asyncio.to_thread(transcribe_video, video_path)
209
+ highlights = detect_highlights(words) or []
210
+ return {"clips": create_clips(video_path, highlights)}, None
211
+
212
+ if task == "viral-score":
213
+ words = await asyncio.to_thread(transcribe_video, video_path)
214
+ segments = detect_highlights(words) or []
215
+ return {"scores": [score_clip(s) for s in segments]}, None
216
 
217
+ if task == "strategy":
218
+ words = await asyncio.to_thread(transcribe_video, video_path)
219
 
220
+ script = rewrite_script(words)
221
+ persona = predict_audience(words)
222
+ curve = simulate_retention(words)
223
 
224
+ return {
225
+ "hook": script["hook"],
226
+ "persona": persona,
227
+ "viral_score": viral_score(curve),
228
+ "platforms": {
229
+ "tiktok": adapt_platform(script, "tiktok"),
230
+ "reels": adapt_platform(script, "reels"),
231
+ },
232
+ }, None
233
+
234
+ return {"error": "Task execution failed"}, None
235
+
236
+
237
+ # ==============================
238
+ # ROUTER
239
+ # ==============================
240
+ @app.post("/execute/{task_name}")
241
+ async def execute_router(
242
+ request: Request,
243
+ task_name: str,
244
+ file: UploadFile = File(None),
245
+ url_input: str = Form(None),
246
+ source: str = Form(None),
247
+ webhook: str = Form(None),
248
+ ):
249
 
250
  try:
251
+ task = normalize_task(task_name)
252
+ payload = {}
 
 
 
253
 
254
+ if request.headers.get("content-type", "").startswith("application/json"):
255
+ payload = await request.json()
 
256
 
257
+ video_path = await safe_resolve(file, url_input or source)
258
 
259
+ result, output = await execute_task(video_path, task, payload, webhook)
 
 
260
 
261
+ if output and isinstance(output, str) and os.path.exists(output):
262
+ return FileResponse(output)
263
 
264
+ return {"task": task, "result": result}
 
 
265
 
266
+ except Exception as e:
267
+ logger.exception(e)
268
+ return JSONResponse({"error": str(e)}, status_code=500)
269
 
 
 
 
 
270
 
271
+ # ==============================
272
+ # HEALTH
273
+ # ==============================
274
+ @app.get("/api/health")
275
+ def health():
276
+ return {"status": "online", "version": "V10.1"}
277
+
278
+
279
+ @app.get("/api/status/{job_id}")
280
+ def status(job_id: str):
281
+ return get_job(job_id) or {"error": "Job not found"}
282
 
 
 
283
 
284
+ # ==============================
285
+ # GRADIO UI
286
+ # ==============================
287
+ async def ui_handler(video, task, webhook, url_input):
288
 
289
+ source = url_input or video
290
+ video_path = await safe_resolve(video, source)
291
 
292
+ result, output = await execute_task(
293
+ video_path,
294
+ normalize_task(task),
295
+ {},
296
+ webhook,
297
+ )
298
+
299
+ return str(result), output
300
+
301
+
302
+ with gr.Blocks() as demo:
303
+ gr.Markdown("# 🚀 Basyx Whisper V10.1 Stable Operator")
304
+
305
+ video_input = gr.Video()
306
+ url_input = gr.Textbox(label="Video URL")
307
 
308
+ task_dropdown = gr.Dropdown(
309
+ choices=list(VALID_TASKS),
310
+ value="autonomous",
311
+ )
312
+
313
+ webhook_input = gr.Textbox(label="Webhook")
314
+
315
+ run_btn = gr.Button("Execute")
316
 
317
+ output_box = gr.Textbox()
318
+ video_output = gr.Video()
319
+
320
+ run_btn.click(
321
+ ui_handler,
322
+ [video_input, task_dropdown, webhook_input, url_input],
323
+ [output_box, video_output],
324
+ )
325
 
326
+ app = gr.mount_gradio_app(app, demo, path="/")