Added async validation

This commit is contained in:
Dries Augustyns
2025-12-01 15:21:12 +01:00
parent 1f3978e89c
commit 361ff6fede
20 changed files with 240 additions and 109 deletions
+6 -3
View File
@@ -1,6 +1,6 @@
import {Controller, Middleware, Post} from '@overnightjs/core';
import {ActionSchemas} from '@plunk/shared';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import type {AuthResponse} from '../middleware/auth.js';
import {requirePublicKey, requireSecretKey} from '../middleware/auth.js';
@@ -10,6 +10,7 @@ import {DomainService} from '../services/DomainService.js';
import {EmailService} from '../services/EmailService.js';
import {EventService} from '../services/EventService.js';
import {NotFound} from '../exceptions/index.js';
import {CatchAsync} from '../utils/asyncHandler.js';
/**
* Public API Actions Controller
@@ -47,7 +48,8 @@ export class Actions {
*/
@Post('track')
@Middleware([requirePublicKey])
public async track(req: Request, res: Response) {
@CatchAsync
public async track(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
// Zod validation - errors automatically handled by global error handler
@@ -152,7 +154,8 @@ export class Actions {
*/
@Post('send')
@Middleware([requireSecretKey])
public async send(req: Request, res: Response) {
@CatchAsync
public async send(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
// Zod validation - errors automatically handled by global error handler
+12 -6
View File
@@ -1,9 +1,10 @@
import {Controller, Get, Middleware} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {ActivityService, ActivityType} from '../services/ActivityService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('activity')
export class Activity {
@@ -21,7 +22,8 @@ export class Activity {
*/
@Get('')
@Middleware([requireAuth])
public async getActivities(req: Request, res: Response) {
@CatchAsync
public async getActivities(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
const cursor = req.query.cursor as string | undefined;
@@ -61,7 +63,8 @@ export class Activity {
*/
@Get('stats')
@Middleware([requireAuth])
public async getStats(req: Request, res: Response) {
@CatchAsync
public async getStats(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined;
const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined;
@@ -80,7 +83,8 @@ export class Activity {
*/
@Get('recent-count')
@Middleware([requireAuth])
public async getRecentCount(req: Request, res: Response) {
@CatchAsync
public async getRecentCount(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const minutes = Math.min(parseInt(req.query.minutes as string) || 5, 60); // Max 60 minutes
@@ -95,7 +99,8 @@ export class Activity {
*/
@Get('types')
@Middleware([requireAuth])
public async getTypes(_req: Request, res: Response) {
@CatchAsync
public async getTypes(_req: Request, res: Response, next: NextFunction) {
const types = Object.values(ActivityType);
return res.status(200).json({types});
}
@@ -110,7 +115,8 @@ export class Activity {
*/
@Get('upcoming')
@Middleware([requireAuth])
public async getUpcoming(req: Request, res: Response) {
@CatchAsync
public async getUpcoming(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
const daysAhead = Math.min(parseInt(req.query.daysAhead as string) || 30, 90);
+6 -3
View File
@@ -1,9 +1,10 @@
import {Controller, Get, Middleware} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {AnalyticsService} from '../services/AnalyticsService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('analytics')
export class Analytics {
@@ -19,7 +20,8 @@ export class Analytics {
*/
@Get('timeseries')
@Middleware([requireAuth])
public async getTimeSeries(req: Request, res: Response) {
@CatchAsync
public async getTimeSeries(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined;
const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined;
@@ -40,7 +42,8 @@ export class Analytics {
*/
@Get('top-campaigns')
@Middleware([requireAuth])
public async getTopCampaigns(req: Request, res: Response) {
@CatchAsync
public async getTopCampaigns(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const limit = Math.min(parseInt(req.query.limit as string) || 10, 50);
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined;
+6 -3
View File
@@ -1,6 +1,6 @@
import {Controller, Get, Post} from '@overnightjs/core';
import {AuthenticationSchemas} from '@plunk/shared';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import {GITHUB_OAUTH_ENABLED, GOOGLE_OAUTH_ENABLED} from '../app/constants.js';
import {prisma} from '../database/prisma.js';
@@ -9,11 +9,13 @@ import {jwt} from '../middleware/auth.js';
import {AuthService} from '../services/AuthService.js';
import {UserService} from '../services/UserService.js';
import {Keys} from '../services/keys.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('auth')
export class Auth {
@Post('login')
public async login(req: Request, res: Response) {
@CatchAsync
public async login(req: Request, res: Response, next: NextFunction) {
const {email, password} = AuthenticationSchemas.login.parse(req.body);
const user = await UserService.email(email);
@@ -43,7 +45,8 @@ export class Auth {
}
@Post('signup')
public async signup(req: Request, res: Response) {
@CatchAsync
public async signup(req: Request, res: Response, next: NextFunction) {
const {email, password} = AuthenticationSchemas.login.parse(req.body);
const user = await UserService.email(email);
+22 -11
View File
@@ -1,7 +1,7 @@
import {Controller, Delete, Get, Middleware, Post, Put} from '@overnightjs/core';
import {CampaignAudienceType, CampaignStatus} from '@plunk/db';
import {CampaignSchemas} from '@plunk/shared';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import {HttpException} from '../exceptions/index.js';
import type {AuthResponse} from '../middleware/auth.js';
@@ -9,6 +9,7 @@ import {requireAuth} from '../middleware/auth.js';
import {CampaignService} from '../services/CampaignService.js';
import {DomainService} from '../services/DomainService.js';
import {type SegmentFilter} from '../services/SegmentService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('campaigns')
export class Campaigns {
@@ -18,7 +19,8 @@ export class Campaigns {
*/
@Post('')
@Middleware([requireAuth])
private async create(req: Request, res: Response) {
@CatchAsync
private async create(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceFilter, segmentId} =
CampaignSchemas.create.parse(req.body);
@@ -60,7 +62,8 @@ export class Campaigns {
*/
@Get('')
@Middleware([requireAuth])
private async list(req: Request, res: Response) {
@CatchAsync
private async list(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const status = req.query.status as CampaignStatus | undefined;
const page = parseInt(req.query.page as string) || 1;
@@ -92,7 +95,8 @@ export class Campaigns {
*/
@Get(':id')
@Middleware([requireAuth])
private async get(req: Request, res: Response) {
@CatchAsync
private async get(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -110,7 +114,8 @@ export class Campaigns {
*/
@Put(':id')
@Middleware([requireAuth])
private async update(req: Request, res: Response) {
@CatchAsync
private async update(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceFilter, segmentId} =
@@ -155,7 +160,8 @@ export class Campaigns {
*/
@Delete(':id')
@Middleware([requireAuth])
private async delete(req: Request, res: Response) {
@CatchAsync
private async delete(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -173,7 +179,8 @@ export class Campaigns {
*/
@Post(':id/duplicate')
@Middleware([requireAuth])
private async duplicate(req: Request, res: Response) {
@CatchAsync
private async duplicate(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -192,7 +199,8 @@ export class Campaigns {
*/
@Post(':id/send')
@Middleware([requireAuth])
private async send(req: Request, res: Response) {
@CatchAsync
private async send(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
const scheduledFor = req.body?.scheduledFor;
@@ -222,7 +230,8 @@ export class Campaigns {
*/
@Post(':id/cancel')
@Middleware([requireAuth])
private async cancel(req: Request, res: Response) {
@CatchAsync
private async cancel(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -241,7 +250,8 @@ export class Campaigns {
*/
@Get(':id/stats')
@Middleware([requireAuth])
private async stats(req: Request, res: Response) {
@CatchAsync
private async stats(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -259,7 +269,8 @@ export class Campaigns {
*/
@Post(':id/test')
@Middleware([requireAuth])
private async sendTest(req: Request, res: Response) {
@CatchAsync
private async sendTest(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
const {email} = CampaignSchemas.sendTest.parse(req.body);
+26 -13
View File
@@ -1,11 +1,12 @@
import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import multer from 'multer';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {ContactService} from '../services/ContactService.js';
import {QueueService} from '../services/QueueService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
// Configure multer for file uploads (memory storage)
const upload = multer({
@@ -31,7 +32,8 @@ export class Contacts {
*/
@Get('')
@Middleware([requireAuth])
public async list(req: Request, res: Response) {
@CatchAsync
public async list(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
const cursor = req.query.cursor as string | undefined;
@@ -48,7 +50,8 @@ export class Contacts {
*/
@Get('fields')
@Middleware([requireAuth])
public async getAvailableFields(req: Request, res: Response) {
@CatchAsync
public async getAvailableFields(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
try {
@@ -73,7 +76,8 @@ export class Contacts {
*/
@Get('fields/:field/values')
@Middleware([requireAuth])
public async getFieldValues(req: Request, res: Response) {
@CatchAsync
public async getFieldValues(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const field = req.params.field;
const limit = Math.min(parseInt(req.query.limit as string) || 100, 200);
@@ -105,7 +109,8 @@ export class Contacts {
*/
@Get(':id')
@Middleware([requireAuth])
public async get(req: Request, res: Response) {
@CatchAsync
public async get(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const contactId = req.params.id;
@@ -124,7 +129,8 @@ export class Contacts {
*/
@Post('')
@Middleware([requireAuth])
public async create(req: Request, res: Response) {
@CatchAsync
public async create(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {email, data, subscribed} = req.body;
@@ -153,7 +159,8 @@ export class Contacts {
*/
@Patch(':id')
@Middleware([requireAuth])
public async update(req: Request, res: Response) {
@CatchAsync
public async update(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const contactId = req.params.id;
const {email, data, subscribed} = req.body;
@@ -173,7 +180,8 @@ export class Contacts {
*/
@Delete(':id')
@Middleware([requireAuth])
public async delete(req: Request, res: Response) {
@CatchAsync
public async delete(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const contactId = req.params.id;
@@ -191,7 +199,8 @@ export class Contacts {
* PUBLIC: Get contact information (no auth required)
*/
@Get('public/:id')
public async getPublic(req: Request, res: Response) {
@CatchAsync
public async getPublic(req: Request, res: Response, next: NextFunction) {
const contactId = req.params.id;
if (!contactId) {
@@ -212,7 +221,8 @@ export class Contacts {
* PUBLIC: Subscribe a contact (no auth required)
*/
@Post('public/:id/subscribe')
public async subscribePublic(req: Request, res: Response) {
@CatchAsync
public async subscribePublic(req: Request, res: Response, next: NextFunction) {
const contactId = req.params.id;
if (!contactId) {
@@ -233,7 +243,8 @@ export class Contacts {
* PUBLIC: Unsubscribe a contact (no auth required)
*/
@Post('public/:id/unsubscribe')
public async unsubscribePublic(req: Request, res: Response) {
@CatchAsync
public async unsubscribePublic(req: Request, res: Response, next: NextFunction) {
const contactId = req.params.id;
if (!contactId) {
@@ -255,7 +266,8 @@ export class Contacts {
*/
@Post('import')
@Middleware([requireAuth, upload.single('file')])
public async importCsv(req: Request, res: Response) {
@CatchAsync
public async importCsv(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
if (!req.file) {
@@ -288,7 +300,8 @@ export class Contacts {
*/
@Get('import/:jobId')
@Middleware([requireAuth])
public async getImportStatus(req: Request, res: Response) {
@CatchAsync
public async getImportStatus(req: Request, res: Response, next: NextFunction) {
const jobId = req.params.jobId;
if (!jobId) {
+10 -5
View File
@@ -1,6 +1,6 @@
import {Controller, Delete, Get, Middleware, Post} from '@overnightjs/core';
import {DomainSchemas, UtilitySchemas} from '@plunk/shared';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import {redis} from '../database/redis.js';
import {NotFound} from '../exceptions/index.js';
@@ -9,6 +9,7 @@ import {isAuthenticated} from '../middleware/auth.js';
import {DomainService} from '../services/DomainService.js';
import {Keys} from '../services/keys.js';
import {prisma} from '../database/prisma.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('domains')
export class Domains {
@@ -17,7 +18,8 @@ export class Domains {
*/
@Get('project/:projectId')
@Middleware([isAuthenticated])
public async getProjectDomains(req: Request, res: Response) {
@CatchAsync
public async getProjectDomains(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {projectId} = DomainSchemas.projectId.parse(req.params);
@@ -43,7 +45,8 @@ export class Domains {
*/
@Post('')
@Middleware([isAuthenticated])
public async addDomain(req: Request, res: Response) {
@CatchAsync
public async addDomain(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {projectId, domain} = DomainSchemas.create.parse(req.body);
@@ -102,7 +105,8 @@ export class Domains {
*/
@Get(':id/verify')
@Middleware([isAuthenticated])
public async checkVerification(req: Request, res: Response) {
@CatchAsync
public async checkVerification(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = UtilitySchemas.id.parse(req.params);
@@ -138,7 +142,8 @@ export class Domains {
*/
@Delete(':id')
@Middleware([isAuthenticated])
public async removeDomain(req: Request, res: Response) {
@CatchAsync
public async removeDomain(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = UtilitySchemas.id.parse(req.params);
+12 -6
View File
@@ -1,9 +1,10 @@
import {Controller, Get, Middleware, Post} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {EventService} from '../services/EventService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('events')
export class Events {
@@ -13,7 +14,8 @@ export class Events {
*/
@Post('track')
@Middleware([requireAuth])
public async track(req: Request, res: Response) {
@CatchAsync
public async track(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {name, contactId, emailId, data} = req.body;
@@ -32,7 +34,8 @@ export class Events {
*/
@Get('')
@Middleware([requireAuth])
public async list(req: Request, res: Response) {
@CatchAsync
public async list(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const eventName = req.query.eventName as string | undefined;
const limit = parseInt(req.query.limit as string) || 100;
@@ -48,7 +51,8 @@ export class Events {
*/
@Get('stats')
@Middleware([requireAuth])
public async stats(req: Request, res: Response) {
@CatchAsync
public async stats(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined;
const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined;
@@ -64,7 +68,8 @@ export class Events {
*/
@Get('contact/:contactId')
@Middleware([requireAuth])
public async getContactEvents(req: Request, res: Response) {
@CatchAsync
public async getContactEvents(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const contactId = req.params.contactId;
const limit = parseInt(req.query.limit as string) || 50;
@@ -84,7 +89,8 @@ export class Events {
*/
@Get('names')
@Middleware([requireAuth])
public async getEventNames(req: Request, res: Response) {
@CatchAsync
public async getEventNames(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const eventNames = await EventService.getUniqueEventNames(auth.projectId!);
+4 -2
View File
@@ -1,5 +1,5 @@
import {Controller, Get} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import {
API_URI,
@@ -11,6 +11,7 @@ import {
import {prisma} from '../../database/prisma.js';
import {jwt} from '../../middleware/auth.js';
import {UserService} from '../../services/UserService.js';
import {CatchAsync} from '../../utils/asyncHandler.js';
@Controller('github')
export class Github {
@@ -31,7 +32,8 @@ export class Github {
}
@Get('callback')
public async callback(req: Request, res: Response) {
@CatchAsync
public async callback(req: Request, res: Response, next: NextFunction) {
if (!GITHUB_OAUTH_ENABLED) {
return res.status(404).json({error: 'GitHub OAuth is not configured'});
}
+4 -2
View File
@@ -1,5 +1,5 @@
import {Controller, Get} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import {
API_URI,
@@ -11,6 +11,7 @@ import {
import {prisma} from '../../database/prisma.js';
import {jwt} from '../../middleware/auth.js';
import {UserService} from '../../services/UserService.js';
import {CatchAsync} from '../../utils/asyncHandler.js';
@Controller('google')
export class Google {
@@ -26,7 +27,8 @@ export class Google {
}
@Get('callback')
public async callback(req: Request, res: Response) {
@CatchAsync
public async callback(req: Request, res: Response, next: NextFunction) {
if (!GOOGLE_OAUTH_ENABLED) {
return res.status(404).json({error: 'Google OAuth is not configured'});
}
+6 -3
View File
@@ -1,10 +1,11 @@
import {Controller, Get, Middleware} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import {prisma} from '../database/prisma.js';
import {HttpException} from '../exceptions/index.js';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('projects')
export class Projects {
@@ -14,7 +15,8 @@ export class Projects {
*/
@Get(':id/setup-state')
@Middleware([requireAuth])
private async getSetupState(req: Request, res: Response) {
@CatchAsync
private async getSetupState(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -86,7 +88,8 @@ export class Projects {
*/
@Get(':id/members')
@Middleware([requireAuth])
private async getMembers(req: Request, res: Response) {
@CatchAsync
private async getMembers(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
+18 -9
View File
@@ -1,9 +1,10 @@
import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {SegmentService} from '../services/SegmentService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('segments')
export class Segments {
@@ -13,7 +14,8 @@ export class Segments {
*/
@Get('')
@Middleware([requireAuth])
public async list(req: Request, res: Response) {
@CatchAsync
public async list(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const segments = await SegmentService.list(auth.projectId!);
@@ -27,7 +29,8 @@ export class Segments {
*/
@Get(':id')
@Middleware([requireAuth])
public async get(req: Request, res: Response) {
@CatchAsync
public async get(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id;
@@ -46,7 +49,8 @@ export class Segments {
*/
@Get(':id/contacts')
@Middleware([requireAuth])
public async getContacts(req: Request, res: Response) {
@CatchAsync
public async getContacts(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id;
const page = parseInt(req.query.page as string) || 1;
@@ -67,7 +71,8 @@ export class Segments {
*/
@Post('')
@Middleware([requireAuth])
public async create(req: Request, res: Response) {
@CatchAsync
public async create(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {name, description, filters, trackMembership} = req.body;
@@ -95,7 +100,8 @@ export class Segments {
*/
@Patch(':id')
@Middleware([requireAuth])
public async update(req: Request, res: Response) {
@CatchAsync
public async update(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id;
const {name, description, filters, trackMembership} = req.body;
@@ -124,7 +130,8 @@ export class Segments {
*/
@Delete(':id')
@Middleware([requireAuth])
public async delete(req: Request, res: Response) {
@CatchAsync
public async delete(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id;
@@ -143,7 +150,8 @@ export class Segments {
*/
@Post(':id/compute')
@Middleware([requireAuth])
public async compute(req: Request, res: Response) {
@CatchAsync
public async compute(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id;
@@ -162,7 +170,8 @@ export class Segments {
*/
@Post(':id/refresh')
@Middleware([requireAuth])
public async refresh(req: Request, res: Response) {
@CatchAsync
public async refresh(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id;
+16 -8
View File
@@ -1,11 +1,12 @@
import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/core';
import {TemplateType} from '@plunk/db';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {DomainService} from '../services/DomainService.js';
import {TemplateService} from '../services/TemplateService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('templates')
export class Templates {
@@ -15,7 +16,8 @@ export class Templates {
*/
@Get('')
@Middleware([requireAuth])
public async list(req: Request, res: Response) {
@CatchAsync
public async list(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const page = parseInt(req.query.page as string) || 1;
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
@@ -33,7 +35,8 @@ export class Templates {
*/
@Get(':id')
@Middleware([requireAuth])
public async get(req: Request, res: Response) {
@CatchAsync
public async get(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const templateId = req.params.id;
@@ -52,7 +55,8 @@ export class Templates {
*/
@Post('')
@Middleware([requireAuth])
public async create(req: Request, res: Response) {
@CatchAsync
public async create(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {name, description, subject, body, from, fromName, replyTo, type} = req.body;
@@ -95,7 +99,8 @@ export class Templates {
*/
@Patch(':id')
@Middleware([requireAuth])
public async update(req: Request, res: Response) {
@CatchAsync
public async update(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const templateId = req.params.id;
const {name, description, subject, body, from, fromName, replyTo, type} = req.body;
@@ -129,7 +134,8 @@ export class Templates {
*/
@Delete(':id')
@Middleware([requireAuth])
public async delete(req: Request, res: Response) {
@CatchAsync
public async delete(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const templateId = req.params.id;
@@ -148,7 +154,8 @@ export class Templates {
*/
@Post(':id/duplicate')
@Middleware([requireAuth])
public async duplicate(req: Request, res: Response) {
@CatchAsync
public async duplicate(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const templateId = req.params.id;
@@ -167,7 +174,8 @@ export class Templates {
*/
@Get(':id/usage')
@Middleware([requireAuth])
public async getUsage(req: Request, res: Response) {
@CatchAsync
public async getUsage(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const templateId = req.params.id;
+4 -2
View File
@@ -1,10 +1,11 @@
import {Controller, Middleware, Post} from '@overnightjs/core';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import multer from 'multer';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import * as S3Service from '../services/S3Service.js';
import {CatchAsync} from '../utils/asyncHandler.js';
// Configure multer for file uploads (memory storage)
const upload = multer({
@@ -32,7 +33,8 @@ export class Uploads {
*/
@Post('image')
@Middleware([requireAuth, upload.single('image')])
public async uploadImage(req: Request, res: Response) {
@CatchAsync
public async uploadImage(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
try {
+26 -13
View File
@@ -2,7 +2,7 @@ import {randomBytes} from 'node:crypto';
import {Controller, Get, Middleware, Patch, Post, Put} from '@overnightjs/core';
import {BillingLimitSchemas, ProjectSchemas} from '@plunk/shared';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import {
DASHBOARD_URI,
@@ -23,12 +23,14 @@ import {isAuthenticated} from '../middleware/auth.js';
import {BillingLimitService} from '../services/BillingLimitService.js';
import {SecurityService} from '../services/SecurityService.js';
import {UserService} from '../services/UserService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('users')
export class Users {
@Get('@me')
@Middleware([isAuthenticated])
public async me(req: Request, res: Response) {
@CatchAsync
public async me(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
if (!auth.userId) {
@@ -46,7 +48,8 @@ export class Users {
@Get('@me/projects')
@Middleware([isAuthenticated])
public async meProjects(req: Request, res: Response) {
@CatchAsync
public async meProjects(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
if (!auth.userId) {
@@ -60,7 +63,8 @@ export class Users {
@Post('@me/projects')
@Middleware([isAuthenticated])
public async createProject(req: Request, res: Response) {
@CatchAsync
public async createProject(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
if (!auth.userId) {
@@ -93,7 +97,8 @@ export class Users {
@Patch('@me/projects/:id')
@Middleware([isAuthenticated])
public async updateProject(req: Request, res: Response) {
@CatchAsync
public async updateProject(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
const data = ProjectSchemas.update.parse(req.body);
@@ -124,7 +129,8 @@ export class Users {
@Post('@me/projects/:id/regenerate-keys')
@Middleware([isAuthenticated])
public async regenerateProjectKeys(req: Request, res: Response) {
@CatchAsync
public async regenerateProjectKeys(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -161,7 +167,8 @@ export class Users {
@Post('@me/projects/:id/checkout')
@Middleware([isAuthenticated])
public async createCheckoutSession(req: Request, res: Response) {
@CatchAsync
public async createCheckoutSession(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -239,7 +246,8 @@ export class Users {
@Post('@me/projects/:id/billing-portal')
@Middleware([isAuthenticated])
public async createBillingPortalSession(req: Request, res: Response) {
@CatchAsync
public async createBillingPortalSession(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -288,7 +296,8 @@ export class Users {
@Get('@me/projects/:id/billing-limits')
@Middleware([isAuthenticated])
public async getBillingLimits(req: Request, res: Response) {
@CatchAsync
public async getBillingLimits(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -320,7 +329,8 @@ export class Users {
@Put('@me/projects/:id/billing-limits')
@Middleware([isAuthenticated])
public async updateBillingLimits(req: Request, res: Response) {
@CatchAsync
public async updateBillingLimits(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -386,7 +396,8 @@ export class Users {
@Get('@me/projects/:id/billing-consumption')
@Middleware([isAuthenticated])
public async getBillingConsumption(req: Request, res: Response) {
@CatchAsync
public async getBillingConsumption(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -485,7 +496,8 @@ export class Users {
@Get('@me/projects/:id/billing-invoices')
@Middleware([isAuthenticated])
public async getBillingInvoices(req: Request, res: Response) {
@CatchAsync
public async getBillingInvoices(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
@@ -572,7 +584,8 @@ export class Users {
@Get('@me/projects/:id/security')
@Middleware([isAuthenticated])
public async getSecurityHealth(req: Request, res: Response) {
@CatchAsync
public async getSecurityHealth(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
+6 -3
View File
@@ -1,7 +1,7 @@
import {Controller, Post} from '@overnightjs/core';
import type {Prisma} from '@plunk/db';
import {EmailStatus} from '@plunk/db';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import signale from 'signale';
import type Stripe from 'stripe';
@@ -9,6 +9,7 @@ import {STRIPE_ENABLED, STRIPE_WEBHOOK_SECRET} from '../app/constants.js';
import {stripe} from '../app/stripe.js';
import {prisma} from '../database/prisma.js';
import {SecurityService} from '../services/SecurityService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
/**
* Webhooks Controller
@@ -21,7 +22,8 @@ export class Webhooks {
* Handles email events: delivery, open, click, bounce, complaint
*/
@Post('sns')
public async receiveSNSWebhook(req: Request, res: Response) {
@CatchAsync
public async receiveSNSWebhook(req: Request, res: Response, next: NextFunction) {
try {
// Parse the SNS message body
const body = JSON.parse(req.body.Message);
@@ -147,7 +149,8 @@ export class Webhooks {
* Handles subscription and payment events: checkout.session.completed, invoice.paid, etc.
*/
@Post('incoming/stripe')
public async receiveStripeWebhook(req: Request, res: Response) {
@CatchAsync
public async receiveStripeWebhook(req: Request, res: Response, next: NextFunction) {
// Return 404 if billing is disabled
if (!STRIPE_ENABLED || !stripe) {
signale.warn('[WEBHOOK] Stripe webhook received but billing is disabled');
+30 -15
View File
@@ -1,10 +1,11 @@
import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/core';
import {WorkflowExecutionStatus} from '@plunk/db';
import type {Request, Response} from 'express';
import type {NextFunction, Request, Response} from 'express';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {WorkflowService} from '../services/WorkflowService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('workflows')
export class Workflows {
@@ -14,7 +15,8 @@ export class Workflows {
*/
@Get('')
@Middleware([requireAuth])
public async list(req: Request, res: Response) {
@CatchAsync
public async list(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const page = parseInt(req.query.page as string) || 1;
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
@@ -31,7 +33,8 @@ export class Workflows {
*/
@Get(':id')
@Middleware([requireAuth])
public async get(req: Request, res: Response) {
@CatchAsync
public async get(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
@@ -50,7 +53,8 @@ export class Workflows {
*/
@Post('')
@Middleware([requireAuth])
public async create(req: Request, res: Response) {
@CatchAsync
public async create(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {name, description, eventName, enabled, allowReentry} = req.body;
@@ -79,7 +83,8 @@ export class Workflows {
*/
@Patch(':id')
@Middleware([requireAuth])
public async update(req: Request, res: Response) {
@CatchAsync
public async update(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const {name, description, triggerType, triggerConfig, enabled, allowReentry} = req.body;
@@ -106,7 +111,8 @@ export class Workflows {
*/
@Delete(':id')
@Middleware([requireAuth])
public async delete(req: Request, res: Response) {
@CatchAsync
public async delete(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
@@ -125,7 +131,8 @@ export class Workflows {
*/
@Post(':id/steps')
@Middleware([requireAuth])
public async addStep(req: Request, res: Response) {
@CatchAsync
public async addStep(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const {type, name, position, config, templateId, autoConnect} = req.body;
@@ -156,7 +163,8 @@ export class Workflows {
*/
@Patch(':id/steps/:stepId')
@Middleware([requireAuth])
public async updateStep(req: Request, res: Response) {
@CatchAsync
public async updateStep(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const stepId = req.params.stepId;
@@ -182,7 +190,8 @@ export class Workflows {
*/
@Delete(':id/steps/:stepId')
@Middleware([requireAuth])
public async deleteStep(req: Request, res: Response) {
@CatchAsync
public async deleteStep(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const stepId = req.params.stepId;
@@ -202,7 +211,8 @@ export class Workflows {
*/
@Post(':id/transitions')
@Middleware([requireAuth])
public async createTransition(req: Request, res: Response) {
@CatchAsync
public async createTransition(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const {fromStepId, toStepId, condition, priority} = req.body;
@@ -231,7 +241,8 @@ export class Workflows {
*/
@Delete(':id/transitions/:transitionId')
@Middleware([requireAuth])
public async deleteTransition(req: Request, res: Response) {
@CatchAsync
public async deleteTransition(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const transitionId = req.params.transitionId;
@@ -251,7 +262,8 @@ export class Workflows {
*/
@Post(':id/executions')
@Middleware([requireAuth])
public async startExecution(req: Request, res: Response) {
@CatchAsync
public async startExecution(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const {contactId, context} = req.body;
@@ -275,7 +287,8 @@ export class Workflows {
*/
@Get(':id/executions')
@Middleware([requireAuth])
public async listExecutions(req: Request, res: Response) {
@CatchAsync
public async listExecutions(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const page = parseInt(req.query.page as string) || 1;
@@ -297,7 +310,8 @@ export class Workflows {
*/
@Get(':id/executions/:executionId')
@Middleware([requireAuth])
public async getExecution(req: Request, res: Response) {
@CatchAsync
public async getExecution(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const executionId = req.params.executionId;
@@ -317,7 +331,8 @@ export class Workflows {
*/
@Delete(':id/executions/:executionId')
@Middleware([requireAuth])
public async cancelExecution(req: Request, res: Response) {
@CatchAsync
public async cancelExecution(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
const executionId = req.params.executionId;