diff --git a/backend/.env-example b/backend/.env-example index a12a9b8..83108f6 100644 --- a/backend/.env-example +++ b/backend/.env-example @@ -1,3 +1,5 @@ DATABASE_URL="sqlite:///./db.sqlite3" # Conexão para o banco de dados JWT_SECRET_KEY="SUA_SECRET_KEY" # Recomendo gerar uma chave usando: python -c "import secrets; print(secrets.token_hex(32))" -JWT_EXPIRATION_TIME=30 # Tempo de expiração do token em minutos \ No newline at end of file +JWT_EXPIRATION_TIME=30 # Tempo de expiração do token em minutos +WEBHOOK_LOG_INFO="" # Preencha com o seu link de webhook +WEBHOOK_LOG_ERROR="" \ No newline at end of file diff --git a/backend/poetry.lock b/backend/poetry.lock index 258d662..8975fab 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1271,6 +1271,25 @@ platformdirs = ">=4.3.6,<5.0.0" python-engineio = ">=4.12.2" python-socketio = {version = "5.13.0", extras = ["client"]} +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +groups = ["main"] +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] + [[package]] name = "mako" version = "1.3.10" @@ -2682,6 +2701,22 @@ MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] + [[package]] name = "wrapt" version = "1.17.2" @@ -2863,4 +2898,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = ">=3.13,<4.0" -content-hash = "147d60e2960aaae94335e43624f4ad7e01e4b9b924a8376b7ce33ded1c80f82a" +content-hash = "22d4893f96464b2b8a743d5128c19324602ca2b5f0744bc10396dac519a7536e" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8cf3108..8021d6e 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -11,7 +11,8 @@ dependencies = [ "passlib[argon2] (>=1.7.4,<2.0.0)", "pyjwt (>=2.10.1,<3.0.0)", "slowapi (>=0.1.9,<0.2.0)", - "apscheduler (>=3.11.0,<4.0.0)" + "apscheduler (>=3.11.0,<4.0.0)", + "loguru (>=0.7.3,<0.8.0)" ] [tool.poetry] diff --git a/backend/src/app.py b/backend/src/app.py index ad8e92c..142954a 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -5,8 +5,11 @@ from fastapi.responses import JSONResponse from slowapi.middleware import SlowAPIMiddleware from slowapi.errors import RateLimitExceeded -from src.utils import limiter +from src.utils import limiter, get_user_ip import uvicorn +from src.logger import setup_discord_logging, logger + +setup_discord_logging() app = FastAPI(docs_url=None, redoc_url=None) app.state.limiter = limiter @@ -22,6 +25,14 @@ allow_headers=['*'], ) +@app.middleware("http") +async def capture_exceptions(request: Request, call_next): + try: + return await call_next(request) + except Exception as exc: + logger.critical(f"`{request.method} {request.url.path}` Erro não tratado!\n- IP: {get_user_ip(request)} - Session ID: {request.cookies.get('session_id')}\n```{exc}```") + return JSONResponse(status_code=500, content={"detail": "Internal Server Error"}) + @app.exception_handler(RateLimitExceeded) async def rate_limit_exception_handler(request: Request, exc: RateLimitExceeded): return JSONResponse( @@ -34,4 +45,5 @@ async def rate_limit_exception_handler(request: Request, exc: RateLimitExceeded) scheduler.start() if __name__ == '__main__': - uvicorn.run(app, host='0.0.0.0', port=80, reload=False) \ No newline at end of file + uvicorn.run(app, host='0.0.0.0', port=80, reload=False) + logger.info("API iniciada!") \ No newline at end of file diff --git a/backend/src/logger.py b/backend/src/logger.py new file mode 100644 index 0000000..35045ff --- /dev/null +++ b/backend/src/logger.py @@ -0,0 +1,30 @@ +import asyncio +from datetime import datetime +import threading +import httpx +from loguru import logger +from src.settings import WEBHOOK_INFO, WEBHOOK_ERROR + +def send_webhook(message: str, webhook_url: str): + def runner(): + async def task(): + try: + async with httpx.AsyncClient(timeout=5) as client: + await client.post(webhook_url, json={"content": message}) + except Exception as e: + logger.error(f"[Log Error]: {e}") + + asyncio.run(task()) + + threading.Thread(target=runner, daemon=True).start() + +def info_sink(message): + send_webhook(f"**[{message.record['level'].name}]** {message.record['message']}\n-# **{datetime.now().strftime('%d/%m/%Y - %H:%M:%S')}**", WEBHOOK_INFO) + +def error_sink(message): + send_webhook(f"**[{message.record['level'].name}]** {message.record['message']}\n-# **{datetime.now().strftime('%d/%m/%Y - %H:%M:%S')}**", WEBHOOK_ERROR) + +def setup_discord_logging(): + logger.remove() + logger.add(info_sink, level="INFO", filter=lambda r: r["level"].name in ("INFO", "WARNING")) + logger.add(error_sink, level="ERROR", filter=lambda r: r["level"].name in ("ERROR", "CRITICAL")) \ No newline at end of file diff --git a/backend/src/routes/auth.py b/backend/src/routes/auth.py index 75985cc..ee44dfe 100644 --- a/backend/src/routes/auth.py +++ b/backend/src/routes/auth.py @@ -4,8 +4,9 @@ from src.db.models import User, RefreshToken from src.security import get_user, verify_password, generate_password_hash, generate_jwt_token, clear_auth_cookie, generate_session_id, invalidate_all_sessions from src.schemas import LoginRequestSchema, RegisterRequestSchema, LoginResponseSchema, UsernameUpdateSchema, EmailUpdateSchema, PasswordUpdateSchema -from src.utils import limiter +from src.utils import limiter, get_user_ip from sqlalchemy.orm import Session +from src.logger import logger router = APIRouter() @@ -14,13 +15,16 @@ def login(login: LoginRequestSchema, request: Request, response: Response, db: Session = Depends(get_db)): user = db.query(User).filter(User.email == login.email).first() if not user: + logger.warning(f"`/auth/login`: Tentativa de login com email inexistente\n```Email: {login.email}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail='E-mail ou senha estão inválidos. Por favor, tente novamente.') if not verify_password(login.password, user.password): + logger.warning(f"`/auth/login`: Tentativa de login com senha inválida\n```Email: {login.email}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail='E-mail ou senha estão inválidos. Por favor, tente novamente.') session_id = generate_session_id(response) token = generate_jwt_token(user.id, user.username, session_id, request, response, login.remember, db) + logger.info(f"`/auth/login`: Login realizado com sucesso\n```Email: {login.email}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return { 'access_token': token['access_token'], 'refresh_token': token['refresh_token'], @@ -35,12 +39,15 @@ def login(login: LoginRequestSchema, request: Request, response: Response, db: S @limiter.limit("3/minute;15/day") def register(register: RegisterRequestSchema, request: Request, response: Response, db: Session = Depends(get_db)): if register.password != register.confirm_password: + logger.warning(f"`/auth/register`: Tentativa de registro com senhas diferentes\n```Username: {register.username} - Email: {register.email}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=400, detail='As senhas não coincidem. Verifique se as senhas estão iguais.') if db.query(User).filter(User.username == register.username).first(): + logger.warning(f"`/auth/register`: Tentativa de registro com username já existente\n```Username: {register.username} - Email: {register.email}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=409, detail='Nome de usuário já registrado. Por favor, tente outro nome de usuário.') if db.query(User).filter(User.email == register.email).first(): + logger.warning(f"`/auth/register`: Tentativa de registro com e-mail já existente\n```Username: {register.username} - Email: {register.email}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=409, detail='E-mail já registrado. Por favor, tente outro e-mail.') user = User(username=register.username, email=register.email, password=generate_password_hash(register.password)) @@ -50,6 +57,7 @@ def register(register: RegisterRequestSchema, request: Request, response: Respon session_id = generate_session_id(response) token = generate_jwt_token(user.id, user.username, session_id, request, response, True, db) + logger.info(f"`/auth/register`: Registro realizado com sucesso\n```Username: {register.username} - Email: {register.email}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return { 'access_token': token['access_token'], 'refresh_token': token['refresh_token'], @@ -66,6 +74,7 @@ def logout(response: Response, request: Request, db: Session = Depends(get_db)): if session_id: db.query(RefreshToken).filter(RefreshToken.session_id == session_id).update({'is_active': False}) db.commit() + logger.info(f"`/auth/logout`: Logout realizado com sucesso\n```Session ID: {session_id}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") clear_auth_cookie(response) return {'message': 'Successfully logged out'} @@ -82,6 +91,7 @@ def get_current_user(request: Request, response: Response, db: Session = Depends def update_username(new: UsernameUpdateSchema, request: Request, response: Response, db: Session = Depends(get_db)): user = get_user(request, response, db) if not user: + logger.warning(f"`/auth/me/username`: Tentativa com token inválido\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail='Invalid token or user not found') if db.query(User).filter(User.username == new.username).first(): @@ -90,6 +100,7 @@ def update_username(new: UsernameUpdateSchema, request: Request, response: Respo user_db = db.query(User).filter(User.id == user['id']).first() user_db.username = new.username db.commit() + logger.info(f"`/auth/me/username`: Username atualizado com sucesso\n```Username: {user['username']} -> {new.username}\nSession ID: {request.cookies.get('session_id')}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'message': 'Username updated successfully'} @router.patch('/me/email') @@ -97,6 +108,7 @@ def update_username(new: UsernameUpdateSchema, request: Request, response: Respo def update_email(new: EmailUpdateSchema, request: Request, response: Response, db: Session = Depends(get_db)): user = get_user(request, response, db) if not user: + logger.warning(f"`/auth/me/email`: Tentativa com token inválido\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail='Invalid token or user not found') if db.query(User).filter(User.email == new.email).first(): @@ -106,6 +118,7 @@ def update_email(new: EmailUpdateSchema, request: Request, response: Response, d user_db.email = new.email db.commit() invalidate_all_sessions(user['id'], request.cookies.get('session_id'), True, db) + logger.info(f"`/auth/me/email`: E-mail atualizado com sucesso\n```Email: {user['email']} -> {new.email}\nSession ID: {request.cookies.get('session_id')}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'message': 'E-mail updated successfully'} @router.patch('/me/password') @@ -113,6 +126,7 @@ def update_email(new: EmailUpdateSchema, request: Request, response: Response, d def update_password(new: PasswordUpdateSchema, request: Request, response: Response, db: Session = Depends(get_db)): user = get_user(request, response, db) if not user: + logger.warning(f"`/auth/me/password`: Tentativa com token inválido\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail='Invalid token or user not found') user_db = db.query(User).filter(User.id == user['id']).first() @@ -122,4 +136,5 @@ def update_password(new: PasswordUpdateSchema, request: Request, response: Respo user_db.password = generate_password_hash(new.password) db.commit() invalidate_all_sessions(user['id'], request.cookies.get('session_id'), True, db) + logger.info(f"`/auth/me/password`: Senha atualizada com sucesso\n```Email: {user['email']} - Session ID: {request.cookies.get('session_id')}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'message': 'Password updated successfully'} diff --git a/backend/src/routes/url.py b/backend/src/routes/url.py index 21e9032..134f631 100644 --- a/backend/src/routes/url.py +++ b/backend/src/routes/url.py @@ -4,10 +4,11 @@ from sqlalchemy import or_ from sqlalchemy.orm import Session from src.schemas import LinkCreateSchema, LinkCreateResponseSchema, LinkPublicSchema, LinkStatsSchema, UserLinksResponseSchema, LinkUpdateSchema, LinkPasswordUpdateSchema, LinkExpirationUpdateSchema -from src.utils import validate_short_url, limiter +from src.utils import validate_short_url, limiter, get_user_ip from src.security import generate_password_hash, verify_password, get_user from src.db.database import SessionLocal from datetime import datetime, timedelta, timezone +from src.logger import logger router = APIRouter() @@ -20,8 +21,9 @@ def clean_expired_links(): for link in expired_links: db.delete(link) db.commit() + logger.info(f"`clean_expired_links`: {len(expired_links)} links expirados removidos com sucesso!") except Exception as e: - print('Erro ao limpar links expirados:', e) + logger.error(f"`clean_expired_links`: Erro ao limpar links expirados\n```{e}```") finally: db.close() @@ -30,9 +32,11 @@ def clean_expired_links(): def get_url(short_id: str, request: Request, password: str = Header(default=None), db: Session = Depends(get_db)): link = db.query(Link).filter(Link.short_url == short_id).first() if not link: + logger.warning(f"`GET /short/{short_id}`: Link não encontrado\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=404, detail="Link not found") if link.expires_at and link.expires_at < datetime.now(timezone.utc).replace(tzinfo=None): + logger.warning(f"`GET /short/{short_id}`: Link expirado\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") db.delete(link) db.commit() raise HTTPException(status_code=404, detail="Link not found") @@ -40,8 +44,10 @@ def get_url(short_id: str, request: Request, password: str = Header(default=None if link.password and not password: raise HTTPException(status_code=401, detail="Link is password protected") if link.password and not verify_password(password, link.password): + logger.warning(f"`GET /short/{short_id}`: Senha do link inválida\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail="Invalid password") + logger.info(f"`GET /short/{short_id}`: Link encontrado\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'original_url': link.original_url, 'clicks': link.clicks, 'created_at': link.created_at} @router.post('/click/{short_id}') @@ -49,11 +55,13 @@ def get_url(short_id: str, request: Request, password: str = Header(default=None def click_url(short_id: str, request: Request, db: Session = Depends(get_db)): link = db.query(Link).filter(Link.short_url == short_id).first() if not link: + logger.warning(f"`/click/{short_id}`: Link não encontrado\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=404, detail="Link not found") link.clicks += 1 db.commit() db.refresh(link) + logger.info(f"`/click/{short_id}`: Adicionado clique ao link\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'message': 'Link clicked successfully'} @router.post('/short', response_model=LinkCreateResponseSchema) @@ -62,6 +70,7 @@ def create_url(url: LinkCreateSchema, request: Request, response: Response, db: user = get_user(request, response, db) validated_short = validate_short_url(url.short_url, db) if not validated_short: + logger.warning(f"`POST /short`: ShortURL inválida\n```URL: {url.original_url}\nShort URL: {url.short_url} - User ID: {user['id'] if user else 'N/A'}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=400, detail="Invalid short URL") if url.password and len(url.password.strip()) < 3: @@ -70,6 +79,7 @@ def create_url(url: LinkCreateSchema, request: Request, response: Response, db: url.short_url = validated_short if db.query(Link).filter(Link.short_url == url.short_url).first(): + logger.warning(f"`POST /short`: ShortURL já existe\n```URL: {url.original_url}\nShort URL: {url.short_url} - User ID: {user['id'] if user else 'N/A'}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=400, detail="Short URL already exists") if url.password: @@ -94,6 +104,7 @@ def create_url(url: LinkCreateSchema, request: Request, response: Response, db: db.add(link) db.commit() db.refresh(link) + logger.info(f"`POST /short`: Link criado com sucesso\n```URL: {url.original_url}\nShort URL: {url.short_url} - Password: {'Sim' if url.password else 'Não'} - Expira em: {url.expires_at if url.expires_at else 'Nunca'}\nUser ID: {user['id'] if user else 'N/A'} - Session ID: {request.cookies.get('session_id')}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return link @router.get('/stats/{short_id}', response_model=LinkStatsSchema) @@ -101,11 +112,13 @@ def create_url(url: LinkCreateSchema, request: Request, response: Response, db: def get_stats(short_id: str, request: Request, db: Session = Depends(get_db)): link = db.query(Link).filter(Link.short_url == short_id, or_(Link.expires_at.is_(None), Link.expires_at > datetime.now(timezone.utc))).first() if not link: + logger.warning(f"`/stats/{short_id}`: Link não encontrado\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=404, detail="Link not found") if link.password: # Links com senha só o usuário que criou pode ver as estatísticas raise HTTPException(status_code=401, detail="Link is password protected") + logger.info(f"`/stats/{short_id}`: Estatísticas do link\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'original_url': link.original_url, 'short_url': link.short_url, 'clicks': link.clicks, 'created_at': link.created_at} @router.get('/user/links', response_model=UserLinksResponseSchema) @@ -113,12 +126,14 @@ def get_stats(short_id: str, request: Request, db: Session = Depends(get_db)): def get_user_links(request: Request, response: Response, db: Session = Depends(get_db)): user = get_user(request, response, db) if not user: + logger.warning(f"`/user/links`: Tentativa com token inválido\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail="You must be logged in to access this resource") links = db.query(Link).filter(Link.user_id == user['id'], or_(Link.expires_at.is_(None), Link.expires_at > datetime.now(timezone.utc))).all() for link in links: link.has_password = bool(link.password) + logger.info(f"`/user/links`: Todos os links do usuário\n```User ID: {user['id']}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'links': links} @router.patch('/short/{short_id}') @@ -126,13 +141,16 @@ def get_user_links(request: Request, response: Response, db: Session = Depends(g def update_short_url(short_id: str, new_url: LinkUpdateSchema, request: Request, response: Response, db: Session = Depends(get_db)): user = get_user(request, response, db) if not user: + logger.warning(f"`PATCH /short/{short_id}`: Tentativa com token inválido\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail="You must be logged in to access this resource") if not validate_short_url(new_url.short_url, db, auto_generate=False): + logger.warning(f"`PATCH /short/{short_id}`: Shorturl inválida\n```URL: {new_url.short_url} - User ID: {user['id']}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=400, detail="Invalid short URL") link = db.query(Link).filter(Link.short_url == short_id, Link.user_id == user['id']).first() if not link: + logger.warning(f"`PATCH /short/{short_id}`: Link não encontrado\n```URL: {new_url.short_url} - User ID: {user['id']}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=404, detail="Link not found") if link.short_url == new_url.short_url or db.query(Link).filter(Link.short_url == new_url.short_url).first(): @@ -141,6 +159,7 @@ def update_short_url(short_id: str, new_url: LinkUpdateSchema, request: Request, link.short_url = new_url.short_url db.commit() db.refresh(link) + logger.info(f"`PATCH /short/{short_id}`: ShortURL atualizada\n```Nova Shorturl: {new_url.short_url}\nUser ID: {user['id']} - Session ID: {request.cookies.get('session_id')}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'message': 'Short URL updated successfully', 'short_url': link.short_url} @router.patch('/short/{short_id}/password') @@ -148,6 +167,7 @@ def update_short_url(short_id: str, new_url: LinkUpdateSchema, request: Request, def update_link_password(short_id: str, password: LinkPasswordUpdateSchema, request: Request, response: Response, db: Session = Depends(get_db)): user = get_user(request, response, db) if not user: + logger.warning(f"`PATCH /short/{short_id}/password`: Tentativa com token inválido\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail="You must be logged in to access this resource") if len(password.password.strip()) < 3: @@ -155,10 +175,12 @@ def update_link_password(short_id: str, password: LinkPasswordUpdateSchema, requ link = db.query(Link).filter(Link.short_url == short_id, Link.user_id == user['id']).first() if not link: + logger.warning(f"`PATCH /short/{short_id}/password`: Link não encontrado\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=404, detail="Link not found") link.password = generate_password_hash(password.password) db.commit() + logger.info(f"`PATCH /short/{short_id}/password`: Senha atualizada\n```User ID: {user['id']} - Session ID: {request.cookies.get('session_id')}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'message': 'Password updated successfully'} @router.patch('/short/{short_id}/expiration') @@ -166,6 +188,7 @@ def update_link_password(short_id: str, password: LinkPasswordUpdateSchema, requ def update_link_expiration(short_id: str, expiration: LinkExpirationUpdateSchema, request: Request, response: Response, db: Session = Depends(get_db)): user = get_user(request, response, db) if not user: + logger.warning(f"`PATCH /short/{short_id}/expiration`: Tentativa com token inválido\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail="You must be logged in to access this resource") if expiration.expires_at: @@ -180,10 +203,12 @@ def update_link_expiration(short_id: str, expiration: LinkExpirationUpdateSchema link = db.query(Link).filter(Link.short_url == short_id, Link.user_id == user['id']).first() if not link: + logger.warning(f"`PATCH /short/{short_id}/expiration`: Link não encontrado\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=404, detail="Link not found") link.expires_at = expiration.expires_at.replace(tzinfo=None) if expiration.expires_at else None db.commit() + logger.info(f"`PATCH /short/{short_id}/expiration`: Expiração atualizada\n```Expira em: {expiration.expires_at if expiration.expires_at else 'Nunca'}\nUser ID: {user['id']} - Session ID: {request.cookies.get('session_id')}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'message': 'Expiration updated successfully'} @router.delete('/short/{short_id}') @@ -191,12 +216,15 @@ def update_link_expiration(short_id: str, expiration: LinkExpirationUpdateSchema def delete_short_url(short_id: str, request: Request, response: Response, db: Session = Depends(get_db)): user = get_user(request, response, db) if not user: + logger.warning(f"`DELETE /short/{short_id}`: Tentativa com token inválido\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=401, detail="You must be logged in to access this resource") link = db.query(Link).filter(Link.short_url == short_id, Link.user_id == user['id']).first() if not link: + logger.warning(f"`DELETE /short/{short_id}`: Link não encontrado\n```IP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") raise HTTPException(status_code=404, detail="Link not found") db.delete(link) db.commit() + logger.info(f"`DELETE /short/{short_id}`: Link deletado\n```Link original: {link.original_url}\nUser ID: {user['id']} - Session ID: {request.cookies.get('session_id')}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'message': 'URL deleted successfully'} diff --git a/backend/src/security.py b/backend/src/security.py index eb4c8b3..b859a88 100644 --- a/backend/src/security.py +++ b/backend/src/security.py @@ -7,6 +7,7 @@ from src.db.models import User, RefreshToken from src.settings import JWT_SECRET_KEY, JWT_EXPIRATION_TIME from src.utils import get_user_agent, get_user_ip +from src.logger import logger import hashlib import secrets import uuid @@ -51,6 +52,7 @@ def generate_jwt_token(user_id: int, username: str, session_id: str, secure=True, samesite='none' ) + logger.info(f"`generate_jwt_token`: Token JWT gerado com sucesso\n```User ID: {user_id} - Username: {username} - Session ID: {session_id}\nIP: {get_user_ip(request)} | User-Agent: {request.headers.get('User-Agent')}```") return {'access_token': jwt, 'refresh_token': refresh_token} def generate_refresh_token(user_id: int, session_id: str, request: Request, remember: bool, db: Session): @@ -83,6 +85,7 @@ def decode_jwt_token(token: str): except ExpiredSignatureError: raise HTTPException(status_code=401, detail='Token expired') except (InvalidAlgorithmError, InvalidSignatureError, InvalidTokenError): + logger.error(f"`decode_jwt_token`: Token JWT inválido\n```Token: {token}```") raise HTTPException(status_code=401, detail='Invalid token') return jwt @@ -106,7 +109,8 @@ def get_user(request: Request, response: Response, db: Session = Depends(get_db) 'username': user.username, 'email': user.email } - except Exception: + except Exception as e: + logger.error(f"`get_user`: Erro ao decodificar token JWT\n```Token: {token}\nError: {e}```") pass if not refresh_token: diff --git a/backend/src/settings.py b/backend/src/settings.py index 397ceb1..23b9dae 100644 --- a/backend/src/settings.py +++ b/backend/src/settings.py @@ -7,4 +7,6 @@ DATABASE_URL = os.getenv("DATABASE_URL") JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") -JWT_EXPIRATION_TIME = int(os.getenv("JWT_EXPIRATION_TIME")) # em minutos \ No newline at end of file +JWT_EXPIRATION_TIME = int(os.getenv("JWT_EXPIRATION_TIME")) # em minutos +WEBHOOK_INFO = os.getenv("WEBHOOK_LOG_INFO") +WEBHOOK_ERROR = os.getenv("WEBHOOK_LOG_ERROR") \ No newline at end of file diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index c488bdc..eca9f42 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -10,6 +10,9 @@ from src.db.models import Link, User from src.security import generate_password_hash, generate_jwt_token, generate_session_id from unittest.mock import MagicMock +from src.logger import logger + +logger.remove() @fixture() def client(db_session: Session):