Merge pull request 'webhooks/integrations' (#142) from webhooks/integrations into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m27s
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m27s
Reviewed-on: #142
This commit was merged in pull request #142.
This commit is contained in:
@@ -21,7 +21,10 @@ CLIENT_URL= # This is whatever URL the client web app is served on
|
|||||||
CLIENT_DOMAIN= #whatever.com
|
CLIENT_DOMAIN= #whatever.com
|
||||||
APPLICATION_VERSION= # Should match release tag
|
APPLICATION_VERSION= # Should match release tag
|
||||||
APPLICATION_ENVIRONMENT= # dev / prod
|
APPLICATION_ENVIRONMENT= # dev / prod
|
||||||
CONFIG_ID= # configures
|
CONFIG_ID= # config version
|
||||||
|
|
||||||
|
# webhooks/integrations
|
||||||
|
DISCORD_APPLICATIONS_WEBHOOK=
|
||||||
|
|
||||||
# Logger
|
# Logger
|
||||||
LOG_DEPTH= # normal / verbose / profiling
|
LOG_DEPTH= # normal / verbose / profiling
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ const sessionOptions: session.SessionOptions = {
|
|||||||
cookie: cookieOptions
|
cookie: cookieOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { initializeDiscordIntegrations } from './services/integrations/discord';
|
||||||
|
|
||||||
|
//event bus setup
|
||||||
|
initializeDiscordIntegrations();
|
||||||
|
|
||||||
app.use(session(sessionOptions));
|
app.use(session(sessionOptions));
|
||||||
app.use(passport.authenticate('session'));
|
app.use(passport.authenticate('session'));
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Request, response, Response } from 'express';
|
|||||||
import { getUserRoles } from '../services/db/rolesService';
|
import { getUserRoles } from '../services/db/rolesService';
|
||||||
import { requireLogin, requireRole } from '../middleware/auth';
|
import { requireLogin, requireRole } from '../middleware/auth';
|
||||||
import { logger } from '../services/logging/logger';
|
import { logger } from '../services/logging/logger';
|
||||||
|
import { bus } from '../services/events/eventBus';
|
||||||
|
|
||||||
//get CoC
|
//get CoC
|
||||||
router.get('/coc', async (req: Request, res: Response) => {
|
router.get('/coc', async (req: Request, res: Response) => {
|
||||||
@@ -45,36 +46,24 @@ router.get('/coc', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
|
|
||||||
// POST /application
|
// POST /application
|
||||||
router.post('/', [requireLogin], async (req, res) => {
|
router.post('/', [requireLogin], async (req: Request, res: Response) => {
|
||||||
const memberID = req.user.id;
|
const memberID = req.user.id;
|
||||||
const App = req.body?.App || {};
|
const App = req.body?.App || {};
|
||||||
const appVersion = 1;
|
const appVersion = 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
req.profiler?.start('createApplication');
|
let appID = await createApplication(memberID, appVersion, JSON.stringify(App));
|
||||||
await createApplication(memberID, appVersion, JSON.stringify(App));
|
|
||||||
req.profiler?.end('createApplication');
|
|
||||||
|
|
||||||
req.profiler?.start('setUserState');
|
|
||||||
await setUserState(memberID, MemberState.Applicant);
|
await setUserState(memberID, MemberState.Applicant);
|
||||||
req.profiler?.end('setUserState');
|
|
||||||
|
|
||||||
res.sendStatus(201);
|
res.sendStatus(201);
|
||||||
|
|
||||||
// Log full route profiling
|
bus.emit("application.create", { application: appID, member_name: req.user.name, member_discord_id: req.user.discord_id || null })
|
||||||
const summary = req.profiler?.summary();
|
|
||||||
if (summary) {
|
logger.info('app', 'Application Posted', {
|
||||||
logger.info(
|
user: memberID,
|
||||||
'profiling',
|
app: appID
|
||||||
'POST /application completed',
|
})
|
||||||
{
|
|
||||||
memberID,
|
|
||||||
appVersion,
|
|
||||||
...summary,
|
|
||||||
},
|
|
||||||
'profiling'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'app',
|
'app',
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ passport.deserializeUser(function (user, cb) {
|
|||||||
|
|
||||||
t = performance.now();
|
t = performance.now();
|
||||||
const userResults = await con.query(
|
const userResults = await con.query(
|
||||||
`SELECT id, name FROM members WHERE id = ?;`,
|
`SELECT id, name, discord_id FROM members WHERE id = ?;`,
|
||||||
[memberID]
|
[memberID]
|
||||||
);
|
);
|
||||||
timings.memberQuery = performance.now() - t;
|
timings.memberQuery = performance.now() - t;
|
||||||
@@ -210,6 +210,7 @@ passport.deserializeUser(function (user, cb) {
|
|||||||
name: string;
|
name: string;
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
state: MemberState;
|
state: MemberState;
|
||||||
|
discord_id?: string;
|
||||||
} = userResults[0];
|
} = userResults[0];
|
||||||
|
|
||||||
t = performance.now();
|
t = performance.now();
|
||||||
@@ -259,6 +260,7 @@ declare global {
|
|||||||
user: {
|
user: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
discord_id: string;
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
state: MemberState;
|
state: MemberState;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,19 @@ import { ApplicationListRow, ApplicationRow, CommentRow } from "@app/shared/type
|
|||||||
import pool from "../../db";
|
import pool from "../../db";
|
||||||
import { error } from "console";
|
import { error } from "console";
|
||||||
|
|
||||||
export async function createApplication(memberID: number, appVersion: number, app: string) {
|
/**
|
||||||
|
* Create an application in the db
|
||||||
|
* @param memberID
|
||||||
|
* @param appVersion
|
||||||
|
* @param app
|
||||||
|
* @returns ID of the created application
|
||||||
|
*/
|
||||||
|
export async function createApplication(memberID: number, appVersion: number, app: string): Promise<number> {
|
||||||
const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`;
|
const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`;
|
||||||
const params = [memberID, appVersion, JSON.stringify(app)]
|
const params = [memberID, appVersion, JSON.stringify(app)]
|
||||||
return await pool.query(sql, params);
|
|
||||||
|
let result = await pool.query(sql, params);
|
||||||
|
return Number(result.insertId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMemberApplication(memberID: number): Promise<ApplicationRow> {
|
export async function getMemberApplication(memberID: number): Promise<ApplicationRow> {
|
||||||
|
|||||||
56
api/src/services/events/eventBus.ts
Normal file
56
api/src/services/events/eventBus.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { logger } from "../logging/logger";
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
id: string
|
||||||
|
type: string
|
||||||
|
occurredAt: string
|
||||||
|
payload?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventHandler = (event: Event) => void | Promise<void>;
|
||||||
|
|
||||||
|
class EventBus {
|
||||||
|
private handlers: Map<string, EventHandler[]> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register event listener
|
||||||
|
* @param type
|
||||||
|
* @param handler
|
||||||
|
*/
|
||||||
|
on(type: string, handler: EventHandler) {
|
||||||
|
const handlers = this.handlers.get(type) ?? [];
|
||||||
|
handlers.push(handler);
|
||||||
|
this.handlers.set(type, handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit event of given type
|
||||||
|
* @param type
|
||||||
|
* @param payload
|
||||||
|
*/
|
||||||
|
async emit(type: string, payload?: Record<string, any>) {
|
||||||
|
const event: Event = {
|
||||||
|
id: randomUUID(),
|
||||||
|
type,
|
||||||
|
occurredAt: new Date().toISOString(),
|
||||||
|
payload
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = this.handlers.get(type) ?? []
|
||||||
|
|
||||||
|
for (const h of handlers) {
|
||||||
|
try {
|
||||||
|
await h(event)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('app', 'Event handler failed', {
|
||||||
|
type: event.type,
|
||||||
|
id: event.id,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bus = new EventBus();
|
||||||
32
api/src/services/integrations/discord.ts
Normal file
32
api/src/services/integrations/discord.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { bus } from "../events/eventBus";
|
||||||
|
|
||||||
|
export function initializeDiscordIntegrations() {
|
||||||
|
bus.on('application.create', async (event) => {
|
||||||
|
let applicantName = event.payload.member_discord_id || event.payload.member_name;
|
||||||
|
if (event.payload.member_discord_id) {
|
||||||
|
applicantName = `<@${event.payload.member_discord_id}>`;
|
||||||
|
}
|
||||||
|
const link = `${process.env.CLIENT_URL}/administration/applications/${event.payload.application}`;
|
||||||
|
|
||||||
|
const embed = {
|
||||||
|
title: "Application Posted",
|
||||||
|
description: `[View Application](${link})`,
|
||||||
|
color: 0x00ff00, // optional: green color
|
||||||
|
timestamp: new Date().toISOString(), // <-- Discord expects ISO8601
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Submitted By",
|
||||||
|
value: applicantName,
|
||||||
|
inline: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// send to Discord webhook
|
||||||
|
await fetch(process.env.DISCORD_APPLICATIONS_WEBHOOK!, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ embeds: [embed] }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user