Compare commits

..

No commits in common. "624b3fa63bf35d9f0c72869201c84a96b6470d7c" and "b92529d8c0b45d15a04b835865ccd1c94fda7dae" have entirely different histories.

42 changed files with 492 additions and 636 deletions

View File

@ -47,7 +47,7 @@ export default function Home() {
else
setLoaded(true)
})
}, [session, status])
}, [session])
return (
<main>

View File

@ -17,13 +17,17 @@ export default function Home() {
useEffect(() => {
if (status !== "authenticated" || previousUsername == session.user?.name) {
console.log("CANCELED")
return
}
setPreviousUsername(session.user?.name as string)
async function saveAccount() {
await axios.post("/api/account")
const data = await axios.post("/api/account")
if (data == null || data == undefined) {
console.log("ERROR")
}
}
saveAccount().catch(console.error)

View File

@ -26,6 +26,7 @@ const permissionPaths = [
{ path: "tts.commands.version", description: "To use !version command" },
{ path: "tts.commands.voice", description: "To use !voice command" },
{ path: "tts.commands.voice.admin", description: "To use !voice command on others" },
].sort((a, b) => a.path.localeCompare(b.path))
const GroupPermissionPage = () => {
@ -43,69 +44,69 @@ const GroupPermissionPage = () => {
setGroups(groups.filter(g => g.id != group.id))
}
function containsGroup(groupName: string) {
return groups.some(g => g.name == groupName)
}
useEffect(() => {
if (status !== "authenticated" || previousUsername == session.user?.name)
return
setPreviousUsername(session.user?.name)
// TODO: fetch groups & permissions
axios.get('/api/settings/groups')
.then(d => {
for (let groupName of specialGroups)
if (!d.data.some((g: { id: string, name: string, priority: number }) => g.name == groupName))
d.data.push({ id: "$" + groupName, name: groupName, priority: 0 });
axios.get('/api/settings/groups/permissions')
.then(d2 => {
setPermissions(d2.data)
setGroups(d.data)
})
})
// TODO: filter permissions by group?
}, [session])
return (
<div>
<div className="text-2xl text-center pt-[50px]">Groups & Permissions</div>
{/* <InfoNotice
message="Redemption actions are activated when specific Twitch channel point redeems have been activated. Aforementioned redeem need to be linked in the redemption part, together with the action, for the action to activate."
hidden={false} /> */}
<div className="grid sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-3">
{groups.map(group =>
<div
className="col-span-1"
key={group.id}>
<GroupElement
id={group.id}
name={group.name}
priority={group.priority}
permissionsLoaded={permissions.filter(p => p.groupId == group.id)}
edit={group.id.startsWith('$')}
showEdit={true}
isNewGroup={group.id.startsWith('$')}
permissionPaths={permissionPaths}
specialGroups={specialGroups}
adder={addGroup}
remover={removeGroup}
contains={containsGroup} />
</div>
)}
{groups.map(group =>
<div
className="col-span-1">
className="col-span-1"
key={group.id}>
<GroupElement
id={undefined}
name={""}
priority={0}
permissionsLoaded={[]}
edit={true}
showEdit={false}
isNewGroup={true}
id={group.id}
name={group.name}
priority={group.priority}
permissionsLoaded={permissions.filter(p => p.groupId == group.id)}
edit={group.id.startsWith('$')}
showEdit={true}
isNewGroup={group.id.startsWith('$')}
permissionPaths={permissionPaths}
specialGroups={specialGroups}
adder={addGroup}
remover={removeGroup}
contains={containsGroup} />
remover={removeGroup} />
</div>
)}
<div
className="col-span-1">
<GroupElement
id={undefined}
name={""}
priority={0}
permissionsLoaded={[]}
edit={true}
showEdit={false}
isNewGroup={true}
permissionPaths={permissionPaths}
specialGroups={specialGroups}
adder={addGroup}
remover={removeGroup} />
</div>
</div>
</div>
);

View File

@ -8,6 +8,7 @@ import RedeemptionAction from "@/components/elements/redeemable-action";
import OBSRedemption from "@/components/elements/redemption";
import { ActionType } from "@prisma/client";
import InfoNotice from "@/components/elements/info-notice";
import { string } from "zod";
const obsTransformations = [
{ label: "scene_name", description: "", placeholder: "Name of the OBS scene" },
@ -65,6 +66,7 @@ const RedemptionsPage = () => {
axios.get('/api/connection')
.then(d => {
console.log(d.data.data)
setConnections(d.data.data)
})

View File

@ -1,5 +1,3 @@
// TODO: remove this page.
import axios from 'axios'
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
@ -12,7 +10,7 @@ export async function GET(req: Request) {
const state = searchParams.get('state') as string
if (!code || !scope || !state) {
return NextResponse.json({ message: 'Missing oauth2 data.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
}
// Verify state against user id in user table.
@ -23,7 +21,7 @@ export async function GET(req: Request) {
})
if (!user) {
return NextResponse.json({ message: 'You do not have permissions for this.', error: null, value: null }, { status: 403 });
return new NextResponse("Bad Request", { status: 400 });
}
// Post to https://id.twitch.tv/oauth2/token
@ -39,7 +37,7 @@ export async function GET(req: Request) {
const { access_token, expires_in, refresh_token, token_type } = token
if (!access_token || !refresh_token || token_type !== "bearer") {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
let info = await axios.get("https://api.twitch.tv/helix/users?login=" + user.name, {
@ -59,9 +57,9 @@ export async function GET(req: Request) {
}
})
return NextResponse.json({ message: null, error: null, value: null }, { status: 200 })
return new NextResponse("", { status: 200 });
} catch (error) {
console.log("[ACCOUNT/AUTHORIZE]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const impersonation = await db.impersonation.findFirst({
@ -18,7 +18,7 @@ export async function GET(req: Request) {
return NextResponse.json(impersonation);
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -26,7 +26,7 @@ export async function POST(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { targetId } = await req.json();
@ -41,7 +41,7 @@ export async function POST(req: Request) {
return NextResponse.json(impersonation);
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -49,7 +49,7 @@ export async function PUT(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { targetId } = await req.json();
@ -66,7 +66,7 @@ export async function PUT(req: Request) {
return NextResponse.json(impersonation);
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -74,7 +74,7 @@ export async function DELETE(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const impersonation = await db.impersonation.delete({
@ -86,6 +86,6 @@ export async function DELETE(req: Request) {
return NextResponse.json(impersonation)
} catch (error) {
console.log("[AUTH/ACCOUNT/IMPERSONATION]", error);
return NextResponse.json({ message: 'Something went wrong.', error: null, value: null }, { status: 500 })
return new NextResponse("Internal Error" + error, { status: 500 });
}
}

View File

@ -9,7 +9,7 @@ export async function GET(req: Request) {
// Verify state against user id in user table.
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const connection = await db.twitchConnection.findFirst({
@ -18,7 +18,7 @@ export async function GET(req: Request) {
}
})
if (!connection) {
return NextResponse.json({ message: 'You do not have permission for this.', error: null, value: null }, { status: 403 })
return new NextResponse("Forbidden", { status: 403 });
}
try {
@ -59,7 +59,7 @@ export async function GET(req: Request) {
const { access_token, expires_in, refresh_token, token_type } = token
if (!access_token || !refresh_token || token_type !== "bearer") {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
await db.twitchConnection.update({
@ -83,6 +83,6 @@ export async function GET(req: Request) {
return NextResponse.json(data)
} catch (error) {
console.log("[ACCOUNT]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -1,3 +1,4 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation';
import axios from "axios";
@ -6,16 +7,16 @@ import { updateTwitchToken } from "@/data/twitch-reauthorize";
export async function GET(req: Request) {
try {
if (!process.env.TWITCH_BOT_CLIENT_ID)
return NextResponse.json({ message: 'Something went wrong.', error: null, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const auth = await updateTwitchToken(user.id)
if (!auth)
return NextResponse.json({ message: 'Failed to authorize to Twitch.', error: null, value: null }, { status: 403 });
return new NextResponse("Bad Request", { status: 400 })
try {
const redemptions = await axios.get("https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + auth.broadcaster_id,
@ -34,6 +35,6 @@ export async function GET(req: Request) {
return NextResponse.json([]);
} catch (error) {
console.log("[REDEMPTIONS/ACTIONS]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -7,7 +7,7 @@ import fetchUser from "@/lib/fetch-user";
export async function GET(req: Request) {
try {
const user = await fetchUser(req)
if (!user) return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 })
if (!user) return new NextResponse("Internal Error", { status: 401 })
const account = await db.account.findFirst({
where: {
@ -18,7 +18,7 @@ export async function GET(req: Request) {
return NextResponse.json({ ... user, broadcasterId: account?.providerAccountId })
} catch (error) {
console.log("[ACCOUNT]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -27,7 +27,7 @@ export async function POST(req: Request) {
const session = await auth()
const user = session?.user?.name
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 })
return new NextResponse("Internal Error", { status: 401 })
}
const exist = await db.user.findFirst({
@ -54,6 +54,7 @@ export async function POST(req: Request) {
username: newUser.name
});
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[ACCOUNT]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -13,8 +13,6 @@ export async function POST(req: Request) {
if (!token_type)
return NextResponse.json({ error: null, message: 'No token type given for the authorization.', success: false }, { status: 400 })
if (token_type !== "bearer")
return NextResponse.json({ error: null, message: 'Invalid token type given for the authorization.', success: false }, { status: 400 })
if (!access_token)
return NextResponse.json({ error: null, message: 'No access token given for the authorization.', success: false }, { status: 400 })

View File

@ -17,7 +17,7 @@ export async function GET(req: Request) {
}
})
return NextResponse.json({ error: null, message: null, success: true, data }, { status: 200 });
return NextResponse.json({ error: null, message: "", success: true, data }, { status: 200 });
} catch (error: any) {
return NextResponse.json({ error, message: "Failed to get default connection", success: false }, { status: 500 });
}
@ -68,7 +68,7 @@ export async function PUT(req: Request) {
}
})
return NextResponse.json({ error: null, message: null, success: true, data }, { status: 200 });
return NextResponse.json({ error: null, message: "", success: true, data }, { status: 200 });
} catch (error: any) {
return NextResponse.json({ error, message: "Failed to update default connection", success: false }, { status: 500 });
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
minor_version: 3,
download: "https://drive.proton.me/urls/YH86153EWM#W6VTyaoAVHKP",
changelog: "Reconnecting should be fixed.\nNew TTS messages when queue is empty will be played faster, up to 200 ms.\nRemoved subscriptions errors when reconnecting on Twitch\nAdded !refresh connections"
//changelog: "When using multiple voices (ex: brian: hello amy: world), TTS messages are now merged as a single TTS message.\nNightbot integration\nTwitch authentication changed. Need to refresh connection every 30-60 days.\nFixed raid spam, probably."
//changelog: "Added raid spam prevention (lasts 30 seconds; works on joined chats as well).\nAdded permission check for chat messages with bits."
//changelog: "Fixed group permissions.\nRemoved default permissions.\nSome commands may have additional permission requirements, which are more strict.\nAdded support for redeemable actions via adbreak, follow, subscription.\nMessage deletion and bans automatically remove TTS messages from queue and playing.\nAdded support to read TTS from multiple chats via !tts join.\nFixed some reconnection problems."

View File

@ -6,7 +6,7 @@ export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const connection = await db.twitchConnection.deleteMany({
@ -18,6 +18,6 @@ export async function POST(req: Request) {
return NextResponse.json(connection);
} catch (error) {
console.log("[CONNECTION/TWITCH]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
@ -27,6 +27,6 @@ export async function GET(req: Request) {
return NextResponse.json(connection);
} catch (error) {
console.log("[CONNECTION/TWITCH]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -4,13 +4,12 @@ import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import axios from "axios";
import { env } from "process";
import { TwitchUpdateAuthorization } from "@/lib/twitch";
import { z } from "zod"
export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
const { searchParams } = new URL(req.url)
const groupId = searchParams.get('groupId') as string
@ -18,7 +17,7 @@ export async function GET(req: Request) {
const search = searchParams.get('search') as string
if (!groupId && search != 'all')
return NextResponse.json({ message: 'Something went wrong', error: null, value: null }, { status: 400 })
return new NextResponse("Bad Request", { status: 400 })
let page = parseInt(pageString)
if (isNaN(page) || page === undefined || page === null)
@ -41,15 +40,18 @@ export async function GET(req: Request) {
})
const paginated = search == 'all' ? chatters : chatters.slice(page * 50, (page + 1) * 50)
if (!paginated || paginated.length == 0)
if (!paginated || paginated.length == 0) {
console.log('No users returned from db')
return NextResponse.json([])
}
const ids = chatters.map(c => c.chatterId)
const idsString = 'id=' + ids.map(i => i.toString()).reduce((a, b) => a + '&id=' + b)
const auth = await TwitchUpdateAuthorization(user.id)
if (!auth)
return NextResponse.json({ message: 'Unauthorized', error: null, value: null }, { status: 403 })
if (!auth) {
return new NextResponse("", { status: 403 })
}
const users = await axios.get("https://api.twitch.tv/helix/users?" + idsString, {
headers: {
@ -58,38 +60,31 @@ export async function GET(req: Request) {
}
})
if (!users)
return NextResponse.json({ message: 'No users found', error: null, value: null }, { status: 400 })
if (!users) {
return new NextResponse("", { status: 400 })
}
if (users.data.data.length == 0)
if (users.data.data.length == 0) {
console.log('No users returned from twitch')
return NextResponse.json([])
}
return NextResponse.json(users.data.data.map((u: any) => ({ id: u.id, username: u.login })));
} catch (error) {
return NextResponse.json({ message: 'Failed to get', error: error, value: null }, { status: 500 })
console.log("[GROUPS/USERS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
const groupIdSchema = z.string({
required_error: "Group ID should be available.",
invalid_type_error: "Group ID must be a string"
}).regex(/^[\w\-\=]{1,32}$/, "Group ID must contain only letters, numbers, dashes, underscores & equal signs.")
export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized', error: null, value: null }, { status: 401 })
return new NextResponse("Unauthorized", { status: 401 });
const { groupId, users }: { groupId: string, users: { id: number, username: string }[] } = await req.json();
if (!groupId)
return NextResponse.json({ message: 'groupId must be set.', error: null, value: null }, { status: 400 });
if (!users)
return NextResponse.json({ message: 'users must be set.', error: null, value: null }, { status: 400 });
const groupIdValidation = await groupIdSchema.safeParseAsync(groupId)
if (!groupIdValidation.success)
return NextResponse.json({ message: 'groupId does not meet requirements.', error: JSON.parse(groupIdValidation.error['message'])[0], value: null }, { status: 400 })
if (!groupId || !users)
return new NextResponse("Bad Request", { status: 400 });
const chatters = await db.chatterGroup.createMany({
data: users.map(u => ({ userId: user.id, chatterId: u.id, groupId, chatterLabel: u.username }))
@ -97,7 +92,8 @@ export async function POST(req: Request) {
return NextResponse.json(chatters, { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Failed to create', error: error, value: null }, { status: 500 })
console.log("[GROUPS/USERS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -105,20 +101,13 @@ export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized', error: null, value: null }, { status: 401 })
return new NextResponse("Unauthorized", { status: 401 });
const { searchParams } = new URL(req.url)
const groupId = searchParams.get('groupId') as string
const ids = searchParams.get('ids') as string
if (!groupId)
return NextResponse.json({ message: 'groupId must be set.', error: null, value: null }, { status: 400 });
if (!ids)
return NextResponse.json({ message: 'ids must be set.', error: null, value: null }, { status: 400 });
const groupIdValidation = await groupIdSchema.safeParseAsync(groupId)
if (!groupIdValidation.success)
return NextResponse.json({ message: 'groupId does not meet requirements.', error: JSON.parse(groupIdValidation.error['message'])[0], value: null }, { status: 400 })
if (!groupId || !ids)
return new NextResponse("Bad Request", { status: 400 });
const chatters = await db.chatterGroup.deleteMany({
where: {
@ -132,6 +121,7 @@ export async function DELETE(req: Request) {
return NextResponse.json(chatters);
} catch (error) {
return NextResponse.json({ message: 'Failed to delete.', error: error, value: null }, { status: 500 })
console.log("[GROUPS/USERS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -1,13 +1,13 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { z } from "zod";
import { ActionType, Prisma } from "@prisma/client";
export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
const commands = await db.groupPermission.findMany({
where: {
@ -17,31 +17,20 @@ export async function GET(req: Request) {
return NextResponse.json(commands.map(({userId, ...attrs}) => attrs));
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS/PERMISSIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
const permissionPathSchema = z.string({
required_error: "Permission path should be available.",
invalid_type_error: "Permission path must be a string"
}).regex(/^[\w\-\.]{1,64}$/, "Permission path must contain only letters, numbers, dashes, periods.")
export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
const { path, allow, groupId }: { path: string, allow: boolean, groupId: string } = await req.json();
if (!path)
return NextResponse.json({ message: 'path does not exist.', error: null, value: null }, { status: 400 });
const permissionPathValidation = permissionPathSchema.safeParse(path)
if (!permissionPathValidation.success)
return NextResponse.json({ message: 'path must meet certain requirements.', error: JSON.parse(permissionPathValidation.error['message'])[0], value: null }, { status: 400 });
if (!groupId)
return NextResponse.json({ message: 'groupId does not exist.', error: null, value: null }, { status: 400 });
if (groupId.length > 64)
return NextResponse.json({ message: 'groupId is too long.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
const permission = await db.groupPermission.create({
data: {
@ -54,7 +43,8 @@ export async function POST(req: Request) {
return NextResponse.json(permission, { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS/PERMISSIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -62,30 +52,30 @@ export async function PUT(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
const { id, path, allow }: { id: string, path: string, allow: boolean|null } = await req.json();
if (!id)
return NextResponse.json({ message: 'id does not exist.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
if (!path)
return NextResponse.json({ message: 'path does not exist.', error: null, value: null }, { status: 400 });
const permissionPathValidation = permissionPathSchema.safeParse(path)
if (!permissionPathValidation.success)
return NextResponse.json({ message: 'path must meet certain requirements.', error: JSON.parse(permissionPathValidation.error['message'])[0], value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
let data: any = {}
if (!!path)
data = { ...data, path }
data = { ...data, allow }
const permission = await db.groupPermission.update({
where: {
id
},
data: {
path,
allow
}
data: data
});
return NextResponse.json(permission, { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS/PERMISSIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -93,7 +83,7 @@ export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
const { searchParams } = new URL(req.url)
const id = searchParams.get('id') as string
@ -105,6 +95,7 @@ export async function DELETE(req: Request) {
return NextResponse.json(permission);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS/PERMISSIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -1,13 +1,13 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { z } from "zod";
import { ActionType, Prisma } from "@prisma/client";
export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const actions = await db.group.findMany({
@ -16,30 +16,23 @@ export async function GET(req: Request) {
}
})
return NextResponse.json(actions.map(({ userId, ...attrs }) => attrs));
return NextResponse.json(actions.map(({userId, ...attrs}) => attrs));
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
const groupNameSchema = z.string({
required_error: "Group name is required.",
invalid_type_error: "Group name must be a string"
}).regex(/^[\w\-\s]{1,20}$/, "Group name must contain only letters, numbers, spaces, dashes, and underscores.")
export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { name, priority }: { name: string, priority: number } = await req.json();
if (!name)
return NextResponse.json({ message: 'name does not exist.', error: null, value: null }, { status: 400 });
const groupNameValidation = await groupNameSchema.safeParseAsync(name)
if (!groupNameValidation.success)
return NextResponse.json({ message: 'name does not meet requirements.', error: JSON.parse(groupNameValidation.error['message'])[0], value: null }, { status: 400 })
return new NextResponse("Bad Request", { status: 400 });
const group = await db.group.create({
data: {
@ -51,7 +44,8 @@ export async function POST(req: Request) {
return NextResponse.json(group, { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -59,24 +53,19 @@ export async function PUT(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { id, name, priority }: { id: string, name: string, priority: number } = await req.json();
if (!id)
return NextResponse.json({ message: 'id does not exist.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
if (!name && !priority)
return NextResponse.json({ message: 'Either name or priority must not be null.', error: null, value: null }, { status: 400 });
if (name) {
const groupNameValidation = await groupNameSchema.safeParseAsync(name)
if (!groupNameValidation.success)
return NextResponse.json({ message: 'name does not meet requirements.', error: JSON.parse(groupNameValidation.error['message'])[0], value: null }, { status: 400 })
}
return new NextResponse("Bad Request", { status: 400 });
let data: any = {}
if (name)
if (!!name)
data = { ...data, name: name.toLowerCase() }
if (priority)
if (!!priority)
data = { ...data, priority }
const group = await db.group.update({
@ -88,7 +77,8 @@ export async function PUT(req: Request) {
return NextResponse.json(group, { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -96,7 +86,7 @@ export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
@ -109,6 +99,7 @@ export async function DELETE(req: Request) {
return NextResponse.json(group);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -8,13 +8,13 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
const { searchParams } = new URL(req.url)
const logins = (searchParams.get('logins') as string)?.split(',').reduce((a,b) => a + ',&login=' + b)
const ids = (searchParams.get('ids') as string)?.split(',').reduce((a,b) => a + ',&id=' + b)
if (!logins && !ids) {
return NextResponse.json({ message: 'Either logins or ids must not be null.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
}
let suffix = ""
@ -27,7 +27,7 @@ export async function GET(req: Request) {
const auth = await TwitchUpdateAuthorization(user.id)
if (!auth) {
return NextResponse.json({ message: 'You do not have permissions for this.', error: null, value: null }, { status: 403 });
return new NextResponse("", { status: 403 })
}
console.log('TWITCH URL:', 'https://api.twitch.tv/helix/users' + suffix)
@ -39,11 +39,17 @@ export async function GET(req: Request) {
}
})
if (!users?.data?.data)
return NextResponse.json([], { status: 200 });
if (!users || !users.data) {
return new NextResponse("", { status: 400 })
}
return NextResponse.json(users.data.data.map((u: any) => ({ id: u.id, username: u.login })), { status: 200 });
if (users.data.data.length == 0) {
return NextResponse.json([])
}
return NextResponse.json(users.data.data.map((u: any) => ({ id: u.id, username: u.login })));
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[GROUPS/USERS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -1,31 +1,22 @@
import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { z } from "zod";
const groupIdSchema = z.string({
required_error: "Group ID should be available.",
invalid_type_error: "Group ID must be a string"
}).regex(/^[\w\-\=]{1,32}$/, "Group ID must contain only letters, numbers, dashes, underscores & equal signs.")
import axios from "axios";
import { env } from "process";
import { TwitchUpdateAuthorization } from "@/lib/twitch";
export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user)
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 })
return new NextResponse("Unauthorized", { status: 401 });
const { searchParams } = new URL(req.url)
const groupId = searchParams.get('groupId') as string
if (groupId) {
const groupIdValidation = await groupIdSchema.safeParseAsync(groupId)
if (!groupIdValidation.success)
return NextResponse.json({ message: 'groupId does not meet requirements.', error: JSON.parse(groupIdValidation.error['message'])[0], value: null }, { status: 400 })
}
let chatters: { userId: string, groupId: string, chatterId: bigint, chatterLabel: string }[]
if (groupId)
if (!!groupId)
chatters = await db.chatterGroup.findMany({
where: {
userId: user.id,
@ -40,8 +31,10 @@ export async function GET(req: Request) {
})
return NextResponse.json(chatters.map(u => ({ ...u, chatterId: Number(u.chatterId) }))
.map(({ userId, chatterLabel, ...attrs }) => attrs))
.map(({userId, chatterLabel, ...attrs}) => attrs))
} catch (error) {
return NextResponse.json({ message: 'Failed to get groups', error: error, value: null }, { status: 500 })
console.log("[GROUPS/USERS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -2,13 +2,12 @@ import { db } from "@/lib/db"
import { NextResponse } from "next/server";
import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation";
import { ActionType, Prisma } from "@prisma/client";
import { z } from "zod";
export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const actions = await db.action.findMany({
@ -18,21 +17,17 @@ export async function GET(req: Request) {
})
return NextResponse.json(actions.map(({ userId, ...attrs }) => attrs));
} catch (error: any) {
return NextResponse.json({ message: null, error: error, value: null }, { status: 500 });
} catch (error) {
console.log("[REDEMPTIONS/ACTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
const nameSchema = z.string({
required_error: "Name is required.",
invalid_type_error: "Name must be a string"
}).regex(/^[\w\-\s]{1,32}$/, "Name must contain only letters, numbers, spaces, dashes, and underscores.")
async function common(req: Request, action: (id: string, name: string, type: ActionType, data: any) => void) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { name, type, scene_name, scene_item_name, rotation, position_x, position_y, file_path, file_content, tts_voice, obs_visible, obs_index, sleep, oauth_name, oauth_type }:
@ -40,21 +35,16 @@ async function common(req: Request, action: (id: string, name: string, type: Act
name: string, type: ActionType, scene_name: string, scene_item_name: string, rotation: string, position_x: string, position_y: string, file_path: string, file_content: string, tts_voice: string, obs_visible: boolean, obs_index: number, sleep: number,
oauth_name: string, oauth_type: string
} = await req.json();
if (!name)
return NextResponse.json({ message: 'name is required.', error: null, value: null }, { status: 400 });
const nameValidation = nameSchema.safeParse(name)
if (!nameValidation.success)
return NextResponse.json({ message: 'name must follow some requirements.', error: nameValidation.error, value: null }, { status: 400 });
if (!type)
return NextResponse.json({ message: 'type is required', error: null, value: null }, { status: 400 });
if (!name && !type)
return new NextResponse("Bad Request", { status: 400 });
if (type == ActionType.OBS_TRANSFORM && (!scene_name || !scene_item_name || !rotation && !position_x && !position_y))
return NextResponse.json({ message: '"scene_name", "scene_item_name" and one of "rotation", "position_x", "position_y" are required.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
if ((type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) && (!file_path || !file_content))
return NextResponse.json({ message: '"scene_name", "scene_item_name", "file_path" & "file_content" are required.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
if (type == ActionType.AUDIO_FILE && !file_path)
return NextResponse.json({ message: '"scene_name", "scene_item_name" & "file_path" are required.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
if ([ActionType.OAUTH, ActionType.NIGHTBOT_PLAY, ActionType.NIGHTBOT_PAUSE, ActionType.NIGHTBOT_SKIP, ActionType.NIGHTBOT_CLEAR_PLAYLIST, ActionType.NIGHTBOT_CLEAR_QUEUE, ActionType.TWITCH_OAUTH].some(t => t == type) && (!oauth_name || !oauth_type))
return NextResponse.json({ message: '"oauth_name" & "oauth_type" are required.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
let data: any = {}
if (type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) {
@ -88,9 +78,10 @@ async function common(req: Request, action: (id: string, name: string, type: Act
action(user.id, name, type, data)
return NextResponse.json({ message: null, error: null, value: null }, { status: 200 });
return new NextResponse("", { status: 200 });
} catch (error: any) {
return NextResponse.json({ message: null, error: error, value: null }, { status: 500 });
//console.log("[REDEMPTIONS/ACTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -129,7 +120,7 @@ export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
@ -144,7 +135,8 @@ export async function DELETE(req: Request) {
})
return NextResponse.json(redemptions);
} catch (error: any) {
return NextResponse.json({ message: null, error: error, value: null }, { status: 500 });
} catch (error) {
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const redemptions = await db.redemption.findMany({
@ -17,7 +17,8 @@ export async function GET(req: Request) {
return NextResponse.json(redemptions.map(({userId, ...attrs}) => attrs));
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -25,15 +26,12 @@ export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { actionName, redemptionId, order, state }: { actionName: string, redemptionId: string, order: number, state: boolean } = await req.json();
if (!redemptionId && !actionName && !order)
return NextResponse.json({ message: 'One of the following fields must exist: redemptionId, actionName, order, state.', error: null, value: null }, { status: 400 })
if (actionName && actionName.length > 32)
return NextResponse.json({ message: 'actionName cannot be longer than 32 characters.', error: null, value: null }, { status: 400 })
if (!redemptionId || !actionName && !order && !state)
return new NextResponse("Bad Request", { status: 400 });
const action = await db.action.findFirst({
where: {
@ -41,7 +39,7 @@ export async function POST(req: Request) {
}
})
if (!action)
return NextResponse.json({ message: 'Action does not exist.', error: null, value: null }, { status: 400 })
return new NextResponse("Bad Request", { status: 400 });
let data:any = {
actionName,
@ -68,7 +66,8 @@ export async function POST(req: Request) {
return NextResponse.json(res, { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -76,15 +75,12 @@ export async function PUT(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { id, actionName, redemptionId, order, state }: { id: string, actionName: string, redemptionId: string, order: number, state: boolean } = await req.json();
if (!redemptionId && !actionName && !order)
return NextResponse.json({ message: 'One of the following fields must exist: redemptionId, actionName, order, state.', error: null, value: null }, { status: 400 })
if (actionName && actionName.length > 32)
return NextResponse.json({ message: 'actionName cannot be longer than 32 characters.', error: null, value: null }, { status: 400 })
if (!redemptionId || !actionName && !order && !state)
return new NextResponse("Bad Request", { status: 400 });
const action = await db.action.findFirst({
where: {
@ -92,7 +88,7 @@ export async function PUT(req: Request) {
}
})
if (!action)
return NextResponse.json({ message: 'Action does not exist.', error: null, value: null }, { status: 400 })
return new NextResponse("Bad Request", { status: 400 });
let data:any = {
actionName,
@ -119,7 +115,8 @@ export async function PUT(req: Request) {
return NextResponse.json(res, { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -127,7 +124,7 @@ export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
@ -140,6 +137,7 @@ export async function DELETE(req: Request) {
return NextResponse.json(redemptions);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[REDEMPTIONS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,48 +6,49 @@ import voices from "@/data/tts";
export async function GET(req: Request) {
try {
if (!voices) {
return NextResponse.json({ message: 'Failed to load voices', error: null, value: null }, { status: 500 })
return new NextResponse("Voices not available.", { status: 500 });
}
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
return NextResponse.json(user.ttsDefaultVoice);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[TTS/FILTER/DEFAULT]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function POST(req: Request) {
try {
if (!voices) {
return NextResponse.json({ message: 'Failed to load voices', error: null, value: null }, { status: 500 })
return new NextResponse("Voices not available.", { status: 500 });
}
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { voice } = await req.json();
if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase()))
return NextResponse.json({ message: 'Voice does not exist.', error: null, value: null }, { status: 400 })
if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 });
const v = voices.find(v => v.toLowerCase() == voice.toLowerCase())
await db.user.update({
where: {
id: user.id
},
data: {
ttsDefaultVoice: v
}
where: {
id: user.id
},
data: {
ttsDefaultVoice: v
}
});
return NextResponse.json({ message: null, error: null, value: null }, { status: 200 })
return new NextResponse("", { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[TTS/FILTER/DEFAULT]", error);
}
return new NextResponse("Internal Error", { status: 500 });
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const filters = await db.ttsUsernameFilter.findMany({
@ -18,7 +18,7 @@ export async function GET(req: Request) {
return NextResponse.json(filters);
} catch (error) {
console.log("[TTS/FILTER/USERS]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -26,14 +26,12 @@ export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { username, tag } = await req.json();
if (!username || username.length < 4 || username.length > 25)
return NextResponse.json({ message: 'Username does not exist.', error: null, value: null }, { status: 400 })
if (!tag || !["blacklisted", "priority"].includes(tag))
return NextResponse.json({ message: 'Tag does not exist.', error: null, value: null }, { status: 400 })
if (!username || username.length < 4 || username.length > 25) return new NextResponse("Bad Request", { status: 400 });
if (!tag || !["blacklisted", "priority"].includes(tag)) return new NextResponse("Bad Request", { status: 400 });
const filter = await db.ttsUsernameFilter.upsert({
where: {
@ -54,7 +52,8 @@ export async function POST(req: Request) {
return NextResponse.json(filter);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[TTS/FILTER/USERS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -62,13 +61,12 @@ export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
const username = searchParams.get('username') as string
if (!username || username.length < 4 || username.length > 25)
return NextResponse.json({ message: 'Username does not exist.', error: null, value: null }, { status: 400 })
if (!username || username.length < 4 || username.length > 25) return new NextResponse("Bad Request", { status: 400 });
const filter = await db.ttsUsernameFilter.delete({
where: {
@ -81,6 +79,7 @@ export async function DELETE(req: Request) {
return NextResponse.json(filter)
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[TTS/FILTER/USERS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const filters = await db.ttsWordFilter.findMany({
@ -18,7 +18,7 @@ export async function GET(req: Request) {
return NextResponse.json(filters);
} catch (error) {
console.log("[TTS/FILTER/WORDS]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -26,102 +26,97 @@ export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { search, replace } = await req.json();
if (!search || search.length < 3 || search.length > 127)
return NextResponse.json({ message: 'search must be between 3 to 127 characters.', error: null, value: null }, { status: 400 })
if (replace == null)
return NextResponse.json({ message: 'replace must not be null', error: null, value: null }, { status: 400 })
if (!search || search.length < 4 || search.length > 200) return new NextResponse("Bad Request", { status: 400 });
if (replace == null) return new NextResponse("Bad Request", { status: 400 });
const filter = await db.ttsWordFilter.create({
data: {
search,
replace,
userId: user.id
}
data: {
search,
replace,
userId: user.id
}
});
return NextResponse.json(filter);
} catch (error) {
console.log("[TTS/FILTER/WORDS]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function PUT(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { id, search, replace } = await req.json();
if (!id || id.length < 1) return new NextResponse("Bad Request", { status: 400 });
if (!search || search.length < 4 || search.length > 200) return new NextResponse("Bad Request", { status: 400 });
if (replace == null) return new NextResponse("Bad Request", { status: 400 });
const filter = await db.ttsWordFilter.update({
where: {
id
},
data: {
search,
replace,
userId: user.id
}
});
const { id, search, replace } = await req.json();
if (!id || id.length < 1)
return NextResponse.json({ message: 'id does not exist.', error: null, value: null }, { status: 400 })
if (!search || search.length < 3 || search.length > 127)
return NextResponse.json({ message: 'search must be between 3 to 127 characters.', error: null, value: null }, { status: 400 })
if (replace == null)
return NextResponse.json({ message: 'replace must not be null.', error: null, value: null }, { status: 400 })
const filter = await db.ttsWordFilter.update({
where: {
id
},
data: {
search,
replace,
userId: user.id
}
});
return NextResponse.json(filter);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
}
return NextResponse.json(filter);
} catch (error) {
console.log("[TTS/FILTER/WORDS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
const id = searchParams.get('id') as string
const search = searchParams.get('search') as string
if (!id && !search) return new NextResponse("Bad Request", { status: 400 });
if (search) {
if (!search || search.length < 3 || search.length > 127)
return NextResponse.json({ message: 'search must be between 3 to 127 characters.', error: null, value: null }, { status: 400 })
if (search.length < 4 || search.length > 200) return new NextResponse("Bad Request", { status: 400 });
const filter = await db.ttsWordFilter.delete({
where: {
userId_search: {
userId: user.id,
search
}
}
});
const filter = await db.ttsWordFilter.delete({
where: {
userId_search: {
userId: user.id,
search
}
}
});
return NextResponse.json(filter)
return NextResponse.json(filter)
} else if (id) {
if (!id || id.length < 1)
return NextResponse.json({ message: 'id does not exist.', error: null, value: null }, { status: 400 })
if (id.length < 1) return new NextResponse("Bad Request", { status: 400 });
const filter = await db.ttsWordFilter.delete({
where: {
id: id
}
});
const filter = await db.ttsWordFilter.delete({
where: {
id: id
}
});
return NextResponse.json(filter)
return NextResponse.json(filter)
}
return NextResponse.json({ message: 'Either id or search must not be null.', error: null, value: null }, { status: 400 })
return new NextResponse("Bad Request", { status: 400 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong.', error: null, value: null }, { status: 500 })
console.log("[TTS/FILTER/WORDS]", error);
return new NextResponse("Internal Error" + error, { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const voiceStates = await db.ttsVoiceState.findMany({
@ -20,7 +20,8 @@ export async function GET(req: Request) {
return NextResponse.json(voiceStates.filter(v => v.state).map(v => voiceNamesMapped[v.ttsVoiceId]));
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[TTS]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -28,7 +29,7 @@ export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { voice, state }: { voice: string, state: boolean } = await req.json();
@ -36,7 +37,7 @@ export async function POST(req: Request) {
const voiceIds = await db.ttsVoice.findMany();
const voiceIdsMapped: { [voice: string]: string } = Object.assign({}, ...voiceIds.map(v => ({ [v.name.toLowerCase()]: v.id })));
if (!voiceIdsMapped[voice.toLowerCase()]) {
return NextResponse.json({ message: 'Voice does not exist.', error: null, value: null }, { status: 400 });
return new NextResponse("Bad Request", { status: 400 });
}
await db.ttsVoiceState.upsert({
@ -56,8 +57,9 @@ export async function POST(req: Request) {
}
});
return NextResponse.json({ message: null, error: null, value: null }, { status: 200 })
return new NextResponse("", { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[TTS/FILTER/USER]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -7,7 +7,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const selected = await db.ttsChatVoice.findMany({
@ -22,7 +22,8 @@ export async function GET(req: Request) {
const data = selected.map(s => ({ chatter_id: new Number(s.chatterId), voice: voiceNamesMapped[s.ttsVoiceId] }))
return NextResponse.json(data);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[TTS/SELECTED]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -30,12 +31,11 @@ export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { voice, chatterId }: { voice: string, chatterId: number } = await req.json();
if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase()))
return NextResponse.json({ message: 'Voice does not exist.', error: null, value: null }, { status: 400 })
if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 });
const v = voices.find(v => v.toLowerCase() == voice.toLowerCase())
const voiceData = await db.ttsVoice.findFirst({
@ -44,7 +44,7 @@ export async function POST(req: Request) {
}
})
if (!voiceData)
return NextResponse.json({ message: 'Voice does not exist.', error: null, value: null }, { status: 400 })
return new NextResponse("Bad Request", { status: 400 });
await db.ttsChatVoice.upsert({
where: {
@ -63,8 +63,9 @@ export async function POST(req: Request) {
}
});
return NextResponse.json({ message: null, error: null, value: null }, { status: 200 })
return new NextResponse("", { status: 200 });
} catch (error) {
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
console.log("[TTS/SELECTED]", error);
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request, { params } : { params: { id: string } })
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
let id = req.headers?.get('x-api-key')
@ -23,7 +23,7 @@ export async function GET(req: Request, { params } : { params: { id: string } })
return NextResponse.json(tokens);
} catch (error) {
console.log("[TOKEN/GET]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}
@ -31,7 +31,7 @@ export async function DELETE(req: Request, { params } : { params: { id: string }
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { id } = params
@ -45,6 +45,6 @@ export async function DELETE(req: Request, { params } : { params: { id: string }
return NextResponse.json(token);
} catch (error) {
console.log("[TOKEN/DELETE]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req);
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const api = await db.twitchConnection.findFirst({
@ -15,7 +15,7 @@ export async function GET(req: Request) {
}
})
if (!api) {
return NextResponse.json({ message: 'You do not have permission for this.', error: null, value: null }, { status: 403 })
return new NextResponse("Forbidden", { status: 403 });
}
const data = {
@ -28,6 +28,6 @@ export async function GET(req: Request) {
return NextResponse.json(data);
} catch (error) {
console.log("[TOKENS/GET]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function POST(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
let { userId, label } = await req.json();
@ -29,7 +29,8 @@ export async function POST(req: Request) {
return NextResponse.json(token);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong.', error: error, value: null }, { status: 500 });
console.log("[TOKEN/POST]", error);
return new NextResponse("Internal Error", { status: 500});
}
}
@ -37,7 +38,7 @@ export async function DELETE(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { id } = await req.json();
@ -54,7 +55,8 @@ export async function DELETE(req: Request) {
return NextResponse.json(token);
} catch (error) {
return NextResponse.json({ message: 'Something went wrong.', error: error, value: null }, { status: 500 });
console.log("[TOKEN/DELETE]", error);
return new NextResponse("Internal Error", { status: 500});
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUserWithImpersonation(req)
if (!user) {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const tokens = await db.apiKey.findMany({
@ -18,6 +18,6 @@ export async function GET(req: Request) {
return NextResponse.json(tokens);
} catch (error) {
console.log("[TOKENS/GET]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

View File

@ -6,7 +6,7 @@ export async function GET(req: Request) {
try {
const user = await fetchUser(req)
if (!user || user.role != "ADMIN") {
return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 });
return new NextResponse("Unauthorized", { status: 401 });
}
const { searchParams } = new URL(req.url)
@ -36,6 +36,6 @@ export async function GET(req: Request) {
return NextResponse.json(users)
} catch (error) {
console.log("[USERS]", error);
return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 })
return new NextResponse("Internal Error", { status: 500 });
}
}

58
app/socket/page.tsx Normal file
View File

@ -0,0 +1,58 @@
"use client"
import { Button } from '@/components/ui/button';
import { v4 } from "uuid"
import { useEffect, useState } from 'react';
import useWebSocket from 'react-use-websocket';
const socketUrl = 'wss://echo.websocket.org';
const SocketPage = () => {
const [mounted, setMounted] = useState(false)
const {
sendMessage,
sendJsonMessage,
lastMessage,
lastJsonMessage,
readyState,
getWebSocket,
} = useWebSocket(socketUrl, {
onOpen: () => console.log('opened'),
onMessage: (e) => console.log("MESSAGE", e),
onError: (e) => console.error(e),
shouldReconnect: (closeEvent) => { console.log("Reconnect"); return true; },
});
const [messageHistory, setMessageHistory] = useState<MessageEvent[]>([]);
useEffect(() => {
if (lastMessage !== null) {
console.log("LAST", lastMessage)
setMessageHistory((prev) => prev.concat(lastMessage));
}
}, [lastMessage, setMessageHistory]);
useEffect(() => {
if (!mounted) {
setMounted(true)
}
}, [])
return (<div className="w-full bg-blue-300">
<p>Hello</p>
<p>{readyState}</p>
<Button onClick={() => sendMessage("uisdhnishdadasdfsd " + v4())}>
Click on me
</Button>
<div>
{lastMessage ? <span>Last message: {lastMessage.data}</span> : null}
<ul>
{messageHistory.map((message, idx) => (
<p className='block' key={idx}>{message ? message.data : null}</p>
))}
</ul>
</div>
</div>)
}
export default SocketPage

View File

@ -1,10 +1,10 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Button } from "../ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
import { Label } from "../ui/label";
import axios from "axios";
export interface ConnectionDefault {
@ -21,10 +21,7 @@ export const ConnectionDefaultElement = ({
const OnDefaultConnectionUpdate = function (con: { name: string, clientId: string, token: string, type: string, scope: string, expiresAt: Date }) {
if (connection && con.name == connection.name)
return
if (connection && !connections.some(c => c.clientId == connection.clientId && c.name == connection.name && c.token == connection.token))
return
return;
axios.put('/api/connection/default', { name: con.name, type: con.type })
.then(d => {
@ -36,6 +33,7 @@ export const ConnectionDefaultElement = ({
const con = connections.filter((c: any) => c.type == type && c.default)
if (con.length > 0)
OnDefaultConnectionUpdate(con[0])
console.log('default', type, connections.filter(c => c.type == type).length > 0)
}, [])
return (

View File

@ -2,12 +2,12 @@
import axios from "axios";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Button } from "../ui/button";
import { useRouter } from "next/navigation";
import { v4 } from "uuid";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { Input } from "../ui/input";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { env } from "process";

View File

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { FaExclamationTriangle } from "react-icons/fa";
import { Button } from "@/components/ui/button";
import { Button } from "../ui/button";
import { X } from "lucide-react";

View File

@ -1,37 +1,32 @@
import axios from "axios";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { z } from "zod";
import { Trash2Icon } from "lucide-react";
import { Label } from "../ui/label";
import { HelpCircleIcon, Trash2Icon } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip"
import { Checkbox } from "../ui/checkbox";
interface Permission {
id: string | undefined
id: string|undefined
path: string
allow: boolean | null
allow: boolean|null
groupId: string
edit: boolean
showEdit: boolean
isNew: boolean
permissionPaths: { path: string, description: string }[]
adder: (id: string, path: string, allow: boolean | null) => void
remover: (redemption: { id: string, path: string, allow: boolean | null }) => void,
contains: (path: string) => boolean
adder: (id: string, path: string, allow: boolean|null) => void
remover: (redemption: { id: string, path: string, allow: boolean|null }) => void
}
const permissionPathSchema = z.string({
required_error: "Permission path should be available.",
invalid_type_error: "Permission path must be a string"
}).regex(/^[\w\-\.]{1,64}$/, "Permission path must contain only letters, numbers, dashes, periods.")
const permissionAllowSchema = z.boolean({
required_error: "Permission should be allowed, revoked or inherited from parent.",
invalid_type_error: "Something went wrong."
}).nullable()
const GroupPermission = ({
id,
path,
@ -42,43 +37,18 @@ const GroupPermission = ({
isNew,
permissionPaths,
adder,
remover,
contains
remover
}: Permission) => {
const [pathOpen, setPathOpen] = useState(false)
const [isEditable, setIsEditable] = useState(edit)
const [oldData, setOldData] = useState<{ path: string, allow: boolean | null } | undefined>(undefined)
const [permission, setPermission] = useState<{ id: string | undefined, path: string, allow: boolean | null }>({ id, path, allow });
const [error, setError] = useState<string | undefined>(undefined)
const [oldData, setOldData] = useState<{ path: string, allow: boolean|null } | undefined>(undefined)
const [permission, setPermission] = useState<{ id: string|undefined, path: string, allow: boolean|null }>({ id, path, allow });
function Save() {
setError(undefined)
if (!permission || !permission.path)
return
if (!permissionPaths.some(p => p.path == permission.path)) {
setError('Permission does not exist.')
return
}
const permissionPathValidation = permissionPathSchema.safeParse(permission.path)
if (!permissionPathValidation.success) {
setError(JSON.parse(permissionPathValidation.error['message'])[0].message)
return
}
const permissionAllowValidation = permissionAllowSchema.safeParse(permission.allow)
if (!permissionAllowValidation.success) {
setError(JSON.parse(permissionAllowValidation.error['message'])[0].message)
return
}
if (isNew) {
if (contains(permission.path)) {
setError("Permission already exists.")
return;
}
axios.post("/api/settings/groups/permissions", {
path: permission.path,
allow: permission.allow,
@ -91,11 +61,6 @@ const GroupPermission = ({
setPermission({ id: undefined, path: "", allow: true })
})
} else {
if (!contains(permission.path)) {
setError("Permission does not exists.")
return;
}
axios.put("/api/settings/groups/permissions", {
id: permission.id,
path: permission.path,
@ -109,8 +74,7 @@ const GroupPermission = ({
function Cancel() {
if (!oldData)
return
setError(undefined)
setPermission({ ...oldData, id: permission.id })
setIsEditable(false)
setOldData(undefined)
@ -160,7 +124,7 @@ const GroupPermission = ({
value={p.path}
key={p.path}
onSelect={(value) => {
setPermission({ ...permission, path: permissionPaths.find(v => v.path.toLowerCase() == value.toLowerCase())?.path ?? value.toLowerCase() })
setPermission({ ...permission, path: permissionPaths.find(v => v.path.toLowerCase() == value.toLowerCase())?.path ?? value.toLowerCase()})
setPathOpen(false)
}}>
<div>
@ -171,7 +135,7 @@ const GroupPermission = ({
{p.description}
</div>
</div>
</CommandItem>
))}
</CommandGroup>
@ -207,11 +171,6 @@ const GroupPermission = ({
}}
disabled={!isEditable || permission === undefined} />
</div>
{error &&
<p className="w-[380px] text-red-600 text-wrap text-sm">
{error}
</p>
}
<div>
{isEditable &&
<Button

View File

@ -2,18 +2,12 @@ import axios from "axios";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Label } from "../ui/label";
import { Maximize2, Minimize2, Trash2Icon } from "lucide-react";
import GroupPermission from "./group-permission";
import { z } from "zod";
import UserList from "./user-list-group";
const groupNameSchema = z.string({
required_error: "Group name is required.",
invalid_type_error: "Group name must be a string"
}).regex(/^[\w\-\s]{1,20}$/, "Group name must contain only letters, numbers, spaces, dashes, and underscores.")
const prioritySchema = z.string().regex(/^-?\d{1,5}$/, "Priority must be a valid number.")
interface Group {
id: string | undefined
name: string
@ -25,8 +19,7 @@ interface Group {
permissionPaths: { path: string, description: string }[]
specialGroups: string[]
adder: (id: string, name: string, priority: number) => void
remover: (group: { id: string, name: string, priority: number }) => void,
contains: (groupName: string) => boolean
remover: (group: { id: string, name: string, priority: number }) => void
}
const GroupElement = ({
@ -40,13 +33,12 @@ const GroupElement = ({
permissionPaths,
specialGroups,
adder,
remover,
contains
remover
}: Group) => {
const [isEditable, setIsEditable] = useState(edit)
const [isNew, setIsNew] = useState(isNewGroup)
const [isMinimized, setIsMinimized] = useState(true)
const [oldData, setOldData] = useState<{ id: string | undefined, name: string, priority: number } | undefined>(undefined)
const [oldData, setOldData] = useState<{ name: string, priority: number } | undefined>(undefined)
const [group, setGroup] = useState<{ id: string | undefined, name: string, priority: number }>({ id, name, priority })
const [permissions, setPermissions] = useState<{ id: string, path: string, allow: boolean | null }[]>(permissionsLoaded);
const isSpecial = (isEditable || oldData === undefined) && !!group && specialGroups.includes(group?.name)
@ -61,25 +53,20 @@ const GroupElement = ({
setPermissions(permissions.filter(p => p.id != permission.id))
}
function containsPermission(path: string) {
return permissions.some(p => p.path == path)
}
const nameSchema = z.string({
required_error: "Name is required.",
invalid_type_error: "Name must be a string"
}).regex(/^[\w\-\s]{1,20}$/, "Name must contain only letters, numbers, dashes, and underscores.")
const prioritySchema = z.string().regex(/^-?\d{1,5}$/, "Priority must be a valid number.")
function Save() {
setError(undefined)
if (!isNew && !id) {
setError("Something went wrong.")
if (!isNew && !id)
return
}
if (!group.name) {
setError("Set a value for the group name")
return
}
const groupNameValidation = groupNameSchema.safeParse(group.name)
if (!groupNameValidation.success) {
setError(JSON.parse(groupNameValidation.error['message'])[0].message)
const nameValidation = nameSchema.safeParse(group.name)
if (!nameValidation.success) {
setError(JSON.parse(nameValidation.error['message'])[0].message)
return
}
@ -90,11 +77,6 @@ const GroupElement = ({
}
if (isNew || group.id?.startsWith('$')) {
if (contains(group.name) && !isSpecial) {
setError("Group already exists. Use another name.")
return;
}
axios.post("/api/settings/groups", {
name: group.name,
priority: group.priority
@ -103,6 +85,7 @@ const GroupElement = ({
setError("Something went wrong.")
return
}
console.log("DATA", d.data)
if (specialGroups.includes(group.name)) {
setIsNew(false)
@ -113,49 +96,33 @@ const GroupElement = ({
setGroup({ id: undefined, name: "", priority: 0 })
}
}).catch(() => {
setError("Something went wrong.")
setError("Potential group name duplicate.")
})
} else {
if (!contains(group.name)) {
setError("Group does not exist. Something went wrong.")
return;
}
axios.put("/api/settings/groups", {
id: group.id,
name: group.name,
priority: group.priority
}).then(d => {
console.log("DATA", d.data)
setIsEditable(false)
}).catch(() => {
setError("Something went wrong.")
setError("Potential group name duplicate.")
})
}
}
function Cancel() {
setError(undefined)
if (!oldData)
return
setError(undefined)
setGroup({ ...oldData, id: group.id })
setIsEditable(false)
setOldData(undefined)
}
function Delete() {
if (!group.id) {
return;
}
if (!contains(oldData?.name ?? group.name)) {
if (oldData?.id)
remover({ ...oldData, id: oldData.id })
else
remover({ ...group, id: group.id })
return;
}
axios.delete("/api/settings/groups?id=" + group.id)
.then(d => {
if (specialGroups.includes(group.name)) {
@ -183,7 +150,7 @@ const GroupElement = ({
Group Name
</Label>
{isSpecial &&
<div className="bg-white text-muted text-xs p-1 rounded m-0 inline-block">
<div className="bg-white text-muted text-xs p-1 rounded m-1 inline-block">
auto-generated
</div>
}
@ -206,10 +173,12 @@ const GroupElement = ({
onChange={e => setGroup(d => {
let temp = { ...group }
const v = parseInt(e.target.value)
if (e.target.value.length == 0 || Number.isNaN(v)) {
if (e.target.value.length == 0) {
temp.priority = 0
} else if (!Number.isNaN(v) && Number.isSafeInteger(v)) {
temp.priority = v
} else if (Number.isNaN(v)) {
temp.priority = 0
}
return temp
})}
@ -281,8 +250,7 @@ const GroupElement = ({
isNew={false}
permissionPaths={permissionPaths}
adder={addPermission}
remover={removePermission}
contains={containsPermission} />
remover={removePermission} />
</div>
)}
<div
@ -297,8 +265,7 @@ const GroupElement = ({
isNew={true}
permissionPaths={permissionPaths}
adder={addPermission}
remover={removePermission}
contains={containsPermission} />
remover={removePermission} />
</div>
</div>
}

View File

@ -1,6 +1,6 @@
import { Info, X } from "lucide-react";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Button } from "../ui/button";
interface NoticeProps {

View File

@ -4,10 +4,10 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
import { Label } from "../ui/label";
import { Maximize2, Minimize2, Trash2Icon } from "lucide-react";
import { ActionType } from "@prisma/client";
import { boolean, z } from "zod";
import { boolean } from "zod";
const actionTypes = [
@ -19,15 +19,13 @@ const actionTypes = [
"type": "text",
"label": "File path",
"key": "file_path",
"placeholder": "Enter local file path, relative or full.",
"required": true
"placeholder": "Enter local file path, relative or full."
},
{
"type": "text",
"label": "File content",
"key": "file_content",
"placeholder": "Enter the text to write to the file.",
"required": true
"placeholder": "Enter the text to write to the file."
}
]
},
@ -39,15 +37,13 @@ const actionTypes = [
"type": "text",
"label": "File path",
"key": "file_path",
"placeholder": "Enter local file path, relative or full.",
"required": true
"placeholder": "Enter local file path, relative or full."
},
{
"type": "text",
"label": "File content",
"key": "file_content",
"placeholder": "Enter the text to append to the file.",
"required": true
"placeholder": "Enter the text to append to the file."
}
]
},
@ -64,8 +60,7 @@ const actionTypes = [
"type": "text",
"label": "File path",
"key": "file_path",
"placeholder": "Enter local file path, relative or full.",
"required": true
"placeholder": "Enter local file path, relative or full."
}
]
},
@ -83,7 +78,6 @@ const actionTypes = [
"label": "TTS Voice",
"key": "tts_voice",
"placeholder": "Name of an enabled TTS voice",
"required": true
}
]
},
@ -95,15 +89,13 @@ const actionTypes = [
"type": "text",
"label": "Scene Name",
"key": "scene_name",
"placeholder": "Name of the OBS scene",
"required": true
"placeholder": "Name of the OBS scene"
},
{
"type": "text",
"label": "Scene Item Name",
"key": "scene_item_name",
"placeholder": "Name of the OBS scene item / source",
"required": true
"placeholder": "Name of the OBS scene item / source"
}
]
},
@ -115,23 +107,20 @@ const actionTypes = [
"type": "text",
"label": "Scene Name",
"key": "scene_name",
"placeholder": "Name of the OBS scene",
"required": true
"placeholder": "Name of the OBS scene"
},
{
"type": "text",
"label": "Scene Item Name",
"key": "scene_item_name",
"placeholder": "Name of the OBS scene item / source",
"required": true
"placeholder": "Name of the OBS scene item / source"
},
{
"type": "text-values",
"label": "Visible",
"key": "obs_visible",
"placeholder": "true for visible; false otherwise",
"values": ["true", "false"],
"required": true
"values": ["true", "false"]
}
]
},
@ -143,22 +132,19 @@ const actionTypes = [
"type": "text",
"label": "Scene Name",
"key": "scene_name",
"placeholder": "Name of the OBS scene",
"required": true
"placeholder": "Name of the OBS scene"
},
{
"type": "text",
"label": "Scene Item Name",
"key": "scene_item_name",
"placeholder": "Name of the OBS scene item / source",
"required": true
"placeholder": "Name of the OBS scene item / source"
},
{
"type": "number",
"label": "Index",
"key": "obs_index",
"placeholder": "index, starting from 0.",
"required": true
"placeholder": "index, starting from 0."
}
]
},
@ -171,7 +157,6 @@ const actionTypes = [
"label": "Sleep",
"key": "sleep",
"placeholder": "Time in milliseconds to do nothing",
"required": true
}
]
},
@ -184,7 +169,6 @@ const actionTypes = [
"label": "nightbot.play",
"key": "nightbot_play",
"placeholder": "",
"required": true
}
]
},
@ -197,7 +181,6 @@ const actionTypes = [
"label": "nightbot.pause",
"key": "nightbot_pause",
"placeholder": "",
"required": true
}
]
},
@ -210,7 +193,6 @@ const actionTypes = [
"label": "nightbot.skip",
"key": "nightbot_skip",
"placeholder": "",
"required": true
}
]
},
@ -223,7 +205,6 @@ const actionTypes = [
"label": "nightbot.clear_playlist",
"key": "nightbot_clear_playlist",
"placeholder": "",
"required": true
}
]
},
@ -236,16 +217,11 @@ const actionTypes = [
"label": "nightbot.clear_queue",
"key": "nightbot_clear_queue",
"placeholder": "",
"required": true
}
]
},
]
const nameSchema = z.string({
required_error: "Name is required.",
invalid_type_error: "Name must be a string"
}).regex(/^[\w\-\s]{1,32}$/, "Name must contain only letters, numbers, spaces, dashes, and underscores.")
interface RedeemableAction {
name: string
@ -280,68 +256,44 @@ const RedemptionAction = ({
const [isEditable, setIsEditable] = useState(edit)
const [isMinimized, setIsMinimized] = useState(!isNew)
const [oldData, setOldData] = useState<{ n: string, t: ActionType | undefined, d: { [k: string]: string } } | undefined>(undefined)
const [error, setError] = useState<string | undefined>(undefined)
function Save(isNew: boolean) {
setError(undefined)
if (!actionName) {
setError("Name is required.")
function Save(name: string, type: ActionType | undefined, data: { [key: string]: string }, isNew: boolean) {
// TODO: validation
if (!name) {
return
}
const nameValidation = nameSchema.safeParse(actionName)
if (!nameValidation.success) {
setError(JSON.parse(nameValidation.error['message'])[0].message)
console.log('typeeee', type)
if (!type) {
return
}
if (!actionType) {
setError("Action type is required.")
if (!data) {
return
}
if (!actionTypes.some(t => t.value == actionType.value)) {
setError("Invalid action type given.")
return
}
if (!actionData) {
setError("Something went wrong with the data.")
return
}
const inputs = actionTypes.find(a => a.value == actionType.value && a.name == actionType.name)!.inputs
const required = inputs.filter(i => i.required)
for (const input of required) {
if (!(input.key in actionData)) {
setError("The field '" + input.label + "' is required.")
return
}
}
let info: any = {
name: actionName,
type: actionType.value,
name,
type
}
info.data = actionData
info = { ...info, ...data }
if (isNew) {
axios.post("/api/settings/redemptions/actions", info)
.then(d => {
adder(actionName, actionType.value, actionData)
adder(name, type, data)
setActionName("")
setActionType(undefined)
setActionData({})
})
.catch(error => setError(error.response.data.message))
} else {
axios.put("/api/settings/redemptions/actions", info)
.then(d => {
setIsEditable(false)
})
.catch(error => setError(error.response.data.message))
}
}
function Cancel(data: { n: string, t: ActionType | undefined, d: { [k: string]: any } } | undefined) {
setError(undefined)
if (!data)
return
@ -353,7 +305,7 @@ const RedemptionAction = ({
}
function Delete() {
axios.delete("/api/settings/redemptions/actions?action_name=" + actionName)
axios.delete("/api/settings/redemptions/actions?action_name=" + name)
.then(d => {
remover(d.data)
})
@ -479,10 +431,12 @@ const RedemptionAction = ({
onChange={e => setActionData(d => {
let abc = { ...actionData }
const v = parseInt(e.target.value)
if (e.target.value.length == 0 || Number.isNaN(v)) {
if (e.target.value.length == 0) {
abc[i.key] = "0"
} else if (!Number.isNaN(v) && Number.isSafeInteger(v)) {
abc[i.key] = v.toString()
} else if (Number.isNaN(v)) {
abc[i.key] = "0"
}
return abc
})}
@ -543,7 +497,7 @@ const RedemptionAction = ({
if (!!connection) {
setActionData({
'oauth_name': connection.name,
'oauth_type': connection.type
'oauth_type' : connection.type
})
}
else
@ -563,6 +517,7 @@ const RedemptionAction = ({
</Popover>
</div>
}
return <div key={i.key}></div>
})}
</div>
}
@ -593,16 +548,11 @@ const RedemptionAction = ({
</div>
}
</div>
{error &&
<div className="text-red-600 font-bold">
{error}
</div>
}
<div>
{isEditable &&
<Button
className="m-3"
onClick={() => Save(isNew)}>
onClick={() => Save(actionName, actionType?.value, actionData, isNew)}>
{isNew ? "Add" : "Save"}
</Button>
}

View File

@ -4,14 +4,14 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
import { Label } from "../ui/label";
import { HelpCircleIcon, Trash2Icon } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
} from "../ui/tooltip"
interface Redemption {
id: string | undefined
@ -47,42 +47,17 @@ const OBSRedemption = ({
const [order, setOrder] = useState<number>(numbering)
const [isEditable, setIsEditable] = useState(edit)
const [oldData, setOldData] = useState<{ r: { id: string, title: string } | undefined, a: string | undefined, o: number } | undefined>(undefined)
const [error, setError] = useState<string | undefined>(undefined)
useEffect(() => {
setTwitchRedemption(twitchRedemptions.find(r => r.id == redemptionId))
}, [])
function Save() {
setError(undefined)
if (!isNew && !id) {
setError('Something went wrong. Refresh the page.')
// TODO: validation
if (!isNew && !id)
return
}
if (!action) {
setError('An action must be selected.')
if (!action || !twitchRedemption)
return
}
if (!actions.some(a => a == action)) {
setError('Ensure the action selected still exists.')
return
}
if (!twitchRedemption) {
setError('A Twitch redemption must be selected.')
return
}
if (!twitchRedemptions.some(r => r.id == twitchRedemption.id)) {
setError('Ensure the action selected still exists.')
return
}
if (isNaN(order)) {
setError('The order cannot be NaN.')
setOrder(0)
return
}
if (isNew) {
axios.post("/api/settings/redemptions", {
@ -113,7 +88,6 @@ const OBSRedemption = ({
if (!oldData)
return
setError(undefined)
setAction(oldData.a)
setTwitchRedemption(oldData.r)
setOrder(oldData.o)
@ -169,8 +143,7 @@ const OBSRedemption = ({
value={redemption.title}
key={redemption.id}
onSelect={(value) => {
console.log('tr', value, redemption.id, redemption.title)
setTwitchRedemption(redemption)
setTwitchRedemption(twitchRedemptions.find(v => v.title.toLowerCase() == value.toLowerCase()))
setRedemptionOpen(false)
}}>
{redemption.title}
@ -245,37 +218,23 @@ const OBSRedemption = ({
className="inline-block w-[300px]"
id="name"
placeholder="Enter an order number for this action"
onChange={e => setOrder(d => {
let temp = order
const v = parseInt(e.target.value)
if (e.target.value.length == 0 || Number.isNaN(v)) {
temp = 0
} else if (!Number.isNaN(v) && Number.isSafeInteger(v)) {
temp = v
}
return temp
})}
onChange={e => setOrder(e.target.value.length == 0 ? 0 : parseInt(e.target.value))}
value={order}
readOnly={!isEditable} />
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircleIcon
className="inline-block ml-3" />
className="inline-block ml-3"/>
</TooltipTrigger>
<TooltipContent>
<p>This decides when this action will be done relative to other actions for this Twitch redemption.<br />
Action start from lowest to highest order number.<br />
Equal order numbers cannot be guaranteed proper order.</p>
<p>This decides when this action will be done relative to other actions for this Twitch redemption.<br/>
Action start from lowest to highest order number.<br/>
Equal order numbers cannot be guaranteed proper order.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{error &&
<div className="text-red-600 font-bold">
{error}
</div>
}
<div>
{isEditable &&
<Button

View File

@ -28,18 +28,17 @@ import RoleGate from "@/components/auth/role-gate";
interface UsersGroup {
groupId: string
groupName: string
//userList: { id: number, username: string }[]
//knownUsers: { id: number, username: string }[]
}
const ITEMS_PER_PAGE: number = 10;
const usernameSchema = z.string({
required_error: "Name is required.",
invalid_type_error: "Name must be a string"
}).regex(/^[\w\-]{4,25}$/, "Invalid Twitch username.")
const UserList = ({
groupId,
groupName
groupName,
//userList,
//knownUsers
}: UsersGroup) => {
const [usersListOpen, setUsersListOpen] = useState(false)
const [users, setUsers] = useState<{ id: number, username: string }[]>([])
@ -61,8 +60,7 @@ const UserList = ({
}).then(d => {
setUsers(d.data)
setKnownUsers(d.data)
var maxPages = Math.ceil(d.data.length / ITEMS_PER_PAGE)
setMaxPages(maxPages)
setMaxPages(Math.ceil(d.data.length / ITEMS_PER_PAGE))
})
}, [groupId, page])
@ -71,12 +69,17 @@ const UserList = ({
setUsersListOpen(false)
}
const usernameSchema = z.string({
required_error: "Name is required.",
invalid_type_error: "Name must be a string"
}).regex(/^[\w\-]{4,25}$/, "Invalid Twitch username.")
function AddUsername() {
setError(undefined)
const usernameValidation = usernameSchema.safeParse(newUser)
if (!usernameValidation.success) {
setError(JSON.parse(usernameValidation.error['message'])[0].message)
const nameValidation = usernameSchema.safeParse(newUser)
if (!nameValidation.success) {
setError(JSON.parse(nameValidation.error['message'])[0].message)
return
}
@ -92,16 +95,12 @@ const UserList = ({
logins: newUser
}
}).then(d => {
if (!d.data) {
setError("That was not a known Twitch username.")
if (!d.data)
return
}
user = d.data[0]
if (!user) {
setError("That was not a known Twitch username.")
if (!user)
return
}
if (deletedUsers.find(u => u.id == user!.id))
setDeletedUsers(deletedUsers.filter(u => u.id != user!.id))
@ -112,7 +111,7 @@ const UserList = ({
setNewUser("")
setMaxPages(Math.ceil((users.length + 1) / ITEMS_PER_PAGE))
}).catch(e => {
setError("That was not a known Twitch username.")
setError("Username does not exist.")
})
return
}

View File

@ -1,7 +1,7 @@
import { X } from "lucide-react";
import React, { useState } from "react";
import { FaExclamationCircle } from "react-icons/fa";
import { Button } from "@/components/ui/button";
import { Button } from "../ui/button";
interface NoticeProps {