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:
2025-10-05 18:02:28 +00:00
parent cbf3bbd5bd
commit afa56b1b16
11 changed files with 90 additions and 24 deletions

View File

@@ -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(),
],
});

View File

@@ -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) {

View File

@@ -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 };
}),
});

View File

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

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

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

View File

@@ -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>

View File

@@ -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();

View File

@@ -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",
});

View File

@@ -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",