feat(auth): add authentication middleware and update auth config
- Introduce auth and dashboard middlewares for server-side authentication checks - Update auth client and server configurations with reactStartCookies plugin - Refactor routes to use middleware for improved authentication handling - Enhance onboarding service to set user roles during account creation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { admin, username } from "better-auth/plugins";
|
||||
import { reactStartCookies } from "better-auth/react-start";
|
||||
import { database } from "../db";
|
||||
import * as schema from "../db/schema/auth";
|
||||
import {
|
||||
@@ -27,16 +28,17 @@ const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
username(),
|
||||
admin({
|
||||
ac: accessControl,
|
||||
roles: {
|
||||
adminRole,
|
||||
moderatorRole,
|
||||
userRole,
|
||||
admin: adminRole,
|
||||
moderator: moderatorRole,
|
||||
user: userRole,
|
||||
},
|
||||
defaultRole: "user",
|
||||
}),
|
||||
username(),
|
||||
reactStartCookies(),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -72,10 +72,11 @@ export class OnboardService extends BaseService {
|
||||
|
||||
public async completeOnboarding(
|
||||
body: OnboardConfigurationData,
|
||||
headers: Headers,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Create user account
|
||||
await auth.api.signUpEmail({
|
||||
const createdAccount = await auth.api.signUpEmail({
|
||||
body: {
|
||||
email: body.account.email,
|
||||
name: body.account.username,
|
||||
@@ -83,6 +84,13 @@ export class OnboardService extends BaseService {
|
||||
username: body.account.username,
|
||||
},
|
||||
});
|
||||
await auth.api.setRole({
|
||||
body: {
|
||||
role: "admin",
|
||||
userId: createdAccount.user.id,
|
||||
},
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
// Save libraries with language settings
|
||||
if (body.libraries && body.libraries.length > 0) {
|
||||
|
||||
@@ -131,7 +131,10 @@ export const onboardRouter = t.router({
|
||||
completeOnboarding: onboardProcedure
|
||||
.input(OnboardConfigurationData)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.onboardService.completeOnboarding(input);
|
||||
await ctx.onboardService.completeOnboarding(
|
||||
input,
|
||||
ctx.requestContext.req.raw.headers,
|
||||
);
|
||||
return { success: true, completed: true };
|
||||
}),
|
||||
});
|
||||
|
||||
0
apps/web/src/functions/auth.ts
Normal file
0
apps/web/src/functions/auth.ts
Normal file
@@ -2,6 +2,7 @@ import { adminRole, moderatorRole, statement, userRole } from "@nontara/server";
|
||||
import { adminClient, usernameClient } from "better-auth/client/plugins";
|
||||
import { createAccessControl } from "better-auth/plugins/access";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
import { reactStartCookies } from "better-auth/react-start";
|
||||
|
||||
const ac = createAccessControl(statement);
|
||||
|
||||
@@ -17,5 +18,6 @@ export const authClient = createAuthClient({
|
||||
user: userRole,
|
||||
},
|
||||
}),
|
||||
reactStartCookies(),
|
||||
],
|
||||
});
|
||||
|
||||
20
apps/web/src/middlewares/auth.ts
Normal file
20
apps/web/src/middlewares/auth.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createMiddleware } from "@tanstack/react-start";
|
||||
import { getRequestHeaders } from "@tanstack/react-start/server";
|
||||
import { authClient } from "../lib/auth-client";
|
||||
|
||||
export const authMiddleware = createMiddleware().server(async ({ next }) => {
|
||||
const auth = await authClient.getSession({
|
||||
fetchOptions: {
|
||||
headers: getRequestHeaders(),
|
||||
},
|
||||
});
|
||||
const isAuthenticated = typeof auth.data?.user.id === "string";
|
||||
|
||||
return await next({
|
||||
context: {
|
||||
user: auth.data?.user,
|
||||
isAuthenticated,
|
||||
isUserBanned: auth.data?.user.banned ?? false,
|
||||
},
|
||||
});
|
||||
});
|
||||
23
apps/web/src/middlewares/dashboard.ts
Normal file
23
apps/web/src/middlewares/dashboard.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createMiddleware } from "@tanstack/react-start";
|
||||
import { getRequestHeaders } from "@tanstack/react-start/server";
|
||||
import { authClient } from "../lib/auth-client";
|
||||
import { authMiddleware } from "./auth";
|
||||
|
||||
export const dashboardMiddleware = createMiddleware()
|
||||
.middleware([authMiddleware])
|
||||
.server(async ({ next, context }) => {
|
||||
const hasPermissions = await authClient.admin.hasPermission({
|
||||
fetchOptions: {
|
||||
headers: getRequestHeaders(),
|
||||
},
|
||||
permissions: {
|
||||
dashboard: ["access"],
|
||||
},
|
||||
});
|
||||
return await next({
|
||||
context: {
|
||||
...context,
|
||||
hasPermissions: hasPermissions.data?.success ?? false,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { AppRouter } from "@nontara/server";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
// import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
HeadContent,
|
||||
Outlet,
|
||||
Scripts,
|
||||
} from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
// import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import type { TRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { AppRouter } from "../../../server/src/trpc";
|
||||
import appCss from "../index.css?url";
|
||||
export interface RouterAppContext {
|
||||
trpc: TRPCOptionsProxy<AppRouter>;
|
||||
@@ -45,7 +45,7 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
|
||||
|
||||
function RootDocument() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
@@ -64,8 +64,8 @@ function RootDocument() {
|
||||
</ScrollArea>
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
<TanStackRouterDevtools position="bottom-left" />
|
||||
<ReactQueryDevtools position="bottom" buttonPosition="bottom-right" />
|
||||
{/* <TanStackRouterDevtools position="bottom-left" /> */}
|
||||
{/* <ReactQueryDevtools position="bottom" buttonPosition="bottom-right" /> */}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -60,7 +60,6 @@ import type { FileRoutesByTo } from "@/routeTree.gen";
|
||||
|
||||
export const Route = createFileRoute("/dashboard")({
|
||||
component: RouteComponent,
|
||||
ssr: true,
|
||||
beforeLoad: async () => {
|
||||
const session = await authClient.getSession();
|
||||
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
|
||||
|
||||
import Header from "@/components/header";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { authMiddleware } from "@/middlewares/auth";
|
||||
|
||||
export const Route = createFileRoute("/home")({
|
||||
component: RouteComponent,
|
||||
beforeLoad: async () => {
|
||||
const session = await authClient.getSession();
|
||||
|
||||
if (!session) {
|
||||
server: {
|
||||
middleware: [authMiddleware],
|
||||
},
|
||||
beforeLoad: async (ctx) => {
|
||||
if (!ctx.serverContext) {
|
||||
throw redirect({
|
||||
to: "/login",
|
||||
params: {
|
||||
reason: "UNAUTHENTICATED",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!ctx.serverContext.user) {
|
||||
throw redirect({
|
||||
to: "/login",
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { authMiddleware } from "@/middlewares/auth";
|
||||
import { isOnboardingNeeded } from "@/utils/functions";
|
||||
import { useTRPC } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomeComponent,
|
||||
beforeLoad: async () => {
|
||||
server: {
|
||||
middleware: [authMiddleware],
|
||||
},
|
||||
beforeLoad: async (ctx) => {
|
||||
// Check if onboarding is needed first
|
||||
const needsOnboarding = await isOnboardingNeeded();
|
||||
if (needsOnboarding.needsOnboarding) {
|
||||
@@ -16,10 +19,7 @@ export const Route = createFileRoute("/")({
|
||||
}
|
||||
|
||||
// Check authentication status
|
||||
const session = await authClient.getSession();
|
||||
|
||||
// Redirect based on auth status
|
||||
if (session) {
|
||||
if (ctx.serverContext?.isAuthenticated) {
|
||||
// User is logged in, redirect to home
|
||||
throw redirect({
|
||||
to: "/home",
|
||||
|
||||
Reference in New Issue
Block a user