adding base project

This commit is contained in:
Azmat-7860
2026-10-25 23:11:19 +05:30
commit 5993c15b70
23 changed files with 1297 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# Application Settings
PROJECT_NAME="AuthSec FastAPI"
API_V1_STR="/api"
PORT=9119
DEBUG=True
# Security
SECRET_KEY="cns-secret-key-change-this-in-production-authsec"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=1440
# Database Configuration
# Default is SQLite for zero-setup local development:
DATABASE_URL="sqlite:///./authsec.db"
# Or use MySQL (e.g. for RDS / Local MySQL):
# DATABASE_URL="mysql+pymysql://cnsdev:cnsdev1234@localhost:3306/authsec"
# CORS Allowed Origins (Comma separated or * for all)
CORS_ORIGINS="http://localhost:4200,http://localhost:3000,http://127.0.0.1:4200,http://localhost:8080,*"
+49
View File
@@ -0,0 +1,49 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
.venv/
env/
ENV/
# Database files
*.sqlite
*.sqlite3
*.db
# Environment variables
.env
# IDEs
.idea/
.vscode/
*.swp
*.swo
# OS specific
.DS_Store
Thumbs.db
+120
View File
@@ -0,0 +1,120 @@
# AuthSec FastAPI - Base Project for Python
A production-ready, modular **FastAPI** backend that serves as a direct drop-in alternative to the Spring Boot base project (`authsec-be`).
It provides **identical API contracts and endpoint paths** so that frontends like **Angular Clarity** or **React** can communicate with it without modifying any frontend code.
---
## 🌟 Key Features
- **100% Endpoint Parity with Spring Boot**:
- `POST /token/session` - User login and JWT session generation
- `GET /token/logout` - User logout
- `GET /token/user` - Currently authenticated user details
- `GET /fndMenu/menuloadbyuser` - Hierarchical role-based menu tree for Angular Clarity sidebar
- `GET`, `POST`, `PUT`, `DELETE` for `/{{TABLE}}` and `/{{TABLE_LOWER}}` - Entity CRUD endpoints
- **Zero-Setup Database**: Runs on **SQLite** out of the box with automatic table creation & seeding on startup. Switchable to MySQL or PostgreSQL by simply editing `DATABASE_URL` in `.env`.
- **Pre-Seeded Accounts**: Comes with a default administrator account:
- **Email**: `admin@cns.com`
- **Password**: `admin123`
- **Dynamic Entity Router**: Any generated entity controller placed in `app/io8ref/*/controllers.py` is automatically registered upon startup.
- **Interactive Swagger UI**: Full interactive API documentation available at `http://localhost:9119/docs`.
---
## 🚀 Quick Start (First Time on Windows)
### Step 1: Install Python
If you don't have Python installed on your machine:
1. Download Python 3.11 or 3.12 from [python.org/downloads](https://www.python.org/downloads/).
2. Run the installer.
3. ⚠️ **IMPORTANT**: Check the box **"Add python.exe to PATH"** at the bottom of the first installer screen before clicking *Install Now*.
### Step 2: Open PowerShell in Project Directory
Open PowerShell in `d:\WAP\Codeextractor\authsec-fastapi`:
```powershell
cd d:\WAP\Codeextractor\authsec-fastapi
```
### Step 3: Create and Activate Virtual Environment
```powershell
# Create a virtual environment named venv
python -m venv venv
# Activate it
.\venv\Scripts\Activate.ps1
```
*(If PowerShell shows an execution policy error, run `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` and then run the activate command again).*
### Step 4: Install Dependencies
```powershell
pip install -r requirements.txt
```
### Step 5: Run the Server
```powershell
python run.py
```
The server will start on:
- **API URL**: `http://localhost:9119`
- **Interactive Swagger Documentation**: `http://localhost:9119/docs`
---
## 📡 API Contract Reference
### 1. Authentication
- **`POST /token/session`** (or `POST /token/generate-token`)
- **Request Body**:
```json
{
"email": "admin@cns.com",
"password": "admin123"
}
```
- **Response Payload**:
```json
{
"operationStatus": "SUCCESS",
"operationMessage": "Login Success",
"item": {
"userId": 1,
"email": "admin@cns.com",
"fullname": "Admin User",
"firstName": "Admin",
"username": "admin",
"roles": ["Admin"],
"token": "eyJhbG..."
}
}
```
- **`GET /token/logout`**
- **`GET /token/user`** (with `Authorization: Bearer <token>`)
### 2. Menu Navigation
- **`GET /fndMenu/menuloadbyuser`**
- Returns root navigation items with their respective sub-menus, action paths, and icons formatted for the Angular layout.
### 3. Entity CRUD Operations
Every entity (such as reference entity `Azzu`) provides both uppercase and lowercase endpoints:
- `POST /Azzu` or `POST /azzu` - Add new record
- `GET /Azzu` or `GET /azzu` - Get all records
- `GET /Azzu/{id}` or `GET /azzu/{id}` - Get record by ID
- `PUT /Azzu/{id}` or `PUT /azzu/{id}` - Update record by ID
- `DELETE /Azzu/{id}` or `DELETE /azzu/{id}` - Delete record by ID
---
## 🛠️ Code Extractor V2 Integration
The pack configuration file is located at [`pack-fastapi-v1.0.json`](./pack-fastapi-v1.0.json).
When Code Extractor generates a new entity (e.g. `Customer`), it produces:
1. `app/io8ref/Customer/models.py` - SQLAlchemy entity model
2. `app/io8ref/Customer/schemas.py` - Pydantic DTO schema
3. `app/io8ref/Customer/services.py` - Business logic and database operations
4. `app/io8ref/Customer/controllers.py` - REST API routes
Because `app/main.py` has automatic discovery enabled, the new entity endpoints are instantly active without needing any manual code registration.
+3
View File
@@ -0,0 +1,3 @@
"""
AuthSec FastAPI Application Package
"""
+38
View File
@@ -0,0 +1,38 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Table
from sqlalchemy.orm import relationship
from datetime import datetime
from app.core.database import Base
# Association table for User <-> Role (many to many)
user_roles = Table(
"app_user_roles",
Base.metadata,
Column("user_id", Integer, ForeignKey("app_users.user_id"), primary_key=True),
Column("role_id", Integer, ForeignKey("roles.id"), primary_key=True)
)
class Role(Base):
__tablename__ = "roles"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String(100), unique=True, index=True, nullable=False)
description = Column(String(255), nullable=True)
class AppUser(Base):
__tablename__ = "app_users"
user_id = Column(Integer, primary_key=True, index=True, autoincrement=True)
email = Column(String(255), unique=True, index=True, nullable=False)
username = Column(String(100), unique=True, index=True, nullable=False)
password = Column(String(255), nullable=True) # Can be null before initial verification
full_name = Column(String(255), nullable=True)
first_name = Column(String(100), nullable=True)
last_name = Column(String(100), nullable=True)
status = Column(String(50), default="ACTIVE")
user_grp_id = Column(Integer, default=1)
is_active = Column(Boolean, default=True)
random_no = Column(String(50), nullable=True) # OTP storage matching Spring Boot
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
roles = relationship("Role", secondary=user_roles, lazy="joined")
+291
View File
@@ -0,0 +1,291 @@
from fastapi import APIRouter, Depends, HTTPException, status, Request, Query, Body
from sqlalchemy.orm import Session
from typing import List, Optional
from app.core.deps import get_db, require_current_user, get_current_user_token
from app.core.security import verify_password, get_password_hash, create_access_token
from app.auth.models import AppUser, Role
from app.auth.schemas import (
LoginRequest,
SessionResponse,
SessionItem,
RegistrationRequest,
SendEmailRequest,
EmailExistsRequest,
UserResponse
)
router = APIRouter(prefix="/token", tags=["Authentication & User Session"])
@router.post("/session", response_model=SessionResponse)
def login_session(login_req: LoginRequest, db: Session = Depends(get_db)):
"""
Authenticates user and returns the exact SessionResponse expected by Angular Clarity login.service.ts.
"""
user = db.query(AppUser).filter(AppUser.email == login_req.email).first()
if not user or not verify_password(login_req.password, user.password):
return SessionResponse(
operationStatus="ERROR",
operationMessage="Login Failed",
item=None
)
# Collect role descriptions / names
roles = [r.description or r.name for r in user.roles] if user.roles else ["Admin"]
# Generate JWT
token = create_access_token(
subject=user.user_id,
extra_claims={
"email": user.email,
"username": user.username,
"roles": roles,
"user_grp": user.user_grp_id
}
)
session_item = SessionItem(
userId=user.user_id,
email=user.email,
fullname=user.full_name or user.username,
firstName=user.first_name or user.full_name or user.username,
username=user.username,
roles=roles,
token=token
)
return SessionResponse(
operationStatus="SUCCESS",
operationMessage="Login Success",
item=session_item
)
@router.post("/generate-token", response_model=SessionResponse)
def generate_token(login_req: LoginRequest, db: Session = Depends(get_db)):
"""Alias for /token/session."""
return login_session(login_req, db)
@router.get("/logout")
@router.post("/logout")
def logout_user():
"""Logs out the user."""
return {"operationStatus": "SUCCESS", "operationMessage": "Logged out successfully"}
@router.get("/user")
def get_current_logged_in_user(
db: Session = Depends(get_db),
token_payload: dict = Depends(require_current_user)
):
"""Returns current user details based on the JWT token."""
user_id = token_payload.get("user_id") or token_payload.get("sub")
user = db.query(AppUser).filter(AppUser.user_id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
roles = [r.description or r.name for r in user.roles] if user.roles else ["Admin"]
return {
"userId": user.user_id,
"email": user.email,
"username": user.username,
"fullname": user.full_name,
"roles": roles,
"status": user.status
}
@router.get("/getuser/{user_id}")
def get_user_by_id(user_id: int, db: Session = Depends(get_db)):
"""Returns user by ID."""
user = db.query(AppUser).filter(AppUser.user_id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
roles = [r.description or r.name for r in user.roles] if user.roles else ["Admin"]
return {
"userId": user.user_id,
"email": user.email,
"username": user.username,
"fullname": user.full_name,
"roles": roles
}
@router.post("/email-exists")
def check_email_exists(req: EmailExistsRequest, db: Session = Depends(get_db)):
"""Checks if email already exists."""
user = db.query(AppUser).filter(AppUser.email == req.email).first()
return {"exists": user is not None}
@router.post("/users/sysaccount/savesysaccount")
@router.post("/savesysaccount")
def save_sys_account(data: dict = Body(...)):
"""
Saves sysaccount during Angular user signup (mirrors Spring Boot SysAccountController /savesysaccount).
"""
account_id = data.get("account_id") or 1
res = dict(data)
res["account_id"] = account_id
res["mongoid"] = f"acc_{account_id}"
return res
@router.post("/addOneAppUser")
def register_user(reg: RegistrationRequest, db: Session = Depends(get_db)):
"""
Registers a new user or activates a pending user created during OTP step.
Mirrors Spring Boot SessionController /addOneAppUser.
"""
email = reg.email.strip()
user = db.query(AppUser).filter(AppUser.email == email).first()
raw_pwd = reg.new_password or reg.password
if not raw_pwd:
raise HTTPException(status_code=400, detail="Password is required")
hashed_pwd = get_password_hash(raw_pwd)
first_name = reg.first_name or reg.fullName or email.split("@")[0]
last_name = reg.last_name or ""
full_name = reg.fullName or f"{first_name} {last_name}".strip()
username = reg.username or email.split("@")[0]
if user:
if user.status == "ACTIVE" and user.password:
raise HTTPException(status_code=400, detail="Email already exists")
# Activate pending user
user.password = hashed_pwd
user.first_name = first_name
user.last_name = last_name
user.full_name = full_name
user.username = username
user.status = "ACTIVE"
user.is_active = True
else:
user = AppUser(
email=email,
username=username,
password=hashed_pwd,
full_name=full_name,
first_name=first_name,
last_name=last_name,
status="ACTIVE",
user_grp_id=1,
is_active=True
)
db.add(user)
# Assign Admin role
admin_role = db.query(Role).filter(Role.name == "ROLE_ADMIN").first()
if admin_role and admin_role not in user.roles:
user.roles.append(admin_role)
db.commit()
db.refresh(user)
return {
"userId": user.user_id,
"email": user.email,
"username": user.username,
"fullName": user.full_name,
"firstName": user.first_name,
"roles": [r.description or r.name for r in user.roles],
"message": "User registered successfully"
}
@router.post("/user/send_email")
def send_email_for_otp(req: SendEmailRequest, db: Session = Depends(get_db)):
"""
Sends OTP for email verification (mirrors Spring Boot SessionController /token/user/send_email).
"""
import random
from app.core.email import email_service
email = req.email.strip()
user = db.query(AppUser).filter(AppUser.email == email).first()
if user and user.status == "ACTIVE" and user.password: # Already registered active user
raise HTTPException(status_code=400, detail=f"{email} already exists")
otp = str(random.randint(100000, 999999))
if not user:
username = req.username or email.split("@")[0]
user = AppUser(
email=email,
username=username,
password="", # Empty string satisfies NOT NULL constraint until password is set
full_name=req.fullName or username,
first_name=req.fullName or username,
status="PENDING",
random_no=otp,
is_active=False
)
db.add(user)
else:
user.random_no = otp
db.commit()
email_service.adduserviaadmin(email, "Email Verification", otp)
return {
"operationStatus": "SUCCESS",
"operationMessage": "Otp send successfully",
"msg": "Otp send successfully",
"otp": otp
}
@router.post("/user/resend_otp")
def resend_otp(
email: Optional[str] = Query(None),
db: Session = Depends(get_db),
body: Optional[SendEmailRequest] = None
):
"""
Resends OTP for email verification (mirrors Spring Boot SessionController /token/user/resend_otp).
Supports query parameter (?email=...) or JSON body.
"""
import random
from app.core.email import email_service
target_email = email or (body.email if body else None)
if not target_email:
raise HTTPException(status_code=400, detail="Email is required")
target_email = target_email.strip()
user = db.query(AppUser).filter(AppUser.email == target_email).first()
if not user:
raise HTTPException(status_code=400, detail=f"{target_email} does not exist")
otp = str(random.randint(100000, 999999))
user.random_no = otp
db.commit()
email_service.adduserviaadmin(target_email, "Email Verification - Resend OTP", otp)
return {
"operationStatus": "SUCCESS",
"operationMessage": "resend Otp send successfully",
"msg": "resend Otp send successfully",
"otp": otp
}
@router.post("/user/otp_verification")
def otp_verification(
email: str = Query(...),
otp: str = Query(...),
db: Session = Depends(get_db)
):
"""
Verifies OTP entered by user (mirrors Spring Boot SessionController /token/user/otp_verification).
Query parameters: ?email=...&otp=...
"""
email_clean = email.replace(" ", "+").strip()
user = db.query(AppUser).filter(AppUser.email == email_clean).first()
if not user:
raise HTTPException(status_code=400, detail="User not found")
if user.random_no and user.random_no.strip() == otp.strip():
return {
"operationStatus": "SUCCESS",
"operationMessage": "OTP Verified",
"msg": "OTP Verified"
}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Wrong OTP"
)
+56
View File
@@ -0,0 +1,56 @@
from pydantic import BaseModel, EmailStr
from typing import Optional, List, Any
class LoginRequest(BaseModel):
email: str
password: str
class SessionItem(BaseModel):
userId: int
email: str
fullname: Optional[str] = ""
firstName: Optional[str] = ""
username: Optional[str] = ""
roles: List[str] = []
token: str
class SessionResponse(BaseModel):
operationStatus: str # "SUCCESS" or "ERROR"
operationMessage: str # "Login Success" or "Login Failed"
item: Optional[SessionItem] = None
class RegistrationRequest(BaseModel):
email: str
password: Optional[str] = None
new_password: Optional[str] = None
confirm_password: Optional[str] = None
fullName: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
username: Optional[str] = None
account_id: Optional[Any] = None
mob_no: Optional[Any] = None
gender: Optional[str] = None
date_of_birth: Optional[str] = None
how_you_know_aboutus: Optional[str] = None
accesstype: Optional[str] = None
class SendEmailRequest(BaseModel):
email: str
password: Optional[str] = None
fullName: Optional[str] = None
username: Optional[str] = None
account_id: Optional[Any] = None
recaptcha: Optional[Any] = None
class EmailExistsRequest(BaseModel):
email: str
class UserResponse(BaseModel):
userId: int
email: str
username: str
fullName: Optional[str] = None
firstName: Optional[str] = None
roles: List[str] = []
status: Optional[str] = "ACTIVE"
+41
View File
@@ -0,0 +1,41 @@
import os
from typing import List, Union
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
PROJECT_NAME: str = "AuthSec FastAPI"
API_V1_STR: str = "/api"
PORT: int = 9119
DEBUG: bool = True
# Security
SECRET_KEY: str = "cns-secret-key-change-this-in-production-authsec"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24 hours
# Database
DATABASE_URL: str = "sqlite:///./authsec.db"
# CORS
CORS_ORIGINS: str = "http://localhost:4200,http://localhost:3000,http://127.0.0.1:4200,http://localhost:8080,*"
# Email / SMTP Settings (mirrors Spring Boot JavaMailSender)
SMTP_HOST: str = ""
SMTP_PORT: int = 587
SMTP_USER: str = ""
SMTP_PASSWORD: str = ""
SMTP_TLS: bool = True
EMAILS_FROM_EMAIL: str = "noreply@authsec.com"
@property
def cors_origins_list(self) -> List[str]:
if not self.CORS_ORIGINS:
return ["*"]
return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()]
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
settings = Settings()
+56
View File
@@ -0,0 +1,56 @@
import re
from urllib.parse import urlparse
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
def ensure_database_exists(db_url: str):
"""
If using MySQL, automatically creates the target database if it does not exist
(mirrors spring.datasource.url createDatabaseIfNotExist=true).
"""
if db_url.startswith("mysql"):
try:
import pymysql
# Parse connection URL safely
clean_url = db_url.replace("mysql+pymysql://", "http://").replace("mysql://", "http://")
parsed = urlparse(clean_url)
db_name = parsed.path.lstrip("/").split("?")[0]
if db_name:
conn = pymysql.connect(
host=parsed.hostname,
port=parsed.port or 3306,
user=parsed.username,
password=parsed.password,
connect_timeout=5
)
with conn.cursor() as cur:
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{db_name}`;")
conn.close()
except Exception as e:
print(f"Note on DB creation: {e}")
ensure_database_exists(settings.DATABASE_URL)
connect_args = {}
if settings.DATABASE_URL.startswith("sqlite"):
connect_args = {"check_same_thread": False}
engine = create_engine(
settings.DATABASE_URL,
connect_args=connect_args,
pool_pre_ping=True
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
"""Dependency for obtaining database session per request."""
db = SessionLocal()
try:
yield db
finally:
db.close()
+34
View File
@@ -0,0 +1,34 @@
from typing import Generator, Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.core.database import SessionLocal
from app.core.security import decode_access_token
security = HTTPBearer(auto_error=False)
def get_db() -> Generator:
"""Dependency that yields a database session and ensures closure."""
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_current_user_token(credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)) -> Optional[dict]:
"""Extracts and verifies JWT token payload from Authorization header."""
if not credentials:
return None
token = credentials.credentials
payload = decode_access_token(token)
return payload
def require_current_user(payload: Optional[dict] = Depends(get_current_user_token)) -> dict:
"""Ensures request is authenticated with a valid token."""
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials or token expired",
headers={"WWW-Authenticate": "Bearer"},
)
return payload
+98
View File
@@ -0,0 +1,98 @@
import smtplib
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional, List
from app.core.config import settings
logger = logging.getLogger(__name__)
class EmailService:
"""
Email Service mirroring Spring Boot's JavaMailSender / EmailService.
Supports SMTP (Gmail, SendGrid, AWS SES, etc.).
If SMTP is not configured, logs the message to console (Dev Mode) so flows never crash.
"""
def __init__(self):
self.host = getattr(settings, "SMTP_HOST", "")
self.port = getattr(settings, "SMTP_PORT", 587)
self.user = getattr(settings, "SMTP_USER", "")
self.password = getattr(settings, "SMTP_PASSWORD", "")
self.tls = getattr(settings, "SMTP_TLS", True)
self.from_email = getattr(settings, "EMAILS_FROM_EMAIL", "noreply@authsec.com")
def is_configured(self) -> bool:
"""Returns True if valid SMTP credentials are provided."""
return bool(self.host and self.user and self.password)
def send_simple_message(self, to: str, subject: str, text: str, from_email: Optional[str] = None) -> bool:
"""Sends a plain text email (mirrors Spring Boot EmailService.sendSimpleMessage)."""
sender = from_email or self.from_email
if not self.is_configured():
print(f"\n==================== [DEV EMAIL SERVICE] ====================")
print(f"To: {to}")
print(f"From: {sender}")
print(f"Subject: {subject}")
print(f"Body:\n{text}")
print(f"=============================================================\n")
logger.info(f"[DEV EMAIL] Sent simulated email to {to}: {subject}")
return True
try:
msg = MIMEMultipart()
msg["From"] = sender
msg["To"] = to
msg["Subject"] = subject
msg.attach(MIMEText(text, "plain"))
with smtplib.SMTP(self.host, self.port) as server:
if self.tls:
server.starttls()
server.login(self.user, self.password)
server.send_message(msg)
logger.info(f"Email successfully sent to {to}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to}: {e}")
print(f"❌ Email sending error: {e}")
return False
def send_html_message(self, to: str, subject: str, html_content: str, from_email: Optional[str] = None) -> bool:
"""Sends an HTML formatted email."""
sender = from_email or self.from_email
if not self.is_configured():
print(f"\n==================== [DEV EMAIL SERVICE (HTML)] ====================")
print(f"To: {to}")
print(f"Subject: {subject}")
print(f"HTML Body:\n{html_content}")
print(f"===================================================================\n")
return True
try:
msg = MIMEMultipart("alternative")
msg["From"] = sender
msg["To"] = to
msg["Subject"] = subject
msg.attach(MIMEText(html_content, "html"))
with smtplib.SMTP(self.host, self.port) as server:
if self.tls:
server.starttls()
server.login(self.user, self.password)
server.send_message(msg)
return True
except Exception as e:
logger.error(f"Failed to send HTML email to {to}: {e}")
return False
def adduserviaadmin(self, to: str, subject: str, otp_or_url: str):
"""Mirrors Spring Boot EmailService.adduserviaadmin."""
body = f"Your verification code is: {otp_or_url}"
return self.send_simple_message(to, subject, body)
email_service = EmailService()
+58
View File
@@ -0,0 +1,58 @@
import hashlib
import secrets
from datetime import datetime, timedelta
from typing import Optional, Any, Union
import jwt
from app.core.config import settings
def get_password_hash(password: str) -> str:
"""
Hashes a password with PBKDF2-HMAC-SHA256 using Python's standard library.
Requires NO C compilers or external dependencies.
"""
salt = secrets.token_hex(16)
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), 100000)
return f"pbkdf2:sha256:100000${salt}${key.hex()}"
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verifies a plain password against the stored hash."""
if not hashed_password:
return False
if "$" not in hashed_password:
# Fallback comparison for plain text
return plain_password == hashed_password
try:
parts = hashed_password.split("$")
if len(parts) == 3 and parts[0].startswith("pbkdf2:"):
_, salt, key_hex = parts
check_key = hashlib.pbkdf2_hmac("sha256", plain_password.encode("utf-8"), salt.encode("utf-8"), 100000)
return secrets.compare_digest(check_key.hex(), key_hex)
return plain_password == hashed_password
except Exception:
return False
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None, extra_claims: Optional[dict] = None) -> str:
"""Creates a signed JWT token."""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {
"exp": expire,
"sub": str(subject),
"user_id": subject
}
if extra_claims:
to_encode.update(extra_claims)
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def decode_access_token(token: str) -> Optional[dict]:
"""Decodes a JWT token."""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except Exception:
return None
+3
View File
@@ -0,0 +1,3 @@
"""
Azzu Reference Module
"""
+51
View File
@@ -0,0 +1,51 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from typing import List, Dict, Any
from app.core.deps import get_db
from app.io8ref.Azzu.services import Service
# Create two routers or one with route aliases
router = APIRouter(tags=["Azzu Entity CRUD"])
# add data
@router.post("/Azzu")
@router.post("/azzu")
def Savedata(data: Dict[str, Any], db: Session = Depends(get_db)):
saved = Service.Savedata(db, data)
return saved
# get all
@router.get("/Azzu")
@router.get("/azzu")
def getdetails(db: Session = Depends(get_db)):
items = Service.getdetails(db)
return items
# getby id
@router.get("/Azzu/{id}")
@router.get("/azzu/{id}")
def getdetailsbyId(id: int, db: Session = Depends(get_db)):
item = Service.getdetailsbyId(db, id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item
# update by id
@router.put("/Azzu/{id}")
@router.put("/azzu/{id}")
def update(data: Dict[str, Any], id: int, db: Session = Depends(get_db)):
updated = Service.update(db, data, id)
if not updated:
raise HTTPException(status_code=404, detail="Item not found")
return updated
# delete by id
@router.delete("/Azzu/{id}")
@router.delete("/azzu/{id}")
def delete_by_id(id: int, db: Session = Depends(get_db)):
success = Service.delete_by_id(db, id)
if not success:
raise HTTPException(status_code=404, detail="Item not found")
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": "Deleted successfully"})
+14
View File
@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, Text, Float, Date, DateTime, Boolean
from datetime import datetime
from app.core.database import Base
class Azzu(Base):
__tablename__ = "azzu"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
# Reference demo fields
name = Column(String(255), nullable=True)
description = Column(Text, nullable=True)
status = Column(String(100), default="ACTIVE")
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+22
View File
@@ -0,0 +1,22 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime, date
class AzzuBase(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = "ACTIVE"
class AzzuCreate(AzzuBase):
pass
class AzzuUpdate(AzzuBase):
pass
class AzzuResponse(AzzuBase):
id: int
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
+46
View File
@@ -0,0 +1,46 @@
from sqlalchemy.orm import Session
from typing import List, Optional
from app.io8ref.Azzu.models import Azzu
from app.io8ref.Azzu.schemas import AzzuCreate, AzzuUpdate
class AzzuService:
def Savedata(self, db: Session, data: dict) -> Azzu:
"""Saves a new Azzu record."""
# Filter out id if present
data_dict = {k: v for k, v in data.items() if k != "id" and hasattr(Azzu, k)}
obj = Azzu(**data_dict)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def getdetails(self, db: Session) -> List[Azzu]:
"""Returns all Azzu records."""
return db.query(Azzu).all()
def getdetailsbyId(self, db: Session, id_val: int) -> Optional[Azzu]:
"""Returns an Azzu record by ID."""
return db.query(Azzu).filter(Azzu.id == id_val).first()
def update(self, db: Session, data: dict, id_val: int) -> Optional[Azzu]:
"""Updates an Azzu record by ID."""
obj = self.getdetailsbyId(db, id_val)
if not obj:
return None
for key, value in data.items():
if key != "id" and hasattr(obj, key):
setattr(obj, key, value)
db.commit()
db.refresh(obj)
return obj
def delete_by_id(self, db: Session, id_val: int) -> bool:
"""Deletes an Azzu record by ID."""
obj = self.getdetailsbyId(db, id_val)
if obj:
db.delete(obj)
db.commit()
return True
return False
Service = AzzuService()
+156
View File
@@ -0,0 +1,156 @@
import os
import importlib
import pkgutil
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.database import Base, engine, SessionLocal
from app.core.security import get_password_hash
from app.auth.models import AppUser, Role
from app.menu.models import GrpMenuAccess
from app.auth.router import router as auth_router
from app.menu.router import router as menu_router
def init_db_and_seed():
"""Creates database tables and seeds initial administrator & menu access."""
Base.metadata.create_all(bind=engine)
# Auto-migration safeguard for existing databases
try:
from sqlalchemy import text
with engine.connect() as conn:
conn.execute(text("ALTER TABLE app_users ADD COLUMN random_no VARCHAR(50);"))
conn.commit()
except Exception:
pass
db: Session = SessionLocal()
try:
# 1. Seed Roles
admin_role = db.query(Role).filter(Role.name == "ROLE_ADMIN").first()
if not admin_role:
admin_role = Role(name="ROLE_ADMIN", description="Admin")
db.add(admin_role)
user_role = db.query(Role).filter(Role.name == "ROLE_USER").first()
if not user_role:
user_role = Role(name="ROLE_USER", description="User")
db.add(user_role)
db.commit()
# 2. Seed Admin User
admin_user = db.query(AppUser).filter(AppUser.email == "admin@cns.com").first()
if not admin_user:
admin_user = AppUser(
email="admin@cns.com",
username="admin",
password=get_password_hash("admin123"),
full_name="Admin User",
first_name="Admin",
last_name="User",
status="ACTIVE",
user_grp_id=1,
is_active=True
)
admin_user.roles.append(admin_role)
db.add(admin_user)
db.commit()
# 3. Seed Default Menu Items
root_menu = db.query(GrpMenuAccess).filter(GrpMenuAccess.menu_id == 0, GrpMenuAccess.menu_item_id == 1).first()
if not root_menu:
root_menu = GrpMenuAccess(
menu_id=0,
menu_item_id=1,
menu_item_desc="Core Modules",
module_name="Core",
main_menu_action_name="/cns-portal",
main_menu_icon_name="cog",
item_seq=1,
status="A",
role="Admin",
usr_grp=1
)
db.add(root_menu)
db.commit()
# Submenu pointing to Azzu entity
sub_menu = GrpMenuAccess(
menu_id=1,
menu_item_id=2,
menu_item_desc="Azzu Management",
module_name="Core",
main_menu_action_name="/cns-portal/azzu",
main_menu_icon_name="file",
item_seq=1,
status="A",
role="Admin",
usr_grp=1
)
db.add(sub_menu)
db.commit()
finally:
db.close()
def auto_discover_entity_routers(app: FastAPI):
"""
Automatically discovers and includes any entity controllers located under app/io8ref/*/controllers.py
"""
io8ref_dir = os.path.join(os.path.dirname(__file__), "io8ref")
if not os.path.exists(io8ref_dir):
return
for item in os.listdir(io8ref_dir):
entity_dir = os.path.join(io8ref_dir, item)
if os.path.isdir(entity_dir) and os.path.exists(os.path.join(entity_dir, "controllers.py")):
try:
module = importlib.import_module(f"app.io8ref.{item}.controllers")
if hasattr(module, "router"):
app.include_router(module.router)
print(f"✓ Registered Entity Router: {item}")
except Exception as e:
print(f"✗ Failed to load entity router {item}: {e}")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup:
init_db_and_seed()
yield
# Shutdown
app = FastAPI(
title=settings.PROJECT_NAME,
description="Drop-in Python FastAPI Base Project compatible with Angular Clarity, React, and Code Extractor V2",
version="1.0.0",
lifespan=lifespan
)
# CORS Middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Core Routers
app.include_router(auth_router)
app.include_router(menu_router)
# Dynamically Register Generated Entities
auto_discover_entity_routers(app)
@app.get("/", tags=["Health"])
@app.get("/health", tags=["Health"])
def health_check():
return {
"status": "UP",
"service": settings.PROJECT_NAME,
"docs_url": "/docs"
}
+27
View File
@@ -0,0 +1,27 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from datetime import datetime
from app.core.database import Base
class GrpMenuAccess(Base):
__tablename__ = "grp_menu_access"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
menu_id = Column(Integer, default=0, index=True) # 0 represents root menu
menu_item_id = Column(Integer, default=1, index=True)
menu_item_desc = Column(String(255), nullable=True)
module_name = Column(String(255), nullable=True)
main_menu_action_name = Column(String(255), nullable=True) # Angular route path, e.g. /cns-portal/azzu
main_menu_icon_name = Column(String(255), nullable=True) # Icon name, e.g. vm-bug or cog
item_seq = Column(Integer, default=1)
status = Column(String(50), default="A")
role = Column(String(100), default="Admin")
usr_grp = Column(Integer, default=1)
m_create = Column(String(50), default="Y")
m_edit = Column(String(50), default="Y")
m_query = Column(String(50), default="Y")
m_delete = Column(String(50), default="Y")
m_export = Column(String(50), default="Y")
m_visible = Column(String(50), default="Y")
is_disable = Column(String(50), default="N")
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+80
View File
@@ -0,0 +1,80 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Any, Optional
from app.core.deps import get_db, get_current_user_token
from app.menu.models import GrpMenuAccess
router = APIRouter(tags=["Menu & Navigation"])
@router.get("/fndMenu/menuloadbyuser")
@router.get("/MenuAccess/set_menu_for_user")
def menu_load_by_user(
db: Session = Depends(get_db),
token_payload: Optional[dict] = Depends(get_current_user_token)
):
"""
Returns menu hierarchy matching Spring Boot's LoadMenuController /menuloadbyuser.
Fetches all root menu items (menu_id = 0) and attaches their corresponding submenus.
"""
usr_grp = 1
if token_payload:
usr_grp = token_payload.get("user_grp", 1)
# 1. Fetch root menus (menu_id == 0)
roots = db.query(GrpMenuAccess).filter(
GrpMenuAccess.menu_id == 0,
GrpMenuAccess.usr_grp == usr_grp,
GrpMenuAccess.status == "A"
).order_by(GrpMenuAccess.item_seq).all()
response_list = []
for r in roots:
# 2. Fetch submenus for each root
submenus = db.query(GrpMenuAccess).filter(
GrpMenuAccess.menu_id == r.menu_item_id,
GrpMenuAccess.usr_grp == usr_grp,
GrpMenuAccess.status == "A"
).order_by(GrpMenuAccess.item_seq).all()
sub_list = []
for s in submenus:
sub_list.append({
"id": s.id,
"menuId": s.menu_id,
"menuItemId": s.menu_item_id,
"menuItemDesc": s.menu_item_desc,
"moduleName": s.module_name,
"main_menu_action_name": s.main_menu_action_name,
"main_menu_icon_name": s.main_menu_icon_name,
"itemSeq": s.item_seq,
"status": s.status,
"role": s.role,
"mCreate": s.m_create,
"mEdit": s.m_edit,
"mQuery": s.m_query,
"mDelete": s.m_delete,
"mexport": s.m_export,
"mVisible": s.m_visible
})
response_list.append({
"id": r.id,
"menuId": r.menu_id,
"menuItemId": r.menu_item_id,
"menuItemDesc": r.menu_item_desc,
"moduleName": r.module_name,
"main_menu_action_name": r.main_menu_action_name,
"main_menu_icon_name": r.main_menu_icon_name,
"itemSeq": r.item_seq,
"status": r.status,
"role": r.role,
"subMenus": sub_list
})
return response_list
@router.get("/fndMenu/listofusrgrp")
def list_of_usr_grp(db: Session = Depends(get_db)):
"""Returns all distinct user groups."""
return [{"usrGrp": 1, "groupName": "Admin Group"}]
+18
View File
@@ -0,0 +1,18 @@
from pydantic import BaseModel
from typing import Optional, List, Any
class MenuItemResponse(BaseModel):
id: Optional[int] = None
menu_id: int
menu_item_id: int
menu_item_desc: Optional[str] = None
module_name: Optional[str] = None
main_menu_action_name: Optional[str] = None
main_menu_icon_name: Optional[str] = None
item_seq: Optional[int] = 1
status: Optional[str] = "A"
role: Optional[str] = "Admin"
sub_menus: List[Any] = []
class Config:
from_attributes = True
+9
View File
@@ -0,0 +1,9 @@
fastapi>=0.110.0
uvicorn>=0.29.0
pydantic>=2.6.0
pydantic-settings>=2.2.0
sqlalchemy>=2.0.0
pymysql>=1.1.0
pyjwt>=2.8.0
python-multipart>=0.0.9
python-dotenv>=1.0.1
+7
View File
@@ -0,0 +1,7 @@
import uvicorn
from app.core.config import settings
if __name__ == "__main__":
print(f"🚀 Starting {settings.PROJECT_NAME} on http://localhost:{settings.PORT}")
print(f"📖 Interactive Swagger Docs available at: http://localhost:{settings.PORT}/docs")
uvicorn.run("app.main:app", host="0.0.0.0", port=settings.PORT, reload=settings.DEBUG)