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 { db } from "@/lib/db"
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { auth } from "@/auth"; import { auth } from "@/auth";
import fetchUserUsingAPI from "@/lib/validate-api"; import fetchUser from "@/lib/fetch-user";
export async function GET(req: Request) { export async function GET(req: Request) {
try { try {
return NextResponse.json(await fetchUserUsingAPI(req)) return NextResponse.json(await fetchUser(req))
} catch (error) { } catch (error) {
console.log("[ACCOUNT]", error); console.log("[ACCOUNT]", 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 { db } from "@/lib/db"
import fetchUserUsingAPI from "@/lib/validate-api"; import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const user = await fetchUserUsingAPI(req) const user = await fetchUserWithImpersonation(req)
if (!user) { if (!user) {
return new NextResponse("Unauthorized", { status: 401 }); return new NextResponse("Unauthorized", { status: 401 });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,17 @@
import { db } from "@/lib/db" import { db } from "@/lib/db"
import fetchUserUsingAPI from "@/lib/validate-api"; import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET(req: Request, { params } : { params: { id: string } }) { export async function GET(req: Request, { params } : { params: { id: string } }) {
try { try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
let id = req.headers?.get('x-api-key') let id = req.headers?.get('x-api-key')
if (id == null) { if (id == null) {
return NextResponse.json(null); return NextResponse.json(null);
} }
const tokens = await db.apiKey.findFirst({ const tokens = await db.apiKey.findFirst({
@ -18,15 +23,19 @@ export async function GET(req: Request, { params } : { params: { id: string } })
return NextResponse.json(tokens); return NextResponse.json(tokens);
} catch (error) { } catch (error) {
console.log("[TOKEN/GET]", 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 } }) { export async function DELETE(req: Request, { params } : { params: { id: string } }) {
try { try {
const { id } = params const user = await fetchUserWithImpersonation(req)
const user = await fetchUserUsingAPI(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { id } = params
const token = await db.apiKey.delete({ const token = await db.apiKey.delete({
where: { where: {
id, id,
@ -37,6 +46,6 @@ export async function DELETE(req: Request, { params } : { params: { id: string }
return NextResponse.json(token); return NextResponse.json(token);
} catch (error) { } catch (error) {
console.log("[TOKEN/DELETE]", 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 { db } from "@/lib/db"
import fetchUserUsingAPI from "@/lib/validate-api"; import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function GET(req: Request) { export async function GET(req: Request) {
try { try {
const user = await fetchUserUsingAPI(req); const user = await fetchUserWithImpersonation(req);
if (!user) { if (!user) {
return new NextResponse("Unauthorized", { status: 401 }); 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 { db } from "@/lib/db"
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
let { userId, label } = await req.json(); let { userId, label } = await req.json();
if (userId == null) { if (userId == null) {
const user = await fetchUserUsingAPI(req); const user = await fetchUserWithImpersonation(req);
if (user != null) { if (user != null) {
userId = user.id; userId = user.id;
} }
@ -31,9 +36,13 @@ export async function POST(req: Request) {
export async function DELETE(req: Request) { export async function DELETE(req: Request) {
try { try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
let { id } = await req.json(); let { id } = await req.json();
const user = await fetchUserUsingAPI(req); if (!id) {
if (!id || !user) {
return NextResponse.json(null) 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 { db } from "@/lib/db"
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
@ -8,7 +8,7 @@ export async function GET(req: Request) {
let userId = searchParams.get('userId') let userId = searchParams.get('userId')
if (userId == null) { if (userId == null) {
const user = await fetchUserUsingAPI(req); const user = await fetchUser(req);
if (user != null) { if (user != null) {
userId = user.id as string; 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 { try {
const keys = (await axios.get("/api/tokens")).data ?? {}; const keys = (await axios.get("/api/tokens")).data ?? {};
setApiKeys(keys) setApiKeys(keys)
console.log(keys);
} catch (error) { } catch (error) {
console.log("ERROR", 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 ( return (
<div> <div>
<div className="px-10 py-5 mx-5 my-10"> <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 { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import * as z from "zod"; import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { DropdownMenu, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from "@/components/ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@ -58,7 +57,6 @@ const TTSFiltersPage = () => {
try { try {
const replacementData = await axios.get("/api/settings/tts/filter/words") const replacementData = await axios.get("/api/settings/tts/filter/words")
console.log(replacementData.data)
setReplacements(replacementData.data ?? []) setReplacements(replacementData.data ?? [])
} catch (e) { } catch (e) {
console.log("ERROR", e) console.log("ERROR", e)
@ -112,8 +110,9 @@ const TTSFiltersPage = () => {
const onReplaceAdd = async () => { const onReplaceAdd = async () => {
await axios.post("/api/settings/tts/filter/words", { search, replace }) await axios.post("/api/settings/tts/filter/words", { search, replace })
.then(d => { .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) setReplacements(replacements)
setSearch("")
}).catch(e => { }).catch(e => {
// TODO: handle already exist. // TODO: handle already exist.
console.log("LOGGED:", e) console.log("LOGGED:", e)
@ -135,7 +134,9 @@ const TTSFiltersPage = () => {
const onReplaceDelete = async (id: string) => { const onReplaceDelete = async (id: string) => {
await axios.delete("/api/settings/tts/filter/words?id=" + id) await axios.delete("/api/settings/tts/filter/words?id=" + id)
.then(d => { .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 => { }).catch(e => {
// TODO: handle does not exist. // TODO: handle does not exist.
console.log("LOGGED:", e) console.log("LOGGED:", e)
@ -166,9 +167,9 @@ const TTSFiltersPage = () => {
<div> <div>
<div className="text-2xl text-center pt-[50px]">TTS Filters</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="px-10 py-5 w-full h-full flex-grow inset-y-1/2">
<div className=""> <div>
{userTags.map((user, index) => ( {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"> <p className="text-base font-medium">
<span className="mr-2 rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground"> <span className="mr-2 rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground">
{user.tag} {user.tag}
@ -200,7 +201,7 @@ const TTSFiltersPage = () => {
<CommandGroup> <CommandGroup>
{tags.map((tag) => ( {tags.map((tag) => (
<CommandItem <CommandItem
key={tag} key={user.username + "-tag"}
value={tag} value={tag}
onSelect={(value) => { onSelect={(value) => {
onAddExtended({ username: userTags[index].username, tag: value}, false) onAddExtended({ username: userTags[index].username, tag: value}, false)
@ -216,7 +217,7 @@ const TTSFiltersPage = () => {
</DropdownMenuSubContent> </DropdownMenuSubContent>
</DropdownMenuSub> </DropdownMenuSub>
<DropdownMenuSeparator /> <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" /> <Trash className="mr-2 h-4 w-4" />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
@ -235,7 +236,7 @@ const TTSFiltersPage = () => {
control={usernameFilteredForm.control} control={usernameFilteredForm.control}
name="username" name="username"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex-grow"> <FormItem key={"new-username"} className="flex-grow">
<FormControl> <FormControl>
<Input id="username" placeholder="Enter a twitch username" {...field} /> <Input id="username" placeholder="Enter a twitch username" {...field} />
</FormControl> </FormControl>
@ -272,6 +273,7 @@ const TTSFiltersPage = () => {
{tags.map((tag) => ( {tags.map((tag) => (
<CommandItem <CommandItem
value={tag} value={tag}
key={tag + "-tag"}
onSelect={(value) => { onSelect={(value) => {
setTag(value) setTag(value)
setOpen(false) setOpen(false)
@ -297,7 +299,7 @@ const TTSFiltersPage = () => {
<p className="text-center text-2xl text-white pt-[80px]">Regex Replacement</p> <p className="text-center text-2xl text-white pt-[80px]">Regex Replacement</p>
<div> <div>
{replacements.map((term: { id: string, search: string, replace: string, userId: string }) => ( {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="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} /> <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)}> <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 { Checkbox } from "@/components/ui/checkbox";
import voices from "@/data/tts"; import voices from "@/data/tts";
const TTSFiltersPage = () => { const TTSVoiceFiltersPage = () => {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const [loading, setLoading] = useState<boolean>(true) const [loading, setLoading] = useState<boolean>(true)
@ -88,7 +88,7 @@ const TTSFiltersPage = () => {
<CommandGroup> <CommandGroup>
{voices.map((voice) => ( {voices.map((voice) => (
<CommandItem <CommandItem
key={voice.value} key={voice.value + "-" + voice.label}
value={voice.value} value={voice.value}
onSelect={(currentValue) => { onSelect={(currentValue) => {
setValue(Number.parseInt(currentValue)) setValue(Number.parseInt(currentValue))
@ -115,7 +115,7 @@ const TTSFiltersPage = () => {
<p className="text-xl text-center justify-center">Voices Enabled</p> <p className="text-xl text-center justify-center">Voices Enabled</p>
<div className="grid grid-cols-4 grid-flow-row gap-4 pt-[20px]"> <div className="grid grid-cols-4 grid-flow-row gap-4 pt-[20px]">
{voices.map((v, i) => ( {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={() => { <Checkbox onClick={() => {
const newVal = enabled ^ (1 << (Number.parseInt(v.value) - 1)) const newVal = enabled ^ (1 << (Number.parseInt(v.value) - 1))
setEnabled(newVal) 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 { db } from "@/lib/db"
import authConfig from "@/auth.config" import authConfig from "@/auth.config"
import { getUserById } from "./data/user" 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" { declare module "@auth/core/types" {
@ -14,7 +15,8 @@ declare module "@auth/core/types" {
*/ */
interface Session { interface Session {
user: { 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 // 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 } & DefaultSession["user"] // To keep the default types
} }
@ -23,7 +25,8 @@ declare module "@auth/core/types" {
declare module "@auth/core/jwt" { declare module "@auth/core/jwt" {
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */ /** Returned by the `jwt` callback and `auth`, when using JWT sessions */
interface JWT { interface JWT {
role: UserRole role: UserRole | null
impersonation: User | null
} }
} }
@ -50,19 +53,47 @@ export const {
if (token.role && session.user) { if (token.role && session.user) {
session.user.role = token.role 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 return session
}, },
async jwt({ token, user, account, profile, isNewUser }) { async jwt({ token, user, account, profile }) {
if (!token.sub) return token if (!token.sub) return token
const existingUser = await getUserById(token.sub) const existingUser = await getUserById(token.sub)
if (!existingUser) return token if (!existingUser) return token
// Update Role
token.role = existingUser.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 return token
} }
}, },

View File

@ -5,16 +5,116 @@ import * as React from 'react';
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { cn } from "@/lib/utils"; 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 AdminProfile = () => {
const session = useSession(); 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 ( return (
<div className={"px-10 py-6 rounded-md bg-red-300 overflow-hidden wrap m-[10px]"}> <div className={"px-5 py-3 rounded-md bg-red-300 overflow-hidden wrap my-[10px] flex flex-grow flex-col gap-y-3"}>
<p className="text-xs text-gray-400">Role:</p> <div>
<p>{session?.data?.user?.role}</p> <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> </div>
); );
} }

View File

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

View File

@ -32,7 +32,7 @@ const UserProfile = () => {
return ( return (
<div className={cn("px-10 py-6 rounded-md bg-blue-300 overflow-hidden wrap", user == null && "hidden")}> <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> <p>{user?.username}</p>
</div> </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) ttsDefaultVoice Int @default(1)
ttsEnabledVoice Int @default(1048575) ttsEnabledVoice Int @default(1048575)
impersonationSources Impersonation[] @relation(name: "impersonationSources")
impersonationTargets Impersonation[] @relation(name: "impersonationTargets")
apiKeys ApiKey[] apiKeys ApiKey[]
accounts Account[] accounts Account[]
twitchConnections TwitchConnection[] twitchConnections TwitchConnection[]
@ -50,6 +53,19 @@ model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId]) @@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 { model ApiKey {