@@ -0,0 +1,288 @@
/**
* MIM4U API — VMID 7811
* Routes: health, events, Stripe checkout/intent/webhook, assistance, contact, donations log
*/
import express from 'express'
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import nodemailer from 'nodemailer'
import Stripe from 'stripe'
const PORT = Number ( process . env . PORT || 3001 )
const DATA _DIR = process . env . MIM _DATA _DIR || path . join ( process . cwd ( ) , 'data' )
const PUBLIC _URL = ( process . env . MIM _PUBLIC _URL || 'https://mim4u.org' ) . replace ( /\/$/ , '' )
const NOTIFY _EMAIL = process . env . MIM _NOTIFY _EMAIL || '[email protected] '
const CORS _ORIGINS = ( process . env . MIM _CORS _ORIGINS || 'https://mim4u.org,https://www.mim4u.org' )
. split ( ',' )
. map ( ( s ) => s . trim ( ) )
. filter ( Boolean )
const stripeKey = process . env . STRIPE _SECRET _KEY
const stripe = stripeKey ? new Stripe ( stripeKey ) : null
fs . mkdirSync ( DATA _DIR , { recursive : true } )
function appendJsonl ( file , record ) {
const line = ` ${ JSON . stringify ( { ... record , ts : new Date ( ) . toISOString ( ) } )} \n `
fs . appendFileSync ( path . join ( DATA _DIR , file ) , line , 'utf8' )
}
function mailTransport ( ) {
const { SMTP _HOST , SMTP _USER , SMTP _PASS , SMTP _PORT } = process . env
if ( ! SMTP _HOST ) return null
return nodemailer . createTransport ( {
host : SMTP _HOST ,
port : Number ( SMTP _PORT || 587 ) ,
secure : Number ( SMTP _PORT ) === 465 ,
auth : SMTP _USER ? { user : SMTP _USER , pass : SMTP _PASS } : undefined ,
} )
}
async function notify ( subject , text , { to } = { } ) {
appendJsonl ( 'notifications.jsonl' , { subject , text , to } )
const transport = mailTransport ( )
if ( ! transport ) {
console . log ( ` [notify] ${ subject } : ${ text . slice ( 0 , 200 ) } ` )
return
}
await transport . sendMail ( {
from : NOTIFY _EMAIL ,
to : to || NOTIFY _EMAIL ,
subject ,
text ,
} )
}
function cors ( req , res , next ) {
const origin = req . headers . origin
if ( origin && ( CORS _ORIGINS . includes ( origin ) || CORS _ORIGINS . includes ( '*' ) ) ) {
res . setHeader ( 'Access-Control-Allow-Origin' , origin )
}
res . setHeader ( 'Access-Control-Allow-Methods' , 'GET,POST,OPTIONS' )
res . setHeader ( 'Access-Control-Allow-Headers' , 'Content-Type, Stripe-Signature' )
if ( req . method === 'OPTIONS' ) return res . sendStatus ( 204 )
next ( )
}
function requireStripe ( _req , res , next ) {
if ( ! stripe ) {
return res . status ( 503 ) . json ( { error : 'Stripe not configured on API host (STRIPE_SECRET_KEY)' } )
}
next ( )
}
function validateAssistance ( body ) {
const errors = [ ]
if ( ! body ? . requestType ) errors . push ( 'requestType required' )
if ( ! body ? . studentInfo ? . firstName ? . trim ( ) ) errors . push ( 'studentInfo.firstName required' )
if ( ! body ? . studentInfo ? . lastName ? . trim ( ) ) errors . push ( 'studentInfo.lastName required' )
if ( ! body ? . studentInfo ? . school ? . trim ( ) ) errors . push ( 'studentInfo.school required' )
if ( ! body ? . contactInfo ? . parentName ? . trim ( ) ) errors . push ( 'contactInfo.parentName required' )
if ( ! body ? . contactInfo ? . phone ? . trim ( ) ) errors . push ( 'contactInfo.phone required' )
if ( ! body ? . contactInfo ? . email ? . trim ( ) ) errors . push ( 'contactInfo.email required' )
if ( ! body ? . contactInfo ? . relationship ? . trim ( ) ) errors . push ( 'contactInfo.relationship required' )
if ( ! body ? . details ? . trim ( ) ) errors . push ( 'details required' )
if ( body ? . website ) errors . push ( 'spam detected' )
return errors
}
const app = express ( )
app . disable ( 'x-powered-by' )
app . use ( cors )
app . get ( '/health' , ( _req , res ) => {
res . type ( 'text/plain' ) . send ( 'ok' )
} )
app . get ( '/api/health' , ( _req , res ) => {
res . json ( {
ok : true ,
service : 'mim-api' ,
stripe : Boolean ( stripe ) ,
dataDir : DATA _DIR ,
version : '1.0.0' ,
} )
} )
app . use ( '/api/events' , express . json ( { limit : '32kb' } ) )
app . post ( '/api/events' , ( req , res ) => {
appendJsonl ( 'events.jsonl' , req . body || { } )
res . status ( 204 ) . end ( )
} )
app . use ( '/api/assistance-requests' , express . json ( { limit : '128kb' } ) )
const postAssistanceRequest = async ( req , res ) => {
const errors = validateAssistance ( req . body )
if ( errors . length ) return res . status ( 400 ) . json ( { error : 'Validation failed' , fields : errors } )
const id = ` asst_ ${ crypto . randomUUID ( ) } `
const record = { id , ... req . body , ip : req . ip , userAgent : req . headers [ 'user-agent' ] }
appendJsonl ( 'assistance-requests.jsonl' , record )
try {
await notify (
` [MIM4U] Assistance request ${ id } ` ,
` New assistance request ( ${ req . body . requestType } ) \n Contact: ${ req . body . contactInfo . parentName } < ${ req . body . contactInfo . email } > \n Phone: ${ req . body . contactInfo . phone } \n Details: ${ req . body . details ? . slice ( 0 , 500 ) } ` ,
)
} catch ( e ) {
console . error ( 'assistance notify failed' , e )
}
res . status ( 201 ) . json ( { ok : true , id , message : 'Request received. We will contact you within 24– 48 hours.' } )
}
app . post ( '/api/assistance-requests' , postAssistanceRequest )
app . post ( '/api/assistance' , postAssistanceRequest )
app . use ( '/api/contact' , express . json ( { limit : '64kb' } ) )
app . post ( '/api/contact' , async ( req , res ) => {
const { type , name , email , message , website , ... rest } = req . body || { }
if ( website ) return res . status ( 400 ) . json ( { error : 'Invalid submission' } )
if ( ! type ) return res . status ( 400 ) . json ( { error : 'type required' } )
if ( ! email ? . trim ( ) && type !== 'story' ) return res . status ( 400 ) . json ( { error : 'email required' } )
if ( type === 'story' && ! message ? . trim ( ) && ! rest . story ? . trim ( ) ) {
return res . status ( 400 ) . json ( { error : 'story text required' } )
}
const id = ` contact_ ${ crypto . randomUUID ( ) } `
appendJsonl ( 'contact.jsonl' , { id , type , name , email , message , ... rest } )
try {
await notify ( ` [MIM4U] ${ type } form ${ id } ` , JSON . stringify ( { name , email , message , ... rest } , null , 2 ) )
} catch ( e ) {
console . error ( 'contact notify failed' , e )
}
res . status ( 201 ) . json ( { ok : true , id , message : 'Thank you — we will be in touch soon.' } )
} )
app . use ( '/api/donations' , express . json ( { limit : '32kb' } ) )
app . post ( '/api/donations' , ( req , res ) => {
appendJsonl ( 'donations-offline.jsonl' , req . body || { } )
res . status ( 201 ) . json ( { ok : true } )
} )
app . use ( '/api/create-payment-intent' , express . json ( { limit : '32kb' } ) )
app . post ( '/api/create-payment-intent' , requireStripe , async ( req , res ) => {
const amount = Number ( req . body ? . amount )
if ( ! Number . isFinite ( amount ) || amount < 100 ) {
return res . status ( 400 ) . json ( { error : 'amount must be at least 100 cents' } )
}
try {
const intent = await stripe . paymentIntents . create ( {
amount : Math . round ( amount ) ,
currency : 'usd' ,
metadata : {
recurring : String ( Boolean ( req . body ? . recurring ) ) ,
donorEmail : req . body ? . customer ? . email || '' ,
donorName : req . body ? . customer ? . name || '' ,
} ,
receipt _email : req . body ? . customer ? . email || undefined ,
} )
res . json ( { client _secret : intent . client _secret } )
} catch ( e ) {
console . error ( 'payment intent error' , e )
res . status ( 502 ) . json ( { error : e . message || 'Stripe error' } )
}
} )
app . use ( '/api/create-checkout-session' , express . json ( { limit : '32kb' } ) )
app . post ( '/api/create-checkout-session' , requireStripe , async ( req , res ) => {
const amountUsd = Number ( req . body ? . amount )
if ( ! Number . isFinite ( amountUsd ) || amountUsd < 1 ) {
return res . status ( 400 ) . json ( { error : 'amount must be at least $1' } )
}
const cents = Math . round ( amountUsd * 100 )
const recurring = Boolean ( req . body ? . recurring )
const email = req . body ? . email ? . trim ( )
const name = req . body ? . name ? . trim ( )
try {
const session = await stripe . checkout . sessions . create ( {
mode : recurring ? 'subscription' : 'payment' ,
success _url : ` ${ PUBLIC _URL } /#/donate?status=success&session_id={CHECKOUT_SESSION_ID} ` ,
cancel _url : ` ${ PUBLIC _URL } /#/donate?status=cancelled ` ,
customer _email : email || undefined ,
line _items : [
recurring
? {
price _data : {
currency : 'usd' ,
product _data : { name : 'Monthly donation — Miracles in Motion Foundation' } ,
unit _amount : cents ,
recurring : { interval : 'month' } ,
} ,
quantity : 1 ,
}
: {
price _data : {
currency : 'usd' ,
product _data : { name : 'Donation — Miracles in Motion Foundation' } ,
unit _amount : cents ,
} ,
quantity : 1 ,
} ,
] ,
metadata : {
donorName : name || '' ,
anonymous : String ( Boolean ( req . body ? . anonymous ) ) ,
} ,
} )
res . json ( { url : session . url , sessionId : session . id } )
} catch ( e ) {
console . error ( 'checkout session error' , e )
res . status ( 502 ) . json ( { error : e . message || 'Stripe error' } )
}
} )
app . post (
'/api/webhooks/stripe' ,
express . raw ( { type : 'application/json' } ) ,
async ( req , res ) => {
if ( ! stripe ) return res . status ( 503 ) . send ( 'Stripe not configured' )
const sig = req . headers [ 'stripe-signature' ]
const secret = process . env . STRIPE _WEBHOOK _SECRET
let event
try {
event = secret
? stripe . webhooks . constructEvent ( req . body , sig , secret )
: JSON . parse ( req . body . toString ( ) )
} catch ( e ) {
console . error ( 'webhook verify failed' , e . message )
return res . status ( 400 ) . send ( ` Webhook Error: ${ e . message } ` )
}
if ( event . type === 'checkout.session.completed' ) {
const session = event . data . object
appendJsonl ( 'donations.jsonl' , {
sessionId : session . id ,
amountTotal : session . amount _total ,
customerEmail : session . customer _details ? . email ,
metadata : session . metadata ,
} )
const donorEmail = session . customer _details ? . email
const amountUsd = ( ( session . amount _total || 0 ) / 100 ) . toFixed ( 2 )
try {
await notify (
` [MIM4U] Donation received ${ session . id } ` ,
` Amount: ${ amountUsd } USD \n Email: ${ donorEmail || 'n/a' } ` ,
)
if ( donorEmail ) {
await notify (
'Thank you for your gift — Miracles in Motion Foundation' ,
` Dear friend, \n \n Thank you for your generous donation of $ ${ amountUsd } to Miracles in Motion Foundation. \n \n Your gift supports outreach, emergency assistance, and compassionate care in Los Angeles County. \n \n EIN ${ process . env . MIM _EIN || '33-4887159' } \n https://mim4u.org \n \n With gratitude, \n Miracles in Motion Foundation ` ,
{ to : donorEmail } ,
)
}
} catch ( e ) {
console . error ( 'donation notify failed' , e )
}
}
res . json ( { received : true } )
} ,
)
app . use ( ( _req , res ) => res . status ( 404 ) . json ( { error : 'Not found' } ) )
app . listen ( PORT , '0.0.0.0' , ( ) => {
console . log ( ` mim-api listening on : ${ PORT } (stripe= ${ Boolean ( stripe ) } ) data= ${ DATA _DIR } ` )
} )