chore: add project scaffold
This commit is contained in:
19
.env.example
Normal file
19
.env.example
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
APP_ENV=local
|
||||||
|
MONGO_URI=mongodb://localhost:27017
|
||||||
|
MONGO_DB_NAME=gapido_auth
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
RABBITMQ_URL=amqp://guest:guest@localhost:5672/
|
||||||
|
JWT_SECRET_KEY=change-me-use-a-long-random-secret
|
||||||
|
JWT_ISSUER=gapido-auth
|
||||||
|
ACCESS_TOKEN_TTL_SECONDS=900
|
||||||
|
REFRESH_TOKEN_TTL_SECONDS=604800
|
||||||
|
OTP_TTL_SECONDS=120
|
||||||
|
OTP_MAX_ATTEMPTS=5
|
||||||
|
OTP_REQUEST_LIMIT=3
|
||||||
|
OTP_REQUEST_WINDOW_SECONDS=300
|
||||||
|
KAVENEGAR_API_KEY=replace-with-real-key
|
||||||
|
KAVENEGAR_LOGIN_TEMPLATE=login-otp
|
||||||
|
ADMIN_MOBILE=989120000000
|
||||||
|
GRPC_HOST=0.0.0.0
|
||||||
|
GRPC_PORT=50051
|
||||||
|
|
||||||
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Test and tooling caches
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Local environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# IDE and OS
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker/local runtime data
|
||||||
|
mongo-data/
|
||||||
|
redis-data/
|
||||||
|
rabbitmq-data/
|
||||||
|
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM python:3.12-slim AS runtime
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONPATH=/app/src
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY pyproject.toml README.md ./
|
||||||
|
COPY src ./src
|
||||||
|
COPY proto ./proto
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir ".[dev]" \
|
||||||
|
&& python -m gapido_auth.tools.generate_proto
|
||||||
|
|
||||||
|
CMD ["python", "-m", "gapido_auth.transport.grpc.server"]
|
||||||
|
|
||||||
20
Makefile
Normal file
20
Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.PHONY: proto test lint typecheck compose-up compose-down
|
||||||
|
|
||||||
|
proto:
|
||||||
|
python -m gapido_auth.tools.generate_proto
|
||||||
|
|
||||||
|
test:
|
||||||
|
pytest
|
||||||
|
|
||||||
|
lint:
|
||||||
|
ruff check .
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
mypy
|
||||||
|
|
||||||
|
compose-up:
|
||||||
|
docker compose up --build
|
||||||
|
|
||||||
|
compose-down:
|
||||||
|
docker compose down --remove-orphans
|
||||||
|
|
||||||
54
README.md
Normal file
54
README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Gapido Backend Code Challenge
|
||||||
|
|
||||||
|
Python gRPC OTP authentication service with MongoDB, Redis, RabbitMQ, and a Kavenegar SMS adapter.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `auth-service`: async `grpc.aio` API for OTP login, token refresh, token revocation, and RBAC demo methods.
|
||||||
|
- `sms-worker`: RabbitMQ consumer that sends OTP messages through Kavenegar.
|
||||||
|
- MongoDB stores users and refresh-token sessions.
|
||||||
|
- Redis stores OTP hashes, TTL, verification attempts, and OTP request rate limits.
|
||||||
|
- RabbitMQ decouples authentication from SMS delivery.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
The gRPC service listens on `localhost:50051`. RabbitMQ management is available at `http://localhost:15672` with `guest` / `guest`.
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
python -m gapido_auth.tools.generate_proto
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
## gRPC Methods
|
||||||
|
|
||||||
|
- `RequestOtp`: public; creates a short-lived OTP and publishes an SMS job.
|
||||||
|
- `VerifyOtp`: public; verifies OTP and returns access and refresh tokens.
|
||||||
|
- `RefreshToken`: public; rotates refresh token and returns a new token pair.
|
||||||
|
- `RevokeRefreshToken`: authenticated; revokes a refresh token session.
|
||||||
|
- `PublicPing`: public.
|
||||||
|
- `UserOnly`: requires any authenticated active user.
|
||||||
|
- `AdminOnly`: requires an authenticated admin.
|
||||||
|
|
||||||
|
Protected calls use metadata:
|
||||||
|
|
||||||
|
```text
|
||||||
|
authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- OTP codes are generated with `secrets`, stored only as HMAC hashes in Redis, and expire after 120 seconds by default.
|
||||||
|
- OTP requests are rate-limited per mobile number and client identity.
|
||||||
|
- Refresh tokens are opaque random values; only SHA-256 hashes are persisted.
|
||||||
|
- Refresh tokens rotate on use.
|
||||||
|
- Kavenegar is hidden behind an adapter and mocked in tests.
|
||||||
|
|
||||||
68
docker-compose.yml
Normal file
68
docker-compose.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
services:
|
||||||
|
auth-service:
|
||||||
|
build: .
|
||||||
|
command: python -m gapido_auth.transport.grpc.server
|
||||||
|
environment:
|
||||||
|
MONGO_URI: mongodb://mongo:27017
|
||||||
|
MONGO_DB_NAME: ${MONGO_DB_NAME:-gapido_auth}
|
||||||
|
REDIS_URL: redis://redis:6379/0
|
||||||
|
RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
|
||||||
|
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-change-me-use-a-long-random-secret}
|
||||||
|
JWT_ISSUER: ${JWT_ISSUER:-gapido-auth}
|
||||||
|
ACCESS_TOKEN_TTL_SECONDS: ${ACCESS_TOKEN_TTL_SECONDS:-900}
|
||||||
|
REFRESH_TOKEN_TTL_SECONDS: ${REFRESH_TOKEN_TTL_SECONDS:-604800}
|
||||||
|
OTP_TTL_SECONDS: ${OTP_TTL_SECONDS:-120}
|
||||||
|
OTP_MAX_ATTEMPTS: ${OTP_MAX_ATTEMPTS:-5}
|
||||||
|
OTP_REQUEST_LIMIT: ${OTP_REQUEST_LIMIT:-3}
|
||||||
|
OTP_REQUEST_WINDOW_SECONDS: ${OTP_REQUEST_WINDOW_SECONDS:-300}
|
||||||
|
KAVENEGAR_LOGIN_TEMPLATE: ${KAVENEGAR_LOGIN_TEMPLATE:-login-otp}
|
||||||
|
ADMIN_MOBILE: ${ADMIN_MOBILE:-989120000000}
|
||||||
|
GRPC_HOST: 0.0.0.0
|
||||||
|
GRPC_PORT: 50051
|
||||||
|
ports:
|
||||||
|
- "50051:50051"
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_started
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
rabbitmq:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
sms-worker:
|
||||||
|
build: .
|
||||||
|
command: python -m gapido_auth.infrastructure.worker
|
||||||
|
environment:
|
||||||
|
MONGO_URI: mongodb://mongo:27017
|
||||||
|
REDIS_URL: redis://redis:6379/0
|
||||||
|
RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
|
||||||
|
KAVENEGAR_API_KEY: ${KAVENEGAR_API_KEY:-replace-with-real-key}
|
||||||
|
depends_on:
|
||||||
|
rabbitmq:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo:7
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3.13-management-alpine
|
||||||
|
ports:
|
||||||
|
- "5672:5672"
|
||||||
|
- "15672:15672"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
69
pyproject.toml
Normal file
69
pyproject.toml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
[project]
|
||||||
|
name = "gapido-auth-service"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Gapido backend code challenge: gRPC OTP auth service with MongoDB, Redis, RabbitMQ, and Kavenegar."
|
||||||
|
requires-python = ">=3.12,<3.14"
|
||||||
|
dependencies = [
|
||||||
|
"aio-pika==9.5.5",
|
||||||
|
"grpcio==1.68.1",
|
||||||
|
"grpcio-health-checking==1.68.1",
|
||||||
|
"grpcio-reflection==1.68.1",
|
||||||
|
"grpcio-tools==1.68.1",
|
||||||
|
"httpx==0.28.1",
|
||||||
|
"motor==3.6.0",
|
||||||
|
"protobuf==5.29.2",
|
||||||
|
"pydantic==2.10.4",
|
||||||
|
"pydantic-settings==2.7.1",
|
||||||
|
"pyjwt==2.10.1",
|
||||||
|
"redis==5.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"mypy==1.14.1",
|
||||||
|
"pytest==8.3.4",
|
||||||
|
"pytest-asyncio==0.25.2",
|
||||||
|
"ruff==0.9.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=75.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["src"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py312"
|
||||||
|
exclude = ["src/gapido_auth/generated/*"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "B", "UP", "ASYNC"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.12"
|
||||||
|
strict = true
|
||||||
|
packages = ["gapido_auth"]
|
||||||
|
exclude = ["src/gapido_auth/generated"]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = ["gapido_auth.generated.*"]
|
||||||
|
ignore_errors = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"grpc",
|
||||||
|
"grpc.*",
|
||||||
|
"grpc_tools",
|
||||||
|
"grpc_tools.*",
|
||||||
|
"grpc_health.*",
|
||||||
|
"grpc_reflection.*",
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
||||||
2
src/gapido_auth/__init__.py
Normal file
2
src/gapido_auth/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Gapido authentication service."""
|
||||||
|
|
||||||
1
src/gapido_auth/py.typed
Normal file
1
src/gapido_auth/py.typed
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Reference in New Issue
Block a user