Spaces:
Sleeping
Sleeping
Oviya commited on
Commit ·
b5ca3a6
1
Parent(s): 10f206e
fix
Browse files- AUTHENTICATION_FLOW_DOCUMENTATION.md +0 -0
- CRITICAL_SECURITY_AUDIT_REPORT.md +0 -0
- EXECUTIVE_SUMMARY.md +0 -0
- QUICK_REFERENCE.md +0 -0
- SECURITY_AUDIT_REPORT.md +0 -0
- auth.service.ts +145 -0
- auth/__init__.py +25 -0
- auth/database.py +168 -0
- auth/models.py +177 -0
- auth/routes.py +346 -0
- auth/utils.py +156 -0
- chroma_db/cc71e05b-734e-41ed-99a2-95d0f26626eb/data_level0.bin +3 -0
- chroma_db/cc71e05b-734e-41ed-99a2-95d0f26626eb/header.bin +3 -0
- chroma_db/cc71e05b-734e-41ed-99a2-95d0f26626eb/length.bin +3 -0
- AUTHENTICATION_FIXES_COMPLETED.md → chroma_db/cc71e05b-734e-41ed-99a2-95d0f26626eb/link_lists.bin +0 -0
- requirements.txt +1 -1
- sign-up.service.ts +17 -0
- src/app/auth/auth.service.ts +145 -0
- src/app/sign-up/sign-up.service.ts +17 -0
- verification.py +136 -716
- AUTHENTICATION_FIXES_REQUIRED.md → verification_new.py +0 -0
AUTHENTICATION_FLOW_DOCUMENTATION.md
DELETED
|
File without changes
|
CRITICAL_SECURITY_AUDIT_REPORT.md
DELETED
|
File without changes
|
EXECUTIVE_SUMMARY.md
DELETED
|
File without changes
|
QUICK_REFERENCE.md
DELETED
|
File without changes
|
SECURITY_AUDIT_REPORT.md
DELETED
|
File without changes
|
auth.service.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http';
|
| 3 |
+
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
|
| 4 |
+
import { catchError, tap } from 'rxjs/operators';
|
| 5 |
+
import { Router } from '@angular/router';
|
| 6 |
+
|
| 7 |
+
@Injectable({
|
| 8 |
+
providedIn: 'root'
|
| 9 |
+
})
|
| 10 |
+
export class AuthService {
|
| 11 |
+
private apiUrl = location.hostname.endsWith('hf.space')
|
| 12 |
+
? 'https://majemaai-mj-learn-backend.hf.space'
|
| 13 |
+
: 'http://localhost:5000';
|
| 14 |
+
|
| 15 |
+
private loggedInSubject = new BehaviorSubject<boolean>(false);
|
| 16 |
+
public loggedIn$ = this.loggedInSubject.asObservable();
|
| 17 |
+
private refreshInterval: any;
|
| 18 |
+
|
| 19 |
+
constructor(private http: HttpClient, private router: Router) { }
|
| 20 |
+
|
| 21 |
+
// ? Updated to use /auth/login endpoint
|
| 22 |
+
login(username: string, password: string): Observable<any> {
|
| 23 |
+
return this.http.post(`${this.apiUrl}/auth/login`, { username, password }, { withCredentials: true });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
setLoggedIn(status: boolean): void {
|
| 27 |
+
this.loggedInSubject.next(status);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
isLoggedIn(): boolean {
|
| 31 |
+
return this.loggedInSubject.value;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// ? Updated to use /auth/refresh endpoint
|
| 35 |
+
startAutoRefresh(): void {
|
| 36 |
+
this.clearAutoRefresh(); // Clear any existing interval
|
| 37 |
+
|
| 38 |
+
this.refreshInterval = setInterval(() => {
|
| 39 |
+
console.log("?? Auto-refreshing token...");
|
| 40 |
+
this.http.post(`${this.apiUrl}/auth/refresh`, {}, { withCredentials: true }).subscribe(
|
| 41 |
+
response => {
|
| 42 |
+
console.log('? Auto-refresh successful');
|
| 43 |
+
},
|
| 44 |
+
error => {
|
| 45 |
+
console.error('? Auto-refresh failed:', error);
|
| 46 |
+
// Clear tokens manually (if needed)
|
| 47 |
+
this.clearTokens();
|
| 48 |
+
|
| 49 |
+
// Redirect to login
|
| 50 |
+
this.router.navigate(['/auth']);
|
| 51 |
+
}
|
| 52 |
+
);
|
| 53 |
+
}, 14 * 60 * 1000); // Every 14 minutes
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
clearAutoRefresh(): void {
|
| 57 |
+
if (this.refreshInterval) {
|
| 58 |
+
clearInterval(this.refreshInterval);
|
| 59 |
+
this.refreshInterval = null;
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// ? Updated to use /auth/logout endpoint
|
| 64 |
+
logout(): Observable<any> {
|
| 65 |
+
console.log("?? Sending logout request with credentials");
|
| 66 |
+
|
| 67 |
+
return this.http.post(`${this.apiUrl}/auth/logout`, {}, { withCredentials: true }).pipe(
|
| 68 |
+
tap(response => {
|
| 69 |
+
console.log('?? Response from backend:', response);
|
| 70 |
+
this.clearTokens();
|
| 71 |
+
this.clearAutoRefresh(); // ? Stop auto-refresh
|
| 72 |
+
this.setLoggedIn(false); // ? Reflect logout in UI
|
| 73 |
+
}),
|
| 74 |
+
catchError(error => {
|
| 75 |
+
console.error('? Error from backend:', error);
|
| 76 |
+
// Still ensure local state reflects logout
|
| 77 |
+
this.clearTokens();
|
| 78 |
+
this.clearAutoRefresh();
|
| 79 |
+
this.setLoggedIn(false);
|
| 80 |
+
return throwError(() => error);
|
| 81 |
+
})
|
| 82 |
+
);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
clearTokens(): void {
|
| 86 |
+
document.cookie = 'access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
| 87 |
+
document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
| 88 |
+
this.clearAutoRefresh(); // ?
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// ? Get access token (using cookies)
|
| 92 |
+
getAccessToken(): string | null {
|
| 93 |
+
const cookies = document.cookie.split('; ');
|
| 94 |
+
for (let cookie of cookies) {
|
| 95 |
+
if (cookie.startsWith('access_token=')) {
|
| 96 |
+
return cookie.split('=')[1];
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
return null;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// ? Save tokens (Not necessary, as tokens are managed via cookies)
|
| 103 |
+
saveTokens(accessToken: string, refreshToken: string): void {
|
| 104 |
+
// No need for this if you're using cookies, but if you want to persist in localStorage, use:
|
| 105 |
+
localStorage.setItem('access_token', accessToken);
|
| 106 |
+
localStorage.setItem('refresh_token', refreshToken);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// ? Updated to use /auth/check-auth endpoint
|
| 110 |
+
checkSession(): Observable<boolean> {
|
| 111 |
+
return this.http.get(`${this.apiUrl}/auth/check-auth`, { withCredentials: true }).pipe(
|
| 112 |
+
tap((res: any) => {
|
| 113 |
+
console.log('? Session valid:', res);
|
| 114 |
+
this.setLoggedIn(true);
|
| 115 |
+
this.startAutoRefresh();
|
| 116 |
+
return true; // ? Important!
|
| 117 |
+
}),
|
| 118 |
+
catchError((err) => {
|
| 119 |
+
if (err.status === 401) {
|
| 120 |
+
// Access token may be expired. Try refresh
|
| 121 |
+
console.warn('?? Access token expired. Trying to refresh...');
|
| 122 |
+
|
| 123 |
+
// ? Updated to use /auth/refresh endpoint
|
| 124 |
+
return this.http.post(`${this.apiUrl}/auth/refresh`, {}, { withCredentials: true }).pipe(
|
| 125 |
+
tap((refreshRes: any) => {
|
| 126 |
+
console.log("? Token refreshed during checkSession.");
|
| 127 |
+
//alert("? Token refreshed during checkSession.");
|
| 128 |
+
this.setLoggedIn(true);
|
| 129 |
+
this.startAutoRefresh();
|
| 130 |
+
}),
|
| 131 |
+
catchError((refreshErr) => {
|
| 132 |
+
console.error("? Refresh token failed during checkSession.", refreshErr);
|
| 133 |
+
this.setLoggedIn(false);
|
| 134 |
+
return of(false);
|
| 135 |
+
})
|
| 136 |
+
);
|
| 137 |
+
} else {
|
| 138 |
+
console.error("? Unknown error during checkSession", err);
|
| 139 |
+
this.setLoggedIn(false);
|
| 140 |
+
return of(false);
|
| 141 |
+
}
|
| 142 |
+
})
|
| 143 |
+
);
|
| 144 |
+
}
|
| 145 |
+
}
|
auth/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication module for MJ Learn Backend
|
| 3 |
+
|
| 4 |
+
This module provides:
|
| 5 |
+
- User authentication and authorization
|
| 6 |
+
- JWT token management
|
| 7 |
+
- Database models for user management
|
| 8 |
+
- Security utilities
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from .models import User, BlacklistedToken, RefreshToken
|
| 12 |
+
from .utils import token_required, anonymize_username
|
| 13 |
+
from .database import get_db_connection, init_db
|
| 14 |
+
from .routes import auth_bp
|
| 15 |
+
|
| 16 |
+
__all__ = [
|
| 17 |
+
'User',
|
| 18 |
+
'BlacklistedToken',
|
| 19 |
+
'RefreshToken',
|
| 20 |
+
'token_required',
|
| 21 |
+
'anonymize_username',
|
| 22 |
+
'get_db_connection',
|
| 23 |
+
'init_db',
|
| 24 |
+
'auth_bp'
|
| 25 |
+
]
|
auth/database.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database connection and initialization module
|
| 3 |
+
|
| 4 |
+
Handles:
|
| 5 |
+
- Database connection management
|
| 6 |
+
- Table creation and initialization
|
| 7 |
+
- Connection string configuration
|
| 8 |
+
- Database diagnostics
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import pyodbc
|
| 13 |
+
from threading import Lock
|
| 14 |
+
from .models import get_table_definitions
|
| 15 |
+
|
| 16 |
+
# Database configuration
|
| 17 |
+
DB_SERVER = os.getenv("DB_SERVER", r"(localdb)\MSSQLLocalDB")
|
| 18 |
+
DB_DATABASE = os.getenv("DB_DATABASE", "AuthenticationDB1")
|
| 19 |
+
DB_DRIVER = os.getenv("DB_DRIVER", "ODBC Driver 17 for SQL Server")
|
| 20 |
+
|
| 21 |
+
# Build connection string
|
| 22 |
+
is_local = (
|
| 23 |
+
DB_SERVER.lower().startswith("localhost")
|
| 24 |
+
or DB_SERVER.startswith(".")
|
| 25 |
+
or DB_SERVER.lower().startswith("(localdb)")
|
| 26 |
+
or "\\" in DB_SERVER
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
if is_local:
|
| 30 |
+
# Windows local / LocalDB using modern ODBC driver
|
| 31 |
+
CONN_STR = (
|
| 32 |
+
f"DRIVER={{{DB_DRIVER}}};"
|
| 33 |
+
f"SERVER={DB_SERVER};"
|
| 34 |
+
f"DATABASE={DB_DATABASE};"
|
| 35 |
+
"Trusted_Connection=yes;"
|
| 36 |
+
"TrustServerCertificate=yes;"
|
| 37 |
+
)
|
| 38 |
+
else:
|
| 39 |
+
# Remote SQL auth
|
| 40 |
+
CONN_STR = (
|
| 41 |
+
f"DRIVER={{{DB_DRIVER}}};"
|
| 42 |
+
f"SERVER={DB_SERVER};DATABASE={DB_DATABASE};"
|
| 43 |
+
f"UID={os.getenv('DB_USER')};PWD={os.getenv('DB_PASSWORD')};"
|
| 44 |
+
"Encrypt=yes;TrustServerCertificate=yes;"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Database initialization tracking
|
| 48 |
+
_db_init_done = False
|
| 49 |
+
_db_init_lock = Lock()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_db_connection():
|
| 53 |
+
"""
|
| 54 |
+
Create a database connection with short timeout
|
| 55 |
+
|
| 56 |
+
Raises:
|
| 57 |
+
RuntimeError: If DB credentials are missing for remote connections
|
| 58 |
+
pyodbc.Error: If connection fails
|
| 59 |
+
"""
|
| 60 |
+
if "Trusted_Connection=yes" not in CONN_STR:
|
| 61 |
+
if not os.getenv("DB_USER") or not os.getenv("DB_PASSWORD"):
|
| 62 |
+
raise RuntimeError("DB_USER/DB_PASSWORD are not set in the environment.")
|
| 63 |
+
return pyodbc.connect(CONN_STR, timeout=5)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def init_db():
|
| 67 |
+
"""
|
| 68 |
+
Create database tables if they do not exist
|
| 69 |
+
|
| 70 |
+
Creates:
|
| 71 |
+
- Users table for authentication
|
| 72 |
+
- BlacklistedTokens table for token management
|
| 73 |
+
- RefreshTokens table for refresh token storage
|
| 74 |
+
"""
|
| 75 |
+
conn = get_db_connection()
|
| 76 |
+
cur = conn.cursor()
|
| 77 |
+
|
| 78 |
+
# Get table definitions
|
| 79 |
+
tables = get_table_definitions()
|
| 80 |
+
|
| 81 |
+
# Create each table
|
| 82 |
+
for table_name, sql in tables.items():
|
| 83 |
+
cur.execute(sql)
|
| 84 |
+
|
| 85 |
+
conn.commit()
|
| 86 |
+
conn.close()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def ensure_database_initialized():
|
| 90 |
+
"""
|
| 91 |
+
Ensure database is initialized (thread-safe)
|
| 92 |
+
|
| 93 |
+
Call this from Flask app startup to initialize database once.
|
| 94 |
+
Controlled by RUN_INIT_DB environment variable.
|
| 95 |
+
"""
|
| 96 |
+
global _db_init_done
|
| 97 |
+
should_init = os.getenv("RUN_INIT_DB", "0") == "1"
|
| 98 |
+
|
| 99 |
+
if should_init and not _db_init_done:
|
| 100 |
+
with _db_init_lock:
|
| 101 |
+
if not _db_init_done:
|
| 102 |
+
try:
|
| 103 |
+
init_db()
|
| 104 |
+
print("? Database initialized successfully")
|
| 105 |
+
return True
|
| 106 |
+
except Exception as e:
|
| 107 |
+
print(f"? Database initialization failed: {e}")
|
| 108 |
+
raise
|
| 109 |
+
finally:
|
| 110 |
+
_db_init_done = True
|
| 111 |
+
|
| 112 |
+
return _db_init_done
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def get_database_info():
|
| 116 |
+
"""
|
| 117 |
+
Get database diagnostic information (admin only)
|
| 118 |
+
|
| 119 |
+
Returns safe diagnostic information without exposing credentials.
|
| 120 |
+
"""
|
| 121 |
+
info = {}
|
| 122 |
+
|
| 123 |
+
# Get available drivers
|
| 124 |
+
try:
|
| 125 |
+
info["drivers_found"] = pyodbc.drivers()
|
| 126 |
+
except Exception as e:
|
| 127 |
+
info["drivers_found_error"] = str(e)
|
| 128 |
+
|
| 129 |
+
# Safe database information
|
| 130 |
+
info["database_name"] = DB_DATABASE
|
| 131 |
+
info["server_type"] = "LocalDB" if is_local else "Remote"
|
| 132 |
+
|
| 133 |
+
# Test connection
|
| 134 |
+
try:
|
| 135 |
+
conn = get_db_connection()
|
| 136 |
+
conn.close()
|
| 137 |
+
info["connection_status"] = "ok"
|
| 138 |
+
except Exception as e:
|
| 139 |
+
info["connection_status"] = "error"
|
| 140 |
+
info["error_type"] = type(e).__name__
|
| 141 |
+
|
| 142 |
+
return info
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_database_connection():
|
| 146 |
+
"""
|
| 147 |
+
Test database connection and return status
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
tuple: (success: bool, message: str)
|
| 151 |
+
"""
|
| 152 |
+
try:
|
| 153 |
+
conn = get_db_connection()
|
| 154 |
+
|
| 155 |
+
# Test basic query
|
| 156 |
+
cur = conn.cursor()
|
| 157 |
+
cur.execute("SELECT 1")
|
| 158 |
+
result = cur.fetchone()
|
| 159 |
+
|
| 160 |
+
conn.close()
|
| 161 |
+
|
| 162 |
+
if result and result[0] == 1:
|
| 163 |
+
return True, "Database connection successful"
|
| 164 |
+
else:
|
| 165 |
+
return False, "Database query failed"
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
return False, f"Database connection failed: {str(e)}"
|
auth/models.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database models and schemas for authentication system
|
| 3 |
+
|
| 4 |
+
Contains:
|
| 5 |
+
- User model with role-based access
|
| 6 |
+
- Token blacklist model
|
| 7 |
+
- Refresh token model
|
| 8 |
+
- Database table definitions
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import pyodbc
|
| 12 |
+
from typing import Optional, Dict, Any
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class User:
|
| 16 |
+
"""User model for authentication and authorization"""
|
| 17 |
+
|
| 18 |
+
def __init__(self, username: str, password_hash: str, role: str = 'user', user_id: int = None):
|
| 19 |
+
self.id = user_id
|
| 20 |
+
self.username = username
|
| 21 |
+
self.password_hash = password_hash
|
| 22 |
+
self.role = role
|
| 23 |
+
|
| 24 |
+
@staticmethod
|
| 25 |
+
def find_by_username(conn: pyodbc.Connection, username: str) -> Optional['User']:
|
| 26 |
+
"""Find user by username"""
|
| 27 |
+
cur = conn.cursor()
|
| 28 |
+
cur.execute("SELECT id, username, password_hash, role FROM Users WHERE username = ?", (username,))
|
| 29 |
+
row = cur.fetchone()
|
| 30 |
+
if row:
|
| 31 |
+
return User(
|
| 32 |
+
user_id=row[0],
|
| 33 |
+
username=row[1],
|
| 34 |
+
password_hash=row[2],
|
| 35 |
+
role=row[3]
|
| 36 |
+
)
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
@staticmethod
|
| 40 |
+
def create_user(conn: pyodbc.Connection, username: str, password_hash: str, role: str = 'user') -> bool:
|
| 41 |
+
"""Create a new user"""
|
| 42 |
+
try:
|
| 43 |
+
cur = conn.cursor()
|
| 44 |
+
cur.execute(
|
| 45 |
+
"INSERT INTO Users (username, password_hash, role) VALUES (?, ?, ?)",
|
| 46 |
+
(username, password_hash, role)
|
| 47 |
+
)
|
| 48 |
+
conn.commit()
|
| 49 |
+
return True
|
| 50 |
+
except pyodbc.IntegrityError:
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
@staticmethod
|
| 54 |
+
def get_all_users(conn: pyodbc.Connection) -> list:
|
| 55 |
+
"""Get all users (admin only)"""
|
| 56 |
+
cur = conn.cursor()
|
| 57 |
+
cur.execute("SELECT id, username, role FROM Users ORDER BY id")
|
| 58 |
+
users = []
|
| 59 |
+
for row in cur.fetchall():
|
| 60 |
+
users.append({
|
| 61 |
+
"id": row[0],
|
| 62 |
+
"username": row[1],
|
| 63 |
+
"role": row[2]
|
| 64 |
+
})
|
| 65 |
+
return users
|
| 66 |
+
|
| 67 |
+
@staticmethod
|
| 68 |
+
def promote_to_admin(conn: pyodbc.Connection, username: str) -> bool:
|
| 69 |
+
"""Promote user to admin role"""
|
| 70 |
+
cur = conn.cursor()
|
| 71 |
+
cur.execute("UPDATE Users SET role = 'admin' WHERE username = ?", (username,))
|
| 72 |
+
conn.commit()
|
| 73 |
+
return cur.rowcount > 0
|
| 74 |
+
|
| 75 |
+
@staticmethod
|
| 76 |
+
def user_count(conn: pyodbc.Connection) -> int:
|
| 77 |
+
"""Get total user count"""
|
| 78 |
+
cur = conn.cursor()
|
| 79 |
+
cur.execute("SELECT COUNT(*) FROM Users")
|
| 80 |
+
return cur.fetchone()[0]
|
| 81 |
+
|
| 82 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 83 |
+
"""Convert user to dictionary (safe for JSON)"""
|
| 84 |
+
return {
|
| 85 |
+
"id": self.id,
|
| 86 |
+
"username": self.username,
|
| 87 |
+
"role": self.role
|
| 88 |
+
# Note: Never include password_hash in dict
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class BlacklistedToken:
|
| 93 |
+
"""Model for blacklisted JWT tokens"""
|
| 94 |
+
|
| 95 |
+
@staticmethod
|
| 96 |
+
def is_blacklisted(conn: pyodbc.Connection, token: str) -> bool:
|
| 97 |
+
"""Check if token is blacklisted"""
|
| 98 |
+
cur = conn.cursor()
|
| 99 |
+
cur.execute("SELECT token FROM BlacklistedTokens WHERE token = ?", (token,))
|
| 100 |
+
return cur.fetchone() is not None
|
| 101 |
+
|
| 102 |
+
@staticmethod
|
| 103 |
+
def add_to_blacklist(conn: pyodbc.Connection, token: str) -> bool:
|
| 104 |
+
"""Add token to blacklist"""
|
| 105 |
+
cur = conn.cursor()
|
| 106 |
+
# Check if already blacklisted
|
| 107 |
+
cur.execute("SELECT token FROM BlacklistedTokens WHERE token = ?", (token,))
|
| 108 |
+
if cur.fetchone():
|
| 109 |
+
return True # Already blacklisted
|
| 110 |
+
|
| 111 |
+
cur.execute("INSERT INTO BlacklistedTokens (token) VALUES (?)", (token,))
|
| 112 |
+
conn.commit()
|
| 113 |
+
return True
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class RefreshToken:
|
| 117 |
+
"""Model for refresh token management"""
|
| 118 |
+
|
| 119 |
+
@staticmethod
|
| 120 |
+
def find_by_token(conn: pyodbc.Connection, token: str) -> Optional[str]:
|
| 121 |
+
"""Find username by refresh token"""
|
| 122 |
+
cur = conn.cursor()
|
| 123 |
+
cur.execute("SELECT username FROM RefreshTokens WHERE token = ?", (token,))
|
| 124 |
+
row = cur.fetchone()
|
| 125 |
+
return row[0] if row else None
|
| 126 |
+
|
| 127 |
+
@staticmethod
|
| 128 |
+
def create_token(conn: pyodbc.Connection, username: str, token: str) -> bool:
|
| 129 |
+
"""Store refresh token"""
|
| 130 |
+
cur = conn.cursor()
|
| 131 |
+
cur.execute("INSERT INTO RefreshTokens (username, token) VALUES (?, ?)", (username, token))
|
| 132 |
+
conn.commit()
|
| 133 |
+
return True
|
| 134 |
+
|
| 135 |
+
@staticmethod
|
| 136 |
+
def delete_user_tokens(conn: pyodbc.Connection, username: str) -> bool:
|
| 137 |
+
"""Delete all refresh tokens for user"""
|
| 138 |
+
cur = conn.cursor()
|
| 139 |
+
cur.execute("DELETE FROM RefreshTokens WHERE username = ?", (username,))
|
| 140 |
+
conn.commit()
|
| 141 |
+
return True
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# Database table creation SQL
|
| 145 |
+
def get_table_definitions():
|
| 146 |
+
"""Get SQL statements for creating authentication tables"""
|
| 147 |
+
return {
|
| 148 |
+
'users': """
|
| 149 |
+
IF OBJECT_ID('Users', 'U') IS NULL
|
| 150 |
+
CREATE TABLE Users (
|
| 151 |
+
id INT IDENTITY(1,1) PRIMARY KEY,
|
| 152 |
+
username NVARCHAR(100) UNIQUE NOT NULL,
|
| 153 |
+
password_hash NVARCHAR(500) NOT NULL,
|
| 154 |
+
role NVARCHAR(50) DEFAULT 'user'
|
| 155 |
+
)
|
| 156 |
+
""",
|
| 157 |
+
|
| 158 |
+
'blacklisted_tokens': """
|
| 159 |
+
IF OBJECT_ID('BlacklistedTokens', 'U') IS NULL
|
| 160 |
+
CREATE TABLE BlacklistedTokens (
|
| 161 |
+
id INT IDENTITY(1,1) PRIMARY KEY,
|
| 162 |
+
token NVARCHAR(1000) UNIQUE NOT NULL,
|
| 163 |
+
created_at DATETIME DEFAULT GETDATE()
|
| 164 |
+
)
|
| 165 |
+
""",
|
| 166 |
+
|
| 167 |
+
'refresh_tokens': """
|
| 168 |
+
IF OBJECT_ID('RefreshTokens', 'U') IS NULL
|
| 169 |
+
CREATE TABLE RefreshTokens (
|
| 170 |
+
id INT IDENTITY(1,1) PRIMARY KEY,
|
| 171 |
+
username NVARCHAR(100) NOT NULL,
|
| 172 |
+
token NVARCHAR(1000) UNIQUE NOT NULL,
|
| 173 |
+
created_at DATETIME DEFAULT GETDATE(),
|
| 174 |
+
FOREIGN KEY (username) REFERENCES Users(username) ON DELETE CASCADE
|
| 175 |
+
)
|
| 176 |
+
"""
|
| 177 |
+
}
|
auth/routes.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication routes and endpoints
|
| 3 |
+
|
| 4 |
+
Contains all authentication-related Flask routes:
|
| 5 |
+
- User registration and login
|
| 6 |
+
- Token refresh and logout
|
| 7 |
+
- Admin user management
|
| 8 |
+
- Database diagnostics
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import datetime
|
| 13 |
+
import bcrypt
|
| 14 |
+
import jwt
|
| 15 |
+
import pyodbc
|
| 16 |
+
from flask import Blueprint, request, jsonify, make_response, current_app
|
| 17 |
+
|
| 18 |
+
from .database import get_db_connection
|
| 19 |
+
from .models import User, BlacklistedToken, RefreshToken
|
| 20 |
+
from .utils import (
|
| 21 |
+
token_required,
|
| 22 |
+
anonymize_username,
|
| 23 |
+
add_cookie,
|
| 24 |
+
validate_user_input,
|
| 25 |
+
is_admin_user,
|
| 26 |
+
log_security_event
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Create authentication blueprint
|
| 30 |
+
auth_bp = Blueprint('auth', __name__)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@auth_bp.route("/dashboard")
|
| 34 |
+
@token_required
|
| 35 |
+
def dashboard(username):
|
| 36 |
+
"""Protected dashboard endpoint"""
|
| 37 |
+
return jsonify({"message": f"Welcome {username} to your dashboard!"})
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@auth_bp.route("/login", methods=["POST"])
|
| 41 |
+
def login():
|
| 42 |
+
"""User login endpoint"""
|
| 43 |
+
data = request.json or {}
|
| 44 |
+
username = data.get('username', '').strip()
|
| 45 |
+
password = data.get('password', '')
|
| 46 |
+
|
| 47 |
+
# Input validation
|
| 48 |
+
is_valid, error_msg = validate_user_input(username, password)
|
| 49 |
+
if not is_valid:
|
| 50 |
+
return jsonify({"message": error_msg}), 400
|
| 51 |
+
|
| 52 |
+
# Normalize username to prevent case sensitivity issues
|
| 53 |
+
username = username.lower()
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
conn = get_db_connection()
|
| 57 |
+
user = User.find_by_username(conn, username)
|
| 58 |
+
conn.close()
|
| 59 |
+
except Exception as e:
|
| 60 |
+
current_app.logger.exception("DB access error on login: %s", e)
|
| 61 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 62 |
+
|
| 63 |
+
if not user:
|
| 64 |
+
log_security_event("failed_login", username, request.remote_addr, "user_not_found")
|
| 65 |
+
return jsonify({"message": "Invalid credentials"}), 401
|
| 66 |
+
|
| 67 |
+
if not bcrypt.checkpw(password.encode('utf-8'), user.password_hash.encode('utf-8')):
|
| 68 |
+
log_security_event("failed_login", username, request.remote_addr, "wrong_password")
|
| 69 |
+
return jsonify({"message": "Invalid credentials"}), 401
|
| 70 |
+
|
| 71 |
+
# Successful login
|
| 72 |
+
log_security_event("successful_login", username, request.remote_addr)
|
| 73 |
+
|
| 74 |
+
# Generate tokens
|
| 75 |
+
access_token = jwt.encode(
|
| 76 |
+
{'username': username, 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=15)},
|
| 77 |
+
current_app.config['SECRET_KEY'],
|
| 78 |
+
algorithm="HS256"
|
| 79 |
+
)
|
| 80 |
+
refresh_token = jwt.encode(
|
| 81 |
+
{'username': username, 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=7)},
|
| 82 |
+
current_app.config['SECRET_KEY'],
|
| 83 |
+
algorithm="HS256"
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
# Store refresh token
|
| 87 |
+
try:
|
| 88 |
+
conn = get_db_connection()
|
| 89 |
+
RefreshToken.create_token(conn, username, refresh_token)
|
| 90 |
+
conn.close()
|
| 91 |
+
except Exception as e:
|
| 92 |
+
current_app.logger.exception("DB write error on login: %s", e)
|
| 93 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 94 |
+
|
| 95 |
+
resp = make_response(jsonify({"message": "Login successful"}))
|
| 96 |
+
add_cookie(resp, 'access_token', access_token, 900) # 15 min
|
| 97 |
+
add_cookie(resp, 'refresh_token', refresh_token, 7*24*60*60) # 7 days
|
| 98 |
+
return resp
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@auth_bp.route("/refresh", methods=["POST"])
|
| 102 |
+
def refresh():
|
| 103 |
+
"""Token refresh endpoint"""
|
| 104 |
+
refresh_token = request.cookies.get("refresh_token")
|
| 105 |
+
if not refresh_token:
|
| 106 |
+
return jsonify({'message': 'Refresh token is missing'}), 400
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
payload = jwt.decode(refresh_token, current_app.config['SECRET_KEY'], algorithms=["HS256"])
|
| 110 |
+
except jwt.ExpiredSignatureError:
|
| 111 |
+
return jsonify({'message': 'Refresh token has expired'}), 401
|
| 112 |
+
except jwt.InvalidTokenError:
|
| 113 |
+
return jsonify({'message': 'Invalid refresh token'}), 401
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
conn = get_db_connection()
|
| 117 |
+
username = RefreshToken.find_by_token(conn, refresh_token)
|
| 118 |
+
conn.close()
|
| 119 |
+
except Exception as e:
|
| 120 |
+
current_app.logger.exception("DB access error on refresh: %s", e)
|
| 121 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 122 |
+
|
| 123 |
+
if not username:
|
| 124 |
+
return jsonify({'message': 'Invalid refresh token'}), 401
|
| 125 |
+
|
| 126 |
+
# Generate new access token
|
| 127 |
+
new_access = jwt.encode(
|
| 128 |
+
{'username': username, 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=15)},
|
| 129 |
+
current_app.config['SECRET_KEY'],
|
| 130 |
+
algorithm="HS256"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
resp = make_response(jsonify({'access_token': new_access}))
|
| 134 |
+
add_cookie(resp, 'access_token', new_access, 900)
|
| 135 |
+
return resp
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@auth_bp.route("/logout", methods=["POST"])
|
| 139 |
+
@token_required
|
| 140 |
+
def logout(username):
|
| 141 |
+
"""User logout endpoint"""
|
| 142 |
+
token = request.cookies.get('access_token')
|
| 143 |
+
if not token:
|
| 144 |
+
return jsonify({"message": "Invalid token format"}), 401
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
conn = get_db_connection()
|
| 148 |
+
|
| 149 |
+
# Add to blacklist
|
| 150 |
+
BlacklistedToken.add_to_blacklist(conn, token)
|
| 151 |
+
|
| 152 |
+
# Delete refresh tokens
|
| 153 |
+
RefreshToken.delete_user_tokens(conn, username)
|
| 154 |
+
|
| 155 |
+
conn.close()
|
| 156 |
+
|
| 157 |
+
log_security_event("logout", username, request.remote_addr)
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
current_app.logger.exception("DB write error on logout: %s", e)
|
| 161 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 162 |
+
|
| 163 |
+
resp = make_response(jsonify({"message": "Logged out successfully!"}))
|
| 164 |
+
resp.delete_cookie('access_token', path='/')
|
| 165 |
+
resp.delete_cookie('refresh_token', path='/')
|
| 166 |
+
return resp
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
@auth_bp.route("/check-auth", methods=["GET"])
|
| 170 |
+
@token_required
|
| 171 |
+
def check_auth(username):
|
| 172 |
+
"""Check authentication status"""
|
| 173 |
+
return jsonify({"message": "Authenticated", "username": username}), 200
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
@auth_bp.route("/signup", methods=["POST"])
|
| 177 |
+
def signup():
|
| 178 |
+
"""User registration endpoint"""
|
| 179 |
+
data = request.json or {}
|
| 180 |
+
username = data.get('username', '').strip()
|
| 181 |
+
password = data.get('password', '')
|
| 182 |
+
|
| 183 |
+
# Input validation
|
| 184 |
+
is_valid, error_msg = validate_user_input(username, password)
|
| 185 |
+
if not is_valid:
|
| 186 |
+
return jsonify({"message": error_msg}), 400
|
| 187 |
+
|
| 188 |
+
# Normalize username (prevent duplicates like "Admin" and "admin")
|
| 189 |
+
username = username.lower()
|
| 190 |
+
|
| 191 |
+
try:
|
| 192 |
+
conn = get_db_connection()
|
| 193 |
+
|
| 194 |
+
# Check if username already exists
|
| 195 |
+
if User.find_by_username(conn, username):
|
| 196 |
+
conn.close()
|
| 197 |
+
return jsonify({"message": "Username already exists"}), 409
|
| 198 |
+
|
| 199 |
+
# Hash password
|
| 200 |
+
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
| 201 |
+
|
| 202 |
+
# Create new user
|
| 203 |
+
if User.create_user(conn, username, password_hash.decode('utf-8')):
|
| 204 |
+
conn.close()
|
| 205 |
+
log_security_event("user_registered", username, request.remote_addr)
|
| 206 |
+
return jsonify({"message": "User registered successfully"}), 201
|
| 207 |
+
else:
|
| 208 |
+
conn.close()
|
| 209 |
+
return jsonify({"message": "Username already exists"}), 409
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
current_app.logger.exception("DB error on signup: %s", e)
|
| 213 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
@auth_bp.route("/admin/promote-user", methods=["POST"])
|
| 217 |
+
@token_required
|
| 218 |
+
def promote_user(username):
|
| 219 |
+
"""Promote a user to admin role - ADMIN ONLY"""
|
| 220 |
+
try:
|
| 221 |
+
conn = get_db_connection()
|
| 222 |
+
|
| 223 |
+
# Check if current user is admin
|
| 224 |
+
if not is_admin_user(conn, username):
|
| 225 |
+
conn.close()
|
| 226 |
+
log_security_event("unauthorized_access", username, request.remote_addr, "promote-user")
|
| 227 |
+
return jsonify({"message": "Unauthorized - Admin access required"}), 403
|
| 228 |
+
|
| 229 |
+
# Get target username from request
|
| 230 |
+
data = request.json or {}
|
| 231 |
+
target_user = data.get('username', '').strip().lower()
|
| 232 |
+
|
| 233 |
+
if not target_user:
|
| 234 |
+
conn.close()
|
| 235 |
+
return jsonify({"message": "Username is required"}), 400
|
| 236 |
+
|
| 237 |
+
# Check if target user exists
|
| 238 |
+
target_user_obj = User.find_by_username(conn, target_user)
|
| 239 |
+
if not target_user_obj:
|
| 240 |
+
conn.close()
|
| 241 |
+
return jsonify({"message": "User not found"}), 404
|
| 242 |
+
|
| 243 |
+
if target_user_obj.role == 'admin':
|
| 244 |
+
conn.close()
|
| 245 |
+
return jsonify({"message": "User is already an admin"}), 400
|
| 246 |
+
|
| 247 |
+
# Promote user to admin
|
| 248 |
+
if User.promote_to_admin(conn, target_user):
|
| 249 |
+
conn.close()
|
| 250 |
+
log_security_event("user_promoted", username, request.remote_addr, f"promoted {target_user}")
|
| 251 |
+
return jsonify({"message": f"User {target_user} promoted to admin successfully"}), 200
|
| 252 |
+
else:
|
| 253 |
+
conn.close()
|
| 254 |
+
return jsonify({"message": "Failed to promote user"}), 500
|
| 255 |
+
|
| 256 |
+
except Exception as e:
|
| 257 |
+
current_app.logger.exception("DB error in promote-user: %s", e)
|
| 258 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@auth_bp.route("/admin/users", methods=["GET"])
|
| 262 |
+
@token_required
|
| 263 |
+
def list_users(username):
|
| 264 |
+
"""List all users - ADMIN ONLY"""
|
| 265 |
+
try:
|
| 266 |
+
conn = get_db_connection()
|
| 267 |
+
|
| 268 |
+
# Check if current user is admin
|
| 269 |
+
if not is_admin_user(conn, username):
|
| 270 |
+
conn.close()
|
| 271 |
+
log_security_event("unauthorized_access", username, request.remote_addr, "list-users")
|
| 272 |
+
return jsonify({"message": "Unauthorized - Admin access required"}), 403
|
| 273 |
+
|
| 274 |
+
# Get all users
|
| 275 |
+
users = User.get_all_users(conn)
|
| 276 |
+
conn.close()
|
| 277 |
+
|
| 278 |
+
log_security_event("admin_action", username, request.remote_addr, "viewed_user_list")
|
| 279 |
+
return jsonify({"users": users, "total": len(users)}), 200
|
| 280 |
+
|
| 281 |
+
except Exception as e:
|
| 282 |
+
current_app.logger.exception("DB error in list-users: %s", e)
|
| 283 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
@auth_bp.route("/admin/create-first-admin", methods=["POST"])
|
| 287 |
+
def create_first_admin():
|
| 288 |
+
"""Create the first admin user - ONLY if no users exist"""
|
| 289 |
+
try:
|
| 290 |
+
conn = get_db_connection()
|
| 291 |
+
|
| 292 |
+
# Check if any users exist
|
| 293 |
+
if User.user_count(conn) > 0:
|
| 294 |
+
conn.close()
|
| 295 |
+
return jsonify({"message": "Users already exist. Cannot create first admin."}), 409
|
| 296 |
+
|
| 297 |
+
# Create first admin user
|
| 298 |
+
username = "admin"
|
| 299 |
+
password = "admin123" # Should be changed immediately
|
| 300 |
+
|
| 301 |
+
# Hash password
|
| 302 |
+
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
| 303 |
+
|
| 304 |
+
# Create admin user
|
| 305 |
+
if User.create_user(conn, username, password_hash.decode('utf-8'), 'admin'):
|
| 306 |
+
conn.close()
|
| 307 |
+
log_security_event("first_admin_created", "system", request.remote_addr)
|
| 308 |
+
return jsonify({
|
| 309 |
+
"message": "First admin user created successfully",
|
| 310 |
+
"username": "admin",
|
| 311 |
+
"password": "admin123",
|
| 312 |
+
"warning": "CHANGE THE PASSWORD IMMEDIATELY!"
|
| 313 |
+
}), 201
|
| 314 |
+
else:
|
| 315 |
+
conn.close()
|
| 316 |
+
return jsonify({"message": "Failed to create admin user"}), 500
|
| 317 |
+
|
| 318 |
+
except Exception as e:
|
| 319 |
+
current_app.logger.exception("DB error creating first admin: %s", e)
|
| 320 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
@auth_bp.route("/db/diag", methods=["GET"])
|
| 324 |
+
@token_required
|
| 325 |
+
def db_diag(username):
|
| 326 |
+
"""Database diagnostics - ADMIN ONLY"""
|
| 327 |
+
try:
|
| 328 |
+
conn = get_db_connection()
|
| 329 |
+
|
| 330 |
+
# Security: Only allow admin users to access diagnostic information
|
| 331 |
+
if not is_admin_user(conn, username):
|
| 332 |
+
conn.close()
|
| 333 |
+
log_security_event("unauthorized_access", username, request.remote_addr, "db-diag")
|
| 334 |
+
return jsonify({"message": "Unauthorized - Admin access required"}), 403
|
| 335 |
+
|
| 336 |
+
conn.close()
|
| 337 |
+
except Exception as e:
|
| 338 |
+
current_app.logger.exception("DB access error in db_diag: %s", e)
|
| 339 |
+
return jsonify({"message": "Database is unavailable"}), 503
|
| 340 |
+
|
| 341 |
+
# Proceed with diagnostics for admin users only
|
| 342 |
+
from .database import get_database_info
|
| 343 |
+
info = get_database_info()
|
| 344 |
+
|
| 345 |
+
log_security_event("admin_action", username, request.remote_addr, "accessed_db_diagnostics")
|
| 346 |
+
return jsonify(info), 200
|
auth/utils.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication utilities and security functions
|
| 3 |
+
|
| 4 |
+
Contains:
|
| 5 |
+
- JWT token validation decorator
|
| 6 |
+
- Security helpers
|
| 7 |
+
- Username anonymization for logging
|
| 8 |
+
- Cookie management utilities
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import jwt
|
| 13 |
+
import hashlib
|
| 14 |
+
from functools import wraps
|
| 15 |
+
from flask import request, jsonify, current_app, make_response
|
| 16 |
+
from .database import get_db_connection
|
| 17 |
+
from .models import BlacklistedToken
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def anonymize_username(username):
|
| 21 |
+
"""Create anonymous hash for logging while preserving uniqueness"""
|
| 22 |
+
if not username:
|
| 23 |
+
return "anonymous"
|
| 24 |
+
return hashlib.sha256(f"user_{username}_salt".encode()).hexdigest()[:12]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def token_required(f):
|
| 28 |
+
"""
|
| 29 |
+
JWT token validation decorator
|
| 30 |
+
|
| 31 |
+
Validates access token from cookies and checks blacklist.
|
| 32 |
+
Returns username to the decorated function.
|
| 33 |
+
"""
|
| 34 |
+
@wraps(f)
|
| 35 |
+
def decorated(*args, **kwargs):
|
| 36 |
+
token = request.cookies.get('access_token')
|
| 37 |
+
if not token:
|
| 38 |
+
return jsonify({"message": "Token is missing"}), 401
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
# Check blacklist
|
| 42 |
+
conn = get_db_connection()
|
| 43 |
+
if BlacklistedToken.is_blacklisted(conn, token):
|
| 44 |
+
conn.close()
|
| 45 |
+
return jsonify({"message": "Token has been revoked. Please log in again."}), 401
|
| 46 |
+
conn.close()
|
| 47 |
+
|
| 48 |
+
# Decode and validate token
|
| 49 |
+
data = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])
|
| 50 |
+
return f(data['username'], *args, **kwargs)
|
| 51 |
+
|
| 52 |
+
except jwt.ExpiredSignatureError:
|
| 53 |
+
return jsonify({"message": "Token has expired"}), 401
|
| 54 |
+
except jwt.InvalidTokenError:
|
| 55 |
+
return jsonify({"message": "Invalid token"}), 401
|
| 56 |
+
except Exception as e:
|
| 57 |
+
current_app.logger.exception("Auth error: %s", e)
|
| 58 |
+
return jsonify({"message": "Server error"}), 500
|
| 59 |
+
return decorated
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def extract_username_from_request(req) -> str | None:
|
| 63 |
+
"""
|
| 64 |
+
Extract username from various sources in request
|
| 65 |
+
|
| 66 |
+
Checks in order:
|
| 67 |
+
1. X-User header
|
| 68 |
+
2. Request body JSON
|
| 69 |
+
3. JWT cookie
|
| 70 |
+
"""
|
| 71 |
+
# 1) Header
|
| 72 |
+
hdr = req.headers.get("X-User")
|
| 73 |
+
if hdr:
|
| 74 |
+
return hdr
|
| 75 |
+
|
| 76 |
+
# 2) Body
|
| 77 |
+
data = req.get_json(silent=True) or {}
|
| 78 |
+
if data.get("username"):
|
| 79 |
+
return data.get("username")
|
| 80 |
+
|
| 81 |
+
# 3) JWT cookie
|
| 82 |
+
token = req.cookies.get("access_token")
|
| 83 |
+
if token:
|
| 84 |
+
try:
|
| 85 |
+
payload = jwt.decode(token, current_app.config["SECRET_KEY"], algorithms=["HS256"])
|
| 86 |
+
return payload.get("username")
|
| 87 |
+
except jwt.ExpiredSignatureError:
|
| 88 |
+
return None
|
| 89 |
+
except jwt.InvalidTokenError:
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def add_cookie(resp, name: str, value: str, max_age: int):
|
| 96 |
+
"""
|
| 97 |
+
Add secure cookie to response
|
| 98 |
+
|
| 99 |
+
In prod: Secure + SameSite=None + Partitioned (works with third-party cookie protections).
|
| 100 |
+
In dev: SameSite=Lax, not Secure.
|
| 101 |
+
"""
|
| 102 |
+
IS_PROD = os.getenv("ENV", "dev").lower() == "prod"
|
| 103 |
+
|
| 104 |
+
if IS_PROD:
|
| 105 |
+
resp.headers.add(
|
| 106 |
+
"Set-Cookie",
|
| 107 |
+
f"{name}={value}; Path=/; Max-Age={max_age}; Secure; HttpOnly; SameSite=None; Partitioned"
|
| 108 |
+
)
|
| 109 |
+
else:
|
| 110 |
+
resp.set_cookie(name, value, httponly=True, secure=False, samesite="Lax", max_age=max_age, path="/")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def validate_user_input(username: str, password: str) -> tuple[bool, str]:
|
| 114 |
+
"""
|
| 115 |
+
Validate user input for signup/login
|
| 116 |
+
|
| 117 |
+
Returns: (is_valid, error_message)
|
| 118 |
+
"""
|
| 119 |
+
if not username or not password:
|
| 120 |
+
return False, "Username and password are required"
|
| 121 |
+
|
| 122 |
+
if len(username) < 3 or len(username) > 50:
|
| 123 |
+
return False, "Username must be 3-50 characters"
|
| 124 |
+
|
| 125 |
+
if len(password) < 8:
|
| 126 |
+
return False, "Password must be at least 8 characters"
|
| 127 |
+
|
| 128 |
+
# Additional validation can be added here
|
| 129 |
+
# - Special character requirements
|
| 130 |
+
# - Username format validation
|
| 131 |
+
# - Password complexity checks
|
| 132 |
+
|
| 133 |
+
return True, ""
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def is_admin_user(conn, username: str) -> bool:
|
| 137 |
+
"""Check if user has admin role"""
|
| 138 |
+
from .models import User
|
| 139 |
+
user = User.find_by_username(conn, username)
|
| 140 |
+
return user is not None and user.role == 'admin'
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def log_security_event(event_type: str, username: str, ip_address: str, details: str = ""):
|
| 144 |
+
"""
|
| 145 |
+
Log security events with anonymized usernames
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
event_type: Type of security event (login, logout, failed_login, etc.)
|
| 149 |
+
username: Username (will be anonymized)
|
| 150 |
+
ip_address: Request IP address
|
| 151 |
+
details: Additional details about the event
|
| 152 |
+
"""
|
| 153 |
+
user_hash = anonymize_username(username)
|
| 154 |
+
current_app.logger.info(
|
| 155 |
+
f"Security Event [{event_type}]: user_hash={user_hash}, ip={ip_address}, details={details}"
|
| 156 |
+
)
|
chroma_db/cc71e05b-734e-41ed-99a2-95d0f26626eb/data_level0.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d3c9fd302f000d7790aa403c2d0d8fec363fe46f30b07d53020b6e33b22435a9
|
| 3 |
+
size 1676000
|
chroma_db/cc71e05b-734e-41ed-99a2-95d0f26626eb/header.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e87a1dc8bcae6f2c4bea6d5dd5005454d4dace8637dae29bff3c037ea771411e
|
| 3 |
+
size 100
|
chroma_db/cc71e05b-734e-41ed-99a2-95d0f26626eb/length.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fc19b1997119425765295aeab72d76faa6927d4f83985d328c26f20468d6cc76
|
| 3 |
+
size 4000
|
AUTHENTICATION_FIXES_COMPLETED.md → chroma_db/cc71e05b-734e-41ed-99a2-95d0f26626eb/link_lists.bin
RENAMED
|
File without changes
|
requirements.txt
CHANGED
|
@@ -43,7 +43,7 @@ pytesseract==0.3.10
|
|
| 43 |
Pillow==10.4.0
|
| 44 |
|
| 45 |
# Misc
|
| 46 |
-
|
| 47 |
tiktoken==0.11.0
|
| 48 |
torchcodec
|
| 49 |
phonemizer
|
|
|
|
| 43 |
Pillow==10.4.0
|
| 44 |
|
| 45 |
# Misc
|
| 46 |
+
|
| 47 |
tiktoken==0.11.0
|
| 48 |
torchcodec
|
| 49 |
phonemizer
|
sign-up.service.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http'; // Import HttpClient for API calls
|
| 3 |
+
import { Observable } from 'rxjs'; // Import Observable for async handling
|
| 4 |
+
|
| 5 |
+
@Injectable({ providedIn: 'root' })
|
| 6 |
+
export class SignUpService {
|
| 7 |
+
private apiUrl = location.hostname.endsWith('hf.space')
|
| 8 |
+
? 'https://majemaai-mj-learn-backend.hf.space'
|
| 9 |
+
: 'http://localhost:5000';
|
| 10 |
+
|
| 11 |
+
constructor(private http: HttpClient) { }
|
| 12 |
+
|
| 13 |
+
// ? Updated to use /auth/signup endpoint
|
| 14 |
+
signUp(payload: any): Observable<any> {
|
| 15 |
+
return this.http.post(`${this.apiUrl}/auth/signup`, payload);
|
| 16 |
+
}
|
| 17 |
+
}
|
src/app/auth/auth.service.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http';
|
| 3 |
+
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
|
| 4 |
+
import { catchError, tap } from 'rxjs/operators';
|
| 5 |
+
import { Router } from '@angular/router';
|
| 6 |
+
|
| 7 |
+
@Injectable({
|
| 8 |
+
providedIn: 'root'
|
| 9 |
+
})
|
| 10 |
+
export class AuthService {
|
| 11 |
+
private apiUrl = location.hostname.endsWith('hf.space')
|
| 12 |
+
? 'https://majemaai-mj-learn-backend.hf.space'
|
| 13 |
+
: 'http://localhost:5000';
|
| 14 |
+
|
| 15 |
+
private loggedInSubject = new BehaviorSubject<boolean>(false);
|
| 16 |
+
public loggedIn$ = this.loggedInSubject.asObservable();
|
| 17 |
+
private refreshInterval: any;
|
| 18 |
+
|
| 19 |
+
constructor(private http: HttpClient, private router: Router) { }
|
| 20 |
+
|
| 21 |
+
// ? Updated to use /auth/login endpoint
|
| 22 |
+
login(username: string, password: string): Observable<any> {
|
| 23 |
+
return this.http.post(`${this.apiUrl}/auth/login`, { username, password }, { withCredentials: true });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
setLoggedIn(status: boolean): void {
|
| 27 |
+
this.loggedInSubject.next(status);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
isLoggedIn(): boolean {
|
| 31 |
+
return this.loggedInSubject.value;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// ? Updated to use /auth/refresh endpoint
|
| 35 |
+
startAutoRefresh(): void {
|
| 36 |
+
this.clearAutoRefresh(); // Clear any existing interval
|
| 37 |
+
|
| 38 |
+
this.refreshInterval = setInterval(() => {
|
| 39 |
+
console.log("?? Auto-refreshing token...");
|
| 40 |
+
this.http.post(`${this.apiUrl}/auth/refresh`, {}, { withCredentials: true }).subscribe(
|
| 41 |
+
response => {
|
| 42 |
+
console.log('? Auto-refresh successful');
|
| 43 |
+
},
|
| 44 |
+
error => {
|
| 45 |
+
console.error('? Auto-refresh failed:', error);
|
| 46 |
+
// Clear tokens manually (if needed)
|
| 47 |
+
this.clearTokens();
|
| 48 |
+
|
| 49 |
+
// Redirect to login
|
| 50 |
+
this.router.navigate(['/auth']);
|
| 51 |
+
}
|
| 52 |
+
);
|
| 53 |
+
}, 14 * 60 * 1000); // Every 14 minutes
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
clearAutoRefresh(): void {
|
| 57 |
+
if (this.refreshInterval) {
|
| 58 |
+
clearInterval(this.refreshInterval);
|
| 59 |
+
this.refreshInterval = null;
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// ? Updated to use /auth/logout endpoint
|
| 64 |
+
logout(): Observable<any> {
|
| 65 |
+
console.log("?? Sending logout request with credentials");
|
| 66 |
+
|
| 67 |
+
return this.http.post(`${this.apiUrl}/auth/logout`, {}, { withCredentials: true }).pipe(
|
| 68 |
+
tap(response => {
|
| 69 |
+
console.log('?? Response from backend:', response);
|
| 70 |
+
this.clearTokens();
|
| 71 |
+
this.clearAutoRefresh(); // ? Stop auto-refresh
|
| 72 |
+
this.setLoggedIn(false); // ? Reflect logout in UI
|
| 73 |
+
}),
|
| 74 |
+
catchError(error => {
|
| 75 |
+
console.error('? Error from backend:', error);
|
| 76 |
+
// Still ensure local state reflects logout
|
| 77 |
+
this.clearTokens();
|
| 78 |
+
this.clearAutoRefresh();
|
| 79 |
+
this.setLoggedIn(false);
|
| 80 |
+
return throwError(() => error);
|
| 81 |
+
})
|
| 82 |
+
);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
clearTokens(): void {
|
| 86 |
+
document.cookie = 'access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
| 87 |
+
document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
| 88 |
+
this.clearAutoRefresh(); // ?
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// ? Get access token (using cookies)
|
| 92 |
+
getAccessToken(): string | null {
|
| 93 |
+
const cookies = document.cookie.split('; ');
|
| 94 |
+
for (let cookie of cookies) {
|
| 95 |
+
if (cookie.startsWith('access_token=')) {
|
| 96 |
+
return cookie.split('=')[1];
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
return null;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// ? Save tokens (Not necessary, as tokens are managed via cookies)
|
| 103 |
+
saveTokens(accessToken: string, refreshToken: string): void {
|
| 104 |
+
// No need for this if you're using cookies, but if you want to persist in localStorage, use:
|
| 105 |
+
localStorage.setItem('access_token', accessToken);
|
| 106 |
+
localStorage.setItem('refresh_token', refreshToken);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// ? Updated to use /auth/check-auth endpoint
|
| 110 |
+
checkSession(): Observable<boolean> {
|
| 111 |
+
return this.http.get(`${this.apiUrl}/auth/check-auth`, { withCredentials: true }).pipe(
|
| 112 |
+
tap((res: any) => {
|
| 113 |
+
console.log('? Session valid:', res);
|
| 114 |
+
this.setLoggedIn(true);
|
| 115 |
+
this.startAutoRefresh();
|
| 116 |
+
return true; // ? Important!
|
| 117 |
+
}),
|
| 118 |
+
catchError((err) => {
|
| 119 |
+
if (err.status === 401) {
|
| 120 |
+
// Access token may be expired. Try refresh
|
| 121 |
+
console.warn('?? Access token expired. Trying to refresh...');
|
| 122 |
+
|
| 123 |
+
// ? Updated to use /auth/refresh endpoint
|
| 124 |
+
return this.http.post(`${this.apiUrl}/auth/refresh`, {}, { withCredentials: true }).pipe(
|
| 125 |
+
tap((refreshRes: any) => {
|
| 126 |
+
console.log("? Token refreshed during checkSession.");
|
| 127 |
+
//alert("? Token refreshed during checkSession.");
|
| 128 |
+
this.setLoggedIn(true);
|
| 129 |
+
this.startAutoRefresh();
|
| 130 |
+
}),
|
| 131 |
+
catchError((refreshErr) => {
|
| 132 |
+
console.error("? Refresh token failed during checkSession.", refreshErr);
|
| 133 |
+
this.setLoggedIn(false);
|
| 134 |
+
return of(false);
|
| 135 |
+
})
|
| 136 |
+
);
|
| 137 |
+
} else {
|
| 138 |
+
console.error("? Unknown error during checkSession", err);
|
| 139 |
+
this.setLoggedIn(false);
|
| 140 |
+
return of(false);
|
| 141 |
+
}
|
| 142 |
+
})
|
| 143 |
+
);
|
| 144 |
+
}
|
| 145 |
+
}
|
src/app/sign-up/sign-up.service.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http'; // Import HttpClient for API calls
|
| 3 |
+
import { Observable } from 'rxjs'; // Import Observable for async handling
|
| 4 |
+
|
| 5 |
+
@Injectable({ providedIn: 'root' })
|
| 6 |
+
export class SignUpService {
|
| 7 |
+
private apiUrl = location.hostname.endsWith('hf.space')
|
| 8 |
+
? 'https://majemaai-mj-learn-backend.hf.space'
|
| 9 |
+
: 'http://localhost:5000';
|
| 10 |
+
|
| 11 |
+
constructor(private http: HttpClient) { }
|
| 12 |
+
|
| 13 |
+
// ? Updated to use /auth/signup endpoint
|
| 14 |
+
signUp(payload: any): Observable<any> {
|
| 15 |
+
return this.http.post(`${this.apiUrl}/auth/signup`, payload);
|
| 16 |
+
}
|
| 17 |
+
}
|
verification.py
CHANGED
|
@@ -1,749 +1,169 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
-
import requests
|
| 5 |
-
import hashlib # Added for secure logging
|
| 6 |
-
from werkzeug.utils import secure_filename
|
| 7 |
-
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
| 8 |
-
load_dotenv(os.path.join(BASEDIR, ".env")) # loads DB_USER, DB_PASSWORD, RUN_INIT_DB
|
| 9 |
-
import socket
|
| 10 |
import logging
|
| 11 |
-
from
|
| 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, current_app
|
| 18 |
from flask_cors import CORS
|
| 19 |
|
| 20 |
-
#
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def anonymize_username(username):
|
| 24 |
-
"""Create anonymous hash for logging while preserving uniqueness"""
|
| 25 |
-
if not username:
|
| 26 |
-
return "anonymous"
|
| 27 |
-
return hashlib.sha256(f"user_{username}_salt".encode()).hexdigest()[:12]
|
| 28 |
-
|
| 29 |
-
# ------------------------------------------------------------------------------
|
| 30 |
-
# App, ENV, CORS
|
| 31 |
-
# ------------------------------------------------------------------------------
|
| 32 |
-
app = Flask(__name__)
|
| 33 |
-
|
| 34 |
-
# FIXED: Move SECRET_KEY to environment variable
|
| 35 |
-
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
|
| 36 |
-
if not app.config['SECRET_KEY']:
|
| 37 |
-
raise RuntimeError("SECRET_KEY must be set in environment variables!")
|
| 38 |
-
|
| 39 |
-
IS_PROD = os.getenv("ENV", "dev").lower() == "prod"
|
| 40 |
-
_origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:4200")
|
| 41 |
-
ALLOWED_ORIGINS = [o.strip() for o in _origins.split(",") if o.strip()]
|
| 42 |
-
# CORS(app, supports_credentials=True, origins=ALLOWED_ORIGINS)
|
| 43 |
-
# Allow both localhost forms by default if env not set
|
| 44 |
-
_default_origins = "http://localhost:4200,http://127.0.0.1:4200"
|
| 45 |
-
_origins = os.getenv("ALLOWED_ORIGINS", _default_origins)
|
| 46 |
-
ALLOWED_ORIGINS = [o.strip() for o in _origins.split(",") if o.strip()]
|
| 47 |
-
|
| 48 |
-
CORS(
|
| 49 |
-
app,
|
| 50 |
-
resources={r"/*": {"origins": ALLOWED_ORIGINS}},
|
| 51 |
-
supports_credentials=True,
|
| 52 |
-
allow_headers=["Content-Type", "Authorization", "X-Requested-With", "X-User"],
|
| 53 |
-
expose_headers=["Set-Cookie"],
|
| 54 |
-
methods=["GET", "POST", "OPTIONS"] # FIXED: Separated "POST, OPTIONS" into individual strings
|
| 55 |
-
)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def extract_username_from_request(req) -> str | None:
|
| 59 |
-
# 1) Header
|
| 60 |
-
hdr = req.headers.get("X-User")
|
| 61 |
-
if hdr:
|
| 62 |
-
return hdr
|
| 63 |
-
|
| 64 |
-
# 2) Body
|
| 65 |
-
data = req.get_json(silent=True) or {}
|
| 66 |
-
if data.get("username"):
|
| 67 |
-
return data.get("username")
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
try:
|
| 73 |
-
payload = jwt.decode(token, current_app.config["SECRET_KEY"], algorithms=["HS256"])
|
| 74 |
-
return payload.get("username")
|
| 75 |
-
except jwt.ExpiredSignatureError:
|
| 76 |
-
return None
|
| 77 |
-
except jwt.InvalidTokenError:
|
| 78 |
-
return None
|
| 79 |
|
| 80 |
-
return None
|
| 81 |
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
origin = request.headers.get("Origin")
|
| 101 |
if origin and origin in ALLOWED_ORIGINS:
|
| 102 |
resp.headers["Access-Control-Allow-Origin"] = origin
|
|
|
|
| 103 |
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
req_method = request.headers.get("Access-Control-Request-Method", "POST")
|
| 107 |
-
resp.headers["Access-Control-Allow-Headers"] = req_headers
|
| 108 |
-
resp.headers["Access-Control-Allow-Methods"] = req_method
|
| 109 |
return resp
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
logging.basicConfig(level=logging.INFO)
|
| 113 |
-
|
| 114 |
-
# NEW: API keys / shared config for blueprints (read from HF Secrets/ENV)
|
| 115 |
-
app.config["COHERE_API_KEY"] = os.getenv("COHERE_API_KEY", "")
|
| 116 |
-
|
| 117 |
-
# ------------------------------------------------------------------------------
|
| 118 |
-
# SQL Server configuration
|
| 119 |
-
# ------------------------------------------------------------------------------
|
| 120 |
-
# DB_SERVER = "pykara-sqlserver.cb60o04yk948.ap-south-1.rds.amazonaws.com,1433"
|
| 121 |
-
# DB_DATABASE = "AuthenticationDB1"
|
| 122 |
-
|
| 123 |
-
DB_SERVER = os.getenv("DB_SERVER", r"(localdb)\MSSQLLocalDB")
|
| 124 |
-
DB_DATABASE = os.getenv("DB_DATABASE", "AuthenticationDB1")
|
| 125 |
-
DB_DRIVER = os.getenv("DB_DRIVER", "ODBC Driver 17 for SQL Server") # 17 in your image
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
# Build connection string (FIXED)
|
| 129 |
-
is_local = (
|
| 130 |
-
DB_SERVER.lower().startswith("localhost")
|
| 131 |
-
or DB_SERVER.startswith(".")
|
| 132 |
-
or DB_SERVER.lower().startswith("(localdb)")
|
| 133 |
-
or "\\" in DB_SERVER
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
if is_local:
|
| 137 |
-
# Windows local / LocalDB using modern ODBC driver
|
| 138 |
-
CONN_STR = (
|
| 139 |
-
f"DRIVER={{{DB_DRIVER}}};"
|
| 140 |
-
f"SERVER={DB_SERVER};"
|
| 141 |
-
f"DATABASE={DB_DATABASE};"
|
| 142 |
-
"Trusted_Connection=yes;"
|
| 143 |
-
"TrustServerCertificate=yes;"
|
| 144 |
-
)
|
| 145 |
-
else:
|
| 146 |
-
# Remote SQL auth
|
| 147 |
-
CONN_STR = (
|
| 148 |
-
f"DRIVER={{{DB_DRIVER}}};"
|
| 149 |
-
f"SERVER={DB_SERVER};DATABASE={DB_DATABASE};"
|
| 150 |
-
f"UID={os.getenv('DB_USER')};PWD={os.getenv('DB_PASSWORD')};"
|
| 151 |
-
"Encrypt=yes;TrustServerCertificate=yes;"
|
| 152 |
-
)
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def get_db_connection():
|
| 156 |
-
"""Create a short-timeout connection. Fail clearly if secrets are missing."""
|
| 157 |
-
if "Trusted_Connection=yes" not in CONN_STR:
|
| 158 |
-
if not os.getenv("DB_USER") or not os.getenv("DB_PASSWORD"):
|
| 159 |
-
raise RuntimeError("DB_USER/DB_PASSWORD are not set in the environment.")
|
| 160 |
-
return pyodbc.connect(CONN_STR, timeout=5)
|
| 161 |
-
|
| 162 |
-
# ------------------------------------------------------------------------------
|
| 163 |
-
# Auth utilities
|
| 164 |
-
# ------------------------------------------------------------------------------
|
| 165 |
-
def token_required(f):
|
| 166 |
-
@wraps(f)
|
| 167 |
-
def decorated(*args, **kwargs):
|
| 168 |
-
token = request.cookies.get('access_token')
|
| 169 |
-
if not token:
|
| 170 |
-
return jsonify({"message": "Token is missing"}), 401
|
| 171 |
-
|
| 172 |
-
try:
|
| 173 |
-
# Check blacklist
|
| 174 |
-
conn = get_db_connection()
|
| 175 |
-
cur = conn.cursor()
|
| 176 |
-
cur.execute("SELECT token FROM BlacklistedTokens WHERE token = ?", (token,))
|
| 177 |
-
if cur.fetchone():
|
| 178 |
-
conn.close()
|
| 179 |
-
return jsonify({"message": "Token has been revoked. Please log in again."}), 401
|
| 180 |
-
conn.close()
|
| 181 |
-
|
| 182 |
-
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
|
| 183 |
-
return f(data['username'], *args, **kwargs)
|
| 184 |
-
|
| 185 |
-
except jwt.ExpiredSignatureError:
|
| 186 |
-
return jsonify({"message": "Token has expired"}), 401
|
| 187 |
-
except jwt.InvalidTokenError:
|
| 188 |
-
return jsonify({"message": "Invalid token"}), 401
|
| 189 |
-
except Exception as e:
|
| 190 |
-
app.logger.exception("Auth error: %s", e)
|
| 191 |
-
return jsonify({"message": "Server error"}), 500
|
| 192 |
-
return decorated
|
| 193 |
-
|
| 194 |
-
@app.get("/db/diag")
|
| 195 |
-
@token_required
|
| 196 |
-
def db_diag(username):
|
| 197 |
-
"""Database diagnostics - ADMIN ONLY"""
|
| 198 |
-
# Security: Only allow admin users to access diagnostic information
|
| 199 |
-
try:
|
| 200 |
-
conn = get_db_connection()
|
| 201 |
-
cur = conn.cursor()
|
| 202 |
-
cur.execute("SELECT role FROM Users WHERE username = ?", (username,))
|
| 203 |
-
row = cur.fetchone()
|
| 204 |
-
|
| 205 |
-
if not row or row[0] != 'admin':
|
| 206 |
-
conn.close()
|
| 207 |
-
user_hash = anonymize_username(username)
|
| 208 |
-
app.logger.warning(f"Unauthorized db/diag access attempt by user_hash: {user_hash} from IP: {request.remote_addr}")
|
| 209 |
-
return jsonify({"message": "Unauthorized - Admin access required"}), 403
|
| 210 |
-
|
| 211 |
-
conn.close()
|
| 212 |
-
except Exception as e:
|
| 213 |
-
app.logger.exception("DB access error in db_diag: %s", e)
|
| 214 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 215 |
-
|
| 216 |
-
# Proceed with diagnostics for admin users only
|
| 217 |
-
info = {}
|
| 218 |
-
try:
|
| 219 |
-
info["drivers_found"] = pyodbc.drivers()
|
| 220 |
-
except Exception as e:
|
| 221 |
-
info["drivers_found_error"] = str(e)
|
| 222 |
-
|
| 223 |
-
# Only show minimal, safe information
|
| 224 |
-
info["database_name"] = DB_DATABASE # Safe to show database name to admin
|
| 225 |
-
info["server_type"] = "LocalDB" if is_local else "Remote" # General info only
|
| 226 |
-
|
| 227 |
-
# Test connection without exposing sensitive details
|
| 228 |
-
try:
|
| 229 |
-
conn = get_db_connection()
|
| 230 |
-
conn.close()
|
| 231 |
-
info["connection_status"] = "ok"
|
| 232 |
-
except Exception as e:
|
| 233 |
-
info["connection_status"] = "error"
|
| 234 |
-
info["error_type"] = type(e).__name__ # General error type only
|
| 235 |
-
|
| 236 |
-
# Security logging (ANONYMIZED)
|
| 237 |
-
user_hash = anonymize_username(username)
|
| 238 |
-
app.logger.info(f"Admin user_hash: {user_hash} accessed database diagnostics from IP: {request.remote_addr}")
|
| 239 |
-
|
| 240 |
-
return jsonify(info), 200
|
| 241 |
-
|
| 242 |
-
def init_db():
|
| 243 |
-
"""Create tables if they do not exist."""
|
| 244 |
-
conn = get_db_connection()
|
| 245 |
-
cur = conn.cursor()
|
| 246 |
-
|
| 247 |
-
cur.execute("""
|
| 248 |
-
IF OBJECT_ID('Users', 'U') IS NULL
|
| 249 |
-
CREATE TABLE Users (
|
| 250 |
-
id INT IDENTITY(1,1) PRIMARY KEY,
|
| 251 |
-
username NVARCHAR(100) UNIQUE NOT NULL,
|
| 252 |
-
password_hash NVARCHAR(500) NOT NULL,
|
| 253 |
-
role NVARCHAR(50) DEFAULT 'user'
|
| 254 |
-
)
|
| 255 |
-
""")
|
| 256 |
-
|
| 257 |
-
cur.execute("""
|
| 258 |
-
IF OBJECT_ID('BlacklistedTokens', 'U') IS NULL
|
| 259 |
-
CREATE TABLE BlacklistedTokens (
|
| 260 |
-
id INT IDENTITY(1,1) PRIMARY KEY,
|
| 261 |
-
token NVARCHAR(1000) UNIQUE NOT NULL,
|
| 262 |
-
created_at DATETIME DEFAULT GETDATE()
|
| 263 |
-
)
|
| 264 |
-
""")
|
| 265 |
-
|
| 266 |
-
cur.execute("""
|
| 267 |
-
IF OBJECT_ID('RefreshTokens', 'U') IS NULL
|
| 268 |
-
CREATE TABLE RefreshTokens (
|
| 269 |
-
id INT IDENTITY(1,1) PRIMARY KEY,
|
| 270 |
-
username NVARCHAR(100) NOT NULL,
|
| 271 |
-
token NVARCHAR(1000) UNIQUE NOT NULL,
|
| 272 |
-
created_at DATETIME DEFAULT GETDATE(),
|
| 273 |
-
FOREIGN KEY (username) REFERENCES Users(username) ON DELETE CASCADE
|
| 274 |
-
)
|
| 275 |
-
""")
|
| 276 |
-
|
| 277 |
-
conn.commit()
|
| 278 |
-
conn.close()
|
| 279 |
-
|
| 280 |
-
# ------------------------------------------------------------------------------
|
| 281 |
-
# One-time DB initialisation (Flask 3.x safe)
|
| 282 |
-
# ------------------------------------------------------------------------------
|
| 283 |
-
_db_init_done = False
|
| 284 |
-
_db_init_lock = Lock()
|
| 285 |
-
_should_init = os.getenv("RUN_INIT_DB", "0") == "1"
|
| 286 |
-
|
| 287 |
-
@app.before_request
|
| 288 |
-
def maybe_init_db():
|
| 289 |
-
global _db_init_done
|
| 290 |
-
if _should_init and not _db_init_done:
|
| 291 |
-
with _db_init_lock:
|
| 292 |
-
if not _db_init_done:
|
| 293 |
-
try:
|
| 294 |
-
init_db()
|
| 295 |
-
app.logger.info("Database initialised.")
|
| 296 |
-
except Exception as e:
|
| 297 |
-
app.logger.exception("DB init failed: %s", e)
|
| 298 |
-
finally:
|
| 299 |
-
_db_init_done = True
|
| 300 |
-
|
| 301 |
-
# ------------------------------------------------------------------------------
|
| 302 |
-
# Cookie helpers
|
| 303 |
-
# ------------------------------------------------------------------------------
|
| 304 |
-
def add_cookie(resp, name: str, value: str, max_age: int):
|
| 305 |
-
"""
|
| 306 |
-
In prod: Secure + SameSite=None + Partitioned (works with third-party cookie protections).
|
| 307 |
-
In dev: SameSite=Lax, not Secure.
|
| 308 |
-
"""
|
| 309 |
-
if IS_PROD:
|
| 310 |
-
resp.headers.add(
|
| 311 |
-
"Set-Cookie",
|
| 312 |
-
f"{name}={value}; Path=/; Max-Age={max_age}; Secure; HttpOnly; SameSite=None; Partitioned"
|
| 313 |
-
)
|
| 314 |
-
else:
|
| 315 |
-
resp.set_cookie(name, value, httponly=True, secure=False, samesite="Lax", max_age=max_age, path="/")
|
| 316 |
-
|
| 317 |
-
# ------------------------------------------------------------------------------
|
| 318 |
-
# Health
|
| 319 |
-
# ------------------------------------------------------------------------------
|
| 320 |
-
@app.get("/")
|
| 321 |
-
def health():
|
| 322 |
-
return {"status": "ok"}, 200
|
| 323 |
-
|
| 324 |
-
# ------------------------------------------------------------------------------
|
| 325 |
-
# Routes (verification/auth only)
|
| 326 |
-
# ----------------------------------------------------------------------------
|
| 327 |
-
@app.get("/dashboard")
|
| 328 |
-
@token_required
|
| 329 |
-
def dashboard(username):
|
| 330 |
-
return jsonify({"message": f"Welcome {username} to your dashboard!"})
|
| 331 |
-
|
| 332 |
-
@app.post("/login")
|
| 333 |
-
def login():
|
| 334 |
-
data = request.json or {}
|
| 335 |
-
username = data.get('username', '').strip()
|
| 336 |
-
password = data.get('password', '')
|
| 337 |
-
|
| 338 |
-
# ADDED: Input validation
|
| 339 |
-
if not username or not password:
|
| 340 |
-
return jsonify({"message": "Username and password are required"}), 400
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
|
| 345 |
-
#
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
except Exception as e:
|
| 355 |
-
app.logger.exception("DB access error on login: %s", e)
|
| 356 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 357 |
-
|
| 358 |
-
if not row:
|
| 359 |
-
# ADDED: Security logging for failed login attempts (ANONYMIZED)
|
| 360 |
-
user_hash = anonymize_username(username)
|
| 361 |
-
app.logger.warning(f"Failed login attempt for user_hash: {user_hash} from IP: {request.remote_addr}")
|
| 362 |
-
return jsonify({"message": "Invalid credentials"}), 401
|
| 363 |
-
|
| 364 |
-
stored_hash = row[0]
|
| 365 |
-
if not bcrypt.checkpw(password.encode('utf-8'), stored_hash.encode('utf-8')):
|
| 366 |
-
# ADDED: Security logging for wrong password (ANONYMIZED)
|
| 367 |
-
user_hash = anonymize_username(username)
|
| 368 |
-
app.logger.warning(f"Failed login (wrong password) for user_hash: {user_hash} from IP: {request.remote_addr}")
|
| 369 |
-
return jsonify({"message": "Invalid credentials"}), 401
|
| 370 |
-
|
| 371 |
-
# ADDED: Security logging for successful login (ANONYMIZED)
|
| 372 |
-
user_hash = anonymize_username(username)
|
| 373 |
-
app.logger.info(f"Successful login for user_hash: {user_hash} from IP: {request.remote_addr}")
|
| 374 |
-
|
| 375 |
-
access_token = jwt.encode(
|
| 376 |
-
{'username': username, 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=15)},
|
| 377 |
-
app.config['SECRET_KEY'],
|
| 378 |
-
algorithm="HS256"
|
| 379 |
-
)
|
| 380 |
-
refresh_token = jwt.encode(
|
| 381 |
-
{'username': username, 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=7)},
|
| 382 |
-
app.config['SECRET_KEY'],
|
| 383 |
-
algorithm="HS256"
|
| 384 |
-
)
|
| 385 |
-
|
| 386 |
-
try:
|
| 387 |
-
conn = get_db_connection()
|
| 388 |
-
cur = conn.cursor()
|
| 389 |
-
cur.execute("INSERT INTO RefreshTokens (username, token) VALUES (?, ?)", (username, refresh_token))
|
| 390 |
-
conn.commit()
|
| 391 |
-
conn.close()
|
| 392 |
-
except Exception as e:
|
| 393 |
-
app.logger.exception("DB write error on login: %s", e)
|
| 394 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 395 |
-
|
| 396 |
-
resp = make_response(jsonify({"message": "Login successful"}))
|
| 397 |
-
add_cookie(resp, 'access_token', access_token, 900) # 15 min
|
| 398 |
-
add_cookie(resp, 'refresh_token', refresh_token, 7*24*60*60) # 7 days
|
| 399 |
-
return resp
|
| 400 |
-
|
| 401 |
-
@app.post("/refresh")
|
| 402 |
-
def refresh():
|
| 403 |
-
refresh_token = request.cookies.get("refresh_token")
|
| 404 |
-
if not refresh_token:
|
| 405 |
-
return jsonify({'message': 'Refresh token is missing'}), 400
|
| 406 |
-
|
| 407 |
-
try:
|
| 408 |
-
payload = jwt.decode(refresh_token, app.config['SECRET_KEY'], algorithms=["HS256"])
|
| 409 |
-
except jwt.ExpiredSignatureError:
|
| 410 |
-
return jsonify({'message': 'Refresh token has expired'}), 401
|
| 411 |
-
except jwt.InvalidTokenError:
|
| 412 |
-
return jsonify({'message': 'Invalid refresh token'}), 401
|
| 413 |
-
|
| 414 |
-
try:
|
| 415 |
-
conn = get_db_connection()
|
| 416 |
-
cur = conn.cursor()
|
| 417 |
-
cur.execute("SELECT username FROM RefreshTokens WHERE token = ?", (refresh_token,))
|
| 418 |
-
row = cur.fetchone()
|
| 419 |
-
conn.close()
|
| 420 |
-
except Exception as e:
|
| 421 |
-
app.logger.exception("DB access error on refresh: %s", e)
|
| 422 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 423 |
-
|
| 424 |
-
if not row:
|
| 425 |
-
return jsonify({'message': 'Invalid refresh token'}), 401
|
| 426 |
-
|
| 427 |
-
username = row[0]
|
| 428 |
-
# FIXED: Use updated datetime format
|
| 429 |
-
new_access = jwt.encode(
|
| 430 |
-
{'username': username, 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=15)},
|
| 431 |
-
app.config['SECRET_KEY'],
|
| 432 |
-
algorithm="HS256"
|
| 433 |
-
)
|
| 434 |
-
|
| 435 |
-
resp = make_response(jsonify({'access_token': new_access}))
|
| 436 |
-
add_cookie(resp, 'access_token', new_access, 900)
|
| 437 |
-
return resp
|
| 438 |
-
|
| 439 |
-
@app.post("/logout")
|
| 440 |
-
@token_required
|
| 441 |
-
def logout(username): # Use this username from decorator!
|
| 442 |
-
token = request.cookies.get('access_token')
|
| 443 |
-
if not token:
|
| 444 |
-
return jsonify({"message": "Invalid token format"}), 401
|
| 445 |
-
|
| 446 |
-
try:
|
| 447 |
-
conn = get_db_connection()
|
| 448 |
-
cur = conn.cursor()
|
| 449 |
-
|
| 450 |
-
# Add to blacklist
|
| 451 |
-
cur.execute("SELECT token FROM BlacklistedTokens WHERE token = ?", (token,))
|
| 452 |
-
if not cur.fetchone():
|
| 453 |
-
cur.execute("INSERT INTO BlacklistedTokens (token) VALUES (?", (token,))
|
| 454 |
-
|
| 455 |
-
# Delete refresh tokens (use username from decorator)
|
| 456 |
-
cur.execute("DELETE FROM RefreshTokens WHERE username = ?", (username,))
|
| 457 |
-
conn.commit()
|
| 458 |
-
conn.close()
|
| 459 |
-
|
| 460 |
-
# ADDED: Security logging for logout (ANONYMIZED)
|
| 461 |
-
user_hash = anonymize_username(username)
|
| 462 |
-
app.logger.info(f"User logged out: user_hash: {user_hash} from IP: {request.remote_addr}")
|
| 463 |
-
|
| 464 |
-
except Exception as e:
|
| 465 |
-
app.logger.exception("DB write error on logout: %s", e)
|
| 466 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 467 |
-
|
| 468 |
-
resp = make_response(jsonify({"message": "Logged out successfully!"}))
|
| 469 |
-
resp.delete_cookie('access_token', path='/')
|
| 470 |
-
resp.delete_cookie('refresh_token', path='/')
|
| 471 |
-
return resp
|
| 472 |
-
|
| 473 |
-
@app.get("/check-auth")
|
| 474 |
-
@token_required
|
| 475 |
-
def check_auth(username):
|
| 476 |
-
return jsonify({"message": "Authenticated", "username": username}), 200
|
| 477 |
-
|
| 478 |
-
@app.post("/signup")
|
| 479 |
-
def signup():
|
| 480 |
-
"""Register a new user"""
|
| 481 |
-
data = request.json or {}
|
| 482 |
-
username = data.get('username', '').strip()
|
| 483 |
-
password = data.get('password', '')
|
| 484 |
|
| 485 |
-
#
|
| 486 |
-
|
| 487 |
-
|
|
|
|
| 488 |
|
| 489 |
-
|
| 490 |
-
|
| 491 |
|
| 492 |
-
|
| 493 |
-
|
| 494 |
|
| 495 |
-
|
| 496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
| 509 |
-
|
| 510 |
-
# Insert new user
|
| 511 |
-
cur.execute(
|
| 512 |
-
"INSERT INTO Users (username, password_hash, role) VALUES (?, ?, ?)",
|
| 513 |
-
(username, password_hash.decode('utf-8'), 'user')
|
| 514 |
-
)
|
| 515 |
-
conn.commit()
|
| 516 |
-
conn.close()
|
| 517 |
-
|
| 518 |
-
# ADDED: Security logging for new user registration (ANONYMIZED)
|
| 519 |
-
user_hash = anonymize_username(username)
|
| 520 |
-
app.logger.info(f"New user registered: user_hash: {user_hash} from IP: {request.remote_addr}")
|
| 521 |
-
|
| 522 |
-
return jsonify({"message": "User registered successfully"}), 201
|
| 523 |
-
|
| 524 |
-
except pyodbc.IntegrityError:
|
| 525 |
-
return jsonify({"message": "Username already exists"}), 409
|
| 526 |
-
except Exception as e:
|
| 527 |
-
app.logger.exception("DB error on signup: %s", e)
|
| 528 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 529 |
-
|
| 530 |
-
@app.post("/admin/promote-user")
|
| 531 |
-
@token_required
|
| 532 |
-
def promote_user(username):
|
| 533 |
-
"""Promote a user to admin role - ADMIN ONLY"""
|
| 534 |
-
# Check if current user is admin
|
| 535 |
-
try:
|
| 536 |
-
conn = get_db_connection()
|
| 537 |
-
cur = conn.cursor()
|
| 538 |
-
cur.execute("SELECT role FROM Users WHERE username = ?", (username,))
|
| 539 |
-
row = cur.fetchone()
|
| 540 |
-
|
| 541 |
-
if not row or row[0] != 'admin':
|
| 542 |
-
conn.close()
|
| 543 |
-
user_hash = anonymize_username(username)
|
| 544 |
-
app.logger.warning(f"Unauthorized promote-user attempt by user_hash: {user_hash} from IP: {request.remote_addr}")
|
| 545 |
-
return jsonify({"message": "Unauthorized - Admin access required"}), 403
|
| 546 |
-
|
| 547 |
-
# Get target username from request
|
| 548 |
-
data = request.json or {}
|
| 549 |
-
target_user = data.get('username', '').strip().lower()
|
| 550 |
-
|
| 551 |
-
if not target_user:
|
| 552 |
-
conn.close()
|
| 553 |
-
return jsonify({"message": "Username is required"}), 400
|
| 554 |
-
|
| 555 |
-
# Check if target user exists
|
| 556 |
-
cur.execute("SELECT role FROM Users WHERE username = ?", (target_user,))
|
| 557 |
-
target_row = cur.fetchone()
|
| 558 |
-
|
| 559 |
-
if not target_row:
|
| 560 |
-
conn.close()
|
| 561 |
-
return jsonify({"message": "User not found"}), 404
|
| 562 |
-
|
| 563 |
-
if target_row[0] == 'admin':
|
| 564 |
-
conn.close()
|
| 565 |
-
return jsonify({"message": "User is already an admin"}), 400
|
| 566 |
-
|
| 567 |
-
# Promote user to admin
|
| 568 |
-
cur.execute("UPDATE Users SET role = 'admin' WHERE username = ?", (target_user,))
|
| 569 |
-
conn.commit()
|
| 570 |
-
conn.close()
|
| 571 |
-
|
| 572 |
-
# Security logging
|
| 573 |
-
admin_hash = anonymize_username(username)
|
| 574 |
-
target_hash = anonymize_username(target_user)
|
| 575 |
-
app.logger.info(f"Admin {admin_hash} promoted user {target_hash} to admin from IP: {request.remote_addr}")
|
| 576 |
-
|
| 577 |
-
return jsonify({"message": f"User {target_user} promoted to admin successfully"}), 200
|
| 578 |
-
|
| 579 |
-
except Exception as e:
|
| 580 |
-
app.logger.exception("DB error in promote-user: %s", e)
|
| 581 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 582 |
-
|
| 583 |
-
@app.get("/admin/users")
|
| 584 |
-
@token_required
|
| 585 |
-
def list_users(username):
|
| 586 |
-
"""List all users - ADMIN ONLY"""
|
| 587 |
-
# Check if current user is admin
|
| 588 |
-
try:
|
| 589 |
-
conn = get_db_connection()
|
| 590 |
-
cur = conn.cursor()
|
| 591 |
-
cur.execute("SELECT role FROM Users WHERE username = ?", (username,))
|
| 592 |
-
row = cur.fetchone()
|
| 593 |
-
|
| 594 |
-
if not row or row[0] != 'admin':
|
| 595 |
-
conn.close()
|
| 596 |
-
user_hash = anonymize_username(username)
|
| 597 |
-
app.logger.warning(f"Unauthorized list-users attempt by user_hash: {user_hash} from IP: {request.remote_addr}")
|
| 598 |
-
return jsonify({"message": "Unauthorized - Admin access required"}), 403
|
| 599 |
-
|
| 600 |
-
# Get all users
|
| 601 |
-
cur.execute("SELECT id, username, role FROM Users ORDER BY id")
|
| 602 |
-
users = []
|
| 603 |
-
for row in cur.fetchall():
|
| 604 |
-
users.append({
|
| 605 |
-
"id": row[0],
|
| 606 |
-
"username": row[1],
|
| 607 |
-
"role": row[2]
|
| 608 |
-
})
|
| 609 |
-
|
| 610 |
-
conn.close()
|
| 611 |
-
|
| 612 |
-
# Security logging
|
| 613 |
-
admin_hash = anonymize_username(username)
|
| 614 |
-
app.logger.info(f"Admin {admin_hash} viewed user list from IP: {request.remote_addr}")
|
| 615 |
-
|
| 616 |
-
return jsonify({"users": users, "total": len(users)}), 200
|
| 617 |
-
|
| 618 |
-
except Exception as e:
|
| 619 |
-
app.logger.exception("DB error in list-users: %s", e)
|
| 620 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 621 |
-
|
| 622 |
-
@app.post("/admin/create-first-admin")
|
| 623 |
-
def create_first_admin():
|
| 624 |
-
"""Create the first admin user - ONLY if no users exist"""
|
| 625 |
-
try:
|
| 626 |
-
conn = get_db_connection()
|
| 627 |
-
cur = conn.cursor()
|
| 628 |
-
|
| 629 |
-
# Check if any users exist
|
| 630 |
-
cur.execute("SELECT COUNT(*) FROM Users")
|
| 631 |
-
user_count = cur.fetchone()[0]
|
| 632 |
-
|
| 633 |
-
if user_count > 0:
|
| 634 |
-
conn.close()
|
| 635 |
-
return jsonify({"message": "Users already exist. Cannot create first admin."}), 409
|
| 636 |
-
|
| 637 |
-
# Create first admin user
|
| 638 |
-
username = "admin"
|
| 639 |
-
password = "admin123" # Change this!
|
| 640 |
-
|
| 641 |
-
# Hash password
|
| 642 |
-
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
| 643 |
-
|
| 644 |
-
# Insert admin user
|
| 645 |
-
cur.execute(
|
| 646 |
-
"INSERT INTO Users (username, password_hash, role) VALUES (?, ?, ?)",
|
| 647 |
-
(username, password_hash.decode('utf-8'), 'admin')
|
| 648 |
-
)
|
| 649 |
-
conn.commit()
|
| 650 |
-
conn.close()
|
| 651 |
-
|
| 652 |
-
# Security logging
|
| 653 |
-
app.logger.info(f"First admin user created from IP: {request.remote_addr}")
|
| 654 |
-
|
| 655 |
-
return jsonify({
|
| 656 |
-
"message": "First admin user created successfully",
|
| 657 |
-
"username": "admin",
|
| 658 |
-
"password": "admin123",
|
| 659 |
-
"warning": "CHANGE THE PASSWORD IMMEDIATELY!"
|
| 660 |
-
}), 201
|
| 661 |
-
|
| 662 |
-
except Exception as e:
|
| 663 |
-
app.logger.exception("DB error creating first admin: %s", e)
|
| 664 |
-
return jsonify({"message": "Database is unavailable"}), 503
|
| 665 |
-
# ----------------------------------------------------------------------------
|
| 666 |
-
# Register Blueprint: grammar (and later media) lives in other files
|
| 667 |
-
# ----------------------------------------------------------------------------
|
| 668 |
-
try:
|
| 669 |
-
from chat import movie_bp
|
| 670 |
-
app.register_blueprint(movie_bp, url_prefix="/media")
|
| 671 |
-
print("✅ Registered movie_bp")
|
| 672 |
-
except ImportError as e:
|
| 673 |
-
print(f"⚠️ Could not import movie_bp: {e}")
|
| 674 |
-
|
| 675 |
-
try:
|
| 676 |
-
from ragg.app import rag_bp
|
| 677 |
-
app.register_blueprint(rag_bp, url_prefix="/rag")
|
| 678 |
-
print("✅ Registered rag_bp")
|
| 679 |
-
except ImportError as e:
|
| 680 |
-
print(f"⚠️ Could not import rag_bp: {e}")
|
| 681 |
-
|
| 682 |
-
try:
|
| 683 |
-
from pron import pron_bp
|
| 684 |
-
app.register_blueprint(pron_bp, url_prefix="/pron")
|
| 685 |
-
print("✅ Registered pron_bp")
|
| 686 |
-
except ImportError as e:
|
| 687 |
-
print(f"⚠️ Could not import pron_bp: {e}")
|
| 688 |
|
| 689 |
-
try:
|
| 690 |
-
from pronvideo import pronvideo_bp
|
| 691 |
-
app.register_blueprint(pronvideo_bp, url_prefix="/pronvideo")
|
| 692 |
-
print("✅ Registered pronvideo_bp")
|
| 693 |
-
except ImportError as e:
|
| 694 |
-
print(f"⚠️ Could not import pronvideo_bp: {e}")
|
| 695 |
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
app.register_blueprint(pronragg_bp, url_prefix="/pronragg")
|
| 699 |
-
print("✅ Registered pronragg_bp")
|
| 700 |
-
except ImportError as e:
|
| 701 |
-
print(f"⚠️ Could not import pronragg_bp: {e}")
|
| 702 |
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
app.register_blueprint(pronragupgrade_bp, url_prefix="/pronragupgrade")
|
| 706 |
-
print("✅ Registered pronragupgrade_bp")
|
| 707 |
-
except ImportError as e:
|
| 708 |
-
print(f"⚠️ Could not import pronragupgrade_bp: {e}")
|
| 709 |
|
| 710 |
-
try:
|
| 711 |
-
from ragg.ingest_trigger import ingest_trigger_bp
|
| 712 |
-
app.register_blueprint(ingest_trigger_bp, url_prefix="/rag")
|
| 713 |
-
print("✅ Registered ingest_trigger_bp")
|
| 714 |
-
except ImportError as e:
|
| 715 |
-
print(f"⚠️ Could not import ingest_trigger_bp: {e}")
|
| 716 |
|
| 717 |
-
# ----------------------------------------------------------------------------
|
| 718 |
-
# Local run (Gunicorn will import `verification:app` on Spaces)
|
| 719 |
-
# ----------------------------------------------------------------------------
|
| 720 |
if __name__ == '__main__':
|
| 721 |
-
print("
|
| 722 |
-
print(f"
|
| 723 |
-
print(f"
|
| 724 |
-
print(
|
| 725 |
-
print("=" * 50)
|
| 726 |
|
| 727 |
port = int(os.getenv("PORT", "5000"))
|
| 728 |
-
print(f"
|
| 729 |
-
print("
|
| 730 |
-
print(" GET /
|
| 731 |
-
print("
|
| 732 |
-
print(" POST /
|
| 733 |
-
print(" POST /
|
| 734 |
-
print(" POST /
|
| 735 |
-
print("
|
| 736 |
-
print(" GET /
|
| 737 |
-
print(" GET /
|
| 738 |
-
print("
|
| 739 |
-
print("
|
| 740 |
-
print("
|
| 741 |
-
print(" POST /admin/
|
| 742 |
-
print("
|
|
|
|
| 743 |
|
| 744 |
try:
|
| 745 |
app.run(host="0.0.0.0", port=port, debug=True)
|
| 746 |
except Exception as e:
|
| 747 |
-
print(f"
|
| 748 |
-
raise
|
| 749 |
-
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MJ Learn Backend - Main Flask Application
|
| 3 |
+
|
| 4 |
+
A clean, professional Flask application with modular authentication.
|
| 5 |
+
|
| 6 |
+
Main Features:
|
| 7 |
+
- JWT-based authentication system
|
| 8 |
+
- Role-based access control (admin/user)
|
| 9 |
+
- Secure token management with blacklisting
|
| 10 |
+
- CORS configuration for cross-origin requests
|
| 11 |
+
- Modular blueprint architecture
|
| 12 |
+
- Environment-based configuration
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
import os
|
| 16 |
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
import logging
|
| 18 |
+
from flask import Flask, request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
from flask_cors import CORS
|
| 20 |
|
| 21 |
+
# Load environment variables first
|
| 22 |
+
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
| 23 |
+
load_dotenv(os.path.join(BASEDIR, ".env"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
# Import authentication module
|
| 26 |
+
from auth import auth_bp
|
| 27 |
+
from auth.database import ensure_database_initialized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
|
|
|
| 29 |
|
| 30 |
+
def create_app():
|
| 31 |
+
"""Application factory pattern for Flask app creation"""
|
| 32 |
+
app = Flask(__name__)
|
| 33 |
|
| 34 |
+
# Security configuration
|
| 35 |
+
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
|
| 36 |
+
if not app.config['SECRET_KEY']:
|
| 37 |
+
raise RuntimeError("SECRET_KEY must be set in environment variables!")
|
| 38 |
+
|
| 39 |
+
# Environment configuration
|
| 40 |
+
IS_PROD = os.getenv("ENV", "dev").lower() == "prod"
|
| 41 |
+
|
| 42 |
+
# CORS configuration
|
| 43 |
+
_default_origins = "http://localhost:4200,http://127.0.0.1:4200"
|
| 44 |
+
_origins = os.getenv("ALLOWED_ORIGINS", _default_origins)
|
| 45 |
+
ALLOWED_ORIGINS = [o.strip() for o in _origins.split(",") if o.strip()]
|
| 46 |
+
|
| 47 |
+
CORS(
|
| 48 |
+
app,
|
| 49 |
+
resources={r"/*": {"origins": ALLOWED_ORIGINS}},
|
| 50 |
+
supports_credentials=True,
|
| 51 |
+
allow_headers=["Content-Type", "Authorization", "X-Requested-With", "X-User"],
|
| 52 |
+
expose_headers=["Set-Cookie"],
|
| 53 |
+
methods=["GET", "POST", "OPTIONS"]
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# API configuration for blueprints
|
| 57 |
+
app.config["COHERE_API_KEY"] = os.getenv("COHERE_API_KEY", "")
|
| 58 |
+
|
| 59 |
+
# CORS handlers
|
| 60 |
+
@app.after_request
|
| 61 |
+
def add_cors_headers(resp):
|
| 62 |
origin = request.headers.get("Origin")
|
| 63 |
if origin and origin in ALLOWED_ORIGINS:
|
| 64 |
resp.headers["Access-Control-Allow-Origin"] = origin
|
| 65 |
+
resp.headers["Vary"] = "Origin"
|
| 66 |
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
| 67 |
+
resp.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Requested-With, X-User"
|
| 68 |
+
resp.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
|
|
|
|
|
|
|
|
|
| 69 |
return resp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
@app.before_request
|
| 72 |
+
def handle_options_early():
|
| 73 |
+
if request.method == "OPTIONS":
|
| 74 |
+
resp = app.make_default_options_response()
|
| 75 |
+
origin = request.headers.get("Origin")
|
| 76 |
+
if origin and origin in ALLOWED_ORIGINS:
|
| 77 |
+
resp.headers["Access-Control-Allow-Origin"] = origin
|
| 78 |
+
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
| 79 |
+
# Mirror browser's requested headers/methods
|
| 80 |
+
req_headers = request.headers.get("Access-Control-Request-Headers", "Content-Type, Authorization, X-Requested-With, X-User")
|
| 81 |
+
req_method = request.headers.get("Access-Control-Request-Method", "POST")
|
| 82 |
+
resp.headers["Access-Control-Allow-Headers"] = req_headers
|
| 83 |
+
resp.headers["Access-Control-Allow-Methods"] = req_method
|
| 84 |
+
return resp
|
| 85 |
|
| 86 |
+
# Initialize database before first request (Flask 3.x compatible)
|
| 87 |
+
@app.before_request
|
| 88 |
+
def maybe_initialize_database():
|
| 89 |
+
if not hasattr(app, '_db_initialized'):
|
| 90 |
+
try:
|
| 91 |
+
ensure_database_initialized()
|
| 92 |
+
app._db_initialized = True
|
| 93 |
+
except Exception as e:
|
| 94 |
+
app.logger.exception("Database initialization failed: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
# Health check endpoint
|
| 97 |
+
@app.route("/")
|
| 98 |
+
def health():
|
| 99 |
+
return {"status": "ok", "service": "MJ Learn Backend"}, 200
|
| 100 |
|
| 101 |
+
# Register authentication blueprint
|
| 102 |
+
app.register_blueprint(auth_bp, url_prefix="/auth")
|
| 103 |
|
| 104 |
+
# Register other feature blueprints
|
| 105 |
+
register_feature_blueprints(app)
|
| 106 |
|
| 107 |
+
return app
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def register_feature_blueprints(app):
|
| 111 |
+
"""Register feature blueprints with error handling"""
|
| 112 |
+
blueprints = [
|
| 113 |
+
("chat", "movie_bp", "/media"),
|
| 114 |
+
("ragg.app", "rag_bp", "/rag"),
|
| 115 |
+
("pron", "pron_bp", "/pron"),
|
| 116 |
+
("pronvideo", "pronvideo_bp", "/pronvideo"),
|
| 117 |
+
("pronragg", "pronragg_bp", "/pronragg"),
|
| 118 |
+
("pronragupgrade", "pronragupgrade_bp", "/pronragupgrade"),
|
| 119 |
+
("ragg.ingest_trigger", "ingest_trigger_bp", "/rag"),
|
| 120 |
+
]
|
| 121 |
|
| 122 |
+
for module_name, blueprint_name, url_prefix in blueprints:
|
| 123 |
+
try:
|
| 124 |
+
module = __import__(module_name, fromlist=[blueprint_name])
|
| 125 |
+
blueprint = getattr(module, blueprint_name)
|
| 126 |
+
app.register_blueprint(blueprint, url_prefix=url_prefix)
|
| 127 |
+
print(f"? Registered {blueprint_name}")
|
| 128 |
+
except ImportError as e:
|
| 129 |
+
print(f"?? Could not import {blueprint_name}: {e}")
|
| 130 |
+
except AttributeError as e:
|
| 131 |
+
print(f"?? Blueprint {blueprint_name} not found in {module_name}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
# Create Flask app instance
|
| 135 |
+
app = create_app()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
+
# Configure logging
|
| 138 |
+
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
|
|
|
|
|
|
|
|
|
| 141 |
if __name__ == '__main__':
|
| 142 |
+
print("?? Starting MJ Learn Backend...")
|
| 143 |
+
print(f"? SECRET_KEY loaded: {bool(app.config.get('SECRET_KEY'))}")
|
| 144 |
+
print(f"? Environment: {os.getenv('ENV', 'development')}")
|
| 145 |
+
print("=" * 60)
|
|
|
|
| 146 |
|
| 147 |
port = int(os.getenv("PORT", "5000"))
|
| 148 |
+
print(f"?? Server starting on http://localhost:{port}")
|
| 149 |
+
print("?? Available endpoints:")
|
| 150 |
+
print(" GET / - Health check")
|
| 151 |
+
print(" ?? Authentication:")
|
| 152 |
+
print(" POST /auth/signup - User registration")
|
| 153 |
+
print(" POST /auth/login - User login")
|
| 154 |
+
print(" POST /auth/refresh - Token refresh")
|
| 155 |
+
print(" POST /auth/logout - User logout")
|
| 156 |
+
print(" GET /auth/dashboard - Protected endpoint")
|
| 157 |
+
print(" GET /auth/check-auth - Auth status check")
|
| 158 |
+
print(" GET /auth/db/diag - Database diagnostics (ADMIN)")
|
| 159 |
+
print(" ?? Admin Management:")
|
| 160 |
+
print(" GET /auth/admin/users - List all users (ADMIN)")
|
| 161 |
+
print(" POST /auth/admin/promote-user - Promote user to admin (ADMIN)")
|
| 162 |
+
print(" POST /auth/admin/create-first-admin - Create first admin")
|
| 163 |
+
print("=" * 60)
|
| 164 |
|
| 165 |
try:
|
| 166 |
app.run(host="0.0.0.0", port=port, debug=True)
|
| 167 |
except Exception as e:
|
| 168 |
+
print(f"? Failed to start server: {e}")
|
| 169 |
+
raise
|
|
|
AUTHENTICATION_FIXES_REQUIRED.md → verification_new.py
RENAMED
|
File without changes
|