This commit is contained in:
2026-05-12 16:00:12 +02:00
parent 138b87595c
commit 7e4bb9f726
9 changed files with 117 additions and 37 deletions
+12 -15
View File
@@ -6,20 +6,17 @@
"image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie",
"features": {
"ghcr.io/devcontainer-community/devcontainer-features/astral.sh-uv:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.black-formatter",
"ms-python.isort",
"charliermarsh.ruff"
]
}
}
// 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.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip3 install --user -r requirements.txt",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
+3
View File
@@ -0,0 +1,3 @@
{
"python-envs.defaultEnvManager": "ms-python.python:venv"
}
+2 -5
View File
@@ -1,12 +1,9 @@
from fastapi import FastAPI
from .routes import user, login
from .models import engine
from .routes import user, login, posts
app = FastAPI()
@app.get("/")
async def root() -> str:
return "Hello World"
app.include_router(user.router)
app.include_router(login.router)
app.include_router(posts.router)
+14 -1
View File
@@ -12,9 +12,22 @@ def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(securit
with Session(engine) as session:
query = select(User).where(User.username == payload["username"]).limit(1)
user: User = session.exec(query).first()
if user == None:
if user is None:
raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED,
detail="Credenciales invalidas"
)
if user.password_version != payload["pwd_v"]:
raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED,
detail = "Credenciales invalidas"
)
return user
def get_staff_user(user: User = Depends(get_current_user)) -> User:
if not user.is_staff:
raise HTTPException(
status_code = status.HTTP_403_FORBIDDEN,
detail = "This user needs to be an Staff to access this resource"
)
return user
+9
View File
@@ -16,6 +16,7 @@ class User(SQLModel, table=True):
username: str = Field(index=True, unique=True)
email: str = Field(index=True, unique=True)
password_hash: str
password_version: int = Field(default=0)
full_name: Optional[str] = None
bio: Optional[str] = None
avatar_url: Optional[str] = None
@@ -44,6 +45,7 @@ class User(SQLModel, table=True):
_HASH_ITERATIONS
)
self.password_hash = f"{salt.hex()}${derived.hex()}"
self.password_version += 1 # Aumenta la versión de la contraseña
def verify_password(self, password: str) -> bool:
try:
@@ -59,6 +61,13 @@ class User(SQLModel, table=True):
)
return secrets.compare_digest(hash_hex, derived.hex())
class Post(SQLModel, table=True):
__tablename__ = "posts"
id: int = Field(default=None, primary_key=True, sa_column_kwargs={"autoincrement": True})
title: str
body: Optional[str]
creator_id: int = Field(foreign_key="users.id")
engine = create_engine(settings.DATABASE_URL)
SQLModel.metadata.create_all(engine)
+10
View File
@@ -25,3 +25,13 @@ class LoginSchema(BaseModel):
class TokenSchema(BaseModel):
access_token: str
type: str = "bearer"
class ChangePasswordSchema(BaseModel):
old_password: str
new_password: str
class PostSchema(BaseModel):
id: Optional[int]
username: Optional[str]
title: str
body: str
+15 -4
View File
@@ -1,11 +1,12 @@
from fastapi import APIRouter, HTTPException
from ..pmodels import LoginSchema, TokenSchema
from fastapi import APIRouter, HTTPException, Depends
from ..auth.dependencies import get_current_user
from ..pmodels import LoginSchema, TokenSchema, StatusModel, ChangePasswordSchema
from ..models import engine, User
from sqlmodel import Session, select
from ..settings import SECRET_KEY
import jwt
router = APIRouter(
prefix="/login",
prefix="/api/login",
tags=["Autenticación"]
)
@@ -18,9 +19,19 @@ async def login(data: LoginSchema) -> TokenSchema:
if user is None:
raise HTTPException(status_code=401, detail="User/Password Incorrect")
if user.verify_password(data.password):
encoded_jwt = jwt.encode({"username": user.username}, SECRET_KEY, algorithm="HS256")
encoded_jwt = jwt.encode({"username": user.username, "pwd_v": user.password_version}, SECRET_KEY, algorithm="HS256")
return TokenSchema(
access_token=encoded_jwt
)
else:
raise HTTPException(status_code=401, detail="User/Password Incorrect")
@router.post("/set-password",
description = "Establece otra contraseña (requiere la anterior)",
summary = "Cambia la contraseña")
async def set_password(data: ChangePasswordSchema, user: User = Depends(get_current_user)) -> StatusModel:
if not user.verify_password(data.old_password):
return 401, StatusModel(status="Invalid Password")
user.set_password(data.new_password)
return StatusModel()
+40
View File
@@ -0,0 +1,40 @@
from typing import Optional
from ..models import User, Post, engine
from sqlmodel import Session, select
from ..pmodels import PostSchema, StatusModel
from fastapi import APIRouter, Depends
from ..auth.dependencies import get_current_user
router = APIRouter(prefix="/api/posts", tags=["Publicaciones"])
@router.get("")
async def get_posts(page: int = 1, user: User = Depends(get_current_user)) -> list[PostSchema]:
posts: list[PostSchema] = []
with Session(engine) as session:
query = select(Post, User).where(Post.creator_id == User.id).limit(10).offset(10*(page-1))
result = session.exec(query)
for post, user in result:
posts.append(PostSchema(
id=post.id,
username=user.username,
title=post.title,
body=post.body
))
return posts
@router.post("")
async def create_post(data: PostSchema, user: User = Depends(get_current_user)) -> PostSchema:
post = Post(
user=user.id,
title=data.title,
body=data.body
)
with Session(engine) as session:
session.add(post)
session.commit()
return PostSchema(
username=user.username,
title=post.title,
body=post.body
)
+9 -9
View File
@@ -3,12 +3,10 @@ from ..pmodels import pUser, pUserWithPassword, StatusModel
from ..models import engine, User
from ..auth.dependencies import get_current_user
from sqlmodel import Session, select
router = APIRouter(
prefix="/users",
tags=["users"],
dependencies=[
Depends(get_current_user)
],
prefix="/api/users",
tags=["Usuarios"],
responses = {
status.HTTP_401_UNAUTHORIZED: {
"description": "Token invalido o ausente",
@@ -21,15 +19,17 @@ router = APIRouter(
}
)
@router.get("/")
async def get_users() -> list[pUser]:
@router.get("/", description="Obtener una lista con todos los usuarios", summary="Obtener una lista con todos los usuarios")
async def get_users(user: User = Depends(get_current_user)) -> list[pUser]:
with Session(engine) as session:
statement = select(User)
results = session.exec(statement).all()
return [user.to_dto() for user in results]
@router.get("/{user_id}")
async def get_user(user_id) -> pUser:
@router.get("/{user_id}",
description="Obtener un usuario en especifico en base a su ID",
summary = "Obtiene un usuario")
async def get_user(user_id, user: User = Depends(get_current_user)) -> pUser:
with Session(engine) as session:
statement = select(User).where(User.id == user_id)
results = session.exec(statement).first()