diff --git a/api/src/routes/auth.py b/api/src/routes/auth.py index 208ebaa..e85686b 100644 --- a/api/src/routes/auth.py +++ b/api/src/routes/auth.py @@ -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.""" @@ -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.""" @@ -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, diff --git a/api/src/routes/users.py b/api/src/routes/users.py index 3e297bf..5ed8096 100644 --- a/api/src/routes/users.py +++ b/api/src/routes/users.py @@ -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 @@ -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: @@ -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 @@ -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: @@ -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() @@ -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: @@ -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: diff --git a/api/src/utils/auth.py b/api/src/utils/auth.py index c637737..dab41c2 100644 --- a/api/src/utils/auth.py +++ b/api/src/utils/auth.py @@ -1,5 +1,7 @@ """Module for authentication utilities.""" + import os +import re from datetime import UTC, datetime, timedelta from fastapi import HTTPException @@ -7,13 +9,14 @@ 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. @@ -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. @@ -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. @@ -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 diff --git a/front-js/src/app/auth/login/page.tsx b/front-js/src/app/auth/login/page.tsx index 9873d29..23bc0a6 100644 --- a/front-js/src/app/auth/login/page.tsx +++ b/front-js/src/app/auth/login/page.tsx @@ -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(""); @@ -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; @@ -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 ( + Networkers diff --git a/front-js/src/app/auth/signup/layout.tsx b/front-js/src/app/auth/signup/layout.tsx new file mode 100644 index 0000000..82ec922 --- /dev/null +++ b/front-js/src/app/auth/signup/layout.tsx @@ -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 {children}; +} diff --git a/front-js/src/app/auth/signup/page.tsx b/front-js/src/app/auth/signup/page.tsx index 39d9d64..9836f29 100644 --- a/front-js/src/app/auth/signup/page.tsx +++ b/front-js/src/app/auth/signup/page.tsx @@ -7,75 +7,137 @@ import Layout from "@/components/Layout"; import Modal from "@/components/Modal"; import Title from "@/components/Title"; import Text from "@/components/Text"; -import { use, useState } from "react"; +import { useState } from "react"; import Link from "@/components/Link"; -import Space from "@/components/Space"; import ValidatePsw from "@/components/ValidatePsw"; -import {validate_passwd} from "@/utils/validatePasswd"; +import { validate_passwd } from "@/utils/validatePasswd"; +import Backlink from "@/components/Backlink"; +import { useRouter } from "next/navigation"; +import axios from "@/axiosConfig"; +import { AxiosError } from "axios"; +import { Alert, CircularProgress } from "@mui/material"; +import { useEffect } from "react"; +import Cookies from "js-cookie"; export default function Home() { - const [username, setUsername] = useState(""); + const router = useRouter(); + const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); + const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState(""); - const isValid = false; + const [loading, setLoading] = useState(false); - return ( - - - Networkers - - - - Inscription - setUsername(e.target.value)} - required - label="Nom d'utilisateur" - /> - setPassword(e.target.value)} - required - label="Mot de passe" - /> - setConfirmPassword(e.target.value)} - required - label="Confimer mot de passe" - /> - + const [error, setError] = useState(""); + + const handleSignup = async (e: { preventDefault: () => void }) => { + e.preventDefault(); + try { + const response = await axios.post("/auth/register", { + username: username, + password: password, + }); + if (response.status === 200) { + router.push("/auth/login"); + } + } catch (error: unknown) { + const axiosError = error as AxiosError; + if (axiosError.response?.status === 400) { + setError("Nom d'utilisateur déjà utilisé"); + } else { + setError("Erreur lors de l'inscription"); + } + } + }; + + useEffect(() => { + const checkAuth = async () => { + const token = Cookies.get("access_token"); + if (token) { + router.replace("/dashboard"); + } + }; + checkAuth(); + }, [router]); + + return ( + + + + Networkers + + + + Inscription +
+ setUsername(e.target.value)} + required + label="Nom d'utilisateur" + /> + setPassword(e.target.value)} + required + label="Mot de passe" + /> + setConfirmPassword(e.target.value)} + required + label="Confirmer mot de passe" + /> + + {error !== "" ? ( + + {error} + + ) : null} + {loading ? ( + + ) : (