Skip to content

Commit 8af92a7

Browse files
authored
Merge pull request #3 from HighError/env-validation
adds env validation with t3 env
2 parents 57f2059 + 28d1c88 commit 8af92a7

File tree

11 files changed

+987
-892
lines changed

11 files changed

+987
-892
lines changed

.env.local.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ DISCORD_APP_ID=
55
DISCORD_APP_PUBLIC_KEY=
66

77
# Settings -> Bot
8-
# Required to register commands, not required to actually run the bot
8+
# Required to register commands and fetch the list of commands in the web app
9+
# Technically not required to actually run the bot
910
DISCORD_BOT_TOKEN=
1011

1112
# Set this with your local ngrok URL for the pokemon images to be served correctly.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,5 @@ yarn-error.log*
3232

3333
# vercel
3434
.vercel
35+
36+
*.tsbuildinfo
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import "./src/env.mjs"
2+
13
/** @type {import('next').NextConfig} */
24
const nextConfig = {}
35

4-
module.exports = nextConfig
6+
export default nextConfig

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"register-commands": "tsx ./scripts/register-commands"
1010
},
1111
"dependencies": {
12+
"@t3-oss/env-nextjs": "^0.7.1",
1213
"autoprefixer": "^10.4.16",
1314
"discord-api-types": "^0.37.54",
1415
"dotenv": "^16.3.1",
@@ -19,7 +20,8 @@
1920
"react": "^18.2.0",
2021
"react-dom": "^18.2.0",
2122
"tailwindcss": "^3.3.3",
22-
"tweetnacl": "^1.0.3"
23+
"tweetnacl": "^1.0.3",
24+
"zod": "^3.22.4"
2325
},
2426
"devDependencies": {
2527
"@ianvs/prettier-plugin-sort-imports": "^4.1.0",

scripts/env.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createEnv } from "@t3-oss/env-nextjs"
2+
import dotenv from "dotenv"
3+
import { z } from "zod"
4+
5+
dotenv.config({ path: ".env.local" })
6+
7+
export const env = createEnv({
8+
server: {
9+
DISCORD_APP_ID: z
10+
.string({
11+
required_error:
12+
"DISCORD_APP_ID is required. Visit https://discord.com/developers/applications -> Your bot -> General information -> Application ID. Required in .env.local",
13+
})
14+
.min(
15+
1,
16+
"DISCORD_APP_ID is required. Visit https://discord.com/developers/applications -> Your bot -> General information -> Application ID. Required in .env.local"
17+
),
18+
DISCORD_BOT_TOKEN: z
19+
.string({
20+
required_error:
21+
"DISCORD_BOT_TOKEN is required. Visit https://discord.com/developers/applications -> Bot -> Token. This variable used only for register commands. Required in .env.local",
22+
})
23+
.min(
24+
1,
25+
"DISCORD_BOT_TOKEN is required. Visit https://discord.com/developers/applications -> Bot -> Token. This variable used only for register commands. Required in .env.local"
26+
),
27+
},
28+
onValidationError: (error) => {
29+
throw new Error(
30+
`❌ Invalid environment variables:\n\n${error.errors.map((e, i) => `❌[${i}]: ${e.message}`).join("\n")}\n`
31+
)
32+
},
33+
})

scripts/register-commands.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,9 @@
1212
*/
1313

1414
import { commands } from "@/commands"
15-
import dotenv from "dotenv"
15+
import { env } from "./env.mjs"
1616

17-
dotenv.config({ path: ".env.local" })
18-
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN
19-
const DISCORD_APP_ID = process.env.DISCORD_APP_ID
20-
if (!DISCORD_BOT_TOKEN) {
21-
throw new Error("The DISCORD_BOT_TOKEN environment variable is required.")
22-
}
23-
if (!DISCORD_APP_ID) {
24-
throw new Error("The DISCORD_APP_ID environment variable is required.")
25-
}
26-
27-
const URL = `https://discord.com/api/v10/applications/${DISCORD_APP_ID}/commands`
17+
const URL = `https://discord.com/api/v10/applications/${env.DISCORD_APP_ID}/commands`
2818

2919
/**
3020
* Register all commands globally. This can take o(minutes), so wait until
@@ -36,7 +26,7 @@ async function main() {
3626
const response = await fetch(URL, {
3727
headers: {
3828
"Content-Type": "application/json",
39-
Authorization: `Bot ${DISCORD_BOT_TOKEN}`,
29+
Authorization: `Bot ${env.DISCORD_BOT_TOKEN}`,
4030
},
4131
method: "PUT",
4232
body: JSON.stringify(Object.values(commands)),

src/app/api/interactions/route.ts

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { commands, RandomPicType } from "@/commands"
22
import { verifyInteractionRequest } from "@/discord/verify-incoming-request"
3+
import { env } from "@/env.mjs"
34
import {
45
APIInteractionDataOptionBase,
56
ApplicationCommandOptionType,
@@ -18,11 +19,7 @@ import { getRandomPic } from "./random-pic"
1819
*/
1920
export const runtime = "edge"
2021

21-
// Your public key can be found on your application in the Developer Portal
22-
const DISCORD_APP_PUBLIC_KEY = process.env.DISCORD_APP_PUBLIC_KEY
23-
const ROOT_URL = process.env.VERCEL_URL
24-
? `https://${process.env.VERCEL_URL}`
25-
: process.env.ROOT_URL || "http://localhost:3000"
22+
const ROOT_URL = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : env.ROOT_URL
2623

2724
function capitalizeFirstLetter(s: string) {
2825
return s.charAt(0).toUpperCase() + s.slice(1)
@@ -34,7 +31,7 @@ function capitalizeFirstLetter(s: string) {
3431
* @see https://discord.com/developers/docs/interactions/receiving-and-responding#receiving-an-interaction
3532
*/
3633
export async function POST(request: Request) {
37-
const verifyResult = await verifyInteractionRequest(request, DISCORD_APP_PUBLIC_KEY!)
34+
const verifyResult = await verifyInteractionRequest(request, env.DISCORD_APP_PUBLIC_KEY)
3835
if (!verifyResult.isValid || !verifyResult.interaction) {
3936
return new NextResponse("Invalid request", { status: 401 })
4037
}
@@ -60,7 +57,7 @@ export async function POST(request: Request) {
6057
return NextResponse.json({
6158
type: InteractionResponseType.ChannelMessageWithSource,
6259
data: {
63-
content: `Click this link to add NextBot to your server: https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_APP_ID}&permissions=2147485696&scope=bot%20applications.commands`,
60+
content: `Click this link to add NextBot to your server: https://discord.com/api/oauth2/authorize?client_id=${env.DISCORD_APP_ID}&permissions=2147485696&scope=bot%20applications.commands`,
6461
flags: MessageFlags.Ephemeral,
6562
},
6663
})
@@ -89,29 +86,6 @@ export async function POST(request: Request) {
8986
[]
9087
)
9188

92-
const r = {
93-
type: InteractionResponseType.ChannelMessageWithSource,
94-
data: {
95-
embeds: [
96-
{
97-
title: capitalizeFirstLetter(pokemon.name),
98-
image: {
99-
url: `${ROOT_URL}/api/pokemon/${idOrName}`,
100-
},
101-
fields: [
102-
{
103-
name: "Pokedex",
104-
value: `#${String(pokemon.id).padStart(3, "0")}`,
105-
},
106-
{
107-
name: "Type",
108-
value: types.join("/"),
109-
},
110-
],
111-
},
112-
],
113-
},
114-
}
11589
return NextResponse.json({
11690
type: InteractionResponseType.ChannelMessageWithSource,
11791
data: {

src/app/global-commands.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { env } from "@/env.mjs"
12
import { APIApplication } from "discord-api-types/v10"
23

34
export async function GlobalCommands() {
45
try {
5-
const commands = await fetch(`https://discord.com/api/v8/applications/${process.env.DISCORD_APP_ID}/commands`, {
6+
const commands = await fetch(`https://discord.com/api/v8/applications/${env.DISCORD_APP_ID}/commands`, {
67
headers: {
7-
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
8+
Authorization: `Bot ${env.DISCORD_BOT_TOKEN}`,
89
},
910
next: { revalidate: 60 * 5 },
1011
}).then((res) => res.json() as Promise<APIApplication[]>)

src/env.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createEnv } from "@t3-oss/env-nextjs"
2+
import { z } from "zod"
3+
4+
export const env = createEnv({
5+
server: {
6+
DISCORD_APP_ID: z
7+
.string({
8+
required_error:
9+
"DISCORD_APP_ID is required. Visit https://discord.com/developers/applications -> Your bot -> General information -> Application ID",
10+
})
11+
.min(
12+
1,
13+
"DISCORD_APP_ID is required. Visit https://discord.com/developers/applications -> Your bot -> General information -> Application ID"
14+
),
15+
DISCORD_APP_PUBLIC_KEY: z
16+
.string({
17+
required_error:
18+
"DISCORD_APP_PUBLIC_KEY is required. Visit https://discord.com/developers/applications -> General information -> PUBLIC KEY",
19+
})
20+
.min(
21+
1,
22+
"DISCORD_APP_PUBLIC_KEY is required. Visit https://discord.com/developers/applications -> General information -> PUBLIC KEY"
23+
),
24+
DISCORD_BOT_TOKEN: z
25+
.string({
26+
required_error:
27+
"DISCORD_BOT_TOKEN is required. Visit https://discord.com/developers/applications -> Bot -> Token. This variable used only for register commands",
28+
})
29+
.min(
30+
1,
31+
"DISCORD_BOT_TOKEN is required. Visit https://discord.com/developers/applications -> Bot -> Token. This variable used only for register commands"
32+
),
33+
ROOT_URL: z.string().url("ROOT_URL must be a valid URL").optional().default("http://localhost:3000"),
34+
},
35+
onInvalidAccess: (error) => {
36+
throw new Error(`❌ Attempted to access a server-side environment variable on the client: ${error}`)
37+
},
38+
onValidationError: (error) => {
39+
throw new Error(
40+
`❌ Invalid environment variables:\n\n${error.errors.map((e, i) => `❌[${i}]: ${e.message}`).join("\n")}\n`
41+
)
42+
},
43+
})

tsconfig.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
"strict": true,
1818
"target": "es5"
1919
},
20-
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "scripts/register-commands.mjs"],
20+
"include": [
21+
"next-env.d.ts",
22+
"**/*.ts",
23+
"**/*.tsx",
24+
".next/types/**/*.ts",
25+
"scripts/register-commands.mjs",
26+
"src/env.mjs"
27+
],
2128
"exclude": ["node_modules"]
2229
}

0 commit comments

Comments
 (0)