mirror of
https://github.com/rybbit-io/rybbit.git
synced 2025-05-11 12:25:36 +02:00
add new user (#18)
This commit is contained in:
parent
8ada9b8c16
commit
61d64c1293
17 changed files with 586 additions and 42 deletions
1
client/package-lock.json
generated
1
client/package-lock.json
generated
|
@ -18,6 +18,7 @@
|
|||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@radix-ui/themes": "^3.2.0",
|
||||
"@tanstack/react-query": "^5.64.2",
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@radix-ui/themes": "^3.2.0",
|
||||
"@tanstack/react-query": "^5.64.2",
|
||||
|
|
|
@ -4,10 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { authClient } from "../../lib/auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { GearSix, User } from "@phosphor-icons/react";
|
||||
import { GearSix, User, Users as Users_ } from "@phosphor-icons/react";
|
||||
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Account } from "./account/Account";
|
||||
import { Users } from "./users/Users";
|
||||
import { Settings } from "./settings/settings";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const session = authClient.useSession();
|
||||
|
@ -36,38 +38,21 @@ export default function SettingsPage() {
|
|||
<GearSix size={16} weight="bold" />
|
||||
Settings
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedTab === "users" ? "default" : "ghost"}
|
||||
onClick={() => setSelectedTab("users")}
|
||||
className="justify-start"
|
||||
>
|
||||
<Users_ size={16} weight="bold" />
|
||||
Users
|
||||
</Button>
|
||||
</div>
|
||||
{selectedTab === "account" && session.data?.user && (
|
||||
<Account session={session} />
|
||||
)}
|
||||
{selectedTab === "users" && <Users />}
|
||||
{selectedTab === "settings" && <Settings />}
|
||||
</div>
|
||||
{/* <div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Settings</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage your analytics preferences and configurations.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await authClient.signOut();
|
||||
router.push("/login");
|
||||
}}
|
||||
>
|
||||
Signout
|
||||
</Button>
|
||||
|
||||
<div className="flex gap-2 flex-col">
|
||||
<Button>Test</Button>
|
||||
<Button variant={"destructive"}>Test</Button>
|
||||
<Button variant={"accent"}>Test</Button>
|
||||
<Button variant={"warning"}>Test</Button>
|
||||
<Button variant={"ghost"}>Test</Button>
|
||||
<Button variant={"link"}>Test</Button>
|
||||
<Button variant={"outline"}>Test</Button>
|
||||
<Button variant={"secondary"}>Test</Button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
44
client/src/app/settings/settings/settings.tsx
Normal file
44
client/src/app/settings/settings/settings.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../../components/ui/card";
|
||||
|
||||
export function Settings() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="p-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{/* <div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
className="w-60"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={({ target }) => setUsername(target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
className="w-60"
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={({ target }) => setEmail(target.value)}
|
||||
/>
|
||||
</div> */}
|
||||
</CardContent>
|
||||
{/* <CardFooter className="flex justify-end">
|
||||
<Button variant={"accent"} disabled={!hasChanges}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter> */}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
154
client/src/app/settings/users/AddUser.tsx
Normal file
154
client/src/app/settings/users/AddUser.tsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
import { useState } from "react";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { authClient } from "../../../lib/auth";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../../components/ui/dialog";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "../../../components/ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Label } from "../../../components/ui/label";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { Switch } from "../../../components/ui/switch";
|
||||
|
||||
export function AddUser() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const passwordsMatch = password === confirmPassword;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setError("");
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("New password and confirm password do not match");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await authClient.admin.createUser({
|
||||
password,
|
||||
email,
|
||||
name,
|
||||
role: isAdmin ? "admin" : "user",
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
setError(String(response.error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
setError(String(error));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setPassword("");
|
||||
setConfirmPassword("");
|
||||
setName("");
|
||||
setEmail("");
|
||||
setError("");
|
||||
setIsAdmin(false);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Add User</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Name"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="is-admin"
|
||||
checked={isAdmin}
|
||||
onCheckedChange={() => setIsAdmin(!isAdmin)}
|
||||
/>
|
||||
<Label htmlFor="is-admin">Admin</Label>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="password"> Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-1.5">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error Creating User</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => setOpen(false)}
|
||||
variant={"ghost"}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
!password || !confirmPassword || !passwordsMatch || !name
|
||||
}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
79
client/src/app/settings/users/Users.tsx
Normal file
79
client/src/app/settings/users/Users.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
"use client";
|
||||
|
||||
import { DateTime } from "luxon";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../../components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import { useListUsers } from "../../../hooks/api";
|
||||
import { AddUser } from "./AddUser";
|
||||
|
||||
export function Users() {
|
||||
const { data: users, refetch } = useListUsers();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="p-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex justify-between items-center">
|
||||
Users
|
||||
<AddUser />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Table>
|
||||
{/* <TableCaption>A list of your recent invoices.</TableCaption> */}
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users?.data?.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>{user.role || "admin"}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
{DateTime.fromISO(user.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.name !== "admin" && (
|
||||
<Button variant={"destructive"} size={"sm"}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{/* <TableRow>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>Credit Card</TableCell>
|
||||
</TableRow> */}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
{/* <CardFooter className="flex justify-end">
|
||||
<Button variant={"accent"} disabled={!hasChanges}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter> */}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,33 @@
|
|||
"use client";
|
||||
|
||||
import { GearSix } from "@phosphor-icons/react/dist/ssr";
|
||||
import Link from "next/link";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "./ui/navigation-menu";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "./ui/dropdown-menu";
|
||||
import { authClient } from "../lib/auth";
|
||||
import { Button } from "./ui/button";
|
||||
import { User } from "@phosphor-icons/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function TopBar() {
|
||||
const session = authClient.useSession();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="flex pt-2 items-center w-full pb-4 bg-neutral-900 justify-center">
|
||||
<div className="flex items-center justify-between max-w-6xl flex-1">
|
||||
|
@ -11,7 +36,18 @@ export function TopBar() {
|
|||
🐸 Frogstats
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="ml-auto flex items-center space-x-6 text-sm">
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/" legacyBehavior passHref>
|
||||
<NavigationMenuLink className="text-neutral-100 flex items-center gap-1 text-sm font-medium">
|
||||
Websites
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
{/* <nav className="ml-auto flex items-center space-x-6 text-sm">
|
||||
<Link href="/" className="text-neutral-100">
|
||||
Websites
|
||||
</Link>
|
||||
|
@ -23,7 +59,28 @@ export function TopBar() {
|
|||
Settings
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
</nav> */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1 text-sm font-medium">
|
||||
<User size={16} weight="bold" />
|
||||
{session.data?.user.name}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Link href="/settings" legacyBehavior passHref>
|
||||
Settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await authClient.signOut();
|
||||
router.push("/login");
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
29
client/src/components/ui/switch.tsx
Normal file
29
client/src/components/ui/switch.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=unchecked]:bg-neutral-800",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0 dark:bg-neutral-950"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
127
client/src/components/ui/table.tsx
Normal file
127
client/src/components/ui/table.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn("[&_tr]:border-b-neutral-600", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-neutral-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-neutral-800/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b border-b-neutral-700 transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-neutral-500 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] dark:text-neutral-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-4 text-sm text-neutral-500 dark:text-neutral-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
|
@ -173,3 +173,18 @@ export function deleteSite(siteId: string) {
|
|||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export type ListUsersResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
|
||||
export function useListUsers() {
|
||||
return useGenericQuery<ListUsersResponse>("list-users");
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { usernameClient } from "better-auth/client/plugins";
|
||||
import { usernameClient, adminClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
|
||||
plugins: [usernameClient()],
|
||||
plugins: [usernameClient(), adminClient()],
|
||||
fetchOptions: {
|
||||
credentials: "include",
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ module.exports = {
|
|||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
border: "red",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
|
|
|
@ -31,7 +31,6 @@ export async function addSite(
|
|||
}
|
||||
try {
|
||||
await sql`INSERT INTO sites (domain, name, created_by) VALUES (${domain}, ${name}, ${session?.user.id})`;
|
||||
|
||||
await loadAllowedDomains();
|
||||
return reply.status(200).send();
|
||||
} catch (err) {
|
||||
|
|
23
server/src/api/listUsers.ts
Normal file
23
server/src/api/listUsers.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { sql } from "../db/postgres/postgres.js";
|
||||
|
||||
type ListUsersResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
|
||||
export async function listUsers(_: FastifyRequest, res: FastifyReply) {
|
||||
try {
|
||||
const users = await sql<ListUsersResponse[]>`SELECT * FROM "user"`;
|
||||
return res.send({ data: users });
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
return res.status(500).send({ error: "Failed to fetch users" });
|
||||
}
|
||||
}
|
|
@ -21,11 +21,13 @@ export async function initializePostgres() {
|
|||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
"id" text not null primary key,
|
||||
"name" text not null,
|
||||
"username" text not null unique,
|
||||
"email" text not null unique,
|
||||
"emailVerified" boolean not null,
|
||||
"image" text,
|
||||
"createdAt" timestamp not null,
|
||||
"updatedAt" timestamp not null
|
||||
"updatedAt" timestamp not null,
|
||||
"role" text not null default 'user'
|
||||
);
|
||||
`,
|
||||
|
||||
|
@ -120,6 +122,8 @@ export async function initializePostgres() {
|
|||
});
|
||||
}
|
||||
|
||||
await sql`UPDATE "user" SET "role" = 'admin' WHERE username = 'admin'`;
|
||||
|
||||
console.log("Tables created successfully.");
|
||||
} catch (err) {
|
||||
console.error("Error creating tables:", err);
|
||||
|
|
|
@ -20,12 +20,12 @@ import { getPages } from "./api/getPages.js";
|
|||
import { getPageViews } from "./api/getPageViews.js";
|
||||
import { getReferrers } from "./api/getReferrers.js";
|
||||
import { initializeClickhouse } from "./db/clickhouse/clickhouse.js";
|
||||
import { initializePostgres, sql } from "./db/postgres/postgres.js";
|
||||
import { initializePostgres } from "./db/postgres/postgres.js";
|
||||
import { cleanupOldSessions } from "./db/postgres/session-cleanup.js";
|
||||
import { auth, initAuth } from "./lib/auth.js";
|
||||
import { allowList, loadAllowedDomains } from "./lib/allowedDomains.js";
|
||||
import { auth } from "./lib/auth.js";
|
||||
import { mapHeaders } from "./lib/betterAuth.js";
|
||||
import { allowList } from "./lib/allowedDomains.js";
|
||||
import { loadAllowedDomains } from "./lib/allowedDomains.js";
|
||||
import { listUsers } from "./api/listUsers.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
@ -128,9 +128,11 @@ server.get("/pages", getPages);
|
|||
server.get("/referrers", getReferrers);
|
||||
server.get("/pageviews", getPageViews);
|
||||
|
||||
// Administrative
|
||||
server.post("/add-site", addSite);
|
||||
server.post("/delete-site/:id", deleteSite);
|
||||
server.get("/get-sites", getSites);
|
||||
server.get("/list-users", listUsers);
|
||||
|
||||
// Track pageview endpoint
|
||||
server.post("/track/pageview", trackPageView);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { betterAuth } from "better-auth";
|
||||
import { username } from "better-auth/plugins";
|
||||
import { username, admin } from "better-auth/plugins";
|
||||
import dotenv from "dotenv";
|
||||
import pg from "pg";
|
||||
|
||||
|
@ -7,7 +7,31 @@ dotenv.config();
|
|||
|
||||
type AuthType = ReturnType<typeof betterAuth> | null;
|
||||
|
||||
export let auth: AuthType | null = null;
|
||||
export let auth: AuthType | null = betterAuth({
|
||||
basePath: "/auth",
|
||||
database: new pg.Pool({
|
||||
host: process.env.POSTGRES_HOST || "postgres",
|
||||
port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
|
||||
database: process.env.POSTGRES_DB,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
deleteUser: {
|
||||
enabled: true,
|
||||
},
|
||||
plugins: [username(), admin()],
|
||||
trustedOrigins: [],
|
||||
advanced: {
|
||||
useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev
|
||||
defaultCookieAttributes: {
|
||||
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
||||
path: "/",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const initAuth = (allowList: string[]) => {
|
||||
auth = betterAuth({
|
||||
|
@ -25,7 +49,7 @@ export const initAuth = (allowList: string[]) => {
|
|||
deleteUser: {
|
||||
enabled: true,
|
||||
},
|
||||
plugins: [username()],
|
||||
plugins: [username(), admin()],
|
||||
trustedOrigins: allowList,
|
||||
advanced: {
|
||||
useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue