added admin system
This commit is contained in:
36
src/app/actions/manage-users.ts
Normal file
36
src/app/actions/manage-users.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
"use server";
|
||||
|
||||
import db from "@/lib/db";
|
||||
import { user } from "@/lib/model/auth-schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { requireAdmin } from "@/lib/auth";
|
||||
|
||||
export async function deleteUser(userId: string) {
|
||||
const result = await requireAdmin();
|
||||
|
||||
if (!result.authenticated || !result.admin) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
await db.delete(user).where(eq(user.id, userId));
|
||||
}
|
||||
|
||||
export async function setUserAdmin(userId: string, isAdmin: boolean) {
|
||||
const result = await requireAdmin();
|
||||
|
||||
if (!result.authenticated || !result.admin) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
await db.update(user).set({ admin: isAdmin }).where(eq(user.id, userId));
|
||||
}
|
||||
|
||||
export async function getAllUsers() {
|
||||
const result = await requireAdmin();
|
||||
|
||||
if (!result.authenticated || !result.admin) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
return db.select().from(user);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import db from '@/lib/db';
|
||||
import { dataset_entry } from '@/lib/model/dataset_entry';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import AudioPlayer from '@/components/AudioPlayer';
|
||||
import { requireAdmin } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
interface DatasetEntryPageProps {
|
||||
params: {
|
||||
@@ -12,6 +14,16 @@ interface DatasetEntryPageProps {
|
||||
}
|
||||
|
||||
export default async function DatasetEntryPage({ params }: DatasetEntryPageProps) {
|
||||
const result = await requireAdmin();
|
||||
|
||||
if (!result.authenticated) {
|
||||
redirect("/user/sign-in");
|
||||
}
|
||||
|
||||
if (!result.admin) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const { id: datasetId, entryId } = await params;
|
||||
const datasetIdNum = parseInt(datasetId, 10);
|
||||
const entryIdNum = parseInt(entryId, 10);
|
||||
|
||||
@@ -6,6 +6,8 @@ import EditableDatasetHeader from '@/components/EditableDatasetHeader';
|
||||
import DatasetEntriesList from '@/components/DatasetEntriesList';
|
||||
import UploadDatasetEntriesModal from '@/components/UploadDatasetEntriesModal';
|
||||
import RemoveAllEntriesButton from '@/components/RemoveAllEntriesButton';
|
||||
import { requireAdmin } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
interface DatasetPageProps {
|
||||
params: {
|
||||
@@ -14,6 +16,16 @@ interface DatasetPageProps {
|
||||
}
|
||||
|
||||
export default async function DatasetPage({ params }: DatasetPageProps) {
|
||||
const result = await requireAdmin();
|
||||
|
||||
if (!result.authenticated) {
|
||||
redirect("/user/sign-in");
|
||||
}
|
||||
|
||||
if (!result.admin) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const {id} = await params;
|
||||
const datasetId = parseInt(id, 10);
|
||||
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
import DatasetsList from "@/components/DatasetsList";
|
||||
import CreateDatasetModal from "@/components/CreateDatasetModal";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { requireAdmin } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers()
|
||||
})
|
||||
const result = await requireAdmin();
|
||||
|
||||
if(!session) {
|
||||
redirect("/user/sign-in")
|
||||
if (!result.authenticated) {
|
||||
redirect("/user/sign-in");
|
||||
}
|
||||
|
||||
if (!result.admin) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-4">Admin Panel</h1>
|
||||
<div className="flex gap-4">
|
||||
<Link
|
||||
href="/admin/users"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Manage Users
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h2 className="text-2xl font-bold mb-4">Datasets</h2>
|
||||
<CreateDatasetModal />
|
||||
<DatasetsList />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
39
src/app/admin/users/page.tsx
Normal file
39
src/app/admin/users/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import Link from "next/link";
|
||||
import UsersList from "@/components/UsersList";
|
||||
import { requireAdmin } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAllUsers } from "@/app/actions/manage-users";
|
||||
|
||||
export default async function UsersPage() {
|
||||
const result = await requireAdmin();
|
||||
|
||||
if (!result.authenticated) {
|
||||
redirect("/user/sign-in");
|
||||
}
|
||||
|
||||
if (!result.admin) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const users = await getAllUsers();
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
href="/admin"
|
||||
className="text-blue-500 hover:text-blue-600 mb-4 inline-block"
|
||||
>
|
||||
← Back to Admin
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold">User Management</h1>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6">
|
||||
<UsersList users={users} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,14 @@ export function Header() {
|
||||
<p className="text-xs text-blue-100 uppercase tracking-wide">Willkommen zurück</p>
|
||||
<p className="text-sm font-semibold">{session.user?.email}</p>
|
||||
</div>
|
||||
{(session.user as any)?.admin && (
|
||||
<Link
|
||||
href="/admin"
|
||||
className="px-4 py-2 bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 text-white font-semibold rounded-lg transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
Admin
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 active:bg-red-800 text-white font-semibold rounded-lg transition-all duration-200 transform hover:scale-105"
|
||||
|
||||
140
src/components/UsersList.tsx
Normal file
140
src/components/UsersList.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import { deleteUser, setUserAdmin } from "@/app/actions/manage-users";
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
admin: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface UsersListProps {
|
||||
users: User[];
|
||||
}
|
||||
|
||||
export default function UsersList({ users: initialUsers }: UsersListProps) {
|
||||
const [users, setUsers] = useState(initialUsers);
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
const query = search.toLowerCase();
|
||||
return users.filter(
|
||||
(u) =>
|
||||
u.email.toLowerCase().includes(query) ||
|
||||
u.name.toLowerCase().includes(query)
|
||||
);
|
||||
}, [users, search]);
|
||||
|
||||
const handleDeleteUser = async (userId: string) => {
|
||||
if (!confirm("Are you sure you want to delete this user?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(userId);
|
||||
try {
|
||||
await deleteUser(userId);
|
||||
setUsers(users.filter((u) => u.id !== userId));
|
||||
} catch (error) {
|
||||
alert("Failed to delete user");
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAdmin = async (userId: string, currentAdmin: boolean) => {
|
||||
setLoading(userId);
|
||||
try {
|
||||
await setUserAdmin(userId, !currentAdmin);
|
||||
setUsers(
|
||||
users.map((u) =>
|
||||
u.id === userId ? { ...u, admin: !currentAdmin } : u
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
alert("Failed to update user");
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by email or name..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white border border-gray-200 rounded-lg">
|
||||
<thead className="bg-gray-100 border-b">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold">Email</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold">Admin</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold">
|
||||
Created
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUsers.map((user) => (
|
||||
<tr
|
||||
key={user.id}
|
||||
className="border-b hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<td className="px-6 py-4">{user.email}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-sm font-medium ${
|
||||
user.admin
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{user.admin ? "Yes" : "No"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">
|
||||
{new Date(user.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 space-x-2">
|
||||
<button
|
||||
onClick={() => handleToggleAdmin(user.id, user.admin)}
|
||||
disabled={loading === user.id}
|
||||
className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading === user.id
|
||||
? "..."
|
||||
: user.admin
|
||||
? "Remove Admin"
|
||||
: "Make Admin"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteUser(user.id)}
|
||||
disabled={loading === user.id}
|
||||
className="px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600 disabled:opacity-50"
|
||||
>
|
||||
{loading === user.id ? "..." : "Delete"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{filteredUsers.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
{search ? "No users found matching your search" : "No users found"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { betterAuth, Session } from "better-auth";
|
||||
import { localization } from "better-auth-localization";
|
||||
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
|
||||
import { headers } from "next/headers";
|
||||
import db from "./db";
|
||||
import { user, session, account, verification } from "./model";
|
||||
import * as schema from "./model/auth-schema";
|
||||
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "sqlite",
|
||||
schema: {
|
||||
user,
|
||||
session,
|
||||
account,
|
||||
verification,
|
||||
},
|
||||
schema: schema
|
||||
}),
|
||||
user:{
|
||||
additionalFields: {
|
||||
admin: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
@@ -25,4 +29,16 @@ export const auth = betterAuth({
|
||||
fallbackLocale: "default" // Fallback to English
|
||||
})
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
export async function requireAdmin() {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers()
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return { authenticated: false, admin: false };
|
||||
}
|
||||
|
||||
return { authenticated: true, admin: session.user.admin, session };
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export const user = sqliteTable("user", {
|
||||
.default(false)
|
||||
.notNull(),
|
||||
image: text("image"),
|
||||
admin: integer("admin", { mode: "boolean" }).default(false).notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
|
||||
Reference in New Issue
Block a user