Improve auth system

This commit is contained in:
sianida26 2024-05-08 01:27:56 +07:00
parent c5024d7b69
commit 3aaec5a323
11 changed files with 138 additions and 57 deletions

View File

@ -30,26 +30,19 @@ const authRoutes = new Hono<HonoEnv>()
async (c) => { async (c) => {
const formData = c.req.valid("form"); const formData = c.req.valid("form");
const user = ( const [user] = await db
await db .select()
.select({ .from(users)
id: users.id, .where(
username: users.username, and(
email: users.email, isNull(users.deletedAt),
password: users.password, eq(users.isEnabled, true),
}) or(
.from(users) eq(users.username, formData.username),
.where( eq(users.email, formData.username)
and(
isNull(users.deletedAt),
eq(users.isEnabled, true),
or(
eq(users.username, formData.username),
eq(users.email, formData.username)
)
) )
) )
)[0]; );
if (!user) { if (!user) {
throw new HTTPException(400, { throw new HTTPException(400, {
@ -99,6 +92,11 @@ const authRoutes = new Hono<HonoEnv>()
return c.json({ return c.json({
accessToken, accessToken,
refreshToken, refreshToken,
user: {
id: user.id,
name: user.name,
permissions: [] as string[],
},
}); });
} }
) )

View File

@ -8,6 +8,7 @@ import { routeTree } from "./routeTree.gen";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css"; import "@mantine/notifications/styles.css";
import { AuthProvider } from "./contexts/AuthContext";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -28,7 +29,9 @@ function App() {
<MantineProvider> <MantineProvider>
<Notifications /> <Notifications />
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RouterProvider router={router} /> <AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</QueryClientProvider> </QueryClientProvider>
</MantineProvider> </MantineProvider>
); );

View File

@ -13,6 +13,8 @@ import logo from "@/assets/logos/logo.png";
import cx from "clsx"; import cx from "clsx";
import classNames from "./styles/appHeader.module.css"; import classNames from "./styles/appHeader.module.css";
import { TbChevronDown } from "react-icons/tb"; import { TbChevronDown } from "react-icons/tb";
import { Link } from "@tanstack/react-router";
import useAuth from "@/hooks/useAuth";
// import getUserMenus from "../actions/getUserMenus"; // import getUserMenus from "../actions/getUserMenus";
// import { useAuth } from "@/modules/auth/contexts/AuthContext"; // import { useAuth } from "@/modules/auth/contexts/AuthContext";
// import UserMenuItem from "./UserMenuItem"; // import UserMenuItem from "./UserMenuItem";
@ -31,7 +33,7 @@ interface Props {
export default function AppHeader(props: Props) { export default function AppHeader(props: Props) {
const [userMenuOpened, setUserMenuOpened] = useState(false); const [userMenuOpened, setUserMenuOpened] = useState(false);
// const { user } = useAuth(); const { user } = useAuth();
// const userMenus = getUserMenus().map((item, i) => ( // const userMenus = getUserMenus().map((item, i) => (
// <UserMenuItem item={item} key={i} /> // <UserMenuItem item={item} key={i} />
@ -66,11 +68,11 @@ export default function AppHeader(props: Props) {
// src={user?.photoProfile} // src={user?.photoProfile}
// alt={user?.name} // alt={user?.name}
radius="xl" radius="xl"
size={20} size={30}
/> />
<Text fw={500} size="sm" lh={1} mr={3}> <Text fw={500} size="sm" lh={1} mr={3}>
{/* {user?.name} */} {/* {user?.name} */}
Username {user?.name ?? "Anonymous"}
</Text> </Text>
<TbChevronDown <TbChevronDown
style={{ width: rem(12), height: rem(12) }} style={{ width: rem(12), height: rem(12) }}
@ -81,7 +83,9 @@ export default function AppHeader(props: Props) {
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Label>Settings</Menu.Label> <Menu.Item component={Link} to="/logout">
Logout
</Menu.Item>
{/* {userMenus} */} {/* {userMenus} */}
</Menu.Dropdown> </Menu.Dropdown>

View 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>
);
}

View File

@ -3,12 +3,14 @@ import { AppType } from "backend";
const backendUrl = import.meta.env.VITE_BACKEND_BASE_URL as string | undefined; const backendUrl = import.meta.env.VITE_BACKEND_BASE_URL as string | undefined;
console.log(backendUrl);
if (!backendUrl) throw new Error("Backend URL not set"); if (!backendUrl) throw new Error("Backend URL not set");
const client = hc<AppType>(backendUrl, { const client = hc<AppType>(backendUrl, {
headers: { headers: () => ({
Authorization: `Bearer ${localStorage.getItem("accessToken")}`, Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
}, }),
}); });
export default client; export default client;

View 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;

View File

@ -1,10 +1,9 @@
import { AppShell } from "@mantine/core"; 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 { useDisclosure } from "@mantine/hooks";
import AppHeader from "../components/AppHeader"; import AppHeader from "../components/AppHeader";
import AppNavbar from "../components/AppNavbar"; import AppNavbar from "../components/AppNavbar";
import isAuthenticated from "@/utils/isAuthenticated"; import useAuth from "@/hooks/useAuth";
import { useEffect } from "react";
export const Route = createFileRoute("/_dashboardLayout")({ export const Route = createFileRoute("/_dashboardLayout")({
component: DashboardLayout, component: DashboardLayout,
@ -19,17 +18,11 @@ export const Route = createFileRoute("/_dashboardLayout")({
}); });
function DashboardLayout() { function DashboardLayout() {
const { isAuthenticated } = useAuth();
const [openNavbar, { toggle }] = useDisclosure(false); const [openNavbar, { toggle }] = useDisclosure(false);
const navigate = useNavigate(); return isAuthenticated ? (
useEffect(() => {
if (!isAuthenticated()) {
navigate({ to: "/login", replace: true });
}
}, [navigate]);
return (
<AppShell <AppShell
padding="md" padding="md"
header={{ height: 70 }} header={{ height: 70 }}
@ -50,5 +43,7 @@ function DashboardLayout() {
<Outlet /> <Outlet />
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>
) : (
<Navigate to="/login" />
); );
} }

View File

@ -15,7 +15,7 @@ import { useForm } from "@mantine/form";
import { z } from "zod"; import { z } from "zod";
import { zodResolver } from "mantine-form-zod-resolver"; import { zodResolver } from "mantine-form-zod-resolver";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import isAuthenticated from "@/utils/isAuthenticated"; import useAuth from "@/hooks/useAuth";
export const Route = createFileRoute("/login/")({ export const Route = createFileRoute("/login/")({
component: LoginPage, component: LoginPage,
@ -35,6 +35,8 @@ export default function LoginPage() {
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthenticated, saveAuthData } = useAuth();
const form = useForm<FormSchema>({ const form = useForm<FormSchema>({
initialValues: { initialValues: {
username: "", username: "",
@ -44,13 +46,13 @@ export default function LoginPage() {
}); });
useEffect(() => { useEffect(() => {
if (isAuthenticated()) { if (isAuthenticated) {
navigate({ navigate({
to: "/dashboard", to: "/dashboard",
replace: true, replace: true,
}); });
} }
}, [navigate]); }, [navigate, isAuthenticated]);
const loginMutation = useMutation({ const loginMutation = useMutation({
mutationFn: async (values: FormSchema) => { mutationFn: async (values: FormSchema) => {
@ -66,13 +68,14 @@ export default function LoginPage() {
}, },
onSuccess: (data) => { onSuccess: (data) => {
console.log(data); saveAuthData(
{
localStorage.setItem("accessToken", data.accessToken); id: data.user.id,
name: data.user.name,
navigate({ permissions: data.user.permissions,
to: "/dashboard", },
}); data.accessToken
);
}, },
onError: async (error) => { onError: async (error) => {

View File

@ -1,4 +1,4 @@
import isAuthenticated from "@/utils/isAuthenticated"; import useAuth from "@/hooks/useAuth";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react"; import { useEffect } from "react";
@ -7,18 +7,19 @@ export const Route = createFileRoute("/logout/")({
}); });
export default function LogoutPage() { export default function LogoutPage() {
const { isAuthenticated, clearAuthData } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
if (isAuthenticated()) { if (isAuthenticated) {
localStorage.removeItem("accessToken"); clearAuthData();
} }
navigate({ navigate({
to: "/login", to: "/login",
replace: true, replace: true,
}); });
}, [navigate]); }, [navigate, isAuthenticated, clearAuthData]);
return <div>Logging out...</div>; return <div>Logging out...</div>;
} }

View File

@ -1,6 +0,0 @@
function isAuthenticated(): boolean {
const accessToken = localStorage.getItem("accessToken");
return accessToken !== null;
}
export default isAuthenticated;