Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ helm/charts/

# CMake и сборка
CMakeCache.txt
CMakeLists.txt
CMakeFiles/
CMakeScripts/
Makefile
Expand Down
21 changes: 0 additions & 21 deletions LICENSE

This file was deleted.

39 changes: 26 additions & 13 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,31 @@

<div align="center">

![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
![Status](https://img.shields.io/badge/Status-Active-success.svg)
[![GitHub stars](https://img.shields.io/github/stars/CS151512/STREMO?style=social)](https://github.com/CS151512/STREMO)
[![Licence](https://img.shields.io/github/license/Ileriayo/markdown-badges?style=for-the-badge)](./LICENSE)

<br>

![C++](https://img.shields.io/badge/C++-00599C?style=flat&logo=c%2B%2B&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white)
![Next.js](https://img.shields.io/badge/Next.js-000000?style=flat&logo=nextdotjs&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-2496ED?style=flat&logo=docker&logoColor=white)
![K3s](https://img.shields.io/badge/K3s-FFC61C?style=flat&logo=k3s&logoColor=black)
![Terraform](https://img.shields.io/badge/Terraform-844FBA?style=flat&logo=terraform&logoColor=white)
![Apache Kafka](https://img.shields.io/badge/Kafka-231F20?style=flat&logo=apachekafka&logoColor=white)
![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1?style=flat&logo=postgresql&logoColor=white)
![Redis](https://img.shields.io/badge/Redis-DC382D?style=flat&logo=redis&logoColor=white)
![Git](https://img.shields.io/badge/Git-F05032?style=flat&logo=git&logoColor=white)
![Figma](https://img.shields.io/badge/figma-%23F24E1E.svg?style=for-the-badge&logo=figma&logoColor=white)
![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)
![ClickHouse](https://img.shields.io/badge/ClickHouse-FFCC01?style=for-the-badge&logo=clickhouse&logoColor=white)
![C++](https://img.shields.io/badge/c++-%2300599C.svg?style=for-the-badge&logo=c%2B%2B&logoColor=white)
![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)
![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)
![Terraform](https://img.shields.io/badge/terraform-%235835CC.svg?style=for-the-badge&logo=terraform&logoColor=white)
![Apache Kafka](https://img.shields.io/badge/Apache%20Kafka-000?style=for-the-badge&logo=apachekafka)
![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white)
![Redis](https://img.shields.io/badge/redis-%23DD0031.svg?style=for-the-badge&logo=redis&logoColor=white)
![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white)
![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white)
![Next JS](https://img.shields.io/badge/Next-black.svg?style=for-the-badge&logo=next.js&logoColor=white)
![LaTeX](https://img.shields.io/badge/latex-%23008080.svg?style=for-the-badge&logo=latex&logoColor=white)
![Postman](https://img.shields.io/badge/Postman-FF6C37?style=for-the-badge&logo=postman&logoColor=white)
![Grafana](https://img.shields.io/badge/grafana-%23F46800.svg?style=for-the-badge&logo=grafana&logoColor=white)
![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?style=for-the-badge&logo=Prometheus&logoColor=white)
![CMake](https://img.shields.io/badge/CMake-%23008FBA.svg?style=for-the-badge&logo=cmake&logoColor=white)
![Nginx](https://img.shields.io/badge/nginx-%23009639.svg?style=for-the-badge&logo=nginx&logoColor=white)

</div>

Expand Down Expand Up @@ -144,4 +154,7 @@ pdflatex main.tex
```

---
**by finnik & s1gmagor**
**by finnik && s1gmagor**

* **Оригинальный дизайн:** [где-то Dribble, я просто уже не могу найти :(](https://dribbble.com/search/shots/popular/mobile?q=streaming-service)
* **Статус:** Данный проект разработан исключительно в образовательных целях (университетская расчетно-графическая работа) и не имеет коммерческой выгоды. Права на исходный визуальный концепт принадлежат его автору. Не надо бить палками...
5 changes: 0 additions & 5 deletions backend/service/auth-service/CMakeLists.txt

This file was deleted.

Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions backend/service/auth-service/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""auth-service - microservice for authentication & authorization"""
__version__ = "0.1.0"
1 change: 1 addition & 0 deletions backend/service/auth-service/src/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""API: HTTP & gRPC interfaces"""
1 change: 1 addition & 0 deletions backend/service/auth-service/src/api/grpc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""gRPC API"""
Empty file.
Empty file.
Empty file.
1 change: 1 addition & 0 deletions backend/service/auth-service/src/api/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""HTTP API"""
1 change: 1 addition & 0 deletions backend/service/auth-service/src/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Core module: configuration, security & exceptions"""
61 changes: 61 additions & 0 deletions backend/service/auth-service/src/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from functools import lru_cache
from pydantic import PostgresDsn, RedisDsn, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)

# App
APP_NAME: str = "auth-service"
APP_ENV: str = "development"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
APP_ENV: str = "development"
# from typing import Literal
APP_ENV: Literal["development", "staging", "production"] = "development"

DEBUG: bool = False

# HTTP Server
HTTP_HOST: str = "0.0.0.0"
HTTP_PORT: int = 8000

# gRPC Server
GRPC_HOST: str = "0.0.0.0"
GRPC_PORT: int = 50051

# JWT
JWT_SECRET_KEY: str
JWT_ALGORITHM: str = "HS256"
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7

# PostgreSQL
POSTGRES_DSN: PostgresDsn

# Redis
REDIS_DSN: RedisDsn

# Kafka
KAFKA_BOOTSTRAP_SERVERS: str
KAFKA_TOPIC_USER_EVENTS: str = "user-events"
KAFKA_CONSUMER_GROUP_ID: str = "auth-service-group"

# Password hashing
BCRYPT_ROUNDS: int = 12

@field_validator("APP_ENV")
@classmethod
def validate_app_env(cls, v: str) -> str:
allowed = {"development", "staging", "production"}
if v not in allowed:
raise ValueError(f"APP_ENV must be one of {allowed}")
return v
Comment on lines +46 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@field_validator("APP_ENV")
@classmethod
def validate_app_env(cls, v: str) -> str:
allowed = {"development", "staging", "production"}
if v not in allowed:
raise ValueError(f"APP_ENV must be one of {allowed}")
return v


@property
def is_production(self) -> bool:
return self.APP_ENV == "production"


@lru_cache
def get_settings() -> Settings:
return Settings()
70 changes: 70 additions & 0 deletions backend/service/auth-service/src/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from http import HTTPStatus


class AppException(Exception):
"""Base application exception."""

status_code: int = HTTPStatus.INTERNAL_SERVER_ERROR
detail: str = "An unexpected error occurred."

def __init__(self, detail: str | None = None) -> None:
self.detail = detail or self.__class__.detail
super().__init__(self.detail)


# --- Auth ---

class AuthException(AppException):
"""Base auth exception."""


class InvalidCredentialsError(AuthException):
status_code = HTTPStatus.UNAUTHORIZED
detail = "Invalid email or password."


class InvalidTokenError(AuthException):
status_code = HTTPStatus.UNAUTHORIZED
detail = "Token is invalid or has expired."


class TokenExpiredError(AuthException):
status_code = HTTPStatus.UNAUTHORIZED
detail = "Token has expired."


class InsufficientPermissionsError(AuthException):
status_code = HTTPStatus.FORBIDDEN
detail = "You do not have permission to perform this action."


# --- User ---

class UserException(AppException):
"""Base user exception."""


class UserNotFoundError(UserException):
status_code = HTTPStatus.NOT_FOUND
detail = "User not found."


class UserAlreadyExistsError(UserException):
status_code = HTTPStatus.CONFLICT
detail = "A user with this email already exists."


class UserInactiveError(UserException):
status_code = HTTPStatus.FORBIDDEN
detail = "This user account is inactive."


# --- Validation ---

class ValidationException(AppException):
status_code = HTTPStatus.UNPROCESSABLE_ENTITY
detail = "Validation failed."


class WeakPasswordError(ValidationException):
detail = "Password does not meet the minimum security requirements."
91 changes: 91 additions & 0 deletions backend/service/auth-service/src/core/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from datetime import datetime, timedelta, timezone
from typing import Any
from uuid import UUID

import bcrypt
from jose import JWTError, jwt

from src.core.config import get_settings
from src.core.exceptions import InvalidTokenError, TokenExpiredError

settings = get_settings()


# PASSWORD HASHING
def hash_password(plain: str) -> str:
"""Return a bcrypt hash of *plain*."""
salt = bcrypt.gensalt(rounds=settings.BCRYPT_ROUNDS)
return bcrypt.hashpw(plain.encode(), salt).decode()


def verify_password(plain: str, hashed: str) -> bool:
"""Return True if *plain* matches *hashed*."""
return bcrypt.checkpw(plain.encode(), hashed.encode())


# JWT
_ACCESS = "access"
_REFRESH = "refresh"


def _create_token(
subject: str | UUID,
token_type: str,
expires_delta: timedelta,
extra_claims: dict[str, Any] | None = None,
) -> str:
now = datetime.now(tz=timezone.utc)
payload: dict[str, Any] = {
"sub": str(subject),
"type": token_type,
"iat": now,
"exp": now + expires_delta,
}
if extra_claims:
payload.update(extra_claims)
return jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)


def create_access_token(user_id: str | UUID, extra_claims: dict[str, Any] | None = None) -> str:
return _create_token(
subject=user_id,
token_type=_ACCESS,
expires_delta=timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES),
extra_claims=extra_claims,
)


def create_refresh_token(user_id: str | UUID) -> str:
return _create_token(
subject=user_id,
token_type=_REFRESH,
expires_delta=timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS),
)


def decode_token(token: str, expected_type: str = _ACCESS) -> dict[str, Any]:
"""
Decode and validate *token*.
Raises:
TokenExpiredError: if the token is past its expiry.
InvalidTokenError: if the token is malformed or has an unexpected type.
"""
try:
payload = jwt.decode(
token,
settings.JWT_SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM],
)
except jwt.ExpiredSignatureError:
raise TokenExpiredError()
except JWTError:
raise InvalidTokenError()

if payload.get("type") != expected_type:
raise InvalidTokenError("Unexpected token type.")

return payload


def decode_refresh_token(token: str) -> dict[str, Any]:
return decode_token(token, expected_type=_REFRESH)
5 changes: 5 additions & 0 deletions backend/service/auth-service/src/domain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Domain: Business entities"""

from src.domain.entities import RefreshToken, User, UserRole, UserStatus

__all__ = ["User", "UserRole", "UserStatus", "RefreshToken"]
Loading
Loading