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 * as Models from "./src/models";
|
||||||
export * from "./src/providers/metadata";
|
export * from "./src/providers/metadata";
|
||||||
export * from "./src/services/extensions/baseExtension";
|
export * from "./src/services/extensions/baseExtension";
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export const user = sqliteTable("user", {
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
username: text("username").unique(),
|
username: text("username").unique(),
|
||||||
displayUsername: text("display_username"),
|
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", {
|
export const session = sqliteTable("session", {
|
||||||
@@ -34,6 +38,7 @@ export const session = sqliteTable("session", {
|
|||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
impersonatedBy: text("impersonated_by"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const account = sqliteTable("account", {
|
export const account = sqliteTable("account", {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
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 { database } from "../db";
|
||||||
import * as schema from "../db/schema/auth";
|
import * as schema from "../db/schema/auth";
|
||||||
|
import {
|
||||||
|
accessControl,
|
||||||
|
adminRole,
|
||||||
|
moderatorRole,
|
||||||
|
userRole,
|
||||||
|
} from "./auth/permissions";
|
||||||
|
|
||||||
const auth = betterAuth({
|
const auth = betterAuth({
|
||||||
database: drizzleAdapter(database, {
|
database: drizzleAdapter(database, {
|
||||||
@@ -20,7 +26,18 @@ const auth = betterAuth({
|
|||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [username()],
|
plugins: [
|
||||||
|
username(),
|
||||||
|
admin({
|
||||||
|
ac: accessControl,
|
||||||
|
roles: {
|
||||||
|
adminRole,
|
||||||
|
moderatorRole,
|
||||||
|
userRole,
|
||||||
|
},
|
||||||
|
defaultRole: "user",
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default auth;
|
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";
|
"use client";
|
||||||
|
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { LayoutDashboard, LogOutIcon } from "lucide-react";
|
import { LayoutDashboard, LogOutIcon } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@@ -77,15 +77,11 @@ export default function UserMenu() {
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem asChild>
|
||||||
onClick={() => {
|
<Link to="/dashboard" onClick={() => setOpen(false)}>
|
||||||
navigate({
|
<LayoutDashboard />
|
||||||
to: "/home/dashboard",
|
Dashboard
|
||||||
});
|
</Link>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LayoutDashboard />
|
|
||||||
Dashboard
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<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";
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
|
||||||
|
const ac = createAccessControl(statement);
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: import.meta.env.VITE_SERVER_URL,
|
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,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Empty,
|
||||||
|
EmptyContent,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyMedia,
|
||||||
|
EmptyTitle,
|
||||||
|
} from "@/components/ui/empty";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -59,6 +67,16 @@ export const Route = createFileRoute("/dashboard")({
|
|||||||
to: "/login",
|
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() {
|
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 (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
|
|||||||
28
package.json
28
package.json
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "nontara",
|
"name": "nontara",
|
||||||
|
"devDependencies": {
|
||||||
|
"turbo": "^2.5.4",
|
||||||
|
"@biomejs/biome": "^2.2.0"
|
||||||
|
},
|
||||||
|
"packageManager": "bun@1.2.22",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"workspaces": [
|
|
||||||
"apps/*",
|
|
||||||
"packages/*",
|
|
||||||
"extensions/*"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": "biome check --write .",
|
"check": "biome check --write .",
|
||||||
"dev": "turbo dev",
|
"dev": "turbo dev",
|
||||||
@@ -18,12 +17,13 @@
|
|||||||
"db:push": "turbo -F @nontara/server db:push",
|
"db:push": "turbo -F @nontara/server db:push",
|
||||||
"db:studio": "turbo -F @nontara/server db:studio",
|
"db:studio": "turbo -F @nontara/server db:studio",
|
||||||
"db:generate": "turbo -F @nontara/server db:generate",
|
"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": {},
|
"type": "module",
|
||||||
"devDependencies": {
|
"workspaces": [
|
||||||
"turbo": "^2.5.4",
|
"apps/*",
|
||||||
"@biomejs/biome": "^2.2.0"
|
"packages/*",
|
||||||
},
|
"extensions/*"
|
||||||
"packageManager": "bun@1.2.22"
|
]
|
||||||
}
|
}
|
||||||
27
turbo.json
27
turbo.json
@@ -3,15 +3,26 @@
|
|||||||
"ui": "tui",
|
"ui": "tui",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": [
|
||||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
"^build"
|
||||||
"outputs": ["dist/**"]
|
],
|
||||||
|
"inputs": [
|
||||||
|
"$TURBO_DEFAULT$",
|
||||||
|
".env*"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"dist/**"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"dependsOn": ["^lint"]
|
"dependsOn": [
|
||||||
|
"^lint"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"check-types": {
|
"check-types": {
|
||||||
"dependsOn": ["^check-types"]
|
"dependsOn": [
|
||||||
|
"^check-types"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
@@ -32,6 +43,10 @@
|
|||||||
"db:generate": {
|
"db:generate": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
|
},
|
||||||
|
"auth:generate": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user