Created basic backend structure, auth and CRUD endpoints.

This commit is contained in:
2025-02-22 13:42:10 +03:00
parent ed5fac3432
commit cabcf837f9
14 changed files with 470 additions and 3 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
.devcontainer .devcontainer
venv/ venv/
.vscode/
files/*
__pycache__/

75
app/connections.py Normal file
View File

@@ -0,0 +1,75 @@
from fastapi.routing import APIRouter
from data.schemas import Connection, ConnectionCreate, ConnectionUpdate
from fastapi import Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from data.crud import (
read_connection,
read_all_connections,
create_connection,
update_connection,
delete_connection,
)
from core.dependencies import get_db, get_current_user, get_admin_user
connections_router = APIRouter()
@connections_router.post("/", status_code=status.HTTP_201_CREATED)
async def create_connection_endpoint(
connection: ConnectionCreate,
db: AsyncSession = Depends(get_db),
admin=Depends(get_admin_user),
) -> Connection:
return await create_connection(db=db, connection=connection, user_id=admin.id)
@connections_router.get(
"/",
response_model=list[Connection],
dependencies=[Depends(get_current_user)],
)
async def read_connections_endpoint(
db: AsyncSession = Depends(get_db),
):
db_connection = await read_all_connections(db)
return db_connection
@connections_router.get(
"/{connection_id}",
response_model=Connection,
dependencies=[Depends(get_current_user)],
)
async def read_connection_endpoint(connection_id: int, db: AsyncSession = Depends(get_db)):
db_connection = await read_connection(db, connection_id)
if db_connection is None:
raise HTTPException(status_code=404, detail="Connection not found")
return db_connection
@connections_router.put(
"/{connection_id}",
response_model=Connection,
dependencies=[Depends(get_admin_user)],
)
async def update_connection_endpoint(
connection_id: int, connection: ConnectionUpdate, db: AsyncSession = Depends(get_db)
):
db_connection = await update_connection(
db=db, connection_id=connection_id, connection=connection
)
if db_connection is None:
raise HTTPException(status_code=404, detail="Connection not found")
return db_connection
@connections_router.delete(
"/{connection_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(get_admin_user)],
)
async def delete_connection_endpoint(connection_id: int, db: AsyncSession = Depends(get_db)):
db_connection = await delete_connection(db=db, connection_id=connection_id)
if db_connection is None:
raise HTTPException(status_code=404, detail="Connection not found")
return None

76
app/users.py Normal file
View File

@@ -0,0 +1,76 @@
from fastapi.routing import APIRouter
from data.schemas import UserOut, UserInDBBase, UserCreate
from data.models import UserRole
from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from data.crud import read_all_users, read_user, create_user, delete_user
from core.dependencies import get_db, get_current_user, get_admin_user
from sqlalchemy.exc import IntegrityError
from core.exceptions import ObjectNotFoundInDB, UserNotFound
from core.scripts import create_secret
users_router = APIRouter()
@users_router.get("/me")
async def get_me(user=Depends(get_current_user)) -> UserOut:
return user
@users_router.get(
"/",
dependencies=[Depends(get_current_user)],
)
async def get_all_users_endpoint(db=Depends(get_db)) -> list[UserOut]:
return await read_all_users(db=db)
@users_router.post(
"/", dependencies=[Depends(get_current_user)], status_code=status.HTTP_201_CREATED
)
async def create_user_endpoint(
user_create: UserCreate, db=Depends(get_db)
) -> UserInDBBase:
try:
return await create_user(user=user_create, db=db)
except IntegrityError:
raise HTTPException(
status_code=400,
detail={
"message": "This username is already taken.",
"code": "duplicated-username",
},
)
@users_router.post('/update-my-api_key/', status_code=status.HTTP_204_NO_CONTENT)
async def update_user_own_api_key(user=Depends(get_current_user), db=Depends(get_db)):
if user.role == UserRole.admin:
raise HTTPException(status_code=400, detail={
'message': 'Admins can\'t use this endpoint to update their API key.',
'code': 'admin-not-allowed'
})
user.api_key = create_secret()
db.add(user)
await db.commit()
await db.refresh(user)
@users_router.post('/update-user-api_key/', status_code=status.HTTP_202_ACCEPTED, dependencies=[Depends(get_admin_user)])
async def update_user_own_api_key(user_id:int, db=Depends(get_db)) -> UserInDBBase:
user = await read_user(db=db, user_id=user_id)
if user is None:
raise UserNotFound()
user.api_key = create_secret()
db.add(user)
await db.commit()
await db.refresh(user)
return user
@users_router.delete(
"/", dependencies=[Depends(get_admin_user)], status_code=status.HTTP_204_NO_CONTENT
)
async def delete_user_endpoint(user_id: int, db=Depends(get_db)):
try:
await delete_user(db=db, user_id=user_id)
return None
except ObjectNotFoundInDB:
raise UserNotFound()

40
core/dependencies.py Normal file
View File

@@ -0,0 +1,40 @@
# dependencies.py
from fastapi import Depends, HTTPException, status, Security
from fastapi.security import APIKeyHeader
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from data.db import SessionLocal
from data.models import User, UserRole
from pydantic import BaseModel
# class UserInDB(User):
# hashed_password: str
async def get_db():
async with SessionLocal() as session:
yield session
API_KEY_NAME = "Authorization"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
async def get_current_user(db: AsyncSession = Depends(get_db), api_key:str = Security(api_key_header)):
if api_key_header is None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="API key missing"
)
if not api_key:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="API key missing, provide it in the header value [Authorization]"
)
user = await db.execute(select(User).filter(User.api_key == api_key))
user = user.scalars().first()
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
return user
async def get_admin_user(current_user: User = Depends(get_current_user)):
if current_user.role != UserRole.admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
return current_user

9
core/enums.py Normal file
View File

@@ -0,0 +1,9 @@
import enum
class ConnectionTypes(str, enum.Enum):
mysql = "mysql"
postgresql = 'postgresql'
class UserRole(enum.Enum):
admin = "admin"
user = "user"

19
core/exceptions.py Normal file
View File

@@ -0,0 +1,19 @@
from fastapi import HTTPException
class ObjectNotFoundInDB(Exception):
def __init__(self, *args):
super().__init__(*args)
class UserNotFound(HTTPException):
def __init__(
self,
status_code=404,
detail={
"message": "Didn't find a user with the provided id.",
"code": "user-not-found",
},
headers=None,
):
super().__init__(status_code, detail, headers)

44
core/scripts.py Normal file
View File

@@ -0,0 +1,44 @@
# add_user.py
import asyncio
import secrets
from sqlalchemy.future import select
from sqlalchemy.exc import IntegrityError
from getpass import getpass
from data.db import engine, SessionLocal
from data.models import Base, User, UserRole
def create_secret():
return secrets.token_hex(32)
async def create_user():
async with SessionLocal() as session:
username = input("Enter username: ").strip()
role_input = input("Enter role (admin/user): ").strip().lower()
print('\n')
if role_input not in UserRole._value2member_map_:
print("> Invalid role. Please enter 'admin' or 'user'.")
return
role = UserRole(role_input)
# Check if username already exists
result = await session.execute(select(User).filter_by(username=username))
existing_user = result.scalars().first()
if existing_user:
print(f"> Username '{username}' is already taken.")
return
# Create new user
api_key = create_secret()
new_user = User(username=username, role=role, api_key=api_key)
session.add(new_user)
await session.commit()
await session.refresh(new_user)
print(f"> User '{username}' with role '{role.value}' created successfully.")
print(f"> API Key: {api_key}")
if __name__ == "__main__":
asyncio.run(create_user())

62
data/crud.py Normal file
View File

@@ -0,0 +1,62 @@
# crud.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from data.models import Connection, User
from data.schemas import ConnectionCreate, ConnectionUpdate, UserCreate
from core.exceptions import ObjectNotFoundInDB
async def read_user(db:AsyncSession, user_id:int):
result = await db.execute(select(User).filter(User.id == user_id))
return result.scalars().first()
async def read_all_users(db:AsyncSession):
result = await db.execute(select(User))
return result.scalars().all()
async def create_user(db: AsyncSession, user: UserCreate):
from core.scripts import create_secret
db_user = User(**user.model_dump(), api_key=create_secret())
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user
async def delete_user(db: AsyncSession, user_id: int):
db_user = await read_user(db, user_id)
if db_user:
await db.delete(db_user)
await db.commit()
return db_user
else:
raise ObjectNotFoundInDB
async def read_connection(db: AsyncSession, connection_id: int):
result = await db.execute(select(Connection).filter(Connection.id == connection_id))
return result.scalars().first()
async def read_all_connections(db: AsyncSession):
result = await db.execute(select(Connection))
return result.scalars().all()
async def create_connection(db: AsyncSession, connection: ConnectionCreate, user_id: int):
db_connection = Connection(**connection.model_dump(), owner_id=user_id)
db.add(db_connection)
await db.commit()
await db.refresh(db_connection)
return db_connection
async def update_connection(db: AsyncSession, connection_id: int, connection: ConnectionUpdate):
db_connection = await read_connection(db, connection_id)
if db_connection:
for key, value in connection.model_dump().items():
setattr(db_connection, key, value)
await db.commit()
await db.refresh(db_connection)
return db_connection
async def delete_connection(db: AsyncSession, connection_id: int):
db_connection = await read_connection(db, connection_id)
if db_connection:
await db.delete(db_connection)
await db.commit()
return db_connection

22
data/db.py Normal file
View File

@@ -0,0 +1,22 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker, declarative_base
DATABASE_URL = "sqlite+aiosqlite:///files/db.sqlite"
engine = create_async_engine(DATABASE_URL, echo=False)
SessionLocal = sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False,
)
Base = declarative_base()
import logging
logging.basicConfig()
logging.getLogger('sqlalchemy').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.engine').disabled = True

29
data/models.py Normal file
View File

@@ -0,0 +1,29 @@
# models.py
from sqlalchemy import Column, Integer, String, Enum, ForeignKey
from sqlalchemy.orm import relationship
from data.db import Base
from core.enums import ConnectionTypes, UserRole
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True, nullable=False)
role = Column(Enum(UserRole), default=UserRole.user, nullable=False)
api_key = Column(String, unique=True, nullable=False)
# repos = relationship("Connection", back_populates="owner")
class Connection(Base):
__tablename__ = "connections"
id = Column(Integer, primary_key=True, index=True)
db_name = Column(String, nullable=False)
type = Column(Enum(ConnectionTypes), nullable=False)
host = Column(String)
port = Column(Integer)
username = Column(String)
password = Column(String)
owner_id = Column(Integer, ForeignKey("users.id"))
# owner = relationship("User", back_populates="connections")

59
data/schemas.py Normal file
View File

@@ -0,0 +1,59 @@
# schemas.py
from pydantic import BaseModel
from typing import Optional
from core.enums import ConnectionTypes, UserRole
class ConnectionBase(BaseModel):
db_name: str
type: ConnectionTypes
host: str
port: int
username: str
password: str
class ConnectionCreate(ConnectionBase):
pass
class ConnectionUpdate(ConnectionBase):
pass
class ConnectionInDBBase(ConnectionBase):
id: int
owner_id: int
class Config:
from_attributes = True
class Connection(ConnectionInDBBase):
pass
class UserBase(BaseModel):
username: str
class UserCreate(UserBase):
role: UserRole
class UserOut(UserBase):
id: int
role: UserRole
class UserInDBBase(UserBase):
id: int
role: UserRole
api_key: str
class Config:
from_attributes = True
class User(UserInDBBase):
pass

2
dbs/mysql.py Normal file
View File

@@ -0,0 +1,2 @@
import aiomysql

27
main.py
View File

@@ -1 +1,26 @@
print(__file__) from fastapi import FastAPI
from data.db import engine, Base
from app.connections import connections_router
from app.users import users_router
app = FastAPI()
app.include_router(router=users_router, prefix="/users", tags=["Users"])
app.include_router(
router=connections_router, prefix="/connections", tags=["Connections"]
)
# @app.on_event("startup")
async def startup():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# import asyncio
# asyncio.run(startup())
# import uvicorn
# uvicorn.run(app=app)

View File

@@ -1,6 +1,8 @@
fastapi fastapi[all]
uvicorn uvicorn
aiomysql aiomysql
websockets websockets
pydantic pydantic
python-dotenv python-dotenv
aiosqlite
sqlalchemy