Compare commits
4 Commits
1f9c810bef
...
166e00c284
| Author | SHA1 | Date | |
|---|---|---|---|
| 166e00c284 | |||
| e4fd30a8fd | |||
| d7a572f496 | |||
| 9c8c1f1127 |
@@ -1,3 +1,10 @@
|
||||
export {
|
||||
accessControl,
|
||||
adminRole,
|
||||
moderatorRole,
|
||||
statement,
|
||||
userRole,
|
||||
} from "./src/lib/auth/permissions";
|
||||
export * as Models from "./src/models";
|
||||
export * from "./src/providers/metadata";
|
||||
export * from "./src/services/extensions/baseExtension";
|
||||
|
||||
@@ -17,6 +17,10 @@ export const user = sqliteTable("user", {
|
||||
.notNull(),
|
||||
username: text("username").unique(),
|
||||
displayUsername: text("display_username"),
|
||||
role: text("role"),
|
||||
banned: integer("banned", { mode: "boolean" }).default(false),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: integer("ban_expires", { mode: "timestamp" }),
|
||||
});
|
||||
|
||||
export const session = sqliteTable("session", {
|
||||
@@ -34,6 +38,7 @@ export const session = sqliteTable("session", {
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
impersonatedBy: text("impersonated_by"),
|
||||
});
|
||||
|
||||
export const account = sqliteTable("account", {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { username } from "better-auth/plugins";
|
||||
import { admin, username } from "better-auth/plugins";
|
||||
import { database } from "../db";
|
||||
import * as schema from "../db/schema/auth";
|
||||
import {
|
||||
accessControl,
|
||||
adminRole,
|
||||
moderatorRole,
|
||||
userRole,
|
||||
} from "./auth/permissions";
|
||||
|
||||
const auth = betterAuth({
|
||||
database: drizzleAdapter(database, {
|
||||
@@ -20,7 +26,18 @@ const auth = betterAuth({
|
||||
httpOnly: true,
|
||||
},
|
||||
},
|
||||
plugins: [username()],
|
||||
plugins: [
|
||||
username(),
|
||||
admin({
|
||||
ac: accessControl,
|
||||
roles: {
|
||||
adminRole,
|
||||
moderatorRole,
|
||||
userRole,
|
||||
},
|
||||
defaultRole: "user",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export default auth;
|
||||
|
||||
29
apps/server/src/lib/auth/permissions.ts
Normal file
29
apps/server/src/lib/auth/permissions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createAccessControl } from "better-auth/plugins/access";
|
||||
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
|
||||
|
||||
export const statement = {
|
||||
...defaultStatements,
|
||||
dashboard: ["access"],
|
||||
libraries: ["create", "read", "update", "delete"],
|
||||
extensions: ["create", "read", "update", "delete"],
|
||||
} as const;
|
||||
|
||||
export const accessControl = createAccessControl(statement);
|
||||
|
||||
export const adminRole = accessControl.newRole({
|
||||
...adminAc.statements,
|
||||
dashboard: ["access"],
|
||||
libraries: ["create", "read", "update", "delete"],
|
||||
extensions: ["create", "read", "update", "delete"],
|
||||
});
|
||||
|
||||
export const moderatorRole = accessControl.newRole({
|
||||
dashboard: ["access"],
|
||||
libraries: ["read"],
|
||||
extensions: ["read"],
|
||||
});
|
||||
|
||||
export const userRole = accessControl.newRole({
|
||||
libraries: ["read"],
|
||||
extensions: ["read"],
|
||||
});
|
||||
34
apps/server/src/trpc/middleware/dashboard.ts
Normal file
34
apps/server/src/trpc/middleware/dashboard.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import auth from "@/lib/auth";
|
||||
import { t } from "@/lib/trpc";
|
||||
|
||||
export const dashboardMiddleware = t.middleware(async ({ ctx, next }) => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: ctx.requestContext.req.raw.headers,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required. Please log in to continue.",
|
||||
});
|
||||
}
|
||||
|
||||
const hasPermission = await auth.api.userHasPermission({
|
||||
body: {
|
||||
userId: session.user.id,
|
||||
permissions: {
|
||||
dashboard: ["access"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You do not have permission to access the dashboard.",
|
||||
});
|
||||
}
|
||||
|
||||
return next({ ctx: { ...ctx, userSession: session } });
|
||||
});
|
||||
0
apps/server/src/trpc/routers/dashboard.ts
Normal file
0
apps/server/src/trpc/routers/dashboard.ts
Normal file
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { LayoutDashboard, LogOutIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
@@ -77,15 +77,11 @@ export default function UserMenu() {
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
navigate({
|
||||
to: "/home/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<LayoutDashboard />
|
||||
Dashboard
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/dashboard" onClick={() => setOpen(false)}>
|
||||
<LayoutDashboard />
|
||||
Dashboard
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { usernameClient } from "better-auth/client/plugins";
|
||||
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";
|
||||
|
||||
const ac = createAccessControl(statement);
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: import.meta.env.VITE_SERVER_URL,
|
||||
plugins: [usernameClient()],
|
||||
plugins: [
|
||||
usernameClient(),
|
||||
adminClient({
|
||||
ac,
|
||||
roles: {
|
||||
admin: adminRole,
|
||||
moderator: moderatorRole,
|
||||
user: userRole,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -31,6 +31,14 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/components/ui/empty";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -59,6 +67,16 @@ export const Route = createFileRoute("/dashboard")({
|
||||
to: "/login",
|
||||
});
|
||||
}
|
||||
|
||||
const { data: hasPermission } = await authClient.admin.hasPermission({
|
||||
permissions: {
|
||||
dashboard: ["access"],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
hasAccess: hasPermission ?? false,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -334,6 +352,35 @@ export function DashboardHeader({ children }: PropsWithChildren) {
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const { hasAccess } = Route.useRouteContext();
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Settings />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Access Denied</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You do not have permission to access the dashboard. Please contact
|
||||
an administrator if you believe this is an error.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button asChild>
|
||||
<Link to="/home">
|
||||
<ArrowLeft className="mr-2 size-4" />
|
||||
Back to Home
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<Sidebar>
|
||||
|
||||
26
package.json
26
package.json
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "nontara",
|
||||
"devDependencies": {
|
||||
"turbo": "^2.5.4",
|
||||
"@biomejs/biome": "^2.2.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.22",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*",
|
||||
"extensions/*"
|
||||
],
|
||||
"scripts": {
|
||||
"check": "biome check --write .",
|
||||
"dev": "turbo dev",
|
||||
@@ -18,12 +17,13 @@
|
||||
"db:push": "turbo -F @nontara/server db:push",
|
||||
"db:studio": "turbo -F @nontara/server db:studio",
|
||||
"db:generate": "turbo -F @nontara/server db:generate",
|
||||
"db:migrate": "turbo -F @nontara/server db:migrate"
|
||||
"db:migrate": "turbo -F @nontara/server db:migrate",
|
||||
"auth:generate": "turbo -F @nontara/server auth:generate"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"turbo": "^2.5.4",
|
||||
"@biomejs/biome": "^2.2.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.22"
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*",
|
||||
"extensions/*"
|
||||
]
|
||||
}
|
||||
25
turbo.json
25
turbo.json
@@ -3,15 +3,26 @@
|
||||
"ui": "tui",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": ["dist/**"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"$TURBO_DEFAULT$",
|
||||
".env*"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
"dependsOn": [
|
||||
"^lint"
|
||||
]
|
||||
},
|
||||
"check-types": {
|
||||
"dependsOn": ["^check-types"]
|
||||
"dependsOn": [
|
||||
"^check-types"
|
||||
]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
@@ -32,6 +43,10 @@
|
||||
"db:generate": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"auth:generate": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user