mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-08 20:08:05 -05:00
* Apply OpenSats UI enhancements * Add register modal and some UI fixes * Add login modal * Add reset password button * Use @t3-oss/env-nextjs for env variables * Email verification without keycloak UI * Password reset without keycloak UI * Add "My donations" page with one-time stripe donations list * Display crypto donations in "My donations" page * Donation form fixes and improvements * Display recurring annual fiat donations * Include keycloak realm export file and remove hardcoded client values * fix: correctly handle btcpay webhooks and fix donationList query * feat: add membership modal and implement membership payment using btcpay * feat: add procedure for membership purchases with stripe and use db as single source of truth for donations * feat: use webhooks to update stripe donation/membership status * feat: memberships list page and fixes * feat: db schema changes and webhook fixes * feat: re-add "donate" and add "get annual membership" buttons to project page * feat: open register modal when clicking membership button while logged out * feat: replace "Get Membership" button with "My Memberships" button when user already has a membership for that project * feat: multiple funds support * deps: bump axios * feat: add different color schemes for each fund and some fixes * feat: add home page * feat: add missing titles and responsiveness improvements * chore: add prod workflow file and compose file * chore(deploy workflow): set environment name * chore(Dockerfile): add necessary lines for prisma * chore: make it skip env validation on build * fix: prevent donation amounts from being fetched from db during build * chore(nginx.conf): remove copy-paste junk * chore(docker compose): correctly set APP_URL env * feat: replace Sendgrid with SES * deps: audit fix * chore(deploy.yml): remove unecessary env * fix: correctly manage client and server env * fix(trpc.submitApplication): get recipient emails from server side env * fix(Dockerfile): define NEXT_PUBLIC_ env on build * chore(trpc): make it log any errors * chore(trpc): improve displaying of errors * chore(trpc): improve displaying of errors * fix: buggy link buttons * feat: use single btcpay store * feat: show form 8283 info in donation form and handle tax deductible donations * feat: have only one privacy and terms page for the entire site * feat: support many social links * feat: add funding required endpoint (wip) * feat: get rates using btcpay api and small refactor * deps: audit fix * fix: correctly handle payment methods on InvoiceSettled event * fix: make index on Donation.btcPayInvoiceId * feat(funding-required): improve asset parameter response * feat(funding-required): add project_status param * feat(funding-required): add fund param * feat(funding-required): implement caching * fix(funding-required): minor fixes * feat(funding-required): add remaining_amount_<currency> fields and fixes * chore: include all services in docker-compose.dev.yml and update .env.example * feat: move terms and privacy links to footer * fix: address font not always loading bug * feat: use fund logos as header image * feat: donation confirmation email * fix: use correct stripe client for each fund on webhooks * feat: add account settings page with change password form * feat: add email change form to settings page * fix: address wrong btcpay invoice url redirect * chore: email change request debug * fix(api): better handle user attributes * feat: ui improvements * feat: add btcpay invoice item description * chore(nginx): api rate limit * feat: remove typing component from fund landing pages * feat: implement refresh token rotation using keycloak * refactor: have gross and net amounts for donations * feat: invalidate user sessions on password/email change * fix: make "Create an account" button work on donate/membership modals * refactor: project props * fix(utils.md): correctly load md project attributes * chore(prisma): make composite unique constraint for fundSlug and projectSlug on ProjectAddresses * chore: mark example project as not funded * fix(utils.md): serialization error * chore(funding-required): btcpay invoice payment methods debug * fix(funding-required): get bitcoin address from correct payment method * fix(funding-required): correctly handle project_status ANY filter * fix(btcpay webhook handler): correctly handle payment methods on InvoicePaymentSettled * chore(docker-compose.yml): expose nginx port 80 * fix(funding-required): correctly concat project url * feat: ui improvements for smaller screens * fix(btcpay webhook handler): correctly get payment method amount on InvoiceSettled * fix(btcpay webhook handler): respond with 200 immediately if there is no metadata * chore(funding-required): debugging * chore(funding-required): debugging * chore(funding-required): debugging * fix(Dockerfile): define BUILD_MODE as arg instead of env to make it blank at runtime * fix: correctly pass current and goal values to project card progress * fix(funding-required): set high monitoring time for static address invoice * fix: correctly handle refresh token expiration on the ui * feat: ui improvements * chore: update README * chore: update README * Initial site text * fix: colors * chore: mention funds accordingly in texts * chore: update realm-export.json * chore: rename docker compose files * Update emails * Form updates * Remove unused pages and page improvements * feat: allow editing navbar links for each fund * Cleanup and Firo projects * chore(deploy.yml): change deploy branch to master * fix(auth): use fetch instead of axios when fetching refresh token due to edge runtime compatibility * fix: keep empty project folders * Fix code scanning alert no. 20: DOM text reinterpreted as HTML Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * chore: sanitize md file paths * Text and link updates --------- Co-authored-by: Artur N <arturnunespe@gmail.com> Co-authored-by: Justin Ehrenhofer <12520755+SamsungGalaxyPlayer@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
import { NextApiRequest, NextApiResponse } from 'next'
|
|
import getRawBody from 'raw-body'
|
|
import crypto from 'crypto'
|
|
import dayjs from 'dayjs'
|
|
|
|
import {
|
|
BtcPayGetRatesRes,
|
|
BtcPayGetPaymentMethodsRes,
|
|
DonationMetadata,
|
|
} from '../../../server/types'
|
|
import { btcpayApi as _btcpayApi, btcpayApi, prisma } from '../../../server/services'
|
|
import { env } from '../../../env.mjs'
|
|
import { sendDonationConfirmationEmail } from '../../../server/utils/mailing'
|
|
|
|
export const config = {
|
|
api: {
|
|
bodyParser: false,
|
|
},
|
|
}
|
|
|
|
type BtcpayBody = Record<string, any> & {
|
|
deliveryId: string
|
|
webhookId: string
|
|
originalDeliveryId: string
|
|
isRedelivery: boolean
|
|
type: string
|
|
timestamp: number
|
|
storeId: string
|
|
invoiceId: string
|
|
metadata?: DonationMetadata
|
|
paymentMethod: string
|
|
}
|
|
|
|
async function handleBtcpayWebhook(req: NextApiRequest, res: NextApiResponse) {
|
|
if (req.method !== 'POST') {
|
|
res.setHeader('Allow', ['POST'])
|
|
res.status(405).end(`Method ${req.method} Not Allowed`)
|
|
return
|
|
}
|
|
|
|
if (typeof req.headers['btcpay-sig'] !== 'string') {
|
|
res.status(400).json({ success: false })
|
|
return
|
|
}
|
|
|
|
const rawBody = await getRawBody(req)
|
|
const body: BtcpayBody = JSON.parse(Buffer.from(rawBody).toString('utf8'))
|
|
|
|
const expectedSigHash = crypto
|
|
.createHmac('sha256', env.BTCPAY_WEBHOOK_SECRET)
|
|
.update(rawBody)
|
|
.digest('hex')
|
|
|
|
const incomingSigHash = (req.headers['btcpay-sig'] as string).split('=')[1]
|
|
|
|
if (expectedSigHash !== incomingSigHash) {
|
|
console.error('Invalid signature')
|
|
res.status(400).json({ success: false })
|
|
return
|
|
}
|
|
|
|
if (!body.metadata) {
|
|
return res.status(200).json({ success: true })
|
|
}
|
|
|
|
if (body.type === 'InvoicePaymentSettled') {
|
|
// Handle payments to funding required API invoices ONLY
|
|
if (body.metadata.staticGeneratedForApi === 'false') {
|
|
return res.status(200).json({ success: true })
|
|
}
|
|
|
|
// Handle payment methods like "BTC-LightningNetwork" if added in the future
|
|
const cryptoCode = body.paymentMethod.includes('-')
|
|
? body.paymentMethod.split('-')[0]
|
|
: body.paymentMethod
|
|
|
|
const { data: rates } = await btcpayApi.get<BtcPayGetRatesRes>(
|
|
`/rates?currencyPair=${cryptoCode}_USD`
|
|
)
|
|
|
|
const cryptoRate = Number(rates[0].rate)
|
|
const cryptoAmount = Number(body.payment.value)
|
|
|
|
await prisma.donation.create({
|
|
data: {
|
|
userId: null,
|
|
btcPayInvoiceId: body.invoiceId,
|
|
projectName: body.metadata.projectName,
|
|
projectSlug: body.metadata.projectSlug,
|
|
fundSlug: body.metadata.fundSlug,
|
|
cryptoCode,
|
|
netCryptoAmount: cryptoAmount,
|
|
grossCryptoAmount: cryptoAmount,
|
|
netFiatAmount: Number((cryptoAmount * cryptoRate).toFixed(2)),
|
|
grossFiatAmount: Number((cryptoAmount * cryptoRate).toFixed(2)),
|
|
},
|
|
})
|
|
}
|
|
|
|
if (body.type === 'InvoiceSettled') {
|
|
// If this is a funding required API invoice, let InvoiceReceivedPayment handle it instead
|
|
if (body.metadata.staticGeneratedForApi === 'true') {
|
|
return res.status(200).json({ success: true })
|
|
}
|
|
|
|
const { data: paymentMethods } = await btcpayApi.get<BtcPayGetPaymentMethodsRes>(
|
|
`/invoices/${body.invoiceId}/payment-methods`
|
|
)
|
|
|
|
await Promise.all(
|
|
paymentMethods.map(async (paymentMethod) => {
|
|
if (!body.metadata) return
|
|
|
|
const cryptoAmount = Number(paymentMethod.paymentMethodPaid)
|
|
|
|
if (!cryptoAmount) return
|
|
|
|
const fiatAmount = Number(paymentMethod.paymentMethodPaid) * Number(paymentMethod.rate)
|
|
|
|
await prisma.donation.create({
|
|
data: {
|
|
userId: body.metadata.userId,
|
|
btcPayInvoiceId: body.invoiceId,
|
|
projectName: body.metadata.projectName,
|
|
projectSlug: body.metadata.projectSlug,
|
|
fundSlug: body.metadata.fundSlug,
|
|
cryptoCode: paymentMethod.cryptoCode,
|
|
netCryptoAmount: cryptoAmount,
|
|
grossCryptoAmount: cryptoAmount,
|
|
netFiatAmount: Number(fiatAmount.toFixed(2)),
|
|
grossFiatAmount: Number(fiatAmount.toFixed(2)),
|
|
membershipExpiresAt:
|
|
body.metadata.isMembership === 'true' ? dayjs().add(1, 'year').toDate() : null,
|
|
},
|
|
})
|
|
|
|
if (body.metadata.donorEmail && body.metadata.donorName) {
|
|
sendDonationConfirmationEmail({
|
|
to: body.metadata.donorEmail,
|
|
donorName: body.metadata.donorName,
|
|
fundSlug: body.metadata.fundSlug,
|
|
projectName: body.metadata.projectName,
|
|
isMembership: body.metadata.isMembership === 'true',
|
|
isSubscription: false,
|
|
pointsReceived: 0,
|
|
btcpayAsset: paymentMethod.cryptoCode as 'BTC' | 'XMR',
|
|
btcpayCryptoAmount: cryptoAmount,
|
|
})
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
res.status(200).json({ success: true })
|
|
}
|
|
|
|
export default handleBtcpayWebhook
|