1+ import { renderEmailWithTheme } from "@/lib/email-themes" ;
12import { getEmailConfig , sendEmail } from "@/lib/emails" ;
23import { getNotificationCategoryByName , hasNotificationEnabled } from "@/lib/notification-categories" ;
4+ import { prismaClient } from "@/prisma-client" ;
35import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler" ;
4- import { adaptSchema , serverOrHigherAuthTypeSchema , yupNumber , yupObject , yupString } from "@stackframe/stack-shared/dist/schema-fields" ;
6+ import { adaptSchema , serverOrHigherAuthTypeSchema , yupArray , yupBoolean , yupNumber , yupObject , yupString } from "@stackframe/stack-shared/dist/schema-fields" ;
7+ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env" ;
58import { StatusError } from "@stackframe/stack-shared/dist/utils/errors" ;
6- import { getUser } from "../../users/crud" ;
79import { unsubscribeLinkVerificationCodeHandler } from "../unsubscribe-link/verification-handler" ;
8- import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env" ;
10+
11+ type UserResult = {
12+ user_id : string ,
13+ user_email ?: string ,
14+ success : boolean ,
15+ error ?: string ,
16+ } ;
917
1018export const POST = createSmartRouteHandler ( {
1119 metadata : {
@@ -17,7 +25,7 @@ export const POST = createSmartRouteHandler({
1725 tenancy : adaptSchema . defined ( ) ,
1826 } ) . defined ( ) ,
1927 body : yupObject ( {
20- user_id : yupString ( ) . defined ( ) ,
28+ user_ids : yupArray ( yupString ( ) . defined ( ) ) . defined ( ) ,
2129 html : yupString ( ) . defined ( ) ,
2230 subject : yupString ( ) . defined ( ) ,
2331 notification_category_name : yupString ( ) . defined ( ) ,
@@ -28,60 +36,108 @@ export const POST = createSmartRouteHandler({
2836 statusCode : yupNumber ( ) . oneOf ( [ 200 ] ) . defined ( ) ,
2937 bodyType : yupString ( ) . oneOf ( [ "json" ] ) . defined ( ) ,
3038 body : yupObject ( {
31- user_email : yupString ( ) . defined ( ) ,
39+ results : yupArray ( yupObject ( {
40+ user_id : yupString ( ) . defined ( ) ,
41+ user_email : yupString ( ) . optional ( ) ,
42+ success : yupBoolean ( ) . defined ( ) ,
43+ error : yupString ( ) . optional ( ) ,
44+ } ) ) . defined ( ) ,
3245 } ) . defined ( ) ,
3346 } ) ,
3447 handler : async ( { body, auth } ) => {
48+ if ( ! getEnvVariable ( "STACK_FREESTYLE_API_KEY" ) ) {
49+ throw new StatusError ( 500 , "STACK_FREESTYLE_API_KEY is not set" ) ;
50+ }
3551 if ( auth . tenancy . config . email_config . type === "shared" ) {
3652 throw new StatusError ( 400 , "Cannot send custom emails when using shared email config" ) ;
3753 }
38- const user = await getUser ( { userId : body . user_id , tenancyId : auth . tenancy . id } ) ;
39- if ( ! user ) {
40- throw new StatusError ( 404 , "User not found" ) ;
41- }
42- if ( ! user . primary_email ) {
43- throw new StatusError ( 400 , "User does not have a primary email" ) ;
44- }
54+ const emailConfig = await getEmailConfig ( auth . tenancy ) ;
4555 const notificationCategory = getNotificationCategoryByName ( body . notification_category_name ) ;
4656 if ( ! notificationCategory ) {
4757 throw new StatusError ( 404 , "Notification category not found" ) ;
4858 }
49- const isNotificationEnabled = await hasNotificationEnabled ( auth . tenancy . id , user . id , notificationCategory . id ) ;
50- if ( ! isNotificationEnabled ) {
51- throw new StatusError ( 400 , "User has disabled notifications for this category" ) ;
52- }
5359
54- let html = body . html ;
55- if ( notificationCategory . can_disable ) {
56- const { code } = await unsubscribeLinkVerificationCodeHandler . createCode ( {
57- tenancy : auth . tenancy ,
58- method : { } ,
59- data : {
60- user_id : user . id ,
61- notification_category_id : notificationCategory . id ,
60+ const users = await prismaClient . projectUser . findMany ( {
61+ where : {
62+ tenancyId : auth . tenancy . id ,
63+ projectUserId : {
64+ in : body . user_ids ,
6265 } ,
63- callbackUrl : undefined
64- } ) ;
65- const unsubscribeLink = new URL ( getEnvVariable ( "NEXT_PUBLIC_STACK_API_URL" ) ) ;
66- unsubscribeLink . pathname = "/api/v1/emails/unsubscribe-link" ;
67- unsubscribeLink . searchParams . set ( "code" , code ) ;
68- html += `<br /><a href="${ unsubscribeLink . toString ( ) } ">Click here to unsubscribe</a>` ;
66+ } ,
67+ include : {
68+ contactChannels : true ,
69+ } ,
70+ } ) ;
71+ const userMap = new Map ( users . map ( user => [ user . projectUserId , user ] ) ) ;
72+ const userSendErrors : Map < string , string > = new Map ( ) ;
73+ const userPrimaryEmails : Map < string , string > = new Map ( ) ;
74+
75+ for ( const userId of body . user_ids ) {
76+ const user = userMap . get ( userId ) ;
77+ if ( ! user ) {
78+ userSendErrors . set ( userId , "User not found" ) ;
79+ continue ;
80+ }
81+ const isNotificationEnabled = await hasNotificationEnabled ( auth . tenancy . id , user . projectUserId , notificationCategory . id ) ;
82+ if ( ! isNotificationEnabled ) {
83+ userSendErrors . set ( userId , "User has disabled notifications for this category" ) ;
84+ continue ;
85+ }
86+ const primaryEmail = user . contactChannels . find ( ( c ) => c . isPrimary === "TRUE" ) ?. value ;
87+ if ( ! primaryEmail ) {
88+ userSendErrors . set ( userId , "User does not have a primary email" ) ;
89+ continue ;
90+ }
91+ userPrimaryEmails . set ( userId , primaryEmail ) ;
92+
93+ let unsubscribeLink : string | null = null ;
94+ if ( notificationCategory . can_disable ) {
95+ const { code } = await unsubscribeLinkVerificationCodeHandler . createCode ( {
96+ tenancy : auth . tenancy ,
97+ method : { } ,
98+ data : {
99+ user_id : user . projectUserId ,
100+ notification_category_id : notificationCategory . id ,
101+ } ,
102+ callbackUrl : undefined
103+ } ) ;
104+ const unsubUrl = new URL ( getEnvVariable ( "NEXT_PUBLIC_STACK_API_URL" ) ) ;
105+ unsubUrl . pathname = "/api/v1/emails/unsubscribe-link" ;
106+ unsubUrl . searchParams . set ( "code" , code ) ;
107+ unsubscribeLink = unsubUrl . toString ( ) ;
108+ }
109+
110+ const renderedEmail = await renderEmailWithTheme ( body . html , auth . tenancy . config . email_theme , unsubscribeLink ) ;
111+ if ( "error" in renderedEmail ) {
112+ userSendErrors . set ( userId , "There was an error rendering the email" ) ;
113+ continue ;
114+ }
115+
116+ try {
117+ await sendEmail ( {
118+ tenancyId : auth . tenancy . id ,
119+ emailConfig,
120+ to : primaryEmail ,
121+ subject : body . subject ,
122+ html : renderedEmail . html ,
123+ text : renderedEmail . text ,
124+ } ) ;
125+ } catch {
126+ userSendErrors . set ( userId , "Failed to send email" ) ;
127+ }
69128 }
70129
71- await sendEmail ( {
72- tenancyId : auth . tenancy . id ,
73- emailConfig : await getEmailConfig ( auth . tenancy ) ,
74- to : user . primary_email ,
75- subject : body . subject ,
76- html,
77- } ) ;
130+ const results : UserResult [ ] = body . user_ids . map ( ( userId ) => ( {
131+ user_id : userId ,
132+ user_email : userPrimaryEmails . get ( userId ) ,
133+ success : ! userSendErrors . has ( userId ) ,
134+ error : userSendErrors . get ( userId ) ,
135+ } ) ) ;
78136
79137 return {
80138 statusCode : 200 ,
81139 bodyType : 'json' ,
82- body : {
83- user_email : user . primary_email ,
84- } ,
140+ body : { results } ,
85141 } ;
86142 } ,
87143} ) ;
0 commit comments