diff --git a/backend/api/admin.py b/backend/api/admin.py index cff9373..d605caf 100644 --- a/backend/api/admin.py +++ b/backend/api/admin.py @@ -1,3 +1,4 @@ +from api.helpers import requires_auth from config import app, db from database.organization import Organization from database.role import Role, Roles @@ -26,10 +27,9 @@ def show_board(org_id): @app.route('/organization/make_admin/', methods=['POST']) +@requires_auth def make_admin(org_id): - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() + sessionObj = request.session current_role = Role.query.filter_by(organization_id=org_id, user_id=sessionObj.user_id).first() print("DEBUG....") print(current_role) @@ -64,10 +64,9 @@ def make_admin(org_id): @app.route('/admins/remove_admin/', methods=['DELETE']) +@requires_auth def remove_admin(org_id): - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() + sessionObj = request.session current_role = Role.query.filter_by(organization_id=org_id, user_id=sessionObj.user_id).first() print("DEBUG....") print(current_role) @@ -91,4 +90,3 @@ def remove_admin(org_id): else: return jsonify(message='You do not allow to remove admin', success=False) - diff --git a/backend/api/event.py b/backend/api/event.py index b3130dc..e36fd8f 100644 --- a/backend/api/event.py +++ b/backend/api/event.py @@ -37,11 +37,7 @@ def get_all_unpublished_event(org_id): events = db.session.query(Event).filter(or_(Event.phase == EventPhase.INITIALIZED, Event.phase == EventPhase.ARCHIVED), Event.organization_id == org_id).all() - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() - - user = sessionObj.user + user = request.user if user.roles.filter(Role.role == Roles.MEMBER): return {'message': 'You are not allowed to see unpublished event', 'success': False} @@ -59,9 +55,7 @@ def get_all_unpublished_event(org_id): @app.route('/event/add/', methods=['POST']) @requires_auth def create_event(org_id): - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() + sessionObj = request.session creator_id = sessionObj.user_id roleObj = db.session.query(Role).filter(Role.user_id == creator_id, @@ -131,9 +125,7 @@ def delete_event(event_id): message="The event does not exists.") else: # Get the session token - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() + sessionObj = request.session # print("...SESSION TOKEN...") # print(sessionObj) user_role = db.session.query(Role).filter(Role.organization_id == event.organization_id, @@ -167,9 +159,7 @@ def register_event(event_id): return jsonify(success=False, message="The event does not exists.") else: - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() + sessionObj = request.session #print("...SESSION TOKEN...") #print(sessionObj) register_id = sessionObj.user_id @@ -212,9 +202,7 @@ def unregister_event(event_id): # Verified the organization id existed or not event_obj = Event.query.filter_by(event_id=event_id).first() event_name = event_obj.event_name - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() + sessionObj = request.session register_obj = db.session.query(Registration).filter(Registration.register_id == sessionObj.user_id, Registration.event_id == event_id).first() @@ -231,9 +219,7 @@ def unregister_event(event_id): @app.route('/event/approve/', methods=['PUT']) @requires_auth def approve_event(event_id): - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() + sessionObj = request.session creator_id = sessionObj.user_id eventObj = db.session.query(Event).filter(Event.event_id == event_id).first() @@ -261,9 +247,7 @@ def approve_event(event_id): @app.route('/event/cancel/', methods=['PUT']) @requires_auth def cancel_event(event_id): - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() + sessionObj = request.session creator_id = sessionObj.user_id eventObj = db.session.query(Event).filter(Event.event_id == event_id).first() if eventObj is None or not eventObj: @@ -297,6 +281,62 @@ def cancel_event(event_id): @requires_auth @requires_json # TODO: Centralize validation on event fields input def edit_event(event_id, **kwargs): + ''' + Edit an existing event + --- + tags: + - event + parameters: + - in: body + name: body + required: true + schema: + required: + - event_name + - start_date + - end_date + - theme + - perks + - categories + - info + properties: + event_name: + type: string + start_date: + type: string + description: An ISO 8601 formatted datetime string + end_date: + type: string + description: An ISO 8601 formatted datetime string + theme: + type: string + perks: + type: string + categories: + type: string + info: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + session: + type: object + properties: + token: + type: string + expires: + type: string + ''' + user = request.user event = db.session.query(Event).filter(Event.event_id == event_id).first() role = user.roles.filter( @@ -318,4 +358,3 @@ def edit_event(event_id, **kwargs): db.session.commit() return {'success': True, 'message': '', 'event': EventSchema().dump(event)} - diff --git a/backend/api/helpers.py b/backend/api/helpers.py index d2e54c9..3df7470 100644 --- a/backend/api/helpers.py +++ b/backend/api/helpers.py @@ -6,12 +6,19 @@ from database.session import Session from database.user import User +if DEBUG: from config import swagger def all_helper(): return None def requires_auth(func): + # Automatic Swagger documentation magic + if not hasattr(func, 'specs_dict'): func.specs_dict = {} + func.specs_dict.update({ + 'security': [{'bearerAuth': []}] + }) + @wraps(func) def wrapper(*args, **kwargs): raw_auth = request.headers.get('Authorization') @@ -49,13 +56,35 @@ def wrapper(*args, **kwargs): def validate_types(expected): def _validate_types(func): + # Automatic Swagger documentation magic + if not hasattr(func, 'specs_dict'): func.specs_dict = {} + if not 'parameters' in func.specs_dict: func.specs_dict['parameters'] = [{ + 'in': 'body', + 'name': 'body', + 'required': True + }] + + TYPE_MAP = {str: 'string', int: 'integer', float: 'number', + bool: 'boolean'} + + schema = { + 'required': list(expected.keys()), + 'properties': {} + } + + for key, data in expected.items(): + schema['properties'][key] = {'type': TYPE_MAP[data['type']]} + + func.specs_dict['parameters'][0].update({'schema': schema}) + + @wraps(func) def wrapper(*args, **body): # Check each type and add to invalid if not correct invalid = {} - for key, type in expected.items(): - if (not key in body or not isinstance(body[key], type)): - invalid[key] = type + for key, data in expected.items(): + if (not key in body or not isinstance(body[key], data['type'])): + invalid[key] = data['type'] # if there's any invalid fields, respond with an error if (len(invalid) > 0): diff --git a/backend/api/user.py b/backend/api/user.py index 115d29b..5745855 100644 --- a/backend/api/user.py +++ b/backend/api/user.py @@ -18,8 +18,34 @@ @app.route('/login', methods=['POST']) @requires_json -@validate_types({'email': str, 'password': str}) +@validate_types({'email': {'type': str}, 'password': {'type': str}}) def login(email, password, **kwargs): + ''' + Authenticate user credentials + --- + tags: + - user + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + session: + type: object + properties: + token: + type: string + expires: + type: string + ''' + try: email_results = validate_email(email) email = '{0}@{1}'.format(email_results.local_part.lower(), email_results.domain) @@ -45,8 +71,34 @@ def login(email, password, **kwargs): @app.route('/signup', methods=['POST']) @requires_json -@validate_types({'name': str, 'email': str, 'password': str}) +@validate_types({'name': {'type': str}, 'email': {'type': str}, 'password': {'type': str}}) def signup(name, email, password, **kwargs): + ''' + Creates a new user + --- + tags: + - user + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + session: + type: object + properties: + token: + type: string + expires: + type: string + ''' + # Validate name min_length = 2 max_length = User.__table__.c['name'].type.length @@ -104,12 +156,7 @@ def signup(name, email, password, **kwargs): #@requires_json def change_profile(): #print('hi') - token = request.headers.get('Authorization') - token = token.split()[1] - sessionObj = db.session.query(Session).filter(Session.session_id == token).first() - user_id = sessionObj.user_id - - userObj = db.session.query(User).filter(User.user_id == user_id).first() + userObj = request.user input_data = request.json @@ -146,6 +193,37 @@ def change_profile(): @app.route('/user/me', methods=['GET']) @requires_auth def get_me(): + ''' + Retrieves a user's information + --- + tags: + - user + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + user: + type: object + properties: + name: + type: string + roles: + type: object + properties: + organization_id: + type: string + role: + type: string + ''' + user_data = request.user.dump() return jsonify({'success': True, 'message': '', 'user': user_data}) @@ -153,8 +231,27 @@ def get_me(): @app.route('/user/me', methods=['DELETE']) @requires_auth @requires_json -@validate_types({'password': str}) +@validate_types({'password': {'type': str}}) def delete_me(password, **kwargs): + ''' + Deletes a user and their associated data + --- + tags: + - user + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + ''' + user = request.user if not user.verify_password(password): return jsonify({'success': False, 'message': 'Invalid password'}) @@ -199,11 +296,7 @@ def get_registered_orgs(): @app.route('/user/me/events', methods=['GET']) @requires_auth def get_registered_events(): - token = request.headers.get('Authorization') - token = token.split()[1] - session_obj = db.session.query(Session).filter(Session.session_id == token).first() - curr_user = session_obj.user_id - register_obj = db.session.query(Registration).filter(Registration.register_id == curr_user).all() + register_obj = db.session.query(Registration).filter(Registration.register_id == request.user.user_id).all() #print("...DEBUGGING...") #print(register_obj) events = [] @@ -221,12 +314,10 @@ def get_registered_events(): @app.route('/user/me/managed_organization', methods=['GET']) @requires_auth def get_managed_organizations(): - token = request.headers.get('Authorization') - token = token.split()[1] - session_obj = db.session.query(Session).filter(Session.session_id == token).first() - managed_obj = db.session.query(Role).filter(Role.user_id == session_obj.user_id, + user = request.user + managed_obj = db.session.query(Role).filter(Role.user_id == user.user_id, or_(Role.role == Roles.ADMIN, Role.role == Roles.CHAIRMAN)).all() - print (session_obj.user_id) + print (user.user_id) print(managed_obj) managed_orgs = [] if managed_obj: @@ -240,4 +331,3 @@ def get_managed_organizations(): return jsonify({'success': True, 'message': 'Showing organizations managed by you', 'managed_orgs': managed_orgs}) else: return jsonify({'success': False, 'message': 'You do not manage any organizations'}) - diff --git a/backend/config.py b/backend/config.py index da63b0f..83ec8b2 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,11 +1,15 @@ import os +import subprocess from dotenv import load_dotenv +from flasgger import Swagger from flask import Flask from flask_marshmallow import Marshmallow from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy load_dotenv(verbose=True) +VERSION = "1.0.0" # SEMVER +GIT_COMMIT = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip() # assume we're in production unless told otherwise DEBUG = (os.getenv('DEBUG', 'false') == 'true') @@ -21,6 +25,22 @@ app.config['SQLALCHEMY_ECHO'] = DEBUG app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +if DEBUG: + app.config['SWAGGER'] = { + 'title': app.import_name, + 'version': '{version}-{commit}'.format(version=VERSION, commit=GIT_COMMIT), + 'openapi': '3.0.2' + } + + swagger = Swagger(app, template={ + 'securityDefinitions': { + 'bearerAuth': { + 'type': 'http', + 'scheme': 'bearer' + } + } + }) + db = SQLAlchemy(app) ma = Marshmallow(app) #migrate = Migrate(app, db) diff --git a/backend/error/helpers.py b/backend/error/helpers.py index 3bb5612..f7366d1 100644 --- a/backend/error/helpers.py +++ b/backend/error/helpers.py @@ -10,7 +10,7 @@ if DEBUG: handler = colorlog.StreamHandler() - handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)s:%(name)s %(message)s')) + handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s[%(levelname)s]%(reset)s %(name)s: %(message)s')) handler.setLevel(logging.DEBUG) else: formatter = logging.Formatter(logging.BASIC_FORMAT) # Update later for deployment diff --git a/backend/error/http.py b/backend/error/http.py index 4124dc6..7cf0616 100644 --- a/backend/error/http.py +++ b/backend/error/http.py @@ -28,7 +28,7 @@ def handle_http_exception(e): elif DEBUG: # Error caught by Flask body['message'] = '[DEBUG] {0}'.format(debug_message) - body['traceback'] = format_tb(traceback) + body['traceback'] = traceback elif 'message' in e.description: # Specific user displayable error body['message'] = e.description['message'] diff --git a/backend/main.py b/backend/main.py index 5b4534d..7e7e161 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,20 +1,60 @@ import os -from config import app, db +from random import shuffle +from config import app, db, GIT_COMMIT, VERSION import api.organization import api.user - import api.admin - import api.event - import error.http import error.internal +authors = [ + {'name': 'Josh Garde', 'email': 'jagarde@cpp.edu', 'github': '@joshgarde'}, + {'name': 'Matthew Tootoonchi', 'github': '@mtootoonchi'}, + {'name': 'Dai Vuong', 'github': '@paulminhdai'}, + {'name': 'Phuong Nguyen', 'github': '@pnguyen-16'}, + {'name': 'Daeyoung Hwang', 'github': '@dyhwang7'}, + {'name': 'Khuong Le', 'github': 'lekhuong07'} +] @app.route('/') def hello_world(): - return 'Hello, World!' + ''' + Get the application's version string + --- + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + version: + type: string + description: A SEMVER version string that includes the git HEAD + authors: + type: array + description: The people behind the magic + items: + type: object + properties: + name: + type: string + github: + type: string + email: + type: string + quote: + type: string + ''' + shuffle(authors) + return { + 'version': '{version}-{commit}'.format(version=VERSION, commit=GIT_COMMIT), + 'authors': authors, + 'quote': 'Written by the CSSPI Fall 2020 backend team' + } if __name__ == "__main__": port = os.getenv("PORT", 9090) diff --git a/backend/requirements.txt b/backend/requirements.txt index bec782d..fcf18f8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,3 +10,4 @@ email-validator==1.1.1 marshmallow-sqlalchemy==0.24.0 marshmallow-enum==1.5.1 colorlog==4.6.0 +flasgger==0.9.5