diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md new file mode 100644 index 0000000..0cd0510 --- /dev/null +++ b/AUTHENTICATION.md @@ -0,0 +1,201 @@ +# 🔐 Authentication Guide + +This guide explains how to use the authentication system in the Student Success Predictor application. + +## 🚀 Quick Start + +### Default Credentials + +The application comes with two pre-configured demo accounts: + +**Administrator Account:** +- Username: `admin` +- Password: `admin123` +- Role: Admin (full access + user management) + +**Educator Account:** +- Username: `educator` +- Password: `hello123` +- Role: Educator (prediction access + export features) + +### First Login + +1. Run the application: `streamlit run app.py` +2. You'll see the login screen with demo credentials displayed +3. Use either account above to log in +4. **Important:** Change default passwords in production! + +## 👥 User Roles & Permissions + +### 🏷️ Role Hierarchy + +1. **Admin** - Full access to everything + - Use prediction features + - Export prediction reports + - Manage users (add/delete) + - Access admin panel + +2. **Educator** - Educational staff access + - Use prediction features + - Export prediction reports + - Cannot manage users + +3. **User** - Basic access (for future use) + - Use prediction features only + - No export capabilities + - No admin functions + +## 🛠️ User Management (Admin Only) + +### Adding New Users + +1. Login as admin +2. In the sidebar, expand "👨‍💼 Admin Panel" +3. Fill out the "Add New User" form: + - Username (unique identifier) + - Password (secure password) + - Full Name (display name) + - Role (user/educator/admin) +4. Click "➕ Add User" + +### Managing Existing Users + +- View all users in the "Existing Users" section +- Delete users with the 🗑️ button (except yourself) +- Users are stored in `users.json` file + +## 🔒 Security Features + +### Password Hashing +- All passwords are hashed using SHA-256 +- Plain text passwords are never stored +- Secure credential verification + +### Session Management +- Automatic session timeout (30 minutes of inactivity) +- Secure session state management +- Proper logout functionality + +### Role-Based Access Control +- Different features available based on user role +- Hierarchical permission system +- Secure admin-only functions + +## 📁 Data Storage + +### User Data File +- Users stored in: `users.json` +- Contains hashed passwords only +- Includes user roles and names +- Automatically created on first run + +### Session Storage +- Uses Streamlit's built-in session state +- No persistent session storage +- Secure temporary authentication + +## 🔧 Customization + +### Adding New Roles + +Edit `src/utils/auth.py` and modify: + +```python +role_hierarchy = { + "admin": 3, + "educator": 2, + "student": 1, # Add new role here + "user": 0 +} +``` + +### Changing Session Timeout + +In `app.py`, modify the timeout value: + +```python +check_session_timeout(60) # 60 minutes instead of 30 +``` + +### Custom Password Requirements + +Add validation in `src/utils/auth.py`: + +```python +def validate_password(password: str) -> bool: + # Add your password requirements here + return len(password) >= 8 and any(c.isdigit() for c in password) +``` + +## 🚨 Production Security + +### Important Security Steps for Production: + +1. **Change Default Passwords** + ```python + # Delete users.json and create new admin account + ``` + +2. **Use Environment Variables** + ```python + import os + ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'secure_password') + ``` + +3. **Implement Database Storage** + - Replace JSON file with proper database + - Use encrypted connection strings + - Implement proper backup procedures + +4. **Add Password Complexity Requirements** + - Minimum length requirements + - Special character requirements + - Password expiration policies + +5. **Implement Rate Limiting** + - Prevent brute force attacks + - Temporary account lockouts + - Login attempt logging + +6. **Enable HTTPS** + - Secure data transmission + - SSL/TLS certificates + - Secure cookie settings + +## 🐛 Troubleshooting + +### Login Issues + +**Problem:** "Invalid username or password" +- **Solution:** Check credentials carefully, case-sensitive + +**Problem:** Session expired message +- **Solution:** Normal after 30 minutes, just log in again + +**Problem:** Cannot access admin features +- **Solution:** Ensure you're logged in with admin role + +### File Permission Issues + +**Problem:** Cannot create users.json +- **Solution:** Check write permissions in app directory + +**Problem:** Cannot save new users +- **Solution:** Ensure sufficient disk space and permissions + +## 📝 Development Notes + +### File Structure +``` +src/utils/auth.py # Main authentication module +users.json # User credentials storage (auto-created) +app.py # Main app with auth integration +``` + +### Key Functions +- `require_auth()` - Decorator for protected functions +- `check_role_permission()` - Role-based access control +- `render_admin_panel()` - User management interface +- `check_session_timeout()` - Session timeout handling + +This authentication system provides a secure, user-friendly way to control access to your Student Success Predictor application! \ No newline at end of file diff --git a/app.py b/app.py index 652baf5..481b265 100644 --- a/app.py +++ b/app.py @@ -14,6 +14,7 @@ sys.path.append(str(src_path)) from src.config.theme import ROSE_PINE_THEME, PAGE_CONFIG, APP_TITLE, APP_SUBTITLE from src.config.mappings import create_feature_mappings from src.utils.model_utils import load_model_and_info +from src.utils.auth import require_auth, check_role_permission, render_admin_panel, check_session_timeout from src.components.form_components import ( render_preset_selector, render_personal_info_section, @@ -77,10 +78,11 @@ def collect_user_input(): return user_input +@require_auth def main(): """Main Streamlit application""" - # Configure page - st.set_page_config(**PAGE_CONFIG) + # Check session timeout (30 minutes) + check_session_timeout(30) # Apply theme apply_theme() @@ -88,9 +90,14 @@ def main(): # Load model and preprocessing info model, preprocessing_info, feature_names = load_model_and_info() - # Render header + # Render header with user context render_header() + # Add admin panel if user is admin + if check_role_permission("admin"): + with st.sidebar: + render_admin_panel() + # Collect user input user_input = collect_user_input() @@ -100,6 +107,10 @@ def main(): with col1: st.markdown('

Prediction Results

', unsafe_allow_html=True) + # Add role-based access message + if check_role_permission("educator"): + st.markdown('
🎓 Educator Access: You have full access to prediction features and student data analysis.
', unsafe_allow_html=True) + # Render prediction section render_prediction_section(model, preprocessing_info, user_input) @@ -107,11 +118,16 @@ def main(): # Render model info and validation render_model_info_sidebar(user_input) - # Render export section - render_export_section(user_input) + # Render export section (role-based access) + if check_role_permission("educator"): + render_export_section(user_input) + else: + st.markdown('
📊 Export Feature: Available for educators and administrators only.
', unsafe_allow_html=True) # Render footer render_footer() if __name__ == "__main__": + # Configure page before authentication + st.set_page_config(**PAGE_CONFIG) main() \ No newline at end of file diff --git a/src/utils/auth.py b/src/utils/auth.py new file mode 100644 index 0000000..5386b61 --- /dev/null +++ b/src/utils/auth.py @@ -0,0 +1,300 @@ +""" +Authentication module for Student Success Predictor +Provides secure login/logout functionality with session management +""" + +import streamlit as st +import hashlib +import json +import os +from typing import Dict, Optional, Tuple +from pathlib import Path + +# Path to store user credentials (in production, use a proper database) +USERS_FILE = Path("users.json") + +# Default admin user (change password in production!) +DEFAULT_USERS = { + "admin": { + "password_hash": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9", # "admin123" + "role": "admin", + "name": "Administrator" + }, + "educator": { + "password_hash": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", # "hello123" + "role": "educator", + "name": "Educator" + } +} + +def hash_password(password: str) -> str: + """Hash a password using SHA-256""" + return hashlib.sha256(password.encode()).hexdigest() + +def load_users() -> Dict: + """Load users from file or create default users""" + if USERS_FILE.exists(): + try: + with open(USERS_FILE, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + pass + + # Create default users file + save_users(DEFAULT_USERS) + return DEFAULT_USERS + +def save_users(users: Dict) -> None: + """Save users to file""" + with open(USERS_FILE, 'w') as f: + json.dump(users, f, indent=2) + +def verify_credentials(username: str, password: str) -> Tuple[bool, Optional[Dict]]: + """Verify user credentials""" + users = load_users() + + if username in users: + stored_hash = users[username]["password_hash"] + input_hash = hash_password(password) + + if stored_hash == input_hash: + return True, users[username] + + return False, None + +def init_session_state(): + """Initialize session state for authentication""" + if 'authenticated' not in st.session_state: + st.session_state.authenticated = False + if 'username' not in st.session_state: + st.session_state.username = None + if 'user_role' not in st.session_state: + st.session_state.user_role = None + if 'user_name' not in st.session_state: + st.session_state.user_name = None + +def login_form(): + """Render login form with Rose Pine styling""" + st.markdown(""" + + """, unsafe_allow_html=True) + + with st.container(): + st.markdown('
', unsafe_allow_html=True) + + st.markdown('

🔐 Student Success Predictor

', unsafe_allow_html=True) + + # Demo credentials info + st.markdown(""" +
+ Demo Credentials:
+ 👨‍💼 Admin: admin / admin123
+ 👩‍🏫 Educator: educator / hello123 +
+ """, unsafe_allow_html=True) + + with st.form("login_form"): + username = st.text_input("Username", placeholder="Enter your username") + password = st.text_input("Password", type="password", placeholder="Enter your password") + + col1, col2, col3 = st.columns([1, 2, 1]) + with col2: + login_button = st.form_submit_button("🚀 Login", use_container_width=True) + + if login_button: + if username and password: + is_valid, user_info = verify_credentials(username, password) + + if is_valid: + st.session_state.authenticated = True + st.session_state.username = username + st.session_state.user_role = user_info["role"] + st.session_state.user_name = user_info["name"] + st.success(f"Welcome back, {user_info['name']}!") + st.rerun() + else: + st.error("❌ Invalid username or password") + else: + st.warning("⚠️ Please enter both username and password") + + st.markdown('
', unsafe_allow_html=True) + +def logout(): + """Logout user and clear session""" + st.session_state.authenticated = False + st.session_state.username = None + st.session_state.user_role = None + st.session_state.user_name = None + st.rerun() + +def render_user_info(): + """Render user information in sidebar""" + if st.session_state.authenticated: + with st.sidebar: + st.markdown("---") + st.markdown("### 👤 User Information") + + # User info card + st.markdown(f""" +
+ 👋 {st.session_state.user_name}
+ Role: {st.session_state.user_role.title()}
+ User: @{st.session_state.username} +
+ """, unsafe_allow_html=True) + + # Logout button + if st.button("🚪 Logout", type="secondary", use_container_width=True): + logout() + +def require_auth(func): + """Decorator to require authentication for functions""" + def wrapper(*args, **kwargs): + init_session_state() + + if not st.session_state.authenticated: + login_form() + return None + + # Add user info to sidebar + render_user_info() + + return func(*args, **kwargs) + + return wrapper + +def check_role_permission(required_role: str = None) -> bool: + """Check if current user has required role""" + if not st.session_state.authenticated: + return False + + if required_role is None: + return True + + user_role = st.session_state.user_role + + # Define role hierarchy (admin > educator > user) + role_hierarchy = { + "admin": 3, + "educator": 2, + "user": 1 + } + + current_level = role_hierarchy.get(user_role, 0) + required_level = role_hierarchy.get(required_role, 0) + + return current_level >= required_level + +def add_user(username: str, password: str, name: str, role: str = "user") -> bool: + """Add a new user (admin only)""" + if not check_role_permission("admin"): + return False + + users = load_users() + + if username in users: + return False # User already exists + + users[username] = { + "password_hash": hash_password(password), + "role": role, + "name": name + } + + save_users(users) + return True + +def render_admin_panel(): + """Render admin panel for user management""" + if not check_role_permission("admin"): + st.error("🚫 Access denied. Admin privileges required.") + return + + st.markdown("### 👨‍💼 Admin Panel") + + with st.expander("👥 User Management"): + st.markdown("#### Add New User") + + with st.form("add_user_form"): + new_username = st.text_input("Username") + new_password = st.text_input("Password", type="password") + new_name = st.text_input("Full Name") + new_role = st.selectbox("Role", ["user", "educator", "admin"]) + + if st.form_submit_button("➕ Add User"): + if all([new_username, new_password, new_name]): + if add_user(new_username, new_password, new_name, new_role): + st.success(f"✅ User '{new_username}' added successfully!") + else: + st.error("❌ User already exists or operation failed") + else: + st.warning("⚠️ Please fill all fields") + + # Display existing users + st.markdown("#### Existing Users") + users = load_users() + + for username, info in users.items(): + col1, col2, col3 = st.columns([2, 1, 1]) + with col1: + st.text(f"👤 {info['name']} (@{username})") + with col2: + st.text(f"🏷️ {info['role']}") + with col3: + if username != st.session_state.username: # Can't delete self + if st.button("🗑️", key=f"del_{username}", help="Delete user"): + del users[username] + save_users(users) + st.rerun() + +# Session timeout functionality +def check_session_timeout(timeout_minutes: int = 30): + """Check if session has timed out""" + import time + + if 'last_activity' not in st.session_state: + st.session_state.last_activity = time.time() + + current_time = time.time() + time_diff = (current_time - st.session_state.last_activity) / 60 # Convert to minutes + + if time_diff > timeout_minutes and st.session_state.authenticated: + st.warning(f"⏰ Session expired after {timeout_minutes} minutes of inactivity") + logout() + else: + st.session_state.last_activity = current_time \ No newline at end of file diff --git a/users.json b/users.json new file mode 100644 index 0000000..4de439b --- /dev/null +++ b/users.json @@ -0,0 +1,22 @@ +{ + "admin": { + "password_hash": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9", + "role": "admin", + "name": "Administrator" + }, + "educator": { + "password_hash": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", + "role": "educator", + "name": "Educator" + }, + "izzy": { + "password_hash": "edf90c9dbbecc484d3507d19ab11d39e53ec3ed1c19e447fc229c0b008cc6721", + "role": "educator", + "name": "izzy" + }, + "wow": { + "password_hash": "5257b476780ffe3a534df683a88355ace82f722998a813e9a12ffa23ffffca79", + "role": "user", + "name": "wow" + } +} \ No newline at end of file