Merge branch 'feature/auth' into 'master'

Feature/auth

See merge request team-15/project!1
This commit is contained in:
Timur
2025-02-28 21:30:28 +00:00
21 changed files with 562 additions and 0 deletions
+360
View File
@@ -0,0 +1,360 @@
name: project_name
services:
backend:
build:
context: ./services/backend
dockerfile: Dockerfile
depends_on:
backend-initdb:
restart: false
condition: service_completed_successfully
required: true
postgres:
restart: false
condition: service_healthy
required: true
redis:
restart: false
condition: service_healthy
required: true
minio:
restart: false
condition: service_healthy
required: true
env_file:
- path: ./infrastructure/backend/.env.template
required: true
- path: ./infrastructure/backend/.env
required: false
ports:
- name: web
target: 8080
published: 8080
host_ip: 127.0.0.1
protocol: tcp
restart: unless-stopped
backend-initdb:
build:
context: ./services/backend
dockerfile: Dockerfile
command: ./scripts/initdb
depends_on:
postgres:
restart: false
condition: service_healthy
required: true
redis:
restart: false
condition: service_healthy
required: true
minio:
restart: false
condition: service_healthy
required: true
env_file:
- path: ./infrastructure/backend/.env.template
required: true
- path: ./infrastructure/backend/.env
required: false
backend-staticfiles:
build:
context: ./services/backend
dockerfile: Dockerfile.staticfiles
env_file:
- path: ./infrastructure/backend/.env.template
required: true
- path: ./infrastructure/backend/.env
required: false
healthcheck:
test: ["CMD", "service", "nginx", "status", "||", " exit 1"]
interval: 1m30s
timeout: 5s
start_period: 5s
start_interval: 2s
retries: 5
ports:
- name: web
target: 80
published: 13241
host_ip: 127.0.0.1
protocol: tcp
restart: unless-stopped
backend-celery-worker:
build:
context: ./services/backend
dockerfile: Dockerfile
command: celery -A config worker -l INFO
depends_on:
redis:
restart: false
condition: service_healthy
required: true
env_file:
- path: ./infrastructure/backend/.env.template
required: true
- path: ./infrastructure/backend/.env
required: false
healthcheck:
test: ["CMD", "celery", "-A", "config", "inspect", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
start_interval: 2s
restart: unless-stopped
celery-exporter:
image: docker.io/danihodovic/celery-exporter:0.11.1
depends_on:
redis:
restart: false
condition: service_healthy
required: true
env_file:
- path: ./infrastructure/celery-exporter/.env.template
required: true
- path: ./infrastructure/celery-exporter/.env
required: false
restart: unless-stopped
redis:
image: docker.io/redis:7-alpine3.21
command: redis-server /usr/local/etc/redis/redis.conf
configs:
- source: redis_config
target: /usr/local/etc/redis/redis.conf
env_file:
- path: ./infrastructure/redis/.env.template
required: true
- path: ./infrastructure/redis/.env
required: false
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1m30s
timeout: 5s
start_period: 5s
start_interval: 2s
retries: 5
restart: unless-stopped
shm_size: 4mb
volumes:
- type: volume
source: redis_data
target: /data
redis-exporter:
image: docker.io/oliver006/redis_exporter:v1.67.0-alpine
depends_on:
redis:
restart: false
condition: service_healthy
required: true
env_file:
- path: ./infrastructure/redis-exporter/.env.template
required: true
- path: ./infrastructure/redis-exporter/.env
required: false
restart: unless-stopped
shm_size: 4mb
postgres:
image: docker.io/postgres:17-alpine3.21
configs:
- source: postgres_config
target: /etc/postgresql/postgresql.conf
env_file:
- path: ./infrastructure/postgres/.env.template
required: true
- path: ./infrastructure/postgres/.env
required: false
healthcheck:
test: ["CMD", "pg_isready"]
interval: 1m30s
timeout: 5s
start_period: 5s
start_interval: 2s
retries: 5
oom_kill_disable: true
restart: unless-stopped
secrets:
- source: postgres_password
target: /run/secrets/postgres_password
shm_size: 128mb
volumes:
- type: volume
source: postgres_data
target: /var/lib/postgresql/data
postgres-exporter:
image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0
depends_on:
postgres:
restart: false
condition: service_healthy
required: true
env_file:
- path: ./infrastructure/postgres-exporter/.env.template
required: true
- path: ./infrastructure/postgres-exporter/.env
required: false
restart: unless-stopped
shm_size: 4mb
pgadmin:
image: docker.io/dpage/pgadmin4:9
configs:
- source: pgadmin_servers
target: /pgadmin4/servers.json
depends_on:
postgres:
restart: false
condition: service_healthy
required: true
env_file:
- path: ./infrastructure/pgadmin/.env.template
required: true
- path: ./infrastructure/pgadmin/.env
required: false
healthcheck:
test: ["CMD", "wget", "-O", "-", "http://localhost:80/misc/ping"]
interval: 1m30s
timeout: 5s
start_period: 5s
start_interval: 2s
retries: 5
ports:
- name: web
target: 80
published: 13242
host_ip: 127.0.0.1
protocol: tcp
restart: unless-stopped
secrets:
- source: pgadmin_password
target: /run/secrets/pgadmin_password
shm_size: 4mb
volumes:
- type: volume
source: pgadmin_data
target: /var/lib/pgadmin
grafana:
image: docker.io/grafana/grafana-oss:11.5.0
configs:
- source: grafana_config
target: /usr/share/grafana/conf/defaults.ini
entrypoint: ["/etc/grafana/scripts/entrypoint.sh"]
healthcheck:
test: ["CMD", "wget", "-O", "-", "http://localhost:3000/api/health"]
interval: 1m30s
timeout: 5s
start_period: 5s
start_interval: 2s
retries: 5
ports:
- name: web
target: 3000
published: 13243
host_ip: 127.0.0.1
protocol: tcp
restart: unless-stopped
shm_size: 4mb
volumes:
- type: volume
source: grafana_data
target: /var/lib/grafana
- type: bind
source: ./infrastructure/grafana/provisioning
target: /etc/grafana/provisioning
- type: bind
source: ./infrastructure/grafana/scripts
target: /etc/grafana/scripts
minio:
command: server --console-address ":9001"
image: docker.io/minio/minio:RELEASE.2025-02-03T21-03-04Z
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1m30s
timeout: 5s
start_period: 5s
start_interval: 2s
retries: 5
env_file:
- path: ./infrastructure/minio/.env.template
required: true
- path: ./infrastructure/minio/.env
required: false
ports:
- name: api
target: 9000
published: 13244
host_ip: 127.0.0.1
protocol: tcp
- name: console
target: 9001
published: 13245
host_ip: 127.0.0.1
protocol: tcp
restart: unless-stopped
volumes:
- type: volume
source: minio_data
target: /data
prometheus:
image: docker.io/prom/prometheus:v3.1.0
command:
- "--config.file=/etc/prometheus/prometheus.yaml"
configs:
- source: prometheus_config
target: /etc/prometheus/prometheus.yaml
healthcheck:
test: ["CMD", "wget", "-O", "-", "http://localhost:9090/-/healthy"]
interval: 1m30s
timeout: 5s
start_period: 5s
start_interval: 2s
retries: 5
ports:
- name: web
target: 9090
published: 13246
host_ip: 127.0.0.1
protocol: tcp
restart: unless-stopped
shm_size: 4mb
volumes:
- type: volume
source: prometheus_data
target: /prometheus
volumes:
redis_data:
postgres_data:
pgadmin_data:
grafana_data:
prometheus_data:
minio_data:
configs:
redis_config:
file: ./infrastructure/redis/redis.conf
postgres_config:
file: ./infrastructure/postgres/postgresql.conf
pgadmin_servers:
file: ./infrastructure/pgadmin/servers.json
grafana_config:
file: ./infrastructure/grafana/grafana.ini
prometheus_config:
file: ./infrastructure/prometheus/prometheus.yaml
secrets:
postgres_password:
file: ./infrastructure/postgres/password
pgadmin_password:
file: ./infrastructure/pgadmin/password
+27
View File
@@ -0,0 +1,27 @@
import datetime
from typing import Optional, Any
import jwt
from django.conf import settings
from django.http import HttpRequest
from ninja.security import HttpBearer
from apps.users.models import User
class BearerAuth(HttpBearer):
def authenticate(self, request: HttpRequest, token: str) -> Optional[Any]:
data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
if data["exp"] < datetime.datetime.now().timestamp():
return None
user = User.objects.get(id=data["id"])
return user
@staticmethod
def generate_jwt(user: User) -> str:
data = {
"exp": (datetime.datetime.now() + datetime.timedelta(days=365)).timestamp(),
"id": str(user.id)
}
return jwt.encode(data, settings.SECRET_KEY, algorithm="HS256")
+4
View File
@@ -17,6 +17,10 @@ router.add_router(
"ping",
ping_router,
)
router.add_router(
"",
users_router,
)
for exception, handler in handlers.exception_handlers:
+26
View File
@@ -0,0 +1,26 @@
from ninja import Schema, ModelSchema
from apps.users.models import User
class TokenSchema(Schema):
token: str
class RegisterSchema(ModelSchema):
class Meta:
model = User
fields = ["email", "username", "password"]
class LoginSchema(ModelSchema):
class Meta:
model = User
fields = ["email", "password"]
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ["email", "username"]
+56
View File
@@ -0,0 +1,56 @@
from ninja import Router
from ninja.errors import AuthenticationError
from api.v1.users.schemas import LoginSchema, RegisterSchema, TokenSchema, UserSchema
from api.v1.auth import BearerAuth
from api.v1.schemas import BadRequestError, ForbiddenError, NotFoundError
from apps.users.models import User
router = Router(tags=["users"])
@router.post(
path="/sign-up",
response={
201: TokenSchema,
400: BadRequestError,
}
)
def sign_up(request, data: RegisterSchema):
user = User(**data.dict())
user.full_clean()
user.save()
token = BearerAuth.generate_jwt(user)
return 201, TokenSchema(token=token)
@router.post(
path="/sign-in",
response={
200: TokenSchema,
400: BadRequestError,
401: ForbiddenError,
}
)
def sign_in(request, data: LoginSchema):
user = User.objects.filter(email=data.email).first()
if not user:
raise AuthenticationError
if user.password != data.password:
raise AuthenticationError
token = BearerAuth.generate_jwt(user)
return 200, TokenSchema(token=token)
@router.get(
path="/user/{user_id}",
response={
200: UserSchema,
400: BadRequestError,
404: NotFoundError,
}
)
def get_user(request, user_id: str):
...
@@ -0,0 +1,7 @@
from django.apps import AppConfig
class CompetitionsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.competitions'
label = 'competitions'
@@ -0,0 +1,28 @@
from django.db import models
from apps.core.models import BaseModel
class CompetitionType(models.TextChoices):
SOLO = "solo"
class CompetitionPartipicationType(models.TextChoices):
EDU = "edu"
COMPETITIVE = "competitive"
class Competition(BaseModel):
title = models.CharField(max_length=100, verbose_name="Название")
description = models.TextField(verbose_name="Описание")
image_url = models.FileField(verbose_name="Изображение соревнования")
due_to = models.DateTimeField(verbose_name="Дедлайн участия")
type = models.CharField(max_length=10, choices=CompetitionType.choices,
verbose_name="Тип участия")
participation_type = models.CharField(max_length=10, choices=CompetitionPartipicationType.choices,
verbose_name="Тип соревнования")
class Meta:
verbose_name = "соревнование"
verbose_name_plural = "соревнования"
+9
View File
@@ -0,0 +1,9 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.users'
label = 'users'
@@ -0,0 +1,28 @@
# Generated by Django 5.1.6 on 2025-02-28 20:46
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Почта')),
('username', models.SlugField(unique=True, verbose_name='Юзернейм')),
('password', models.TextField(verbose_name='Пароль')),
],
options={
'verbose_name': 'пользователь',
'verbose_name_plural': 'пользователи',
},
),
]
+16
View File
@@ -0,0 +1,16 @@
from django.db import models
from apps.core.models import BaseModel
class User(BaseModel):
email = models.EmailField(unique=True, verbose_name="Почта")
username = models.SlugField(unique=True, verbose_name="Юзернейм")
password = models.TextField(verbose_name="Пароль")
def __str__(self):
return self.username
class Meta:
verbose_name = "пользователь"
verbose_name_plural = "пользователи"
+1
View File
@@ -443,6 +443,7 @@ INSTALLED_APPS = [
"minio_storage",
# Internal apps
"apps.core",
"apps.users",
]
# GUID
View File
View File