-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #196 from FalkorDB/users
fix #191 Add Users view
- Loading branch information
Showing
14 changed files
with
405 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export interface User{ | ||
username: string | ||
password?: string | ||
role?: string | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { NextRequest, NextResponse } from "next/server"; | ||
import { getClient } from "@/app/api/auth/[...nextauth]/options"; | ||
|
||
const ROLE = new Map<string, string[]>( | ||
[ | ||
["Admin", ["on", "~*", "&*", "+@all"]], | ||
["Read-Write", ["on", "~*", "resetchannels", "-@all", "+graph.query", "+graph.profile", "+graph.explain", "+graph.list", "+ping"]], | ||
["Read-Only", ["on", "~*", "resetchannels", "-@all", "+graph.ro_query", "+graph.explain", "+graph.list", "+ping"]] | ||
] | ||
) | ||
|
||
interface User { | ||
username: string | ||
password: string | ||
role: string | ||
} | ||
|
||
// eslint-disable-next-line import/prefer-default-export | ||
export async function GET() { | ||
|
||
const client = await getClient() | ||
if (client instanceof NextResponse) { | ||
return client | ||
} | ||
try { | ||
const list = await client.connection.aclList() | ||
const users = list | ||
.map((userACL: string) => userACL.split(" ")) | ||
.filter((userDetails: string[]) => userDetails.length > 1 && userDetails[0] === "user") | ||
.map((userDetails: string[]) => { | ||
// find key in ROLE that has a value equals to userDetails by comaring each array | ||
const role = Array | ||
.from(ROLE.entries()) | ||
.find(([, value]) => { | ||
const reversed = value.slice(1).reverse() | ||
return reversed.every((val, index) => userDetails[userDetails.length - 1 - index] === val) | ||
}) | ||
|
||
return { | ||
username: userDetails[1], | ||
role: role ? role[0] : "Unknown" | ||
} | ||
}) | ||
|
||
return NextResponse.json({ result: { users } }, { status: 200 }) | ||
} catch (err: unknown) { | ||
return NextResponse.json({ message: (err as Error).message }, { status: 400 }) | ||
} | ||
} | ||
|
||
// eslint-disable-next-line import/prefer-default-export | ||
export async function POST(req: NextRequest) { | ||
|
||
const client = await getClient() | ||
if (client instanceof NextResponse) { | ||
return client | ||
} | ||
|
||
const { username, password, role } = await req.json() as User | ||
|
||
const roleValue = ROLE.get(role) | ||
try { | ||
if (!username || !password || !roleValue) throw (new Error("Missing parameters")) | ||
|
||
try { | ||
const user = await client.connection.aclGetUser(username) | ||
|
||
if (user) { | ||
return NextResponse.json({ message: `User ${username} already exists` }, { status: 409 }) | ||
} | ||
} catch (err: unknown) { | ||
// Just a workaround for https://github.com/redis/node-redis/issues/2745 | ||
} | ||
|
||
await client.connection.aclSetUser(username, roleValue.concat(`>${password}`)) | ||
return NextResponse.json( | ||
{ message: "User created" }, | ||
{ | ||
status: 201, | ||
headers: { | ||
location: `/api/db/user/${username}` | ||
} | ||
} | ||
) | ||
} catch (err: unknown) { | ||
return NextResponse.json({ message: (err as Error).message }, { status: 400 }) | ||
} | ||
} | ||
|
||
// eslint-disable-next-line import/prefer-default-export | ||
export async function DELETE(req: NextRequest) { | ||
|
||
const client = await getClient() | ||
if (client instanceof NextResponse) { | ||
return client | ||
} | ||
|
||
const { users } = await req.json() | ||
try { | ||
await Promise.all(users.map(async (user: User) => { | ||
await client.connection.aclDelUser(user.username) | ||
})) | ||
return NextResponse.json({ message: "Users deleted" }, { status: 200 }) | ||
} catch (err: unknown) { | ||
return NextResponse.json({ message: (err as Error).message }, { status: 400 }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { Button } from "@/components/ui/button"; | ||
import { createRef, useState } from "react" | ||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; | ||
import { Input } from "@/components/ui/input"; | ||
import { Label } from "@/components/ui/label"; | ||
import { toast } from "@/components/ui/use-toast"; | ||
import { securedFetch } from "@/lib/utils"; | ||
import Combobox from "../components/combobox"; | ||
|
||
|
||
export default function AddUser() { | ||
const usernameInputRef = createRef<HTMLInputElement>() | ||
const passwordInputRef = createRef<HTMLInputElement>() | ||
const [selectedValue, setSelectedValue] = useState("Read-Only") | ||
|
||
const addUser = async (event: React.FormEvent<HTMLFormElement>) => { | ||
event.preventDefault(); | ||
const username = usernameInputRef.current?.value; | ||
const password = passwordInputRef.current?.value; | ||
if (!username || !password) { | ||
return; | ||
} | ||
const response = await securedFetch('/api/user/', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: JSON.stringify({ username, password, role: selectedValue}) | ||
}) | ||
|
||
if (response.ok) { | ||
toast({ | ||
title: "Success", | ||
description: "User created", | ||
}); | ||
} | ||
}; | ||
return ( | ||
<Dialog> | ||
<DialogTrigger asChild> | ||
<Button variant="outline">Add new user</Button> | ||
</DialogTrigger> | ||
<DialogContent className="sm:max-w-[425px]"> | ||
<form onSubmit={addUser} > | ||
<DialogHeader> | ||
<DialogTitle>Add User</DialogTitle> | ||
<DialogDescription> | ||
Pick a new username to add a new user. | ||
</DialogDescription> | ||
</DialogHeader> | ||
<div className="grid gap-4 py-4"> | ||
<div className="grid grid-cols-4 items-center gap-4"> | ||
<Label htmlFor="username" className="text-right"> | ||
Username | ||
</Label> | ||
<Input id="username" ref={usernameInputRef} className="col-span-3" required /> | ||
</div> | ||
<div className="grid grid-cols-4 items-center gap-4"> | ||
<Label htmlFor="password" className="text-right"> | ||
Password | ||
</Label> | ||
<Input id="password" type="password" ref={passwordInputRef} className="col-span-3" required /> | ||
</div> | ||
<div className="grid grid-cols-4 items-center gap-4"> | ||
<Label htmlFor="role" className="text-right"> | ||
Role | ||
</Label> | ||
<Combobox type="Role" options={["Admin", "Read-Write", "Read-Only"]} selectedValue={selectedValue} setSelectedValue={setSelectedValue} /> | ||
</div> | ||
</div> | ||
<DialogFooter> | ||
<DialogClose asChild> | ||
<Button type="submit" variant="secondary">Add</Button> | ||
</DialogClose> | ||
</DialogFooter> | ||
</form> | ||
</DialogContent> | ||
</Dialog> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; | ||
import { Button } from "@/components/ui/button"; | ||
import { toast } from "@/components/ui/use-toast"; | ||
import { User } from "@/app/api/user/model"; | ||
import { securedFetch } from "@/lib/utils"; | ||
|
||
interface DeleteUserProps { | ||
users: User[] | ||
selectedRows: boolean[] | ||
} | ||
|
||
export default function DeleteUser({ users, selectedRows} : DeleteUserProps) { | ||
|
||
const selected = users.filter((_: User, index: number) => selectedRows[index]) | ||
|
||
const deleteSelected = async () => { | ||
if (selected.length === 0) return | ||
|
||
const response = await securedFetch(`/api/user/`, { | ||
method: 'DELETE', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: JSON.stringify({ users: selected }) | ||
}) | ||
|
||
if (response.ok) { | ||
toast({ | ||
title: "Success", | ||
description: "Users deleted", | ||
}) | ||
} | ||
} | ||
|
||
return ( | ||
<AlertDialog> | ||
<AlertDialogTrigger disabled={selected.length === 0} asChild> | ||
<Button variant="outline">Delete selected users</Button> | ||
</AlertDialogTrigger> | ||
<AlertDialogContent> | ||
<AlertDialogHeader> | ||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> | ||
<AlertDialogDescription> | ||
This action cannot be undone. This will permanently delete the selected users. | ||
</AlertDialogDescription> | ||
</AlertDialogHeader> | ||
<AlertDialogFooter> | ||
<AlertDialogCancel>Cancel</AlertDialogCancel> | ||
<AlertDialogAction onClick={deleteSelected} >Continue</AlertDialogAction> | ||
</AlertDialogFooter> | ||
</AlertDialogContent> | ||
</AlertDialog> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
"use client"; | ||
|
||
import React from "react"; | ||
import useSWR from "swr"; | ||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; | ||
import { Checkbox } from "@/components/ui/checkbox"; | ||
import { CheckedState } from "@radix-ui/react-checkbox"; | ||
import { User } from "@/app/api/user/model"; | ||
import { securedFetch } from "@/lib/utils"; | ||
import DeleteUser from "./DeleteUser"; | ||
import AddUser from "./AddUser"; | ||
|
||
const fetcher = async (url: string) => { | ||
const response = await securedFetch(url, { | ||
method: 'GET', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
} | ||
}) | ||
|
||
if (response.ok) { | ||
const data = await response.json() | ||
return data.result | ||
} | ||
return { users: [] } | ||
} | ||
|
||
// Shows the details of a current database connection | ||
export default function Page() { | ||
|
||
const { data } = useSWR(`/api/user/`, fetcher, { refreshInterval: 1000 }) | ||
const [selectedRows, setSelectedRows] = React.useState<boolean[]>([]) | ||
|
||
const users: User[] = (data && data.users) || [] | ||
if (users.length !== selectedRows.length) { | ||
setSelectedRows(new Array(users.length).fill(false)) | ||
} | ||
|
||
// Handle the select/unselect a checkbox | ||
const onSelect = (checked: CheckedState, index: number) => { | ||
setSelectedRows((prev) => { | ||
const next = [...prev] | ||
next[index] = checked === true | ||
return next | ||
}) | ||
} | ||
|
||
return ( | ||
<div className="w-full h-full p-2 flex flex-col space-y-4"> | ||
<h1 className="text-2xl font-bold">Users</h1> | ||
<div className="space-y-2 list-disc border rounded-lg border-gray-300 p-2"> | ||
<div className="flex flex-row space-x-2"> | ||
<AddUser /> | ||
<DeleteUser users={users} selectedRows={selectedRows} /> | ||
</div> | ||
<Table> | ||
<TableHeader> | ||
<TableRow> | ||
<TableHead className="w-[32px]"> | ||
<Checkbox id="select-all" onCheckedChange={(checked: CheckedState) => setSelectedRows(new Array(users.length).fill(checked))} /> | ||
</TableHead> | ||
<TableHead>Username</TableHead> | ||
<TableHead>Role</TableHead> | ||
</TableRow> | ||
</TableHeader> | ||
<TableBody> | ||
{users.map((user: User, index: number) => ( | ||
<TableRow key={user.username}> | ||
<TableCell> | ||
<Checkbox | ||
checked={selectedRows[index]} | ||
onCheckedChange={(checked) => onSelect(checked, index)} /> | ||
</TableCell> | ||
<TableCell>{user.username}</TableCell> | ||
<TableCell>{user.role}</TableCell> | ||
</TableRow> | ||
))} | ||
</TableBody> | ||
</Table> | ||
</div> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.