From 3d8ba6d3be3b11bc447b529e7419835ce2b90315 Mon Sep 17 00:00:00 2001 From: tylen Date: Tue, 29 Oct 2024 21:00:35 +0000 Subject: [PATCH] initial commit --- .gitignore | 162 +++++++++++++++++++++++++ README.md | 7 ++ backend/Dockerfile.backend | 12 ++ backend/README.md | 5 + backend/entrypoint.sh | 6 + backend/requirements.txt | 3 + backend/run.sh | 13 ++ backend/src/auth/auth_api.py | 110 +++++++++++++++++ backend/src/auth/middleware.py | 35 ++++++ backend/src/dashboard_api.py | 13 ++ backend/src/database/compose.yaml | 11 ++ backend/src/database/praport_db_cli.py | 47 +++++++ backend/src/server.py | 55 +++++++++ 13 files changed, 479 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/Dockerfile.backend create mode 100644 backend/README.md create mode 100755 backend/entrypoint.sh create mode 100644 backend/requirements.txt create mode 100755 backend/run.sh create mode 100644 backend/src/auth/auth_api.py create mode 100644 backend/src/auth/middleware.py create mode 100644 backend/src/dashboard_api.py create mode 100644 backend/src/database/compose.yaml create mode 100644 backend/src/database/praport_db_cli.py create mode 100644 backend/src/server.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d381cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# ---> Python +# 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/README.md b/README.md new file mode 100644 index 0000000..26f1910 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# dungeon + +A Dungeon for your API keys, passwords, passphrases, secrets and what not! + +Dungeon is a a simple open-source application to manage your secrets either +through a Browser or a Cli. Simplicity, security and reliability are key +factors followed while developing this app. \ No newline at end of file diff --git a/backend/Dockerfile.backend b/backend/Dockerfile.backend new file mode 100644 index 0000000..980957e --- /dev/null +++ b/backend/Dockerfile.backend @@ -0,0 +1,12 @@ +FROM python:3.11-slim-buster + +ENV PYTHONPATH=/usr/local/lib + +COPY . /app +WORKDIR /app + +RUN pip3 install -r requirements.txt + +CMD ["/bin/sh", "entrypoint.sh"] + + diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..b3e9078 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,5 @@ +# Dungeon Backend Source + +This directory is intented for the source code of the Dungeon's backend service. + + diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100755 index 0000000..ce86293 --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,6 @@ +#! /bin/sh + +export DB_PASSWORD="UJ4tDsE39bT!" +export DB_SERVER="192.168.100.95" + +python3 -m flask --app /app/src/server.py run --host=0.0.0.0 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..45eb420 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +flask==3.0.2 +pymssql==2.3.0 +argon2-cffi diff --git a/backend/run.sh b/backend/run.sh new file mode 100755 index 0000000..49e1c8f --- /dev/null +++ b/backend/run.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Generate a random name for the container using uuidgen +container_name=$(uuidgen) + +# Build the Docker image with your project code and unit tests +docker build -t dungeon-backend -f Dockerfile.backend . + +# Run the Docker container with the unit tests and attach the output +docker run --tty --rm --name "$container_name" -p 5000:5000 dungeon-backend + +# Exit the script with the exit code of the container +exit $? diff --git a/backend/src/auth/auth_api.py b/backend/src/auth/auth_api.py new file mode 100644 index 0000000..4fcbc44 --- /dev/null +++ b/backend/src/auth/auth_api.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# encoding: utf-8 + +''' +auth.py is the functional api generator for users +''' + +from flask import jsonify, make_response, request +import uuid +from argon2 import PasswordHasher, exceptions + +USER_KEYS = ['email', 'password'] +class User(): + def __init__(self, json): + self.email = None + self.password = None + self.uid = None + if all(key in json for key in USER_KEYS): + self.email = json['email'] + self.password = json["password"] + + def hash_password(self): + if self.password: + ph = PasswordHasher() + self.password = ph.hash(self.password) + + def set_uid(self, uid): + if uid: + self.uid = int(uid) + + +class AuthApi: + def __init__(self, db_cli): + self.db = db_cli + + def valid_user(self, user): + return isinstance(user.email, str) and user.email.strip() != '' and \ + isinstance(user.password, str) and user.password.strip() != '' + + def email_exists(self, email): + checkUserEmail = f""" + SELECT * + FROM users + WHERE userEmail='{email}'; + """ + return True if self.db.query(checkUserEmail) else False + + def verify_user(self, user): + extractUserHash = f""" + SELECT * + FROM users + WHERE userEmail='{user.email}'; + """ + db_user_object = self.db.query(extractUserHash) + if not db_user_object: + return False + db_user_hash = db_user_object[0]['userPassword'] + user.set_uid(db_user_object[0]['UID']) + if db_user_hash: + ph = PasswordHasher() + try: + return ph.verify(db_user_hash, user.password) + except exceptions.VerifyMismatchError as e: + return False + return False + + def generate_session_token(self, user): + token = str(uuid.uuid4()) + createNewToken = f""" + INSERT INTO sessions (sessionToken, userID) + VALUES ('{token}', '{user.uid}') + """ + print(self.db.query(createNewToken, quiet=True)) + return token + + def create_user(self, user): + if not self.valid_user(user): + return jsonify({"message": "Corrupted data supplied."}), 400 + if self.email_exists(user.email): + return jsonify({"message": "Email already in use"}), 400 + user.hash_password() + initialiseUser = f""" + INSERT INTO users (userEmail, userPassword) + VALUES ('{user.email}', '{user.password}'); + """ + print(self.db.query(initialiseUser, quiet=True)) + return jsonify({'message': 'User added successfully!'}), 201 + + def create_session(self, user): + if not self.valid_user(user): + return jsonify({"message": "Corrupted data supplied."}), 400 + if self.verify_user(user): + res = make_response(jsonify({'message': f'New session token generated for {user.email}'}), 201) + res.set_cookie('SESSION_ID', self.generate_session_token(user)) + return res + else: + return jsonify({'message': 'Passwords don\'t match or user not found.'}), 400 + + def delete_session(self): + if "SESSION_ID" not in request.cookies: + return jsonify({'message': "No session token provided."}) + + deleteSession = f""" + DELETE FROM sessions WHERE + sessionToken = '{request.cookies.get("SESSION_ID")}' + """ + self.db.query(deleteSession, quiet=True) + res = make_response(jsonify({'message': 'Logout!'})) + res.set_cookie('SESSION_ID', expires=0) + return res diff --git a/backend/src/auth/middleware.py b/backend/src/auth/middleware.py new file mode 100644 index 0000000..71791fb --- /dev/null +++ b/backend/src/auth/middleware.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# encoding: utf-8 + +''' +middleware.py is the module for user auth middleware +''' + +from flask import Flask, request, make_response, g + +class AuthMiddleware: + def __init__(self, db_cli): + self.db = db_cli + + def extract_user_from_token(self, token): + tokenIsPresent = f""" + SELECT * + FROM sessions + WHERE sessionToken ='{token}' + """ + data = self.db.query(tokenIsPresent) + return data[0]['userID'] + + def validate_request(self): + if request.path == '/register' or request.path == '/login': + return None + session_token = request.cookies.get('SESSION_ID') + if not session_token: + return make_response('Unauthorized: Missing session_token cookie', 401) + userID = self.extract_user_from_token(session_token) + if not userID: + return make_response('Unauthorized: corrupted session_token cookie', 401) + + # Flask's global object for custom data + g.user_info = {'id': userID} + return None diff --git a/backend/src/dashboard_api.py b/backend/src/dashboard_api.py new file mode 100644 index 0000000..4c64e0c --- /dev/null +++ b/backend/src/dashboard_api.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# encoding: utf-8 + +''' +dashboard.py is the functional api generator for dashboard route +''' + +def construct_dashboard(): + return {'page': 'dashboard'} + +if __name__ == "__main__": + print("This is the dashboard.py file.") + diff --git a/backend/src/database/compose.yaml b/backend/src/database/compose.yaml new file mode 100644 index 0000000..680969a --- /dev/null +++ b/backend/src/database/compose.yaml @@ -0,0 +1,11 @@ +version: '3.8' + +services: + sql-server: + image: mcr.microsoft.com/mssql/server:latest + environment: + SA_PASSWORD: "UJ4tDsE39bT!" + ACCEPT_EULA: "Y" + ports: + - "1433:1433" + diff --git a/backend/src/database/praport_db_cli.py b/backend/src/database/praport_db_cli.py new file mode 100644 index 0000000..f1de445 --- /dev/null +++ b/backend/src/database/praport_db_cli.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# encoding: utf-8 + +''' +dungeon_db_cli.py is the module for managing teh Dungeon's database services. +''' + +import json +import pymssql +import os + +class DungeonDBClient: + def __init__(self): + self.db_server = os.environ.get('DB_SERVER') + print(self.db_server) + self.password = os.environ.get('DB_PASSWORD') + print(self.password) + self.database = 'dungeondev' + + self.connection = pymssql.connect( + server=self.db_server, + user='sa', + password=self.password, + as_dict=True, + autocommit=True + ) + + self.cursor = self.connection.cursor() + self.initialize_database() + + def create_database(self, db_name): + query = f"IF NOT EXISTS(SELECT * FROM sys.databases WHERE name='{db_name}') CREATE DATABASE {db_name};" + self.cursor.execute(query) + + def switch_database(self, db_name): + self.cursor.execute(f"USE {db_name};") + + def initialize_database(self): + self.create_database(self.database) + self.switch_database(self.database) + + def query(self, query_str, quiet=False): + self.cursor.execute(query_str) + if quiet: + return [] + return self.cursor.fetchall() + diff --git a/backend/src/server.py b/backend/src/server.py new file mode 100644 index 0000000..316c622 --- /dev/null +++ b/backend/src/server.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# encoding: utf-8 + +''' +server.py is the main source file for the Dungeon's backend service. +''' + +import json +from flask import Flask, redirect, url_for, request, jsonify +from auth.auth_api import User, AuthApi +from auth.middleware import AuthMiddleware +from dashboard_api import construct_dashboard +from database.dungeon_db_cli import DungeonDBClient + +app = Flask(__name__) +database = DungeonDBClient() +auth_api = AuthApi(database) +middleware = AuthMiddleware(database) + +@app.before_request +def validator(): + res = middleware.validate_request() + if res: + return res + +@app.route('/register', methods=['POST']) +def register(): + if request.is_json: + new_user = User(request.json) + return auth_api.create_user(new_user) + else: + return jsonify({'error': 'Request must contain JSON data'}), 400 + +@app.route('/login', methods=['POST']) +def login(): + if request.is_json: + new_user = User(request.json) + return auth_api.create_session(new_user) + else: + return jsonify({'error': 'Request must contain JSON data'}), 400 + +@app.route('/logout') +def logout(): + return auth_api.delete_session() + +@app.route('/dashboard') +def dashboard(): + return jsonify(construct_dashboard()) + +@app.route('/projects') +def projects(): + return jsonify(construct_projects()) + +if __name__ == "__main__": + app.run(debug=True)