From 6548ce33e01f4600d704bd6d7be889e90d3f7025 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 24 Jun 2024 22:16:55 +0000 Subject: [PATCH] Added redemptions & redeemable actions. Fixed a few bugs. --- app/api/account/reauthorize/route.ts | 46 ++- app/api/account/redemptions/route.ts | 35 ++ app/api/account/route.ts | 4 +- app/api/info/version/route.ts | 13 + app/api/settings/redemptions/actions/route.ts | 150 +++++++++ app/api/settings/redemptions/route.ts | 143 ++++++++ app/api/settings/tts/default/route.ts | 25 +- app/api/settings/tts/filter/words/route.ts | 4 +- app/api/settings/tts/route.ts | 57 ++-- app/api/settings/tts/selected/route.ts | 71 ++++ app/api/token/[id]/route.ts | 1 - app/api/token/bot/route.ts | 5 + app/api/token/route.ts | 32 +- app/api/tokens/route.ts | 17 +- app/api/users/route.ts | 4 +- app/settings/api/keys/page.tsx | 65 ++-- app/settings/connections/page.tsx | 8 +- app/settings/layout.tsx | 9 +- app/settings/redemptions/page.tsx | 143 ++++++++ app/settings/tts/filters/page.tsx | 2 +- app/settings/tts/voices/page.tsx | 73 ++-- app/socket/page.tsx | 58 ++++ components/elements/redeemable-action.tsx | 316 ++++++++++++++++++ components/elements/redemption.tsx | 274 +++++++++++++++ components/navigation/adminprofile.tsx | 194 ++++++----- components/navigation/settings.tsx | 11 + components/navigation/userprofile.tsx | 2 +- components/ui/tooltip.tsx | 30 ++ data/tts.ts | 126 +------ data/twitch-reauthorize.ts | 72 ++++ hooks/ApiKeyHooks.tsx | 0 lib/fetch-user-impersonation.ts | 6 +- lib/fetch-user.ts | 4 +- next.config.js | 5 +- prisma/schema.prisma | 253 ++++++++++---- 35 files changed, 1787 insertions(+), 471 deletions(-) create mode 100644 app/api/account/redemptions/route.ts create mode 100644 app/api/info/version/route.ts create mode 100644 app/api/settings/redemptions/actions/route.ts create mode 100644 app/api/settings/redemptions/route.ts create mode 100644 app/api/settings/tts/selected/route.ts create mode 100644 app/settings/redemptions/page.tsx create mode 100644 app/socket/page.tsx create mode 100644 components/elements/redeemable-action.tsx create mode 100644 components/elements/redemption.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 data/twitch-reauthorize.ts create mode 100644 hooks/ApiKeyHooks.tsx diff --git a/app/api/account/reauthorize/route.ts b/app/api/account/reauthorize/route.ts index ec797ca..b9f9d93 100644 --- a/app/api/account/reauthorize/route.ts +++ b/app/api/account/reauthorize/route.ts @@ -1,22 +1,20 @@ import axios from 'axios' import { db } from "@/lib/db" import { NextResponse } from "next/server"; +import fetchUser from '@/lib/fetch-user'; +import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation'; export async function GET(req: Request) { try { // Verify state against user id in user table. - const key = await db.apiKey.findFirst({ - where: { - id: req.headers.get('x-api-key') as string - } - }) - if (!key) { - return new NextResponse("Forbidden", { status: 403 }); + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); } const connection = await db.twitchConnection.findFirst({ where: { - userId: key.userId + userId: user.id } }) if (!connection) { @@ -29,8 +27,22 @@ export async function GET(req: Request) { Authorization: 'OAuth ' + connection.accessToken } })).data; - if (expires_in > 3600) - return new NextResponse("", { status: 201 }); + + if (expires_in > 3600) { + let data = await db.twitchConnection.findFirst({ + where: { + userId: user.id + } + }) + + let dataFormatted = { + user_id: user.id, + access_token: data?.accessToken, + refresh_token: data?.refreshToken, + broadcaster_id: connection.broadcasterId + } + return NextResponse.json(dataFormatted, { status: 201 }); + } } catch (error) { } @@ -51,14 +63,22 @@ export async function GET(req: Request) { await db.twitchConnection.update({ where: { - userId: key.userId + userId: user.id }, data: { - accessToken: access_token + accessToken: access_token, + refreshToken: refresh_token } }) + + const data = { + user_id: user.id, + access_token, + refresh_token, + broadcaster_id: connection.broadcasterId + } - return new NextResponse("", { status: 200 }); + return NextResponse.json(data) } catch (error) { console.log("[ACCOUNT]", error); return new NextResponse("Internal Error", { status: 500 }); diff --git a/app/api/account/redemptions/route.ts b/app/api/account/redemptions/route.ts new file mode 100644 index 0000000..6ca7b51 --- /dev/null +++ b/app/api/account/redemptions/route.ts @@ -0,0 +1,35 @@ +import { db } from "@/lib/db" +import { NextResponse } from "next/server"; +import fetchUserWithImpersonation from '@/lib/fetch-user-impersonation'; +import axios from "axios"; +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 }); + + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const auth = await updateTwitchToken(user.id) + if (!auth) + return new NextResponse("Bad Request", { status: 400 }) + + const redemptions = await axios.get("https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=" + auth.broadcaster_id, + { + headers: { + "Client-Id": process.env.TWITCH_BOT_CLIENT_ID, + "Authorization": "Bearer " + auth.access_token + } + } + ) + + return NextResponse.json(redemptions.data); + } catch (error) { + console.log("[REDEMPTIONS/ACTIONS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/account/route.ts b/app/api/account/route.ts index 28e4b63..0d31abb 100644 --- a/app/api/account/route.ts +++ b/app/api/account/route.ts @@ -6,7 +6,9 @@ import fetchUser from "@/lib/fetch-user"; export async function GET(req: Request) { try { - return NextResponse.json(await fetchUser(req)) + const user = await fetchUser(req) + if (!user) return new NextResponse("Internal Error", { status: 401 }) + return NextResponse.json(user) } catch (error) { console.log("[ACCOUNT]", error); return new NextResponse("Internal Error", { status: 500 }); diff --git a/app/api/info/version/route.ts b/app/api/info/version/route.ts new file mode 100644 index 0000000..aead9be --- /dev/null +++ b/app/api/info/version/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; + +export async function GET(req: Request) { + return NextResponse.json({ + major_version: 3, + minor_version: 3, + download: "https://drive.proton.me/urls/KVGW0ZKE9C#2Y0WGGt5uHFZ", + changelog: "Revised the redeem system, activated via channel point redeems.\nAdded OBS transformation to redeems.\nLogs changed & writes to logs folder as well." + //changelog: "Added new command for mods: !refresh - Used to refresh data if done via website.\nAdded new command for mods: !tts - To delete, enable, or disable a specific voice." + //changelog: "Save TTS voices set by chatters.\nAdded more options for TTS voices." 3.1 + //changelog: "Added a message when new updates are available.\nFixed 7tv renames not being applied correctly." 3.0 + }); +} \ No newline at end of file diff --git a/app/api/settings/redemptions/actions/route.ts b/app/api/settings/redemptions/actions/route.ts new file mode 100644 index 0000000..b5bc884 --- /dev/null +++ b/app/api/settings/redemptions/actions/route.ts @@ -0,0 +1,150 @@ +import { db } from "@/lib/db" +import { NextResponse } from "next/server"; +import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation"; +import { ActionType, Prisma } from "@prisma/client"; +import { JsonSerializer } from "typescript-json-serializer"; + +export async function GET(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const actions = await db.action.findMany({ + where: { + userId: user.id + } + }) + + return NextResponse.json(actions.map(({userId, ...attrs}) => attrs)); + } catch (error) { + console.log("[REDEMPTIONS/ACTIONS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function POST(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { name, type, scene_name, scene_item_name, rotation, position_x, position_y, file_path, file_content }: { 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 } = await req.json(); + if (!name && !type) + return new NextResponse("Bad Request", { status: 400 }); + if (type == ActionType.OBS_TRANSFORM && (!scene_name || !scene_item_name || !rotation && !position_x && !position_y)) + return new NextResponse("Bad Request", { status: 400 }); + if ((type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) && (!file_path || !file_content)) + return new NextResponse("Bad Request", { status: 400 }); + if (type == ActionType.AUDIO_FILE && !file_path) + return new NextResponse("Bad Request", { status: 400 }); + + let data:any = { } + if (type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) { + data = { file_path, file_content, ...data } + } else if (type == ActionType.OBS_TRANSFORM) { + data = { scene_name, scene_item_name, ...data } + if (!!rotation) + data = { rotation, ...data } + if (!!position_x) + data = { position_x, ...data } + if (!!position_y) + data = { position_y, ...data } + } else if (type == ActionType.AUDIO_FILE) { + data = { file_path, ...data } + } + + await db.action.create({ + data: { + userId: user.id, + name, + type, + data: data as Prisma.JsonObject + } + }); + + return new NextResponse("", { status: 200 }); + } catch (error) { + console.log("[REDEMPTIONS/ACTIONS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function PUT(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { name, type, scene_name, scene_item_name, rotation, position_x, position_y, file_path, file_content }: { 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 } = await req.json(); + if (!name && !type) + return new NextResponse("Bad Request", { status: 400 }); + if (type == ActionType.OBS_TRANSFORM && (!scene_name || !scene_item_name || !rotation && !position_x && !position_y)) + return new NextResponse("Bad Request", { status: 400 }); + if ((type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) && (!file_path || !file_content)) + return new NextResponse("Bad Request", { status: 400 }); + if (type == ActionType.AUDIO_FILE && !file_path) + return new NextResponse("Bad Request", { status: 400 }); + + let data:any = { } + if (type == ActionType.WRITE_TO_FILE || type == ActionType.APPEND_TO_FILE) { + data = { file_path, file_content, ...data } + } else if (type == ActionType.OBS_TRANSFORM) { + data = { scene_name, scene_item_name, ...data } + if (!!rotation) + data = { rotation, ...data } + if (!!position_x) + data = { position_x, ...data } + if (!!position_y) + data = { position_y, ...data } + } else if (type == ActionType.AUDIO_FILE) { + data = { file_path, ...data } + } + + await db.action.update({ + where: { + userId_name: { + userId: user.id, + name + } + }, + data: { + type, + data: data as Prisma.JsonObject + } + }); + + return new NextResponse("", { status: 200 }); + } catch (error) { + console.log("[REDEMPTIONS/ACTIONS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function DELETE(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { searchParams } = new URL(req.url) + const name = searchParams.get('action_name') as string + const redemptions = await db.action.delete({ + where: { + userId_name: { + userId: user.id, + name + } + } + }) + + return NextResponse.json(redemptions); + } catch (error) { + console.log("[REDEMPTIONS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/settings/redemptions/route.ts b/app/api/settings/redemptions/route.ts new file mode 100644 index 0000000..e24854b --- /dev/null +++ b/app/api/settings/redemptions/route.ts @@ -0,0 +1,143 @@ +import { db } from "@/lib/db" +import { NextResponse } from "next/server"; +import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation"; + +export async function GET(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const redemptions = await db.redemption.findMany({ + where: { + userId: user.id + } + }) + + return NextResponse.json(redemptions.map(({userId, ...attrs}) => attrs)); + } catch (error) { + console.log("[REDEMPTIONS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function POST(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { actionName, redemptionId, order, state }: { actionName: string, redemptionId: string, order: number, state: boolean } = await req.json(); + if (!redemptionId || !actionName && !order && !state) + return new NextResponse("Bad Request", { status: 400 }); + + const action = await db.action.findFirst({ + where: { + name: actionName + } + }) + if (!action) + return new NextResponse("Bad Request", { status: 400 }); + + let data:any = { + actionName, + order, + state + } + + if (!data.actionName) + data = { actionName, ...data } + if (!data.order) + data = { order, ...data } + if (!data.state) + data = { state, ...data } + + const res = await db.redemption.create({ + data: { + userId: user.id, + redemptionId, + order, + state: true, + ...data + } + }); + + return NextResponse.json(res, { status: 200 }); + } catch (error) { + console.log("[REDEMPTIONS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function PUT(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { id, actionName, redemptionId, order, state }: { id: string, actionName: string, redemptionId: string, order: number, state: boolean } = await req.json(); + if (!redemptionId || !actionName && !order && !state) + return new NextResponse("Bad Request", { status: 400 }); + + const action = await db.action.findFirst({ + where: { + name: actionName + } + }) + if (!action) + return new NextResponse("Bad Request", { status: 400 }); + + let data:any = { + actionName, + redemptionId, + order, + state + } + + if (!data.actionName) + data = { actionName, ...data } + if (!data.order) + data = { order, ...data } + if (!data.state) + data = { state, ...data } + if (!data.redemptionId) + data = { redemptionId, ...data } + + const res = await db.redemption.update({ + where: { + id + }, + data: data + }); + + return NextResponse.json(res, { status: 200 }); + } catch (error) { + console.log("[REDEMPTIONS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function DELETE(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { searchParams } = new URL(req.url) + const id = searchParams.get('id') as string + const redemptions = await db.redemption.delete({ + where: { + id + } + }) + + return NextResponse.json(redemptions); + } catch (error) { + console.log("[REDEMPTIONS]", error); + return new NextResponse("Internal Error", { 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 eab0a6f..cd4a442 100644 --- a/app/api/settings/tts/default/route.ts +++ b/app/api/settings/tts/default/route.ts @@ -5,19 +5,16 @@ import voices from "@/data/tts"; export async function GET(req: Request) { try { + if (!voices) { + return new NextResponse("Voices not available.", { status: 500 }); + } + const user = await fetchUserWithImpersonation(req) if (!user) { return new NextResponse("Unauthorized", { status: 401 }); } - const u = await db.user.findFirst({ - where: { - id: user.id - } - }); - - const voice = voices.find(v => v.value == new String(u?.ttsDefaultVoice)) - return NextResponse.json(voice); + return NextResponse.json(user.ttsDefaultVoice); } catch (error) { console.log("[TTS/FILTER/DEFAULT]", error); return new NextResponse("Internal Error", { status: 500 }); @@ -26,28 +23,32 @@ export async function GET(req: Request) { export async function POST(req: Request) { try { + if (!voices) { + return new NextResponse("Voices not available.", { status: 500 }); + } + const user = await fetchUserWithImpersonation(req) if (!user) { return new NextResponse("Unauthorized", { status: 401 }); } const { voice } = await req.json(); - if (!voice || !voices.map(v => v.label.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 }); + if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 }); - const v = voices.find(v => v.label.toLowerCase() == voice.toLowerCase()) + const v = voices.find(v => v.toLowerCase() == voice.toLowerCase()) await db.user.update({ where: { id: user.id }, data: { - ttsDefaultVoice: Number.parseInt(v.value) + ttsDefaultVoice: v } }); return new NextResponse("", { status: 200 }); } catch (error) { console.log("[TTS/FILTER/DEFAULT]", error); - return new NextResponse("Internal Error", { status: 500 }); } + return new NextResponse("Internal Error", { 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 bae2306..d2f23b9 100644 --- a/app/api/settings/tts/filter/words/route.ts +++ b/app/api/settings/tts/filter/words/route.ts @@ -31,7 +31,7 @@ export async function POST(req: Request) { const { search, replace } = await req.json(); if (!search || search.length < 4 || search.length > 200) return new NextResponse("Bad Request", { status: 400 }); - if (!replace) return new NextResponse("Bad Request", { status: 400 }); + if (replace == null) return new NextResponse("Bad Request", { status: 400 }); const filter = await db.ttsWordFilter.create({ data: { @@ -58,7 +58,7 @@ export async function PUT(req: Request) { 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) return new NextResponse("Bad Request", { status: 400 }); + if (replace == null) return new NextResponse("Bad Request", { status: 400 }); const filter = await db.ttsWordFilter.update({ where: { diff --git a/app/api/settings/tts/route.ts b/app/api/settings/tts/route.ts index e3e04a8..068964c 100644 --- a/app/api/settings/tts/route.ts +++ b/app/api/settings/tts/route.ts @@ -1,7 +1,6 @@ import { db } from "@/lib/db" import { NextResponse } from "next/server"; import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation"; -import voices from "@/data/tts"; export async function GET(req: Request) { try { @@ -10,23 +9,18 @@ export async function GET(req: Request) { return new NextResponse("Unauthorized", { status: 401 }); } - let list : { - value: string; - label: string; - gender: string; - language: string; - }[] = [] - const enabled = user.ttsEnabledVoice - for (let v of voices) { - var n = Number.parseInt(v.value) - 1 - if ((enabled & (1 << n)) > 0) { - list.push(v) - } - } + const voiceStates = await db.ttsVoiceState.findMany({ + where: { + userId: user.id + } + }); - return NextResponse.json(list); + const voiceNames = await db.ttsVoice.findMany(); + const voiceNamesMapped: { [id: string]: string } = Object.assign({}, ...voiceNames.map(v => ({ [v.id]: v.name }))) + + return NextResponse.json(voiceStates.filter(v => v.state).map(v => voiceNamesMapped[v.ttsVoiceId])); } catch (error) { - console.log("[TTS/FILTER/USER]", error); + console.log("[TTS]", error); return new NextResponse("Internal Error", { status: 500 }); } } @@ -38,16 +32,29 @@ export async function POST(req: Request) { return new NextResponse("Unauthorized", { status: 401 }); } - let { voice } = await req.json(); - voice = voice & ((1 << voices.length) - 1) + const { voice, state }: { voice: string, state: boolean } = await req.json(); - await db.user.update({ - where: { - id: user.id - }, - data: { - ttsEnabledVoice: voice - } + 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 }); + } + + await db.ttsVoiceState.upsert({ + where: { + userId_ttsVoiceId: { + userId: user.id, + ttsVoiceId: voiceIdsMapped[voice.toLowerCase()] + } + }, + update: { + state + }, + create: { + userId: user.id, + ttsVoiceId: voiceIdsMapped[voice.toLowerCase()], + state + } }); return new NextResponse("", { status: 200 }); diff --git a/app/api/settings/tts/selected/route.ts b/app/api/settings/tts/selected/route.ts new file mode 100644 index 0000000..79f3a45 --- /dev/null +++ b/app/api/settings/tts/selected/route.ts @@ -0,0 +1,71 @@ +import { db } from "@/lib/db" +import { NextResponse } from "next/server"; +import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation"; +import voices from "@/data/tts"; + +export async function GET(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const selected = await db.ttsChatVoice.findMany({ + where: { + userId: user.id + } + }) + + const voices = await db.ttsVoice.findMany(); + const voiceNamesMapped: { [id: string]: string } = Object.assign({}, ...voices.map(v => ({ [v.id]: v.name }))) + + 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 }); + } +} + +export async function POST(req: Request) { + try { + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { voice, chatterId }: { voice: string, chatterId: number } = await req.json(); + if (!voice || !voices.map(v => v.toLowerCase()).includes(voice.toLowerCase())) return new NextResponse("Bad Request", { status: 400 }); + + const v = voices.find(v => v.toLowerCase() == voice.toLowerCase()) + const voiceData = await db.ttsVoice.findFirst({ + where: { + name: v + } + }) + if (!voiceData) + return new NextResponse("Bad Request", { status: 400 }); + + await db.ttsChatVoice.upsert({ + where: { + userId_chatterId: { + userId: user.id, + chatterId + } + }, + create: { + userId: user.id, + chatterId, + ttsVoiceId: voiceData.id + }, + update: { + ttsVoiceId: voiceData.id + } + }); + + return new NextResponse("", { status: 200 }); + } catch (error) { + console.log("[TTS/SELECTED]", error); + return new NextResponse("Internal Error", { 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 09b43f8..70aab29 100644 --- a/app/api/token/[id]/route.ts +++ b/app/api/token/[id]/route.ts @@ -30,7 +30,6 @@ export async function GET(req: Request, { params } : { params: { id: string } }) export async function DELETE(req: Request, { params } : { params: { id: string } }) { try { const user = await fetchUserWithImpersonation(req) - if (!user) { return new NextResponse("Unauthorized", { status: 401 }); } diff --git a/app/api/token/bot/route.ts b/app/api/token/bot/route.ts index cca9cdf..ec2723e 100644 --- a/app/api/token/bot/route.ts +++ b/app/api/token/bot/route.ts @@ -4,20 +4,24 @@ import { NextResponse } from "next/server"; export async function GET(req: Request) { try { + console.log("ABC 1") const user = await fetchUserWithImpersonation(req); if (!user) { return new NextResponse("Unauthorized", { status: 401 }); } + console.log("ABC 2") const api = await db.twitchConnection.findFirst({ where: { userId: user.id } }) + console.log("ABC 3") if (!api) { return new NextResponse("Forbidden", { status: 403 }); } + console.log("ABC 4") const data = { client_id: process.env.TWITCH_BOT_CLIENT_ID, client_secret: process.env.TWITCH_BOT_CLIENT_SECRET, @@ -25,6 +29,7 @@ export async function GET(req: Request) { refresh_token: api.refreshToken, broadcaster_id: api.broadcasterId } + console.log("ABC 5", data) return NextResponse.json(data); } catch (error) { console.log("[TOKENS/GET]", error); diff --git a/app/api/token/route.ts b/app/api/token/route.ts index 3609867..ba100f3 100644 --- a/app/api/token/route.ts +++ b/app/api/token/route.ts @@ -20,11 +20,11 @@ export async function POST(req: Request) { const id = generateToken() const token = await db.apiKey.create({ - data: { - id, - label, - userId: userId as string - } + data: { + id, + label, + userId: userId as string + } }); return NextResponse.json(token); @@ -41,16 +41,16 @@ export async function DELETE(req: Request) { return new NextResponse("Unauthorized", { status: 401 }); } - let { id } = await req.json(); + const { id } = await req.json(); if (!id) { return NextResponse.json(null) } const token = await db.apiKey.delete({ - where: { - id, - userId: user?.id - } + where: { + id, + userId: user?.id + } }); return NextResponse.json(token); @@ -60,12 +60,12 @@ export async function DELETE(req: Request) { } } -export function generateToken() { - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; - var string_length = 32; - var randomstring = ''; - for (var i = 0; i < string_length; i++) { - var rnum = Math.floor(Math.random() * chars.length); +function generateToken() { + let chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; + let string_length = 32; + let randomstring = ''; + for (let i = 0; i < string_length; i++) { + let rnum = Math.floor(Math.random() * chars.length); randomstring += chars[rnum]; } return randomstring; diff --git a/app/api/tokens/route.ts b/app/api/tokens/route.ts index 6f69725..f091d07 100644 --- a/app/api/tokens/route.ts +++ b/app/api/tokens/route.ts @@ -1,28 +1,23 @@ -import fetchUser from "@/lib/fetch-user"; import { db } from "@/lib/db" import { NextResponse } from "next/server"; +import fetchUserWithImpersonation from "@/lib/fetch-user-impersonation"; export async function GET(req: Request) { try { - const { searchParams } = new URL(req.url) - let userId = searchParams.get('userId') - - if (userId == null) { - const user = await fetchUser(req); - if (user != null) { - userId = user.id as string; - } + const user = await fetchUserWithImpersonation(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); } const tokens = await db.apiKey.findMany({ where: { - userId: userId as string + userId: user.id } }); return NextResponse.json(tokens); } catch (error) { console.log("[TOKENS/GET]", error); - return new NextResponse("Internal Error", { status: 500}); + return new NextResponse("Internal Error", { status: 500 }); } } \ No newline at end of file diff --git a/app/api/users/route.ts b/app/api/users/route.ts index f4f3a24..a495f51 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -24,7 +24,7 @@ export async function GET(req: Request) { return NextResponse.json(users) } if (id) { - const users = await db.user.findUnique({ + const users = await db.user.findFirst({ where: { id: id } @@ -35,7 +35,7 @@ export async function GET(req: Request) { const users = await db.user.findMany(); return NextResponse.json(users) } catch (error) { - console.log("[AUTH/ACCOUNT/IMPERSONATION]", error); + console.log("[USERS]", error); return new NextResponse("Internal Error", { status: 500 }); } } \ No newline at end of file diff --git a/app/settings/api/keys/page.tsx b/app/settings/api/keys/page.tsx index 456e720..06004fd 100644 --- a/app/settings/api/keys/page.tsx +++ b/app/settings/api/keys/page.tsx @@ -3,63 +3,46 @@ import axios from "axios"; import { Button } from "@/components/ui/button"; import * as React from 'react'; -import { ApiKey, User } from "@prisma/client"; import { useEffect, useState } from "react"; -import { useSession } from "next-auth/react"; import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -const SettingsPage = () => { - const { data: session, status } = useSession(); - - const [apiKeyViewable, setApiKeyViewable] = useState(0) - const [apiKeyChanges, setApiKeyChanges] = useState(0) - const [apiKeys, setApiKeys] = useState([]) +const ApiKeyPage = () => { + const [apiKeyViewable, setApiKeyViewable] = useState(-1) + const [apiKeys, setApiKeys] = useState<{ id: string, label: string, userId: string }[]>([]) useEffect(() => { const fetchData = async () => { - try { - const keys = (await axios.get("/api/tokens")).data ?? {}; - setApiKeys(keys) - } catch (error) { - console.log("ERROR", error) - } + await axios.get("/api/tokens") + .then(d => setApiKeys(d.data ?? [])) + .catch(console.error) }; - fetchData().catch(console.error); - }, [apiKeyChanges]); + fetchData(); + }, []); - const onApiKeyAdd = async () => { - try { - await axios.post("/api/token", { - label: "Key label" - }); - setApiKeyChanges(apiKeyChanges + 1) - } catch (error) { - console.log("ERROR", error) - } + const onApiKeyAdd = async (label: string) => { + await axios.post("/api/token", { label }) + .then(d => setApiKeys(apiKeys.concat([d.data]))) + .catch(console.error) } const onApiKeyDelete = async (id: string) => { - try { - await axios.delete("/api/token/" + id); - setApiKeyChanges(apiKeyChanges - 1) - } catch (error) { - console.log("ERROR", error) - } + await axios.delete("/api/token/" + id) + .then((d) => setApiKeys(apiKeys.filter(k => k.id != d.data.id))) + .catch(console.error) } return (
-
API Keys
- +
API Keys
+
A list of your secret API keys. Label Token - View Action @@ -67,20 +50,20 @@ const SettingsPage = () => { {apiKeys.map((key, index) => ( {key.label} - {(apiKeyViewable & (1 << index)) > 0 ? key.id : "*".repeat(key.id.length)} + {apiKeyViewable == index ? key.id : "*".repeat(key.id.length)} - + - + ))} - - +
@@ -90,4 +73,4 @@ const SettingsPage = () => { ); } -export default SettingsPage; \ No newline at end of file +export default ApiKeyPage; \ No newline at end of file diff --git a/app/settings/connections/page.tsx b/app/settings/connections/page.tsx index 195d84e..387e64c 100644 --- a/app/settings/connections/page.tsx +++ b/app/settings/connections/page.tsx @@ -10,7 +10,7 @@ import Link from "next/link"; import { cn } from "@/lib/utils"; import { Skeleton } from "@/components/ui/skeleton"; -const SettingsPage = () => { +const ConnectionsPage = () => { const { data: session, status } = useSession(); const [previousUsername, setPreviousUsername] = useState() const [userId, setUserId] = useState() @@ -24,7 +24,7 @@ const SettingsPage = () => { setPreviousUsername(session.user?.name as string) if (session.user?.name) { const fetchData = async () => { - var connection: User = (await axios.get("/api/account")).data + let connection: User = (await axios.get("/api/account")).data setUserId(connection.id) setLoading(false) } @@ -36,7 +36,7 @@ const SettingsPage = () => { const [twitchUser, setTwitchUser] = useState(null) useEffect(() => { const fetchData = async () => { - var connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data + let connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data setTwitchUser(connection) } @@ -97,4 +97,4 @@ const SettingsPage = () => { ); } -export default SettingsPage; \ No newline at end of file +export default ConnectionsPage; \ No newline at end of file diff --git a/app/settings/layout.tsx b/app/settings/layout.tsx index 3650185..d67bda8 100644 --- a/app/settings/layout.tsx +++ b/app/settings/layout.tsx @@ -3,8 +3,11 @@ import { cn } from "@/lib/utils"; import { headers } from 'next/headers'; import React from "react"; -const SettingsLayout = async ( - { children } : { children:React.ReactNode } ) => { +const SettingsLayout = async ({ + children +} : { + children:React.ReactNode +} ) => { const headersList = headers(); const header_url = headersList.get('x-url') || ""; @@ -14,7 +17,7 @@ const SettingsLayout = async ( header_url.endsWith("/settings") && "flex h-full w-full md:w-[250px] z-30 flex-col fixed inset-y-0")}>
-
+
{children}
diff --git a/app/settings/redemptions/page.tsx b/app/settings/redemptions/page.tsx new file mode 100644 index 0000000..117b643 --- /dev/null +++ b/app/settings/redemptions/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import axios from "axios"; +import * as React from 'react'; +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +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"; + +const obsTransformations = [ + { label: "scene_name", description: "", placeholder: "Name of the OBS scene" }, + { label: "scene_item_name", description: "", placeholder: "Name of the OBS scene item / source" }, + { label: "rotation", description: "", placeholder: "An expression using x as the previous value" }, + { label: "position_x", description: "", placeholder: "An expression using x as the previous value" }, + { label: "position_y", description: "", placeholder: "An expression using x as the previous value" } +] + +const RedemptionsPage = () => { + const { data: session, status } = useSession(); + const [previousUsername, setPreviousUsername] = useState() + const [loading, setLoading] = useState(true) + const [open, setOpen] = useState(false) + const [actions, setActions] = useState<{ name: string, type: string, data: any }[]>([]) + const [twitchRedemptions, setTwitchRedemptions] = useState<{ id: string, title: string }[]>([]) + const [redemptions, setRedemptions] = useState<{ id: string, redemptionId: string, actionName: string, order: number }[]>([]) + + function addAction(name: string, type: ActionType, data: { [key: string]: string }) { + setActions([...actions, { name, type, data }]) + } + + function removeAction(action: { name: string, type: string, data: any }) { + setActions(actions.filter(a => a.name != action.name)) + } + + function addRedemption(id: string, actionName: string, redemptionId: string, order: number) { + setRedemptions([...redemptions, { id, redemptionId, actionName, order }]) + } + + function removeRedemption(redemption: { id: string, redemptionId: string, actionName: string, order: number }) { + setRedemptions(redemptions.filter(r => r.id != redemption.id)) + } + + useEffect(() => { + if (status !== "authenticated" || previousUsername == session.user?.name) { + return + } + setPreviousUsername(session.user?.name) + + axios.get("/api/settings/redemptions/actions") + .then(d => { + setActions(d.data) + }) + + axios.get("/api/account/redemptions") + .then(d => { + const rs = d.data.data?.map(r => ({ id: r.id, title: r.title })) ?? [] + setTwitchRedemptions(rs) + + axios.get("/api/settings/redemptions") + .then(d => { + setRedemptions(d.data) + }) + }) + }, [session]) + + return ( +
+
Redemption Actions
+
+ ); +} + +export default RedemptionsPage; \ No newline at end of file diff --git a/app/settings/tts/filters/page.tsx b/app/settings/tts/filters/page.tsx index ced59a5..66ee753 100644 --- a/app/settings/tts/filters/page.tsx +++ b/app/settings/tts/filters/page.tsx @@ -230,7 +230,7 @@ const TTSFiltersPage = () => {
-