adding base project
This commit is contained in:
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
AuthSec FastAPI Application Package
|
||||
"""
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Azzu Reference Module
|
||||
"""
|
||||
@@ -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"})
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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"}]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user