diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py
index d33c768649d85edfdde1555ec389adb4e53fc51f..5364b2e631bb50b9fc9feb9e1cd71d1f39ecda74 100644
--- a/server/app/apis/__init__.py
+++ b/server/app/apis/__init__.py
@@ -2,7 +2,9 @@ from functools import wraps
 
 from flask_jwt_extended import verify_jwt_in_request
 from flask_jwt_extended.utils import get_jwt
+from flask_jwt_extended.view_decorators import jwt_required
 from flask_smorest import Blueprint, abort
+from marshmallow import Schema, fields
 
 Blueprint.PAGINATION_HEADER_FIELD_NAME = "pagination"
 
@@ -29,56 +31,75 @@ def _has_access(in_claim, in_route):
     return not in_route or in_claim and in_claim == in_route
 
 
-def protect_route(allowed_roles=None, allowed_views=None):
-    def decorator(func):
-        @wraps(func)
-        def wrapper(*args, **kwargs):
-            verify_jwt_in_request()
-            jwt = get_jwt()
+# class AuthorizationHeadersSchema(Schema):
 
-            # Authorize request if roles has access to the route #
+#     Authorization = fields.String(required=True)
 
-            nonlocal allowed_roles
-            allowed_roles = allowed_roles or []
-            role = jwt.get("role")
-            if _is_allowed(allowed_roles, role):
-                return func(*args, **kwargs)
 
-            # Authorize request if view has access and is trying to access the
-            # competition its in. Also check team if client is a team.
-            # Allow request if route doesn't belong to any competition.
-
-            nonlocal allowed_views
-            allowed_views = allowed_views or []
-            view = jwt.get("view")
-            if not _is_allowed(allowed_views, view):
-                abort(
-                    http_codes.UNAUTHORIZED,
-                    f"Client with view '{view}' is not allowed to access route with allowed views {allowed_views}.",
-                )
-
-            claim_competition_id = jwt.get("competition_id")
-            route_competition_id = kwargs.get("competition_id")
-            if not _has_access(claim_competition_id, route_competition_id):
-                abort(
-                    http_codes.UNAUTHORIZED,
-                    f"Client in competition '{claim_competition_id}' is not allowed to access competition '{route_competition_id}'.",
-                )
-
-            if view == "Team":
-                claim_team_id = jwt.get("team_id")
-                route_team_id = kwargs.get("team_id")
-                if not _has_access(claim_team_id, route_team_id):
+class ExtendedBlueprint(Blueprint):
+    def authorization(self, allowed_roles=None, allowed_views=None):
+        def decorator(func):
+
+            # func = self.arguments(AuthorizationHeadersSchema, location="headers")(func)
+            func = self.alt_response(http_codes.UNAUTHORIZED, None, description="Unauthorized")(func)
+
+            @wraps(func)
+            def wrapper(*args, **kwargs):
+
+                # Check that allowed_roles and allowed_views have correct type
+                nonlocal allowed_roles
+                nonlocal allowed_views
+                allowed_roles = allowed_roles or []
+                allowed_views = allowed_views or []
+                assert (
+                    isinstance(allowed_roles, list) or allowed_roles == "*"
+                ), f"Allowed roles must be a list or '*', not '{allowed_roles}'"
+                assert (
+                    isinstance(allowed_views, list) or allowed_views == "*"
+                ), f"Allowed views must be a list or '*', not '{allowed_views}'"
+
+                verify_jwt_in_request()
+                jwt = get_jwt()
+
+                # Authorize request if roles has access to the route #
+
+                role = jwt.get("role")
+                if _is_allowed(allowed_roles, role):
+                    return func(*args, **kwargs)
+
+                # Authorize request if view has access and is trying to access the
+                # competition its in. Also check team if client is a team.
+                # Allow request if route doesn't belong to any competition.
+
+                view = jwt.get("view")
+                if not _is_allowed(allowed_views, view):
                     abort(
                         http_codes.UNAUTHORIZED,
-                        f"Client in team '{claim_team_id}' is not allowed to access team '{route_team_id}'.",
+                        f"Client with view '{view}' is not allowed to access route with allowed views {allowed_views}.",
                     )
 
-            return func(*args, **kwargs)
+                claim_competition_id = jwt.get("competition_id")
+                route_competition_id = kwargs.get("competition_id")
+                if not _has_access(claim_competition_id, route_competition_id):
+                    abort(
+                        http_codes.UNAUTHORIZED,
+                        f"Client in competition '{claim_competition_id}' is not allowed to access competition '{route_competition_id}'.",
+                    )
+
+                if view == "Team":
+                    claim_team_id = jwt.get("team_id")
+                    route_team_id = kwargs.get("team_id")
+                    if not _has_access(claim_team_id, route_team_id):
+                        abort(
+                            http_codes.UNAUTHORIZED,
+                            f"Client in team '{claim_team_id}' is not allowed to access team '{route_team_id}'.",
+                        )
+
+                return func(*args, **kwargs)
 
-        return wrapper
+            return wrapper
 
-    return decorator
+        return decorator
 
 
 from flask_smorest import Api
diff --git a/server/app/apis/alternatives.py b/server/app/apis/alternatives.py
index a3143a7cea6093d1fe9d06210f780c7930c70f1d..e454a5faaa0e1fbf492b61b88c4271fbcbfb19fa 100644
--- a/server/app/apis/alternatives.py
+++ b/server/app/apis/alternatives.py
@@ -5,17 +5,16 @@ Default route: /api/competitions/<competition_id>/slides/<slide_id>/questions/<q
 
 
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core import ma
 from app.core.schemas import BaseSchema, QuestionAlternativeSchema
 from app.database import models
 from app.database.models import Question, QuestionAlternative
 from flask.views import MethodView
-from flask_smorest import Blueprint, abort
+from flask_smorest import abort
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "alternative",
     "alternative",
     url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/questions/<question_id>/alternatives",
@@ -43,13 +42,13 @@ class AlternativeEditArgsSchema(BaseSchema):
 
 @blp.route("")
 class Alternatives(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, QuestionAlternativeSchema(many=True))
     def get(self, competition_id, slide_id, question_id):
         """ Gets the all question alternatives to the specified question. """
         return dbc.get.question_alternative_list(competition_id, slide_id, question_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(AlternativeAddArgsSchema)
     @blp.response(http_codes.OK, QuestionAlternativeSchema)
     @blp.alt_response(http_codes.CONFLICT, None, description="Could not add alternative")
@@ -63,14 +62,14 @@ class Alternatives(MethodView):
 
 @blp.route("/<alternative_id>")
 class QuestionAlternatives(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, QuestionAlternativeSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find alternative")
     def get(self, competition_id, slide_id, question_id, alternative_id):
         """ Gets the specified question alternative. """
         return dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(AlternativeEditArgsSchema)
     @blp.response(http_codes.OK, QuestionAlternativeSchema)
     @blp.alt_response(http_codes.BAD_REQUEST, None, description="Paramters to edit alternative with is incorrect")
@@ -111,7 +110,7 @@ class QuestionAlternatives(MethodView):
 
         return dbc.edit.default(item, **args)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find alternative")
     @blp.alt_response(http_codes.CONFLICT, None, description="Could not delete alternative")
diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py
index 2bcbf159604eae735f781c5cbbe122d0bcb89986..b1116a14a1f57cb9b413cfeeffcd31c20ce52ffd 100644
--- a/server/app/apis/answers.py
+++ b/server/app/apis/answers.py
@@ -4,16 +4,14 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers
 """
 
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core import ma
 from app.core.schemas import BaseSchema, QuestionAlternativeAnswerSchema
 from app.database import models
 from flask.views import MethodView
-from flask_smorest import Blueprint, abort, response
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "answer",
     "answer",
     url_prefix="/api/competitions/<competition_id>/teams/<team_id>/answers",
@@ -30,7 +28,7 @@ class AnswerAddArgsSchema(BaseSchema):
 
 @blp.route("")
 class QuestionAlternativeList(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema)
     def get(self, competition_id, team_id):
         """ Gets all question answers that the specified team has given. """
@@ -39,13 +37,13 @@ class QuestionAlternativeList(MethodView):
 
 @blp.route("/<answer_id>")
 class QuestionAlternativeAnswers(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema)
     def get(self, competition_id, team_id, answer_id):
         """ Gets the specified question answer. """
         return dbc.get.question_alternative_answer(competition_id, team_id, answer_id)
 
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.arguments(AnswerAddArgsSchema)
     @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema)
     def put(self, args, competition_id, team_id, answer_id):
diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py
index b6379f281becb2d6acf7d0a48388098faf27e545..434ddfaf616228d7e045a1e2db9a1cae161d2437 100644
--- a/server/app/apis/auth.py
+++ b/server/app/apis/auth.py
@@ -3,24 +3,23 @@ All API calls concerning question answers.
 Default route: /api/auth
 """
 
-from datetime import datetime, timedelta, timezone
+from datetime import datetime, timedelta
 
 import app.database.controller as dbc
 import marshmallow as ma
 from app.core.codes import verify_code
-from app.core.schemas import UserSchema
 from app.core.sockets import is_active_competition
 from app.database.controller.delete import whitelist_to_blacklist
-from app.database.models import User, Whitelist
+from app.database.models import Whitelist
 from flask import current_app, has_app_context
 from flask.views import MethodView
 from flask_jwt_extended import create_access_token, get_jti
 from flask_jwt_extended.utils import get_jti, get_jwt
-from flask_smorest import Blueprint, abort, arguments
+from flask_smorest import abort
 
-from . import http_codes, protect_route
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "auth", "auth", url_prefix="/api/auth", description="Logging in as a user or with a code, and logging out"
 )
 
@@ -59,7 +58,7 @@ def get_code_claims(item_code):
 
 @blp.route("/test")
 class AuthSignup(MethodView):
-    @protect_route(allowed_roles=["Admin"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["Admin"], allowed_views=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     def get(self):
         """ Tests that the user is admin or is in a competition. """
@@ -119,7 +118,7 @@ class AuthLogin(MethodView):
 
 @blp.route("/logout")
 class AuthLogout(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     def post(self):
         """ Logs out. """
diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py
index aa8b806511d8679704296a679ce238aecfa54d83..e712428dd7ce1202b8e64086dbbaaf3b9e8521ae 100644
--- a/server/app/apis/codes.py
+++ b/server/app/apis/codes.py
@@ -4,22 +4,20 @@ Default route: /api/competitions/<competition_id>/codes
 """
 
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core.schemas import CodeSchema
 from app.database.models import Code
 from flask.views import MethodView
-from flask_smorest import Blueprint
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "code", "code", url_prefix="/api/competitions/<competition_id>/codes", description="Operations on codes"
 )
 
 
 @blp.route("")
 class CodesList(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["Operator"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["Operator"])
     @blp.response(http_codes.OK, CodeSchema(many=True))
     def get(self, competition_id):
         """ Gets the all competition codes. """
@@ -28,7 +26,7 @@ class CodesList(MethodView):
 
 @blp.route("/<code_id>")
 class CodesById(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, CodeSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Code not found")
     def put(self, competition_id, code_id):
diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py
index 10399bd8d7bdb8e3f95162914f45dfe09e544184..037446900869d3604e68cecff200cdb5c9f779d8 100644
--- a/server/app/apis/competitions.py
+++ b/server/app/apis/competitions.py
@@ -4,16 +4,17 @@ Default route: /api/competitions
 """
 
 import app.database.controller as dbc
-from app.apis import http_codes, protect_route
+from app.apis import ExtendedBlueprint, http_codes
 from app.core import ma
 from app.core.rich_schemas import CompetitionSchemaRich
 from app.core.schemas import BaseSchema, CompetitionSchema
 from app.database import models
 from app.database.models import Competition
 from flask.views import MethodView
-from flask_smorest import Blueprint, arguments
 
-blp = Blueprint("competitions", "competitions", url_prefix="/api/competitions", description="Operations competitions")
+blp = ExtendedBlueprint(
+    "competitions", "competitions", url_prefix="/api/competitions", description="Operations competitions"
+)
 
 
 class CompetitionAddArgsSchema(BaseSchema):
@@ -47,7 +48,7 @@ class CompetitionSearchArgsSchema(BaseSchema):
 
 @blp.route("")
 class Competitions(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(CompetitionAddArgsSchema)
     @blp.response(http_codes.OK, CompetitionSchema)
     @blp.alt_response(http_codes.CONFLICT, None, description="Competition could not be added")
@@ -58,14 +59,14 @@ class Competitions(MethodView):
 
 @blp.route("/<competition_id>")
 class CompetitionById(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, CompetitionSchemaRich)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Competition not found")
     def get(self, competition_id):
         """ Gets the specified competition. """
         return dbc.get.competition(competition_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(CompetitionEditArgsSchema)
     @blp.response(http_codes.OK, CompetitionSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Competition not found")
@@ -74,7 +75,7 @@ class CompetitionById(MethodView):
         """ Edits the specified competition with the specified arguments. """
         return dbc.edit.default(dbc.get.one(Competition, competition_id), **args)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Competition not found")
     @blp.alt_response(http_codes.CONFLICT, None, description="Competition could not be deleted")
@@ -86,7 +87,7 @@ class CompetitionById(MethodView):
 
 @blp.route("/search")
 class CompetitionSearch(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(CompetitionSearchArgsSchema, location="query")
     @blp.paginate()
     @blp.response(http_codes.OK, CompetitionSchema(many=True))
@@ -97,7 +98,7 @@ class CompetitionSearch(MethodView):
 
 @blp.route("/<competition_id>/copy")
 class SlidesOrder(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, CompetitionSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Competition not found")
     @blp.alt_response(http_codes.CONFLICT, None, description="Competition could not be copied")
diff --git a/server/app/apis/components.py b/server/app/apis/components.py
index d0e45da6020652c0e627f17f01d5b5e7785d878d..8e3f8800c4f183a2824eb40d6b81cf8f2b71d9f8 100644
--- a/server/app/apis/components.py
+++ b/server/app/apis/components.py
@@ -4,17 +4,13 @@ Default route: /api/competitions/<competition_id>/slides/<slide_id>/components
 """
 
 import app.database.controller as dbc
-from app.apis import protect_route
-from app.core import ma
 from app.core.schemas import BaseSchema, ComponentSchema
-from app.database import models
 from flask.views import MethodView
-from flask_smorest import Blueprint, abort
 from marshmallow import fields
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "component",
     "component",
     url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/components",
@@ -54,13 +50,13 @@ class ComponentEditArgsSchema(BaseSchema):
 
 @blp.route("")
 class Components(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, ComponentSchema(many=True))
     def get(self, competition_id, slide_id):
         """ Gets all components in the specified slide and competition. """
         return dbc.get.component_list(competition_id, slide_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(ComponentAddArgsSchema)
     @blp.response(http_codes.OK, ComponentSchema)
     def post(self, args, competition_id, slide_id):
@@ -70,14 +66,14 @@ class Components(MethodView):
 
 @blp.route("/<component_id>")
 class ComponentById(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, ComponentSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find component")
     def get(self, competition_id, slide_id, component_id):
         """ Gets the specified component. """
         return dbc.get.component(competition_id, slide_id, component_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(ComponentEditArgsSchema)
     @blp.response(http_codes.OK, ComponentSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find component")
@@ -86,7 +82,7 @@ class ComponentById(MethodView):
         """ Edits the specified component using the provided arguments. """
         return dbc.edit.default(dbc.get.component(competition_id, slide_id, component_id), **args)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find component")
     @blp.alt_response(http_codes.CONFLICT, None, description="Could not delete component")
@@ -98,7 +94,7 @@ class ComponentById(MethodView):
 
 @blp.route("/<component_id>/copy/<view_type_id>")
 class ComponentCopy(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, ComponentSchema)
     def post(self, competition_id, slide_id, component_id, view_type_id):
         """ Creates a deep copy of the specified component. """
diff --git a/server/app/apis/media.py b/server/app/apis/media.py
index 05e7f0c5e2ecb4ef511ec7e512a1d25c6c300224..b24515e1a6dbb7ce7ebb872c321a2488abc19633 100644
--- a/server/app/apis/media.py
+++ b/server/app/apis/media.py
@@ -5,18 +5,17 @@ Default route: /api/media
 
 import app.core.files as files
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core import ma
 from app.core.schemas import BaseSchema, MediaSchema
 from app.database import models
 from flask import request
 from flask.views import MethodView
 from flask_jwt_extended import get_jwt_identity
-from flask_smorest import Blueprint, abort
+from flask_smorest import abort
 from flask_uploads import UploadNotAllowed
 from sqlalchemy import exc
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
 
 class ImageSearchArgsSchema(BaseSchema):
@@ -26,12 +25,12 @@ class ImageSearchArgsSchema(BaseSchema):
     filename = ma.auto_field(required=False)
 
 
-blp = Blueprint("media", "media", url_prefix="/api/media", description="Operations on media")
+blp = ExtendedBlueprint("media", "media", url_prefix="/api/media", description="Operations on media")
 
 
 @blp.route("/images")
 class Images(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(ImageSearchArgsSchema, location="query")
     @blp.paginate()
     @blp.response(http_codes.OK, MediaSchema(many=True))
@@ -39,7 +38,7 @@ class Images(MethodView):
         """ Gets a list of all images with the specified filename. """
         return dbc.search.image(pagination_parameters, **args)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, MediaSchema)
     @blp.alt_response(http_codes.BAD_REQUEST, None, description="Could not save image")
     @blp.alt_response(http_codes.INTERNAL_SERVER_ERROR, None, description="Could not save image")
@@ -63,14 +62,14 @@ class Images(MethodView):
 
 @blp.route("/images/<media_id>")
 class ImageById(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, MediaSchema)
     @blp.alt_response(http_codes.NOT_FOUND, MediaSchema, description="Could not find image")
     def get(self, media_id):
         """ Gets the specified image. """
         return dbc.get.one(models.Media, media_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     @blp.response(http_codes.CONFLICT, None, description="Could not delete image it is used by something")
     @blp.response(http_codes.BAD_REQUEST, None, description="Failed to delete image")
diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py
index 902407ddf20278b452365766e83c149b7c0a7d31..033b91679839d182fe4b514f2d01c9bb5294fa67 100644
--- a/server/app/apis/misc.py
+++ b/server/app/apis/misc.py
@@ -5,7 +5,6 @@ Default route: /api/misc
 
 import app.database.controller as dbc
 import marshmallow as ma
-from app.apis import protect_route
 from app.core.schemas import (
     BaseSchema,
     CitySchema,
@@ -16,15 +15,13 @@ from app.core.schemas import (
     ViewTypeSchema,
 )
 from app.database import models
-from app.database.controller.add import competition
 from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType
 from flask.views import MethodView
-from flask_smorest import Blueprint, response
 from marshmallow_sqlalchemy import auto_field
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint("misc", "misc", url_prefix="/api/misc", description="Roles, regions, types and statistics")
+blp = ExtendedBlueprint("misc", "misc", url_prefix="/api/misc", description="Roles, regions, types and statistics")
 
 
 class TypesResponseSchema(BaseSchema):
@@ -49,7 +46,7 @@ class Types(MethodView):
 
 @blp.route("/roles")
 class RoleList(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, RoleSchema(many=True))
     def get(self):
         """ Gets a list of all roles. """
@@ -65,13 +62,13 @@ class CityAddArgsSchema(BaseSchema):
 
 @blp.route("/cities")
 class CitiesList(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, CitySchema(many=True))
     def get(self):
         """ Gets a list of all cities. """
         return dbc.get.all(City)
 
-    @protect_route(allowed_roles=["Admin"])
+    @blp.authorization(allowed_roles=["Admin"])
     @blp.arguments(CitySchema)
     @blp.response(http_codes.OK, CitySchema(many=True))
     def post(self, args):
@@ -82,7 +79,7 @@ class CitiesList(MethodView):
 
 @blp.route("/cities/<city_id>")
 class Cities(MethodView):
-    @protect_route(allowed_roles=["Admin"])
+    @blp.authorization(allowed_roles=["Admin"])
     @blp.arguments(CitySchema)
     @blp.response(http_codes.OK, CitySchema(many=True))
     @blp.alt_response(http_codes.NOT_FOUND, None, description="City not found")
@@ -92,7 +89,7 @@ class Cities(MethodView):
         dbc.edit.default(dbc.get.one(City, city_id), **args)
         return dbc.get.all(City)
 
-    @protect_route(allowed_roles=["Admin"])
+    @blp.authorization(allowed_roles=["Admin"])
     @blp.response(http_codes.OK, CitySchema(many=True))
     @blp.alt_response(http_codes.NOT_FOUND, None, description="City not found")
     @blp.alt_response(http_codes.CONFLICT, None, description="The city can't be updated with the provided values")
@@ -110,7 +107,7 @@ class StatisticsResponseSchema(BaseSchema):
 
 @blp.route("/statistics")
 class Statistics(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, StatisticsResponseSchema)
     def get(self):
         """ Gets statistics. """
diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py
index 60ed21006c095cf3787d8c302bc4489c4edfaf10..f3190d11a8a067975da8dec8790e574f4eae5c35 100644
--- a/server/app/apis/questions.py
+++ b/server/app/apis/questions.py
@@ -4,17 +4,14 @@ Default route: /api/competitions/<competition_id>
 """
 
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core import ma
-from app.core.schemas import BaseSchema, QuestionSchema, SlideSchema
+from app.core.schemas import BaseSchema, QuestionSchema
 from app.database import models
-from app.database.controller.edit import default
 from flask.views import MethodView
-from flask_smorest import Blueprint
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "question",
     "question",
     url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/questions",
@@ -44,13 +41,13 @@ class QuestionEditArgsSchema(BaseSchema):
 
 @blp.route("")
 class Questions(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, QuestionSchema(many=True))
     def get(self, competition_id, slide_id):
         """ Gets all questions in the specified competition and slide. """
         return dbc.get.question_list(competition_id, slide_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(QuestionAddArgsSchema)
     @blp.response(http_codes.OK, QuestionSchema)
     @blp.alt_response(http_codes.CONFLICT, None, description="Could not add question")
@@ -61,7 +58,7 @@ class Questions(MethodView):
 
 @blp.route("/<question_id>")
 class QuestionById(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, QuestionSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find question")
     def get(self, competition_id, slide_id, question_id):
@@ -70,7 +67,7 @@ class QuestionById(MethodView):
         """
         return dbc.get.question(competition_id, slide_id, question_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(QuestionEditArgsSchema)
     @blp.response(http_codes.OK, QuestionSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find question")
@@ -79,7 +76,7 @@ class QuestionById(MethodView):
         """ Edits the specified question with the provided arguments. """
         return dbc.edit.default(dbc.get.question(competition_id, slide_id, question_id), **args)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find question")
     @blp.alt_response(http_codes.CONFLICT, None, description="Could not delete question")
diff --git a/server/app/apis/scores.py b/server/app/apis/scores.py
index c90e4e5c569f917bf20af832e2ca6d5fe444cac4..a906185c09d369e2956d25e978286fbc892a4b6e 100644
--- a/server/app/apis/scores.py
+++ b/server/app/apis/scores.py
@@ -4,16 +4,14 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers/quesit
 """
 
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core import ma
 from app.core.schemas import BaseSchema, QuestionScoreSchema
 from app.database import models
 from flask.views import MethodView
-from flask_smorest import Blueprint, abort, response
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "score",
     "score",
     url_prefix="/api/competitions/<competition_id>/teams/<team_id>/scores",
@@ -30,7 +28,7 @@ class ScoreAddArgsSchema(BaseSchema):
 
 @blp.route("")
 class QuestionScoreList(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, QuestionScoreSchema(many=True))
     def get(self, competition_id, team_id):
         """ Gets all question answers that the specified team has given. """
@@ -39,14 +37,14 @@ class QuestionScoreList(MethodView):
 
 @blp.route("/<question_id>")
 class QuestionScores(MethodView):
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.response(http_codes.OK, QuestionScoreSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Cant find answer")
     def get(self, competition_id, team_id, question_id):
         """ Gets the score for the provided team on the provided question. """
         return dbc.get.question_score(competition_id, team_id, question_id)
 
-    @protect_route(allowed_roles=["*"], allowed_views=["*"])
+    @blp.authorization(allowed_roles=["*"], allowed_views=["*"])
     @blp.arguments(ScoreAddArgsSchema)
     @blp.response(http_codes.OK, QuestionScoreSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Cant find score")
diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py
index a8d447fa0995c8ad216f73580eecf4622ec0bcfb..177c57179779e83b3652e5dbe7e2f5832c2af3d2 100644
--- a/server/app/apis/slides.py
+++ b/server/app/apis/slides.py
@@ -4,17 +4,16 @@ Default route: /api/competitions/<competition_id>/slides
 """
 
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core import ma
 from app.core.schemas import BaseSchema, SlideSchema
 from app.database import models
 from app.database.models import Competition, Slide
 from flask.views import MethodView
-from flask_smorest import Blueprint, abort
+from flask_smorest import abort
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "slide",
     "slide",
     url_prefix="/api/competitions/<competition_id>/slides",
@@ -34,13 +33,13 @@ class SlideEditArgsSchema(BaseSchema):
 
 @blp.route("")
 class Slides(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, SlideSchema(many=True))
     def get(self, competition_id):
         """ Gets all slides from the specified competition. """
         return dbc.get.slide_list(competition_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, SlideSchema)
     @blp.alt_response(http_codes.CONFLICT, None, description="Can't add slide")
     def post(self, competition_id):
@@ -50,14 +49,14 @@ class Slides(MethodView):
 
 @blp.route("/<slide_id>")
 class Slides(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, SlideSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None)
     def get(self, competition_id, slide_id):
         """ Gets the specified slide. """
         return dbc.get.slide(competition_id, slide_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(SlideEditArgsSchema)
     @blp.response(http_codes.OK, SlideSchema)
     @blp.alt_response(http_codes.CONFLICT, None, description="Can't edit slide")
@@ -77,7 +76,7 @@ class Slides(MethodView):
 
         return dbc.edit.default(item_slide, **args)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Slide not found")
     @blp.alt_response(http_codes.CONFLICT, None, description="Can't delete slide")
@@ -89,7 +88,7 @@ class Slides(MethodView):
 
 @blp.route("/<slide_id>/copy")
 class SlideCopy(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, SlideSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Can't find slide")
     def post(self, competition_id, slide_id):
diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py
index 4071a89ddee43e8d7783a2a78f71507c99e15230..03cf0be7113f9b7e140cca707b703f184cc5747b 100644
--- a/server/app/apis/teams.py
+++ b/server/app/apis/teams.py
@@ -4,16 +4,14 @@ Default route: /api/competitions/<competition_id>/teams
 """
 
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core import ma
 from app.core.schemas import BaseSchema, TeamSchema
 from app.database import models
 from flask.views import MethodView
-from flask_smorest import Blueprint
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "team",
     "team",
     url_prefix="/api/competitions/<competition_id>/teams",
@@ -37,13 +35,13 @@ class TeamEditArgsSchema(BaseSchema):
 
 @blp.route("")
 class Teams(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, TeamSchema(many=True))
     def get(self, competition_id):
         """ Gets all teams to the specified competition. """
         return dbc.get.team_list(competition_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(TeamAddArgsSchema)
     @blp.response(http_codes.OK, TeamSchema)
     @blp.alt_response(http_codes.CONFLICT, None, description="Could not add team")
@@ -54,14 +52,14 @@ class Teams(MethodView):
 
 @blp.route("/<team_id>")
 class TeamsById(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, TeamSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find team")
     def get(self, competition_id, team_id):
         """ Gets the specified team. """
         return dbc.get.team(competition_id, team_id)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(TeamEditArgsSchema)
     @blp.response(http_codes.OK, TeamSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find team")
@@ -69,7 +67,7 @@ class TeamsById(MethodView):
         """ Edits the specified team using the provided arguments. """
         return dbc.edit.default(dbc.get.team(competition_id, team_id), **args)
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.NO_CONTENT, None)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find team")
     def delete(self, competition_id, team_id):
diff --git a/server/app/apis/users.py b/server/app/apis/users.py
index a982bb0b0656445cf05368d57698224dd745467f..28849ab6e5fd3885e669f8cd92fca22ec43c53f7 100644
--- a/server/app/apis/users.py
+++ b/server/app/apis/users.py
@@ -5,19 +5,18 @@ Default route: /api/users
 
 
 import app.database.controller as dbc
-from app.apis import protect_route
 from app.core import ma
 from app.core.schemas import BaseSchema, UserSchema
 from app.database import models
 from app.database.models import User, Whitelist
 from flask.views import MethodView
 from flask_jwt_extended.utils import get_jwt_identity
-from flask_smorest import Blueprint, abort
+from flask_smorest import abort
 from marshmallow import fields
 
-from . import http_codes
+from . import ExtendedBlueprint, http_codes
 
-blp = Blueprint(
+blp = ExtendedBlueprint(
     "users", "users", url_prefix="/api/users", description="Adding, updating, deleting and searching for users"
 )
 
@@ -55,20 +54,20 @@ class UserSearchArgsSchema(BaseSchema):
 
 @blp.route("")
 class Users(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, UserSchema)
     def get(self):
         """ Get currently logged in user. """
         return dbc.get.one(User, get_jwt_identity())
 
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(UserEditArgsSchema)
     @blp.response(http_codes.OK, UserSchema)
     def put(self, args):
         """ Edit current user. """
         return _edit_user(dbc.get.one(User, get_jwt_identity()), args)
 
-    @protect_route(allowed_roles=["Admin"])
+    @blp.authorization(allowed_roles=["Admin"])
     @blp.arguments(UserAddArgsSchema)
     @blp.response(http_codes.OK, UserSchema)
     def post(self, args):
@@ -78,14 +77,14 @@ class Users(MethodView):
 
 @blp.route("/<user_id>")
 class UsersById(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.response(http_codes.OK, UserSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="User not found")
     def get(self, user_id):
         """ Get user with <user_id> """
         return dbc.get.one(User, user_id)
 
-    @protect_route(allowed_roles=["Admin"])
+    @blp.authorization(allowed_roles=["Admin"])
     @blp.arguments(UserEditArgsSchema)
     @blp.response(http_codes.OK, UserSchema)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="User not found")
@@ -94,7 +93,7 @@ class UsersById(MethodView):
         """ Edits user with <user_id> """
         return _edit_user(dbc.get.one(User, user_id), args)
 
-    @protect_route(allowed_roles=["Admin"])
+    @blp.authorization(allowed_roles=["Admin"])
     @blp.response(http_codes.NO_CONTENT, None)
     @blp.alt_response(http_codes.NOT_FOUND, None, description="User not found")
     @blp.alt_response(http_codes.CONFLICT, None, description="The user can't be deleted")
@@ -108,7 +107,7 @@ class UsersById(MethodView):
 
 @blp.route("/search")
 class UserSearch(MethodView):
-    @protect_route(allowed_roles=["*"])
+    @blp.authorization(allowed_roles=["*"])
     @blp.arguments(UserSearchArgsSchema, location="query")
     @blp.paginate()
     @blp.response(http_codes.OK, UserSchema(many=True))