initial commit: fork
This commit is contained in:
34
packages/api/src/env.mjs
Normal file
34
packages/api/src/env.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createEnv } from '@t3-oss/env-core';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const env = createEnv({
|
||||
clientPrefix: '',
|
||||
/**
|
||||
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
|
||||
* built with invalid env vars.
|
||||
*/
|
||||
server: {
|
||||
DATABASE_URL: z.string(),
|
||||
DISCORD_TOKEN: z.string(),
|
||||
DISCORD_CLIENT_ID: z.string(),
|
||||
DISCORD_CLIENT_SECRET: z.string()
|
||||
},
|
||||
/**
|
||||
* Specify your client-side environment variables schema here.
|
||||
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
|
||||
*/
|
||||
client: {
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
},
|
||||
/**
|
||||
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
|
||||
*/
|
||||
runtimeEnv: {
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
DISCORD_TOKEN: process.env.DISCORD_TOKEN,
|
||||
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
|
||||
DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET
|
||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||
},
|
||||
skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION
|
||||
});
|
||||
27
packages/api/src/root.ts
Normal file
27
packages/api/src/root.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { channelRouter } from './routers/channel';
|
||||
import { commandRouter } from './routers/command';
|
||||
import { guildRouter } from './routers/guild';
|
||||
import { hubRouter } from './routers/hub';
|
||||
import { playlistRouter } from './routers/playlist';
|
||||
import { reminderRouter } from './routers/reminder';
|
||||
import { songRouter } from './routers/song';
|
||||
import { twitchRouter } from './routers/twitch';
|
||||
import { userRouter } from './routers/user';
|
||||
import { welcomeRouter } from './routers/welcome';
|
||||
import { createTRPCRouter } from './trpc';
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
user: userRouter,
|
||||
guild: guildRouter,
|
||||
playlist: playlistRouter,
|
||||
song: songRouter,
|
||||
twitch: twitchRouter,
|
||||
channel: channelRouter,
|
||||
welcome: welcomeRouter,
|
||||
command: commandRouter,
|
||||
hub: hubRouter,
|
||||
reminder: reminderRouter
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
||||
42
packages/api/src/routers/channel.ts
Normal file
42
packages/api/src/routers/channel.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getFetch } from '@trpc/client';
|
||||
import type {
|
||||
APIGuildChannel,
|
||||
APIGuildTextChannel
|
||||
} from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { env } from '../env.mjs';
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
const fetch = getFetch();
|
||||
|
||||
export const channelRouter = createTRPCRouter({
|
||||
getAll: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const token = env.DISCORD_TOKEN;
|
||||
|
||||
// call the discord api with the token and the guildId and get all the guild's text channels
|
||||
const response = await fetch(
|
||||
`https://discordapp.com/api/guilds/${guildId}/channels`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const responseChannels =
|
||||
(await response.json()) as APIGuildChannel<any>[];
|
||||
|
||||
const channels: APIGuildTextChannel<0>[] = responseChannels.filter(
|
||||
channel => channel.type === 0
|
||||
);
|
||||
return { channels };
|
||||
})
|
||||
});
|
||||
361
packages/api/src/routers/command.ts
Normal file
361
packages/api/src/routers/command.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { getFetch } from '@trpc/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import type {
|
||||
APIApplicationCommandPermission,
|
||||
APIGuildChannel,
|
||||
APIRole,
|
||||
ChannelType
|
||||
} from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { env } from '../env.mjs';
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
import { discordApi } from '../utils/axiosWithRefresh';
|
||||
|
||||
const fetch = getFetch();
|
||||
|
||||
export interface CommandType {
|
||||
code: number;
|
||||
id: string;
|
||||
applicationId: string;
|
||||
version: string;
|
||||
default_permission: string;
|
||||
default_member_permissions: null | string[];
|
||||
type: number;
|
||||
name: string;
|
||||
description: string;
|
||||
dm_permission: boolean;
|
||||
options: any[];
|
||||
}
|
||||
|
||||
export interface CommandPermissionsResponseOkay {
|
||||
id: string;
|
||||
application_id: string;
|
||||
guild_id: string;
|
||||
permissions: APIApplicationCommandPermission[];
|
||||
}
|
||||
|
||||
export interface CommandPermissionsResponseNotOkay {
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export const commandRouter = createTRPCRouter({
|
||||
getDisabledCommands: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
disabledCommands: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!guild) {
|
||||
throw new TRPCError({
|
||||
message: 'Guild not found',
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
return { disabledCommands: guild.disabledCommands };
|
||||
}),
|
||||
getCommands: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
try {
|
||||
const token = env.DISCORD_TOKEN;
|
||||
const response = await fetch(
|
||||
`https://discordapp.com/api/applications/${env.DISCORD_CLIENT_ID}/commands`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const commands = (await response.json()) as CommandType[];
|
||||
|
||||
return { commands };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds'
|
||||
});
|
||||
}
|
||||
}),
|
||||
getCommandAndGuildChannels: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
commandId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
message: 'Not Authenticated',
|
||||
code: 'UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
const token = env.DISCORD_TOKEN;
|
||||
const clientID = env.DISCORD_CLIENT_ID;
|
||||
const { guildId, commandId } = input;
|
||||
|
||||
const account = await ctx.prisma.account.findFirst({
|
||||
where: {
|
||||
userId: ctx.session?.user?.id
|
||||
},
|
||||
select: {
|
||||
access_token: true,
|
||||
providerAccountId: true,
|
||||
user: {
|
||||
select: {
|
||||
discordId: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const [
|
||||
guildChannelsResponse,
|
||||
guildRolesResponse,
|
||||
commandResponse,
|
||||
permissionsResponse
|
||||
] = await Promise.all([
|
||||
fetch(`https://discord.com/api/guilds/${guildId}/channels`, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}).then((res: any) => res.json()) as Promise<unknown>,
|
||||
fetch(`https://discord.com/api/guilds/${guildId}/roles`, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}).then((res: any) => res.json()) as Promise<unknown>,
|
||||
fetch(
|
||||
`https://discord.com/api/applications/${clientID}/commands/${commandId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}
|
||||
).then((res: any) => res.json()) as Promise<unknown>,
|
||||
discordApi
|
||||
.get(
|
||||
`https://discord.com/api/v10/applications/${clientID}/guilds/${guildId}/commands/${commandId}/permissions`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${account?.access_token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res: any) => res.data)
|
||||
]);
|
||||
|
||||
const channels =
|
||||
guildChannelsResponse as APIGuildChannel<ChannelType>[];
|
||||
const roles = guildRolesResponse as APIRole[];
|
||||
const command = commandResponse as CommandType;
|
||||
const permissions = permissionsResponse;
|
||||
|
||||
return { channels, roles, command, permissions };
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds'
|
||||
});
|
||||
}
|
||||
}),
|
||||
getCommandPermissions: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
commandId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const clientID = env.DISCORD_CLIENT_ID;
|
||||
const { guildId, commandId } = input;
|
||||
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
message: 'Not Authenticated',
|
||||
code: 'UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
const account = await ctx.prisma.account.findFirst({
|
||||
where: {
|
||||
userId: ctx.session?.user?.id
|
||||
},
|
||||
select: {
|
||||
access_token: true,
|
||||
providerAccountId: true,
|
||||
user: {
|
||||
select: {
|
||||
discordId: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://discord.com/api/applications/${clientID}/guilds/${guildId}/commands/${commandId}/permissions`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${account?.access_token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const command = await response.json();
|
||||
if (!command) throw new Error();
|
||||
|
||||
return { command };
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds'
|
||||
});
|
||||
}
|
||||
}),
|
||||
editCommandPermissions: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
commandId: z.string(),
|
||||
permissions: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.number(),
|
||||
permission: z.boolean()
|
||||
})
|
||||
),
|
||||
type: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const clientID = env.DISCORD_CLIENT_ID;
|
||||
const { guildId, commandId, permissions, type } = input;
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
message: 'Not Authenticated',
|
||||
code: 'UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
const account = await ctx.prisma.account.findFirst({
|
||||
where: {
|
||||
userId: ctx.session?.user?.id
|
||||
},
|
||||
select: {
|
||||
access_token: true,
|
||||
providerAccountId: true,
|
||||
user: {
|
||||
select: {
|
||||
discordId: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const everyone = {
|
||||
id: guildId,
|
||||
type: 1,
|
||||
permission: type === 'allow' ? true : false
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://discord.com/api/applications/${clientID}/guilds/${guildId}/commands/${commandId}/permissions`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${account?.access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ permissions: [everyone, ...permissions] })
|
||||
}
|
||||
);
|
||||
const command = await response.json();
|
||||
if (!command) throw new Error();
|
||||
|
||||
return { command };
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds'
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
toggleCommand: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
commandId: z.string(),
|
||||
status: z.boolean()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, commandId, status } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
disabledCommands: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!guild) {
|
||||
throw new TRPCError({
|
||||
message: 'Guild not found',
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
let updatedGuild;
|
||||
|
||||
if (status) {
|
||||
updatedGuild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
disabledCommands: {
|
||||
set: [...guild.disabledCommands, commandId]
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updatedGuild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
disabledCommands: {
|
||||
set: guild?.disabledCommands.filter(cid => cid !== commandId)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { updatedGuild };
|
||||
})
|
||||
});
|
||||
178
packages/api/src/routers/guild.ts
Normal file
178
packages/api/src/routers/guild.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { getFetch } from '@trpc/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import type { APIGuild, APIRole } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||
import { discordApi } from '../utils/axiosWithRefresh';
|
||||
|
||||
const fetch = getFetch();
|
||||
|
||||
function getUserGuilds(
|
||||
access_token: string,
|
||||
refresh_token: string,
|
||||
user_id: string
|
||||
) {
|
||||
return discordApi.get('https://discord.com/api/v10/users/@me/guilds', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
// set user agent
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
|
||||
'X-User-Id': user_id,
|
||||
'X-Refresh-Token': refresh_token
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const guildRouter = createTRPCRouter({
|
||||
getGuild: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
ownerId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, ownerId, name } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.upsert({
|
||||
where: {
|
||||
id: id
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
id: id,
|
||||
ownerId: ownerId,
|
||||
volume: 100,
|
||||
name: name
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.delete({
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
updateVolume: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
volume: z.number()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, volume } = input;
|
||||
|
||||
await ctx.prisma.guild.update({
|
||||
where: { id: guildId },
|
||||
data: { volume }
|
||||
});
|
||||
}),
|
||||
getRoles: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
const token = process.env.DISCORD_TOKEN;
|
||||
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
message: 'Not Authenticated',
|
||||
code: 'UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://discord.com/api/guilds/${guildId}/roles`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const roles = (await response.json()) as APIRole[];
|
||||
|
||||
return { roles };
|
||||
}),
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
const account = await ctx.prisma.account.findFirst({
|
||||
where: {
|
||||
userId: ctx.session?.user?.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!account?.access_token || !account?.refresh_token) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Account not found'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const dbGuilds = await ctx.prisma.guild.findMany({
|
||||
where: {
|
||||
ownerId: account.providerAccountId
|
||||
}
|
||||
});
|
||||
|
||||
const response = await getUserGuilds(
|
||||
account.access_token,
|
||||
account.refresh_token,
|
||||
account.userId
|
||||
);
|
||||
|
||||
// get the guilds from response data
|
||||
const apiGuilds = response.data as APIGuild[];
|
||||
|
||||
const apiGuildsOwns = apiGuilds.filter(guild => guild.owner);
|
||||
|
||||
return {
|
||||
apiGuilds: apiGuildsOwns,
|
||||
dbGuilds,
|
||||
apiGuildsIds: apiGuildsOwns.map(guild => guild.id),
|
||||
dbGuildsIds: dbGuilds.map(guild => guild.id)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds from DB'
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
203
packages/api/src/routers/hub.ts
Normal file
203
packages/api/src/routers/hub.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { getFetch } from '@trpc/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
const fetch = getFetch();
|
||||
|
||||
export const hubRouter = createTRPCRouter({
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, name } = input;
|
||||
const token = process.env.DISCORD_TOKEN;
|
||||
|
||||
let parent;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://discordapp.com/api/guilds/${guildId}/channels`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
type: 4
|
||||
})
|
||||
}
|
||||
);
|
||||
parent = (await response.json()) as any;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new TRPCError({
|
||||
message: 'Could not create channel',
|
||||
code: 'INTERNAL_SERVER_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
let hubChannel;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://discordapp.com/api/guilds/${guildId}/channels`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'Join To Create',
|
||||
type: 2,
|
||||
parent_id: parent.id
|
||||
})
|
||||
}
|
||||
);
|
||||
hubChannel = (await response.json()) as any;
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
message: 'Could not create channel',
|
||||
code: 'INTERNAL_SERVER_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
const updatedGuild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
hub: parent.id,
|
||||
hubChannel: hubChannel.id
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
guild: updatedGuild
|
||||
};
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const token = process.env.DISCORD_TOKEN;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
hub: true,
|
||||
hubChannel: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!guild) {
|
||||
throw new TRPCError({
|
||||
message: 'Guild not found',
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
Promise.all([
|
||||
fetch(`https://discordapp.com/api/channels/${guild.hubChannel}`, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
},
|
||||
method: 'DELETE'
|
||||
}),
|
||||
fetch(`https://discordapp.com/api/channels/${guild.hub}`, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
},
|
||||
method: 'DELETE'
|
||||
})
|
||||
]).then(async () => {
|
||||
await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
hub: null,
|
||||
hubChannel: null
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new TRPCError({
|
||||
message: 'Could not delete channel',
|
||||
code: 'INTERNAL_SERVER_ERROR'
|
||||
});
|
||||
}
|
||||
}),
|
||||
getTempChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
ownerId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId, ownerId } = input;
|
||||
|
||||
const tempChannel = await ctx.prisma.tempChannel.findFirst({
|
||||
where: {
|
||||
guildId,
|
||||
ownerId
|
||||
}
|
||||
});
|
||||
|
||||
return { tempChannel };
|
||||
}),
|
||||
createTempChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
ownerId: z.string(),
|
||||
channelId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, ownerId, channelId } = input;
|
||||
|
||||
const tempChannel = await ctx.prisma.tempChannel.create({
|
||||
data: {
|
||||
guildId,
|
||||
ownerId,
|
||||
id: channelId
|
||||
}
|
||||
});
|
||||
|
||||
return { tempChannel };
|
||||
}),
|
||||
deleteTempChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
channelId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { channelId } = input;
|
||||
|
||||
const tempChannel = await ctx.prisma.tempChannel.delete({
|
||||
where: {
|
||||
id: channelId
|
||||
}
|
||||
});
|
||||
|
||||
return { tempChannel };
|
||||
})
|
||||
});
|
||||
33
packages/api/src/routers/index.ts
Normal file
33
packages/api/src/routers/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createTRPCRouter } from '../trpc';
|
||||
import { channelRouter } from './channel';
|
||||
import { commandRouter } from './command';
|
||||
import { guildRouter } from './guild';
|
||||
import { hubRouter } from './hub';
|
||||
import { playlistRouter } from './playlist';
|
||||
import { reminderRouter } from './reminder';
|
||||
import { songRouter } from './song';
|
||||
import { twitchRouter } from './twitch';
|
||||
import { userRouter } from './user';
|
||||
import { welcomeRouter } from './welcome';
|
||||
|
||||
/**
|
||||
* Create your application's root router
|
||||
* If you want to use SSG, you need export this
|
||||
* @link https://trpc.io/docs/ssg
|
||||
* @link https://trpc.io/docs/router
|
||||
*/
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
user: userRouter,
|
||||
guild: guildRouter,
|
||||
playlist: playlistRouter,
|
||||
song: songRouter,
|
||||
twitch: twitchRouter,
|
||||
channel: channelRouter,
|
||||
welcome: welcomeRouter,
|
||||
command: commandRouter,
|
||||
hub: hubRouter,
|
||||
reminder: reminderRouter
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
93
packages/api/src/routers/playlist.ts
Normal file
93
packages/api/src/routers/playlist.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const playlistRouter = createTRPCRouter({
|
||||
getPlaylist: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { userId, name } = input;
|
||||
|
||||
const playlist = await ctx.prisma.playlist.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
name
|
||||
},
|
||||
include: {
|
||||
songs: true
|
||||
}
|
||||
});
|
||||
|
||||
return { playlist };
|
||||
}),
|
||||
getAll: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { userId } = input;
|
||||
|
||||
const playlists = await ctx.prisma.playlist.findMany({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
include: {
|
||||
songs: true
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
return { playlists };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, name } = input;
|
||||
|
||||
const playlist = await ctx.prisma.playlist.create({
|
||||
data: {
|
||||
name,
|
||||
user: {
|
||||
connect: {
|
||||
id: userId
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { playlist };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, name } = input;
|
||||
|
||||
const playlist = await ctx.prisma.playlist.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
name
|
||||
}
|
||||
});
|
||||
|
||||
return { playlist };
|
||||
})
|
||||
});
|
||||
103
packages/api/src/routers/reminder.ts
Normal file
103
packages/api/src/routers/reminder.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const reminderRouter = createTRPCRouter({
|
||||
getAll: publicProcedure.query(async ({ ctx }) => {
|
||||
const reminders = await ctx.prisma.reminder.findMany();
|
||||
|
||||
return { reminders };
|
||||
}),
|
||||
getReminder: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
event: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, event } = input;
|
||||
|
||||
const reminder = await ctx.prisma.reminder.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
event
|
||||
},
|
||||
include: { user: true }
|
||||
});
|
||||
|
||||
return { reminder };
|
||||
}),
|
||||
getByUserId: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId } = input;
|
||||
|
||||
const reminders = await ctx.prisma.reminder.findMany({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
select: {
|
||||
event: true,
|
||||
dateTime: true,
|
||||
description: true
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
return { reminders };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
event: z.string(),
|
||||
description: z.nullable(z.string()),
|
||||
dateTime: z.string(),
|
||||
repeat: z.nullable(z.string()),
|
||||
timeOffset: z.number()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, event, description, dateTime, repeat, timeOffset } =
|
||||
input;
|
||||
|
||||
const reminder = await ctx.prisma.reminder.create({
|
||||
data: {
|
||||
event,
|
||||
description,
|
||||
dateTime,
|
||||
repeat,
|
||||
timeOffset,
|
||||
user: { connect: { discordId: userId } }
|
||||
}
|
||||
});
|
||||
|
||||
return { reminder };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
event: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, event } = input;
|
||||
|
||||
const reminder = await ctx.prisma.reminder.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
event
|
||||
}
|
||||
});
|
||||
|
||||
return { reminder };
|
||||
})
|
||||
});
|
||||
38
packages/api/src/routers/song.ts
Normal file
38
packages/api/src/routers/song.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const songRouter = createTRPCRouter({
|
||||
createMany: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
songs: z.array(z.any())
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { songs } = input;
|
||||
|
||||
const songsCreated = await ctx.prisma.song.createMany({
|
||||
data: songs
|
||||
});
|
||||
|
||||
return { songsCreated };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const song = await ctx.prisma.song.delete({
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
});
|
||||
|
||||
return { song };
|
||||
})
|
||||
});
|
||||
106
packages/api/src/routers/twitch.ts
Normal file
106
packages/api/src/routers/twitch.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const twitchRouter = createTRPCRouter({
|
||||
getAll: publicProcedure.query(async ({ ctx }) => {
|
||||
const notifications = await ctx.prisma.twitchNotify.findMany();
|
||||
|
||||
return { notifications };
|
||||
}),
|
||||
findUserById: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const notification = await ctx.prisma.twitchNotify.findFirst({
|
||||
where: {
|
||||
twitchId: id
|
||||
}
|
||||
});
|
||||
|
||||
return { notification };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
userImage: z.string(),
|
||||
channelId: z.string(),
|
||||
sendTo: z.array(z.string())
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, userImage, channelId, sendTo } = input;
|
||||
await ctx.prisma.twitchNotify.upsert({
|
||||
create: {
|
||||
twitchId: userId,
|
||||
channelIds: [channelId],
|
||||
logo: userImage,
|
||||
sent: false
|
||||
},
|
||||
update: { channelIds: sendTo },
|
||||
where: { twitchId: userId }
|
||||
});
|
||||
}),
|
||||
updateNotification: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
channelIds: z.array(z.string())
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, channelIds } = input;
|
||||
|
||||
const notification = await ctx.prisma.twitchNotify.update({
|
||||
where: {
|
||||
twitchId: userId
|
||||
},
|
||||
data: {
|
||||
channelIds
|
||||
}
|
||||
});
|
||||
|
||||
return { notification };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId } = input;
|
||||
|
||||
const notification = await ctx.prisma.twitchNotify.delete({
|
||||
where: {
|
||||
twitchId: userId
|
||||
}
|
||||
});
|
||||
|
||||
return { notification };
|
||||
}),
|
||||
updateNotificationStatus: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
live: z.boolean(),
|
||||
sent: z.boolean()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { live, sent, userId } = input;
|
||||
|
||||
const notification = await ctx.prisma.twitchNotify.update({
|
||||
where: { twitchId: userId },
|
||||
data: { live, sent }
|
||||
});
|
||||
|
||||
return { notification };
|
||||
})
|
||||
});
|
||||
80
packages/api/src/routers/user.ts
Normal file
80
packages/api/src/routers/user.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
getUserById: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const user = await ctx.prisma.user.findUnique({
|
||||
where: {
|
||||
discordId: id
|
||||
}
|
||||
});
|
||||
|
||||
return { user };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, name } = input;
|
||||
const user = await ctx.prisma.user.upsert({
|
||||
where: {
|
||||
discordId: id
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
discordId: id,
|
||||
name
|
||||
}
|
||||
});
|
||||
return { user };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const user = await ctx.prisma.user.delete({
|
||||
where: {
|
||||
discordId: id
|
||||
}
|
||||
});
|
||||
|
||||
return { user };
|
||||
}),
|
||||
updateTimeOffset: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
timeOffset: z.number()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, timeOffset } = input;
|
||||
const userTime = await ctx.prisma.user.update({
|
||||
where: {
|
||||
discordId: id
|
||||
},
|
||||
data: { timeOffset: timeOffset },
|
||||
select: { timeOffset: true }
|
||||
});
|
||||
|
||||
return { userTime };
|
||||
})
|
||||
});
|
||||
128
packages/api/src/routers/welcome.ts
Normal file
128
packages/api/src/routers/welcome.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const welcomeRouter = createTRPCRouter({
|
||||
getMessage: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: guild?.welcomeMessage
|
||||
};
|
||||
}),
|
||||
setMessage: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
message: z.string().min(4).max(100),
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { message, guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
welcomeMessage: message
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
setChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
channelId: z.string(),
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { channelId, guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
welcomeMessageChannel: channelId
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
getChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
welcomeMessageChannel: true
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
getStatus: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
welcomeMessageEnabled: true
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
toggle: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
status: z.boolean()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, status } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
welcomeMessageEnabled: status
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
})
|
||||
});
|
||||
131
packages/api/src/trpc.ts
Normal file
131
packages/api/src/trpc.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
||||
* 1. You want to modify request context (see Part 1)
|
||||
* 2. You want to create a new middleware or type of procedure (see Part 3)
|
||||
*
|
||||
* tl;dr - this is where all the tRPC server stuff is created and plugged in.
|
||||
* The pieces you will need to use are documented accordingly near the end
|
||||
*/
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import superjson from 'superjson';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import { auth } from '@master-bot/auth';
|
||||
import type { Session } from '@master-bot/auth';
|
||||
import { prisma } from '@master-bot/db';
|
||||
|
||||
/**
|
||||
* 1. CONTEXT
|
||||
*
|
||||
* This section defines the "contexts" that are available in the backend API
|
||||
*
|
||||
* These allow you to access things like the database, the session, etc, when
|
||||
* processing a request
|
||||
*
|
||||
*/
|
||||
interface CreateContextOptions {
|
||||
session: Session | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper generates the "internals" for a tRPC context. If you need to use
|
||||
* it, you can export it from here
|
||||
*
|
||||
* Examples of things you may need it for:
|
||||
* - testing, so we dont have to mock Next.js' req/res
|
||||
* - trpc's `createSSGHelpers` where we don't have req/res
|
||||
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
|
||||
*/
|
||||
const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||
return {
|
||||
session: opts.session,
|
||||
prisma
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the actual context you'll use in your router. It will be used to
|
||||
* process every request that goes through your tRPC endpoint
|
||||
* @link https://trpc.io/docs/context
|
||||
*/
|
||||
export const createTRPCContext = async (opts: {
|
||||
req?: Request;
|
||||
auth?: Session;
|
||||
}) => {
|
||||
const session = opts.auth ?? (await auth());
|
||||
// const source = opts.req?.headers.get('x-trpc-source') ?? 'unknown';
|
||||
|
||||
// console.log('>>> tRPC Request from', source, 'by', session?.user);
|
||||
|
||||
return createInnerTRPCContext({
|
||||
session
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. INITIALIZATION
|
||||
*
|
||||
* This is where the trpc api is initialized, connecting the context and
|
||||
* transformer
|
||||
*/
|
||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape, error }) {
|
||||
return {
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||
*
|
||||
* These are the pieces you use to build your tRPC API. You should import these
|
||||
* a lot in the /src/server/api/routers folder
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is how you create new routers and subrouters in your tRPC API
|
||||
* @see https://trpc.io/docs/router
|
||||
*/
|
||||
export const createTRPCRouter = t.router;
|
||||
|
||||
/**
|
||||
* Public (unauthed) procedure
|
||||
*
|
||||
* This is the base piece you use to build new queries and mutations on your
|
||||
* tRPC API. It does not guarantee that a user querying is authorized, but you
|
||||
* can still access user session data if they are logged in
|
||||
*/
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
/**
|
||||
* Reusable middleware that enforces users are logged in before running the
|
||||
* procedure
|
||||
*/
|
||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session?.user) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session, user: ctx.session.user }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Protected (authed) procedure
|
||||
*
|
||||
* If you want a query or mutation to ONLY be accessible to logged in users, use
|
||||
* this. It verifies the session is valid and guarantees ctx.session.user is not
|
||||
* null
|
||||
*
|
||||
* @see https://trpc.io/docs/procedures
|
||||
*/
|
||||
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
|
||||
137
packages/api/src/utils/axiosWithRefresh.ts
Normal file
137
packages/api/src/utils/axiosWithRefresh.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import axios, { type AxiosError } from 'axios';
|
||||
|
||||
import { prisma } from '@master-bot/db';
|
||||
|
||||
import { env } from '../env.mjs';
|
||||
|
||||
// const baseURL = 'https://discord.com/api/v10'; // Update to the appropriate Discord API version
|
||||
|
||||
const discordApi = axios.create();
|
||||
|
||||
async function refreshAccessToken(refreshToken: string, userId: string) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
client_id: env.DISCORD_CLIENT_ID,
|
||||
client_secret: env.DISCORD_CLIENT_SECRET,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken
|
||||
});
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await discordApi.post(
|
||||
'https://discord.com/api/v10/oauth2/token',
|
||||
params,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('error in refreshing token', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const {
|
||||
access_token,
|
||||
refresh_token: newRefreshToken,
|
||||
expires_in
|
||||
} = response.data;
|
||||
|
||||
// Update the access and refresh tokens in the database
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
data: {
|
||||
access_token,
|
||||
refresh_token: newRefreshToken,
|
||||
expires_at: expires_in
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: access_token,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn: expires_in
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error refreshing access token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUserTokens(
|
||||
newTokens: {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
},
|
||||
userId: string
|
||||
) {
|
||||
try {
|
||||
const updatedAccount = await prisma.account.update({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
data: {
|
||||
access_token: newTokens.accessToken,
|
||||
refresh_token: newTokens.refreshToken,
|
||||
expires_at: newTokens.expiresIn
|
||||
}
|
||||
});
|
||||
|
||||
return updatedAccount;
|
||||
} catch (error) {
|
||||
console.error('Error updating user tokens:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
discordApi.interceptors.response.use(
|
||||
response => {
|
||||
// if response is ok return it
|
||||
return response;
|
||||
},
|
||||
async (error: Error | AxiosError) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.code === 'ERR_BAD_REQUEST') {
|
||||
const { 'X-User-Id': userId, 'X-Refresh-Token': refreshToken } =
|
||||
originalRequest!.headers;
|
||||
|
||||
if (typeof userId !== 'string' || typeof refreshToken !== 'string') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const newTokens = await refreshAccessToken(refreshToken, userId);
|
||||
|
||||
if (!newTokens?.accessToken) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Save the new access token and refresh token to the DB
|
||||
try {
|
||||
await updateUserTokens(newTokens, userId);
|
||||
} catch {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Set the new access token in the header and retry the original request
|
||||
originalRequest!.headers[
|
||||
'Authorization'
|
||||
] = `Bearer ${newTokens.accessToken}`;
|
||||
|
||||
return discordApi(originalRequest!);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { discordApi };
|
||||
Reference in New Issue
Block a user