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", "image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie",
"features": { "features": {
"ghcr.io/devcontainer-community/devcontainer-features/astral.sh-uv:1": {} "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 fastapi import FastAPI
from .routes import user, login from .routes import user, login, posts
from .models import engine
app = FastAPI() app = FastAPI()
@app.get("/")
async def root() -> str:
return "Hello World"
app.include_router(user.router) app.include_router(user.router)
app.include_router(login.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: with Session(engine) as session:
query = select(User).where(User.username == payload["username"]).limit(1) query = select(User).where(User.username == payload["username"]).limit(1)
user: User = session.exec(query).first() user: User = session.exec(query).first()
if user == None: if user is None:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED, status_code = status.HTTP_401_UNAUTHORIZED,
detail="Credenciales invalidas" 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 return user
+9
View File
@@ -16,6 +16,7 @@ class User(SQLModel, table=True):
username: str = Field(index=True, unique=True) username: str = Field(index=True, unique=True)
email: str = Field(index=True, unique=True) email: str = Field(index=True, unique=True)
password_hash: str password_hash: str
password_version: int = Field(default=0)
full_name: Optional[str] = None full_name: Optional[str] = None
bio: Optional[str] = None bio: Optional[str] = None
avatar_url: Optional[str] = None avatar_url: Optional[str] = None
@@ -44,6 +45,7 @@ class User(SQLModel, table=True):
_HASH_ITERATIONS _HASH_ITERATIONS
) )
self.password_hash = f"{salt.hex()}${derived.hex()}" 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: def verify_password(self, password: str) -> bool:
try: try:
@@ -59,6 +61,13 @@ class User(SQLModel, table=True):
) )
return secrets.compare_digest(hash_hex, derived.hex()) 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) engine = create_engine(settings.DATABASE_URL)
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)
+10
View File
@@ -25,3 +25,13 @@ class LoginSchema(BaseModel):
class TokenSchema(BaseModel): class TokenSchema(BaseModel):
access_token: str access_token: str
type: str = "bearer" 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 fastapi import APIRouter, HTTPException, Depends
from ..pmodels import LoginSchema, TokenSchema from ..auth.dependencies import get_current_user
from ..pmodels import LoginSchema, TokenSchema, StatusModel, ChangePasswordSchema
from ..models import engine, User from ..models import engine, User
from sqlmodel import Session, select from sqlmodel import Session, select
from ..settings import SECRET_KEY from ..settings import SECRET_KEY
import jwt import jwt
router = APIRouter( router = APIRouter(
prefix="/login", prefix="/api/login",
tags=["Autenticación"] tags=["Autenticación"]
) )
@@ -18,9 +19,19 @@ async def login(data: LoginSchema) -> TokenSchema:
if user is None: if user is None:
raise HTTPException(status_code=401, detail="User/Password Incorrect") raise HTTPException(status_code=401, detail="User/Password Incorrect")
if user.verify_password(data.password): 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( return TokenSchema(
access_token=encoded_jwt access_token=encoded_jwt
) )
else: else:
raise HTTPException(status_code=401, detail="User/Password Incorrect") 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 ..models import engine, User
from ..auth.dependencies import get_current_user from ..auth.dependencies import get_current_user
from sqlmodel import Session, select from sqlmodel import Session, select
router = APIRouter( router = APIRouter(
prefix="/users", prefix="/api/users",
tags=["users"], tags=["Usuarios"],
dependencies=[
Depends(get_current_user)
],
responses = { responses = {
status.HTTP_401_UNAUTHORIZED: { status.HTTP_401_UNAUTHORIZED: {
"description": "Token invalido o ausente", "description": "Token invalido o ausente",
@@ -21,15 +19,17 @@ router = APIRouter(
} }
) )
@router.get("/") @router.get("/", description="Obtener una lista con todos los usuarios", summary="Obtener una lista con todos los usuarios")
async def get_users() -> list[pUser]: async def get_users(user: User = Depends(get_current_user)) -> list[pUser]:
with Session(engine) as session: with Session(engine) as session:
statement = select(User) statement = select(User)
results = session.exec(statement).all() results = session.exec(statement).all()
return [user.to_dto() for user in results] return [user.to_dto() for user in results]
@router.get("/{user_id}") @router.get("/{user_id}",
async def get_user(user_id) -> pUser: 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: with Session(engine) as session:
statement = select(User).where(User.id == user_id) statement = select(User).where(User.id == user_id)
results = session.exec(statement).first() results = session.exec(statement).first()