diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 957ac8e..3314a36 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,10 @@ import "bootstrap/dist/css/bootstrap.min.css"; +import LoadingMask from "components/LoadingMask"; import TopNavbar from "components/nav/TopNavbar"; import NotFoundRedirect from "components/NotFoundRedirect"; +import { AuthProvider } from "contexts/AuthContext"; +import { LoadingProvider } from "contexts/LoadingContext"; import { AlertQueue, AlertQueueProvider } from "hooks/alerts"; -import { AuthenticationProvider, OneTimePasswordWrapper } from "hooks/auth"; import { ThemeProvider } from "hooks/theme"; import CollectionPage from "pages/Collection"; import Collections from "pages/Collections"; @@ -18,10 +20,10 @@ const App = () => { return ( - - - - + + + + } /> @@ -42,10 +44,11 @@ const App = () => { - - - - + + + + + ); diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 579037d..ab433e0 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -1,47 +1,29 @@ // src/api/auth.ts import axios from "axios"; -import { - SigninData, - SignupData, - SignupResponse, - SignupResponseBusiness, -} from "types/auth"; +import { Response, SigninData, SignupData } from "types/auth"; const API_URL = process.env.REACT_APP_BACKEND_URL || "https://localhost:8080"; -export const signup = async (data: SignupData): Promise => { +export const signup = async (data: SignupData): Promise => { const response = await axios.post(`${API_URL}/signup`, data); return response.data; }; -export const read_me = async (token: string): Promise => { - const response = await axios.get(`${API_URL}/api/user/me`, { +export const read_me = async (token: string): Promise => { + const response = await axios.get(`${API_URL}/me`, { headers: { Authorization: `Bearer ${token}`, }, }); return response.data; }; -export const read_me_business = async ( - token: string, -): Promise => { - const response = await axios.get(`${API_URL}/api/business/me`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - return response.data; -}; -export const signin = async (data: SigninData): Promise => { - const params = new URLSearchParams(); - params.append("username", data.email); - params.append("password", data.password); - const response = await axios.post(`${API_URL}/api/user/token`, params); +export const signin = async (data: SigninData): Promise => { + const response = await axios.post(`${API_URL}/signin`, data); console.log(response); return response.data; }; export const social_facebook_login = async ( token: string, -): Promise => { +): Promise => { console.log(token); const response = await axios.post( `${API_URL}/api/facebook/callback`, @@ -58,7 +40,7 @@ export const social_facebook_login = async ( }; export const social_Instagram_login = async ( token: string, -): Promise => { +): Promise => { const response = await axios.post( `${API_URL}/api/instagram/callback`, { diff --git a/frontend/src/components/LoadingMask.tsx b/frontend/src/components/LoadingMask.tsx new file mode 100644 index 0000000..41f2830 --- /dev/null +++ b/frontend/src/components/LoadingMask.tsx @@ -0,0 +1,19 @@ +// src/components/LoadingMask.tsx +import { useLoading } from "contexts/LoadingContext"; +import React from "react"; + +const LoadingMask: React.FC = () => { + const { loading } = useLoading(); + + if (!loading) return null; + + return ( +
+
+ Loading... +
+
+ ); +}; + +export default LoadingMask; diff --git a/frontend/src/components/nav/Sidebar.tsx b/frontend/src/components/nav/Sidebar.tsx index 9dc035b..b2d166e 100644 --- a/frontend/src/components/nav/Sidebar.tsx +++ b/frontend/src/components/nav/Sidebar.tsx @@ -1,3 +1,5 @@ +import clsx from "clsx"; +import { useAuth } from "contexts/AuthContext"; import { FaHome, FaLock, @@ -8,8 +10,6 @@ import { } from "react-icons/fa"; import { useNavigate } from "react-router-dom"; -import clsx from "clsx"; - interface SidebarItemProps { title: string; icon?: JSX.Element; @@ -70,7 +70,7 @@ interface SidebarProps { const Sidebar = ({ show, onClose }: SidebarProps) => { const navigate = useNavigate(); - + const { is_auth, signout } = useAuth(); return (
{show ? ( @@ -106,15 +106,19 @@ const Sidebar = ({ show, onClose }: SidebarProps) => { }} size="md" /> - } - onClick={() => { - navigate("/collections"); - onClose(); - }} - size="md" - /> + {is_auth ? ( + } + onClick={() => { + navigate("/collections"); + onClose(); + }} + size="md" + /> + ) : ( + <> + )} } @@ -130,25 +134,29 @@ const Sidebar = ({ show, onClose }: SidebarProps) => {
    - } - onClick={() => { - navigate("/login"); - onClose(); - }} - size="md" - /> - } - onClick={() => { - // Handle logout logic here - navigate("/login"); - onClose(); - }} - size="md" - /> + {is_auth ? ( + } + onClick={() => { + // Handle logout logic here + signout(); + navigate("/login"); + onClose(); + }} + size="md" + /> + ) : ( + } + onClick={() => { + navigate("/login"); + onClose(); + }} + size="md" + /> + )}
diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index b49c06d..b3977d4 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,5 +1,5 @@ // src/context/AuthContext.tsx -import { read_me, read_me_business } from "api/auth"; +import { read_me } from "api/auth"; import React, { createContext, ReactNode, @@ -7,78 +7,48 @@ import React, { useEffect, useState, } from "react"; -import { SignupResponse, SignupResponseBusiness } from "types/auth"; +import { Response } from "types/auth"; interface AuthContextType { - auth: SignupResponse | null; - auth_business: SignupResponseBusiness | null; - auth_type: "user" | "business"; - setAuth: React.Dispatch>; - setAuthBusiness: React.Dispatch< - React.SetStateAction - >; - setAuthType: React.Dispatch>; + is_auth: boolean; + auth: Response | null; + setAuth: React.Dispatch>; signout: () => void; } const AuthContext = createContext(undefined); const AuthProvider = ({ children }: { children: ReactNode }) => { - const [auth, setAuth] = useState(null); - const [auth_business, setAuthBusiness] = - useState(null); - const [auth_type, setAuthType] = useState<"user" | "business">("user"); + const [auth, setAuth] = useState(null); + const [is_auth, setFlag] = useState(false); const signout = () => { localStorage.removeItem("token"); - localStorage.removeItem("type"); setAuth({}); - setAuthBusiness({}); + setFlag(false); }; useEffect(() => { - console.log("1"); const token = localStorage.getItem("token"); - const auth_type = localStorage.getItem("type"); if (token) { - if (auth_type == "user") { - const fetch_data = async (token: string) => { - const response = await read_me(token); - console.log(response); - if (response) setAuth(response); - }; - fetch_data(token); - } else { - const fetch_data = async (token: string) => { - const response = await read_me_business(token); - console.log(response); - if (response) setAuthBusiness(response); - }; - fetch_data(token); - } + const fetch_data = async (token: string) => { + const response = await read_me(token); + console.log(response); + if (response) setAuth(response); + }; + fetch_data(token); } else signout(); }, []); useEffect(() => { - console.log("2"); - if (auth?.access_token) { - localStorage.setItem("token", auth.access_token); - localStorage.setItem("type", "user"); + if (auth?.token) { + localStorage.setItem("token", auth.token); + setFlag(true); } }, [auth]); - useEffect(() => { - console.log("3"); - if (auth_business?.access_token) { - localStorage.setItem("token", auth_business.access_token); - localStorage.setItem("type", "business"); - } - }, [auth_business]); return ( diff --git a/frontend/src/contexts/LoadingContext.tsx b/frontend/src/contexts/LoadingContext.tsx new file mode 100644 index 0000000..3356e82 --- /dev/null +++ b/frontend/src/contexts/LoadingContext.tsx @@ -0,0 +1,37 @@ +// src/context/LoadingContext.tsx +import React, { createContext, ReactNode, useContext, useState } from "react"; + +interface LoadingContextType { + loading: boolean; + startLoading: () => void; + stopLoading: () => void; +} + +const LoadingContext = createContext(undefined); + +export const useLoading = (): LoadingContextType => { + const context = useContext(LoadingContext); + if (!context) { + throw new Error("useLoading must be used within a LoadingProvider"); + } + return context; +}; + +interface LoadingProviderProps { + children: ReactNode; +} + +export const LoadingProvider: React.FC = ({ + children, +}) => { + const [loading, setLoading] = useState(false); + + const startLoading = () => setLoading(true); + const stopLoading = () => setLoading(false); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index a4bf4bf..8ce4141 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,15 +1,24 @@ -import React, { useState } from "react"; +import { signin, signup } from "api/auth"; +import { useAuth } from "contexts/AuthContext"; +import { useLoading } from "contexts/LoadingContext"; +import React, { useEffect, useState } from "react"; import { Google } from "react-bootstrap-icons"; +import { useNavigate } from "react-router-dom"; const LoginPage: React.FC = () => { const [isSignup, setIsSignup] = useState(false); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [username, setName] = useState(""); + const { startLoading, stopLoading } = useLoading(); + const { is_auth, setAuth } = useAuth(); + const navigate = useNavigate(); + useEffect(() => { + if (is_auth) navigate("/collections"); + }, [is_auth]); // Toggle between login and signup forms const handleSwitch = () => { setIsSignup(!isSignup); }; - // Handle form submission const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -17,9 +26,16 @@ const LoginPage: React.FC = () => { // Add your logic for login/signup here if (isSignup) { // You can call your API for sign-up - // const user = await signup({email, password, username}) + startLoading(); + const user = await signup({ email, password, username }); + setAuth(user); + stopLoading(); } else { // You can call your API for login + startLoading(); + const user = await signin({ email, password }); + setAuth(user); + stopLoading(); } }; diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts index eac0a5e..a8e7174 100644 --- a/frontend/src/types/auth.ts +++ b/frontend/src/types/auth.ts @@ -8,24 +8,8 @@ export interface SigninData { email: string; password: string; } -export interface SignupResponse { +export interface Response { username?: string; email?: string; - phone?: string; - bio?: string; - error_message?: string; - access_token?: string | null; - link1?: string; - link2?: string; - link3?: string; - link4?: string; - avatar_url?: string; -} -export interface SignupResponseBusiness { - name?: string; - email?: string; - bio?: string; - error_message?: string; - access_token?: string | null; - avatar_url?: string; + token?: string | null; } diff --git a/linguaphoto/api/user.py b/linguaphoto/api/user.py index 12ae4ae..56725a6 100644 --- a/linguaphoto/api/user.py +++ b/linguaphoto/api/user.py @@ -14,8 +14,8 @@ router = APIRouter() -@router.post("/signup", response_model=UserSigninRespondFragment) -async def signup(user: UserSignupFragment, user_crud: UserCrud = Depends()) -> dict: +@router.post("/signup", response_model=UserSigninRespondFragment | None) +async def signup(user: UserSignupFragment, user_crud: UserCrud = Depends()) -> dict | None: """User registration endpoint. This endpoint allows a new user to sign up by providing the necessary user details. @@ -23,13 +23,15 @@ async def signup(user: UserSignupFragment, user_crud: UserCrud = Depends()) -> d """ async with user_crud: new_user = await user_crud.create_user_from_email(user) - token = create_access_token({"id": user.id}, timedelta(hours=24)) - res_user = UserSigninRespondFragment(id=new_user.id, token=token, username=user.username, email=user.email) + if new_user is None: + return None + token = create_access_token({"id": new_user.id}, timedelta(hours=24)) + res_user = UserSigninRespondFragment(token=token, username=user.username, email=user.email) return res_user.model_dump() -@router.post("/signin", response_model=UserSigninRespondFragment) -async def signin(user: UserSigninFragment, user_crud: UserCrud = Depends()) -> dict: +@router.post("/signin", response_model=UserSigninRespondFragment | None) +async def signin(user: UserSigninFragment, user_crud: UserCrud = Depends()) -> dict | None: """User login endpoint. This endpoint allows an existing user to sign in by verifying their credentials. @@ -46,8 +48,8 @@ async def signin(user: UserSigninFragment, user_crud: UserCrud = Depends()) -> d raise HTTPException(status_code=422, detail="Could not validate credentials") -@router.get("/me", response_model=UserSigninRespondFragment) -async def get_me(token: str = Depends(oauth2_schema), user_crud: UserCrud = Depends()) -> dict: +@router.get("/me", response_model=UserSigninRespondFragment | None) +async def get_me(token: str = Depends(oauth2_schema), user_crud: UserCrud = Depends()) -> dict | None: """Retrieve the currently authenticated user's information. This endpoint uses the provided token to decode and identify the user. diff --git a/linguaphoto/crud/base.py b/linguaphoto/crud/base.py index 83f51fa..0b02030 100644 --- a/linguaphoto/crud/base.py +++ b/linguaphoto/crud/base.py @@ -98,7 +98,7 @@ async def _add_item(self, item: LinguaBaseModel, unique_fields: list[str] | None # Log the item data before insertion for debugging purposes logger.info("Inserting item into DynamoDB: %s", item_data) - + print(condition) try: await table.put_item( Item=item_data, diff --git a/linguaphoto/crud/user.py b/linguaphoto/crud/user.py index b6b3455..d9869b3 100644 --- a/linguaphoto/crud/user.py +++ b/linguaphoto/crud/user.py @@ -8,10 +8,13 @@ class UserCrud(BaseCrud): - async def create_user_from_email(self, user: UserSignupFragment) -> User: - user = User.create(user) - await self._add_item(user, unique_fields=["email"]) - return user + async def create_user_from_email(self, user: UserSignupFragment) -> User | None: + duplicated_user = await self._get_items_from_secondary_index("email", user.email, User) + if duplicated_user: + return None + new_user = User.create(user) + await self._add_item(new_user, unique_fields=["email"]) + return new_user async def get_user(self, id: str, throw_if_missing: bool = False) -> User | None: return await self._get_item(id, User, throw_if_missing=throw_if_missing) diff --git a/linguaphoto/schemas/user.py b/linguaphoto/schemas/user.py index 28d6288..a869398 100644 --- a/linguaphoto/schemas/user.py +++ b/linguaphoto/schemas/user.py @@ -23,7 +23,6 @@ class UserSigninFragment(BaseModel): class UserSigninRespondFragment(BaseModel): - id: str token: str username: str email: EmailStr diff --git a/linguaphoto/settings.py b/linguaphoto/settings.py index 7c0e596..89a4621 100644 --- a/linguaphoto/settings.py +++ b/linguaphoto/settings.py @@ -19,7 +19,7 @@ class Settings: bucket_name = os.getenv("S3_BUCKET_NAME") - dynamodb_table_name = os.getenv("DYNAMODB_TABLE_NAME") + dynamodb_table_name = os.getenv("DYNAMODB_TABLE_NAME", "linguaphoto") media_hosting_server = os.getenv("MEDIA_HOSTING_SERVER") key_pair_id = os.getenv("KEY_PAIR_ID") aws_region_name = os.getenv("AWS_REGION")