Oviya commited on
Commit
b5ca3a6
·
1 Parent(s): 10f206e
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
- pysqlite3-binary==0.5.3.post1
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
- # --- load .env FIRST ---
 
 
 
 
 
 
 
 
 
 
 
 
 
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 threading import Lock
12
- from functools import wraps
13
- import datetime
14
- import bcrypt
15
- import jwt
16
- import pyodbc
17
- from flask import Flask, request, jsonify, make_response, current_app
18
  from flask_cors import CORS
19
 
20
- # ------------------------------------------------------------------------------
21
- # Security Helper Functions
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
- # 3) JWT cookie from verification.py
70
- token = req.cookies.get("access_token")
71
- if token:
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
- @app.after_request
84
- def add_cors_headers(resp):
85
- origin = request.headers.get("Origin")
86
- if origin and origin in ALLOWED_ORIGINS:
87
- # echo the origin, never '*', when using credentials
88
- resp.headers["Access-Control-Allow-Origin"] = origin
89
- resp.headers["Vary"] = "Origin"
90
- resp.headers["Access-Control-Allow-Credentials"] = "true"
91
- resp.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Requested-With, X-User"
92
- resp.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
93
- return resp
94
-
95
-
96
- @app.before_request
97
- def handle_options_early():
98
- if request.method == "OPTIONS":
99
- resp = app.make_default_options_response()
 
 
 
 
 
 
 
 
 
 
 
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
- # Mirror browser's requested headers/methods
105
- req_headers = request.headers.get("Access-Control-Request-Headers", "Content-Type, Authorization, X-Requested-With, X-User")
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
- if len(username) < 3 or len(username) > 50:
343
- return jsonify({"message": "Invalid username format"}), 400
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
- # ADDED: Normalize username to prevent case sensitivity issues
346
- username = username.lower()
347
-
348
- try:
349
- conn = get_db_connection()
350
- cur = conn.cursor()
351
- cur.execute("SELECT password_hash FROM Users WHERE username = ?", (username,))
352
- row = cur.fetchone()
353
- conn.close()
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
- # INPUT VALIDATION
486
- if not username or not password:
487
- return jsonify({"message": "Username and password are required"}), 400
 
488
 
489
- if len(username) < 3 or len(username) > 50:
490
- return jsonify({"message": "Username must be 3-50 characters"}), 400
491
 
492
- if len(password) < 8:
493
- return jsonify({"message": "Password must be at least 8 characters"}), 400
494
 
495
- # Normalize username (prevent duplicates like "Admin" and "admin")
496
- username = username.lower()
 
 
 
 
 
 
 
 
 
 
 
 
497
 
498
- # Check if username already exists
499
- try:
500
- conn = get_db_connection()
501
- cur = conn.cursor()
502
- cur.execute("SELECT username FROM Users WHERE username = ?", (username,))
503
- if cur.fetchone():
504
- conn.close()
505
- return jsonify({"message": "Username already exists"}), 409
506
-
507
- # Hash password
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
- try:
697
- from pronragg import pronragg_bp
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
- try:
704
- from pronragupgrade import pronragupgrade_bp
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("🚀 Starting Flask Authentication Server...")
722
- print(f" SECRET_KEY loaded: {bool(app.config.get('SECRET_KEY'))}")
723
- print(f" Database: {DB_DATABASE} on {DB_SERVER}")
724
- print(f" CORS Origins: {ALLOWED_ORIGINS}")
725
- print("=" * 50)
726
 
727
  port = int(os.getenv("PORT", "5000"))
728
- print(f"🌐 Server starting on http://localhost:{port}")
729
- print("📍 Available endpoints:")
730
- print(" GET / - Health check")
731
- print(" POST /signup - User registration")
732
- print(" POST /login - User login")
733
- print(" POST /refresh - Token refresh")
734
- print(" POST /logout - User logout")
735
- print(" GET /dashboard - Protected endpoint")
736
- print(" GET /check-auth - Auth status check")
737
- print(" GET /db/diag - Database diagnostics (ADMIN ONLY)")
738
- print(" 📋 Admin Management:")
739
- print(" GET /admin/users - List all users (ADMIN ONLY)")
740
- print(" POST /admin/promote-user - Promote user to admin (ADMIN ONLY)")
741
- print(" POST /admin/create-first-admin - Create first admin (if no users exist)")
742
- print("=" * 50)
 
743
 
744
  try:
745
  app.run(host="0.0.0.0", port=port, debug=True)
746
  except Exception as e:
747
- print(f" Failed to start server: {e}")
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