feat(auth): add session-based login/logout with bcrypt hashing, seed default admin, templates and navbar updates; add auth middleware; pin SQLAlchemy 1.4.x for Py3.13; update TODOs
This commit is contained in:
16
TODO.md
16
TODO.md
@@ -6,20 +6,20 @@ Refer to `del.plan.md` for context. Check off items as they’re completed.
|
|||||||
- [ ] Create requirements.txt with minimal deps
|
- [ ] Create requirements.txt with minimal deps
|
||||||
- [ ] Copy delphi-logo.webp into static/logo/
|
- [ ] Copy delphi-logo.webp into static/logo/
|
||||||
- [ ] Set up SQLAlchemy Base and engine/session helpers
|
- [ ] Set up SQLAlchemy Base and engine/session helpers
|
||||||
- [ ] Add User model with username and password_hash
|
- [x] Add User model with username and password_hash
|
||||||
- [ ] Add Client model (rolodex_id and core fields)
|
- [ ] Add Client model (rolodex_id and core fields)
|
||||||
- [ ] Add Phone model with FK to Client
|
- [ ] Add Phone model with FK to Client
|
||||||
- [ ] Add Case model (file_no unique, FK to Client)
|
- [ ] Add Case model (file_no unique, FK to Client)
|
||||||
- [ ] Add Transaction model with FK to Case
|
- [ ] Add Transaction model with FK to Case
|
||||||
- [ ] Add Document model with FK to Case
|
- [ ] Add Document model with FK to Case
|
||||||
- [ ] Add Payment model with FK to Case
|
- [ ] Add Payment model with FK to Case
|
||||||
- [ ] Create tables and seed default admin user
|
- [x] Create tables and seed default admin user
|
||||||
- [ ] Create FastAPI app with DB session dependency
|
- [x] Create FastAPI app with DB session dependency
|
||||||
- [ ] Add SessionMiddleware with SECRET_KEY from env
|
- [x] Add SessionMiddleware with SECRET_KEY from env
|
||||||
- [ ] Configure Jinja2 templates and mount static files
|
- [x] Configure Jinja2 templates and mount static files
|
||||||
- [ ] Create base.html with Bootstrap 5 CDN and nav
|
- [x] Create base.html with Bootstrap 5 CDN and nav
|
||||||
- [ ] Implement login form, POST handler, and logout
|
- [x] Implement login form, POST handler, and logout
|
||||||
- [ ] Create login.html form
|
- [x] Create login.html form
|
||||||
- [ ] Implement dashboard route listing cases
|
- [ ] Implement dashboard route listing cases
|
||||||
- [ ] Add simple search by file_no/name/keyword
|
- [ ] Add simple search by file_no/name/keyword
|
||||||
- [ ] Create dashboard.html with table and search box
|
- [ ] Create dashboard.html with table and search box
|
||||||
|
|||||||
BIN
app/__pycache__/auth.cpython-313.pyc
Normal file
BIN
app/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
186
app/auth.py
Normal file
186
app/auth.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
Authentication utilities for Delphi Database application.
|
||||||
|
|
||||||
|
This module provides password hashing, user authentication, and session management
|
||||||
|
functions for secure user login/logout functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from .models import User
|
||||||
|
from .database import SessionLocal
|
||||||
|
|
||||||
|
# Configure password hashing context
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
"""
|
||||||
|
Hash a password using bcrypt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
password (str): Plain text password to hash
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Hashed password
|
||||||
|
"""
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Verify a password against its hash.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plain_password (str): Plain text password to verify
|
||||||
|
hashed_password (str): Hashed password to check against
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if password matches hash, False otherwise
|
||||||
|
"""
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_user(username: str, password: str) -> User | None:
|
||||||
|
"""
|
||||||
|
Authenticate a user with username and password.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username to authenticate
|
||||||
|
password (str): Password to verify
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User | None: User object if authentication successful, None otherwise
|
||||||
|
"""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
user = db.query(User).filter(User.username == username).first()
|
||||||
|
if not user:
|
||||||
|
logger.warning(f"Authentication failed: User '{username}' not found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not verify_password(password, user.password_hash):
|
||||||
|
logger.warning(f"Authentication failed: Invalid password for user '{username}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not user.is_active:
|
||||||
|
logger.warning(f"Authentication failed: User '{username}' is inactive")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(f"User '{username}' authenticated successfully")
|
||||||
|
return user
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Authentication error for user '{username}': {e}")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username: str, password: str, is_active: bool = True) -> User | None:
|
||||||
|
"""
|
||||||
|
Create a new user with hashed password.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username for the new user
|
||||||
|
password (str): Plain text password (will be hashed)
|
||||||
|
is_active (bool): Whether the user should be active
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User | None: Created user object if successful, None if user already exists
|
||||||
|
"""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
# Check if user already exists
|
||||||
|
existing_user = db.query(User).filter(User.username == username).first()
|
||||||
|
if existing_user:
|
||||||
|
logger.warning(f"User creation failed: User '{username}' already exists")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Hash the password
|
||||||
|
password_hash = hash_password(password)
|
||||||
|
|
||||||
|
# Create new user
|
||||||
|
new_user = User(
|
||||||
|
username=username,
|
||||||
|
password_hash=password_hash,
|
||||||
|
is_active=is_active
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(new_user)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_user)
|
||||||
|
|
||||||
|
logger.info(f"User '{username}' created successfully")
|
||||||
|
return new_user
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logger.error(f"Error creating user '{username}': {e}")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def seed_admin_user() -> None:
|
||||||
|
"""
|
||||||
|
Create a default admin user if one doesn't exist.
|
||||||
|
|
||||||
|
This function should be called during application startup to ensure
|
||||||
|
there's at least one admin user for initial access.
|
||||||
|
"""
|
||||||
|
admin_username = "admin"
|
||||||
|
admin_password = "admin123" # In production, use a more secure default
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
# Check if admin user already exists
|
||||||
|
existing_admin = db.query(User).filter(User.username == admin_username).first()
|
||||||
|
if existing_admin:
|
||||||
|
logger.info(f"Admin user '{admin_username}' already exists")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create admin user
|
||||||
|
admin_user = create_user(admin_username, admin_password)
|
||||||
|
if admin_user:
|
||||||
|
logger.info(f"Default admin user '{admin_username}' created successfully")
|
||||||
|
else:
|
||||||
|
logger.error("Failed to create default admin user")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error seeding admin user: {e}")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user_from_session(session_data: dict) -> User | None:
|
||||||
|
"""
|
||||||
|
Get current user from session data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_data (dict): Session data dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User | None: Current user if session is valid, None otherwise
|
||||||
|
"""
|
||||||
|
user_id = session_data.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
user = db.query(User).filter(User.id == user_id, User.is_active == True).first()
|
||||||
|
if not user:
|
||||||
|
logger.warning(f"No active user found for session user_id: {user_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving current user from session: {e}")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@@ -33,8 +33,8 @@ engine = create_engine(
|
|||||||
# Create session factory
|
# Create session factory
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
# Create declarative base for models
|
# Import Base from models for SQLAlchemy 1.x compatibility
|
||||||
Base = declarative_base()
|
from .models import Base
|
||||||
|
|
||||||
|
|
||||||
def get_db() -> Generator[Session, None, None]:
|
def get_db() -> Generator[Session, None, None]:
|
||||||
@@ -68,6 +68,14 @@ def create_tables() -> None:
|
|||||||
"""
|
"""
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
# Seed default admin user after creating tables
|
||||||
|
try:
|
||||||
|
from .auth import seed_admin_user
|
||||||
|
seed_admin_user()
|
||||||
|
except ImportError:
|
||||||
|
# Handle case where auth module isn't available yet during initial import
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_database_url() -> str:
|
def get_database_url() -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
141
app/main.py
141
app/main.py
@@ -10,15 +10,18 @@ import logging
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, Depends, Request
|
from fastapi import FastAPI, Depends, Request
|
||||||
from fastapi.middleware.sessions import SessionMiddleware
|
from fastapi.responses import RedirectResponse
|
||||||
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
from .database import create_tables, get_db, get_database_url
|
from .database import create_tables, get_db, get_database_url
|
||||||
from .models import User
|
from .models import User
|
||||||
|
from .auth import authenticate_user, get_current_user_from_session
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -36,6 +39,34 @@ logger = logging.getLogger(__name__)
|
|||||||
templates = Jinja2Templates(directory="app/templates")
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
|
||||||
|
|
||||||
|
class AuthMiddleware(BaseHTTPMiddleware):
|
||||||
|
"""
|
||||||
|
Simple session-based authentication middleware.
|
||||||
|
|
||||||
|
Redirects unauthenticated users to /login for protected routes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app, exempt_paths: list[str] | None = None):
|
||||||
|
super().__init__(app)
|
||||||
|
self.exempt_paths = exempt_paths or []
|
||||||
|
|
||||||
|
async def dispatch(self, request, call_next):
|
||||||
|
path = request.url.path
|
||||||
|
|
||||||
|
# Allow exempt paths and static assets
|
||||||
|
if (
|
||||||
|
path in self.exempt_paths
|
||||||
|
or path.startswith("/static")
|
||||||
|
or path.startswith("/favicon")
|
||||||
|
):
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
# Enforce authentication for other paths
|
||||||
|
if not request.session.get("user_id"):
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""
|
"""
|
||||||
@@ -79,7 +110,11 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add SessionMiddleware for session management
|
# Register authentication middleware with exempt paths
|
||||||
|
EXEMPT_PATHS = ["/", "/health", "/login", "/logout"]
|
||||||
|
app.add_middleware(AuthMiddleware, exempt_paths=EXEMPT_PATHS)
|
||||||
|
|
||||||
|
# Add SessionMiddleware for session management (must be added LAST so it runs FIRST)
|
||||||
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY)
|
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY)
|
||||||
|
|
||||||
# Mount static files directory
|
# Mount static files directory
|
||||||
@@ -114,3 +149,105 @@ async def health_check(db: Session = Depends(get_db)):
|
|||||||
"database": "error",
|
"database": "error",
|
||||||
"error": str(e)
|
"error": str(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/login")
|
||||||
|
async def login_form(request: Request):
|
||||||
|
"""
|
||||||
|
Display login form.
|
||||||
|
|
||||||
|
If user is already logged in, redirect to dashboard.
|
||||||
|
"""
|
||||||
|
# Check if user is already logged in
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if user:
|
||||||
|
return RedirectResponse(url="/dashboard", status_code=302)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("login.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/login")
|
||||||
|
async def login_submit(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Handle login form submission.
|
||||||
|
|
||||||
|
Authenticates user credentials and sets up session.
|
||||||
|
"""
|
||||||
|
form = await request.form()
|
||||||
|
username = form.get("username")
|
||||||
|
password = form.get("password")
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
error_message = "Username and password are required"
|
||||||
|
return templates.TemplateResponse("login.html", {
|
||||||
|
"request": request,
|
||||||
|
"error": error_message
|
||||||
|
})
|
||||||
|
|
||||||
|
# Authenticate user
|
||||||
|
user = authenticate_user(username, password)
|
||||||
|
if not user:
|
||||||
|
error_message = "Invalid username or password"
|
||||||
|
return templates.TemplateResponse("login.html", {
|
||||||
|
"request": request,
|
||||||
|
"error": error_message
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set up user session
|
||||||
|
request.session["user_id"] = user.id
|
||||||
|
request.session["user"] = {"id": user.id, "username": user.username}
|
||||||
|
|
||||||
|
logger.info(f"User '{username}' logged in successfully")
|
||||||
|
|
||||||
|
# Redirect to dashboard after successful login
|
||||||
|
return RedirectResponse(url="/dashboard", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/logout")
|
||||||
|
async def logout(request: Request):
|
||||||
|
"""
|
||||||
|
Handle user logout.
|
||||||
|
|
||||||
|
Clears user session and redirects to home page.
|
||||||
|
"""
|
||||||
|
username = request.session.get("user", {}).get("username", "unknown")
|
||||||
|
request.session.clear()
|
||||||
|
logger.info(f"User '{username}' logged out")
|
||||||
|
|
||||||
|
return RedirectResponse(url="/", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/dashboard")
|
||||||
|
async def dashboard(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Dashboard page - requires authentication.
|
||||||
|
|
||||||
|
Shows an overview of the system and provides navigation to main features.
|
||||||
|
"""
|
||||||
|
# Check authentication
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("dashboard.html", {
|
||||||
|
"request": request,
|
||||||
|
"user": user
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/admin")
|
||||||
|
async def admin_panel(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Admin panel - requires authentication.
|
||||||
|
|
||||||
|
Provides administrative functions like data import and system management.
|
||||||
|
"""
|
||||||
|
# Check authentication
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("admin.html", {
|
||||||
|
"request": request,
|
||||||
|
"user": user
|
||||||
|
})
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ All models inherit from Base which is configured in the database module.
|
|||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float, Text, Boolean
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float, Text, Boolean
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from .database import Base
|
|
||||||
|
# Create Base for SQLAlchemy 1.x compatibility
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
|
|||||||
@@ -43,10 +43,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{% if 'user' in session %}
|
{% if request.session.get('user') %}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ session.user.username }}
|
<i class="bi bi-person-circle me-1"></i>{{ request.session.get('user').username if request.session.get('user') else '' }}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="userDropdown">
|
<ul class="dropdown-menu" aria-labelledby="userDropdown">
|
||||||
<li><a class="dropdown-item" href="/profile">Profile</a></li>
|
<li><a class="dropdown-item" href="/profile">Profile</a></li>
|
||||||
|
|||||||
@@ -1 +1,111 @@
|
|||||||
<!-- Login form template -->
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Login - Delphi Database{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="{{ url_for('static', path='/logo/delphi-logo.webp') }}" alt="Delphi Logo" class="mb-3" style="width: 60px; height: 60px;">
|
||||||
|
<h2 class="card-title">Welcome Back</h2>
|
||||||
|
<p class="text-muted">Sign in to access Delphi Database</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/login">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Username</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required
|
||||||
|
placeholder="Enter your username" autocomplete="username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required
|
||||||
|
placeholder="Enter your password" autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="rememberMe">
|
||||||
|
<label class="form-check-label" for="rememberMe">
|
||||||
|
Remember me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-2"></i>Sign In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
Default credentials: admin / admin123
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<small class="text-muted">
|
||||||
|
Don't have an account? Contact your administrator.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>
|
||||||
|
// Auto-focus on username field
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
document.getElementById('username').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide password toggle (optional enhancement)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const passwordField = document.getElementById('password');
|
||||||
|
const toggleBtn = document.createElement('button');
|
||||||
|
toggleBtn.type = 'button';
|
||||||
|
toggleBtn.className = 'btn btn-outline-secondary';
|
||||||
|
toggleBtn.innerHTML = '<i class="bi bi-eye"></i>';
|
||||||
|
toggleBtn.style.border = 'none';
|
||||||
|
toggleBtn.style.background = 'transparent';
|
||||||
|
|
||||||
|
// Add toggle functionality
|
||||||
|
toggleBtn.addEventListener('click', function() {
|
||||||
|
if (passwordField.type === 'password') {
|
||||||
|
passwordField.type = 'text';
|
||||||
|
this.innerHTML = '<i class="bi bi-eye-slash"></i>';
|
||||||
|
} else {
|
||||||
|
passwordField.type = 'password';
|
||||||
|
this.innerHTML = '<i class="bi bi-eye"></i>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert toggle button into password input group
|
||||||
|
const passwordInputGroup = passwordField.closest('.input-group');
|
||||||
|
if (passwordInputGroup) {
|
||||||
|
const span = passwordInputGroup.querySelector('.input-group-text');
|
||||||
|
passwordInputGroup.insertBefore(toggleBtn, span.nextSibling);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
5
cookies.txt
Normal file
5
cookies.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# https://curl.se/docs/http-cookies.html
|
||||||
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
|
#HttpOnly_localhost FALSE / FALSE 1761003936 session eyJ1c2VyX2lkIjogMSwgInVzZXIiOiB7ImlkIjogMSwgInVzZXJuYW1lIjogImFkbWluIn19.aORUoA.KSAst9pcXJJJHTc1m-mY_jQ0P6A
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fastapi==0.104.1
|
fastapi==0.104.1
|
||||||
sqlalchemy==2.0.23
|
sqlalchemy==1.4.54
|
||||||
alembic==1.12.1
|
alembic==1.12.1
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]==3.3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user