diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index d97c756a..2d5e1ccc 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -2,6 +2,7 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useAlertQueue } from "hooks/useAlertQueue"; +import { useAuthentication } from "hooks/useAuth"; import { LoginSchema, LoginType } from "types"; import { Button } from "components/ui/Button/Button"; @@ -18,11 +19,27 @@ const LoginForm = () => { resolver: zodResolver(LoginSchema), }); - const { addAlert } = useAlertQueue(); + const { addAlert, addErrorAlert } = useAlertQueue(); + const auth = useAuthentication(); const onSubmit: SubmitHandler = async (data: LoginType) => { - // TODO: Add an api endpoint to send the credentials details to backend and email verification. - addAlert(`Not yet implemented: ${data.email}`, "success"); + try { + const { data: response, error } = await auth.client.POST("/users/login", { + body: data, + }); + + if (error) { + addErrorAlert(error); + } else { + addAlert(`Login successful! Welcome, back!`, "success"); + + // Successful Login + // TODO: authenticated login state + console.log(JSON.stringify(response)); + } + } catch { + addErrorAlert("An unexpected error occurred during login."); + } }; return ( diff --git a/frontend/src/components/auth/SignupForm.tsx b/frontend/src/components/auth/SignupForm.tsx index f379a413..f5a87d11 100644 --- a/frontend/src/components/auth/SignupForm.tsx +++ b/frontend/src/components/auth/SignupForm.tsx @@ -69,7 +69,8 @@ const SignupForm: React.FC = ({ signupTokenId }) => { return (
+ className="grid grid-cols-1 space-y-6" + > {/* Email Input */}
@@ -106,7 +107,8 @@ const SignupForm: React.FC = ({ signupTokenId }) => { {/* Signup Button */} diff --git a/frontend/src/components/footer/Footer.tsx b/frontend/src/components/footer/Footer.tsx index b82d6550..af9908b7 100644 --- a/frontend/src/components/footer/Footer.tsx +++ b/frontend/src/components/footer/Footer.tsx @@ -34,21 +34,24 @@ const Footer = () => { href="https://www.linkedin.com/company/kscale" ariaLabel="Visit K-Scale's LinkedIn Page" bgColor={LinkedinPrimaryColor} - ringColor="focus:ring-sky-500"> + ringColor="focus:ring-sky-500" + > + ringColor="focus:ring-black" + > + ringColor="focus:ring-black" + >
@@ -60,7 +63,8 @@ const Footer = () => { href="https://kscale.dev/about/" className="hover:text-gray-500" target="_blank" - rel="noopener noreferrer"> + rel="noopener noreferrer" + > About us diff --git a/frontend/src/components/ui/Input/PasswordInput.tsx b/frontend/src/components/ui/Input/PasswordInput.tsx index b47bb35b..c99327c3 100644 --- a/frontend/src/components/ui/Input/PasswordInput.tsx +++ b/frontend/src/components/ui/Input/PasswordInput.tsx @@ -110,7 +110,8 @@ const PasswordInput = ({
Password Strength:{" "} + className={`font-semibold text-${getStrengthColor(passwordStrength)}`} + > {getStrengthLabel(passwordStrength)}
diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts index ef5cd60b..95ee0d79 100644 --- a/frontend/src/gen/api.ts +++ b/frontend/src/gen/api.ts @@ -73,6 +73,23 @@ export interface paths { patch?: never; trace?: never; }; + "/users/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Login User */ + post: operations["login_user_users_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/users/batch": { parameters: { query?: never; @@ -574,6 +591,23 @@ export interface components { /** Image Url */ image_url: string | null; }; + /** LoginRequest */ + LoginRequest: { + /** + * Email + * Format: email + */ + email: string; + /** Password */ + password: string; + }; + /** LoginResponse */ + LoginResponse: { + /** User Id */ + user_id: string; + /** Token */ + token: string; + }; /** NewListingRequest */ NewListingRequest: { /** Name */ @@ -769,6 +803,39 @@ export interface operations { }; }; }; + login_user_users_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LoginResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_users_batch_endpoint_users_batch_get: { parameters: { query: { diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index fa7e1dbd..b37b00b6 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -67,7 +67,8 @@ const Register = () => { className="w-full text-white bg-blue-600 hover:bg-opacity-70" onClick={() => { navigate("/login"); - }}> + }} + > Login / Signup diff --git a/store/app/routers/users.py b/store/app/routers/users.py index 72904957..13e786c4 100644 --- a/store/app/routers/users.py +++ b/store/app/routers/users.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from fastapi.security.utils import get_authorization_scheme_param +from pydantic import EmailStr from pydantic.main import BaseModel as PydanticBaseModel from store.app.crud.base import ItemNotFoundError @@ -16,6 +17,8 @@ from store.app.model import User, UserPermission from store.app.routers.auth.github import github_auth_router from store.app.utils.email import send_delete_email +from store.app.utils.password import verify_password +from store.utils import new_uuid logger = logging.getLogger(__name__) @@ -133,7 +136,9 @@ class SendRegister(BaseModel): class UserRegister(BaseModel): - token: str + signup_token_id: str + email: str + password: str class UserInfoResponse(BaseModel): @@ -182,16 +187,10 @@ class PublicUserInfoResponse(BaseModel): users: list[SinglePublicUserInfoResponseItem] -class UserRegister(BaseModel): - signup_token_id: str - email: str - password: str - - @users_router.post("/register", response_model=SinglePublicUserInfoResponseItem) async def register_user( data: UserRegister, email_signup_crud: EmailSignUpCrud = Depends(), user_crud: UserCrud = Depends() -): +) -> SinglePublicUserInfoResponseItem: # Added return type annotation async with email_signup_crud, user_crud: signup_token = await email_signup_crud.get_email_signup_token(data.signup_token_id) if not signup_token: @@ -208,6 +207,35 @@ async def register_user( return SinglePublicUserInfoResponseItem(id=user.id, email=user.email) +class LoginRequest(BaseModel): + email: EmailStr + password: str + + +class LoginResponse(BaseModel): + user_id: str + token: str + + +@users_router.post("/login", response_model=LoginResponse) +async def login_user( + data: LoginRequest, user_crud: UserCrud = Depends() +) -> LoginResponse: # Added return type annotation + async with user_crud: + # Fetch user by email + user = await user_crud.get_user_from_email(data.email) + if not user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password") + + # Verify password + if not verify_password(data.password, user.hashed_password): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password") + + token = new_uuid() + + return LoginResponse(user_id=user.id, token=token) + + @users_router.get("/batch", response_model=PublicUserInfoResponse) async def get_users_batch_endpoint( crud: Annotated[Crud, Depends(Crud.get)],