Added impersonation for admins
This commit is contained in:
parent
320c826684
commit
8f7f18e069
91
app/api/account/impersonate/route.ts
Normal file
91
app/api/account/impersonate/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
@ -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 });
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
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);
|
||||
@ -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 });
|
||||
}
|
||||
}
|
@ -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 });
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
41
app/api/users/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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)}>
|
||||
|
@ -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
41
auth.ts
@ -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
|
||||
}
|
||||
},
|
||||
|
@ -5,17 +5,117 @@ 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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
</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="">
|
||||
|
@ -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
10
data/impersonation.ts
Normal 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;
|
||||
}
|
||||
}
|
67
lib/fetch-user-impersonation.ts
Normal file
67
lib/fetch-user-impersonation.ts
Normal 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
43
lib/fetch-user.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user