Improve auth system
This commit is contained in:
parent
c5024d7b69
commit
3aaec5a323
|
|
@ -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[],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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;
|
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;
|
||||||
|
|
|
||||||
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 { 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" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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