commit 0f605c4f42b18625a4584df1a6b98986e1eba645 Author: DarkSlein Date: Wed Jan 29 00:42:37 2025 +0300 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..763bdb6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,142 @@ +.git +Dockerfile +.DS_Store +.gitignore +.dockerignore + +/credentials +/cache +/store + +/node_modules + +# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore + +# General +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..591e530 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt /app +RUN pip3 install -r requirements.txt + +COPY . . + +ENV FLASK_APP=src/main.py + +CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5000"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2458e4 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Talkpal - Backend + +This is the backend service for a **Meeting Room Booking System**, providing RESTful API endpoints for managing rooms, events, and users. +It is built using **Python** and integrates with a database for persistent storage. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..0ba9e5c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,9 @@ +version: '2.1' +services: + talkpal-backend: + container_name: talkpal-backend + image: talkpal-backend + build: . + network_mode: host + volumes: + - "./src:/app/src" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c70a816 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +blinker==1.8.2 +click==8.1.8 +flask==3.0.3 +importlib-metadata==8.5.0 +itsdangerous==2.2.0 +jinja2==3.1.5 +MarkupSafe==2.1.5 +werkzeug==3.0.6 +zipp==3.20.2 diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..900d289 --- /dev/null +++ b/src/app.py @@ -0,0 +1,13 @@ +from flask import Flask +from pymongo import MongoClient +from flask_jwt_extended import JWTManager +from singletons.database_singleton import DatabaseSingleton + +app = Flask(__name__) +app.config['JWT_SECRET_KEY'] = Config.JWT_SECRET_KEY +jwt = JWTManager(app) +client = MongoClient(Config.MONGO_URI) +db = DatabaseSingleton.get_instance() + +if __name__ == '__main__': + app.run(debug=True) diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..95e76a3 --- /dev/null +++ b/src/config.py @@ -0,0 +1,6 @@ +import os + +class Config: + DB_NAME = os.getenv('DB_NAME', 'meeting_reservation') + JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'your_jwt_secret_key') + MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost:27017/') \ No newline at end of file diff --git a/src/controllers/reservation_controller.py b/src/controllers/reservation_controller.py new file mode 100644 index 0000000..40b0303 --- /dev/null +++ b/src/controllers/reservation_controller.py @@ -0,0 +1,65 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity + +from middlewares.validate_request import validate_request +from dtos.reservation_dtos import CreateReservationDTO, UpdateReservationDTO +from models.reservation_model import Reservation +from repos.reservation_repo import ReservationRepository + +reservation_blueprint = Blueprint('reservation', __name__) + +@reservation_blueprint.route('/', methods=['POST']) +@validate_request(CreateReservationDTO) +@jwt_required() +def create_reservation(): + current_user = get_jwt_identity() + reservation = Reservation(**request.json.dict(), creator=current_user) + ReservationRepository.insert(reservation) + return jsonify({"message": "Reservation created successfully"}) + +@reservation_blueprint.route('/', methods=['DELETE']) +@jwt_required() +def cancel_reservation(reservation_id): + result = ReservationRepository.delete(reservation_id) + if not result or result.deleted_count == 0: + return jsonify({"error": "Reservation not found"}), 404 + return jsonify({"message": "Reservation cancelled"}) + +@reservation_blueprint.route('/', methods=['PUT']) +@validate_request(UpdateReservationDTO) +@jwt_required() +def change_reservation(reservation_id): + data = request.json + try: + result = ReservationRepository.update(reservation_id, data) + if not result or result.matched_count == 0: + return jsonify({"error": "Reservation not found"}), 404 + except: + return jsonify({"error": "Invalid reservation ID"}), 400 + + return jsonify({"message": "Reservation updated"}) + +@reservation_blueprint.route('/', methods=['GET']) +@jwt_required() +def get_reservation(reservation_id): + try: + reservation = ReservationRepository.get_by_id() + if not reservation: + return jsonify({"error": "Reservation not found"}), 404 + except: + return jsonify({"error": "Invalid reservation ID"}), 400 + + return jsonify(reservation) + +@reservation_blueprint.route('/', methods=['GET']) +@jwt_required() +def list_reservations(): + filters = { + "start_date": request.args.get("start_date"), + "end_date": request.args.get("end_date"), + "room_id": request.args.get("room_id") + } + filters = {k: v for k, v in filters.items() if v is not None} + + reservations = ReservationRepository.list_all(filters) + return jsonify(reservations) \ No newline at end of file diff --git a/src/controllers/user_controller.py b/src/controllers/user_controller.py new file mode 100644 index 0000000..ccc1fa3 --- /dev/null +++ b/src/controllers/user_controller.py @@ -0,0 +1,29 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import create_access_token + +from middlewares.validate_request import validate_request +from dtos.user_dtos import RegisterUserDTO, LoginUserDTO +from repos.user_repo import UserRepo +from models.user_model import User + +user_blueprint = Blueprint('user', __name__) + +@user_blueprint.route('/register', methods=['POST']) +@validate_request(RegisterUserDTO) +def register_user(): + data = request.json + if UserRepo.find_by_username(data.username): + return jsonify({"error": "User already exists"}), 409 + user = User(**data.dict()) + UserRepo.insert(user) + return jsonify({"message": "User registered successfully"}) + +@user_blueprint.route('/login', methods=['POST']) +@validate_request(LoginUserDTO) +def login_user(): + data = request.json + user = UserRepo.find_by_username(data['username']) + if user: + access_token = create_access_token(identity=user['username']) + return jsonify(access_token=access_token) + return jsonify({"error": "Invalid credentials"}), 401 \ No newline at end of file diff --git a/src/dtos/reservation_dtos.py b/src/dtos/reservation_dtos.py new file mode 100644 index 0000000..870ce57 --- /dev/null +++ b/src/dtos/reservation_dtos.py @@ -0,0 +1,17 @@ +from typing import Optional +from pydantic import BaseModel +from datetime import datetime + +class CreateReservationDTO(BaseModel): + title: str + description: Optional[str] = None + room_id: str + start_date: datetime + finish_date: datetime + +class UpdateReservationDTO(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + room_id: Optional[str] = None + start_date: Optional[datetime] = None + finish_date: Optional[datetime] = None \ No newline at end of file diff --git a/src/dtos/user_dtos.py b/src/dtos/user_dtos.py new file mode 100644 index 0000000..92ceb35 --- /dev/null +++ b/src/dtos/user_dtos.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel, EmailStr + +class RegisterUserDTO(BaseModel): + username: str + password: str + email: EmailStr + first_name: str + last_name: str + department: str + position: str + is_admin: bool = False + +class LoginUserDTO(BaseModel): + username: str + password: str \ No newline at end of file diff --git a/src/middlewares/validate_request.py b/src/middlewares/validate_request.py new file mode 100644 index 0000000..f139ae6 --- /dev/null +++ b/src/middlewares/validate_request.py @@ -0,0 +1,17 @@ +from functools import wraps +from flask import request, jsonify +from pydantic import ValidationError + +def validate_request(dto_class): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + data = request.get_json() + dto = dto_class(**data) + return func(*args, **kwargs) + except ValidationError as e: + print(e) + return jsonify({"error": e.errors()}), 400 + return wrapper + return decorator \ No newline at end of file diff --git a/src/models/reservation_model.py b/src/models/reservation_model.py new file mode 100644 index 0000000..14aae56 --- /dev/null +++ b/src/models/reservation_model.py @@ -0,0 +1,8 @@ +class Reservation: + def __init__(self, title, description, room_id, creator, start_date, finish_date): + self.title = title + self.description = description + self.room_id = room_id + self.creator = creator + self.start_date = start_date + self.finish_date = finish_date \ No newline at end of file diff --git a/src/models/room_model.py b/src/models/room_model.py new file mode 100644 index 0000000..e52fead --- /dev/null +++ b/src/models/room_model.py @@ -0,0 +1,4 @@ +class Room: + def __init__(self, title, office_id): + self.title = title + self.office_id = office_id \ No newline at end of file diff --git a/src/models/user_model.py b/src/models/user_model.py new file mode 100644 index 0000000..cf7a8f6 --- /dev/null +++ b/src/models/user_model.py @@ -0,0 +1,9 @@ +class User: + def __init__(self, username, email, first_name, last_name, department, position, is_admin=False): + self.username = username + self.email = email + self.first_name = first_name + self.last_name = last_name + self.department = department + self.position = position + self.is_admin = is_admin \ No newline at end of file diff --git a/src/repos/reservation_repo.py b/src/repos/reservation_repo.py new file mode 100644 index 0000000..197bd60 --- /dev/null +++ b/src/repos/reservation_repo.py @@ -0,0 +1,40 @@ +from singletons.database_singleton import DatabaseSingleton + +class ReservationRepository: + db = DatabaseSingleton.get_instance() + + @classmethod + def insert(cls, reservation): + cls.db.reservation.insert_one(reservation.__dict__) + + @classmethod + def delete(cls, reservation_id): + try: + result = cls.db.reservation.delete_one({"_id": ObjectId(reservation_id)}) + return result + except: + return None + + @classmethod + def update(cls, reservation_id, data): + try: + result = cls.db.reservation.update_one({"_id": ObjectId(reservation_id)}, {"$set": data}) + return result + except: + return None + + @classmethod + def get_by_id(cls): + return list(cls.db.reservation.find({"_id": ObjectId(reservation_id)})) + + @classmethod + def list_all(cls, filters=None): + query = {} + if filters: + if "start_date" in filters: + query["start_date"] = {"$gte": filters["start_date"]} + if "end_date" in filters: + query["start_date"] = {"$lte": filters["end_date"]} + if "room_id" in filters: + query["room_id"] = filters["room_id"] + return list(cls.db.reservation.find(query)) \ No newline at end of file diff --git a/src/repos/user_repo.py b/src/repos/user_repo.py new file mode 100644 index 0000000..00854de --- /dev/null +++ b/src/repos/user_repo.py @@ -0,0 +1,12 @@ +from singletons.database_singleton import DatabaseSingleton + +class UserRepo: + db = DatabaseSingleton.get_instance() + + @classmethod + def insert(cls, user): + cls.db.user.insert_one(user.__dict__) + + @classmethod + def find_by_username(cls, username): + return cls.db.user.find_one({"username": username}) \ No newline at end of file diff --git a/src/singletons/app_singleton.py b/src/singletons/app_singleton.py new file mode 100644 index 0000000..f83a0ba --- /dev/null +++ b/src/singletons/app_singleton.py @@ -0,0 +1,12 @@ +from config import Config +from flask import Flask + +class AppSingleton: + _instance = None + + @staticmethod + def get_instance(): + if AppSingleton._instance is None: + AppSingleton._instance = Flask(__name__) + AppSingleton._instance['JWT_SECRET_KEY'] = Config.JWT_SECRET_KEY + return AppSingleton._instance[Config.DB_NAME] \ No newline at end of file diff --git a/src/singletons/database_singleton.py b/src/singletons/database_singleton.py new file mode 100644 index 0000000..f810517 --- /dev/null +++ b/src/singletons/database_singleton.py @@ -0,0 +1,11 @@ +from config import Config +from pymongo import MongoClient + +class DatabaseSingleton: + _instance = None + + @staticmethod + def get_instance(): + if DatabaseSingleton._instance is None: + DatabaseSingleton._instance = MongoClient(Config.MONGO_URI) + return DatabaseSingleton._instance[Config.DB_NAME] \ No newline at end of file