Compare commits

...

3 Commits

Author SHA1 Message Date
afa56b1b16 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
2025-10-05 18:02:28 +00:00
cbf3bbd5bd chore(deps): update web app dependencies
Update various dependencies in apps/web/package.json to latest versions, including TanStack Router, tRPC, Tailwind CSS, Zod, and testing libraries. Adjust router.tsx to use new import paths and function names for compatibility with updated packages. Regenerate bun.lock to reflect these changes.
2025-10-05 18:01:47 +00:00
3fa13c0386 feat(dashboard): enable SSR and refactor header logic
Remove dashboard pathname check from header component to centralize logic.
Add SSR configuration to dashboard route for server-side rendering.
Include type declarations in routeTree for router and SSR support.
2025-10-05 15:12:55 +00:00
17 changed files with 325 additions and 783 deletions

View File

@@ -8,3 +8,4 @@ export {
export * as Models from "./src/models";
export * from "./src/providers/metadata";
export * from "./src/services/extensions/baseExtension";
export type { AppRouter } from "./src/trpc";

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

@@ -18,45 +18,45 @@
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.8",
"@tanstack/react-form": "^1.0.5",
"@tanstack/react-query": "^5.85.5",
"@tanstack/react-router": "^1.121.0-alpha.27",
"@tanstack/react-router-with-query": "^1.121.0",
"@tanstack/react-start": "^1.121.0-alpha.27",
"@tanstack/router-plugin": "^1.121.0",
"@trpc/client": "^11.5.0",
"@trpc/server": "^11.5.0",
"@trpc/tanstack-react-query": "^11.5.0",
"better-auth": "^1.3.10",
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-form": "^1.23.5",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-router": "^1.132.37",
"@tanstack/react-router-with-query": "^1.130.17",
"@tanstack/react-start": "^1.132.38",
"@tanstack/router-plugin": "^1.132.37",
"@trpc/client": "^11.6.0",
"@trpc/server": "^11.6.0",
"@trpc/tanstack-react-query": "^11.6.0",
"better-auth": "^1.3.26",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"lodash": "^4.17.21",
"lucide-react": "^0.544.0",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.2",
"radix-ui": "^1.4.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"sonner": "^2.0.3",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.3",
"tw-animate-css": "^1.2.5",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^5.1.4",
"zod": "^4.0.2"
"zod": "^4.1.11"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.85.5",
"@tanstack/react-router-devtools": "^1.121.0-alpha.27",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-router-devtools": "^1.132.37",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@types/lodash": "^4.17.20",
"@types/react": "~19.1.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^5.0.1",
"jsdom": "^26.0.0",
"typescript": "^5.7.2",
"vite": "^7.0.2",
"web-vitals": "^5.0.3"
"@types/react": "~19.1.17",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^26.1.0",
"typescript": "^5.9.3",
"vite": "^7.1.9",
"web-vitals": "^5.1.0"
}
}

View File

@@ -120,10 +120,6 @@ function HeaderBar() {
const routerState = useRouterState();
const pathname = routerState.location.pathname;
if (pathname.endsWith("dashboard") || pathname.endsWith("dashboard/")) {
return;
}
return (
<header className="sticky top-0 z-50 flex items-center justify-between border-border border-b bg-muted/95 px-5 py-3 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-muted/80">
<div className="flex items-center gap-3">

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

@@ -418,3 +418,12 @@ const rootRouteChildren: RootRouteChildren = {
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

View File

@@ -1,6 +1,7 @@
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import Loader from "./components/loader";
import "./index.css";
import type { AppRouter } from "@nontara/server";
import {
QueryCache,
QueryClient,
@@ -9,7 +10,6 @@ import {
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
import { toast } from "sonner";
import type { AppRouter } from "../../server/src/routers";
import { routeTree } from "./routeTree.gen";
import { TRPCProvider } from "./utils/trpc";
@@ -48,11 +48,12 @@ const trpc = createTRPCOptionsProxy({
queryClient: queryClient,
});
export const createRouter = () => {
export const getRouter = () => {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreloadStaleTime: 0,
defaultPreload: "intent",
context: { trpc, queryClient },
defaultPendingComponent: () => <Loader />,
defaultNotFoundComponent: () => <div>Not Found</div>,
@@ -69,6 +70,6 @@ export const createRouter = () => {
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
router: ReturnType<typeof getRouter>;
}
}

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

@@ -74,9 +74,13 @@ export const Route = createFileRoute("/dashboard")({
dashboard: ["access"],
},
});
if (!hasPermission)
return {
hasAccess: false,
};
return {
hasAccess: hasPermission ?? false,
hasAccess: hasPermission.success ?? false,
};
},
});

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

914
bun.lock

File diff suppressed because it is too large Load Diff