Skip to content

Commit

Permalink
Merge pull request #4 from moevm/dev
Browse files Browse the repository at this point in the history
Initial app
  • Loading branch information
Dlexeyn authored Nov 8, 2024
2 parents 53a4520 + ac6895b commit 3ff2905
Show file tree
Hide file tree
Showing 70 changed files with 14,524 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Flask template
instance/*
!instance/.gitignore
.webassets-cache
.idea
__pycache__/ *.py[cod]
__pycache__/
Binary file modified assets/nosql.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=password123
MONGO_INITDB_DATABASE=fastapi

DATABASE_URL=mongodb://admin:password123@mongo_db:27017/?authSource=admin

ACCESS_TOKEN_EXPIRES_IN=15
REFRESH_TOKEN_EXPIRES_IN=60
JWT_ALGORITHM=HS256
JWT_SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7

CLIENT_ORIGIN=http://app-frontend:8080

JWT_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBMExNZXQvc296T1RneVliTitZbXczTnlUYkFRemU4eDRQVHk2TmN4K3BVc1RTemh1CmVab0RnRU0wZmtVVHVjd2kyUERaWXdYckhrTmFDTVdPbHZEV3Uzc1RoS3VUOG8zVEYyUy9hRmJ5eE5jenpuNG0KUHIwU3k2VUZKM29XZHBGakNpNnFFMjNlZm9MZDhIanJ1Rlc0eXRyZkxJQU1uT0RQdVk5R3VadkduT1B2Z2VoKwp5aHo5K0NWd2s2bGRaSzZzYVZLZDBPbm9MU1g5U1REWmJrU3R1d0VqVG9ld0ZqMjNwdW14b3hLNCtLM2czUFpjCnJEcHZFdFkxeE1hbEJyVjVIRDYwQlc1bmo4bGgrbW5JQUlrWTJBSkRRYzV5MFJpRlZjcy9JVmxPQk5HdXJBZ3gKYlJGQ1VXOVBRdjVCVGxwODZlNlRqNVhPL2thR3ZhdXRVSS9yVVFJREFRQUJBb0lCQUhxaTl3Y242S2JXVEIxQQpRT05FL1JBYjhlbEVZcmg1dzZKQWdDL0M5aHpOakEza29FNkdxVTRDcitNUFZuTVV1Tm1BVmszeEdXT1VNbUQ3Ckxqb1dWaWlmUHkzejRTRmtJOG9ZWXIyK2NqUW5QWU0yNytSb0dKWmdaekgyZFNMQmRsQnljWEN2WEZJOU5vdnIKa3FDa2hzMTFaalZ4SFhoR1J1cUVmZ3Z0dFAxVmlBVUkxTkVSUjBzbzBrMzlCUUo3NVpTRkVsckhKSW0ybTRQVQpBMEpQaHFZNDNhLzRsNU5FVTMrdGlvanNzTUpnNzkzclZ5aEFLQVFaMnNUbWwwcG1vQURUNXYyYk1aZDFYSk9ZCjlxSElJNUwrQW85QkJvdktxcjNtdWNzQWpnbm9zR3hzU0FvdzdLbjA0Qm5idm1hSU9uMVdTQXN3dS92NGRmM1oKc25qUzNBRUNnWUVBN3ppWlJqMWJaeVMrYkwrNkFiTktKeW9DMjIzOGt0QzlRcFJ3WFVRdzZTUnoyWDlDVkhZWgpmNUwzY0FVOTZkSTh4STlpbjVoY2ZUUnpDVVBoWnBtNnZadHhpaHBNcTNEVlFkOUcxTHJMc1JhQ0thaGkvMzdQCldTSWcxbzV6SEVDWnZGVHVsNjRETlkvbkR2aTV1R3h6T2NYUVBJNHUwN1BQSld6RElsK1JIUkVDZ1lFQTMxWjgKVnBCUmtGeU8rZkhDakRkSyt3WXFiWlg5VDlNWkx6VFdqNFVFQ2xKUCtRdFF4Sml0VXdVZlNwZ1poUE5yQTJINApXT1dHaDI5RSsyTVpaZ0xtWTNmSndUc2xXenlUTVNuajZUVzZTUWZNeWJqRm1ROXE3bmhGV1cwVVhGVnJybm5FCnQxZ2g5QWp2Z1cvWXh4MHh4N0cwUUNzWXVGSGNxdWh0QVIzRzZrRUNnWUVBbWloV0ZiNktmWEJmU29OUEliTmgKTU5YUTI0a0lQN0JHbG5aRDVzWi80bTQ4UGNmVmZjcFJhalhTUUowUUpmTDJlQkNTbEpoQjJlbUh6RXV6SUVRbQo0L01jK3NzeDV6VWlLSDN6RGptRjlBdTJPNVFvbjg4ZlhhZ3hrekpmR2JERG9XcjJDa2I0Q0hkQWhoUmcwbWtJCjVBMEd3VTg2Ky9BZXFGWnJkV1l5aEpFQ2dZQW5NWGsrZzdNY24zR2o0VTVmNXZBc24wZGcxZHFQWUo5aHptYjgKNXIzdnhjUXRFMVJJTy9ibXc5WmE4OWcrb2EwYytkdG9WbGRHZXp0aTFtQkZxNnFjdUEvYTdqTS9FS0ZRRm1iZApyVVVVdmQ2dFk5U2hhTGcrUXpNQVg0a2NMdzFub0F6cWsvZlphSndIWGdadjR1cXlmYmdCTHM3MndiNzA2emI5CjVDamRRUUtCZ1FEbk5SaUFHZXBBOW9PRURiejIwSExrQmd2VGIxTUtmbWVxQmthUnRuNGFURlpxcGJPWFNYci8KL005UHhCMklFSm5kd2FHWFRvVWdsYm5QNWhGNTdCOEprOFFFWDUvWVJtZGVYT2FYSnBxZGo3WlFxdUhscnBzdgpuRE4xZzRDMkRmdHlaMG1vT3BPdEhZeVRlbkpjbmlPTjhPTnVKTHpDOVN5NDJOWUFDVkY2T3c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==
JWT_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEwTE1ldC9zb3pPVGd5WWJOK1ltdwozTnlUYkFRemU4eDRQVHk2TmN4K3BVc1RTemh1ZVpvRGdFTTBma1VUdWN3aTJQRFpZd1hySGtOYUNNV09sdkRXCnUzc1RoS3VUOG8zVEYyUy9hRmJ5eE5jenpuNG1QcjBTeTZVRkozb1dkcEZqQ2k2cUUyM2Vmb0xkOEhqcnVGVzQKeXRyZkxJQU1uT0RQdVk5R3VadkduT1B2Z2VoK3loejkrQ1Z3azZsZFpLNnNhVktkME9ub0xTWDlTVERaYmtTdAp1d0VqVG9ld0ZqMjNwdW14b3hLNCtLM2czUFpjckRwdkV0WTF4TWFsQnJWNUhENjBCVzVuajhsaCttbklBSWtZCjJBSkRRYzV5MFJpRlZjcy9JVmxPQk5HdXJBZ3hiUkZDVVc5UFF2NUJUbHA4NmU2VGo1WE8va2FHdmF1dFVJL3IKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t
14 changes: 14 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM python:3.12-alpine

RUN mkdir app
WORKDIR /app

COPY requirements.txt .
COPY .env .

RUN python3 -m pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "-m", "main"]

26 changes: 26 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os

from dotenv import load_dotenv
from pydantic_settings import BaseSettings

load_dotenv()

class Settings(BaseSettings):
DATABASE_URL: str
MONGO_INITDB_DATABASE: str
JWT_PUBLIC_KEY: str
JWT_PRIVATE_KEY: str

ACCESS_TOKEN_EXPIRES_IN: int
REFRESH_TOKEN_EXPIRES_IN: int
JWT_ALGORITHM: str
JWT_SECRET_KEY: str

CLIENT_ORIGIN: str

class Config:
env_file = "../.env"
env_file_encoding = "utf-8"
extra = "allow"

settings = Settings()
46 changes: 46 additions & 0 deletions backend/dao/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from bson import ObjectId
from datetime import datetime, timezone
from database import db
from schemas.user import UserCreateSchema, UserDao, UserBaseSchema


def object_id_to_str(user) -> dict[str, str]:
user['id'] = str(user['_id'])
return user


async def find_user_by_email(email) -> UserDao | None:
users = db.get_collection('user')
user = await users.find_one({'email': email})
if not user:
return None
user = object_id_to_str(user)
return UserDao(**user)


async def find_user_by_id(user_id: ObjectId) -> UserDao | None:
users = db.get_collection('user')
user = await users.find_one({'_id': ObjectId(user_id)})
if not user:
return None
user = object_id_to_str(user)
return UserDao(**user)


async def find_all_users() -> list[UserBaseSchema]:
users_collection = db.get_collection('user')
users = users_collection.find()
users_list = []
for user in await users.to_list():
user = object_id_to_str(user)
users_list.append(UserBaseSchema(**user))
return users_list


async def create_user(user: UserCreateSchema):
users_collection = db.get_collection('user')
user_dict = user.model_dump()
user_dict['created_at'] = datetime.now(timezone.utc)
user_dict['updated_at'] = datetime.now(timezone.utc)
result = await users_collection.insert_one(user_dict)
return str(result.inserted_id)
11 changes: 11 additions & 0 deletions backend/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from motor.motor_asyncio import AsyncIOMotorClient

from config import settings

client = AsyncIOMotorClient(settings.DATABASE_URL, serverSelectionTimeoutMS=5000)
db = client.get_database(settings.MONGO_INITDB_DATABASE)





31 changes: 31 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import uvicorn
import logging
import time
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware

from database import db
from routers import auth, user

app = FastAPI()
app.database = db

allowed_origins = [
"http://localhost:8080",
"http://localhost:8000",
"http://localhost",
]

app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "OPTIONS", "PATCH", "DELETE"],
allow_headers=["*"]
)

app.include_router(auth.router, tags=['Auth'], prefix="/api/auth")
app.include_router(user.router, tags=['User'], prefix="/api/user")

if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
12 changes: 12 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
fastapi==0.115.4
fastapi_jwt_auth==0.5.0
pydantic
uvicorn~=0.32.0
pydantic[email]
passlib~=1.7.4
python-dotenv~=1.0.1
bcrypt==4.0.1
motor
passlib[bcrypt]
pydantic_settings
python-jose
67 changes: 67 additions & 0 deletions backend/routers/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from typing import List
from fastapi import APIRouter, HTTPException, status, Response, Depends

from config import settings
from dao.user import find_all_users, find_user_by_email, create_user
from schemas.user import UserCreateSchema, UserLoginSchema, UserDao, UserBaseSchema
from utils.password import verify_password, create_access_token, get_password_hash
from utils.token import get_current_user

router = APIRouter()
ACCESS_TOKEN_EXPIRES_IN = settings.ACCESS_TOKEN_EXPIRES_IN
ALGORITHM = settings.JWT_ALGORITHM
SECRET_KEY = settings.JWT_SECRET_KEY


@router.post("/register")
async def register_user(user_data: UserCreateSchema) -> dict:
user = await find_user_by_email(user_data.email.lower())
if user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail='Пользователь уже существует'
)

user_data.password = get_password_hash(user_data.password)
user_id = await create_user(user_data)
return {"status": "success", "user_id": user_id}


@router.get("/get")
async def get_users() -> dict[str, List[UserBaseSchema]]:
users = await find_all_users()
return {"users": users}


@router.post("/login")
async def login_user(response: Response, user_data: UserLoginSchema) -> dict:
user = await find_user_by_email(user_data.email.lower())
if not user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Пользователя не существует'
)
if not verify_password(user_data.password, user.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Неверный пароль'
)
access_token = create_access_token({"sub": user.id})
response.set_cookie(
key="users_access_token",
value=access_token,
secure=False,
httponly=True,
samesite='lax')
return {'access_token': access_token, 'refresh_token': None}


@router.get("/me/")
async def get_me(user_data: UserDao = Depends(get_current_user)):
return user_data


@router.post("/logout/")
async def logout_user(response: Response):
response.delete_cookie(key="users_access_token")
return {'message': 'Пользователь успешно вышел из системы'}
3 changes: 3 additions & 0 deletions backend/routers/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from fastapi import APIRouter

router = APIRouter()
9 changes: 9 additions & 0 deletions backend/schemas/token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pydantic import BaseModel


class Token(BaseModel):
access_token: str
token_type: str

class TokenData(BaseModel):
username: str | None = None
38 changes: 38 additions & 0 deletions backend/schemas/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from datetime import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, EmailStr, Field


class Role(str, Enum):
admin = "Администратор"
foreman = "Прораб"
worker = "Рабочий"
customer = "Заказчик"


class UserCreateSchema(BaseModel):
email: EmailStr = Field(..., description="Электронная почта пользователя")
name: str = Field(..., min_length=1, max_length=100, description="Имя пользователя")
lastname: str = Field(..., min_length=1, max_length=100, description="Фамилия пользователя")
middlename: Optional[str] = Field(None, max_length=100, description="Отчество пользователя")
password: str = Field(min_length=8)
role: Role

class Config:
from_attributes = True


class UserBaseSchema(UserCreateSchema):
id: str
created_at: datetime | None = None
updated_at: datetime | None = None


class UserLoginSchema(BaseModel):
email: EmailStr = Field(..., description="Электронная почта пользователя")
password: str = Field(min_length=8)


class UserDao(UserBaseSchema):
password: str = Field(min_length=8)
34 changes: 34 additions & 0 deletions backend/serializers/userSerializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
def user_entity(user) -> dict:
return {
"id": str(user["_id"]),
"name": user["name"],
"email": user["email"],
"role": user["role"],
"verified": user["verified"],
"password": user["password"],
"created_at": user["created_at"],
"updated_at": user["updated_at"]
}


def user_response_entity(user) -> dict:
return {
"id": str(user["_id"]),
"name": user["name"],
"email": user["email"],
"role": user["role"],
"created_at": user["created_at"],
"updated_at": user["updated_at"]
}


def embedded_user_response(user) -> dict:
return {
"id": str(user["_id"]),
"name": user["name"],
"email": user["email"],
}


def user_list_entity(users) -> list:
return [user_entity(user) for user in users]
21 changes: 21 additions & 0 deletions backend/utils/password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from datetime import datetime, timezone, timedelta
from jose import jwt
from passlib.context import CryptContext

from config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def get_password_hash(password: str) -> str:
return pwd_context.hash(password)


def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)

def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(days=30)
to_encode.update({"exp": expire})
encode_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encode_jwt
37 changes: 37 additions & 0 deletions backend/utils/token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from bson import ObjectId
from fastapi import Request, HTTPException, status, Depends
from jose import jwt, JWTError
from datetime import datetime, timezone

from config import settings
from dao.user import find_user_by_id


def get_token(request: Request):
token = request.cookies.get('users_access_token')
if not token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Token not found')
return token


async def get_current_user(token: str = Depends(get_token)):
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Токен не валидный!')

expire: str = payload.get('exp')
expire_time = datetime.fromtimestamp(int(expire), tz=timezone.utc)
if (not expire) or (expire_time < datetime.now(timezone.utc)):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Токен истек')

user_id = payload.get('sub')
if not user_id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Не найден ID пользователя')

user = await find_user_by_id(ObjectId(str(user_id)))
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='User not found')

del user.password
return user
Loading

0 comments on commit 3ff2905

Please sign in to comment.