add basic db and auth functionality and dev container

This commit is contained in:
maxid
2025-02-13 21:03:51 +01:00
parent f22eeef608
commit ab6fc52e42
13 changed files with 363 additions and 13 deletions

15
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.11-bullseye
ENV PYTHONUNBUFFERED 1
# [Optional] If your requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

View File

@@ -0,0 +1,28 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/postgres
{
"name": "Python 3 & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
// "forwardPorts": [5000, 5432],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pip install --user -r ./MediaManager/src/requirements.txt",
// Configure tool-specific properties.
"customizations" : {
"jetbrains" : {
"backend" : "PyCharm"
}
},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@@ -0,0 +1,35 @@
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
# volumes:
# - ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
volumes:
postgres-data:

0
MediaManager/res/.env Normal file
View File

View File

@@ -1,13 +0,0 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/hello/{name}")
async def say_hello(name: str):
return {"message": f"Hello {name}"}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,9 @@
from fastapi import Depends, APIRouter
from fastapi.security import OpenIdConnect
oidc = OpenIdConnect(openIdConnectUrl="http://localhost:8080/realms/tools/.well-known/openid-configuration")
app = APIRouter()
@app.get("/foo")
async def bar(token = Depends(oidc)):
return token

View File

@@ -0,0 +1,110 @@
from datetime import datetime, timedelta, timezone
from typing import Annotated
import jwt
from fastapi import Depends, FastAPI, HTTPException, status, APIRouter
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from passlib.context import CryptContext
from pydantic import BaseModel
import database
from database import UserInternal, User
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = APIRouter()
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def authenticate_user(email: str, password: str) -> UserInternal:
user = database.get_user(email)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except InvalidTokenError:
raise credentials_exception
user = database.get_user(token_data.username)
if user is None:
raise credentials_exception
return user
@app.post("/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> Token:
print("post:",form_data.username, form_data.password)
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")

View File

@@ -0,0 +1,114 @@
import logging
import os
import sys
from abc import ABC, abstractmethod
from logging import getLogger
from uuid import uuid4
import psycopg
from pydantic import BaseModel
log = getLogger(__name__)
log.addHandler(logging.StreamHandler(sys.stdout))
log.level = logging.DEBUG
class User(BaseModel):
name: str
lastname: str
email: str
class UserInternal(User):
id: str = str(uuid4())
hashed_password: str
class Database(ABC):
"""
Database context manager
"""
def __init__(self, driver) -> None:
self.driver = driver
@abstractmethod
def connect_to_database(self):
raise NotImplementedError()
def __enter__(self):
self.connection = self.connect_to_database()
return self
def __exit__(self, exception_type, exc_val, traceback):
self.connection.close()
class PgDatabase(Database):
"""PostgreSQL Database context manager using psycopg"""
def __init__(self) -> None:
self.driver = psycopg
super().__init__(self.driver)
def connect_to_database(self):
return self.driver.connect(
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT"),
user=os.getenv("DB_USERNAME"),
password=os.getenv("DB_PASSWORD"),
dbname=os.getenv("DB_NAME")
)
def init_db():
with PgDatabase() as db:
db.connection.execute("""
CREATE TABLE IF NOT EXISTS users (
id TEXT NOT NULL PRIMARY KEY,
lastname TEXT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
hashed_password TEXT NOT NULL
);
""")
log.info("User Table initialized successfully")
def drop_tables() -> None:
with PgDatabase() as db:
db.connection.execute("DROP TABLE IF EXISTS users CASCADE;")
log.info("User Table dropped")
def create_user(user: UserInternal) -> bool:
with PgDatabase() as db:
try:
db.connection.execute(
"""
INSERT INTO users (id, name, lastname, email, hashed_password)
VALUES (%s, %s, %s, %s, %s)
""",
(user.id, user.name, user.lastname, user.email, user.hashed_password)
)
except psycopg.errors.UniqueViolation:
return False
log.info("User inserted successfully")
log.debug(f"User {user.model_dump()} created successfully")
return True
def get_user(email: str) -> UserInternal:
with PgDatabase() as db:
result = db.connection.execute(
"SELECT id, name, lastname, email, hashed_password FROM users WHERE email=%s",
(email,)
).fetchone()
if result is None:
return None
user = UserInternal.model_construct(**dict(zip(["id", "name", "lastname", "email", "hashed_password"], result)))
log.debug(f"User {user.model_dump()} retrieved successfully")
return user

10
MediaManager/src/main.py Normal file
View File

@@ -0,0 +1,10 @@
from fastapi import FastAPI
import database
from routers import users
from auth import password
app = FastAPI()
app.include_router(users.router, tags=["users"])
app.include_router(password.app, tags=["authentication"])
database.__init__()

Binary file not shown.

View File

View File

@@ -0,0 +1,41 @@
import logging
from fastapi import APIRouter
from fastapi import Depends
import database
from auth.password import authenticate_user, get_password_hash
from database import User, UserInternal
router = APIRouter(
prefix="/users",
)
class CreateUser(User):
password: str
log = logging.getLogger(__file__)
@router.post("/", response_model=User)
async def create_user(user: CreateUser):
internal_user = UserInternal(name=user.name, lastname=user.lastname, email=user.email,
hashed_password=get_password_hash(user.password))
database.create_user(internal_user)
return user
@router.get("/me/", response_model=User)
async def read_users_me(
current_user: User = Depends(authenticate_user),
):
return current_user
@router.get("/me/items/")
async def read_own_items(
current_user: User = Depends(authenticate_user),
):
return [{"item_id": "Foo", "owner": current_user.username}]