diff --git a/apps/backend/src/routes/auth/route.ts b/apps/backend/src/routes/auth/route.ts index fff1d06..c0ac00e 100644 --- a/apps/backend/src/routes/auth/route.ts +++ b/apps/backend/src/routes/auth/route.ts @@ -30,26 +30,19 @@ const authRoutes = new Hono() async (c) => { const formData = c.req.valid("form"); - const user = ( - await db - .select({ - id: users.id, - username: users.username, - email: users.email, - password: users.password, - }) - .from(users) - .where( - and( - isNull(users.deletedAt), - eq(users.isEnabled, true), - or( - eq(users.username, formData.username), - eq(users.email, formData.username) - ) + const [user] = await db + .select() + .from(users) + .where( + and( + isNull(users.deletedAt), + eq(users.isEnabled, true), + or( + eq(users.username, formData.username), + eq(users.email, formData.username) ) ) - )[0]; + ); if (!user) { throw new HTTPException(400, { @@ -99,6 +92,11 @@ const authRoutes = new Hono() return c.json({ accessToken, refreshToken, + user: { + id: user.id, + name: user.name, + permissions: [] as string[], + }, }); } ) diff --git a/apps/frontend/src/.env.example b/apps/frontend/.env.example similarity index 100% rename from apps/frontend/src/.env.example rename to apps/frontend/.env.example diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 4eda94d..f5dc7ae 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { routeTree } from "./routeTree.gen"; import "@mantine/core/styles.css"; import "@mantine/notifications/styles.css"; +import { AuthProvider } from "./contexts/AuthContext"; const queryClient = new QueryClient(); @@ -28,7 +29,9 @@ function App() { - + + + ); diff --git a/apps/frontend/src/components/AppHeader.tsx b/apps/frontend/src/components/AppHeader.tsx index a5a62b6..836b93d 100644 --- a/apps/frontend/src/components/AppHeader.tsx +++ b/apps/frontend/src/components/AppHeader.tsx @@ -13,6 +13,8 @@ import logo from "@/assets/logos/logo.png"; import cx from "clsx"; import classNames from "./styles/appHeader.module.css"; import { TbChevronDown } from "react-icons/tb"; +import { Link } from "@tanstack/react-router"; +import useAuth from "@/hooks/useAuth"; // import getUserMenus from "../actions/getUserMenus"; // import { useAuth } from "@/modules/auth/contexts/AuthContext"; // import UserMenuItem from "./UserMenuItem"; @@ -31,7 +33,7 @@ interface Props { export default function AppHeader(props: Props) { const [userMenuOpened, setUserMenuOpened] = useState(false); - // const { user } = useAuth(); + const { user } = useAuth(); // const userMenus = getUserMenus().map((item, i) => ( // @@ -66,11 +68,11 @@ export default function AppHeader(props: Props) { // src={user?.photoProfile} // alt={user?.name} radius="xl" - size={20} + size={30} /> {/* {user?.name} */} - Username + {user?.name ?? "Anonymous"} - Settings + + Logout + {/* {userMenus} */} diff --git a/apps/frontend/src/contexts/AuthContext.tsx b/apps/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..6931685 --- /dev/null +++ b/apps/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,67 @@ +import { ReactNode } from "@tanstack/react-router"; +import { createContext, useState } from "react"; + +interface AuthContextType { + user: { + id: string; + name: string; + permissions: string[]; + } | null; + accessToken: string | null; + saveAuthData: ( + userData: NonNullable, + accessToken: NonNullable + ) => void; + clearAuthData: () => void; + isAuthenticated: boolean; +} + +export const AuthContext = createContext( + undefined +); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [userId, setUserId] = useState(null); + const [userName, setUserName] = useState(null); + const [permissions, setPermissions] = useState(null); + const [accessToken, setAccessToken] = useState( + localStorage.getItem("accessToken") + ); + + const saveAuthData = ( + userData: NonNullable, + accessToken: NonNullable + ) => { + setUserId(userData.id); + setUserName(userData.name); + setPermissions(userData.permissions); + setAccessToken(accessToken); + localStorage.setItem("accessToken", accessToken); + }; + + const clearAuthData = () => { + setUserId(null); + setUserName(null); + setPermissions(null); + setAccessToken(null); + localStorage.removeItem("accessToken"); + }; + + const isAuthenticated = Boolean(accessToken); + + return ( + + {children} + + ); +} diff --git a/apps/frontend/src/honoClient.ts b/apps/frontend/src/honoClient.ts index f4a8186..5091e90 100644 --- a/apps/frontend/src/honoClient.ts +++ b/apps/frontend/src/honoClient.ts @@ -3,12 +3,14 @@ import { AppType } from "backend"; const backendUrl = import.meta.env.VITE_BACKEND_BASE_URL as string | undefined; +console.log(backendUrl); + if (!backendUrl) throw new Error("Backend URL not set"); const client = hc(backendUrl, { - headers: { + headers: () => ({ Authorization: `Bearer ${localStorage.getItem("accessToken")}`, - }, + }), }); export default client; diff --git a/apps/frontend/src/hooks/useAuth.ts b/apps/frontend/src/hooks/useAuth.ts new file mode 100644 index 0000000..ac07f3d --- /dev/null +++ b/apps/frontend/src/hooks/useAuth.ts @@ -0,0 +1,14 @@ +import { AuthContext } from "@/contexts/AuthContext"; +import { useContext } from "react"; + +const useAuth = () => { + const context = useContext(AuthContext); + + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + + return context; +}; + +export default useAuth; diff --git a/apps/frontend/src/routes/_dashboardLayout.tsx b/apps/frontend/src/routes/_dashboardLayout.tsx index ad398d8..613d834 100644 --- a/apps/frontend/src/routes/_dashboardLayout.tsx +++ b/apps/frontend/src/routes/_dashboardLayout.tsx @@ -1,10 +1,9 @@ import { AppShell } from "@mantine/core"; -import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router"; +import { Navigate, Outlet, createFileRoute } from "@tanstack/react-router"; import { useDisclosure } from "@mantine/hooks"; import AppHeader from "../components/AppHeader"; import AppNavbar from "../components/AppNavbar"; -import isAuthenticated from "@/utils/isAuthenticated"; -import { useEffect } from "react"; +import useAuth from "@/hooks/useAuth"; export const Route = createFileRoute("/_dashboardLayout")({ component: DashboardLayout, @@ -19,17 +18,11 @@ export const Route = createFileRoute("/_dashboardLayout")({ }); function DashboardLayout() { + const { isAuthenticated } = useAuth(); + const [openNavbar, { toggle }] = useDisclosure(false); - const navigate = useNavigate(); - - useEffect(() => { - if (!isAuthenticated()) { - navigate({ to: "/login", replace: true }); - } - }, [navigate]); - - return ( + return isAuthenticated ? ( + ) : ( + ); } diff --git a/apps/frontend/src/routes/login/index.lazy.tsx b/apps/frontend/src/routes/login/index.lazy.tsx index d4a374e..ffa3130 100644 --- a/apps/frontend/src/routes/login/index.lazy.tsx +++ b/apps/frontend/src/routes/login/index.lazy.tsx @@ -15,7 +15,7 @@ import { useForm } from "@mantine/form"; import { z } from "zod"; import { zodResolver } from "mantine-form-zod-resolver"; import { useEffect, useState } from "react"; -import isAuthenticated from "@/utils/isAuthenticated"; +import useAuth from "@/hooks/useAuth"; export const Route = createFileRoute("/login/")({ component: LoginPage, @@ -35,6 +35,8 @@ export default function LoginPage() { const [errorMessage, setErrorMessage] = useState(""); const navigate = useNavigate(); + const { isAuthenticated, saveAuthData } = useAuth(); + const form = useForm({ initialValues: { username: "", @@ -44,13 +46,13 @@ export default function LoginPage() { }); useEffect(() => { - if (isAuthenticated()) { + if (isAuthenticated) { navigate({ to: "/dashboard", replace: true, }); } - }, [navigate]); + }, [navigate, isAuthenticated]); const loginMutation = useMutation({ mutationFn: async (values: FormSchema) => { @@ -66,13 +68,14 @@ export default function LoginPage() { }, onSuccess: (data) => { - console.log(data); - - localStorage.setItem("accessToken", data.accessToken); - - navigate({ - to: "/dashboard", - }); + saveAuthData( + { + id: data.user.id, + name: data.user.name, + permissions: data.user.permissions, + }, + data.accessToken + ); }, onError: async (error) => { diff --git a/apps/frontend/src/routes/logout/index.lazy.tsx b/apps/frontend/src/routes/logout/index.lazy.tsx index ece921c..510d20f 100644 --- a/apps/frontend/src/routes/logout/index.lazy.tsx +++ b/apps/frontend/src/routes/logout/index.lazy.tsx @@ -1,4 +1,4 @@ -import isAuthenticated from "@/utils/isAuthenticated"; +import useAuth from "@/hooks/useAuth"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useEffect } from "react"; @@ -7,18 +7,19 @@ export const Route = createFileRoute("/logout/")({ }); export default function LogoutPage() { + const { isAuthenticated, clearAuthData } = useAuth(); const navigate = useNavigate(); useEffect(() => { - if (isAuthenticated()) { - localStorage.removeItem("accessToken"); + if (isAuthenticated) { + clearAuthData(); } navigate({ to: "/login", replace: true, }); - }, [navigate]); + }, [navigate, isAuthenticated, clearAuthData]); return
Logging out...
; } diff --git a/apps/frontend/src/utils/isAuthenticated.ts b/apps/frontend/src/utils/isAuthenticated.ts deleted file mode 100644 index 6e44146..0000000 --- a/apps/frontend/src/utils/isAuthenticated.ts +++ /dev/null @@ -1,6 +0,0 @@ -function isAuthenticated(): boolean { - const accessToken = localStorage.getItem("accessToken"); - return accessToken !== null; -} - -export default isAuthenticated;