diff --git a/server/app/__init__.py b/server/app/__init__.py index 8b9293e319c9e6fec0788ac9a9dad2bdcf82f571..7b5a8add00813f9d44c3ae1db7f21d9910e63fd6 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -1,20 +1,13 @@ -from flask import Flask -from flask_bcrypt import Bcrypt -from flask_jwt_extended.jwt_manager import JWTManager -from flask_sqlalchemy import SQLAlchemy +from flask import Flask, redirect, request -from app.database import Base - -bcrypt = Bcrypt() -jwt = JWTManager() -db = SQLAlchemy(model_class=Base) - -from app.database import models +import app.core.models as models +from app.core import bcrypt, db, jwt def create_app(config_name="configmodule.DevelopmentConfig"): app = Flask(__name__) app.config.from_object(config_name) + app.url_map.strict_slashes = False with app.app_context(): @@ -22,9 +15,16 @@ def create_app(config_name="configmodule.DevelopmentConfig"): jwt.init_app(app) db.init_app(app) - from app.api import api_blueprint + from app.apis import flask_api + + flask_api.init_app(app) + + @app.before_request + def clear_trailing(): + rp = request.path + if rp != "/" and rp.endswith("/"): + return redirect(rp[:-1]) - app.register_blueprint(api_blueprint, url_prefix="/api") return app diff --git a/server/app/api/competitions.py b/server/app/api/competitions.py deleted file mode 100644 index f89b20b9b2d5e49a22014c4b948a202947ce7dd3..0000000000000000000000000000000000000000 --- a/server/app/api/competitions.py +++ /dev/null @@ -1,46 +0,0 @@ -### -# Admin stuff placed here for later use -# No need to implement this before the application is somewhat done -### - -import app.database.controller as dbc -import app.utils.http_codes as codes -from app import db -from app.api import admin_required, api_blueprint, object_response, query_response, text_response -from app.database.models import Blacklist, City, Competition, Role, User -from app.utils.validator import edit_user_schema, login_schema, register_schema, validate_object -from flask import request -from flask_jwt_extended import ( - create_access_token, - create_refresh_token, - get_jwt_identity, - get_raw_jwt, - jwt_refresh_token_required, - jwt_required, -) - - -@api_blueprint.route("/competitions/<int:id>", methods=["GET"]) -@jwt_required -def get(id): - item = Competition.query.filter(Competition.id == id).first() - if not item: - return text_response("Competition not found", codes.NOT_FOUND) - - return query_response(item) - - -@api_blueprint.route("/competitions", methods=["POST"]) -@jwt_required -def create(): - json_dict = request.get_json(force=True) - - name = json_dict.get("name") - city_id = json_dict.get("city_id") - year = json_dict.get("year", 2020) - style_id = json_dict.get("style_id", 0) - - dbc.add.competition(name, year, style_id, city_id) - - item = Competition.query.filter(Competition.name == name).first() - return query_response(item) diff --git a/server/app/api/users.py b/server/app/api/users.py deleted file mode 100644 index 6b0e3984b330e7e7a16d34b3b68769b79238c8ef..0000000000000000000000000000000000000000 --- a/server/app/api/users.py +++ /dev/null @@ -1,211 +0,0 @@ -import datetime - -import app.database.controller as dbc -import app.utils.http_codes as codes -from app import db -from app.api import admin_required, api_blueprint, object_response, query_response, text_response -from app.database.models import Blacklist, City, Role, User -from app.utils.validator import edit_user_schema, login_schema, register_schema, validate_object -from flask import request -from flask_jwt_extended import ( - create_access_token, - create_refresh_token, - get_jwt_identity, - get_raw_jwt, - jwt_refresh_token_required, - jwt_required, -) - - -##Helpers -def _get_current_user(): - return User.query.filter_by(id=get_jwt_identity()).first() - - -def _create_token(user): - expires = datetime.timedelta(days=7) - claims = {"role": user.role.name} - - return create_access_token(identity=user.id, expires_delta=expires, user_claims=claims) - - -@api_blueprint.route("/users/test", methods=["GET"]) -def test(): - return text_response("hello teknik8") - - -@api_blueprint.route("/users/test_auth", methods=["GET"]) -@jwt_required -@admin_required() -def test_auth(): - return text_response("you are authenticated") - - -@api_blueprint.route("/roles", methods=["GET"]) -@jwt_required -def get_roles(): - return query_response(Role.query.all()) - - -@api_blueprint.route("/users/login", methods=["POST"]) -def login(): - json_dict = request.get_json(force=True) - - validate_msg = validate_object(login_schema, json_dict) - if validate_msg is not None: - return text_response(validate_msg, codes.BAD_REQUEST) - - email = json_dict.get("email") - password = json_dict.get("password") - user = User.query.filter_by(email=email).first() - - if not user or not user.is_correct_password(password): - return text_response("Invalid email or password", codes.UNAUTHORIZED) - - access_token = _create_token(user) - refresh_token = create_refresh_token(identity=user.id) - - response = {"id": user.id, "access_token": access_token, "refresh_token": refresh_token} - return object_response(response) - - -@api_blueprint.route("/users/logout", methods=["POST"]) -@jwt_required -def logout(): - jti = get_raw_jwt()["jti"] - - db.session.add(Blacklist(jti)) - db.session.commit() - return text_response("Logged out") - - -@api_blueprint.route("/users/refresh", methods=["POST"]) -@jwt_refresh_token_required -def refresh(): - current_user = get_jwt_identity() - response = {"access_token": _create_token(current_user)} - - return object_response(response) - - -@api_blueprint.route("/users/", methods=["POST"]) -def create(): - json_dict = request.get_json(force=True) - - validate_msg = validate_object(register_schema, json_dict) - if validate_msg is not None: - return text_response(validate_msg, codes.BAD_REQUEST) - - email = json_dict.get("email") - password = json_dict.get("password") - role = json_dict.get("role") - city = json_dict.get("city") - - existing_user = User.query.filter(User.email == email).first() - - if existing_user is not None: - return text_response("User already exists", codes.BAD_REQUEST) - - dbc.add.user(email, password, role, city) - - item_user = User.query.filter(User.email == email).first() - - return query_response(item_user) - - -@api_blueprint.route("/users/", defaults={"user_id": None}, methods=["PUT"]) -@api_blueprint.route("/users/<int:user_id>", methods=["PUT"]) -@jwt_required -def edit(user_id): - json_dict = request.get_json(force=True) - - validate_msg = validate_object(edit_user_schema, json_dict) - if validate_msg is not None: - return text_response(validate_msg, codes.BAD_REQUEST) - - if user_id: - item_user = User.query.filter(User.id == user_id).first() - else: - item_user = _get_current_user() - - name = json_dict.get("name") - role = json_dict.get("city") - city = json_dict.get("password") - - if name: - item_user.name = name.title() - - if city: - if City.query.filter(City.name == city).first() is None: - return text_response(f"City {city} does not exist", codes.BAD_REQUEST) - item_user.city = city - - if role: - if Role.query.filter(Role.name == role).first() is None: - return text_response(f"Role {role} does not exist", codes.BAD_REQUEST) - item_user.role = role - - db.session.commit() - db.session.refresh(item_user) - return query_response(item_user) - - -@api_blueprint.route("/users/", defaults={"user_id": None}, methods=["DELETE"]) -@api_blueprint.route("/users/<int:user_id>", methods=["DELETE"]) -@jwt_required -def delete(user_id): - if user_id: - item_user = User.query.filter(User.id == user_id).first() - else: - item_user = _get_current_user() - - db.session.delete(item_user) - jti = get_raw_jwt()["jti"] - db.session.add(Blacklist(jti)) - db.session.commit() - - return text_response("User deleted") - - -### -# Getters -### -@api_blueprint.route("/users/", defaults={"user_id": None}, methods=["GET"]) -@api_blueprint.route("/users/<int:user_id>", methods=["GET"]) -@jwt_required -def get(user_id): - - if user_id: - user = User.query.filter(User.id == user_id).first() - else: - user = _get_current_user() - - if not user: - return text_response("User not found", codes.NOT_FOUND) - - return query_response(user) - - -# Searchable, returns 15 max at default -@api_blueprint.route("/users/search", methods=["GET"]) -@jwt_required -def search(): - arguments = request.args - query = User.query - - if "name" in arguments: - query = query.filter(User.name.like(f"%{arguments['name']}%")) - - if "email" in arguments: - query = query.filter(User.email.like(f"%{arguments['email']}%")) - - if "page" in arguments: - page_size = 15 - if "page_size" in arguments: - page_size = int(arguments["page_size"]) - query = query.limit(page_size) - query = query.offset(int(arguments["page"]) * page_size) - else: - query = query.limit(15) - - return query_response(query.all()) diff --git a/server/app/api/__init__.py b/server/app/apis/__init__.py similarity index 70% rename from server/app/api/__init__.py rename to server/app/apis/__init__.py index 09ac41cf450eec1a478a395c7a27003a3b383a91..dbff66c786e573fa5acb9c8a63ce187e344f745d 100644 --- a/server/app/api/__init__.py +++ b/server/app/apis/__init__.py @@ -1,6 +1,5 @@ from functools import wraps -from flask import Blueprint from flask_jwt_extended import verify_jwt_in_request from flask_jwt_extended.utils import get_jwt_claims @@ -37,7 +36,15 @@ def object_response(items, code=200): return {"result": items}, code -api_blueprint = Blueprint("api", __name__) +from flask_restx import Api -# Import the rest of the routes. -from app.api import admin, users +from .auth import api as ns2 +from .competitions import api as ns4 +from .slides import api as ns3 +from .users import api as ns1 + +flask_api = Api() +flask_api.add_namespace(ns1, path="/api/users") +flask_api.add_namespace(ns3, path="/api/slides") +flask_api.add_namespace(ns2, path="/api/auth") +flask_api.add_namespace(ns4, path="/api/competitions") diff --git a/server/app/api/admin.py b/server/app/apis/admin.py similarity index 100% rename from server/app/api/admin.py rename to server/app/apis/admin.py diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..06706127ccf9694d28ce71d880042a6f78782df7 --- /dev/null +++ b/server/app/apis/auth.py @@ -0,0 +1,94 @@ +import app.core.controller as dbc +import app.core.utils.http_codes as codes +from app.apis import admin_required +from app.core.models import User +from app.core.parsers import create_user_parser, login_parser +from flask_jwt_extended import ( + create_access_token, + create_refresh_token, + get_jwt_identity, + get_raw_jwt, + jwt_refresh_token_required, + jwt_required, +) +from flask_restx import Namespace, Resource + +api = Namespace("auth") + + +def get_user_claims(item_user): + return {"role": item_user.role.name, "city": item_user.city.name} + + +@api.route("/signup") +class AuthSignup(Resource): + @jwt_required + def post(self): + args = create_user_parser.parse_args(strict=True) + email = args.get("email") + password = args.get("password") + role = args.get("role") + city = args.get("city") + + if User.query.filter(User.email == email).count() > 0: + api.abort(codes.BAD_REQUEST, "User already exists") + + item_user = dbc.add.user(email, password, role, city) + if not item_user: + api.abort(codes.BAD_REQUEST, "User could not be created") + + return {"id": item_user.id} + + +@api.route("/delete/<ID>") +@api.param("ID") +class AuthDelete(Resource): + @jwt_required + def delete(self, ID): + item_user = User.query.filter(User.id == ID).first() + dbc.delete(item_user) + if ID == get_jwt_identity(): + jti = get_raw_jwt()["jti"] + dbc.add.blacklist(jti) + return "deleted" + + +@api.route("/login") +class AuthLogin(Resource): + def post(self): + args = login_parser.parse_args(strict=True) + email = args.get("email") + password = args.get("password") + item_user = User.query.filter_by(email=email).first() + + if not item_user or not item_user.is_correct_password(password): + api.abort(codes.UNAUTHORIZED, "Invalid email or password") + + access_token = create_access_token(item_user.id, user_claims=get_user_claims(item_user)) + refresh_token = create_refresh_token(item_user.id) + + response = {"id": item_user.id, "access_token": access_token, "refresh_token": refresh_token} + return response + + +@api.route("/logout") +class AuthLogout(Resource): + @jwt_required + def post(self): + jti = get_raw_jwt()["jti"] + dbc.add.blacklist(jti) + return "logout" + + +@api.route("/refresh") +class AuthRefresh(Resource): + @jwt_required + @jwt_refresh_token_required + def post(self): + old_jti = get_raw_jwt()["jti"] + + item_user = User.query.filter_by(id=get_jwt_identity()).first() + access_token = create_access_token(item_user.id, user_claims=get_user_claims(item_user)) + dbc.add.blacklist(old_jti) + response = {"access_token": access_token} + return response diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py new file mode 100644 index 0000000000000000000000000000000000000000..46c0e88f99f6976d49817be0a7dd6eb36ef01385 --- /dev/null +++ b/server/app/apis/competitions.py @@ -0,0 +1,75 @@ +import app.core.controller as dbc +import app.core.utils.http_codes as codes +from app.apis import admin_required +from app.core.models import Competition, User +from app.core.parsers import competition_parser, competition_search_parser +from app.core.schemas import competition_schema +from flask_jwt_extended import get_jwt_identity, jwt_required +from flask_restx import Namespace, Resource + +api = Namespace("competitions") +competition_model = api.model(*competition_schema) + + +@api.route("/") +class CompetitionBase(Resource): + @jwt_required + @api.marshal_with(competition_model) + def post(self): + args = competition_parser.parse_args(strict=True) + + name = args.get("name") + city_id = args.get("city_id") + year = args.get("year") + style_id = args.get("style_id") + + # Add competition + item_competition = dbc.add.competition(name, year, style_id, city_id) + + # Add default slide + item_slide = dbc.add.slide(item_competition.id) + + return item_competition + + +@api.route("/<ID>") +@api.param("ID") +class CompetitionByID(Resource): + @jwt_required + @api.marshal_with(competition_model) + def get(self, ID): + item = Competition.query.filter(Competition.id == ID).first() + return item + + @jwt_required + @api.marshal_with(competition_model) + def put(self, ID): + args = competition_parser.parse_args(strict=True) + + item = Competition.query.filter(Competition.id == ID).first() + name = args.get("name") + year = args.get("year") + city_id = args.get("city_id") + style_id = args.get("style_id") + return dbc.edit.competition(item, name, year, city_id, style_id) + + @jwt_required + def delete(self, ID): + item = Competition.query.filter(Competition.id == ID).first() + dbc.delete(item) + return "deleted" + + +@api.route("/search") +class CompetitionSearch(Resource): + @jwt_required + @api.marshal_with(competition_model) + def get(self): + args = competition_search_parser.parse_args(strict=True) + name = args.get("name") + year = args.get("year") + city_id = args.get("city_id") + style_id = args.get("style_id") + page = args.get("page") + page_size = args.get("page_size") + return dbc.get.search_competitions(name, year, city_id, style_id, page, page_size) diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py new file mode 100644 index 0000000000000000000000000000000000000000..bc6dfa3d748a569d154a2179e4cdf25b7b9416ed --- /dev/null +++ b/server/app/apis/slides.py @@ -0,0 +1,3 @@ +from flask_restx import Namespace, Resource, fields + +api = Namespace("slides") diff --git a/server/app/apis/users.py b/server/app/apis/users.py new file mode 100644 index 0000000000000000000000000000000000000000..424e608e824ec7202580aaba32b1e1d0451f4879 --- /dev/null +++ b/server/app/apis/users.py @@ -0,0 +1,71 @@ +import app.core.controller as dbc +import app.core.utils.http_codes as codes +from app.apis import admin_required +from app.core.models import User +from app.core.parsers import user_parser, user_search_parser +from app.core.schemas import user_schema +from flask_jwt_extended import get_jwt_identity, jwt_required +from flask_restx import Namespace, Resource + +api = Namespace("users") +user_model = api.model(*user_schema) + + +def edit_user(item_user, args): + email = args.get("email") + name = args.get("name") + city = args.get("city") + role = args.get("role") + + if email: + if User.query.filter(User.email == args["email"]).count() > 0: + api.abort(codes.BAD_REQUEST, "Email is already in use") + + return dbc.edit.user(item_user, name, email, city, role) + + +@api.route("/") +class UserBase(Resource): + @jwt_required + @api.marshal_list_with(user_model) + def get(self): + return User.query.filter(User.id == get_jwt_identity()).first() + + @jwt_required + @api.marshal_with(user_model) + def put(self): + args = user_parser.parse_args(strict=True) + item_user = User.query.filter(User.id == get_jwt_identity()).first() + return edit_user(item_user, args) + + +@api.route("/<ID>") +@api.param("ID") +class UserByID(Resource): + @jwt_required + @api.marshal_with(user_model) + def get(self, ID): + return User.query.filter(User.id == ID).first() + + @jwt_required + @api.marshal_with(user_model) + def put(self, ID): + args = user_parser.parse_args(strict=True) + item_user = User.query.filter(User.id == ID).first() + return edit_user(item_user, args) + + +@api.route("/search") +class UserSearch(Resource): + @jwt_required + @api.marshal_list_with(user_model) + def get(self): + args = user_search_parser.parse_args(strict=True) + name = args.get("name") + email = args.get("email") + role = args.get("role") + city = args.get("city") + page = args.get("page", 1) + page_size = args.get("page_size", 15) + + return dbc.get.search_user(email, name, city, role, page, page_size) diff --git a/server/app/database/__init__.py b/server/app/core/__init__.py similarity index 59% rename from server/app/database/__init__.py rename to server/app/core/__init__.py index c2b33c83f718b98c97bc7f01b2f26a998c497e9b..1f6cad4846d15460bd6c6e113297c4aa319dbb63 100644 --- a/server/app/database/__init__.py +++ b/server/app/core/__init__.py @@ -1,4 +1,7 @@ import sqlalchemy as sa +from flask_bcrypt import Bcrypt +from flask_jwt_extended.jwt_manager import JWTManager +from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.model import Model from sqlalchemy.sql import func @@ -7,3 +10,8 @@ class Base(Model): __abstract__ = True _created = sa.Column(sa.DateTime(timezone=True), server_default=func.now()) _updated = sa.Column(sa.DateTime(timezone=True), onupdate=func.now()) + + +db = SQLAlchemy(model_class=Base) +bcrypt = Bcrypt() +jwt = JWTManager() diff --git a/server/app/core/controller/__init__.py b/server/app/core/controller/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..05f51a94602e790ffc2034f0807218f806fb6709 --- /dev/null +++ b/server/app/core/controller/__init__.py @@ -0,0 +1,8 @@ +# import add, get +from app.core import db +from app.core.controller import add, edit, get + + +def delete(item): + db.session.delete(item) + db.session.commit() diff --git a/server/app/database/controller/add.py b/server/app/core/controller/add.py similarity index 78% rename from server/app/database/controller/add.py rename to server/app/core/controller/add.py index 5cc7a0b7ea666f60d2e6b91a704e14658a6f6c12..48f9c949238f8a175590d876f9499c12b22b5087 100644 --- a/server/app/database/controller/add.py +++ b/server/app/core/controller/add.py @@ -1,5 +1,10 @@ -from app import db -from app.database.models import City, Competition, Role, Slide, Style, User +from app.core import db +from app.core.models import Blacklist, City, Competition, Role, Slide, User + + +def blacklist(jti): + db.session.add(Blacklist(jti)) + db.session.commit() def user(email, plaintext_password, role, city): @@ -27,5 +32,5 @@ def slide(competition_id): db.session.add(Slide(order, competition_id)) db.session.commit() - filters = (Slide.order == order) & (Competition.competition_id == competition_id) + filters = (Slide.order == order) & (Slide.competition_id == competition_id) return Slide.query.filter(filters).first() diff --git a/server/app/core/controller/edit.py b/server/app/core/controller/edit.py new file mode 100644 index 0000000000000000000000000000000000000000..da5340812343310a5e92b3205db804157eb10393 --- /dev/null +++ b/server/app/core/controller/edit.py @@ -0,0 +1,38 @@ +from app.core import db +from app.core.models import Blacklist, City, Competition, Role, Slide, User + + +def competition(item, name=None, year=None, city_id=None, style_id=None): + if name: + item.name = name + if year: + item.year = year + if city_id: + item.city_id = city_id + if style_id: + item.style_id = style_id + + db.session.commit() + db.session.refresh(item) + return item + + +def user(item, name=None, email=None, city=None, role=None): + + if name: + item.name = name.title() + + if email: + item.email = email + + if city: + item_city = City.query.filter(City.name == city).first() + item.city_id = item_city.id + + if role: + item_role = Role.query.filter(Role.name == role).first() + item.role_id = item_role.id + + db.session.commit() + db.session.refresh(item) + return item diff --git a/server/app/core/controller/get.py b/server/app/core/controller/get.py new file mode 100644 index 0000000000000000000000000000000000000000..6f4fae14078a9d5ac6d0e1e9b44a61ad492553a7 --- /dev/null +++ b/server/app/core/controller/get.py @@ -0,0 +1,33 @@ +from app.core.models import Competition, User + + +def search_user(email=None, name=None, city=None, role=None, page=1, page_size=15): + query = User.query + if name: + query = query.filter(User.name.like(f"%{name}%")) + if email: + query = query.filter(User.email.like(f"%{email}%")) + if city: + query = query.filter(User.city.name == city) + if role: + query = query.filter(User.role.name == role) + + query = query.limit(page_size).offset(page * page_size) + + return query.all() + + +def search_competitions(name=None, year=None, city_id=None, style_id=None, page=1, page_size=15): + query = Competition.query + if name: + query = query.filter(Competition.name.like(f"%{name}%")) + if year: + query = query.filter(Competition.year == year) + if city_id: + query = query.filter(Competition.city_id == city_id) + if style_id: + query = query.filter(Competition.style_id == style_id) + + query = query.limit(page_size).offset(page * page_size) + + return query.all() diff --git a/server/app/database/models.py b/server/app/core/models.py similarity index 80% rename from server/app/database/models.py rename to server/app/core/models.py index 62e07094cf1d85d11abb9fc34c52e0dae48b0b58..520887e39780809ecf8e325ba27c008a51bf0c95 100644 --- a/server/app/database/models.py +++ b/server/app/core/models.py @@ -1,4 +1,4 @@ -from app import bcrypt, db +from app.core import bcrypt, db from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property STRING_SIZE = 254 @@ -72,7 +72,7 @@ class User(db.Model): "role_id": self.role_id, "city_id": self.city_id, } - + @hybrid_property def password(self): return self._password @@ -227,60 +227,3 @@ class QuestionType(db.Model): def __init__(self, name): self.name = name - -""" -QuestionHandler = db.Table( - "question_handler", - db.Column("question_id", db.Integer, db.ForeignKey("question.id"), primary_key=True), - db.Column("sub_question_id", db.Integer, unique=True), - db.Column("question_type", db.Integer, nullable=False), -) - -class TrueFalseQuestion(db.Model): - id = db.Column(db.Integer, primary_key=True) - true_false = db.Column(db.Boolean, nullable=False, default=False) - - def __init__(self, true_false): - self.true_false = true_false - - -class TextQuestion(db.Model): - id = db.Column(db.Integer, primary_key=True) - alternatives = db.relationship("TextQuestionAlternative", backref="text_question") - - -class TextQuestionAlternative(db.Model): - id = db.Column(db.Integer, primary_key=True) - text = db.Column(db.String(STRING_SIZE), nullable=False) - text_question_id = db.Column(db.Integer, db.ForeignKey("text_question.id"), nullable=False) - - def __init__(self, text, text_question_id): - self.text = text - self.text_question_id = text_question_id - - -class MCQuestion(db.Model): - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(STRING_SIZE), nullable=False) - timer = db.Column(db.Integer, nullable=False) - alternatives = db.relationship("MCQuestionAlternative", backref="mc_question") - - def __init__(self, title, timer): - self.title = title - self.timer = timer - - -class MCQuestionAlternative(db.Model): - id = db.Column(db.Integer, primary_key=True) - text = db.Column(db.String(STRING_SIZE), nullable=False) - true_false = db.Column(db.Boolean, nullable=False, default=False) - mc_id = db.Column(db.Integer, db.ForeignKey("mc_question.id"), nullable=False) - - def __init__(self, text, true_false, mc_id): - self.text = text - self.true_false = true_false - self.mc_id = mc_id - - - -""" diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py new file mode 100644 index 0000000000000000000000000000000000000000..5c354d9079da45fd9187968003d06e5e9ebb9d35 --- /dev/null +++ b/server/app/core/parsers.py @@ -0,0 +1,47 @@ +from flask_restx import inputs, reqparse + +###SEARCH#### +search_parser = reqparse.RequestParser() +search_parser.add_argument("page", type=int, default=0, location="args") +search_parser.add_argument("page_size", type=int, default=15, location="args") + +###LOGIN#### +login_parser = reqparse.RequestParser() +login_parser.add_argument("email", type=inputs.email(), required=True, location="json") +login_parser.add_argument("password", required=True, location="json") + +###CREATE_USER#### +create_user_parser = login_parser.copy() +create_user_parser.add_argument("email", type=inputs.email(), required=True, location="json") +create_user_parser.add_argument("city", required=True, location="json") +create_user_parser.add_argument("role", required=True, location="json") + +###USER#### +user_parser = reqparse.RequestParser() +user_parser.add_argument("email", type=inputs.email(), location="json") +user_parser.add_argument("name", location="json") +user_parser.add_argument("city", location="json") +user_parser.add_argument("role", location="json") + +###SEARCH_USER#### +user_search_parser = search_parser.copy() +user_search_parser.add_argument("name", default=None, location="args") +user_search_parser.add_argument("email", default=None, location="args") +user_search_parser.add_argument("city", default=None, location="args") +user_search_parser.add_argument("role", default=None, location="args") + + +###COMPETIION#### +competition_parser = reqparse.RequestParser() +competition_parser.add_argument("name", location="json") +competition_parser.add_argument("year", type=int, location="json") +competition_parser.add_argument("city_id", type=int, location="json") +competition_parser.add_argument("style_id", type=int, location="json") + + +###SEARCH_COMPETITOIN#### +competition_search_parser = search_parser.copy() +competition_search_parser.add_argument("name", default=None, location="args") +competition_search_parser.add_argument("year", default=None, location="args") +competition_search_parser.add_argument("city_id", default=None, location="args") +competition_search_parser.add_argument("style_id", default=None, location="args") diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..fb2681ba5b39d4fb2f34f89f0c2910fb8fcc8088 --- /dev/null +++ b/server/app/core/schemas.py @@ -0,0 +1,23 @@ +from flask_restx import Namespace, Resource, abort, fields, inputs, model, reqparse + +user_schema = ( + "User", + { + "id": fields.Integer(), + "name": fields.String(), + "email": fields.String(), + "role": fields.String(attribute=lambda x: x.role.name), + "city": fields.String(attribute=lambda x: x.city.name), + }, +) + +competition_schema = ( + "Competition", + { + "id": fields.Integer(), + "name": fields.String(), + "year": fields.Integer(), + "style_id": fields.Integer(), + "city_id": fields.Integer(), + }, +) diff --git a/server/app/core/utils/__init__.py b/server/app/core/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/server/app/utils/http_codes.py b/server/app/core/utils/http_codes.py similarity index 100% rename from server/app/utils/http_codes.py rename to server/app/core/utils/http_codes.py diff --git a/server/app/utils/test_helpers.py b/server/app/core/utils/test_helpers.py similarity index 54% rename from server/app/utils/test_helpers.py rename to server/app/core/utils/test_helpers.py index 821e4981daca8d935105ce1a05165ee2e11a37f3..968281d5af22c91a117aa4ebf58b0fe21ace11eb 100644 --- a/server/app/utils/test_helpers.py +++ b/server/app/core/utils/test_helpers.py @@ -1,9 +1,47 @@ import json -from app import db +import app.core.controller as dbc +import pytest +from app.core import db +from app.core.models import City, MediaType, QuestionType, Role, Style + + +def add_default_values(): + media_types = ["Image", "Video"] + question_types = ["Boolean", "Multiple", "Text"] + roles = ["Admin", "Editor"] + cities = ["Linköping"] + + # Add media types + for item in media_types: + db.session.add(MediaType(item)) + + # Add question types + for item in question_types: + db.session.add(QuestionType(item)) + + # Add roles + for item in roles: + db.session.add(Role(item)) + + # Add cities + for item in cities: + db.session.add(City(item)) + + # Add deafult style + db.session.add(Style("Main Style", "")) + + # Commit changes to db + db.session.commit() + + # Add user with role and city + dbc.add.user("test@test.se", "password", "Admin", "Linköping") def post(client, url, data, headers=None): + if headers is None: + headers = {} + headers["Content-Type"] = "application/json" response = client.post(url, data=json.dumps(data), headers=headers) body = json.loads(response.data.decode()) return response, body @@ -16,6 +54,10 @@ def get(client, url, query_string=None, headers=None): def put(client, url, data, headers=None): + if headers is None: + headers = {} + headers["Content-Type"] = "application/json" + response = client.put(url, data=json.dumps(data), headers=headers) body = json.loads(response.data.decode()) return response, body diff --git a/server/app/database/controller/__init__.py b/server/app/database/controller/__init__.py deleted file mode 100644 index b3cfb1fc8c5cc4edcd7a01d0d829689084a3e063..0000000000000000000000000000000000000000 --- a/server/app/database/controller/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# import add, get -from app.database.controller import add, get diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py deleted file mode 100644 index eb65ce784021ea768d7fafeb5d1a70b22fc94023..0000000000000000000000000000000000000000 --- a/server/app/database/controller/get.py +++ /dev/null @@ -1,3 +0,0 @@ -from app import db -from app.database.models import City, Competition, Role, Slide, Style, User -from sqlalchemy import and_, or_ diff --git a/server/app/database/populate.py b/server/app/database/populate.py deleted file mode 100644 index 7eb587107096619050b8c2c3dece8ce6996eddd9..0000000000000000000000000000000000000000 --- a/server/app/database/populate.py +++ /dev/null @@ -1,33 +0,0 @@ -import app.database.controller as dbc -from app import db -from app.database.models import City, MediaType, QuestionType, Role - -media_types = ["Image", "Video"] -question_types = ["Boolean", "Multiple", "Text"] -roles = ["Admin", "Editor"] -cities = ["Linköping"] - - -def add_default_values(): - - # Add media types - for item in media_types: - db.session.add(MediaType(item)) - - # Add question types - for item in question_types: - db.session.add(QuestionType(item)) - - # Add roles - for item in roles: - db.session.add(Role(item)) - - # Add cities - for item in cities: - db.session.add(City(item)) - - # Commit changes to db - db.session.commit() - - # Add user with role and city - dbc.add.user("test@test.se", "password", "Admin", "Linköping") diff --git a/server/app/utils/helpers.py b/server/app/utils/helpers.py deleted file mode 100644 index ddb0bfe0c54c4c7b71eda91072358e4509b9fa30..0000000000000000000000000000000000000000 --- a/server/app/utils/helpers.py +++ /dev/null @@ -1,5 +0,0 @@ -import datetime - - -def parse_date(date_to_parse): - return datetime.datetime.strptime(date_to_parse, "%Y-%m-%d").date() diff --git a/server/app/utils/validator.py b/server/app/utils/validator.py deleted file mode 100644 index e196f7b83685cef33c850dc17c4f9971d740a66b..0000000000000000000000000000000000000000 --- a/server/app/utils/validator.py +++ /dev/null @@ -1,28 +0,0 @@ -from cerberus import Validator - - -def validate_object(schema, obj, allow_unknown=False): - v = Validator(schema, allow_unknown) - if not v.validate(obj): - return v.errors - - -_email_regex = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$" - -login_schema = { - "email": {"type": "string", "required": True, "regex": _email_regex}, - "password": {"type": "string", "required": True, "minlength": 6, "maxlength": 128}, -} - -register_schema = { - "email": {"type": "string", "required": True, "regex": _email_regex}, - "password": {"type": "string", "required": True, "minlength": 6, "maxlength": 128}, - "role": {"type": "string", "required": True}, - "city": {"type": "string", "required": True}, -} - -edit_user_schema = { - "name": {"type": "string", "required": False, "minlength": 1, "maxlength": 50}, - "role": {"type": "string", "required": False}, - "city": {"type": "string", "required": False}, -} diff --git a/server/configmodule.py b/server/configmodule.py index fb1117ad59aa01f7443fe4057564d8710b5c8077..f7c2935fa6e2f11747f14b7dfcfc5e426b6de1b5 100644 --- a/server/configmodule.py +++ b/server/configmodule.py @@ -1,3 +1,6 @@ +from datetime import timedelta + + class Config: DEBUG = False TESTING = False @@ -5,6 +8,9 @@ class Config: JWT_SECRET_KEY = "super-secret" JWT_BLACKLIST_ENABLED = True JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] + BUNDLE_ERRORS = True + JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=2) + JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) class DevelopmentConfig(Config): diff --git a/server/populate.py b/server/populate.py index 21c00dcaf5726d648bf82c40b901d7893e09d2a8..452769918189a1b6c4937578a0714ad777b281b6 100644 --- a/server/populate.py +++ b/server/populate.py @@ -1,6 +1,6 @@ -import app.database.controller as dbc +import app.core.controller as dbc from app import create_app, db -from app.database.models import City, MediaType, QuestionType, Role +from app.core.models import City, MediaType, QuestionType, Role, Style user = {"email": "test@test.se", "password": "password", "role": "Admin", "city": "Linköping"} media_types = ["Image", "Video"] @@ -31,6 +31,12 @@ def _add_items(): db.session.add(City(item)) db.session.commit() + # Add deafult style + db.session.add(Style("Main Style", "")) + + # Commit changes to db + db.session.commit() + # Add user with role and city dbc.add.user("test@test.se", "password", "Admin", "Linköping") diff --git a/server/requirements.txt b/server/requirements.txt index 31bb42fb147e77cea0e83c9af0230046efacb0b4..463ef4fe07f14b03b7771a25842170c8903f73f7 100644 Binary files a/server/requirements.txt and b/server/requirements.txt differ diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 1275b0f2e74c38c1942dc21bb45a2ed8627fa02f..6d6e16dcbd5a5c8128e78a832e94cb6321120497 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -1,50 +1,64 @@ import json -from app.database.populate import add_default_values -from app.utils.test_helpers import delete, get, post, put +from app.core.utils.test_helpers import add_default_values, get, post, put -from tests import app, client +from tests import app, client, db + + +def test_competition(client): + add_default_values() + + # Login in with default user + response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) + assert response.status_code == 200 + headers = {"Authorization": "Bearer " + body["access_token"]} + + # Create competition + data = {"name": "c1", "year": 2020, "city_id": 1, "style_id": 1} + response, body = post(client, "/api/competitions", data, headers=headers) + assert response.status_code == 200 + assert body["name"] == "c1" + + # Get competition + response, body = get(client, "/api/competitions/1", headers=headers) + assert response.status_code == 200 + assert body["name"] == "c1" def test_app(client): add_default_values() # Login in with default user - response, body = post(client, "/api/users/login", {"email": "test@test.se", "password": "password"}) - item = body["result"][0] - headers = {"Authorization": "Bearer " + item["access_token"]} + response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) + assert response.status_code == 200 + headers = {"Authorization": "Bearer " + body["access_token"]} # Create user register_data = {"email": "test1@test.se", "password": "abc123", "role": "Admin", "city": "Linköping"} - response, body = post(client, "/api/users/", register_data, headers) - item = body["result"][0] + response, body = post(client, "/api/auth/signup", register_data, headers) assert response.status_code == 200 - assert item["id"] == 2 - assert "password" not in item - assert item["email"] == "test1@test.se" + assert body["id"] == 2 + assert "password" not in body # Try loggin with wrong PASSWORD - response, body = post(client, "/api/users/login", {"email": "test1@test.se", "password": "abc1234"}) + response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc1234"}) assert response.status_code == 401 # Try loggin with wrong Email - response, body = post(client, "/api/users/login", {"email": "testx@test.se", "password": "abc1234"}) + response, body = post(client, "/api/auth/login", {"email": "testx@test.se", "password": "abc1234"}) assert response.status_code == 401 # Try loggin with right PASSWORD - response, body = post(client, "/api/users/login", {"email": "test1@test.se", "password": "abc123"}) - item = body["result"][0] - headers = {"Authorization": "Bearer " + item["access_token"]} + response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc123"}) assert response.status_code == 200 + headers = {"Authorization": "Bearer " + body["access_token"]} # Get the current user - response, body = get(client, "/api/users/", headers=headers) - item = body["result"][0] + response, body = get(client, "/api/users", headers=headers) assert response.status_code == 200 - assert item["email"] == "test1@test.se" + assert body["email"] == "test1@test.se" - response, body = put(client, "/api/users/", {"name": "carl carlsson"}, headers=headers) - item = body["result"][0] + response, body = put(client, "/api/users", {"name": "carl carlsson"}, headers=headers) assert response.status_code == 200 - assert item["name"] == "Carl Carlsson" + assert body["name"] == "Carl Carlsson" diff --git a/server/tests/test_db.py b/server/tests/test_db.py index 832329cc47fbb5bef8597a3e747facf5d3423a83..f670e4926390e69bc101dcda39ab1769e6c64aa9 100644 --- a/server/tests/test_db.py +++ b/server/tests/test_db.py @@ -1,21 +1,5 @@ -import app.database.controller as dbc -import pytest -from app.database.models import ( - City, - Competition, - Media, - MediaType, - Question, - QuestionAnswer, - QuestionType, - Role, - Slide, - Style, - Team, - User, -) -from app.database.populate import add_default_values -from app.utils.test_helpers import assert_exists, assert_insert_fail, assert_object_values +from app.core.models import City, Competition, Media, MediaType, Question, QuestionType, Role, Slide, Style, Team, User +from app.core.utils.test_helpers import add_default_values, assert_exists, assert_insert_fail from tests import app, client, db