Created basic backend structure, auth and CRUD endpoints.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
|||||||
.devcontainer
|
.devcontainer
|
||||||
venv/
|
venv/
|
||||||
|
.vscode/
|
||||||
|
files/*
|
||||||
|
__pycache__/
|
||||||
75
app/connections.py
Normal file
75
app/connections.py
Normal 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
76
app/users.py
Normal 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
40
core/dependencies.py
Normal 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
9
core/enums.py
Normal 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
19
core/exceptions.py
Normal 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
44
core/scripts.py
Normal 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
62
data/crud.py
Normal 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
22
data/db.py
Normal 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
29
data/models.py
Normal 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
59
data/schemas.py
Normal 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
2
dbs/mysql.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import aiomysql
|
||||||
|
|
||||||
27
main.py
27
main.py
@@ -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)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
fastapi
|
fastapi[all]
|
||||||
uvicorn
|
uvicorn
|
||||||
aiomysql
|
aiomysql
|
||||||
websockets
|
websockets
|
||||||
pydantic
|
pydantic
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
aiosqlite
|
||||||
|
sqlalchemy
|
||||||
Reference in New Issue
Block a user