| import sqlite3 |
| import hashlib |
| import re |
|
|
| DATABASE_FILE = "cbt.db" |
|
|
| def get_db_connection(): |
| """Establishes connection to the sqlite3 database file.""" |
| conn = sqlite3.connect(DATABASE_FILE) |
| conn.row_factory = sqlite3.Row |
| return conn |
|
|
| def init_db(): |
| """Creates the database tables if they do not already exist.""" |
| conn = get_db_connection() |
| cursor = conn.cursor() |
| |
| |
| cursor.execute("PRAGMA journal_mode=WAL;") |
| |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS users ( |
| id TEXT PRIMARY KEY, |
| full_name TEXT NOT NULL, |
| role TEXT NOT NULL CHECK(role IN ('student', 'teacher')), |
| class_or_subject TEXT NOT NULL, |
| pin_hash TEXT NOT NULL, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ); |
| """) |
| |
| conn.commit() |
| conn.close() |
| print("Database successfully initialized or verified!") |
|
|
| def hash_pin(pin: str) -> str: |
| """Hashes a numeric PIN securely using SHA-256.""" |
| return hashlib.sha256(pin.strip().encode()).hexdigest() |
|
|
| def clean_input(text: str) -> str: |
| """Cleans names and subjects of non-alphanumeric characters.""" |
| return re.sub(r'[^a-zA-Z0-9]', '', text).strip() |
|
|
| def get_next_sequence(prefix: str) -> str: |
| """Queries SQLite to find the next sequence number for a prefix and returns pad-3 string (e.g., '002').""" |
| conn = get_db_connection() |
| cursor = conn.cursor() |
| |
| cursor.execute("SELECT id FROM users WHERE id LIKE ? ORDER BY id DESC", (f"{prefix}%",)) |
| rows = cursor.fetchall() |
| conn.close() |
| |
| max_num = 0 |
| for row in rows: |
| user_id = row['id'] |
| parts = user_id.split('-') |
| if len(parts) >= 4: |
| try: |
| num = int(parts[3]) |
| if num > max_num: |
| max_num = num |
| except ValueError: |
| continue |
| |
| next_num = max_num + 1 |
| return f"{next_num:03d}" |
|
|
| def calculate_student_id(full_name: str, class_name: str) -> str: |
| """ |
| Generates ID in format: STU-SS1-ADM-001 (Role-Class-name-Number) |
| """ |
| |
| clean_class = class_name.upper().replace(" ", "") |
| if not clean_class: |
| clean_class = "SS1" |
| |
| |
| parts = full_name.strip().split() |
| first_part = clean_input(parts[0]).upper() if parts else "STU" |
| if len(first_part) >= 3: |
| name_code = first_part[:3] |
| else: |
| name_code = (first_part + "XXX")[:3] |
| |
| |
| prefix_base = f"STU-{clean_class}-{name_code}" |
| next_num = get_next_sequence(prefix_base) |
| |
| return f"{prefix_base}-{next_num}" |
|
|
| def calculate_teacher_id(full_name: str, subject_name: str) -> str: |
| """ |
| Generates ID in format: TCH-MTH-BI-001 (Role-Subject-name-Number) |
| """ |
| SUBJECT_CODES = { |
| "MATHEMATICS": "MTH", |
| "ENGLISH": "ENG", |
| "PHYSICS": "PHY", |
| "CHEMISTRY": "CHM", |
| "BIOLOGY": "BIO", |
| "ICT": "ICT" |
| } |
| |
| |
| clean_sub = clean_input(subject_name).upper() |
| sub_code = SUBJECT_CODES.get(clean_sub, clean_sub[:3]) |
| if len(sub_code) < 3: |
| sub_code = (sub_code + "XXX")[:3] |
| |
| |
| parts = [clean_input(p) for p in full_name.strip().split() if clean_input(p)] |
| if len(parts) >= 2: |
| name_code = (parts[0][0] + parts[1][0]).upper() |
| elif len(parts) == 1: |
| name_code = parts[0][:2].upper().ljust(2, "X") |
| else: |
| name_code = "XX" |
| |
| |
| prefix_base = f"TCH-{sub_code}-{name_code}" |
| next_num = get_next_sequence(prefix_base) |
| |
| return f"{prefix_base}-{next_num}" |
|
|
| def register_user(full_name: str, role: str, class_or_subject: str, pin: str, custom_id=None) -> str: |
| """ |
| Registers a new student or teacher. Hash PIN and saves securely. |
| Returns the assigned User ID. |
| """ |
| |
| if role.strip().lower() == 'student': |
| user_id = calculate_student_id(full_name, class_or_subject) |
| else: |
| user_id = calculate_teacher_id(full_name, class_or_subject) |
| |
| |
| |
| if custom_id and len(custom_id.split('-')) == 4: |
| user_id = custom_id.strip().upper() |
|
|
| conn = get_db_connection() |
| cursor = conn.cursor() |
| |
| |
| cursor.execute("SELECT id FROM users WHERE id = ?", (user_id,)) |
| if cursor.fetchone() is not None: |
| conn.close() |
| raise ValueError(f"User ID {user_id} already exists! Try again.") |
| |
| |
| cursor.execute( |
| "INSERT INTO users (id, full_name, role, class_or_subject, pin_hash) VALUES (?, ?, ?, ?, ?)", |
| (user_id, full_name.strip(), role.strip().lower(), class_or_subject.strip(), hash_pin(pin)) |
| ) |
| conn.commit() |
| conn.close() |
| return user_id |
|
|
| def authenticate_user(user_id: str, pin: str): |
| """ |
| Verifies user authentication against secure PIN hashes. |
| Returns the user datatable dictionary if successful, or None. |
| """ |
| clean_id = user_id.strip().upper() |
| conn = get_db_connection() |
| cursor = conn.cursor() |
| |
| cursor.execute("SELECT * FROM users WHERE id = ?", (clean_id,)) |
| user = cursor.fetchone() |
| conn.close() |
| |
| if user: |
| if user['pin_hash'] == hash_pin(pin): |
| return dict(user) |
| |
| return None |
|
|