From 624b3fa63bf35d9f0c72869201c84a96b6470d7c Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 25 Aug 2024 21:35:46 +0000 Subject: [PATCH] Added basic validation for requests --- app/(protected)/connection/authorize/page.tsx | 2 +- app/(protected)/page.tsx | 6 +- .../settings/groups/permissions/page.tsx | 60 ++++----- app/(protected)/settings/redemptions/page.tsx | 2 - app/api/account/authorize/route.ts | 12 +- app/api/account/impersonate/route.ts | 16 +-- app/api/account/reauthorize/route.ts | 8 +- app/api/account/redemptions/route.ts | 9 +- app/api/account/route.ts | 9 +- app/api/connection/authorize/route.ts | 2 + app/api/connection/default/route.ts | 4 +- app/api/info/version/route.ts | 2 +- .../connections/twitch/delete/route.ts | 4 +- app/api/settings/connections/twitch/route.ts | 4 +- app/api/settings/groups/chatters/route.ts | 62 +++++---- app/api/settings/groups/permissions/route.ts | 53 ++++---- app/api/settings/groups/route.ts | 47 ++++--- .../settings/groups/twitchchatters/route.ts | 20 +-- app/api/settings/groups/users/route.ts | 25 ++-- app/api/settings/redemptions/actions/route.ts | 44 ++++--- app/api/settings/redemptions/route.ts | 38 +++--- app/api/settings/tts/default/route.ts | 33 +++-- app/api/settings/tts/filter/users/route.ts | 23 ++-- app/api/settings/tts/filter/words/route.ts | 121 +++++++++--------- app/api/settings/tts/route.ts | 14 +- app/api/settings/tts/selected/route.ts | 17 ++- app/api/token/[id]/route.ts | 8 +- app/api/token/bot/route.ts | 6 +- app/api/token/route.ts | 10 +- app/api/tokens/route.ts | 4 +- app/api/users/route.ts | 4 +- app/socket/page.tsx | 58 --------- components/elements/connection-default.tsx | 10 +- components/elements/connection.tsx | 4 +- components/elements/error-notice.tsx | 2 +- components/elements/group-permission.tsx | 81 +++++++++--- components/elements/group.tsx | 60 +++++---- components/elements/info-notice.tsx | 2 +- components/elements/redeemable-action.tsx | 114 ++++++++++++----- components/elements/redemption.tsx | 63 +++++++-- components/elements/user-list-group.tsx | 35 ++--- components/elements/warning-notice.tsx | 2 +- 42 files changed, 608 insertions(+), 492 deletions(-) delete mode 100644 app/socket/page.tsx diff --git a/app/(protected)/connection/authorize/page.tsx b/app/(protected)/connection/authorize/page.tsx index 763698f..633466f 100644 --- a/app/(protected)/connection/authorize/page.tsx +++ b/app/(protected)/connection/authorize/page.tsx @@ -47,7 +47,7 @@ export default function Home() { else setLoaded(true) }) - }, [session]) + }, [session, status]) return (
diff --git a/app/(protected)/page.tsx b/app/(protected)/page.tsx index 0a87c22..5c4eee1 100644 --- a/app/(protected)/page.tsx +++ b/app/(protected)/page.tsx @@ -17,17 +17,13 @@ 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() { - const data = await axios.post("/api/account") - if (data == null || data == undefined) { - console.log("ERROR") - } + await axios.post("/api/account") } saveAccount().catch(console.error) diff --git a/app/(protected)/settings/groups/permissions/page.tsx b/app/(protected)/settings/groups/permissions/page.tsx index b14b49b..72485ca 100644 --- a/app/(protected)/settings/groups/permissions/page.tsx +++ b/app/(protected)/settings/groups/permissions/page.tsx @@ -26,7 +26,6 @@ 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 = () => { @@ -53,7 +52,6 @@ const GroupPermissionPage = () => { return setPreviousUsername(session.user?.name) - // TODO: fetch groups & permissions axios.get('/api/settings/groups') .then(d => { for (let groupName of specialGroups) @@ -66,52 +64,48 @@ const GroupPermissionPage = () => { setGroups(d.data) }) }) - // TODO: filter permissions by group? }, [session]) return (
Groups & Permissions
- {/*
); diff --git a/app/(protected)/settings/redemptions/page.tsx b/app/(protected)/settings/redemptions/page.tsx index 90c2b08..19cb45b 100644 --- a/app/(protected)/settings/redemptions/page.tsx +++ b/app/(protected)/settings/redemptions/page.tsx @@ -8,7 +8,6 @@ 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" }, @@ -66,7 +65,6 @@ const RedemptionsPage = () => { axios.get('/api/connection') .then(d => { - console.log(d.data.data) setConnections(d.data.data) }) diff --git a/app/api/account/authorize/route.ts b/app/api/account/authorize/route.ts index 6d73447..e0c86d4 100644 --- a/app/api/account/authorize/route.ts +++ b/app/api/account/authorize/route.ts @@ -1,3 +1,5 @@ +// TODO: remove this page. + import axios from 'axios' import { db } from "@/lib/db" import { NextResponse } from "next/server"; @@ -10,7 +12,7 @@ export async function GET(req: Request) { const state = searchParams.get('state') as string if (!code || !scope || !state) { - return new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'Missing oauth2 data.', error: null, value: null }, { status: 400 }); } // Verify state against user id in user table. @@ -21,7 +23,7 @@ export async function GET(req: Request) { }) if (!user) { - return new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'You do not have permissions for this.', error: null, value: null }, { status: 403 }); } // Post to https://id.twitch.tv/oauth2/token @@ -37,7 +39,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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } let info = await axios.get("https://api.twitch.tv/helix/users?login=" + user.name, { @@ -57,9 +59,9 @@ export async function GET(req: Request) { } }) - return new NextResponse("", { status: 200 }); + return NextResponse.json({ message: null, error: null, value: null }, { status: 200 }) } catch (error) { console.log("[ACCOUNT/AUTHORIZE]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/account/impersonate/route.ts b/app/api/account/impersonate/route.ts index 1887674..111cb91 100644 --- a/app/api/account/impersonate/route.ts +++ b/app/api/account/impersonate/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUser(req) if (!user || user.role != "ADMIN") { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -26,7 +26,7 @@ export async function POST(req: Request) { try { const user = await fetchUser(req) if (!user || user.role != "ADMIN") { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -49,7 +49,7 @@ export async function PUT(req: Request) { try { const user = await fetchUser(req) if (!user || user.role != "ADMIN") { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -74,7 +74,7 @@ export async function DELETE(req: Request) { try { const user = await fetchUser(req) if (!user || user.role != "ADMIN") { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error" + error, { status: 500 }); + return NextResponse.json({ message: 'Something went wrong.', error: null, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/account/reauthorize/route.ts b/app/api/account/reauthorize/route.ts index 4669731..8afc6cf 100644 --- a/app/api/account/reauthorize/route.ts +++ b/app/api/account/reauthorize/route.ts @@ -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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const connection = await db.twitchConnection.findFirst({ @@ -18,7 +18,7 @@ export async function GET(req: Request) { } }) if (!connection) { - return new NextResponse("Forbidden", { status: 403 }); + return NextResponse.json({ message: 'You do not have permission for this.', error: null, value: null }, { 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/account/redemptions/route.ts b/app/api/account/redemptions/route.ts index ae30834..380553b 100644 --- a/app/api/account/redemptions/route.ts +++ b/app/api/account/redemptions/route.ts @@ -1,4 +1,3 @@ -import { db } from "@/lib/db" import { NextResponse } from "next/server"; import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation'; import axios from "axios"; @@ -7,16 +6,16 @@ import { updateTwitchToken } from "@/data/twitch-reauthorize"; export async function GET(req: Request) { try { if (!process.env.TWITCH_BOT_CLIENT_ID) - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong.', error: null, value: null }, { status: 500 }) const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const auth = await updateTwitchToken(user.id) if (!auth) - return new NextResponse("Bad Request", { status: 400 }) + return NextResponse.json({ message: 'Failed to authorize to Twitch.', error: null, value: null }, { status: 403 }); try { const redemptions = await axios.get("https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + auth.broadcaster_id, @@ -35,6 +34,6 @@ export async function GET(req: Request) { return NextResponse.json([]); } catch (error) { console.log("[REDEMPTIONS/ACTIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/account/route.ts b/app/api/account/route.ts index b7f1eb5..bdb80fb 100644 --- a/app/api/account/route.ts +++ b/app/api/account/route.ts @@ -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 new NextResponse("Internal Error", { status: 401 }) + if (!user) return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -27,7 +27,7 @@ export async function POST(req: Request) { const session = await auth() const user = session?.user?.name if (!user) { - return new NextResponse("Internal Error", { status: 401 }) + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }) } const exist = await db.user.findFirst({ @@ -54,7 +54,6 @@ export async function POST(req: Request) { username: newUser.name }); } catch (error) { - console.log("[ACCOUNT]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/connection/authorize/route.ts b/app/api/connection/authorize/route.ts index ee7b273..aaab125 100644 --- a/app/api/connection/authorize/route.ts +++ b/app/api/connection/authorize/route.ts @@ -13,6 +13,8 @@ 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 }) diff --git a/app/api/connection/default/route.ts b/app/api/connection/default/route.ts index c66f121..a5723b3 100644 --- a/app/api/connection/default/route.ts +++ b/app/api/connection/default/route.ts @@ -17,7 +17,7 @@ export async function GET(req: Request) { } }) - return NextResponse.json({ error: null, message: "", success: true, data }, { status: 200 }); + return NextResponse.json({ error: null, message: null, 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: "", success: true, data }, { status: 200 }); + return NextResponse.json({ error: null, message: null, success: true, data }, { status: 200 }); } catch (error: any) { return NextResponse.json({ error, message: "Failed to update default connection", success: false }, { status: 500 }); } diff --git a/app/api/info/version/route.ts b/app/api/info/version/route.ts index f35b1cb..7359565 100644 --- a/app/api/info/version/route.ts +++ b/app/api/info/version/route.ts @@ -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." diff --git a/app/api/settings/connections/twitch/delete/route.ts b/app/api/settings/connections/twitch/delete/route.ts index 42efdaf..ff887e4 100644 --- a/app/api/settings/connections/twitch/delete/route.ts +++ b/app/api/settings/connections/twitch/delete/route.ts @@ -6,7 +6,7 @@ export async function POST(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/connections/twitch/route.ts b/app/api/settings/connections/twitch/route.ts index b2712e8..8321fa5 100644 --- a/app/api/settings/connections/twitch/route.ts +++ b/app/api/settings/connections/twitch/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/groups/chatters/route.ts b/app/api/settings/groups/chatters/route.ts index be43191..d80e9c7 100644 --- a/app/api/settings/groups/chatters/route.ts +++ b/app/api/settings/groups/chatters/route.ts @@ -4,12 +4,13 @@ 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); const { searchParams } = new URL(req.url) const groupId = searchParams.get('groupId') as string @@ -17,7 +18,7 @@ export async function GET(req: Request) { const search = searchParams.get('search') as string if (!groupId && search != 'all') - return new NextResponse("Bad Request", { status: 400 }) + return NextResponse.json({ message: 'Something went wrong', error: null, value: null }, { status: 400 }) let page = parseInt(pageString) if (isNaN(page) || page === undefined || page === null) @@ -40,18 +41,15 @@ export async function GET(req: Request) { }) const paginated = search == 'all' ? chatters : chatters.slice(page * 50, (page + 1) * 50) - if (!paginated || paginated.length == 0) { - console.log('No users returned from db') + if (!paginated || paginated.length == 0) 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 new NextResponse("", { status: 403 }) - } + if (!auth) + return NextResponse.json({ message: 'Unauthorized', error: null, value: null }, { status: 403 }) const users = await axios.get("https://api.twitch.tv/helix/users?" + idsString, { headers: { @@ -60,31 +58,38 @@ export async function GET(req: Request) { } }) - if (!users) { - return new NextResponse("", { status: 400 }) - } + if (!users) + return NextResponse.json({ message: 'No users found', error: null, value: null }, { status: 400 }) - if (users.data.data.length == 0) { - console.log('No users returned from twitch') + 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) { - console.log("[GROUPS/USERS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Failed to get', error: error, value: null }, { 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized', error: null, value: null }, { status: 401 }) const { groupId, users }: { groupId: string, users: { id: number, username: string }[] } = await req.json(); - if (!groupId || !users) - return new NextResponse("Bad Request", { status: 400 }); + 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 }) const chatters = await db.chatterGroup.createMany({ data: users.map(u => ({ userId: user.id, chatterId: u.id, groupId, chatterLabel: u.username })) @@ -92,8 +97,7 @@ export async function POST(req: Request) { return NextResponse.json(chatters, { status: 200 }); } catch (error) { - console.log("[GROUPS/USERS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Failed to create', error: error, value: null }, { status: 500 }) } } @@ -101,13 +105,20 @@ export async function DELETE(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized', error: null, value: null }, { status: 401 }) const { searchParams } = new URL(req.url) const groupId = searchParams.get('groupId') as string const ids = searchParams.get('ids') as string - if (!groupId || !ids) - return new NextResponse("Bad Request", { status: 400 }); + 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 }) const chatters = await db.chatterGroup.deleteMany({ where: { @@ -121,7 +132,6 @@ export async function DELETE(req: Request) { return NextResponse.json(chatters); } catch (error) { - console.log("[GROUPS/USERS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Failed to delete.', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/groups/permissions/route.ts b/app/api/settings/groups/permissions/route.ts index 53bb50e..ab8aa64 100644 --- a/app/api/settings/groups/permissions/route.ts +++ b/app/api/settings/groups/permissions/route.ts @@ -1,13 +1,13 @@ 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); const commands = await db.groupPermission.findMany({ where: { @@ -17,20 +17,31 @@ export async function GET(req: Request) { return NextResponse.json(commands.map(({userId, ...attrs}) => attrs)); } catch (error) { - console.log("[GROUPS/PERMISSIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); const { path, allow, groupId }: { path: string, allow: boolean, groupId: string } = await req.json(); if (!path) - return new NextResponse("Bad Request", { status: 400 }); + 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 }); const permission = await db.groupPermission.create({ data: { @@ -43,8 +54,7 @@ export async function POST(req: Request) { return NextResponse.json(permission, { status: 200 }); } catch (error) { - console.log("[GROUPS/PERMISSIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -52,30 +62,30 @@ export async function PUT(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); const { id, path, allow }: { id: string, path: string, allow: boolean|null } = await req.json(); if (!id) - return new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'id does not exist.', error: null, value: null }, { status: 400 }); if (!path) - return new NextResponse("Bad Request", { status: 400 }); - - let data: any = {} - if (!!path) - data = { ...data, path } - data = { ...data, allow } + 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 }); const permission = await db.groupPermission.update({ where: { id }, - data: data + data: { + path, + allow + } }); return NextResponse.json(permission, { status: 200 }); } catch (error) { - console.log("[GROUPS/PERMISSIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -83,7 +93,7 @@ export async function DELETE(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); const { searchParams } = new URL(req.url) const id = searchParams.get('id') as string @@ -95,7 +105,6 @@ export async function DELETE(req: Request) { return NextResponse.json(permission); } catch (error) { - console.log("[GROUPS/PERMISSIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/groups/route.ts b/app/api/settings/groups/route.ts index ea6b7be..ea1c50f 100644 --- a/app/api/settings/groups/route.ts +++ b/app/api/settings/groups/route.ts @@ -1,13 +1,13 @@ 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const actions = await db.group.findMany({ @@ -16,23 +16,30 @@ export async function GET(req: Request) { } }) - return NextResponse.json(actions.map(({userId, ...attrs}) => attrs)); + return NextResponse.json(actions.map(({ userId, ...attrs }) => attrs)); } catch (error) { - console.log("[GROUPS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { name, priority }: { name: string, priority: number } = await req.json(); if (!name) - return new NextResponse("Bad Request", { status: 400 }); + 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 }) const group = await db.group.create({ data: { @@ -44,8 +51,7 @@ export async function POST(req: Request) { return NextResponse.json(group, { status: 200 }); } catch (error) { - console.log("[GROUPS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -53,19 +59,24 @@ export async function PUT(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { id, name, priority }: { id: string, name: string, priority: number } = await req.json(); if (!id) - return new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'id does not exist.', error: null, value: null }, { status: 400 }); if (!name && !priority) - return new NextResponse("Bad Request", { status: 400 }); + 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 }) + } 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({ @@ -77,8 +88,7 @@ export async function PUT(req: Request) { return NextResponse.json(group, { status: 200 }); } catch (error) { - console.log("[GROUPS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -86,7 +96,7 @@ export async function DELETE(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { searchParams } = new URL(req.url) @@ -99,7 +109,6 @@ export async function DELETE(req: Request) { return NextResponse.json(group); } catch (error) { - console.log("[GROUPS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/groups/twitchchatters/route.ts b/app/api/settings/groups/twitchchatters/route.ts index 1439058..2543ed3 100644 --- a/app/api/settings/groups/twitchchatters/route.ts +++ b/app/api/settings/groups/twitchchatters/route.ts @@ -8,13 +8,13 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'Either logins or ids must not be null.', error: null, value: null }, { status: 400 }); } let suffix = "" @@ -27,7 +27,7 @@ export async function GET(req: Request) { const auth = await TwitchUpdateAuthorization(user.id) if (!auth) { - return new NextResponse("", { status: 403 }) + return NextResponse.json({ message: 'You do not have permissions for this.', error: null, value: null }, { status: 403 }); } console.log('TWITCH URL:', 'https://api.twitch.tv/helix/users' + suffix) @@ -39,17 +39,11 @@ export async function GET(req: Request) { } }) - if (!users || !users.data) { - return new NextResponse("", { status: 400 }) - } + if (!users?.data?.data) + return NextResponse.json([], { 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 }))); + return NextResponse.json(users.data.data.map((u: any) => ({ id: u.id, username: u.login })), { status: 200 }); } catch (error) { - console.log("[GROUPS/USERS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/groups/users/route.ts b/app/api/settings/groups/users/route.ts index 50534ee..b2ccf10 100644 --- a/app/api/settings/groups/users/route.ts +++ b/app/api/settings/groups/users/route.ts @@ -1,22 +1,31 @@ import { db } from "@/lib/db" import { NextResponse } from "next/server"; import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation"; -import axios from "axios"; -import { env } from "process"; -import { TwitchUpdateAuthorization } from "@/lib/twitch"; +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.") export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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, @@ -31,10 +40,8 @@ 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) { - console.log("[GROUPS/USERS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Failed to get groups', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/redemptions/actions/route.ts b/app/api/settings/redemptions/actions/route.ts index 6ab5615..23796d4 100644 --- a/app/api/settings/redemptions/actions/route.ts +++ b/app/api/settings/redemptions/actions/route.ts @@ -2,12 +2,13 @@ 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const actions = await db.action.findMany({ @@ -17,17 +18,21 @@ export async function GET(req: Request) { }) return NextResponse.json(actions.map(({ userId, ...attrs }) => attrs)); - } catch (error) { - console.log("[REDEMPTIONS/ACTIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + } catch (error: any) { + return NextResponse.json({ message: null, error: error, value: null }, { 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 }: @@ -35,16 +40,21 @@ 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 && !type) - return new NextResponse("Bad Request", { status: 400 }); + 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 (type == ActionType.OBS_TRANSFORM && (!scene_name || !scene_item_name || !rotation && !position_x && !position_y)) - return new NextResponse("Bad Request", { status: 400 }); + 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 }); if ((type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) && (!file_path || !file_content)) - return new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: '"scene_name", "scene_item_name", "file_path" & "file_content" are required.', error: null, value: null }, { status: 400 }); if (type == ActionType.AUDIO_FILE && !file_path) - return new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: '"scene_name", "scene_item_name" & "file_path" are required.', error: null, value: null }, { 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 new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: '"oauth_name" & "oauth_type" are required.', error: null, value: null }, { status: 400 }); let data: any = {} if (type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) { @@ -78,10 +88,9 @@ async function common(req: Request, action: (id: string, name: string, type: Act action(user.id, name, type, data) - return new NextResponse("", { status: 200 }); + return NextResponse.json({ message: null, error: null, value: null }, { status: 200 }); } catch (error: any) { - //console.log("[REDEMPTIONS/ACTIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: null, error: error, value: null }, { status: 500 }); } } @@ -120,7 +129,7 @@ export async function DELETE(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { searchParams } = new URL(req.url) @@ -135,8 +144,7 @@ export async function DELETE(req: Request) { }) return NextResponse.json(redemptions); - } catch (error) { - console.log("[REDEMPTIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + } catch (error: any) { + return NextResponse.json({ message: null, error: error, value: null }, { status: 500 }); } } \ No newline at end of file diff --git a/app/api/settings/redemptions/route.ts b/app/api/settings/redemptions/route.ts index e24854b..12f0f0b 100644 --- a/app/api/settings/redemptions/route.ts +++ b/app/api/settings/redemptions/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const redemptions = await db.redemption.findMany({ @@ -17,8 +17,7 @@ export async function GET(req: Request) { return NextResponse.json(redemptions.map(({userId, ...attrs}) => attrs)); } catch (error) { - console.log("[REDEMPTIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -26,12 +25,15 @@ export async function POST(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { actionName, redemptionId, order, state }: { actionName: string, redemptionId: string, order: number, state: boolean } = await req.json(); - if (!redemptionId || !actionName && !order && !state) - return new NextResponse("Bad Request", { status: 400 }); + 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 }) const action = await db.action.findFirst({ where: { @@ -39,7 +41,7 @@ export async function POST(req: Request) { } }) if (!action) - return new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'Action does not exist.', error: null, value: null }, { status: 400 }) let data:any = { actionName, @@ -66,8 +68,7 @@ export async function POST(req: Request) { return NextResponse.json(res, { status: 200 }); } catch (error) { - console.log("[REDEMPTIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -75,12 +76,15 @@ export async function PUT(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 && !state) - return new NextResponse("Bad Request", { status: 400 }); + 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 }) const action = await db.action.findFirst({ where: { @@ -88,7 +92,7 @@ export async function PUT(req: Request) { } }) if (!action) - return new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'Action does not exist.', error: null, value: null }, { status: 400 }) let data:any = { actionName, @@ -115,8 +119,7 @@ export async function PUT(req: Request) { return NextResponse.json(res, { status: 200 }); } catch (error) { - console.log("[REDEMPTIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -124,7 +127,7 @@ export async function DELETE(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { searchParams } = new URL(req.url) @@ -137,7 +140,6 @@ export async function DELETE(req: Request) { return NextResponse.json(redemptions); } catch (error) { - console.log("[REDEMPTIONS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/tts/default/route.ts b/app/api/settings/tts/default/route.ts index cd4a442..61639c4 100644 --- a/app/api/settings/tts/default/route.ts +++ b/app/api/settings/tts/default/route.ts @@ -6,49 +6,48 @@ import voices from "@/data/tts"; export async function GET(req: Request) { try { if (!voices) { - return new NextResponse("Voices not available.", { status: 500 }); + return NextResponse.json({ message: 'Failed to load voices', error: null, value: null }, { status: 500 }) } - + const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } return NextResponse.json(user.ttsDefaultVoice); } catch (error) { - console.log("[TTS/FILTER/DEFAULT]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } export async function POST(req: Request) { try { if (!voices) { - return new NextResponse("Voices not available.", { status: 500 }); + return NextResponse.json({ message: 'Failed to load voices', error: null, value: null }, { status: 500 }) } const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { voice } = await req.json(); - if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 }); + if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) + return NextResponse.json({ message: 'Voice does not exist.', error: null, value: null }, { 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 new NextResponse("", { status: 200 }); + return NextResponse.json({ message: null, error: null, value: null }, { status: 200 }) } catch (error) { - console.log("[TTS/FILTER/DEFAULT]", error); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } - return new NextResponse("Internal Error", { status: 500 }); } \ No newline at end of file diff --git a/app/api/settings/tts/filter/users/route.ts b/app/api/settings/tts/filter/users/route.ts index fd87aef..49b3c66 100644 --- a/app/api/settings/tts/filter/users/route.ts +++ b/app/api/settings/tts/filter/users/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -26,12 +26,14 @@ export async function POST(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { username, tag } = await req.json(); - 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 }); + 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 }) const filter = await db.ttsUsernameFilter.upsert({ where: { @@ -52,8 +54,7 @@ export async function POST(req: Request) { return NextResponse.json(filter); } catch (error) { - console.log("[TTS/FILTER/USERS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -61,12 +62,13 @@ export async function DELETE(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { searchParams } = new URL(req.url) const username = searchParams.get('username') as string - if (!username || username.length < 4 || username.length > 25) return new NextResponse("Bad Request", { status: 400 }); + if (!username || username.length < 4 || username.length > 25) + return NextResponse.json({ message: 'Username does not exist.', error: null, value: null }, { status: 400 }) const filter = await db.ttsUsernameFilter.delete({ where: { @@ -79,7 +81,6 @@ export async function DELETE(req: Request) { return NextResponse.json(filter) } catch (error) { - console.log("[TTS/FILTER/USERS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/tts/filter/words/route.ts b/app/api/settings/tts/filter/words/route.ts index d2f23b9..15a4462 100644 --- a/app/api/settings/tts/filter/words/route.ts +++ b/app/api/settings/tts/filter/words/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -26,97 +26,102 @@ export async function POST(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { search, replace } = await req.json(); - 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 }); + 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.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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } export async function PUT(req: Request) { - 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 + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } - }); - return NextResponse.json(filter); - } catch (error) { - console.log("[TTS/FILTER/WORDS]", error); - return new NextResponse("Internal Error", { status: 500 }); - } + 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 }) + } } export async function DELETE(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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.length < 4 || search.length > 200) return new NextResponse("Bad Request", { 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 }) - 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.length < 1) return new NextResponse("Bad Request", { status: 400 }); + if (!id || id.length < 1) + return NextResponse.json({ message: 'id does not exist.', error: null, value: null }, { 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 new NextResponse("Bad Request", { status: 400 }); + + return NextResponse.json({ message: 'Either id or search must not be null.', error: null, value: null }, { status: 400 }) } catch (error) { - console.log("[TTS/FILTER/WORDS]", error); - return new NextResponse("Internal Error" + error, { status: 500 }); + return NextResponse.json({ message: 'Something went wrong.', error: null, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/tts/route.ts b/app/api/settings/tts/route.ts index 068964c..48ae128 100644 --- a/app/api/settings/tts/route.ts +++ b/app/api/settings/tts/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const voiceStates = await db.ttsVoiceState.findMany({ @@ -20,8 +20,7 @@ export async function GET(req: Request) { return NextResponse.json(voiceStates.filter(v => v.state).map(v => voiceNamesMapped[v.ttsVoiceId])); } catch (error) { - console.log("[TTS]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -29,7 +28,7 @@ export async function POST(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { voice, state }: { voice: string, state: boolean } = await req.json(); @@ -37,7 +36,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 new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'Voice does not exist.', error: null, value: null }, { status: 400 }); } await db.ttsVoiceState.upsert({ @@ -57,9 +56,8 @@ export async function POST(req: Request) { } }); - return new NextResponse("", { status: 200 }); + return NextResponse.json({ message: null, error: null, value: null }, { status: 200 }) } catch (error) { - console.log("[TTS/FILTER/USER]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/settings/tts/selected/route.ts b/app/api/settings/tts/selected/route.ts index 79f3a45..1bc8ca4 100644 --- a/app/api/settings/tts/selected/route.ts +++ b/app/api/settings/tts/selected/route.ts @@ -7,7 +7,7 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const selected = await db.ttsChatVoice.findMany({ @@ -22,8 +22,7 @@ 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) { - console.log("[TTS/SELECTED]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } @@ -31,11 +30,12 @@ export async function POST(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { voice, chatterId }: { voice: string, chatterId: number } = await req.json(); - if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 }); + if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) + return NextResponse.json({ message: 'Voice does not exist.', error: null, value: null }, { 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 new NextResponse("Bad Request", { status: 400 }); + return NextResponse.json({ message: 'Voice does not exist.', error: null, value: null }, { status: 400 }) await db.ttsChatVoice.upsert({ where: { @@ -63,9 +63,8 @@ export async function POST(req: Request) { } }); - return new NextResponse("", { status: 200 }); + return NextResponse.json({ message: null, error: null, value: null }, { status: 200 }) } catch (error) { - console.log("[TTS/SELECTED]", error); - return new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/token/[id]/route.ts b/app/api/token/[id]/route.ts index 70aab29..5d0e6b7 100644 --- a/app/api/token/[id]/route.ts +++ b/app/api/token/[id]/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request, { params } : { params: { id: string } }) try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { 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 new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/token/bot/route.ts b/app/api/token/bot/route.ts index cca9cdf..ef4d244 100644 --- a/app/api/token/bot/route.ts +++ b/app/api/token/bot/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req); if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const api = await db.twitchConnection.findFirst({ @@ -15,7 +15,7 @@ export async function GET(req: Request) { } }) if (!api) { - return new NextResponse("Forbidden", { status: 403 }); + return NextResponse.json({ message: 'You do not have permission for this.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/token/route.ts b/app/api/token/route.ts index ba100f3..73c6ec1 100644 --- a/app/api/token/route.ts +++ b/app/api/token/route.ts @@ -6,7 +6,7 @@ export async function POST(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } let { userId, label } = await req.json(); @@ -29,8 +29,7 @@ export async function POST(req: Request) { return NextResponse.json(token); } catch (error) { - console.log("[TOKEN/POST]", error); - return new NextResponse("Internal Error", { status: 500}); + return NextResponse.json({ message: 'Something went wrong.', error: error, value: null }, { status: 500 }); } } @@ -38,7 +37,7 @@ export async function DELETE(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { status: 401 }); } const { id } = await req.json(); @@ -55,8 +54,7 @@ export async function DELETE(req: Request) { return NextResponse.json(token); } catch (error) { - console.log("[TOKEN/DELETE]", error); - return new NextResponse("Internal Error", { status: 500}); + return NextResponse.json({ message: 'Something went wrong.', error: error, value: null }, { status: 500 }); } } diff --git a/app/api/tokens/route.ts b/app/api/tokens/route.ts index f091d07..9380cce 100644 --- a/app/api/tokens/route.ts +++ b/app/api/tokens/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUserWithImpersonation(req) if (!user) { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/api/users/route.ts b/app/api/users/route.ts index a495f51..3e824ff 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -6,7 +6,7 @@ export async function GET(req: Request) { try { const user = await fetchUser(req) if (!user || user.role != "ADMIN") { - return new NextResponse("Unauthorized", { status: 401 }); + return NextResponse.json({ message: 'Unauthorized.', error: null, value: null }, { 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 new NextResponse("Internal Error", { status: 500 }); + return NextResponse.json({ message: 'Something went wrong', error: error, value: null }, { status: 500 }) } } \ No newline at end of file diff --git a/app/socket/page.tsx b/app/socket/page.tsx deleted file mode 100644 index b8ddced..0000000 --- a/app/socket/page.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"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([]); - - useEffect(() => { - if (lastMessage !== null) { - console.log("LAST", lastMessage) - setMessageHistory((prev) => prev.concat(lastMessage)); - } - }, [lastMessage, setMessageHistory]); - - useEffect(() => { - if (!mounted) { - setMounted(true) - } - }, []) - - return (
-

Hello

-

{readyState}

- -
- {lastMessage ? Last message: {lastMessage.data} : null} -
    - {messageHistory.map((message, idx) => ( -

    {message ? message.data : null}

    - ))} -
-
-
) -} - -export default SocketPage \ No newline at end of file diff --git a/components/elements/connection-default.tsx b/components/elements/connection-default.tsx index 3c09b04..0f63599 100644 --- a/components/elements/connection-default.tsx +++ b/components/elements/connection-default.tsx @@ -1,10 +1,10 @@ "use client"; import { useEffect, useState } from "react"; -import { Button } from "../ui/button"; +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 "../ui/label"; +import { Label } from "@/components/ui/label"; import axios from "axios"; export interface ConnectionDefault { @@ -21,7 +21,10 @@ 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; + return + + if (connection && !connections.some(c => c.clientId == connection.clientId && c.name == connection.name && c.token == connection.token)) + return axios.put('/api/connection/default', { name: con.name, type: con.type }) .then(d => { @@ -33,7 +36,6 @@ 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 ( diff --git a/components/elements/connection.tsx b/components/elements/connection.tsx index 9ec4631..8b2216f 100644 --- a/components/elements/connection.tsx +++ b/components/elements/connection.tsx @@ -2,12 +2,12 @@ import axios from "axios"; import { useState } from "react"; -import { Button } from "../ui/button"; +import { Button } from "@/components/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 "../ui/input"; +import { Input } from "@/components/ui/input"; import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; import { env } from "process"; diff --git a/components/elements/error-notice.tsx b/components/elements/error-notice.tsx index bd38971..6ce662a 100644 --- a/components/elements/error-notice.tsx +++ b/components/elements/error-notice.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { FaExclamationTriangle } from "react-icons/fa"; -import { Button } from "../ui/button"; +import { Button } from "@/components/ui/button"; import { X } from "lucide-react"; diff --git a/components/elements/group-permission.tsx b/components/elements/group-permission.tsx index 8a6ab34..9753bc3 100644 --- a/components/elements/group-permission.tsx +++ b/components/elements/group-permission.tsx @@ -1,32 +1,37 @@ import axios from "axios"; -import { useEffect, useState } from "react"; +import { 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 "../ui/label"; -import { HelpCircleIcon, Trash2Icon } from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, - } from "../ui/tooltip" -import { Checkbox } from "../ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { z } from "zod"; +import { Trash2Icon } from "lucide-react"; 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 + adder: (id: string, path: string, allow: boolean | null) => void + remover: (redemption: { id: string, path: string, allow: boolean | null }) => void, + contains: (path: string) => boolean } +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, @@ -37,18 +42,43 @@ const GroupPermission = ({ isNew, permissionPaths, adder, - remover + remover, + contains }: 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 [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(undefined) 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, @@ -61,6 +91,11 @@ 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, @@ -74,7 +109,8 @@ const GroupPermission = ({ function Cancel() { if (!oldData) return - + + setError(undefined) setPermission({ ...oldData, id: permission.id }) setIsEditable(false) setOldData(undefined) @@ -124,7 +160,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) }}>
@@ -135,7 +171,7 @@ const GroupPermission = ({ {p.description}
- + ))} @@ -171,6 +207,11 @@ const GroupPermission = ({ }} disabled={!isEditable || permission === undefined} /> + {error && +

+ {error} +

+ }
{isEditable &&
)}
+ remover={removePermission} + contains={containsPermission} />
} diff --git a/components/elements/info-notice.tsx b/components/elements/info-notice.tsx index 9ec70e5..9b95f6c 100644 --- a/components/elements/info-notice.tsx +++ b/components/elements/info-notice.tsx @@ -1,6 +1,6 @@ import { Info, X } from "lucide-react"; import React, { useState } from "react"; -import { Button } from "../ui/button"; +import { Button } from "@/components/ui/button"; interface NoticeProps { diff --git a/components/elements/redeemable-action.tsx b/components/elements/redeemable-action.tsx index 18e514c..73b7ec4 100644 --- a/components/elements/redeemable-action.tsx +++ b/components/elements/redeemable-action.tsx @@ -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 "../ui/label"; +import { Label } from "@/components/ui/label"; import { Maximize2, Minimize2, Trash2Icon } from "lucide-react"; import { ActionType } from "@prisma/client"; -import { boolean } from "zod"; +import { boolean, z } from "zod"; const actionTypes = [ @@ -19,13 +19,15 @@ const actionTypes = [ "type": "text", "label": "File path", "key": "file_path", - "placeholder": "Enter local file path, relative or full." + "placeholder": "Enter local file path, relative or full.", + "required": true }, { "type": "text", "label": "File content", "key": "file_content", - "placeholder": "Enter the text to write to the file." + "placeholder": "Enter the text to write to the file.", + "required": true } ] }, @@ -37,13 +39,15 @@ const actionTypes = [ "type": "text", "label": "File path", "key": "file_path", - "placeholder": "Enter local file path, relative or full." + "placeholder": "Enter local file path, relative or full.", + "required": true }, { "type": "text", "label": "File content", "key": "file_content", - "placeholder": "Enter the text to append to the file." + "placeholder": "Enter the text to append to the file.", + "required": true } ] }, @@ -60,7 +64,8 @@ const actionTypes = [ "type": "text", "label": "File path", "key": "file_path", - "placeholder": "Enter local file path, relative or full." + "placeholder": "Enter local file path, relative or full.", + "required": true } ] }, @@ -78,6 +83,7 @@ const actionTypes = [ "label": "TTS Voice", "key": "tts_voice", "placeholder": "Name of an enabled TTS voice", + "required": true } ] }, @@ -89,13 +95,15 @@ const actionTypes = [ "type": "text", "label": "Scene Name", "key": "scene_name", - "placeholder": "Name of the OBS scene" + "placeholder": "Name of the OBS scene", + "required": true }, { "type": "text", "label": "Scene Item Name", "key": "scene_item_name", - "placeholder": "Name of the OBS scene item / source" + "placeholder": "Name of the OBS scene item / source", + "required": true } ] }, @@ -107,20 +115,23 @@ const actionTypes = [ "type": "text", "label": "Scene Name", "key": "scene_name", - "placeholder": "Name of the OBS scene" + "placeholder": "Name of the OBS scene", + "required": true }, { "type": "text", "label": "Scene Item Name", "key": "scene_item_name", - "placeholder": "Name of the OBS scene item / source" + "placeholder": "Name of the OBS scene item / source", + "required": true }, { "type": "text-values", "label": "Visible", "key": "obs_visible", "placeholder": "true for visible; false otherwise", - "values": ["true", "false"] + "values": ["true", "false"], + "required": true } ] }, @@ -132,19 +143,22 @@ const actionTypes = [ "type": "text", "label": "Scene Name", "key": "scene_name", - "placeholder": "Name of the OBS scene" + "placeholder": "Name of the OBS scene", + "required": true }, { "type": "text", "label": "Scene Item Name", "key": "scene_item_name", - "placeholder": "Name of the OBS scene item / source" + "placeholder": "Name of the OBS scene item / source", + "required": true }, { "type": "number", "label": "Index", "key": "obs_index", - "placeholder": "index, starting from 0." + "placeholder": "index, starting from 0.", + "required": true } ] }, @@ -157,6 +171,7 @@ const actionTypes = [ "label": "Sleep", "key": "sleep", "placeholder": "Time in milliseconds to do nothing", + "required": true } ] }, @@ -169,6 +184,7 @@ const actionTypes = [ "label": "nightbot.play", "key": "nightbot_play", "placeholder": "", + "required": true } ] }, @@ -181,6 +197,7 @@ const actionTypes = [ "label": "nightbot.pause", "key": "nightbot_pause", "placeholder": "", + "required": true } ] }, @@ -193,6 +210,7 @@ const actionTypes = [ "label": "nightbot.skip", "key": "nightbot_skip", "placeholder": "", + "required": true } ] }, @@ -205,6 +223,7 @@ const actionTypes = [ "label": "nightbot.clear_playlist", "key": "nightbot_clear_playlist", "placeholder": "", + "required": true } ] }, @@ -217,11 +236,16 @@ 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 @@ -256,44 +280,68 @@ 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(undefined) - function Save(name: string, type: ActionType | undefined, data: { [key: string]: string }, isNew: boolean) { - // TODO: validation - if (!name) { + function Save(isNew: boolean) { + setError(undefined) + if (!actionName) { + setError("Name is required.") return } - console.log('typeeee', type) - if (!type) { + const nameValidation = nameSchema.safeParse(actionName) + if (!nameValidation.success) { + setError(JSON.parse(nameValidation.error['message'])[0].message) return } - if (!data) { + if (!actionType) { + setError("Action type is required.") 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, - type + name: actionName, + type: actionType.value, } - info = { ...info, ...data } + info.data = actionData if (isNew) { axios.post("/api/settings/redemptions/actions", info) .then(d => { - adder(name, type, data) + adder(actionName, actionType.value, actionData) 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 @@ -305,7 +353,7 @@ const RedemptionAction = ({ } function Delete() { - axios.delete("/api/settings/redemptions/actions?action_name=" + name) + axios.delete("/api/settings/redemptions/actions?action_name=" + actionName) .then(d => { remover(d.data) }) @@ -431,12 +479,10 @@ const RedemptionAction = ({ onChange={e => setActionData(d => { let abc = { ...actionData } const v = parseInt(e.target.value) - if (e.target.value.length == 0) { + if (e.target.value.length == 0 || Number.isNaN(v)) { 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 })} @@ -497,7 +543,7 @@ const RedemptionAction = ({ if (!!connection) { setActionData({ 'oauth_name': connection.name, - 'oauth_type' : connection.type + 'oauth_type': connection.type }) } else @@ -517,7 +563,6 @@ const RedemptionAction = ({ } - return
})} } @@ -548,11 +593,16 @@ const RedemptionAction = ({ } + {error && +
+ {error} +
+ }
{isEditable && } diff --git a/components/elements/redemption.tsx b/components/elements/redemption.tsx index 147872f..1bc8643 100644 --- a/components/elements/redemption.tsx +++ b/components/elements/redemption.tsx @@ -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 "../ui/label"; +import { Label } from "@/components/ui/label"; import { HelpCircleIcon, Trash2Icon } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, - } from "../ui/tooltip" +} from "@/components/ui/tooltip" interface Redemption { id: string | undefined @@ -47,17 +47,42 @@ const OBSRedemption = ({ const [order, setOrder] = useState(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(undefined) useEffect(() => { setTwitchRedemption(twitchRedemptions.find(r => r.id == redemptionId)) }, []) function Save() { - // TODO: validation - if (!isNew && !id) + setError(undefined) + if (!isNew && !id) { + setError('Something went wrong. Refresh the page.') return - if (!action || !twitchRedemption) + } + + if (!action) { + setError('An action must be selected.') 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", { @@ -88,6 +113,7 @@ const OBSRedemption = ({ if (!oldData) return + setError(undefined) setAction(oldData.a) setTwitchRedemption(oldData.r) setOrder(oldData.o) @@ -143,7 +169,8 @@ const OBSRedemption = ({ value={redemption.title} key={redemption.id} onSelect={(value) => { - setTwitchRedemption(twitchRedemptions.find(v => v.title.toLowerCase() == value.toLowerCase())) + console.log('tr', value, redemption.id, redemption.title) + setTwitchRedemption(redemption) setRedemptionOpen(false) }}> {redemption.title} @@ -218,23 +245,37 @@ const OBSRedemption = ({ className="inline-block w-[300px]" id="name" placeholder="Enter an order number for this action" - onChange={e => setOrder(e.target.value.length == 0 ? 0 : parseInt(e.target.value))} + 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 + })} value={order} readOnly={!isEditable} /> + className="inline-block ml-3" /> -

This decides when this action will be done relative to other actions for this Twitch redemption.
- Action start from lowest to highest order number.
- Equal order numbers cannot be guaranteed proper order.

+

This decides when this action will be done relative to other actions for this Twitch redemption.
+ Action start from lowest to highest order number.
+ Equal order numbers cannot be guaranteed proper order.

+ {error && +
+ {error} +
+ }
{isEditable &&