Added impersonation for admins

This commit is contained in:
Tom 2024-01-04 21:57:32 +00:00
parent 320c826684
commit 8f7f18e069
25 changed files with 494 additions and 131 deletions

View File

@ -0,0 +1,91 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUser from "@/lib/fetch-user";
export async function GET(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return new NextResponse("Unauthorized", { status: 401 });
}
const impersonation = await db.impersonation.findFirst({
where: {
sourceId: user.id
}
});
return NextResponse.json(impersonation);
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function POST(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return new NextResponse("Unauthorized", { status: 401 });
}
const { targetId } = await req.json();
const impersonation = await db.impersonation.create({
data: {
sourceId: user.id,
targetId
}
});
return NextResponse.json(impersonation);
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function PUT(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return new NextResponse("Unauthorized", { status: 401 });
}
const { targetId } = await req.json();
const impersonation = await db.impersonation.update({
where: {
sourceId: user.id,
},
data: {
targetId
}
});
return NextResponse.json(impersonation);
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function DELETE(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return new NextResponse("Unauthorized", { status: 401 });
}
const impersonation = await db.impersonation.delete({
where: {
sourceId: user.id
}
});
return NextResponse.json(impersonation)
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return new NextResponse("Internal Error" + error, { status: 500 });
}
}

View File

@ -1,12 +1,12 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import { auth } from "@/auth";
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUser from "@/lib/fetch-user";
export async function GET(req: Request) {
try {
return NextResponse.json(await fetchUserUsingAPI(req))
return NextResponse.json(await fetchUser(req))
} catch (error) {
console.log("[ACCOUNT]", error);
return new NextResponse("Internal Error", { status: 500 });

View File

@ -1,10 +1,10 @@
import { db } from "@/lib/db"
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}

View File

@ -1,10 +1,10 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
export async function GET(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}

View File

@ -1,11 +1,11 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import voices from "@/data/tts";
export async function GET(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
@ -26,7 +26,7 @@ export async function GET(req: Request) {
export async function POST(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}

View File

@ -1,10 +1,10 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
export async function GET(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
@ -24,7 +24,7 @@ export async function GET(req: Request) {
export async function POST(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
@ -57,7 +57,7 @@ export async function POST(req: Request) {
export async function DELETE(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}

View File

@ -1,10 +1,10 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
export async function GET(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
@ -24,7 +24,7 @@ export async function GET(req: Request) {
export async function POST(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
@ -35,7 +35,7 @@ export async function POST(req: Request) {
data: {
search,
replace,
userId: user.id as string
userId: user.id
}
});
@ -48,7 +48,7 @@ export async function POST(req: Request) {
export async function PUT(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
@ -61,7 +61,8 @@ export async function PUT(req: Request) {
},
data: {
search,
replace
replace,
userId: user.id
}
});
@ -74,7 +75,7 @@ export async function PUT(req: Request) {
export async function DELETE(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
@ -87,7 +88,7 @@ export async function DELETE(req: Request) {
const filter = await db.ttsWordFilter.delete({
where: {
userId_search: {
userId: user.id as string,
userId: user.id,
search
}
}

View File

@ -1,11 +1,11 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import voices from "@/data/tts";
export async function GET(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
@ -40,7 +40,7 @@ export async function GET(req: Request) {
export async function POST(req: Request) {
try {
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}

View File

@ -1,12 +1,17 @@
import { db } from "@/lib/db"
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { NextResponse } from "next/server";
export async function GET(req: Request, { params } : { params: { id: string } }) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
let id = req.headers?.get('x-api-key')
if (id == null) {
return NextResponse.json(null);
return NextResponse.json(null);
}
const tokens = await db.apiKey.findFirst({
@ -18,15 +23,19 @@ export async function GET(req: Request, { params } : { params: { id: string } })
return NextResponse.json(tokens);
} catch (error) {
console.log("[TOKEN/GET]", error);
return new NextResponse("Internal Error", { status: 500});
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function DELETE(req: Request, { params } : { params: { id: string } }) {
try {
const { id } = params
const user = await fetchUserUsingAPI(req)
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { id } = params
const token = await db.apiKey.delete({
where: {
id,
@ -37,6 +46,6 @@ export async function DELETE(req: Request, { params } : { params: { id: string }
return NextResponse.json(token);
} catch (error) {
console.log("[TOKEN/DELETE]", error);
return new NextResponse("Internal Error", { status: 500});
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -1,10 +1,10 @@
import { db } from "@/lib/db"
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { NextResponse } from "next/server";
export async function GET(req: Request) {
try {
const user = await fetchUserUsingAPI(req);
const user = await fetchUserWithImpersonation(req);
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}

View File

@ -1,13 +1,18 @@
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
let { userId, label } = await req.json();
if (userId == null) {
const user = await fetchUserUsingAPI(req);
const user = await fetchUserWithImpersonation(req);
if (user != null) {
userId = user.id;
}
@ -31,9 +36,13 @@ export async function POST(req: Request) {
export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
let { id } = await req.json();
const user = await fetchUserUsingAPI(req);
if (!id || !user) {
if (!id) {
return NextResponse.json(null)
}

View File

@ -1,4 +1,4 @@
import fetchUserUsingAPI from "@/lib/validate-api";
import fetchUser from "@/lib/fetch-user";
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
@ -8,7 +8,7 @@ export async function GET(req: Request) {
let userId = searchParams.get('userId')
if (userId == null) {
const user = await fetchUserUsingAPI(req);
const user = await fetchUser(req);
if (user != null) {
userId = user.id as string;
}

41
app/api/users/route.ts Normal file
View File

@ -0,0 +1,41 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUser from "@/lib/fetch-user";
export async function GET(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
const qn = searchParams.get('qn') as string
const id = searchParams.get('id') as string
if (qn) {
const users = await db.user.findMany({
where: {
name: {
contains: qn
}
}
})
return NextResponse.json(users)
}
if (id) {
const users = await db.user.findUnique({
where: {
id: id
}
})
return NextResponse.json(users)
}
const users = await db.user.findMany();
return NextResponse.json(users)
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -20,7 +20,6 @@ const SettingsPage = () => {
try {
const keys = (await axios.get("/api/tokens")).data ?? {};
setApiKeys(keys)
console.log(keys);
} catch (error) {
console.log("ERROR", error)
}
@ -49,20 +48,6 @@ const SettingsPage = () => {
}
}
useEffect(() => {
const fetchData = async () => {
try {
const keys = (await axios.get("/api/tokens")).data;
setApiKeys(keys)
console.log(keys);
} catch (error) {
console.log("ERROR", error)
}
};
fetchData().catch(console.error);
}, [apiKeyViewable]);
return (
<div>
<div className="px-10 py-5 mx-5 my-10">

View File

@ -13,8 +13,7 @@ import { useForm } from "react-hook-form";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { DropdownMenu, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from "@/components/ui/dropdown-menu";
import { DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Label } from "@/components/ui/label";
@ -58,7 +57,6 @@ const TTSFiltersPage = () => {
try {
const replacementData = await axios.get("/api/settings/tts/filter/words")
console.log(replacementData.data)
setReplacements(replacementData.data ?? [])
} catch (e) {
console.log("ERROR", e)
@ -112,8 +110,9 @@ const TTSFiltersPage = () => {
const onReplaceAdd = async () => {
await axios.post("/api/settings/tts/filter/words", { search, replace })
.then(d => {
replacements.push(d.data)
replacements.push({ id: d.data.id, search: d.data.search, replace: d.data.replace, userId: d.data.userId })
setReplacements(replacements)
setSearch("")
}).catch(e => {
// TODO: handle already exist.
console.log("LOGGED:", e)
@ -135,7 +134,9 @@ const TTSFiltersPage = () => {
const onReplaceDelete = async (id: string) => {
await axios.delete("/api/settings/tts/filter/words?id=" + id)
.then(d => {
setReplacements(replacements.filter(r => r.id != id))
const r = replacements.filter(r => r.id != d.data.id)
setReplacements(r)
console.log(r)
}).catch(e => {
// TODO: handle does not exist.
console.log("LOGGED:", e)
@ -166,9 +167,9 @@ const TTSFiltersPage = () => {
<div>
<div className="text-2xl text-center pt-[50px]">TTS Filters</div>
<div className="px-10 py-5 w-full h-full flex-grow inset-y-1/2">
<div className="">
<div>
{userTags.map((user, index) => (
<div className="flex w-full items-start justify-between rounded-md border px-4 py-1">
<div key={user.username + "-tags"} className="flex w-full items-start justify-between rounded-md border px-4 py-1">
<p className="text-base font-medium">
<span className="mr-2 rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground">
{user.tag}
@ -200,7 +201,7 @@ const TTSFiltersPage = () => {
<CommandGroup>
{tags.map((tag) => (
<CommandItem
key={tag}
key={user.username + "-tag"}
value={tag}
onSelect={(value) => {
onAddExtended({ username: userTags[index].username, tag: value}, false)
@ -216,7 +217,7 @@ const TTSFiltersPage = () => {
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onDelete} className="text-red-600">
<DropdownMenuItem key={user.username + "-delete"} onClick={onDelete} className="text-red-600">
<Trash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
@ -235,7 +236,7 @@ const TTSFiltersPage = () => {
control={usernameFilteredForm.control}
name="username"
render={({ field }) => (
<FormItem className="flex-grow">
<FormItem key={"new-username"} className="flex-grow">
<FormControl>
<Input id="username" placeholder="Enter a twitch username" {...field} />
</FormControl>
@ -272,6 +273,7 @@ const TTSFiltersPage = () => {
{tags.map((tag) => (
<CommandItem
value={tag}
key={tag + "-tag"}
onSelect={(value) => {
setTag(value)
setOpen(false)
@ -297,7 +299,7 @@ const TTSFiltersPage = () => {
<p className="text-center text-2xl text-white pt-[80px]">Regex Replacement</p>
<div>
{replacements.map((term: { id: string, search: string, replace: string, userId: string }) => (
<div className="flex flex-row w-full items-start justify-between rounded-lg border px-4 py-3 gap-3 mt-[15px]">
<div key={term.id} className="flex flex-row w-full items-start justify-between rounded-lg border px-4 py-3 gap-3 mt-[15px]">
<Input id="search" placeholder={term.search} className="flex" onChange={e => term.search = e.target.value } defaultValue={term.search} />
<Input id="replace" placeholder={term.replace} className="flex" onChange={e => term.replace = e.target.value } defaultValue={term.replace} />
<Button className="bg-blue-500 hover:bg-blue-600 items-center align-middle" onClick={_ => onReplaceUpdate(term)}>

View File

@ -14,7 +14,7 @@ import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import voices from "@/data/tts";
const TTSFiltersPage = () => {
const TTSVoiceFiltersPage = () => {
const { data: session, status } = useSession();
const [loading, setLoading] = useState<boolean>(true)
@ -88,7 +88,7 @@ const TTSFiltersPage = () => {
<CommandGroup>
{voices.map((voice) => (
<CommandItem
key={voice.value}
key={voice.value + "-" + voice.label}
value={voice.value}
onSelect={(currentValue) => {
setValue(Number.parseInt(currentValue))
@ -115,7 +115,7 @@ const TTSFiltersPage = () => {
<p className="text-xl text-center justify-center">Voices Enabled</p>
<div className="grid grid-cols-4 grid-flow-row gap-4 pt-[20px]">
{voices.map((v, i) => (
<div className="h-[30px] row-span-1 col-span-1 align-middle flex items-center justify-center">
<div key={v.label + "-enabled"} className="h-[30px] row-span-1 col-span-1 align-middle flex items-center justify-center">
<Checkbox onClick={() => {
const newVal = enabled ^ (1 << (Number.parseInt(v.value) - 1))
setEnabled(newVal)
@ -132,4 +132,4 @@ const TTSFiltersPage = () => {
);
}
export default TTSFiltersPage;
export default TTSVoiceFiltersPage;

41
auth.ts
View File

@ -5,7 +5,8 @@ import { PrismaAdapter } from "@auth/prisma-adapter"
import { db } from "@/lib/db"
import authConfig from "@/auth.config"
import { getUserById } from "./data/user"
import { UserRole } from "@prisma/client"
import { User, UserRole } from "@prisma/client"
import { getImpersonationById } from "./data/impersonation"
declare module "@auth/core/types" {
@ -14,7 +15,8 @@ declare module "@auth/core/types" {
*/
interface Session {
user: {
role: UserRole
role: UserRole | null
impersonation: User | null
// By default, TypeScript merges new interface properties and overwrite existing ones. In this case, the default session user properties will be overwritten, with the new one defined above. To keep the default session user properties, you need to add them back into the newly declared interface
} & DefaultSession["user"] // To keep the default types
}
@ -23,7 +25,8 @@ declare module "@auth/core/types" {
declare module "@auth/core/jwt" {
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
interface JWT {
role: UserRole
role: UserRole | null
impersonation: User | null
}
}
@ -50,19 +53,47 @@ export const {
if (token.role && session.user) {
session.user.role = token.role
} else {
session.user.role = null
}
if (token.impersonation && session.user) {
session.user.impersonation = token.impersonation
} else {
token.impersonation = null
}
return session
},
async jwt({ token, user, account, profile, isNewUser }) {
async jwt({ token, user, account, profile }) {
if (!token.sub) return token
const existingUser = await getUserById(token.sub)
if (!existingUser) return token
// Update Role
token.role = existingUser.role
// Update Impersonation
const impersonation = await getImpersonationById(existingUser.id)
if (token.role == "ADMIN" && impersonation && impersonation.targetId != existingUser.id) {
const impersonationUser = await getUserById(impersonation.targetId)
if (impersonation) {
token.impersonation = impersonationUser
} else {
token.impersonation = null
}
} else if (impersonation && impersonation.targetId == existingUser.id) {
await db.impersonation.delete({
where: {
sourceId: existingUser.id
}
})
token.impersonation = null
} else {
token.impersonation = null
}
return token
}
},

View File

@ -5,16 +5,116 @@ import * as React from 'react';
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { cn } from "@/lib/utils";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "../ui/button";
import { Check, ChevronsUpDown } from "lucide-react";
import { User } from "@prisma/client";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "../ui/command";
const AdminProfile = () => {
const session = useSession();
const [user, setUser] = useState<{ id: string, username: string }>()
const [impersonation, setImpersonation] = useState<string | null>(null)
const [open, setOpen] = useState(false)
const [users, setUsers] = useState<User[]>([])
useEffect(() => {
const fetch = async (userId: string | undefined) => {
if (!userId) return
await axios.get<User>("/api/users?id=" + userId)
.then(u => {
setImpersonation(u.data?.name)
})
}
console.log(session)
fetch(session?.data?.user?.impersonation?.id)
}, [])
useEffect(() => {
const fetchUsers = async () => {
await axios.get<User[]>("/api/users")
.then((u) => {
setUsers(u.data.filter(x => x.id != session.data?.user.id))
})
}
fetchUsers()
}, [])
const onImpersonationChange = async (userId: string, name: string) => {
if (impersonation) {
if (impersonation == session.data?.user.impersonation?.name) {
await axios.delete("/api/account/impersonate")
.then(() => {
setImpersonation(null)
window.location.reload()
})
} else {
await axios.put("/api/account/impersonate", { targetId: userId })
.then(() => {
setImpersonation(name)
window.location.reload()
})
}
} else {
await axios.post("/api/account/impersonate", { targetId: userId })
.then(() => {
setImpersonation(name)
window.location.reload()
})
}
}
return (
<div className={"px-10 py-6 rounded-md bg-red-300 overflow-hidden wrap m-[10px]"}>
<p className="text-xs text-gray-400">Role:</p>
<p>{session?.data?.user?.role}</p>
<div className={"px-5 py-3 rounded-md bg-red-300 overflow-hidden wrap my-[10px] flex flex-grow flex-col gap-y-3"}>
<div>
<p className="text-xs text-gray-200">Role:</p>
<p>{session?.data?.user?.role}</p>
</div>
<div>
<p className="text-xs text-gray-200">Impersonation:</p>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="flex flex-grow justify-between text-xs">
{impersonation ? impersonation : "Select a user"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search users by name" />
<CommandEmpty>No voices found.</CommandEmpty>
<CommandGroup>
{users.map((user) => (
<CommandItem
key={user.id}
value={user.name ?? undefined}
onSelect={(currentValue) => {
onImpersonationChange(user.id, user.name ?? "")
setOpen(false)
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
user.name == impersonation ? "opacity-100" : "opacity-0"
)}
/>
{user.name}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
);
}

View File

@ -10,28 +10,26 @@ const SettingsNavigation = async () => {
<div className="text-4xl flex pl-[15px] pb-[33px]">Hermes</div>
<div className="w-full pl-[30px] pr-[30px] pb-[50px]">
<div className="gap-5">
<UserProfile />
<RoleGate roles={["ADMIN"]}>
<AdminProfile />
</RoleGate>
</div>
<UserProfile />
<RoleGate roles={["ADMIN"]}>
<AdminProfile />
</RoleGate>
</div>
<div className="flex h-full z-20 inset-y-1/3 w-full">
<ul className="rounded-lg shadow-md pl-[25px] flex flex-col w-full justify-between text-center align-center">
<li className="text-xs text-gray-400">
<div className="flex flex-grow h-full w-full">
<ul className="rounded-lg shadow-md flex flex-col w-full justify-between text-center align-center">
<li className="text-xs text-gray-200">
Settings
</li>
<li className="">
<Link href={"/settings/connections"}>
<li>
<Link href={"/settings/connections"} className="m-0 p-0 gap-0">
<Button variant="ghost" className="w-full text-lg">
Connections
</Button>
</Link>
</li>
<li className="text-xs text-gray-400">
<li className="text-xs text-gray-200">
Text to Speech
</li>
<li className="">
@ -49,7 +47,7 @@ const SettingsNavigation = async () => {
</Link>
</li>
<li className="text-xs text-gray-400">
<li className="text-xs text-gray-200">
API
</li>
<li className="">

View File

@ -32,7 +32,7 @@ const UserProfile = () => {
return (
<div className={cn("px-10 py-6 rounded-md bg-blue-300 overflow-hidden wrap", user == null && "hidden")}>
<p className="text-xs text-gray-400">Logged in as:</p>
<p className="text-xs text-gray-200">Logged in as:</p>
<p>{user?.username}</p>
</div>
);

10
data/impersonation.ts Normal file
View File

@ -0,0 +1,10 @@
import { db } from "@/lib/db";
export const getImpersonationById = async (id: string) => {
try {
const impersonation = await db.impersonation.findUnique({ where: { sourceId: id }})
return impersonation;
} catch {
return null;
}
}

View File

@ -0,0 +1,67 @@
import { auth } from "@/auth";
import { db } from "./db";
export default async function fetchUserWithImpersonation(req: Request) {
const session = await auth()
if (session) {
const user = fetch(session.user.id)
if (user) {
return user
}
}
const token = req.headers?.get('x-api-key')
if (token === null || token === undefined)
return null
const key = await db.apiKey.findFirst({
where: {
id: token as string
}
})
if (!key) return null
return fetch(key.userId)
}
const fetch = async (userId: string) => {
const user = await db.user.findFirst({
where: {
id: userId
}
})
if (!user) return null
if (user.role == "ADMIN") {
const impersonation = await db.impersonation.findFirst({
where: {
sourceId: userId
}
})
if (impersonation) {
const copied = await db.user.findFirst({
where: {
id: impersonation.targetId
}
})
if (copied) {
return {
id: copied.id,
username: copied.name,
role: copied.role
}
}
}
}
return {
id: user.id,
username: user.name,
role: user.role
}
}

43
lib/fetch-user.ts Normal file
View File

@ -0,0 +1,43 @@
import { auth } from "@/auth";
import { db } from "./db";
export default async function fetchUser(req: Request) {
const session = await auth()
if (session) {
const user = fetch(session.user.id)
if (user) {
return user
}
}
const token = req.headers?.get('x-api-key')
if (token === null || token === undefined)
return null
const key = await db.apiKey.findFirst({
where: {
id: token as string
}
})
if (!key) return null
return fetch(key.userId)
}
const fetch = async (userId: string) => {
const user = await db.user.findFirst({
where: {
id: userId
}
})
if (!user) return null
return {
id: user.id,
username: user.name,
role: user.role
}
}

View File

@ -1,40 +0,0 @@
import { auth } from "@/auth";
import { db } from "./db";
export default async function fetchUserUsingAPI(req: Request) {
const session = await auth()
if (session) {
const user = await db.user.findFirst({
where: {
name: session.user?.name
}
})
return {
id: user?.id,
username: user?.name
}
}
const token = req.headers?.get('x-api-key')
if (token === null || token === undefined)
return null
const key = await db.apiKey.findFirst({
where: {
id: token as string
}
})
const user = await db.user.findFirst({
where: {
id: key?.userId
}
})
return {
id: user?.id,
username: user?.name
}
}

View File

@ -23,6 +23,9 @@ model User {
ttsDefaultVoice Int @default(1)
ttsEnabledVoice Int @default(1048575)
impersonationSources Impersonation[] @relation(name: "impersonationSources")
impersonationTargets Impersonation[] @relation(name: "impersonationTargets")
apiKeys ApiKey[]
accounts Account[]
twitchConnections TwitchConnection[]
@ -50,6 +53,19 @@ model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@index([userId])
}
model Impersonation {
sourceId String
targetId String
source User @relation(name: "impersonationSources", fields: [sourceId], references: [id], onDelete: Cascade)
target User @relation(name: "impersonationTargets", fields: [targetId], references: [id], onDelete: Cascade)
@@id([sourceId])
@@index([sourceId])
@@index([targetId])
}
model ApiKey {