Skip to content

Commit

Permalink
Merge branch 'main' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
EliotAmn committed Feb 4, 2025
2 parents 5a57a83 + a92d2e9 commit 88eb5f0
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 34 deletions.
61 changes: 57 additions & 4 deletions app/api/routes/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

from flask import request

from app.api.middlewares.student_auth_middleware import student_auth_middleware
from app.models.Student import Student
from app.services.redis_service import RedisService
from app.services.student_service import StudentService
from app.tools.jwt_engine import generate_jwt
from app.tools.password_tools import check_password
from app.tools.password_tools import check_password, hash_password

PASSWORD_REGEX = "^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$"


def load_auth_routes(app):
Expand Down Expand Up @@ -38,6 +42,19 @@ def validate_email_login():
if student is None:
return {"error": "Invalid email or password"}, 400

# Check if it's password reset
redis_key = f"reset_password:{student.id}"
reset_password = RedisService.get(redis_key)
if reset_password is not None:
if password == reset_password:
# Reset password
new_pass_hash = hash_password(password)
student.password_hash = new_pass_hash
StudentService.update_student(student)
RedisService.delete(redis_key)
token = StudentService.generate_jwt_token(student)
return {"token": token}, 200

if not check_password(password, student.password_hash):
return {"error": "Invalid email or password"}, 400

Expand All @@ -52,9 +69,7 @@ def register_student():
if ticket is None:
return {"error": "Missing ticket"}, 400
password = data.get("password", None)
# 8 characters, 1 uppercase, 1 lowercase, 1 digit
regex = "^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$"
if password is None or not re.match(regex, password):
if password is None or not re.match(PASSWORD_REGEX, password):
return {"error": "Invalid password"}, 400

student = StudentService.create_student_by_ticket(ticket, password)
Expand All @@ -76,3 +91,41 @@ def check_ticket():
return {"error": "Invalid ticket"}, 400

return {"login": user}, 200

@app.route("/api/auth/reset", methods=["POST"])
def reset_password():
data = request.json
email = data.get("email", None)
if email is None:
return {"error": "Missing email"}, 400

student = StudentService.get_student_by_login(email)
if student is None:
return {"error": "Invalid email"}, 400

StudentService.send_reset_password_email(email)
return {"success": True}, 200

@app.route("/api/auth/change", methods=["POST"])
@student_auth_middleware()
def change_password():
data = request.json
student = request.student

old_password = data.get("oldPassword", None)
new_password = data.get("newPassword", None)

if old_password is None or new_password is None:
return {"error": "Missing old or new password"}, 400

if not check_password(old_password, student.password_hash):
return {"error": "Invalid old password"}, 400

if not re.match(PASSWORD_REGEX, new_password):
return {"error": "Invalid new password"}, 400

new_pass_hash = hash_password(new_password)
student.password_hash = new_pass_hash
StudentService.update_student(student)

return {"success": True}, 200
2 changes: 1 addition & 1 deletion app/api/routes/global_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def get_student(student_id: str):
return {
"login": stud.login,
"id": stud.id,
"name": f"{stud.first_name} {stud.last_name}",
"name": f"{stud.first_name} {stud.last_name}" if stud.first_name and stud.last_name else None,
}

@app.route("/api/global/picture/<string:student_login>", methods=["GET"])
Expand Down
35 changes: 34 additions & 1 deletion app/services/student_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def get_student_by_id(student_id: str) -> Student or None:

@staticmethod
def filter_share_consent(student_ids: [str]) -> [str]:
students = Globals.database["students"].find({"_id": {"$in": [str(sid) for sid in student_ids]}, "is_consent_share": True})
students = Globals.database["students"].find(
{"_id": {"$in": [str(sid) for sid in student_ids]}, "is_consent_share": True})

return [student["_id"] for student in students if student]

Expand Down Expand Up @@ -78,6 +79,38 @@ def regenerate_scraper_token(student: Student):
StudentService.update_student(student)
return student.scraper_token

@staticmethod
def send_reset_password_email(email: str):
student = StudentService.get_student_by_login(email)
if not student:
return

redis_key = f"reset_password:{student.id}"
if RedisService.get(redis_key):
# Already sent a reset password email, wait 5 minutes
return
password = ''.join(random.choices(string.ascii_letters + string.digits + string.punctuation, k=15))
RedisService.set(redis_key, password, 60 * 5) # Expires in 5 minutes

if os.getenv("ENABLE_MAILER") == "true":
subject = "Reset Your TekBetter Password"
body = f"""\
Hello you,
It seems you have requested a password reset for your TekBetter account. To reset your password please enter this password for the next 5 minutes:
{password}
After you have logged in, this password will erase your last password.
If you did not request this password reset, you can safely ignore this email.
Best regards,
The TekBetter Team
"""
MailService.send_mail(email, subject, body)
else:
log_warning(
f"Mailer is disabled, not sending email. This is the new password for {email}: {password}")

@staticmethod
def create_register_ticket(email: str):
ticket = ''.join(
Expand Down
15 changes: 15 additions & 0 deletions web/src/api/auth.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ export async function loginWithPassword(email: string, password: string): Promis
return res.status === 200;
}

export async function resetPassword(email: string): Promise<boolean> {
const res = await api.post(`/auth/reset`, {
email: email
});
return res.status === 200;
}

export async function changePasswordRequest(oldPassword: string, newPassword: string): Promise<boolean> {
const res = await api.post(`/auth/change`, {
oldPassword: oldPassword,
newPassword: newPassword
});
return res.status === 200;
}

export async function registerWithTicket(ticket: string, password: string): Promise<boolean> {
const res = await api.post(`/auth/register`, {
ticket: ticket,
Expand Down
3 changes: 3 additions & 0 deletions web/src/api/global.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export async function getStudentData(id: string): Promise<StudentData> {
}

const res = await api.get(`/student/${id}`);
if (res.data === null) {
throw new Error("Student not found");
}
const student = new StudentData(res.data);
vars.studentsCache.push(student);
return student;
Expand Down
13 changes: 13 additions & 0 deletions web/src/comps/NoSyncComp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

export default function NoSyncComp() : React.ReactElement {
return (
<div className={"shadow flex-grow text-center"}>
<h1 className={"font-bold text-red-400 text-2xl"}>Your account was never synced</h1>
<p>
Please go to the Sync page to connect your epitech microsoft account with TekBetter
</p>

</div>
)
}
25 changes: 11 additions & 14 deletions web/src/comps/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,17 @@ function UserComp() {

if (user === null) return null;

const user_name = user.name === null ? "New user" : user.name;

return <div className={"flex flex-row h-full items-center px-3 rounded-2xl"}>
<img
src={`${vars.backend_url}/api/global/picture/${user.login}`}
alt={"Epitech"}
className={"w-10 h-10 ml-1 shadow rounded-full object-cover"}
/>
<div className={"flex flex-col ml-2 items-start justify-center w-fit"}>
<p className={"font-bold text-nowrap hidden xl:block"}>{user?.name}</p>
<p className={"font-bold text-nowrap xl:hidden"}>{user?.name.split(" ")[0]}</p>
<p className={"font-bold text-nowrap hidden xl:block"}>{user_name}</p>
<p className={"font-bold text-nowrap xl:hidden"}>{user_name}</p>

<GlobalSyncStatus className={"hidden sm:flex"}/>
</div>
Expand Down Expand Up @@ -168,6 +170,10 @@ export default function TopBar(): React.ReactElement {

]

// If we are on the /auth page, we don't want to display navigation items

const show_nav = !window.location.pathname.startsWith("/auth");

return (
<div>
<div
Expand All @@ -193,21 +199,12 @@ export default function TopBar(): React.ReactElement {
</div>

<div className={"hidden lg:flex flex-row gap-0.5 rounded-2xl justify-start ml-2 shadow-lg"}>
{routes.map((route) => <NavElement text={route.text} link={route.link} icon={route.icon}/>)}
{routes
.filter(route => show_nav)
.map((route) => <NavElement text={route.text} link={route.link} icon={route.icon}/>)}
</div>

<UserComp/>

{/*<div className={"flex flex-row items-center mr-8 shadow-lg p-3 rounded-2xl"}>*/}
{/* <img*/}
{/* src={require("../assets/tblogo.png")}*/}
{/* alt={"Epitech"}*/}
{/* className={"w-9 ml-1 shadow rounded-full"}*/}
{/* />*/}
{/* <p className={"ml-1 mr-2 font-bold"}>Eliot Amanieu</p>*/}
{/* <SyncStatus/>*/}
{/*</div>*/}

</div>

<PhoneBar className={phoneBarOpen ? "" : "hidden"} routes={routes} close={() => setPhoneBarOpen(false)}/>
Expand Down
2 changes: 1 addition & 1 deletion web/src/models/PersonsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function PersonsModal(props: { students_ids: string[], mouseX: number, mouseY: n
alt={stud.name}
className={"rounded-full mr-2 object-cover w-10 h-10"}
/>
<p className={"text-sm text-nowrap"}>{stud.name}</p>
<p className={"text-sm break-words whitespace-normal"}>{stud.name}</p>
</div>
})}
</div>}
Expand Down
Loading

0 comments on commit 88eb5f0

Please sign in to comment.