Skip to content

Commit

Permalink
adding the password functionality in the backend api and writing the …
Browse files Browse the repository at this point in the history
…tests according to it. Adding possibility to pass additional fields in api response generator
  • Loading branch information
ooemperor committed Jan 15, 2024
1 parent f065457 commit 0e31192
Show file tree
Hide file tree
Showing 15 changed files with 520 additions and 19 deletions.
34 changes: 31 additions & 3 deletions codeGrader/backend/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
from functools import wraps
from gevent.pywsgi import WSGIServer


from flask import Flask, request, send_file
from codeGrader.backend.config import config
from codeGrader.backend.api.handlers import UserHandler, ProfileHandler, AdminUserHandler, SubjectHandler, \
ExerciseHandler, TaskHandler, FileHandler, SubmissionHandler, TestCaseHandler, AdminUserLoginHandler, \
authentication, AdminTypeHandler, UserLoginHandler, InstructionHandler, AttachmentHandler, ScoreHandler
authentication, AdminTypeHandler, UserLoginHandler, InstructionHandler, AttachmentHandler, ScoreHandler, \
AdminUserPasswordResetHandler, UserPasswordResetHandler
from codeGrader.backend.api.logger import Logger
from codeGrader.backend.api.util import upload_file, preprocess_task_file
import logging
Expand Down Expand Up @@ -64,7 +64,7 @@ def app_index():
method = route.methods
rule = route.rule
endpoint = route.endpoint
output_data.append({rule:{"methods": method, "endpoint": endpoint} })
output_data.append({rule: {"methods": method, "endpoint": endpoint}})
output["routes"] = output_data
return output

Expand Down Expand Up @@ -135,6 +135,20 @@ def user(id_: int) -> dict:
return UserHandler().delete(id_)


@app.route("/user/<int:id_>/passwordreset", methods=['POST'])
@authentication
def user_password_reset(id_: int) -> dict:
"""
Route for password reset on a user
@param id_: The identifier of the user
@type id_: int
@return: Custom Response messgae that we get from the handler class.
@rtype: dict
"""
if request.method == 'POST':
return UserPasswordResetHandler().reset(id_)


@app.route("/users", methods=['GET'])
@authentication
def users() -> dict:
Expand Down Expand Up @@ -177,6 +191,20 @@ def admin(id_: int) -> dict:
return AdminUserHandler().delete(id_)


@app.route("/admin/<int:id_>/passwordreset", methods=['POST'])
@authentication
def admin_password_reset(id_: int) -> dict:
"""
Route for password reset on a admin user
@param id_: The identifier of the admin user
@type id_: int
@return: Custom Response messgae that we get from the handler class.
@rtype: dict
"""
if request.method == 'POST':
return AdminUserPasswordResetHandler().reset(id_)


@app.route("/admins", methods=['GET'])
@authentication
def admins() -> dict:
Expand Down
8 changes: 4 additions & 4 deletions codeGrader/backend/api/handlers/Base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __init__(self):
self.errorResponseHandler = ErrorResponseHandler() # adding errorhandler in case we need it
self.genericResponseHandler = GenericResponseHandler() # generic Response Handler when there are no errors

def create_generic_response(self, method: str, response: str, id_: int = None):
def create_generic_response(self, method: str, response: str, id_: int = None, **kwargs):
"""
Generates a generic method for a response Body
@param method: the request method
Expand All @@ -60,9 +60,9 @@ def create_generic_response(self, method: str, response: str, id_: int = None):
@return: The response JSON
@rtype: dict
"""
return self.genericResponseHandler.generate_response(method, response, id_)
return self.genericResponseHandler.generate_response(method, response, id_, **kwargs)

def create_generic_error_response(self, method: str, exception: Exception, id_: int = None):
def create_generic_error_response(self, method: str, exception: Exception, id_: int = None, **kwargs):
"""
Generating a generic error response when there is some sort of a Problem.
@param method: The method used to call the handler e.g. PUT, POST and more
Expand All @@ -74,7 +74,7 @@ def create_generic_error_response(self, method: str, exception: Exception, id_:
@return: The custom error message that shall be provided to the user.
@rtype: dict
"""
return self.errorResponseHandler.generate_response(method, exception, id_)
return self.errorResponseHandler.generate_response(method, exception, id_, **kwargs)

def _preprocess_data_dict(self, dict_: dict):
"""
Expand Down
4 changes: 1 addition & 3 deletions codeGrader/backend/api/handlers/LoginHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,9 @@ class LoginHandler(BaseHandler):
Used by the frontend to check the login status of a AdminUser.
"""

# instance variables that are not set in this parent class

def __init__(self) -> None:
"""
Constructor of the LoginHandler Partent Class.
Constructor of the LoginHandler Parent Class.
Uses the Basehandler
"""
super().__init__()
Expand Down
109 changes: 109 additions & 0 deletions codeGrader/backend/api/handlers/PasswordReset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# CodeGrader - https://github.com/ooemperor/CodeGrader
# Copyright © 2023, 2024 Michael Kaiser <michael.kaiser@emplabs.ch>
#
# This file is part of CodeGrader.
#
# CodeGrader is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CodeGrader is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CodeGrader. If not, see <http://www.gnu.org/licenses/>.

"""
Handler for the Password Reset on the User and Admin Objects
@author: mkaiser
"""
import string

import sqlalchemy.exc

from .Base import BaseHandler
from codeGrader.backend.db.Admin import AdminUser
from codeGrader.backend.db.User import User
from sqlalchemy import select
import hashlib
import random


class PasswordResetHandler(BaseHandler):
"""
Class for the password Reset on the user objects
Specific User PW Reset Handlers will inherit this class
"""

def __init__(self) -> None:
"""
Constructor of the PasswordReset Parent Class.
Uses the Basehandler
"""
super().__init__()
self.password = None
self.generate_random_password()

def generate_random_password(self):
char_list = string.ascii_letters + string.digits + "!-_.?/"
new_password = ""
for i in range(10):
j = random.randint(0, len(char_list) - 1)
new_password += char_list[j]

self.password = new_password

def reset(self, id_: int) -> dict:
"""
Reset function on the User Object
@param id_: the id of the user in the database
@type id_: int
@return: A Dictionary reporting success or failure
@rtype: dict
"""
try:

self.password = self.password.encode('utf-8')
self.password = hashlib.sha256(self.password)
self.password = self.password.hexdigest()

user = self.sql_session.get_object(self.dbClass, id_)

user_dict = {"username": user.username, "password": self.password}
print(self.password)
self.sql_session.update(self.dbClass, user.id, user_dict)

return self.create_generic_response('POST', "Password has been reset", password=self.password)

except Exception as e:
print(e)
return self.create_generic_error_response('POST', "Password Reset failed for User")


class AdminUserPasswordResetHandler(PasswordResetHandler):
"""
Class for the Password Reset Handler of the admin user.
"""

def __init__(self) -> None:
"""
Constructor of the AdminUserPasswordResetHandler
"""
super().__init__()
self.dbClass = AdminUser


class UserPasswordResetHandler(PasswordResetHandler):
"""
Class for the Password Reset Handler of the user.
"""

def __init__(self):
"""
Constructor of the UserPasswordResetHandler
"""
super().__init__()
self.dbClass = User
5 changes: 4 additions & 1 deletion codeGrader/backend/api/handlers/ResponseGenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def _get_response_code(method: str) -> int:
else:
return 200

def generate_response(self, method: str, response: str, id_: int = None) -> (dict, int):
def generate_response(self, method: str, response: str, id_: int = None, **kwargs) -> (dict, int):
"""
Generates a generic method for a response Body
@param method: the request method
Expand All @@ -70,6 +70,9 @@ def generate_response(self, method: str, response: str, id_: int = None) -> (dic
out["method"] = method
out["response"] = _response

for key, value in kwargs.items(): # adding custom arguments to the response message
out[key] = value

return out, self._get_response_code(method)


Expand Down
3 changes: 2 additions & 1 deletion codeGrader/backend/api/handlers/User.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
# TODO: Define how to handle realtions of Profile and more.
"""
from codeGrader.backend.api.handlers.Base import BaseHandler
from codeGrader.backend.db import User, AdminUser
from codeGrader.backend.db import User
import hashlib


class UserHandler(BaseHandler):
"""
Handler for a UserObject
Expand Down
3 changes: 2 additions & 1 deletion codeGrader/backend/api/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
from .TestCase import TestCaseHandler
from .LoginHandler import AdminUserLoginHandler, UserLoginHandler
from .Score import ScoreHandler
from .PasswordReset import AdminUserPasswordResetHandler, UserPasswordResetHandler

__all__ = ["BaseHandler", "UserHandler", "AdminUserHandler", "ProfileHandler", "SubjectHandler", "TaskHandler",
"ExerciseHandler", "FileHandler", "SubmissionHandler", "authentication", "AuthorizationFail",
"TestCaseHandler", "AdminUserLoginHandler", "AdminTypeHandler", "UserLoginHandler", "AttachmentHandler",
"InstructionHandler", "ScoreHandler"]
"InstructionHandler", "ScoreHandler", "AdminUserPasswordResetHandler", "UserPasswordResetHandler"]
4 changes: 0 additions & 4 deletions codeGrader/backend/evaluation/Evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,8 @@ def evaluate(self, expectedSolution, actualSolution) -> bool: # TODO: define da
@rtype: Boolean.
"""

expectedSolution
actualSolution
assert actualSolution is not None
assert self.evaluation_type is not None
print(expectedSolution[0])
print(actualSolution)
if self.evaluation_type == "basic":
evaluation = self._basic_full_compare_evaluation(expectedSolution, actualSolution)
elif self.evaluation_type == "line-per-line":
Expand Down
2 changes: 1 addition & 1 deletion codeGrader/frontend/user/templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<!-- Add all page content inside this div if you want the side nav to push page content to the right (not used if you only want the sidenav to sit on top of the page -->
<div id="main" class="main">
<div>
<h1 class="header1">Settings</h1>
<h1>Settings</h1>
</div>
<div class="row">
<p class="core_content">User {{ username }}</p>
Expand Down
46 changes: 45 additions & 1 deletion tests/backend/api_tests/test_ApiAdmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,51 @@ def test_createUpdateAndDeleteAdminUser(self):
r = requests.delete(f"{adminUser_url}{adminUser_id}", headers=self.headers)
self.assertIsNotNone(r)
self.assertEqual(204, r.status_code)
def test_createAndDeleteAdminUser_withPasswordReset(self):
"""
Test Case for creating and deleting the AdminUser with Password Reset
Covers post, get and delete for the api/user
@return: No return
"""
headers = dict()
headers["Authorization"] = f"{api_config.apiAuthentication} {api_config.apiToken}"
create_url = f"http://{config.tests_ApiHost}:{config.tests_ApiPort}/admin/add"
adminUser_url = f"http://{config.tests_ApiHost}:{config.tests_ApiPort}/admin/"
adminUser_dict = {
"username": "admin_test",
"first_name": "admin",
"last_name": "user",
"email": "test.user@mail.com",
"password": "myPassword",
"tag": "usertag",
"admin_type": 1
}

# creating the user
r = requests.post(create_url, json=adminUser_dict, headers=self.headers)
self.assertIsNotNone(r)
self.assertEqual(201, r.status_code)
adminUser_id = json.loads(r.text)["response"]["id"]

# checks after creation
r = requests.get(f"{adminUser_url}{adminUser_id}", headers=self.headers)
self.assertEqual(200, r.status_code)
self.assertEqual("admin_test", json.loads(r.text)["username"])
self.assertEqual("admin", json.loads(r.text)["first_name"])
self.assertEqual("user", json.loads(r.text)["last_name"])
self.assertEqual("test.user@mail.com", json.loads(r.text)["email"])
self.assertEqual("usertag", json.loads(r.text)["tag"])

# passwordreset
r = requests.post(f"{adminUser_url}{adminUser_id}/passwordreset", headers=self.headers)
self.assertEqual(201, r.status_code)
self.assertIsNotNone(r)
self.assertTrue("password" in json.loads(r.text).keys())

# deleting the user after the test
r = requests.delete(f"{adminUser_url}{adminUser_id}", headers=self.headers)
self.assertEqual(204, r.status_code)
self.assertIsNotNone(r)

def test_GETAdminsEndpoint(self):
"""
Expand All @@ -140,4 +185,3 @@ def test_GETAdminsEndpoint(self):
self.assertIsNotNone(r)
self.assertEqual(200, r.status_code)
self.assertIsNotNone(json.loads(r.text)["adminuser"])

Loading

0 comments on commit 0e31192

Please sign in to comment.