ciyidogan commited on
Commit
1e4a027
·
verified ·
1 Parent(s): 0e7f4f9

Upload 7 files

Browse files
utils/encrypt_string.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse, os, sys
2
+ from cryptography.fernet import Fernet, InvalidToken
3
+
4
+ ENV = "FLARE_TOKEN_KEY"
5
+
6
+ def get_fernet(key_arg: str | None) -> Fernet:
7
+ key = key_arg or os.getenv(ENV)
8
+ if not key:
9
+ print(f"[HATA] Anahtar yok. --key parametresi verin veya {ENV} ortam değişkenini ayarlayın.", file=sys.stderr)
10
+ sys.exit(1)
11
+ try:
12
+ return Fernet(key.encode())
13
+ except Exception as e:
14
+ print(f"[HATA] Anahtar geçersiz: {e}", file=sys.stderr)
15
+ sys.exit(1)
16
+
17
+ def main():
18
+ parser = argparse.ArgumentParser(description="String şifreleyici")
19
+ parser.add_argument("plain", help="Şifrelenecek string")
20
+ parser.add_argument("--key", help="Fernet anahtarı (opsiyonel, yoksa env kullanılacak)")
21
+ args = parser.parse_args()
22
+
23
+ f = get_fernet(args.key)
24
+ enc = f.encrypt(args.plain.encode()).decode()
25
+ print(f"enc:{enc}")
26
+
27
+ if __name__ == "__main__":
28
+ main()
utils/encrypt_token.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CLI: python encrypt_token.py <PLAIN_TOKEN>
3
+ Çıktıyı service_config.jsonc içindeki cloud_token alanına yapıştır.
4
+ """
5
+
6
+ import sys, os
7
+ from encryption_utils import encrypt
8
+
9
+ if len(sys.argv) < 2:
10
+ print("Usage: python encrypt_token.py <plain_token>")
11
+ sys.exit(1)
12
+
13
+ plain = sys.argv[1]
14
+ print(encrypt(plain))
utils/encryption_utils.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Flare – Fernet şifreleme yardımcıları
3
+ - encrypt(): düz string → "enc:<blob>"
4
+ - decrypt(): enc:<blob> → düz string (veya enc: yoksa aynen döner)
5
+ Anahtar: FLARE_TOKEN_KEY (32-bayt, base64, URL-safe)
6
+ """
7
+
8
+ import os
9
+ from typing import Optional
10
+ from cryptography.fernet import Fernet, InvalidToken
11
+ from logger import log_error, log_warning
12
+
13
+ _ENV_KEY = "FLARE_TOKEN_KEY"
14
+
15
+ def _get_key() -> Fernet:
16
+ """Get encryption key with better error messages"""
17
+ # Direkt environment variable kullan
18
+ key = os.getenv(_ENV_KEY)
19
+
20
+ # .env dosyasından yüklemeyi dene
21
+ if not key:
22
+ try:
23
+ from dotenv import load_dotenv
24
+ load_dotenv()
25
+ key = os.getenv(_ENV_KEY)
26
+ except ImportError:
27
+ pass
28
+
29
+ if not key:
30
+ error_msg = (
31
+ f"{_ENV_KEY} ortam değişkeni tanımlanmadı. "
32
+ f"Lütfen 32-byte base64 key oluşturun: python generate_key.py"
33
+ )
34
+ log_error(error_msg)
35
+ raise RuntimeError(error_msg)
36
+
37
+ # Key formatını kontrol et
38
+ try:
39
+ return Fernet(key.encode())
40
+ except Exception as e:
41
+ error_msg = (
42
+ f"{_ENV_KEY} geçersiz format. "
43
+ f"32-byte base64 URL-safe key olmalı. "
44
+ f"Yeni key için: python generate_key.py"
45
+ )
46
+ log_error(error_msg, error=str(e))
47
+ raise RuntimeError(error_msg)
48
+
49
+ def encrypt(plain: str) -> str:
50
+ """düz string → enc:..."""
51
+ if not plain:
52
+ log_warning("Empty string passed to encrypt")
53
+ return ""
54
+
55
+ try:
56
+ f = _get_key()
57
+ encrypted = f.encrypt(plain.encode()).decode()
58
+ return "enc:" + encrypted
59
+ except Exception as e:
60
+ log_error("Encryption failed", error=str(e))
61
+ raise
62
+
63
+ def decrypt(value: Optional[str]) -> Optional[str]:
64
+ """enc:... ise çözer, değilse aynen döndürür"""
65
+ if value is None or not isinstance(value, str):
66
+ return value
67
+
68
+ if not value.startswith("enc:"):
69
+ return value
70
+
71
+ token = value.split("enc:", 1)[1]
72
+
73
+ try:
74
+ f = _get_key()
75
+ decrypted = f.decrypt(token.encode()).decode()
76
+ return decrypted
77
+ except InvalidToken:
78
+ error_msg = (
79
+ "Şifre çözme başarısız. Muhtemel sebepler:\n"
80
+ "1. FLARE_TOKEN_KEY değişti\n"
81
+ "2. Şifreli veri bozuldu\n"
82
+ "3. Farklı bir key ile şifrelendi"
83
+ )
84
+ log_error(error_msg)
85
+ raise RuntimeError(error_msg)
86
+ except Exception as e:
87
+ log_error("Decryption error", error=str(e))
88
+ raise
utils/exceptions.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom Exception Classes for Flare Platform
3
+ """
4
+ from typing import Optional, Dict, Any
5
+ from datetime import datetime
6
+
7
+ class FlareException(Exception):
8
+ """Base exception for Flare"""
9
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
10
+ self.message = message
11
+ self.details = details or {}
12
+ self.timestamp = datetime.utcnow()
13
+ super().__init__(self.message)
14
+
15
+ def to_dict(self) -> Dict[str, Any]:
16
+ """Convert exception to dictionary"""
17
+ return {
18
+ "error": self.__class__.__name__,
19
+ "message": self.message,
20
+ "details": self.details,
21
+ "timestamp": self.timestamp.isoformat()
22
+ }
23
+
24
+ def to_http_detail(self) -> Dict[str, Any]:
25
+ """Convert to HTTP response detail"""
26
+ return {
27
+ "detail": self.message,
28
+ "error_type": self.__class__.__name__.lower().replace('error', ''),
29
+ **self.details
30
+ }
31
+
32
+ class RaceConditionError(FlareException):
33
+ """Raised when a race condition is detected during concurrent updates"""
34
+ def __init__(
35
+ self,
36
+ message: str,
37
+ current_user: Optional[str] = None,
38
+ last_update_user: Optional[str] = None,
39
+ last_update_date: Optional[str] = None,
40
+ entity_type: Optional[str] = None,
41
+ entity_id: Optional[Any] = None
42
+ ):
43
+ details = {
44
+ "current_user": current_user,
45
+ "last_update_user": last_update_user,
46
+ "last_update_date": last_update_date,
47
+ "entity_type": entity_type,
48
+ "entity_id": entity_id,
49
+ "action": "Please reload the data and try again"
50
+ }
51
+ super().__init__(message, details)
52
+ self.current_user = current_user
53
+ self.last_update_user = last_update_user
54
+ self.last_update_date = last_update_date
55
+
56
+ def to_http_detail(self) -> Dict[str, Any]:
57
+ """Convert to HTTPException detail format with proper serialization"""
58
+ return {
59
+ "message": self.message,
60
+ "last_update_user": self.last_update_user,
61
+ "last_update_date": self.last_update_date.isoformat() if isinstance(self.last_update_date, datetime) else self.last_update_date,
62
+ "type": "race_condition"
63
+ }
64
+
65
+ class ConfigurationError(FlareException):
66
+ """Raised when there's a configuration issue"""
67
+ def __init__(self, message: str, config_key: Optional[str] = None):
68
+ details = {"config_key": config_key} if config_key else {}
69
+ super().__init__(message, details)
70
+
71
+ class ValidationError(FlareException):
72
+ """Raised when validation fails"""
73
+ def __init__(self, message: str, field: Optional[str] = None, value: Any = None):
74
+ details = {}
75
+ if field:
76
+ details["field"] = field
77
+ if value is not None:
78
+ details["value"] = str(value)
79
+ super().__init__(message, details)
80
+
81
+ class AuthenticationError(FlareException):
82
+ """Raised when authentication fails"""
83
+ def __init__(self, message: str = "Authentication failed"):
84
+ super().__init__(message)
85
+
86
+ class AuthorizationError(FlareException):
87
+ """Raised when authorization fails"""
88
+ def __init__(self, message: str = "Insufficient permissions", required_permission: Optional[str] = None):
89
+ details = {"required_permission": required_permission} if required_permission else {}
90
+ super().__init__(message, details)
91
+
92
+ class SessionError(FlareException):
93
+ """Raised when there's a session-related error"""
94
+ def __init__(self, message: str, session_id: Optional[str] = None):
95
+ details = {"session_id": session_id} if session_id else {}
96
+ super().__init__(message, details)
97
+
98
+ class ProviderError(FlareException):
99
+ """Raised when a provider (LLM, TTS, STT) fails"""
100
+ def __init__(self, message: str, provider_type: str, provider_name: str, original_error: Optional[str] = None):
101
+ details = {
102
+ "provider_type": provider_type,
103
+ "provider_name": provider_name,
104
+ "original_error": original_error
105
+ }
106
+ super().__init__(message, details)
107
+
108
+ class APICallError(FlareException):
109
+ """Raised when an external API call fails"""
110
+ def __init__(
111
+ self,
112
+ message: str,
113
+ api_name: str,
114
+ status_code: Optional[int] = None,
115
+ response_body: Optional[str] = None
116
+ ):
117
+ details = {
118
+ "api_name": api_name,
119
+ "status_code": status_code,
120
+ "response_body": response_body
121
+ }
122
+ super().__init__(message, details)
123
+
124
+ class WebSocketError(FlareException):
125
+ """Raised when WebSocket operations fail"""
126
+ def __init__(self, message: str, session_id: Optional[str] = None, state: Optional[str] = None):
127
+ details = {
128
+ "session_id": session_id,
129
+ "state": state
130
+ }
131
+ super().__init__(message, details)
132
+
133
+ class ResourceNotFoundError(FlareException):
134
+ """Raised when a requested resource is not found"""
135
+ def __init__(self, resource_type: str, resource_id: Any):
136
+ message = f"{resource_type} not found: {resource_id}"
137
+ details = {
138
+ "resource_type": resource_type,
139
+ "resource_id": str(resource_id)
140
+ }
141
+ super().__init__(message, details)
142
+
143
+ class DuplicateResourceError(FlareException):
144
+ """Raised when attempting to create a duplicate resource"""
145
+ def __init__(self, resource_type: str, identifier: str):
146
+ message = f"{resource_type} already exists: {identifier}"
147
+ details = {
148
+ "resource_type": resource_type,
149
+ "identifier": identifier
150
+ }
151
+ super().__init__(message, details)
152
+
153
+ # Error response formatters
154
+ def format_error_response(error: Exception, request_id: Optional[str] = None) -> Dict[str, Any]:
155
+ """Format any exception into a standardized error response"""
156
+ if isinstance(error, FlareException):
157
+ response = error.to_dict()
158
+ else:
159
+ # Generic error
160
+ response = {
161
+ "error": error.__class__.__name__,
162
+ "message": str(error),
163
+ "details": {},
164
+ "timestamp": datetime.utcnow().isoformat()
165
+ }
166
+
167
+ if request_id:
168
+ response["request_id"] = request_id
169
+
170
+ return response
171
+
172
+ def get_http_status_code(error: Exception) -> int:
173
+ """Get appropriate HTTP status code for an exception"""
174
+ status_map = {
175
+ ValidationError: 422,
176
+ AuthenticationError: 401,
177
+ AuthorizationError: 403,
178
+ ResourceNotFoundError: 404,
179
+ DuplicateResourceError: 409,
180
+ RaceConditionError: 409,
181
+ ConfigurationError: 500,
182
+ ProviderError: 503,
183
+ APICallError: 502,
184
+ WebSocketError: 500,
185
+ SessionError: 400
186
+ }
187
+
188
+ for error_class, status_code in status_map.items():
189
+ if isinstance(error, error_class):
190
+ return status_code
191
+
192
+ # Default to 500 for unknown errors
193
+ return 500
utils/generate_key.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ generate_key.py
4
+ ----------------------------------
5
+ Çalıştır: python generate_key.py
6
+ Çıktı: 8rSihw0d3Sh_ceyDYobMHNPrgSg0riwGSK4Vco3O5qA=
7
+ Bu çıktıyı ortam değişkeni (veya HF Spaces `Secrets`) olarak kullan.
8
+
9
+ Not: cryptography==42.x yüklü olmalı (requirements.txt’de varsa yeterli).
10
+ """
11
+ from cryptography.fernet import Fernet
12
+
13
+ def main():
14
+ key = Fernet.generate_key().decode()
15
+ print(key)
16
+
17
+ if __name__ == "__main__":
18
+ main()
utils/logger.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Centralized Logging System for Flare Platform
3
+ """
4
+ import sys
5
+ import logging
6
+ import json
7
+ import os
8
+ import threading
9
+ import traceback
10
+ from datetime import datetime
11
+ from enum import Enum
12
+ from typing import Optional, Dict, Any, Union
13
+ from pathlib import Path
14
+
15
+ class LogLevel(Enum):
16
+ DEBUG = "DEBUG"
17
+ INFO = "INFO"
18
+ WARNING = "WARNING"
19
+ ERROR = "ERROR"
20
+ CRITICAL = "CRITICAL"
21
+
22
+ class FlareLogger:
23
+ _instance = None
24
+ _lock = threading.Lock()
25
+
26
+ def __new__(cls):
27
+ if cls._instance is None:
28
+ with cls._lock:
29
+ if cls._instance is None:
30
+ cls._instance = super().__new__(cls)
31
+ cls._instance._initialized = False
32
+ return cls._instance
33
+
34
+ def __init__(self):
35
+ if self._initialized:
36
+ return
37
+
38
+ self._initialized = True
39
+
40
+ # Log level from environment
41
+ self.log_level = LogLevel[os.getenv('LOG_LEVEL', 'INFO')]
42
+
43
+ # Configure Python logging
44
+ self.logger = logging.getLogger('flare')
45
+ self.logger.setLevel(self.log_level.value)
46
+
47
+ # Remove default handlers
48
+ self.logger.handlers = []
49
+
50
+ # Console handler with custom format
51
+ console_handler = logging.StreamHandler(sys.stdout)
52
+ console_handler.setFormatter(self._get_formatter())
53
+ self.logger.addHandler(console_handler)
54
+
55
+ # File handler for production
56
+ if os.getenv('LOG_TO_FILE', 'false').lower() == 'true':
57
+ log_dir = Path('logs')
58
+ log_dir.mkdir(exist_ok=True)
59
+ file_handler = logging.FileHandler(
60
+ log_dir / f"flare_{datetime.now().strftime('%Y%m%d')}.log"
61
+ )
62
+ file_handler.setFormatter(self._get_formatter())
63
+ self.logger.addHandler(file_handler)
64
+
65
+ # Future: Add ElasticSearch handler here
66
+ # if os.getenv('ELASTICSEARCH_URL'):
67
+ # from elasticsearch_handler import ElasticsearchHandler
68
+ # es_handler = ElasticsearchHandler(
69
+ # hosts=[os.getenv('ELASTICSEARCH_URL')],
70
+ # index='flare-logs'
71
+ # )
72
+ # self.logger.addHandler(es_handler)
73
+
74
+ def _get_formatter(self):
75
+ return logging.Formatter(
76
+ '[%(asctime)s.%(msecs)03d] [%(levelname)s] [%(name)s] %(message)s',
77
+ datefmt='%H:%M:%S'
78
+ )
79
+
80
+ def log(self, level: LogLevel, message: str, **kwargs):
81
+ """Central logging method with structured data"""
82
+ # Add context data
83
+ extra_data = {
84
+ 'timestamp': datetime.utcnow().isoformat(),
85
+ 'service': 'flare',
86
+ 'thread_id': threading.get_ident(),
87
+ **kwargs
88
+ }
89
+
90
+ # Log with structured data
91
+ log_message = message
92
+ if kwargs:
93
+ # Format kwargs for readability
94
+ kwargs_str = json.dumps(kwargs, ensure_ascii=False, default=str)
95
+ log_message = f"{message} | {kwargs_str}"
96
+
97
+ getattr(self.logger, level.value.lower())(log_message, extra={'data': extra_data})
98
+
99
+ # Always flush for real-time debugging
100
+ sys.stdout.flush()
101
+
102
+ def debug(self, message: str, **kwargs):
103
+ """Log debug message"""
104
+ self.log(LogLevel.DEBUG, message, **kwargs)
105
+
106
+ def info(self, message: str, **kwargs):
107
+ """Log info message"""
108
+ self.log(LogLevel.INFO, message, **kwargs)
109
+
110
+ def warning(self, message: str, **kwargs):
111
+ """Log warning message"""
112
+ self.log(LogLevel.WARNING, message, **kwargs)
113
+
114
+ def error(self, message: str, **kwargs):
115
+ """Log error message"""
116
+ self.log(LogLevel.ERROR, message, **kwargs)
117
+
118
+ def critical(self, message: str, **kwargs):
119
+ """Log critical message"""
120
+ self.log(LogLevel.CRITICAL, message, **kwargs)
121
+
122
+ def set_level(self, level: str):
123
+ """Dynamically change log level"""
124
+ try:
125
+ self.log_level = LogLevel[level.upper()]
126
+ self.logger.setLevel(self.log_level.value)
127
+ self.info(f"Log level changed to {level}")
128
+ except KeyError:
129
+ self.warning(f"Invalid log level: {level}")
130
+
131
+ # Global logger instance
132
+ logger = FlareLogger()
133
+
134
+ # Convenience functions
135
+ def log_debug(message: str, **kwargs):
136
+ """Log debug message"""
137
+ logger.debug(message, **kwargs)
138
+
139
+ def log_info(message: str, **kwargs):
140
+ """Log info message"""
141
+ logger.info(message, **kwargs)
142
+
143
+ def log_warning(message: str, **kwargs):
144
+ """Log warning message"""
145
+ logger.warning(message, **kwargs)
146
+
147
+ def log_error(message: str, exception: Optional[Exception] = None, **kwargs):
148
+ """
149
+ Log error message with optional exception
150
+
151
+ Usage:
152
+ log_error("Error occurred")
153
+ log_error("Error occurred", e) # Otomatik olarak str(e) ve traceback ekler
154
+ log_error("Error occurred", error="custom error")
155
+ log_error("Error occurred", e, extra_field="value")
156
+ """
157
+ import traceback
158
+
159
+ # Eğer exception parametresi verilmişse, otomatik olarak error ve traceback ekle
160
+ if exception is not None:
161
+ # Eğer kwargs'da error yoksa, exception'dan al
162
+ if 'error' not in kwargs:
163
+ kwargs['error'] = str(exception)
164
+
165
+ # Exception tipini ekle
166
+ if 'error_type' not in kwargs:
167
+ kwargs['error_type'] = type(exception).__name__
168
+
169
+ # Eğer kwargs'da traceback yoksa ve bu bir Exception ise, traceback ekle
170
+ if 'traceback' not in kwargs and isinstance(exception, Exception):
171
+ kwargs['traceback'] = traceback.format_exc()
172
+
173
+ # Özel exception tipleri için ekstra bilgi
174
+ if hasattr(exception, '__dict__'):
175
+ # Custom exception'ların attribute'larını ekle
176
+ for attr, value in exception.__dict__.items():
177
+ if not attr.startswith('_') and attr not in kwargs:
178
+ kwargs[f'exc_{attr}'] = value
179
+
180
+ # HTTP status code varsa ekle
181
+ if hasattr(exception, 'status_code') and 'status_code' not in kwargs:
182
+ kwargs['status_code'] = exception.status_code
183
+
184
+ # Orijinal logger'a gönder
185
+ logger.error(message, **kwargs)
186
+
187
+ def log_critical(message: str, **kwargs):
188
+ """Log critical message"""
189
+ logger.critical(message, **kwargs)
190
+
191
+ # Backward compatibility
192
+ def log(message: str, level: str = "INFO", **kwargs):
193
+ """Legacy log function for compatibility"""
194
+ getattr(logger, level.lower())(message, **kwargs)
195
+
196
+ # Performance logging helpers
197
+ class LogTimer:
198
+ """Context manager for timing operations"""
199
+ def __init__(self, operation_name: str, **extra_kwargs):
200
+ self.operation_name = operation_name
201
+ self.extra_kwargs = extra_kwargs
202
+ self.start_time = None
203
+
204
+ def __enter__(self):
205
+ self.start_time = datetime.now()
206
+ log_debug(f"Starting {self.operation_name}", **self.extra_kwargs)
207
+ return self
208
+
209
+ def __exit__(self, exc_type, exc_val, exc_tb):
210
+ duration_ms = (datetime.now() - self.start_time).total_seconds() * 1000
211
+ if exc_type:
212
+ log_error(
213
+ f"{self.operation_name} failed after {duration_ms:.2f}ms",
214
+ error=str(exc_val),
215
+ duration_ms=duration_ms,
216
+ **self.extra_kwargs
217
+ )
218
+ else:
219
+ log_info(
220
+ f"{self.operation_name} completed in {duration_ms:.2f}ms",
221
+ duration_ms=duration_ms,
222
+ **self.extra_kwargs
223
+ )
utils/utils.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ from typing import Optional
4
+ from fastapi import HTTPException, Depends
5
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
6
+ from datetime import datetime, timedelta, timezone
7
+ import jwt
8
+ from logger import log_info, log_warning
9
+
10
+ security = HTTPBearer()
11
+
12
+ # ===================== Rate Limiting =====================
13
+ class RateLimiter:
14
+ """Simple in-memory rate limiter"""
15
+ def __init__(self):
16
+ self.requests = {} # {key: [(timestamp, count)]}
17
+ self.lock = threading.Lock()
18
+
19
+ def is_allowed(self, key: str, max_requests: int, window_seconds: int) -> bool:
20
+ """Check if request is allowed"""
21
+ with self.lock:
22
+ now = datetime.now(timezone.utc)
23
+
24
+ if key not in self.requests:
25
+ self.requests[key] = []
26
+
27
+ # Remove old entries
28
+ cutoff = now.timestamp() - window_seconds
29
+ self.requests[key] = [
30
+ (ts, count) for ts, count in self.requests[key]
31
+ if ts > cutoff
32
+ ]
33
+
34
+ # Count requests in window
35
+ total = sum(count for _, count in self.requests[key])
36
+
37
+ if total >= max_requests:
38
+ return False
39
+
40
+ # Add this request
41
+ self.requests[key].append((now.timestamp(), 1))
42
+ return True
43
+
44
+ def reset(self, key: str):
45
+ """Reset rate limit for key"""
46
+ with self.lock:
47
+ if key in self.requests:
48
+ del self.requests[key]
49
+
50
+ # Create global rate limiter instance
51
+ import threading
52
+ rate_limiter = RateLimiter()
53
+
54
+ # ===================== JWT Config =====================
55
+ def get_jwt_config():
56
+ """Get JWT configuration based on environment"""
57
+ # Check if we're in HuggingFace Space
58
+ if os.getenv("SPACE_ID"):
59
+ # Cloud mode - use secrets from environment
60
+ jwt_secret = os.getenv("JWT_SECRET")
61
+ if not jwt_secret:
62
+ log_warning("⚠️ WARNING: JWT_SECRET not found in environment, using fallback")
63
+ jwt_secret = "flare-admin-secret-key-change-in-production" # Fallback
64
+ else:
65
+ # On-premise mode - use .env file
66
+ from dotenv import load_dotenv
67
+ load_dotenv()
68
+ jwt_secret = os.getenv("JWT_SECRET", "flare-admin-secret-key-change-in-production")
69
+
70
+ return {
71
+ "secret": jwt_secret,
72
+ "algorithm": os.getenv("JWT_ALGORITHM", "HS256"),
73
+ "expiration_hours": int(os.getenv("JWT_EXPIRATION_HOURS", "24"))
74
+ }
75
+
76
+ # ===================== Auth Helpers =====================
77
+ def create_token(username: str) -> str:
78
+ """Create JWT token for user"""
79
+ config = get_jwt_config()
80
+ expiry = datetime.now(timezone.utc) + timedelta(hours=config["expiration_hours"])
81
+
82
+ payload = {
83
+ "sub": username,
84
+ "exp": expiry,
85
+ "iat": datetime.now(timezone.utc)
86
+ }
87
+
88
+ return jwt.encode(payload, config["secret"], algorithm=config["algorithm"])
89
+
90
+ def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
91
+ """Verify JWT token and return username"""
92
+ token = credentials.credentials
93
+ config = get_jwt_config()
94
+
95
+ try:
96
+ payload = jwt.decode(token, config["secret"], algorithms=[config["algorithm"]])
97
+ return payload["sub"]
98
+ except jwt.ExpiredSignatureError:
99
+ raise HTTPException(status_code=401, detail="Token expired")
100
+ except jwt.InvalidTokenError:
101
+ raise HTTPException(status_code=401, detail="Invalid token")
102
+
103
+ # ===================== Utility Functions =====================
104
+
105
+ def truncate_string(text: str, max_length: int = 100, suffix: str = "...") -> str:
106
+ """Truncate string to max length"""
107
+ if len(text) <= max_length:
108
+ return text
109
+ return text[:max_length - len(suffix)] + suffix
110
+
111
+ def format_file_size(size_bytes: int) -> str:
112
+ """Format file size in human readable format"""
113
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
114
+ if size_bytes < 1024.0:
115
+ return f"{size_bytes:.2f} {unit}"
116
+ size_bytes /= 1024.0
117
+ return f"{size_bytes:.2f} PB"
118
+
119
+ def is_safe_path(path: str, base_path: str) -> bool:
120
+ """Check if path is safe (no directory traversal)"""
121
+ import os
122
+ # Resolve to absolute paths
123
+ base = os.path.abspath(base_path)
124
+ target = os.path.abspath(os.path.join(base, path))
125
+
126
+ # Check if target is under base
127
+ return target.startswith(base)
128
+
129
+ def get_current_timestamp() -> str:
130
+ """
131
+ Get current UTC timestamp in ISO format with Z suffix
132
+ Returns: "2025-01-10T12:00:00.123Z"
133
+ """
134
+ return datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
135
+
136
+ def normalize_timestamp(timestamp: Optional[str]) -> str:
137
+ """
138
+ Normalize timestamp string for consistent comparison
139
+ Handles various formats:
140
+ - "2025-01-10T12:00:00Z"
141
+ - "2025-01-10T12:00:00.000Z"
142
+ - "2025-01-10T12:00:00+00:00"
143
+ - "2025-01-10 12:00:00+00:00"
144
+ """
145
+ if not timestamp:
146
+ return ""
147
+
148
+ # Normalize various formats
149
+ normalized = timestamp.replace(' ', 'T') # Space to T
150
+ normalized = normalized.replace('+00:00', 'Z') # UTC timezone
151
+
152
+ # Remove milliseconds if present for comparison
153
+ if '.' in normalized and normalized.endswith('Z'):
154
+ normalized = normalized.split('.')[0] + 'Z'
155
+
156
+ return normalized
157
+
158
+ def timestamps_equal(ts1: Optional[str], ts2: Optional[str]) -> bool:
159
+ """
160
+ Compare two timestamps regardless of format differences
161
+ """
162
+ return normalize_timestamp(ts1) == normalize_timestamp(ts2)