diff --git a/.vscode/tasks.json b/.vscode/tasks.json index df39c2057c01130eecf18b74915e8ddb13fb7c4c..7029dbe39d5a29ad684540e3d3eaf8272bb6bdaa 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -29,7 +29,6 @@ "options": { "cwd": "${workspaceFolder}/client" }, - }, { "label": "Start server", @@ -54,6 +53,16 @@ "cwd": "${workspaceFolder}/server" }, }, + { + "label": "Populate server", + "type": "shell", + "group": "build", + "command": "env/Scripts/python populate.py", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server" + }, + }, { "label": "Open server coverage", "type": "shell", @@ -63,7 +72,6 @@ "options": { "cwd": "${workspaceFolder}/server" }, - }, { "label": "Start client and server", diff --git a/server/app/__init__.py b/server/app/__init__.py index 6655bdb00a0c113a530685810cf828340a450be2..8b9293e319c9e6fec0788ac9a9dad2bdcf82f571 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -25,7 +25,6 @@ def create_app(config_name="configmodule.DevelopmentConfig"): from app.api import api_blueprint app.register_blueprint(api_blueprint, url_prefix="/api") - return app diff --git a/server/app/api/__init__.py b/server/app/api/__init__.py index 3f113bf331b4718bfb14d52ffb2938eed5dcc159..09ac41cf450eec1a478a395c7a27003a3b383a91 100644 --- a/server/app/api/__init__.py +++ b/server/app/api/__init__.py @@ -1,6 +1,43 @@ +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 + + +def admin_required(): + def wrapper(fn): + @wraps(fn) + def decorator(*args, **kwargs): + verify_jwt_in_request() + claims = get_jwt_claims() + if claims["role"] == "Admin": + return fn(*args, **kwargs) + else: + return {"message:": "Admins only"}, 403 + + return decorator + + return wrapper + + +def text_response(text, code=200): + return {"message": text}, code + + +def query_response(db_items, code=200): + if type(db_items) is not list: + db_items = [db_items] + return {"result": [i.get_dict() for i in db_items]}, code + + +def object_response(items, code=200): + if type(items) is not list: + items = [items] + return {"result": items}, code + api_blueprint = Blueprint("api", __name__) # Import the rest of the routes. -from app.api import users, admin +from app.api import admin, users diff --git a/server/app/api/competitions.py b/server/app/api/competitions.py new file mode 100644 index 0000000000000000000000000000000000000000..f89b20b9b2d5e49a22014c4b948a202947ce7dd3 --- /dev/null +++ b/server/app/api/competitions.py @@ -0,0 +1,46 @@ +### +# 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 index 94f2107cea6abd03666e6f990fb4a252a8d519e0..6b0e3984b330e7e7a16d34b3b68769b79238c8ef 100644 --- a/server/app/api/users.py +++ b/server/app/api/users.py @@ -1,9 +1,10 @@ import datetime import app.database.controller as dbc +import app.utils.http_codes as codes from app import db -from app.api import api_blueprint -from app.database.models import Blacklist, User +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 ( @@ -16,19 +17,34 @@ from flask_jwt_extended import ( ) -def get_current_user(): +##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 {"message": "hello teknik8"}, 200 + return text_response("hello teknik8") @api_blueprint.route("/users/test_auth", methods=["GET"]) @jwt_required +@admin_required() def test_auth(): - return {"message": "you are authenticated"}, 200 + 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"]) @@ -37,26 +53,20 @@ def login(): validate_msg = validate_object(login_schema, json_dict) if validate_msg is not None: - return {"message": validate_msg}, 400 + return text_response(validate_msg, codes.BAD_REQUEST) - email = json_dict["email"] - password = json_dict["password"] + email = json_dict.get("email") + password = json_dict.get("password") user = User.query.filter_by(email=email).first() - # Don't show the user that the email was correct unless the password was also correct - if not user: - return {"message": "The email or password you entered is incorrect."}, 401 - - if not user.is_correct_password(password): - return {"message": "The email or password you entered is incorrect."}, 401 + if not user or not user.is_correct_password(password): + return text_response("Invalid email or password", codes.UNAUTHORIZED) - expires = datetime.timedelta(days=7) - access_token = create_access_token(identity=user.id, expires_delta=expires) + access_token = _create_token(user) refresh_token = create_refresh_token(identity=user.id) - return ( - {"id": user.id, "access_token": access_token, "refresh_token": refresh_token}, - 200, - ) + + response = {"id": user.id, "access_token": access_token, "refresh_token": refresh_token} + return object_response(response) @api_blueprint.route("/users/logout", methods=["POST"]) @@ -66,15 +76,16 @@ def logout(): db.session.add(Blacklist(jti)) db.session.commit() - return {"message": "message fully logged out"}, 200 + return text_response("Logged out") @api_blueprint.route("/users/refresh", methods=["POST"]) @jwt_refresh_token_required def refresh(): current_user = get_jwt_identity() - ret = {"access_token": create_access_token(identity=current_user)} - return ret, 200 + response = {"access_token": _create_token(current_user)} + + return object_response(response) @api_blueprint.route("/users/", methods=["POST"]) @@ -83,68 +94,101 @@ def create(): validate_msg = validate_object(register_schema, json_dict) if validate_msg is not None: - return {"message": validate_msg}, 400 + 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_by(email=json_dict["email"]).first() + existing_user = User.query.filter(User.email == email).first() if existing_user is not None: - return {"message": "User already exists"}, 400 + return text_response("User already exists", codes.BAD_REQUEST) - dbc.add.user(json_dict["email"], json_dict["password"], json_dict["role"], json_dict["city"]) + dbc.add.user(email, password, role, city) - item_user = User.query.filter_by(email=json_dict["email"]).first() + item_user = User.query.filter(User.email == email).first() - return item_user.get_dict(), 200 + return query_response(item_user) -@api_blueprint.route("/users/", methods=["PUT"]) +@api_blueprint.route("/users/", defaults={"user_id": None}, methods=["PUT"]) +@api_blueprint.route("/users/<int:user_id>", methods=["PUT"]) @jwt_required -def edit(): +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 {"message": validate_msg}, 400 + 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() - user = get_current_user() - user.name = json_dict["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() - return user.get_dict(), 200 + db.session.refresh(item_user) + return query_response(item_user) -@api_blueprint.route("/users/", methods=["DELETE"]) +@api_blueprint.route("/users/", defaults={"user_id": None}, methods=["DELETE"]) +@api_blueprint.route("/users/<int:user_id>", methods=["DELETE"]) @jwt_required -def delete(): - user = get_current_user() - db.session.delete(user) +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 {"message": "User was deleted"}, 200 + + return text_response("User deleted") ### # Getters ### -@api_blueprint.route("/users/", defaults={"UserID": None}, methods=["GET"]) -@api_blueprint.route("/users/<int:UserID>", methods=["GET"]) +@api_blueprint.route("/users/", defaults={"user_id": None}, methods=["GET"]) +@api_blueprint.route("/users/<int:user_id>", methods=["GET"]) @jwt_required -def get(UserID): +def get(user_id): - if UserID: - user = User.query.filter_by(id=UserID).first() + if user_id: + user = User.query.filter(User.id == user_id).first() else: - user = get_current_user() + user = _get_current_user() if not user: - return {"message": "User not found"}, 404 + return text_response("User not found", codes.NOT_FOUND) - return user.get_dict(), 200 + return query_response(user) -# Searchable, returns 10 max at default +# Searchable, returns 15 max at default @api_blueprint.route("/users/search", methods=["GET"]) +@jwt_required def search(): arguments = request.args query = User.query @@ -164,4 +208,4 @@ def search(): else: query = query.limit(15) - return [i.get_dict() for i in query.all()], 200 + return query_response(query.all()) diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index c6d0e5328b43cbc5ff1651bd63d7fe5f14d963d5..c2b33c83f718b98c97bc7f01b2f26a998c497e9b 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -5,5 +5,5 @@ from sqlalchemy.sql import func 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()) + _created = sa.Column(sa.DateTime(timezone=True), server_default=func.now()) + _updated = sa.Column(sa.DateTime(timezone=True), onupdate=func.now()) diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index d00b791f931188e4f95b31aa858e9bc5b2481e53..5cc7a0b7ea666f60d2e6b91a704e14658a6f6c12 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -13,8 +13,8 @@ def user(email, plaintext_password, role, city): return User.query.filter(User.email == email).first() -def competition(name, style_id, city_id): - db.session.add(Competition(name, style_id, city_id)) +def competition(name, year, style_id, city_id): + db.session.add(Competition(name, year, style_id, city_id)) db.session.commit() filters = (Competition.name == name) & (Competition.city_id == city_id) diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 9a4fb26b360708d2aa076af1ec3b21e3a44f8fbb..eb65ce784021ea768d7fafeb5d1a70b22fc94023 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -1,7 +1,3 @@ from app import db from app.database.models import City, Competition, Role, Slide, Style, User from sqlalchemy import and_, or_ - - -def user(): - return diff --git a/server/app/database/models.py b/server/app/database/models.py index 29e076dcefd9e60fda434ee49caa29ee243cba4e..62e07094cf1d85d11abb9fc34c52e0dae48b0b58 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -12,6 +12,9 @@ class Blacklist(db.Model): def __init__(self, jti): self.jti = jti + def get_dict(self): + return {"id": self.id, "jti": self.jti, "expire_date": self.expire_date} + class Role(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -22,6 +25,9 @@ class Role(db.Model): def __init__(self, name): self.name = name + def get_dict(self): + return {"id": self.id, "name": self.name} + # TODO Region? class City(db.Model): @@ -59,7 +65,13 @@ class User(db.Model): self.authenticated = False def get_dict(self): - return {"id": self.id, "email": self.email, "name": self.name} + return { + "id": self.id, + "email": self.email, + "name": self.name, + "role_id": self.role_id, + "city_id": self.city_id, + } @hybrid_property def password(self): @@ -105,14 +117,17 @@ class Style(db.Model): class Competition(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) + year = db.Column(db.Integer, nullable=False, default=2020) + style_id = db.Column(db.Integer, db.ForeignKey("style.id"), nullable=False) city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=False) slides = db.relationship("Slide", backref="competition") teams = db.relationship("Team", backref="competition") - def __init__(self, name, style_id, city_id): + def __init__(self, name, year, style_id, city_id): self.name = name + self.year = year self.style_id = style_id self.city_id = city_id diff --git a/server/app/utils/http_codes.py b/server/app/utils/http_codes.py new file mode 100644 index 0000000000000000000000000000000000000000..cfa3a2b1c4867ef906516b3f2bec25113f43019b --- /dev/null +++ b/server/app/utils/http_codes.py @@ -0,0 +1,8 @@ +OK = 200 +BAD_REQUEST = 400 +UNAUTHORIZED = 401 +FORBIDDEN = 403 +NOT_FOUND = 404 +GONE = 410 +INTERNAL_SERVER_ERROR = 500 +SERVICE_UNAVAILABLE = 503 diff --git a/server/app/utils/test_helpers.py b/server/app/utils/test_helpers.py index b8798492782df1abd79e36b30b8cb55f1f1c25c1..821e4981daca8d935105ce1a05165ee2e11a37f3 100644 --- a/server/app/utils/test_helpers.py +++ b/server/app/utils/test_helpers.py @@ -1,6 +1,32 @@ +import json + from app import db +def post(client, url, data, headers=None): + response = client.post(url, data=json.dumps(data), headers=headers) + body = json.loads(response.data.decode()) + return response, body + + +def get(client, url, query_string=None, headers=None): + response = client.get(url, query_string=query_string, headers=headers) + body = json.loads(response.data.decode()) + return response, body + + +def put(client, url, data, headers=None): + response = client.put(url, data=json.dumps(data), headers=headers) + body = json.loads(response.data.decode()) + return response, body + + +def delete(client, url, data, headers=None): + response = client.delete(url, data=json.dumps(data), headers=headers) + body = json.loads(response.data.decode()) + return response, body + + # Try insert invalid row. If it fails then the test is passed def assert_insert_fail(db_type, *args): try: diff --git a/server/app/utils/validator.py b/server/app/utils/validator.py index 2e31fac5e82b3e301bc1c86c8acf4e0b84a9d5ad..e196f7b83685cef33c850dc17c4f9971d740a66b 100644 --- a/server/app/utils/validator.py +++ b/server/app/utils/validator.py @@ -23,4 +23,6 @@ register_schema = { 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 290c91325fa99e9bb4098e0963fd63f3404c4ff3..fb1117ad59aa01f7443fe4057564d8710b5c8077 100644 --- a/server/configmodule.py +++ b/server/configmodule.py @@ -9,6 +9,7 @@ class Config: class DevelopmentConfig(Config): DEBUG = True + SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" class TestingConfig(Config): diff --git a/server/populate.py b/server/populate.py new file mode 100644 index 0000000000000000000000000000000000000000..21c00dcaf5726d648bf82c40b901d7893e09d2a8 --- /dev/null +++ b/server/populate.py @@ -0,0 +1,44 @@ +import app.database.controller as dbc +from app import create_app, db +from app.database.models import City, MediaType, QuestionType, Role + +user = {"email": "test@test.se", "password": "password", "role": "Admin", "city": "Linköping"} +media_types = ["Image", "Video"] +question_types = ["Boolean", "Multiple", "Text"] +roles = ["Admin", "Editor"] +cities = ["Linköping"] + + +def _add_items(): + # Add media types + for item in media_types: + db.session.add(MediaType(item)) + + db.session.commit() + + # Add question types + for item in question_types: + db.session.add(QuestionType(item)) + db.session.commit() + + # Add roles + for item in roles: + db.session.add(Role(item)) + db.session.commit() + + # Add cities + for item in cities: + db.session.add(City(item)) + db.session.commit() + + # Add user with role and city + dbc.add.user("test@test.se", "password", "Admin", "Linköping") + + db.session.flush() + + +app = create_app("configmodule.DevelopmentConfig") + +with app.app_context(): + db.create_all() + _add_items() diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 88a5cfbfd24f24a82de51617a366f9a3fac2c936..1275b0f2e74c38c1942dc21bb45a2ed8627fa02f 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -1,6 +1,7 @@ import json from app.database.populate import add_default_values +from app.utils.test_helpers import delete, get, post, put from tests import app, client @@ -8,40 +9,42 @@ from tests import app, client def test_app(client): add_default_values() - register_data = {"email": "test1@test.se", "password": "abc123", "role": "Admin", "city": "Linköping"} + # 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"]} + # Create user - rv = client.post( - "/api/users/", - data=json.dumps(register_data), - ) - rv_dict = json.loads(rv.data.decode()) + 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] - assert rv.status_code == 200 - assert rv_dict["id"] == 2 - assert "password" not in rv_dict - assert rv_dict["email"] == "test1@test.se" + assert response.status_code == 200 + assert item["id"] == 2 + assert "password" not in item + assert item["email"] == "test1@test.se" # Try loggin with wrong PASSWORD - rv = client.post("/api/users/login", data=json.dumps({"email": "test1@test.se", "password": "abc1234"})) - assert rv.status_code == 401 + response, body = post(client, "/api/users/login", {"email": "test1@test.se", "password": "abc1234"}) + assert response.status_code == 401 # Try loggin with wrong Email - rv = client.post("/api/users/login", data=json.dumps({"email": "testx@test.se", "password": "abc1234"})) - assert rv.status_code == 401 + response, body = post(client, "/api/users/login", {"email": "testx@test.se", "password": "abc1234"}) + assert response.status_code == 401 # Try loggin with right PASSWORD - rv = client.post("/api/users/login", data=json.dumps({"email": "test1@test.se", "password": "abc123"})) - rv_dict = json.loads(rv.data.decode()) - assert rv.status_code == 200 - headers = {"Authorization": "Bearer " + rv_dict["access_token"]} + response, body = post(client, "/api/users/login", {"email": "test1@test.se", "password": "abc123"}) + item = body["result"][0] + headers = {"Authorization": "Bearer " + item["access_token"]} + assert response.status_code == 200 # Get the current user - rv = client.get("/api/users/", headers=headers) - rv_dict = json.loads(rv.data.decode()) - assert rv.status_code == 200 - assert rv_dict["email"] == "test1@test.se" - - rv = client.put("/api/users/", data=json.dumps({"name": "carl carlsson"}), headers=headers) - rv_dict = json.loads(rv.data.decode()) - assert rv.status_code == 200 - assert rv_dict["name"] == "Carl Carlsson" + response, body = get(client, "/api/users/", headers=headers) + item = body["result"][0] + assert response.status_code == 200 + assert item["email"] == "test1@test.se" + + response, body = put(client, "/api/users/", {"name": "carl carlsson"}, headers=headers) + item = body["result"][0] + assert response.status_code == 200 + assert item["name"] == "Carl Carlsson" diff --git a/server/tests/test_db.py b/server/tests/test_db.py index a68012798e042677774122f07b9ccf37257de39a..832329cc47fbb5bef8597a3e747facf5d3423a83 100644 --- a/server/tests/test_db.py +++ b/server/tests/test_db.py @@ -1,3 +1,4 @@ +import app.database.controller as dbc import pytest from app.database.models import ( City, @@ -13,9 +14,8 @@ from app.database.models import ( Team, User, ) -import app.database.controller as dbc from app.database.populate import add_default_values -from app.utils.test_helpers import * +from app.utils.test_helpers import assert_exists, assert_insert_fail, assert_object_values from tests import app, client, db @@ -89,8 +89,8 @@ def test_question(client): # Add competition item_city = City.query.filter_by(name="Linköping").first() - db.session.add(Competition("teknik8", item_style.id, item_city.id)) - db.session.add(Competition("teknik9", item_style.id, item_city.id)) + db.session.add(Competition("teknik8", 2020, item_style.id, item_city.id)) + db.session.add(Competition("teknik9", 2020, item_style.id, item_city.id)) db.session.commit() item_competition = Competition.query.filter_by(name="teknik8").first() item_competition_2 = Competition.query.filter_by(name="teknik9").first()