feat:
--FastAPI added
This commit is contained in:
parent
6907288be7
commit
c52f1eb478
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
venv/
|
venv/
|
||||||
|
.env
|
||||||
|
|
||||||
|
|
||||||
# Ignore VSCode settings (optional)
|
# Ignore VSCode settings (optional)
|
||||||
|
|||||||
BIN
__pycache__/database.cpython-312.pyc
Normal file
BIN
__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-312.pyc
Normal file
BIN
__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/models.cpython-312.pyc
Normal file
BIN
__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/schemas.cpython-312.pyc
Normal file
BIN
__pycache__/schemas.cpython-312.pyc
Normal file
Binary file not shown.
36
database.py
Normal file
36
database.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL")
|
||||||
|
|
||||||
|
if not DATABASE_URL:
|
||||||
|
raise RuntimeError("DATABASE_URL is not configured")
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
DATABASE_URL,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
SessionLocal = sessionmaker(
|
||||||
|
bind=engine,
|
||||||
|
autoflush=False,
|
||||||
|
autocommit=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
6
experimental/main.py
Normal file
6
experimental/main.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
ADMIN_API_KEY = os.getenv("ADMIN_API_KEY")
|
||||||
@ -23,7 +23,7 @@ def stop_stream(signum=None, frame=None):
|
|||||||
running = False
|
running = False
|
||||||
print("\nStopping screen stream safely...")
|
print("\nStopping screen stream safely...")
|
||||||
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, stop_stream)
|
signal.signal(signal.SIGINT, stop_stream)
|
||||||
signal.signal(signal.SIGTERM, stop_stream)
|
signal.signal(signal.SIGTERM, stop_stream)
|
||||||
|
|
||||||
214
main.py
Normal file
214
main.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import os
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import Depends, FastAPI, Header, HTTPException, Response, status
|
||||||
|
from pwdlib import PasswordHash
|
||||||
|
from sqlalchemy import or_, select
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from database import Base, engine, get_db
|
||||||
|
from models import User
|
||||||
|
from schemas import UserCreate, UserResponse
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="FastAPI User Admin",
|
||||||
|
description="Simple PostgreSQL-backed user administration API",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
password_hash = PasswordHash.recommended()
|
||||||
|
|
||||||
|
ADMIN_API_KEY = os.getenv("ADMIN_API_KEY")
|
||||||
|
print(f"ADMIN_API_KEY: {ADMIN_API_KEY}")
|
||||||
|
if not ADMIN_API_KEY:
|
||||||
|
raise RuntimeError("ADMIN_API_KEY is not configured")
|
||||||
|
|
||||||
|
|
||||||
|
# Create tables when the application starts.
|
||||||
|
# For larger projects, use Alembic migrations instead.
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_admin_key(
|
||||||
|
x_admin_key: Annotated[str | None, Header()] = None,
|
||||||
|
) -> None:
|
||||||
|
if x_admin_key != ADMIN_API_KEY:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid or missing admin API key",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
AdminDependency = Annotated[None, Depends(verify_admin_key)]
|
||||||
|
DatabaseDependency = Annotated[Session, Depends(get_db)]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root():
|
||||||
|
return {
|
||||||
|
"message": "FastAPI admin service is running",
|
||||||
|
"admin_docs": "/docs",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/admin/users",
|
||||||
|
response_model=UserResponse,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
def create_user(
|
||||||
|
user_data: UserCreate,
|
||||||
|
db: DatabaseDependency,
|
||||||
|
_: AdminDependency,
|
||||||
|
):
|
||||||
|
existing_user = db.scalar(
|
||||||
|
select(User).where(
|
||||||
|
or_(
|
||||||
|
User.username == user_data.username,
|
||||||
|
User.email == user_data.email,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_user:
|
||||||
|
if existing_user.username == user_data.username:
|
||||||
|
detail = "Username already exists"
|
||||||
|
else:
|
||||||
|
detail = "Email address already exists"
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_user = User(
|
||||||
|
username=user_data.username,
|
||||||
|
email=user_data.email,
|
||||||
|
hashed_password=password_hash.hash(user_data.password),
|
||||||
|
is_admin=user_data.is_admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(new_user)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_user)
|
||||||
|
except IntegrityError:
|
||||||
|
db.rollback()
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Username or email already exists",
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_user
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/admin/users",
|
||||||
|
response_model=list[UserResponse],
|
||||||
|
)
|
||||||
|
def list_users(
|
||||||
|
db: DatabaseDependency,
|
||||||
|
_: AdminDependency,
|
||||||
|
):
|
||||||
|
users = db.scalars(
|
||||||
|
select(User).order_by(User.id)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
return list(users)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/admin/users/{user_id}",
|
||||||
|
response_model=UserResponse,
|
||||||
|
)
|
||||||
|
def get_user(
|
||||||
|
user_id: int,
|
||||||
|
db: DatabaseDependency,
|
||||||
|
_: AdminDependency,
|
||||||
|
):
|
||||||
|
user = db.get(User, user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch(
|
||||||
|
"/admin/users/{user_id}/disable",
|
||||||
|
response_model=UserResponse,
|
||||||
|
)
|
||||||
|
def disable_user(
|
||||||
|
user_id: int,
|
||||||
|
db: DatabaseDependency,
|
||||||
|
_: AdminDependency,
|
||||||
|
):
|
||||||
|
user = db.get(User, user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
user.is_active = False
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch(
|
||||||
|
"/admin/users/{user_id}/enable",
|
||||||
|
response_model=UserResponse,
|
||||||
|
)
|
||||||
|
def enable_user(
|
||||||
|
user_id: int,
|
||||||
|
db: DatabaseDependency,
|
||||||
|
_: AdminDependency,
|
||||||
|
):
|
||||||
|
user = db.get(User, user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
user.is_active = True
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete(
|
||||||
|
"/admin/users/{user_id}",
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
def delete_user(
|
||||||
|
user_id: int,
|
||||||
|
db: DatabaseDependency,
|
||||||
|
_: AdminDependency,
|
||||||
|
):
|
||||||
|
user = db.get(User, user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
db.delete(user)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
52
models.py
Normal file
52
models.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, DateTime, String, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
primary_key=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
username: Mapped[str] = mapped_column(
|
||||||
|
String(50),
|
||||||
|
unique=True,
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
email: Mapped[str] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
unique=True,
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
hashed_password: Mapped[str] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_active: Mapped[bool] = mapped_column(
|
||||||
|
Boolean,
|
||||||
|
default=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_admin: Mapped[bool] = mapped_column(
|
||||||
|
Boolean,
|
||||||
|
default=False,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
sqlalchemy
|
||||||
|
psycopg[binary]
|
||||||
|
python-dotenv
|
||||||
|
pwdlib[argon2]
|
||||||
|
email-validator
|
||||||
31
schemas.py
Normal file
31
schemas.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
username: str = Field(
|
||||||
|
min_length=3,
|
||||||
|
max_length=50,
|
||||||
|
pattern=r"^[a-zA-Z0-9_.-]+$",
|
||||||
|
)
|
||||||
|
|
||||||
|
email: EmailStr
|
||||||
|
|
||||||
|
password: str = Field(
|
||||||
|
min_length=8,
|
||||||
|
max_length=128,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_admin: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
email: EmailStr
|
||||||
|
is_active: bool
|
||||||
|
is_admin: bool
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
0
static/index.html
Normal file
0
static/index.html
Normal file
Loading…
Reference in New Issue
Block a user