Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions api/src/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
create_access_token,
create_refresh_token,
get_hashed_password,
validate_password,
verify_jwt,
verify_password,
)

router = APIRouter()


@router.post("/login", summary="Login to the app")
async def login(auth: Auth) -> dict:
"""Login to the app with username and password."""
Expand All @@ -35,6 +37,7 @@ async def login(auth: Auth) -> dict:
refresh_token = create_refresh_token(user.username)
return {"access_token": access_token, "refresh_token": refresh_token}


@router.post("/refresh", summary="Refresh the access token")
async def refresh(refresh_token: RefreshToken) -> dict:
"""Refresh the access token."""
Expand All @@ -47,11 +50,23 @@ async def refresh(refresh_token: RefreshToken) -> dict:
access_token = create_access_token(token["sub"])
return {"access_token": access_token}


@router.post("/register", summary="Register to the app")
async def register(auth: Auth) -> dict:
"""Register to the app with username and password."""
if auth.password is None or not validate_password(auth.password):
raise HTTPException(status_code=400, detail="Invalid password")

user = User(auth.username, get_hashed_password(auth.password), "user")

stmtverify = user_table.select().where(user_table.c.username == user.username)

with db.engine.begin() as conn:
result = conn.execute(stmtverify).fetchall()

if len(result) > 0:
raise HTTPException(status_code=400, detail="User already exists")

stmt = user_table.insert().values(
username=user.username,
password=user.password,
Expand Down
108 changes: 80 additions & 28 deletions api/src/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
from dependencies.jwt import jwt_bearer
from models import db
from models.user import User, UserPasswordUpdate, UserUpdate, user_table
from utils.auth import get_hashed_password, verify_password
from utils.auth import get_hashed_password, verify_password, validate_password

router = APIRouter()

@router.get("/", response_model=list[User],
summary="Get all users", dependencies=[Depends(jwt_bearer)])

@router.get(
"/",
response_model=list[User],
summary="Get all users",
dependencies=[Depends(jwt_bearer)],
)
async def get_all_users(token: dict = Depends(jwt_bearer)) -> dict: # noqa: B008, FAST002
"""Get all users."""
# Check if user is admin
Expand All @@ -29,9 +34,14 @@ async def get_all_users(token: dict = Depends(jwt_bearer)) -> dict: # noqa: B00

return [User.from_db(row[0], row[1], row[2]) for row in result if len(row) > 0]

@router.get("/me", response_model=User, summary="Get current user",
dependencies=[Depends(jwt_bearer)])
async def get_current_user(token: dict = Depends(jwt_bearer)) -> dict: # noqa: B008, FAST002

@router.get(
"/me",
response_model=User,
summary="Get current user",
dependencies=[Depends(jwt_bearer)],
)
async def get_current_user(token: dict = Depends(jwt_bearer)) -> dict: # noqa: B008, FAST002
"""Get current user."""
stmt = user_table.select().where(user_table.c.username == token["sub"])
with db.engine.begin() as conn:
Expand All @@ -42,9 +52,14 @@ async def get_current_user(token: dict = Depends(jwt_bearer)) -> dict: # noqa: B

return User.from_db(result[0], result[1], result[2])

@router.get("/{username}", response_model=User,
summary="Get user by username", dependencies=[Depends(jwt_bearer)])
async def get_user_by_name(username: str, token: dict = Depends(jwt_bearer)) -> dict:\

@router.get(
"/{username}",
response_model=User,
summary="Get user by username",
dependencies=[Depends(jwt_bearer)],
)
async def get_user_by_name(username: str, token: dict = Depends(jwt_bearer)) -> dict:
# noqa: B008, FAST002
"""Get user by username."""
# Check if user is admin
Expand All @@ -64,10 +79,16 @@ async def get_user_by_name(username: str, token: dict = Depends(jwt_bearer)) ->

return User.from_db(result[0], result[1], result[2])

@router.patch("/me", response_model=User, summary="Update current user",
dependencies=[Depends(jwt_bearer)])
async def update_current_user(user_udpate: UserUpdate,\
token: dict = Depends(jwt_bearer)) -> dict: # noqa: B008, FAST002

@router.patch(
"/me",
response_model=User,
summary="Update current user",
dependencies=[Depends(jwt_bearer)],
)
async def update_current_user(
user_udpate: UserUpdate, token: dict = Depends(jwt_bearer)
) -> dict: # noqa: B008, FAST002
"""Update current user."""
stmt = user_table.select().where(user_table.c.username == token["sub"])
with db.engine.begin() as conn:
Expand All @@ -87,18 +108,30 @@ async def update_current_user(user_udpate: UserUpdate,\

user.username = user_udpate.username

stmt = user_table.update().where(user_table.c.username == token["sub"])\
stmt = (
user_table.update()
.where(user_table.c.username == token["sub"])
.values(user.to_dict())
)
with db.engine.begin() as conn:
conn.execute(stmt)

return user

@router.patch("/me/password", summary="Update current user password",
dependencies=[Depends(jwt_bearer)])
async def update_current_user_password(user_password_update: UserPasswordUpdate,\
token: dict = Depends(jwt_bearer)) -> dict: # noqa: B008, FAST002

@router.patch(
"/me/password",
summary="Update current user password",
dependencies=[Depends(jwt_bearer)],
)
async def update_current_user_password(
user_password_update: UserPasswordUpdate, token: dict = Depends(jwt_bearer)
) -> dict: # noqa: B008, FAST002
"""Update current user password."""
if user_password_update.password is None or not validate_password(
user_password_update.password
):
raise HTTPException(status_code=400, detail="Invalid password")
stmt = user_table.select().where(user_table.c.username == token["sub"])
with db.engine.begin() as conn:
result = conn.execute(stmt).fetchone()
Expand All @@ -115,17 +148,26 @@ async def update_current_user_password(user_password_update: UserPasswordUpdate,

user.password = get_hashed_password(user_password_update.password)

stmt = user_table.update().where(user_table.c.username == token["sub"])\
stmt = (
user_table.update()
.where(user_table.c.username == token["sub"])
.values(user.to_dict())
)
with db.engine.begin() as conn:
conn.execute(stmt)

return {"message": "Password updated"}

@router.patch("/{username}", response_model=User, summary="Update an user by username",
dependencies=[Depends(jwt_bearer)])
async def update_user(username: str, user_udpate: UserUpdate,\
token: dict = Depends(jwt_bearer)) -> dict: # noqa: B008, FAST002

@router.patch(
"/{username}",
response_model=User,
summary="Update an user by username",
dependencies=[Depends(jwt_bearer)],
)
async def update_user(
username: str, user_udpate: UserUpdate, token: dict = Depends(jwt_bearer)
) -> dict: # noqa: B008, FAST002
"""Update an user by username."""
stmt = user_table.select().where(user_table.c.username == token["sub"])
with db.engine.begin() as conn:
Expand All @@ -149,20 +191,30 @@ async def update_user(username: str, user_udpate: UserUpdate,\

user.username = user_udpate.username

if user_udpate.password is not None and not validate_password(user_udpate.password):
raise HTTPException(status_code=400, detail="Invalid password")

if not verify_password(user_udpate.password, user.password):
user.password = get_hashed_password(user_udpate.password)

stmt = user_table.update().where(user_table.c.username == username)\
stmt = (
user_table.update()
.where(user_table.c.username == username)
.values(user.to_dict())
)
with db.engine.begin() as conn:
conn.execute(stmt)

return user

@router.patch("/{username}", summary="Delete an user by username",
dependencies=[Depends(jwt_bearer)])
async def delete_user(username: str, token: dict = Depends(jwt_bearer)) -> dict:\
# noqa: B008, FAST002

@router.patch(
"/{username}",
summary="Delete an user by username",
dependencies=[Depends(jwt_bearer)],
)
async def delete_user(username: str, token: dict = Depends(jwt_bearer)) -> dict:
# noqa: B008, FAST002
"""Delete an user by username."""
stmt = user_table.select().where(user_table.c.username == token["sub"])
with db.engine.begin() as conn:
Expand Down
40 changes: 35 additions & 5 deletions api/src/utils/auth.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
"""Module for authentication utilities."""

import os
import re
from datetime import UTC, datetime, timedelta

from fastapi import HTTPException
from jose import JWTError, jwt
from passlib.context import CryptContext

ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 1 day
REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
ALGORITHM = "HS256"
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
JWT_REFRESH_SECRET_KEY = os.getenv("JWT_REFRESH_KEY")

password_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def get_hashed_password(password: str) -> str:
"""Return the hashed password.

Expand Down Expand Up @@ -55,12 +58,14 @@ def create_access_token(subject: str, expires_delta: int | None = None) -> str:
if expires_delta is not None:
expires_delta = datetime.now(tz=UTC) + expires_delta
else:
expires_delta = datetime.now(tz=UTC) + \
timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
expires_delta = datetime.now(tz=UTC) + timedelta(
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
)

to_encode = {"exp": expires_delta, "sub": str(subject)}
return jwt.encode(to_encode, JWT_SECRET_KEY, ALGORITHM)


def create_refresh_token(subject: str, expires_delta: int | None = None) -> str:
"""Return the refresh token.

Expand All @@ -75,12 +80,14 @@ def create_refresh_token(subject: str, expires_delta: int | None = None) -> str:
if expires_delta is not None:
expires_delta = datetime.now(tz=UTC) + expires_delta
else:
expires_delta = datetime.now(tz=UTC) + \
timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)
expires_delta = datetime.now(tz=UTC) + timedelta(
minutes=REFRESH_TOKEN_EXPIRE_MINUTES
)

to_encode = {"exp": expires_delta, "sub": str(subject)}
return jwt.encode(to_encode, JWT_REFRESH_SECRET_KEY, ALGORITHM)


def verify_jwt(token: str) -> dict:
"""Verify JWT token.

Expand All @@ -98,3 +105,26 @@ def verify_jwt(token: str) -> dict:
return jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM])
except JWTError as err:
raise HTTPException(status_code=403, detail="Invalid token") from err


def validate_password(password: str) -> bool:
"""Return True if the password is valid, False otherwise.

Args:
password (str): Password to validate

Returns:
bool: True if the password is valid, False otherwise

"""
if password is None:
return False
if len(password) < 8:
return False
if re.match(r"[A-Z]", password) is None:
return False
if re.match(r"[a-z]", password) is None:
return False
if re.match(r"[0-9]", password) is None:
return False
return re.match("[^A-Za-z0-9]", password) is not None
19 changes: 17 additions & 2 deletions front-js/src/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import Layout from "@/components/Layout";
import Modal from "@/components/Modal";
import Title from "@/components/Title";
import Text from "@/components/Text";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "@/components/Link";
import axios from "@/axiosConfig";
import Cookies from "js-cookie";
import { AxiosError } from "axios";
import { Alert, CircularProgress } from "@mui/material";
import Backlink from "@/components/Backlink";
import { useRouter } from "next/navigation";

export default function Home() {
const router = useRouter();

const [username, setUsername] = useState("");

const [password, setPassword] = useState("");
Expand All @@ -34,7 +38,7 @@ export default function Home() {
if (response.status === 200) {
Cookies.set("access_token", data.access_token);
Cookies.set("refresh_token", data.refresh_token);
window.location.href = "/";
router.replace("/dashboard");
}
} catch (error: unknown) {
const axiosError = error as AxiosError;
Expand All @@ -51,8 +55,19 @@ export default function Home() {
}
};

useEffect(() => {
const checkAuth = async () => {
const token = Cookies.get("access_token");
if (token) {
router.replace("/dashboard");
}
};
checkAuth();
}, [router]);

return (
<Layout type="home">
<Backlink onClick={router.back} />
<Box align="center" margin={{ top: "100px", bottom: "50px" }}>
<Title level={1}>Networkers</Title>
</Box>
Expand Down
14 changes: 14 additions & 0 deletions front-js/src/app/auth/signup/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Inscription",
description: "Networkers",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <body>{children}</body>;
}
Loading