diff --git a/drizzle.config.ts b/drizzle.config.ts
index bbdb7e4..816a735 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -3,7 +3,7 @@ import { defineConfig } from 'drizzle-kit';
import env from '@/env';
export default defineConfig({
- schema: './src/database/schema/index.ts',
+ schema: './src/database/tables/index.ts',
dialect: 'postgresql',
out: './drizzle',
dbCredentials: {
diff --git a/package.json b/package.json
index e37a3cb..021b5c3 100644
--- a/package.json
+++ b/package.json
@@ -31,8 +31,11 @@
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
+ "@radix-ui/react-scroll-area": "^1.1.0",
+ "@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
+ "@tanstack/react-query": "^5.52.1",
"arctic": "^1.9.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cb39791..29a5663 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,12 +26,21 @@ dependencies:
'@radix-ui/react-popover':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-scroll-area':
+ specifier: ^1.1.0
+ version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-select':
+ specifier: ^2.1.1
+ version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot':
specifier: ^1.1.0
version: 1.1.0(@types/react@18.3.4)(react@18.3.1)
'@radix-ui/react-toast':
specifier: ^1.2.1
version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@tanstack/react-query':
+ specifier: ^5.52.1
+ version: 5.52.1(react@18.3.1)
arctic:
specifier: ^1.9.2
version: 1.9.2
@@ -1376,6 +1385,10 @@ packages:
requiresBuild: true
optional: true
+ /@radix-ui/number@1.1.0:
+ resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
+ dev: false
+
/@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies:
@@ -2036,6 +2049,74 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
+ /@radix-ui/react-scroll-area@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/number': 1.1.0
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@types/react': 18.3.4
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-select@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/number': 1.1.0
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.4)(react@18.3.1)
+ '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.3.4
+ '@types/react-dom': 18.3.0
+ aria-hidden: 1.2.4
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.5.7(@types/react@18.3.4)(react@18.3.1)
+ dev: false
+
/@radix-ui/react-slot@1.0.2(@types/react@18.3.4)(react@18.3.1):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
@@ -2208,6 +2289,19 @@ packages:
react: 18.3.1
dev: false
+ /@radix-ui/react-use-previous@1.1.0(@types/react@18.3.4)(react@18.3.1):
+ resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.4
+ react: 18.3.1
+ dev: false
+
/@radix-ui/react-use-rect@1.1.0(@types/react@18.3.4)(react@18.3.1):
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
peerDependencies:
@@ -2275,6 +2369,19 @@ packages:
tslib: 2.6.3
dev: false
+ /@tanstack/query-core@5.52.0:
+ resolution: {integrity: sha512-U1DOEgltjUwalN6uWYTewSnA14b+tE7lSylOiASKCAO61ENJeCq9VVD/TXHA6O5u9+6v5+UgGYBSccTKDoyMqw==}
+ dev: false
+
+ /@tanstack/react-query@5.52.1(react@18.3.1):
+ resolution: {integrity: sha512-soyn4dNIUZ8US8NaPVXv06gkZFHaZnPfKWPDjRJjFRW3Y7WZ0jx72eT6zhw3VQlkMPysmXye8l35ewPHspKgbQ==}
+ peerDependencies:
+ react: ^18 || ^19
+ dependencies:
+ '@tanstack/query-core': 5.52.0
+ react: 18.3.1
+ dev: false
+
/@tybys/wasm-util@0.8.3:
resolution: {integrity: sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==}
requiresBuild: true
diff --git a/src/app/(private)/dashboard/(admin)/users/layout.tsx b/src/app/(private)/dashboard/(admin)/users/layout.tsx
new file mode 100644
index 0000000..69fb83e
--- /dev/null
+++ b/src/app/(private)/dashboard/(admin)/users/layout.tsx
@@ -0,0 +1,20 @@
+import { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'Users | Sainseni Dashboard',
+ authors: [
+ {
+ name: 'Khoironi Kurnia Syah',
+ url: 'https://zekhoi.dev',
+ },
+ ],
+ description: 'Community for community',
+};
+
+export default function UsersLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return <>{children}>;
+}
diff --git a/src/app/(private)/dashboard/(admin)/users/page.tsx b/src/app/(private)/dashboard/(admin)/users/page.tsx
new file mode 100644
index 0000000..278d927
--- /dev/null
+++ b/src/app/(private)/dashboard/(admin)/users/page.tsx
@@ -0,0 +1,140 @@
+'use client';
+
+import { Menu, Users } from 'lucide-react';
+import { useState } from 'react';
+
+import { useGetRoles } from '@/lib/queries/roles.query';
+import { useGetUsers } from '@/lib/queries/users.query';
+
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+
+import RoleFormDialog from './role-form';
+export default function Component() {
+ const {
+ data: roleData,
+ isLoading: isRoleLoading,
+ error: roleError,
+ } = useGetRoles({});
+
+ const {
+ data: userData,
+ isLoading: isuserLoading,
+ error: userError,
+ } = useGetUsers({});
+ const [newMember, setNewMember] = useState({
+ name: '',
+ email: '',
+ role: '',
+ });
+ const [newRole, setNewRole] = useState('');
+ const [editingRole, setEditingRole] = useState('');
+ console.error(userData, 'userData');
+
+ return (
+
+
+
+
+ User Management
+
+
+
+
+
+
+
+
+
+ User List
+
+
+
+
+
+
+
+ Name
+
+
+ Email
+
+ Role
+
+ Actions
+
+
+
+
+ {userData &&
+ userData.map((member) => (
+
+
+ {member.name}
+
+
+ {member.email}
+
+
+ {member.roleName}
+
+
+
+
+
+
+
+
+ Actions
+
+
+ Edit User
+
+
+ Change Role
+
+
+
+ Delete User
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(private)/dashboard/(admin)/users/role-form.tsx b/src/app/(private)/dashboard/(admin)/users/role-form.tsx
new file mode 100644
index 0000000..e7b4282
--- /dev/null
+++ b/src/app/(private)/dashboard/(admin)/users/role-form.tsx
@@ -0,0 +1,161 @@
+'use client';
+
+import {
+ AlertCircle,
+ Loader2,
+ Pencil,
+ Plus,
+ Settings,
+ Trash2,
+} from 'lucide-react';
+import { useState } from 'react';
+
+import { useGetRoles } from '@/lib/queries/roles.query';
+
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { ScrollArea } from '@/components/ui/scroll-area';
+
+export default function RoleFormDialog() {
+ const [newRoleName, setNewRoleName] = useState('');
+ const {
+ data: roleData,
+ isLoading: isRoleLoading,
+ error: roleError,
+ refetch: refetchRoles,
+ } = useGetRoles({});
+
+ const handleAddRole = () => {
+ // Implement add role logic here
+ console.log('Adding new role:', newRoleName);
+ setNewRoleName('');
+ refetchRoles();
+ };
+
+ const handleEditRole = (roleId: string) => {
+ // Implement edit role logic here
+ console.log('Editing role:', roleId);
+ };
+
+ const handleDeleteRole = (roleId: string) => {
+ // Implement delete role logic here
+ console.log('Deleting role:', roleId);
+ refetchRoles();
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(private)/dashboard/(admin)/users/user-form.tsx b/src/app/(private)/dashboard/(admin)/users/user-form.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/(private)/layout.tsx b/src/app/(private)/layout.tsx
index 60922b0..46685cb 100644
--- a/src/app/(private)/layout.tsx
+++ b/src/app/(private)/layout.tsx
@@ -1,7 +1,7 @@
import { Metadata } from 'next';
import { redirect } from 'next/navigation';
-import { validateRequest } from '@/lib/auth';
+import { checkAdmin, validateRequest } from '@/lib/auth';
import PrivateHeader from '@/components/private/header';
@@ -27,9 +27,11 @@ export default async function PrivateLayout({
return redirect('/auth/signin');
}
+ const isAdmin = await checkAdmin();
+
return (
);
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9702c58..55c674c 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -3,8 +3,6 @@ import { Inter } from 'next/font/google';
import './globals.css';
-import { Toaster } from '@/components/ui/toaster';
-
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
@@ -27,7 +25,6 @@ export default function RootLayout({
{children}
-
);
diff --git a/src/app/template.tsx b/src/app/template.tsx
new file mode 100644
index 0000000..4dd7d39
--- /dev/null
+++ b/src/app/template.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import dynamic from 'next/dynamic';
+
+import { Toaster } from '@/components/ui/toaster';
+export const queryClient = new QueryClient();
+
+function RootTemplate({ children }: React.PropsWithChildren) {
+ return (
+
+ {children}
+
+
+ );
+}
+export default dynamic(() => Promise.resolve(RootTemplate), {
+ ssr: false,
+});
diff --git a/src/components/private/header.tsx b/src/components/private/header.tsx
index a9baa43..7f6c524 100644
--- a/src/components/private/header.tsx
+++ b/src/components/private/header.tsx
@@ -4,7 +4,8 @@ import { CircleUser, Menu, Sparkles } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
-import { signOut } from '@/lib/auth/action';
+import { signOut } from '@/lib/actions/auth';
+import { DatabaseUserAttributes } from '@/lib/auth';
import { Button } from '@/components/ui/button';
import {
@@ -17,18 +18,24 @@ import {
} from '@/components/ui/dropdown-menu';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
-const navbarMenu = [
- { name: 'Dashboard', url: '/dashboard' },
- { name: 'Projects', url: '/dashboard/projects' },
- { name: 'Friends', url: '/dashboard/friends' },
- { name: 'References', url: '/dashboard/references' },
- { name: 'Events', url: '/dashboard/events' },
- { name: 'Users', url: '/dashboard/users' },
- { name: 'Roles', url: '/dashboard/roles' },
- { name: 'Keys', url: '/dashboard/keys' },
-];
+type PrivateHeaderProps = {
+ isAdmin: boolean;
+ userData: DatabaseUserAttributes;
+};
+export default function PrivateHeader({
+ isAdmin = false,
+ userData,
+}: PrivateHeaderProps) {
+ const navbarMenu = [
+ { name: 'Dashboard', url: '/dashboard', isAdmin: false },
+ { name: 'Projects', url: '/dashboard/projects', isAdmin: false },
+ { name: 'Friends', url: '/dashboard/friends', isAdmin: false },
+ { name: 'References', url: '/dashboard/references', isAdmin: false },
+ { name: 'Events', url: '/dashboard/events', isAdmin: false },
+ { name: 'Users', url: '/dashboard/users', isAdmin: true },
+ { name: 'Keys', url: '/dashboard/keys', isAdmin: true },
+ ];
-export default function PrivateHeader() {
const pathname = usePathname();
const isActive = (path: string) => {
@@ -42,25 +49,31 @@ export default function PrivateHeader() {
@@ -110,7 +123,7 @@ export default function PrivateHeader() {
- My Name
+ {userData.name}
Settings
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..0b4a48d
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000..cbe5a36
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,160 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/src/database/index.ts b/src/database/index.ts
index 02f25d4..b9185ff 100644
--- a/src/database/index.ts
+++ b/src/database/index.ts
@@ -5,8 +5,8 @@ import postgres from 'postgres';
import env from '@/env';
-import * as schema from './schema';
-export * from './schema';
+import * as schema from './tables';
+export * from './tables';
let database: PostgresJsDatabase;
let pg: ReturnType;
diff --git a/src/database/schema/index.ts b/src/database/schema/index.ts
deleted file mode 100644
index 682d360..0000000
--- a/src/database/schema/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export * from './event.schema';
-export * from './friend.schema';
-export * from './friend.schema';
-export * from './project.schema';
-export * from './role.schema';
-export * from './session.schema';
-export * from './user.schema';
diff --git a/src/database/seed.ts b/src/database/seed.ts
index c2072a5..cf9aed6 100644
--- a/src/database/seed.ts
+++ b/src/database/seed.ts
@@ -1,6 +1,6 @@
import 'dotenv/config';
-import * as table from '@/database/schema';
+import * as table from '@/database/tables';
import env from '@/env';
import { database, pg } from './index';
@@ -10,14 +10,14 @@ async function main() {
throw new Error('Seed is only allowed in development environment');
}
- const [adminRole, userRole] = await database
+ const [adminRole, memberRole] = await database
.insert(table.role)
.values([
{
name: 'admin',
},
{
- name: 'user',
+ name: 'member',
},
])
.onConflictDoNothing()
@@ -38,7 +38,7 @@ async function main() {
email: 'prabowo@indonesia.go.id',
name: 'Prabowo Subianto',
accountType: 'github',
- roleId: userRole.id,
+ roleId: memberRole.id,
},
])
.onConflictDoNothing()
diff --git a/src/database/schema/event.schema.ts b/src/database/tables/event.table.ts
similarity index 100%
rename from src/database/schema/event.schema.ts
rename to src/database/tables/event.table.ts
diff --git a/src/database/schema/friend.schema.ts b/src/database/tables/friend.table.ts
similarity index 100%
rename from src/database/schema/friend.schema.ts
rename to src/database/tables/friend.table.ts
diff --git a/src/database/tables/index.ts b/src/database/tables/index.ts
new file mode 100644
index 0000000..223d1fc
--- /dev/null
+++ b/src/database/tables/index.ts
@@ -0,0 +1,7 @@
+export * from './event.table';
+export * from './friend.table';
+export * from './friend.table';
+export * from './project.table';
+export * from './role.table';
+export * from './session.table';
+export * from './user.table';
diff --git a/src/database/schema/project.schema.ts b/src/database/tables/project.table.ts
similarity index 100%
rename from src/database/schema/project.schema.ts
rename to src/database/tables/project.table.ts
diff --git a/src/database/schema/role.schema.ts b/src/database/tables/role.table.ts
similarity index 62%
rename from src/database/schema/role.schema.ts
rename to src/database/tables/role.table.ts
index 3ccdb87..4c32c5a 100644
--- a/src/database/schema/role.schema.ts
+++ b/src/database/tables/role.table.ts
@@ -1,9 +1,10 @@
-import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
+import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { v7 as uuidv7 } from 'uuid';
export const role = pgTable('roles', {
id: uuid('id').primaryKey().$defaultFn(uuidv7),
name: text('name').unique().notNull(),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
});
export type Role = typeof role.$inferSelect;
diff --git a/src/database/schema/session.schema.ts b/src/database/tables/session.table.ts
similarity index 91%
rename from src/database/schema/session.schema.ts
rename to src/database/tables/session.table.ts
index f3d171b..257a918 100644
--- a/src/database/schema/session.schema.ts
+++ b/src/database/tables/session.table.ts
@@ -1,6 +1,6 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
-import { user } from './user.schema';
+import { user } from './user.table';
export const session = pgTable('session', {
id: text('id').primaryKey(),
diff --git a/src/database/schema/user.schema.ts b/src/database/tables/user.table.ts
similarity index 85%
rename from src/database/schema/user.schema.ts
rename to src/database/tables/user.table.ts
index c0726f3..c302b3f 100644
--- a/src/database/schema/user.schema.ts
+++ b/src/database/tables/user.table.ts
@@ -1,7 +1,7 @@
import { pgEnum, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { v7 as uuidv7 } from 'uuid';
-import { role } from '@/database/schema/role.schema';
+import { role } from '@/database/tables';
export const accountTypeEnum = pgEnum('type', ['google', 'github']);
@@ -19,3 +19,4 @@ export const user = pgTable('users', {
});
export type User = typeof user.$inferSelect;
+export type ModifiedUser = Pick;
diff --git a/src/lib/auth/action.ts b/src/lib/actions/auth.ts
similarity index 100%
rename from src/lib/auth/action.ts
rename to src/lib/actions/auth.ts
diff --git a/src/lib/actions/index.ts b/src/lib/actions/index.ts
new file mode 100644
index 0000000..aadee9f
--- /dev/null
+++ b/src/lib/actions/index.ts
@@ -0,0 +1,3 @@
+export * from './auth';
+export * from './roles';
+export * from './users';
diff --git a/src/lib/actions/roles.ts b/src/lib/actions/roles.ts
new file mode 100644
index 0000000..201fa0d
--- /dev/null
+++ b/src/lib/actions/roles.ts
@@ -0,0 +1,15 @@
+'use server';
+
+import { and, desc, ilike } from 'drizzle-orm';
+
+import { RoleSearch } from '@/lib/schema';
+
+import { database, role } from '@/database';
+
+export async function getRoles(search: RoleSearch) {
+ return await database
+ .select()
+ .from(role)
+ .where(and(search.name ? ilike(role.name, search.name) : undefined))
+ .orderBy(desc(role.createdAt));
+}
diff --git a/src/lib/actions/users.ts b/src/lib/actions/users.ts
new file mode 100644
index 0000000..5989536
--- /dev/null
+++ b/src/lib/actions/users.ts
@@ -0,0 +1,29 @@
+'use server';
+
+import { and, desc, eq, ilike, inArray } from 'drizzle-orm';
+
+import { UserSearch } from '@/lib/schema';
+
+import { database, role, user } from '@/database';
+
+export async function getUsers(search: UserSearch) {
+ return await database
+ .select({
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ roleName: role.name,
+ })
+ .from(user)
+ .fullJoin(role, eq(user.roleId, role.id))
+ .where(
+ and(
+ search.name ? ilike(user.name, search.name) : undefined,
+ search.email ? eq(user.email, search.email) : undefined,
+ search.roleIds
+ ? inArray(user.roleId, search.roleIds)
+ : undefined,
+ ),
+ )
+ .orderBy(desc(user.createdAt));
+}
diff --git a/src/lib/auth/account.ts b/src/lib/auth/account.ts
index 677d456..31e68e4 100644
--- a/src/lib/auth/account.ts
+++ b/src/lib/auth/account.ts
@@ -12,11 +12,11 @@ export async function createUser(
data: CreateUserProps,
type: 'google' | 'github',
) {
- const userRole = await database.query.role.findFirst({
+ const memberRole = await database.query.role.findFirst({
where: eq(role.name, 'user'),
});
- if (!userRole) {
+ if (!memberRole) {
throw new Error('User role not found');
}
@@ -26,7 +26,7 @@ export async function createUser(
accountType: type,
accountId: data.accountId,
email: data.email,
- roleId: userRole.id,
+ roleId: memberRole.id,
name: data.name,
avatar: data.avatar,
})
diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts
index d5ec15f..45cfaf1 100644
--- a/src/lib/auth/index.ts
+++ b/src/lib/auth/index.ts
@@ -4,8 +4,8 @@ import { Lucia } from 'lucia';
import { cookies } from 'next/headers';
import { cache } from 'react';
+import type { User as DatabaseUser } from '@/database';
import { adapter } from '@/database';
-import type { User as DatabaseUser } from '@/database/schema';
import env from '@/env';
export * from './account';
@@ -20,7 +20,7 @@ export const lucia = new Lucia(adapter, {
getUserAttributes: (attribute) => {
return {
id: attribute.id,
- role: attribute.roleId,
+ roleId: attribute.roleId,
name: attribute.name,
};
},
@@ -29,10 +29,15 @@ export const lucia = new Lucia(adapter, {
declare module 'lucia' {
interface Register {
Lucia: typeof lucia;
- DatabaseUserAttributes: DatabaseUser;
+ DatabaseUserAttributes: DatabaseUserAttributes;
}
}
+export type DatabaseUserAttributes = Pick<
+ DatabaseUser,
+ 'id' | 'roleId' | 'name'
+>;
+
export const validateRequest = cache(
async (): Promise<
{ user: User; session: Session } | { user: null; session: null }
diff --git a/src/lib/auth/session.ts b/src/lib/auth/session.ts
index 0da6e15..d0cadf6 100644
--- a/src/lib/auth/session.ts
+++ b/src/lib/auth/session.ts
@@ -6,7 +6,7 @@ import 'server-only';
import { lucia, validateRequest } from '@/lib/auth';
import { AuthenticationError } from '@/lib/error';
-import { database, role, User } from '@/database';
+import { database, User } from '@/database';
export const getCurrentUser = cache(async () => {
const { user } = await validateRequest();
@@ -24,21 +24,14 @@ export const assertAuthenticated = async () => {
return user;
};
-export const checkRole = async () => {
+export const checkAdmin = async () => {
const user = await assertAuthenticated();
- const [admin] = await database
- .select({
- name: role.name,
- })
- .from(role)
- .where(eq(role.id, user?.role));
-
- return admin?.name !== 'admin';
- // if (!isAdmin) {
- // throw new AuthorizationError();
- // }
- // return isAdmin;
+ const adminRole = await database.query.role.findFirst({
+ where: (role) => eq(role.id, user?.roleId),
+ });
+
+ return adminRole?.name !== 'admin';
};
export async function setSession(user: User) {
diff --git a/src/lib/features/README.md b/src/lib/features/README.md
deleted file mode 100644
index efcc379..0000000
--- a/src/lib/features/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Every logic is here
diff --git a/src/lib/features/users.ts b/src/lib/features/users.ts
new file mode 100644
index 0000000..01abce4
--- /dev/null
+++ b/src/lib/features/users.ts
@@ -0,0 +1,22 @@
+'use server';
+
+import { eq } from 'drizzle-orm';
+
+import { database, ModifiedUser, user as userTable } from '@/database';
+
+export async function editUser(userId: string, data: Partial) {
+ const [selectedUser] = await database
+ .select({ id: userTable.id })
+ .from(userTable)
+ .where(eq(userTable.id, userId));
+
+ if (!selectedUser) {
+ throw new Error('User not found');
+ }
+
+ await database
+ .update(userTable)
+ .set({ ...data })
+ .where(eq(userTable.id, selectedUser.id))
+ .execute();
+}
diff --git a/src/lib/queries/roles.query.ts b/src/lib/queries/roles.query.ts
new file mode 100644
index 0000000..c803e46
--- /dev/null
+++ b/src/lib/queries/roles.query.ts
@@ -0,0 +1,11 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { getRoles } from '@/lib/actions';
+import { RoleSearch } from '@/lib/schema';
+
+export function useGetRoles(search: RoleSearch) {
+ return useQuery({
+ queryKey: ['roles', search],
+ queryFn: () => getRoles(search),
+ });
+}
diff --git a/src/lib/queries/users.query.ts b/src/lib/queries/users.query.ts
new file mode 100644
index 0000000..27b9747
--- /dev/null
+++ b/src/lib/queries/users.query.ts
@@ -0,0 +1,11 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { getUsers } from '@/lib/actions';
+import { UserSearch } from '@/lib/schema';
+
+export function useGetUsers(search: UserSearch) {
+ return useQuery({
+ queryKey: ['users', search],
+ queryFn: () => getUsers(search),
+ });
+}
diff --git a/src/lib/schema/index.ts b/src/lib/schema/index.ts
new file mode 100644
index 0000000..e525192
--- /dev/null
+++ b/src/lib/schema/index.ts
@@ -0,0 +1,2 @@
+export * from './roles.schema';
+export * from './users.schema';
diff --git a/src/lib/schema/roles.schema.ts b/src/lib/schema/roles.schema.ts
new file mode 100644
index 0000000..ef3cbbf
--- /dev/null
+++ b/src/lib/schema/roles.schema.ts
@@ -0,0 +1,7 @@
+import { z } from 'zod';
+
+export const RoleSearchSchema = z.object({
+ name: z.string().optional(),
+});
+
+export type RoleSearch = z.infer;
diff --git a/src/lib/schema/users.schema.ts b/src/lib/schema/users.schema.ts
new file mode 100644
index 0000000..544fd89
--- /dev/null
+++ b/src/lib/schema/users.schema.ts
@@ -0,0 +1,9 @@
+import { z } from 'zod';
+
+export const UserSearchSchema = z.object({
+ name: z.string().optional(),
+ email: z.string().optional(),
+ roleIds: z.array(z.string()).optional(),
+});
+
+export type UserSearch = z.infer;