Skip to content

Commit fe6831c

Browse files
committed
chore: new endpoints
1 parent d8230fe commit fe6831c

11 files changed

Lines changed: 411 additions & 90 deletions

File tree

app/core/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class Settings(BaseSettings):
1010
SECRET_KEY: str = "your_secret_key"
1111
JWT_ALGORITHM: str = "HS256"
1212
ACCESS_TOKEN_EXPIRE_MINUTES: int = 10080 # 7 days
13+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
14+
LOG_LEVEL: str = "INFO"
15+
LOG_JSON_FORMAT: bool = True
1316

1417
# MongoDB Configuration
1518
MONGO_INITDB_DATABASE: str
@@ -22,4 +25,4 @@ class Settings(BaseSettings):
2225
)
2326

2427

25-
settings = Settings()
28+
settings = Settings() # type: ignore[call-arg]

app/core/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
API_PREFIX = "/api/v1"

app/core/jwt.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime, timedelta, timezone
2-
from typing import Dict
2+
from typing import Any, Dict
33

44
import jwt
55

@@ -11,7 +11,21 @@ def create_access_token(data: Dict):
1111
expire = datetime.now(timezone.utc) + timedelta(
1212
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
1313
)
14-
to_encode.update({"exp": expire})
14+
to_encode.update({"exp": expire, "type": "access"})
15+
encoded_jwt = jwt.encode(
16+
to_encode,
17+
settings.SECRET_KEY,
18+
algorithm=settings.JWT_ALGORITHM,
19+
)
20+
return encoded_jwt
21+
22+
23+
def create_refresh_token(data: Dict[str, str]) -> str:
24+
to_encode: Dict[str, Any] = data.copy()
25+
expire = datetime.now(timezone.utc) + timedelta(
26+
days=settings.REFRESH_TOKEN_EXPIRE_DAYS
27+
)
28+
to_encode.update({"exp": expire, "type": "refresh"})
1529
encoded_jwt = jwt.encode(
1630
to_encode,
1731
settings.SECRET_KEY,
@@ -22,3 +36,12 @@ def create_access_token(data: Dict):
2236

2337
def decode_access_token(token: str):
2438
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
39+
40+
41+
def verify_refresh_token(token: str) -> Dict | None:
42+
payload = jwt.decode(
43+
token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
44+
)
45+
if payload.get("type") != "refresh":
46+
return None
47+
return payload

app/core/logger.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import json
2+
import logging
3+
import logging.config
4+
import sys
5+
from datetime import datetime, timezone
6+
from pathlib import Path
7+
from typing import Any, Dict
8+
9+
10+
class JsonFormatter(logging.Formatter):
11+
"""Custom JSON formatter for structured logging.
12+
13+
Formats log records as JSON objects with consistent structure including
14+
timestamp, level, logger name, module, line number, message and exceptions.
15+
"""
16+
17+
def format(self, record: logging.LogRecord) -> str:
18+
"""Format the log record as a JSON string.
19+
20+
Args:
21+
record: The log record to format.
22+
23+
Returns:
24+
JSON-formatted string with log information.
25+
"""
26+
log_data: Dict[str, Any] = {
27+
"timestamp": datetime.now(timezone.utc).isoformat(),
28+
"level": record.levelname,
29+
"logger": record.name,
30+
"module": record.module,
31+
"function": record.funcName,
32+
"line": record.lineno,
33+
"message": record.getMessage(),
34+
}
35+
if record.exc_info:
36+
log_data["exception"] = self.formatException(record.exc_info)
37+
# core/logging.py
38+
if hasattr(record, "request_id"):
39+
log_data["request_id"] = getattr(record, "request_id")
40+
if hasattr(record, "method"):
41+
log_data["method"] = getattr(record, "method")
42+
if hasattr(record, "path"):
43+
log_data["path"] = getattr(record, "path")
44+
if hasattr(record, "client_host"):
45+
log_data["client_host"] = getattr(record, "client_host")
46+
if hasattr(record, "query_params"):
47+
log_data["query_params"] = getattr(record, "query_params")
48+
if hasattr(record, "status_code"):
49+
log_data["status_code"] = getattr(record, "status_code")
50+
if hasattr(record, "duration_ms"):
51+
log_data["duration_ms"] = getattr(record, "duration_ms")
52+
# all routers
53+
return json.dumps(log_data, default=str)
54+
55+
56+
LOG_DIR = Path("/var/log/backend")
57+
if "pytest" in sys.modules:
58+
LOG_DIR = Path("/tmp/logs")
59+
LOG_DIR.mkdir(exist_ok=True)
60+
61+
LOGGING_CONFIG = {
62+
"version": 1,
63+
"disable_existing_loggers": False,
64+
"formatters": {
65+
"default": {
66+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
67+
"datefmt": "%Y-%m-%d %H:%M:%S",
68+
},
69+
"json": {
70+
"()": JsonFormatter,
71+
},
72+
},
73+
"handlers": {
74+
"console": {
75+
"class": "logging.StreamHandler",
76+
"level": "INFO",
77+
"formatter": "default",
78+
"stream": sys.stdout,
79+
},
80+
"console_json": {
81+
"class": "logging.StreamHandler",
82+
"level": "DEBUG",
83+
"formatter": "json",
84+
"stream": sys.stdout,
85+
},
86+
"file": {
87+
"class": "logging.handlers.RotatingFileHandler",
88+
"level": "INFO",
89+
"formatter": "json",
90+
"filename": str(LOG_DIR / "app.log"),
91+
"maxBytes": 10485760, # 10MB
92+
"backupCount": 5,
93+
"encoding": "utf-8",
94+
},
95+
"error_file": {
96+
"class": "logging.handlers.RotatingFileHandler",
97+
"level": "ERROR",
98+
"formatter": "json",
99+
"filename": str(LOG_DIR / "error.log"),
100+
"maxBytes": 10485760, # 10MB
101+
"backupCount": 5,
102+
"encoding": "utf-8",
103+
},
104+
},
105+
"loggers": {
106+
# Root logger
107+
"": {
108+
"level": "INFO",
109+
"handlers": ["console", "file", "error_file"],
110+
},
111+
# App logger
112+
"app": {
113+
"level": "DEBUG",
114+
"handlers": ["console_json", "file", "error_file"],
115+
"propagate": False,
116+
},
117+
# Uvicorn loggers
118+
"uvicorn": {
119+
"level": "INFO",
120+
"handlers": ["console"],
121+
"propagate": False,
122+
},
123+
"uvicorn.access": {
124+
"level": "INFO",
125+
"handlers": ["file"],
126+
"propagate": False,
127+
},
128+
"uvicorn.error": {
129+
"level": "INFO",
130+
"handlers": ["console", "error_file"],
131+
"propagate": False,
132+
},
133+
# Silence watchfiles (used by uvicorn --reload)
134+
"watchfiles": {
135+
"level": "WARNING",
136+
"handlers": ["console"],
137+
"propagate": False,
138+
},
139+
"watchfiles.main": {
140+
"level": "WARNING",
141+
"handlers": ["console"],
142+
"propagate": False,
143+
},
144+
},
145+
}
146+
147+
148+
def setup_logging(log_level: str = "INFO", use_json: bool = False) -> None:
149+
"""Configure logging for the application.
150+
151+
Args:
152+
log_level: The minimum log level to capture (DEBUG, INFO, WARNING, ERROR, CRITICAL).
153+
use_json: Whether to use JSON formatting for console output.
154+
"""
155+
LOGGING_CONFIG["loggers"][""]["level"] = log_level
156+
LOGGING_CONFIG["loggers"]["app"]["level"] = log_level
157+
if use_json:
158+
LOGGING_CONFIG["loggers"][""]["handlers"] = [
159+
"console_json",
160+
"file",
161+
"error_file",
162+
]
163+
else:
164+
LOGGING_CONFIG["loggers"][""]["handlers"] = ["console", "file", "error_file"]
165+
logging.config.dictConfig(config=LOGGING_CONFIG)
166+
167+
168+
def get_logger(name: str) -> logging.Logger:
169+
"""Get a logger instance with the specified name.
170+
171+
Args:
172+
name: The name of the logger (typically __name__ of the module).
173+
174+
Returns:
175+
A configured logger instance.
176+
"""
177+
return logging.getLogger(name=name)

app/repositories/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def get_collection():
66
"""
77
Collection for user
88
"""
9-
return mongodb.db["users"]
9+
return mongodb.db["users"] # type: ignore
1010

1111

1212
async def find_user_by_username(username: str) -> UserInDB | None:

app/routers/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from fastapi import APIRouter
22

3-
from routers.user import user_router
3+
from routers.auth import auth_router
4+
from routers.me import me_router
45

56
router = APIRouter(prefix="/api/v1")
6-
router.include_router(router=user_router)
7+
router.include_router(router=auth_router)
8+
router.include_router(router=me_router)

0 commit comments

Comments
 (0)