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 { dataset_entry } from '@/lib/model/dataset_entry';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import AudioPlayer from '@/components/AudioPlayer';
|
import AudioPlayer from '@/components/AudioPlayer';
|
||||||
|
import { requireAdmin } from '@/lib/auth';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
interface DatasetEntryPageProps {
|
interface DatasetEntryPageProps {
|
||||||
params: {
|
params: {
|
||||||
@@ -12,6 +14,16 @@ interface DatasetEntryPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function DatasetEntryPage({ params }: 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 { id: datasetId, entryId } = await params;
|
||||||
const datasetIdNum = parseInt(datasetId, 10);
|
const datasetIdNum = parseInt(datasetId, 10);
|
||||||
const entryIdNum = parseInt(entryId, 10);
|
const entryIdNum = parseInt(entryId, 10);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import EditableDatasetHeader from '@/components/EditableDatasetHeader';
|
|||||||
import DatasetEntriesList from '@/components/DatasetEntriesList';
|
import DatasetEntriesList from '@/components/DatasetEntriesList';
|
||||||
import UploadDatasetEntriesModal from '@/components/UploadDatasetEntriesModal';
|
import UploadDatasetEntriesModal from '@/components/UploadDatasetEntriesModal';
|
||||||
import RemoveAllEntriesButton from '@/components/RemoveAllEntriesButton';
|
import RemoveAllEntriesButton from '@/components/RemoveAllEntriesButton';
|
||||||
|
import { requireAdmin } from '@/lib/auth';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
interface DatasetPageProps {
|
interface DatasetPageProps {
|
||||||
params: {
|
params: {
|
||||||
@@ -14,6 +16,16 @@ interface DatasetPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function DatasetPage({ params }: 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 {id} = await params;
|
||||||
const datasetId = parseInt(id, 10);
|
const datasetId = parseInt(id, 10);
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
import DatasetsList from "@/components/DatasetsList";
|
import DatasetsList from "@/components/DatasetsList";
|
||||||
import CreateDatasetModal from "@/components/CreateDatasetModal";
|
import CreateDatasetModal from "@/components/CreateDatasetModal";
|
||||||
import { auth } from "@/lib/auth";
|
import { requireAdmin } from "@/lib/auth";
|
||||||
import { headers } from "next/headers";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
const session = await auth.api.getSession({
|
const result = await requireAdmin();
|
||||||
headers: await headers()
|
|
||||||
})
|
|
||||||
|
|
||||||
if(!session) {
|
if (!result.authenticated) {
|
||||||
redirect("/user/sign-in")
|
redirect("/user/sign-in");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.admin) {
|
||||||
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<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 />
|
<CreateDatasetModal />
|
||||||
<DatasetsList />
|
<DatasetsList />
|
||||||
|
</div>
|
||||||
</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-xs text-blue-100 uppercase tracking-wide">Willkommen zurück</p>
|
||||||
<p className="text-sm font-semibold">{session.user?.email}</p>
|
<p className="text-sm font-semibold">{session.user?.email}</p>
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={handleSignOut}
|
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"
|
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 { localization } from "better-auth-localization";
|
||||||
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
|
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
|
||||||
|
import { headers } from "next/headers";
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
import { user, session, account, verification } from "./model";
|
import * as schema from "./model/auth-schema";
|
||||||
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: "sqlite",
|
provider: "sqlite",
|
||||||
schema: {
|
schema: schema
|
||||||
user,
|
|
||||||
session,
|
|
||||||
account,
|
|
||||||
verification,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
user:{
|
||||||
|
additionalFields: {
|
||||||
|
admin: {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
@@ -26,3 +30,15 @@ export const auth = betterAuth({
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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)
|
.default(false)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
image: text("image"),
|
image: text("image"),
|
||||||
|
admin: integer("admin", { mode: "boolean" }).default(false).notNull(),
|
||||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
|||||||
Reference in New Issue
Block a user