Improve auth system
This commit is contained in:
parent
c5024d7b69
commit
3aaec5a323
|
|
@ -30,26 +30,19 @@ const authRoutes = new Hono<HonoEnv>()
|
|||
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<HonoEnv>()
|
|||
return c.json({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
permissions: [] as string[],
|
||||
},
|
||||
});
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<MantineProvider>
|
||||
<Notifications />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
// <UserMenuItem item={item} key={i} />
|
||||
|
|
@ -66,11 +68,11 @@ export default function AppHeader(props: Props) {
|
|||
// src={user?.photoProfile}
|
||||
// alt={user?.name}
|
||||
radius="xl"
|
||||
size={20}
|
||||
size={30}
|
||||
/>
|
||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||
{/* {user?.name} */}
|
||||
Username
|
||||
{user?.name ?? "Anonymous"}
|
||||
</Text>
|
||||
<TbChevronDown
|
||||
style={{ width: rem(12), height: rem(12) }}
|
||||
|
|
@ -81,7 +83,9 @@ export default function AppHeader(props: Props) {
|
|||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Item component={Link} to="/logout">
|
||||
Logout
|
||||
</Menu.Item>
|
||||
|
||||
{/* {userMenus} */}
|
||||
</Menu.Dropdown>
|
||||
|
|
|
|||
67
apps/frontend/src/contexts/AuthContext.tsx
Normal file
67
apps/frontend/src/contexts/AuthContext.tsx
Normal file
|
|
@ -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<AuthContextType["user"]>,
|
||||
accessToken: NonNullable<AuthContextType["accessToken"]>
|
||||
) => void;
|
||||
clearAuthData: () => void;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [permissions, setPermissions] = useState<string[] | null>(null);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(
|
||||
localStorage.getItem("accessToken")
|
||||
);
|
||||
|
||||
const saveAuthData = (
|
||||
userData: NonNullable<AuthContextType["user"]>,
|
||||
accessToken: NonNullable<AuthContextType["accessToken"]>
|
||||
) => {
|
||||
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 (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user: userId
|
||||
? { id: userId, name: userName!, permissions: permissions! }
|
||||
: null,
|
||||
accessToken,
|
||||
saveAuthData,
|
||||
clearAuthData,
|
||||
isAuthenticated,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<AppType>(backendUrl, {
|
||||
headers: {
|
||||
headers: () => ({
|
||||
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export default client;
|
||||
|
|
|
|||
14
apps/frontend/src/hooks/useAuth.ts
Normal file
14
apps/frontend/src/hooks/useAuth.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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 ? (
|
||||
<AppShell
|
||||
padding="md"
|
||||
header={{ height: 70 }}
|
||||
|
|
@ -50,5 +43,7 @@ function DashboardLayout() {
|
|||
<Outlet />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
) : (
|
||||
<Navigate to="/login" />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FormSchema>({
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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 <div>Logging out...</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
function isAuthenticated(): boolean {
|
||||
const accessToken = localStorage.getItem("accessToken");
|
||||
return accessToken !== null;
|
||||
}
|
||||
|
||||
export default isAuthenticated;
|
||||
Loading…
Reference in New Issue
Block a user