mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-22 23:17:09 +00:00
Merge branch 'feature/auth' into 'master'
Feature/auth See merge request team-15/project!1
This commit is contained in:
+360
@@ -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
|
||||
@@ -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")
|
||||
@@ -17,6 +17,10 @@ router.add_router(
|
||||
"ping",
|
||||
ping_router,
|
||||
)
|
||||
router.add_router(
|
||||
"",
|
||||
users_router,
|
||||
)
|
||||
|
||||
|
||||
for exception, handler in handlers.exception_handlers:
|
||||
|
||||
@@ -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"]
|
||||
@@ -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 = "соревнования"
|
||||
@@ -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': 'пользователи',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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 = "пользователи"
|
||||
@@ -443,6 +443,7 @@ INSTALLED_APPS = [
|
||||
"minio_storage",
|
||||
# Internal apps
|
||||
"apps.core",
|
||||
"apps.users",
|
||||
]
|
||||
|
||||
# GUID
|
||||
|
||||
Reference in New Issue
Block a user