diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py
index 5eab3f829ae81e17ecc269d98f568eeacc45a68c..e5ec3d2ca1e1d9de6d4c5cca5ec1e3fba40f33ee 100644
--- a/server/app/apis/__init__.py
+++ b/server/app/apis/__init__.py
@@ -13,24 +13,62 @@ def validate_editor(db_item, *views):
         abort(http_codes.UNAUTHORIZED)
 
 
-def check_jwt(editor=False, *views):
-    def wrapper(fn):
-        @wraps(fn)
-        def decorator(*args, **kwargs):
+def _is_allowed(allowed, actual):
+    return actual and "*" in allowed or actual in allowed
+
+
+def _has_access(in_claim, in_route):
+    in_route = int(in_route) if in_route else None
+    return not in_route or in_claim and in_claim == in_route
+
+
+def protect_route(allowed_roles=None, allowed_views=None):
+    def wrapper(func):
+        def inner(*args, **kwargs):
             verify_jwt_in_request()
             claims = get_jwt_claims()
+
+            # Authorize request if roles has access to the route #
+
+            nonlocal allowed_roles
+            allowed_roles = allowed_roles or []
             role = claims.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 = claims.get("view")
-            if role == "Admin":
-                return fn(*args, **kwargs)
-            elif editor and role == "Editor":
-                return fn(*args, **kwargs)
-            elif view in views:
-                return fn(*args, **kwargs)
-            else:
-                abort(http_codes.UNAUTHORIZED)
-
-        return decorator
+            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 = claims.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 = claims.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 inner
 
     return wrapper
 
diff --git a/server/app/apis/alternatives.py b/server/app/apis/alternatives.py
index 2adad553ddb34bbd43e1405223c7c45962d66e19..0ce74d53b0a742ea31689d826f898e1b12f33b41 100644
--- a/server/app/apis/alternatives.py
+++ b/server/app/apis/alternatives.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import QuestionAlternativeDTO
 from flask_restx import Resource
 from flask_restx import reqparse
@@ -17,12 +17,12 @@ question_alternative_parser.add_argument("value", type=int, default=None, locati
 @api.route("")
 @api.param("competition_id, slide_id, question_id")
 class QuestionAlternativeList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self, competition_id, slide_id, question_id):
         items = dbc.get.question_alternative_list(competition_id, slide_id, question_id)
         return list_response(list_schema.dump(items))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self, competition_id, slide_id, question_id):
         args = question_alternative_parser.parse_args(strict=True)
         item = dbc.add.question_alternative(**args, question_id=question_id)
@@ -32,19 +32,19 @@ class QuestionAlternativeList(Resource):
 @api.route("/<alternative_id>")
 @api.param("competition_id, slide_id, question_id, alternative_id")
 class QuestionAlternatives(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self, competition_id, slide_id, question_id, alternative_id):
         items = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id)
         return item_response(schema.dump(items))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def put(self, competition_id, slide_id, question_id, alternative_id):
         args = question_alternative_parser.parse_args(strict=True)
         item = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id)
         item = dbc.edit.default(item, **args)
         return item_response(schema.dump(item))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def delete(self, competition_id, slide_id, question_id, alternative_id):
         item = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id)
         dbc.delete.default(item)
diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py
index 79a9c7a9080ae50e6353894ec6780987597c929f..8f72d3bde8610c255083df9deefc69b89a68560a 100644
--- a/server/app/apis/answers.py
+++ b/server/app/apis/answers.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import QuestionAnswerDTO
 from flask_restx import Resource
 from flask_restx import reqparse
@@ -22,12 +22,12 @@ question_answer_edit_parser.add_argument("score", type=int, default=None, locati
 @api.route("")
 @api.param("competition_id, team_id")
 class QuestionAnswerList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self, competition_id, team_id):
         items = dbc.get.question_answer_list(competition_id, team_id)
         return list_response(list_schema.dump(items))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def post(self, competition_id, team_id):
         args = question_answer_parser.parse_args(strict=True)
         item = dbc.add.question_answer(**args, team_id=team_id)
@@ -37,11 +37,12 @@ class QuestionAnswerList(Resource):
 @api.route("/<answer_id>")
 @api.param("competition_id, team_id, answer_id")
 class QuestionAnswers(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self, competition_id, team_id, answer_id):
         item = dbc.get.question_answer(competition_id, team_id, answer_id)
         return item_response(schema.dump(item))
 
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def put(self, competition_id, team_id, answer_id):
         args = question_answer_edit_parser.parse_args(strict=True)
         item = dbc.get.question_answer(competition_id, team_id, answer_id)
diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py
index 37da9e98d4b5970cd82b47dd54ad940cc53b6736..9c9ed24a876a9b130c385194bb8cc322bef65c9d 100644
--- a/server/app/apis/auth.py
+++ b/server/app/apis/auth.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, text_response
+from app.apis import item_response, protect_route, text_response
 from app.core.codes import verify_code
 from app.core.dto import AuthDTO, CodeDTO
 from flask_jwt_extended import (
@@ -12,6 +12,8 @@ from flask_jwt_extended import (
 )
 from flask_restx import Resource
 from flask_restx import inputs, reqparse
+from datetime import timedelta
+from app.core import sockets
 
 api = AuthDTO.api
 schema = AuthDTO.schema
@@ -33,9 +35,13 @@ def get_user_claims(item_user):
     return {"role": item_user.role.name, "city_id": item_user.city_id}
 
 
+def get_code_claims(item_code):
+    return {"view": item_code.view_type.name, "competition_id": item_code.competition_id, "team_id": item_code.team_id}
+
+
 @api.route("/signup")
 class AuthSignup(Resource):
-    @check_jwt(editor=False)
+    @protect_route(allowed_roles=["Admin"])
     def post(self):
         args = create_user_parser.parse_args(strict=True)
         email = args.get("email")
@@ -50,7 +56,7 @@ class AuthSignup(Resource):
 @api.route("/delete/<ID>")
 @api.param("ID")
 class AuthDelete(Resource):
-    @check_jwt(editor=False)
+    @protect_route(allowed_roles=["Admin"])
     def delete(self, ID):
         item_user = dbc.get.user(ID)
 
@@ -86,24 +92,38 @@ class AuthLoginCode(Resource):
         code = args["code"]
 
         if not verify_code(code):
-            api.abort(codes.BAD_REQUEST, "Invalid code")
+            api.abort(codes.UNAUTHORIZED, "Invalid code")
 
         item_code = dbc.get.code_by_code(code)
-        return item_response(CodeDTO.schema.dump(item_code))
+
+        if item_code.competition_id not in sockets.presentations:
+            api.abort(codes.UNAUTHORIZED, "Competition not active")
+
+        access_token = create_access_token(
+            item_code.id, user_claims=get_code_claims(item_code), expires_delta=timedelta(hours=8)
+        )
+
+        response = {
+            "competition_id": item_code.competition_id,
+            "view_type_id": item_code.view_type_id,
+            "team_id": item_code.team_id,
+            "access_token": access_token,
+        }
+        return response
 
 
 @api.route("/logout")
 class AuthLogout(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def post(self):
         jti = get_raw_jwt()["jti"]
         dbc.add.blacklist(jti)
-        return text_response("User logout")
+        return text_response("Logout")
 
 
 @api.route("/refresh")
 class AuthRefresh(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     @jwt_refresh_token_required
     def post(self):
         old_jti = get_raw_jwt()["jti"]
diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py
index 8a4105d24a77f07b04da1e1e2ed477746ea7a932..d07e17435aed31417254acfd83ed9315f1477ba3 100644
--- a/server/app/apis/codes.py
+++ b/server/app/apis/codes.py
@@ -1,5 +1,5 @@
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core import http_codes as codes
 from app.core.dto import CodeDTO
 from app.database.models import Code
@@ -13,7 +13,7 @@ list_schema = CodeDTO.list_schema
 @api.route("")
 @api.param("competition_id")
 class CodesList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, competition_id):
         items = dbc.get.code_list(competition_id)
         return list_response(list_schema.dump(items), len(items))
@@ -22,7 +22,7 @@ class CodesList(Resource):
 @api.route("/<code_id>")
 @api.param("competition_id, code_id")
 class CodesById(Resource):
-    @check_jwt(editor=False)
+    @protect_route(allowed_roles=["*"])
     def put(self, competition_id, code_id):
         item = dbc.get.one(Code, code_id)
         item.code = dbc.utils.generate_unique_code()
diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py
index fded52ec97df7d72acaf531eb5f0033c25227669..2a7b2a95b97ffcfdef986ad877f2b6a39e088539 100644
--- a/server/app/apis/competitions.py
+++ b/server/app/apis/competitions.py
@@ -1,7 +1,7 @@
 import time
 
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import CompetitionDTO
 from app.database.models import Competition
 from flask_restx import Resource
@@ -29,7 +29,7 @@ competition_search_parser.add_argument("city_id", type=int, default=None, locati
 
 @api.route("")
 class CompetitionsList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self):
         args = competition_parser.parse_args(strict=True)
 
@@ -44,12 +44,13 @@ class CompetitionsList(Resource):
 @api.route("/<competition_id>")
 @api.param("competition_id")
 class Competitions(Resource):
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self, competition_id):
         item = dbc.get.competition(competition_id)
 
         return item_response(rich_schema.dump(item))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def put(self, competition_id):
         args = competition_edit_parser.parse_args(strict=True)
         item = dbc.get.one(Competition, competition_id)
@@ -57,7 +58,7 @@ class Competitions(Resource):
 
         return item_response(schema.dump(item))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def delete(self, competition_id):
         item = dbc.get.one(Competition, competition_id)
         dbc.delete.competition(item)
@@ -67,7 +68,7 @@ class Competitions(Resource):
 
 @api.route("/search")
 class CompetitionSearch(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self):
         args = competition_search_parser.parse_args(strict=True)
         items, total = dbc.search.competition(**args)
@@ -77,7 +78,7 @@ class CompetitionSearch(Resource):
 @api.route("/<competition_id>/copy")
 @api.param("competition_id")
 class SlidesOrder(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self, competition_id):
         item_competition = dbc.get.competition(competition_id)
 
diff --git a/server/app/apis/components.py b/server/app/apis/components.py
index 8ab67b8cb8bd001c9641b6331325f328639b581e..c22ce4ad671329538e05a6a6ee7bb5fd9026ca38 100644
--- a/server/app/apis/components.py
+++ b/server/app/apis/components.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import ComponentDTO
 from flask_restx import Resource
 from flask_restx import reqparse
@@ -27,12 +27,12 @@ component_create_parser.add_argument("type_id", type=int, required=True, locatio
 @api.route("/<component_id>")
 @api.param("competition_id, slide_id, component_id")
 class ComponentByID(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self, competition_id, slide_id, component_id):
         item = dbc.get.component(competition_id, slide_id, component_id)
         return item_response(schema.dump(item))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def put(self, competition_id, slide_id, component_id):
         args = component_edit_parser.parse_args(strict=True)
         item = dbc.get.component(competition_id, slide_id, component_id)
@@ -40,7 +40,7 @@ class ComponentByID(Resource):
         item = dbc.edit.default(item, **args_without_none)
         return item_response(schema.dump(item))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def delete(self, competition_id, slide_id, component_id):
         item = dbc.get.component(competition_id, slide_id, component_id)
         dbc.delete.component(item)
@@ -50,12 +50,12 @@ class ComponentByID(Resource):
 @api.route("")
 @api.param("competition_id, slide_id")
 class ComponentList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self, competition_id, slide_id):
         items = dbc.get.component_list(competition_id, slide_id)
         return list_response(list_schema.dump(items))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self, competition_id, slide_id):
         args = component_create_parser.parse_args()
         item = dbc.add.component(slide_id=slide_id, **args)
diff --git a/server/app/apis/media.py b/server/app/apis/media.py
index 5d89550ac19aaff78643a34ae0a3059c011aed23..c7de8c4d4c1cbd3f482d386e654a9bfd0063370b 100644
--- a/server/app/apis/media.py
+++ b/server/app/apis/media.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import MediaDTO
 from app.core.parsers import search_parser
 from app.database.models import Media
@@ -22,13 +22,13 @@ media_parser_search.add_argument("filename", type=str, default=None, location="a
 
 @api.route("/images")
 class ImageList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self):
         args = media_parser_search.parse_args(strict=True)
         items, total = dbc.search.image(**args)
         return list_response(list_schema.dump(items), total)
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self):
         if "image" not in request.files:
             api.abort(codes.BAD_REQUEST, "Missing image in request.files")
@@ -48,12 +48,12 @@ class ImageList(Resource):
 @api.route("/images/<ID>")
 @api.param("ID")
 class ImageList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self, ID):
         item = dbc.get.one(Media, ID)
         return item_response(schema.dump(item))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def delete(self, ID):
         item = dbc.get.one(Media, ID)
         try:
diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py
index ab52362d46d026d8483c52f5c4d8384f32de755d..20a84e4c17c138b3a94ea6e3902e67154036cabc 100644
--- a/server/app/apis/misc.py
+++ b/server/app/apis/misc.py
@@ -1,5 +1,5 @@
 import app.database.controller as dbc
-from app.apis import check_jwt, list_response
+from app.apis import list_response, protect_route
 from app.core import http_codes
 from app.core.dto import MiscDTO
 from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType
@@ -23,6 +23,7 @@ name_parser.add_argument("name", type=str, required=True, location="json")
 
 @api.route("/types")
 class TypesList(Resource):
+    @protect_route(allowed_roles=["*"], allowed_views=["*"])
     def get(self):
         result = {}
         result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType))
@@ -34,7 +35,7 @@ class TypesList(Resource):
 
 @api.route("/roles")
 class RoleList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self):
         items = dbc.get.all(Role)
         return list_response(role_schema.dump(items))
@@ -42,12 +43,12 @@ class RoleList(Resource):
 
 @api.route("/cities")
 class CitiesList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self):
         items = dbc.get.all(City)
         return list_response(city_schema.dump(items))
 
-    @check_jwt(editor=False)
+    @protect_route(allowed_roles=["Admin"])
     def post(self):
         args = name_parser.parse_args(strict=True)
         dbc.add.city(args["name"])
@@ -58,7 +59,7 @@ class CitiesList(Resource):
 @api.route("/cities/<ID>")
 @api.param("ID")
 class Cities(Resource):
-    @check_jwt(editor=False)
+    @protect_route(allowed_roles=["Admin"])
     def put(self, ID):
         item = dbc.get.one(City, ID)
         args = name_parser.parse_args(strict=True)
@@ -67,7 +68,7 @@ class Cities(Resource):
         items = dbc.get.all(City)
         return list_response(city_schema.dump(items))
 
-    @check_jwt(editor=False)
+    @protect_route(allowed_roles=["Admin"])
     def delete(self, ID):
         item = dbc.get.one(City, ID)
         dbc.delete.default(item)
@@ -77,7 +78,7 @@ class Cities(Resource):
 
 @api.route("/statistics")
 class Statistics(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self):
         user_count = User.query.count()
         competition_count = Competition.query.count()
diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py
index aaefafc40608d14bf3a4e8a2a3b7fdfbda559f2c..de40a310db5f11a1b0c03cb20e1546f74682af70 100644
--- a/server/app/apis/questions.py
+++ b/server/app/apis/questions.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import QuestionDTO
 from flask_restx import Resource
 from flask_restx import reqparse
@@ -18,7 +18,7 @@ question_parser.add_argument("type_id", type=int, default=None, location="json")
 @api.route("/questions")
 @api.param("competition_id")
 class QuestionList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, competition_id):
         items = dbc.get.question_list_for_competition(competition_id)
         return list_response(list_schema.dump(items))
@@ -27,12 +27,12 @@ class QuestionList(Resource):
 @api.route("/slides/<slide_id>/questions")
 @api.param("competition_id, slide_id")
 class QuestionListForSlide(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, competition_id, slide_id):
         items = dbc.get.question_list(competition_id, slide_id)
         return list_response(list_schema.dump(items))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self, competition_id, slide_id):
         args = question_parser.parse_args(strict=True)
         item = dbc.add.question(slide_id=slide_id, **args)
@@ -42,12 +42,12 @@ class QuestionListForSlide(Resource):
 @api.route("/slides/<slide_id>/questions/<question_id>")
 @api.param("competition_id, slide_id, question_id")
 class QuestionById(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, competition_id, slide_id, question_id):
         item_question = dbc.get.question(competition_id, slide_id, question_id)
         return item_response(schema.dump(item_question))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def put(self, competition_id, slide_id, question_id):
         args = question_parser.parse_args(strict=True)
 
@@ -56,7 +56,7 @@ class QuestionById(Resource):
 
         return item_response(schema.dump(item_question))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def delete(self, competition_id, slide_id, question_id):
         item_question = dbc.get.question(competition_id, slide_id, question_id)
         dbc.delete.question(item_question)
diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py
index da29633a794cdb1262d2c307b8c80a94c794830e..4357df9e24e95c6febc448f6df5b7fd55912a5a2 100644
--- a/server/app/apis/slides.py
+++ b/server/app/apis/slides.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import SlideDTO
 from flask_restx import Resource
 from flask_restx import reqparse
@@ -19,12 +19,12 @@ slide_parser.add_argument("background_image_id", default=None, type=int, locatio
 @api.route("")
 @api.param("competition_id")
 class SlidesList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, competition_id):
         items = dbc.get.slide_list(competition_id)
         return list_response(list_schema.dump(items))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self, competition_id):
         item_slide = dbc.add.slide(competition_id)
         return item_response(schema.dump(item_slide))
@@ -33,12 +33,12 @@ class SlidesList(Resource):
 @api.route("/<slide_id>")
 @api.param("competition_id,slide_id")
 class Slides(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, competition_id, slide_id):
         item_slide = dbc.get.slide(competition_id, slide_id)
         return item_response(schema.dump(item_slide))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def put(self, competition_id, slide_id):
         args = slide_parser.parse_args(strict=True)
 
@@ -47,7 +47,7 @@ class Slides(Resource):
 
         return item_response(schema.dump(item_slide))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def delete(self, competition_id, slide_id):
         item_slide = dbc.get.slide(competition_id, slide_id)
 
@@ -58,7 +58,7 @@ class Slides(Resource):
 @api.route("/<slide_id>/order")
 @api.param("competition_id,slide_id")
 class SlideOrder(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def put(self, competition_id, slide_id):
         args = slide_parser.parse_args(strict=True)
         order = args.get("order")
@@ -87,7 +87,7 @@ class SlideOrder(Resource):
 @api.route("/<slide_id>/copy")
 @api.param("competition_id,slide_id")
 class SlideCopy(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self, competition_id, slide_id):
         item_slide = dbc.get.slide(competition_id, slide_id)
 
diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py
index 3fcb276c959c8880be30713bf08869d6b30b8dbb..c951ae53e6b7839d1457e60ccce60ea398ee617d 100644
--- a/server/app/apis/teams.py
+++ b/server/app/apis/teams.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import TeamDTO
 from flask_restx import Resource, reqparse
 from flask_restx import reqparse
@@ -16,12 +16,12 @@ team_parser.add_argument("name", type=str, location="json")
 @api.route("")
 @api.param("competition_id")
 class TeamsList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, competition_id):
         items = dbc.get.team_list(competition_id)
         return list_response(list_schema.dump(items))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def post(self, competition_id):
         args = team_parser.parse_args(strict=True)
         item_team = dbc.add.team(args["name"], competition_id)
@@ -31,19 +31,19 @@ class TeamsList(Resource):
 @api.route("/<team_id>")
 @api.param("competition_id,team_id")
 class Teams(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, competition_id, team_id):
         item = dbc.get.team(competition_id, team_id)
         return item_response(schema.dump(item))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def delete(self, competition_id, team_id):
         item_team = dbc.get.team(competition_id, team_id)
 
         dbc.delete.team(item_team)
         return {}, codes.NO_CONTENT
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def put(self, competition_id, team_id):
         args = team_parser.parse_args(strict=True)
         name = args.get("name")
diff --git a/server/app/apis/users.py b/server/app/apis/users.py
index a5b2d58b01f9ce8931fd29cba3af9d1d8b825cc0..50470db724ebf3648a2ff3a206a87b53650417ee 100644
--- a/server/app/apis/users.py
+++ b/server/app/apis/users.py
@@ -1,6 +1,6 @@
 import app.core.http_codes as codes
 import app.database.controller as dbc
-from app.apis import check_jwt, item_response, list_response
+from app.apis import item_response, list_response, protect_route
 from app.core.dto import UserDTO
 from flask_jwt_extended import get_jwt_identity
 from flask_restx import Resource
@@ -40,12 +40,12 @@ def _edit_user(item_user, args):
 
 @api.route("")
 class UsersList(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self):
         item = dbc.get.user(get_jwt_identity())
         return item_response(schema.dump(item))
 
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def put(self):
         args = user_parser.parse_args(strict=True)
         item = dbc.get.user(get_jwt_identity())
@@ -56,12 +56,12 @@ class UsersList(Resource):
 @api.route("/<ID>")
 @api.param("ID")
 class Users(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self, ID):
         item = dbc.get.user(ID)
         return item_response(schema.dump(item))
 
-    @check_jwt(editor=False)
+    @protect_route(allowed_roles=["Admin"])
     def put(self, ID):
         args = user_parser.parse_args(strict=True)
         item = dbc.get.user(ID)
@@ -71,7 +71,7 @@ class Users(Resource):
 
 @api.route("/search")
 class UserSearch(Resource):
-    @check_jwt(editor=True)
+    @protect_route(allowed_roles=["*"])
     def get(self):
         args = user_search_parser.parse_args(strict=True)
         items, total = dbc.search.user(**args)
diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py
index a83f5b95e8988a3d0024ca6d2f3b9601426147cc..57e41705cb06f500c8678510ac1ba0629eb9c0e7 100644
--- a/server/app/database/controller/add.py
+++ b/server/app/database/controller/add.py
@@ -151,9 +151,13 @@ def competition(name, year, city_id):
 
     # Add code for Judge view
     code(2, item_competition.id)
+
     # Add code for Audience view
     code(3, item_competition.id)
 
+    # Add code for Operator view
+    code(4, item_competition.id)
+
     item_competition = utils.refresh(item_competition)
     return item_competition
 
diff --git a/server/app/database/models.py b/server/app/database/models.py
index 3012938955f5c9e4fe1e285a0d95478ca7f7b8dd..b697b830cf2a8c4d55d3fb69a5be56ceececa50e 100644
--- a/server/app/database/models.py
+++ b/server/app/database/models.py
@@ -230,6 +230,8 @@ class Code(db.Model):
     competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False)
     team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=True)
 
+    view_type = db.relationship("ViewType", uselist=False)
+
     def __init__(self, code, view_type_id, competition_id=None, team_id=None):
         self.code = code
         self.view_type_id = view_type_id
@@ -240,7 +242,6 @@ class Code(db.Model):
 class ViewType(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     name = db.Column(db.String(STRING_SIZE), unique=True)
-    codes = db.relationship("Code", backref="view_type")
 
     def __init__(self, name):
         self.name = name
diff --git a/server/tests/test_app.py b/server/tests/test_app.py
index 199df387e878fdbbde759a80e6aa3d9ea486ea7d..d59428a6380b74abe8853c6c09c4df0bafc031e0 100644
--- a/server/tests/test_app.py
+++ b/server/tests/test_app.py
@@ -3,7 +3,9 @@ This file tests the api function calls.
 """
 
 import app.core.http_codes as codes
+from app.database.controller.add import competition
 from app.database.models import Slide
+from app.core import sockets
 
 from tests import app, client, db
 from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put
@@ -342,7 +344,7 @@ def test_question_api(client):
     slide_order = 1
     response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
     assert response.status_code == codes.OK
-    assert body["count"] == 1
+    assert body["count"] == 2
 
     # Get questions from another competition that should have some questions
     CID = 3
@@ -385,3 +387,67 @@ def test_question_api(client):
     response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_slide_order}/questions/{QID}", headers=headers)
     assert response.status_code == codes.NOT_FOUND
     """
+
+
+def test_authorization(client):
+    add_default_values()
+
+    # Fake that competition 1 is active
+    sockets.presentations[1] = {}
+
+    #### TEAM ####
+    # Login in with team code
+    response, body = post(client, "/api/auth/login/code", {"code": "111111"})
+    assert response.status_code == codes.OK
+    headers = {"Authorization": "Bearer " + body["access_token"]}
+
+    competition_id = body["competition_id"]
+    team_id = body["team_id"]
+
+    # Get competition team is in
+    response, body = get(client, f"/api/competitions/{competition_id}", headers=headers)
+    assert response.status_code == codes.OK
+
+    # Try to delete competition team is in
+    response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers)
+    assert response.status_code == codes.UNAUTHORIZED
+
+    # Try to get a different competition
+    response, body = get(client, f"/api/competitions/{competition_id+1}", headers=headers)
+    assert response.status_code == codes.UNAUTHORIZED
+
+    # Get own answers
+    response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers)
+    assert response.status_code == codes.OK
+
+    # Try to get another teams answers
+    response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers)
+    assert response.status_code == codes.UNAUTHORIZED
+
+    #### JUDGE ####
+    # Login in with judge code
+    response, body = post(client, "/api/auth/login/code", {"code": "222222"})
+    assert response.status_code == codes.OK
+    headers = {"Authorization": "Bearer " + body["access_token"]}
+
+    competition_id = body["competition_id"]
+
+    # Get competition judge is in
+    response, body = get(client, f"/api/competitions/{competition_id}", headers=headers)
+    assert response.status_code == codes.OK
+
+    # Try to delete competition judge is in
+    response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers)
+    assert response.status_code == codes.UNAUTHORIZED
+
+    # Try to get a different competition
+    response, body = get(client, f"/api/competitions/{competition_id+1}", headers=headers)
+    assert response.status_code == codes.UNAUTHORIZED
+
+    # Get team answers
+    response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers)
+    assert response.status_code == codes.OK
+
+    # Also get antoher teams answers
+    response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers)
+    assert response.status_code == codes.OK
\ No newline at end of file
diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py
index b5f1e54e136b759d9924a534acf2f37e6f7b08cd..5dabdbbe5508538291ce9eb52e078c21c8995132 100644
--- a/server/tests/test_helpers.py
+++ b/server/tests/test_helpers.py
@@ -3,14 +3,14 @@ import json
 import app.core.http_codes as codes
 import app.database.controller as dbc
 from app.core import db
-from app.database.models import City, Role
+from app.database.models import City, Code, Role
 
 
 def add_default_values():
     media_types = ["Image", "Video"]
     question_types = ["Boolean", "Multiple", "Text"]
     component_types = ["Text", "Image"]
-    view_types = ["Team", "Judge", "Audience"]
+    view_types = ["Team", "Judge", "Audience", "Operator"]
 
     roles = ["Admin", "Editor"]
     cities = ["Linköping", "Testköping"]
@@ -40,6 +40,20 @@ def add_default_values():
 
     # Add competitions
     item_competition = dbc.add.competition("Tom tävling", 2012, item_city.id)
+
+    item_question = dbc.add.question("hej", 5, 1, item_competition.slides[0].id)
+
+    item_team1 = dbc.add.team("Hej lag 3", item_competition.id)
+    item_team2 = dbc.add.team("Hej lag 4", item_competition.id)
+
+    db.session.add(Code("111111", 1, item_competition.id, item_team1.id))  # Team
+    db.session.add(Code("222222", 2, item_competition.id))  # Judge
+
+    dbc.add.QuestionAnswer("hej", 5, item_question.id, item_team1)
+    dbc.add.QuestionAnswer("då", 5, item_question.id, item_team2)
+
+    db.session.commit()
+
     for j in range(2):
         item_comp = dbc.add.competition(f"Tävling {j}", 2012, item_city.id)
         # Add two more slides to competition