mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-08-22 00:24:34 -05:00
Add Discord admin webhook notifications
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -40,6 +40,7 @@
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"discord-api-types": "^0.38.32",
|
||||
"patch-package": "^8.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^4",
|
||||
@@ -3728,6 +3729,16 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.32",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.32.tgz",
|
||||
"integrity": "sha512-UhIqkFuUVwBzejLPPWF18qixYPucMf718RnGh1NxZYNS7czXUmcUsWWkzWR7lRWj5pjfj4LwrnN9McvpfLvGqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
]
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"discord-api-types": "^0.38.32",
|
||||
"patch-package": "^8.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^4",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { sendDiscordMessageEmbed } from "@/utils/discord";
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
|
||||
export type UpdateState =
|
||||
@@ -23,3 +24,44 @@ export async function updatePassword(state: UpdateState, payload: FormData): Pro
|
||||
|
||||
return { ok: true, error: null } as const;
|
||||
}
|
||||
|
||||
export async function setupProfile(username: string): Promise<UpdateState> {
|
||||
const supabase = await createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error: authError,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (authError || !user) {
|
||||
return { ok: false, error: "You must be logged in to set up your profile." } as const;
|
||||
}
|
||||
|
||||
if (!username || username.length < 3 || username.length > 20) {
|
||||
return { ok: false, error: "Username must be between 3 and 20 characters." } as const;
|
||||
}
|
||||
|
||||
const { error } = await supabase.from("profiles").upsert({
|
||||
id: user.id,
|
||||
username,
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { ok: false, error: error.message || "There was an error saving. Please try again." } as const;
|
||||
}
|
||||
|
||||
if (process.env.DISCORD_WEBHOOK_ADMIN_URL) {
|
||||
await sendDiscordMessageEmbed(process.env.DISCORD_WEBHOOK_ADMIN_URL, [
|
||||
{
|
||||
title: 'New Profile Setup',
|
||||
description: `A new user (\`${user.id}\`) has setup their profile with the username: \`@${username}\``,
|
||||
color: 0x40f56a,
|
||||
footer: {
|
||||
text: 'This message brought to you by Hackdex'
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
return { ok: true, error: null } as const;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { get } from '@vercel/edge-config'
|
||||
|
||||
import { createClient, createServiceClient } from '@/utils/supabase/server'
|
||||
import { validateEmail, validatePassword } from '@/utils/auth'
|
||||
import { sendDiscordMessageEmbed } from '@/utils/discord'
|
||||
|
||||
function getErrorMessage(error: AuthError): string {
|
||||
const code = (error.code)?.toLowerCase()
|
||||
@@ -87,6 +88,18 @@ export async function signup(state: AuthActionState, payload: FormData) {
|
||||
const userId = signUpResult.user?.id || null
|
||||
if (isStaticInvite) {
|
||||
console.log('[signup] Static invite code used:', { inviteCode, userId })
|
||||
if (process.env.DISCORD_WEBHOOK_ADMIN_URL) {
|
||||
await sendDiscordMessageEmbed(process.env.DISCORD_WEBHOOK_ADMIN_URL, [
|
||||
{
|
||||
title: 'New User Signup',
|
||||
description: `A new user (\`${userId}\`) has signed up using the static invite code: \`${inviteCode}\``,
|
||||
color: 0x40f56a,
|
||||
footer: {
|
||||
text: 'A notification will be sent when this user has created their profile'
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Finalize: set used_by to the new user id iff still unused (atomic)
|
||||
const { data: finalized, error: finalizeError } = await service
|
||||
@@ -105,6 +118,19 @@ export async function signup(state: AuthActionState, payload: FormData) {
|
||||
} catch {}
|
||||
}
|
||||
return { error: 'Invite code is no longer available. Please try again.' }
|
||||
} else {
|
||||
if (process.env.DISCORD_WEBHOOK_ADMIN_URL) {
|
||||
await sendDiscordMessageEmbed(process.env.DISCORD_WEBHOOK_ADMIN_URL, [
|
||||
{
|
||||
title: 'New User Signup',
|
||||
description: `A new user (\`${userId}\`) has signed up using the invite code: \`${inviteCode}\``,
|
||||
color: 0x40f56a,
|
||||
footer: {
|
||||
text: 'A notification will be sent when this user has created their profile'
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import type { TablesInsert } from "@/types/db";
|
||||
import { getMinioClient, PATCHES_BUCKET } from "@/utils/minio/server";
|
||||
import { sendDiscordMessageEmbed } from "@/utils/discord";
|
||||
import { APIEmbed } from "discord-api-types/v10";
|
||||
|
||||
type HackInsert = TablesInsert<"hacks">;
|
||||
|
||||
@@ -152,7 +154,7 @@ export async function presignPatchAndSaveCovers(args: {
|
||||
return { ok: true, presignedUrl: url, objectKey } as const;
|
||||
}
|
||||
|
||||
export async function confirmPatchUpload(args: { slug: string; objectKey: string; version: string }) {
|
||||
export async function confirmPatchUpload(args: { slug: string; objectKey: string; version: string, firstUpload?: boolean }) {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
@@ -161,7 +163,7 @@ export async function confirmPatchUpload(args: { slug: string; objectKey: string
|
||||
|
||||
const { data: hack, error: hErr } = await supabase
|
||||
.from("hacks")
|
||||
.select("slug, created_by")
|
||||
.select("slug, created_by, title")
|
||||
.eq("slug", args.slug)
|
||||
.maybeSingle();
|
||||
if (hErr) return { ok: false, error: hErr.message } as const;
|
||||
@@ -192,6 +194,33 @@ export async function confirmPatchUpload(args: { slug: string; objectKey: string
|
||||
.eq("slug", args.slug);
|
||||
if (uErr) return { ok: false, error: uErr.message } as const;
|
||||
|
||||
if (process.env.DISCORD_WEBHOOK_ADMIN_URL) {
|
||||
const { data: profile } = await supabase.from('profiles').select('*').eq('id', user.id).single();
|
||||
|
||||
const displayName = profile?.username ? `@${profile.username}` : user.id;
|
||||
const embed: APIEmbed = args.firstUpload ? {
|
||||
title: `:tada: ${hack.title}`,
|
||||
description: `A new hack by **${displayName}** is pending approval by an admin.`,
|
||||
color: 0x40f56a,
|
||||
url: `${process.env.NEXT_PUBLIC_SITE_URL}/hack/${args.slug}`,
|
||||
footer: {
|
||||
text: `This message brought to you by Hackdex`
|
||||
}
|
||||
} : {
|
||||
title: `New update for ${hack.title}`,
|
||||
description: `**${hack.title}** has been updated to **${args.version}**`,
|
||||
color: 0x40f56a,
|
||||
url: `${process.env.NEXT_PUBLIC_SITE_URL}/hack/${args.slug}`,
|
||||
footer: {
|
||||
text: `This message brought to you by Hackdex`
|
||||
}
|
||||
}
|
||||
|
||||
await sendDiscordMessageEmbed(process.env.DISCORD_WEBHOOK_ADMIN_URL, [
|
||||
embed,
|
||||
]);
|
||||
}
|
||||
|
||||
return { ok: true, patchId: patch.id, redirectTo: `/hack/${args.slug}` } as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createClient } from '@/utils/supabase/client'
|
||||
import { type User } from '@supabase/supabase-js'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { FiCheck, FiX, FiCopy } from 'react-icons/fi'
|
||||
import { setupProfile } from '@/app/account/actions'
|
||||
|
||||
type UsernameState =
|
||||
| { status: 'idle'; message: string }
|
||||
@@ -114,13 +115,12 @@ export default function AccountSetupForm({ user }: { user: User }) {
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
const { error } = await supabase.from('profiles').upsert({
|
||||
id: user.id,
|
||||
username,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
const result = await setupProfile(username)
|
||||
|
||||
if (error) throw error
|
||||
if (!result || !result.ok) {
|
||||
setUsernameState({ status: 'invalid', message: result?.error || 'There was an error saving. Please try again.' })
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh SSR boundary so the page re-renders into the update form state
|
||||
router.refresh()
|
||||
|
||||
@@ -410,7 +410,7 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
|
||||
|
||||
if (patchFile) {
|
||||
await fetch(presigned.presignedUrl, { method: 'PUT', body: patchFile, headers: { 'Content-Type': 'application/octet-stream' } });
|
||||
const finalized = await confirmPatchUpload({ slug: prepared.slug, objectKey: presigned.objectKey!, version });
|
||||
const finalized = await confirmPatchUpload({ slug: prepared.slug, objectKey: presigned.objectKey!, version, firstUpload: true });
|
||||
if (!finalized.ok) throw new Error(finalized.error || 'Failed to finalize');
|
||||
try {
|
||||
if (draftKey) {
|
||||
|
||||
27
src/utils/discord.ts
Normal file
27
src/utils/discord.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { APIEmbed, RESTPostAPIWebhookWithTokenJSONBody } from "discord-api-types/v10";
|
||||
|
||||
export const sendDiscordMessage = async (url: string, message: RESTPostAPIWebhookWithTokenJSONBody) => {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(message),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const sendDiscordMessageEmbed = async (url: string, embeds: APIEmbed[]) => {
|
||||
const message: RESTPostAPIWebhookWithTokenJSONBody = {
|
||||
embeds,
|
||||
};
|
||||
return sendDiscordMessage(url, message);
|
||||
};
|
||||
|
||||
export const sendDiscordMessageText = async (url: string, content: string) => {
|
||||
const message: RESTPostAPIWebhookWithTokenJSONBody = {
|
||||
content,
|
||||
};
|
||||
return sendDiscordMessage(url, message);
|
||||
};
|
||||
Reference in New Issue
Block a user