Compare commits

..

1 Commits

Author SHA1 Message Date
Mythie
19e960f593 fix: improve stripe webhook endpoint
Improve the stripe webhook endpoint by checking for
subscriptions prior to performing an update to handle
cases where accounts have no created subscription.

This can happen in sitations such as when a checkout_session has been created but the payment fails.
2023-05-31 21:11:54 +10:00
8 changed files with 136 additions and 236 deletions

View File

@@ -4,8 +4,8 @@
# Option 3: Use the provided dx setup (RECOMMENDED)
# => postgres://documenso:password@127.0.0.1:54320/documenso
#
# ⚠ WARNING: The test database can be reset or taken offline at any point.
# ⚠ WARNING: Please be aware that nothing written to the test database is private.
# ⚠ WARNING: The test database can be resetted or taken offline at any point.
# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
DATABASE_URL=''
# URL
@@ -51,4 +51,4 @@ NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
#FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
NEXT_PUBLIC_ALLOW_SIGNUP=true
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=false
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=true

View File

@@ -1,3 +1,10 @@
<div align="center" style="margin-top: 12px; margin-bottom: 3332px;">
<p>
We are LIVE on Product Hunt. Come say hi..
</p>
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The&#0032;Open&#0032;Source&#0032;DocuSign&#0032;Alternative&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<br>
<p align="center" style="margin-top: 120px">
<a href="https://github.com/documenso/documenso.com">
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo">
@@ -11,7 +18,7 @@
<a href="https://documenso.com"><strong>Learn more »</strong></a>
<br />
<br />
<a href="https://documen.so/slack">Slack</a>
<a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w">Slack</a>
·
<a href="https://documenso.com">Website</a>
·
@@ -22,7 +29,7 @@
</p>
<p align="center">
<a href="https://documen.so/slack"><img src="https://img.shields.io/badge/Slack-documenso.slack.com-%234A154B" alt="Join Documenso on Slack"></a>
<a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w"><img src="https://img.shields.io/badge/Slack-documenso.slack.com-%234A154B" alt="Join Documenso on Slack"></a>
<a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
@@ -56,18 +63,13 @@
Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
## Recognition
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" alt="Documenso - The&#0032;open&#0032;source&#0032;DocuSign&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The&#0032;Open&#0032;Source&#0032;DocuSign&#0032;Alternative&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## Community and Next Steps 🎯
The current project goal is to <b>[release a production ready version](https://github.com/documenso/documenso/milestone/1)</b> for self-hosting as soon as possible. If you want to help making that happen you can:
- Check out the first source code release in this repository and test it
- Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions)
- Join the [Slack Channel](https://documen.so/slack) for any questions and getting to know to other community members
- Join the [Slack Channel](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w) for any questions and getting to know to other community members
- ⭐ the repository to help us raise awareness
- Spread the word on Twitter, that Documenso is working towards a more open signing tool
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
@@ -196,32 +198,3 @@ Want to create a production ready docker image? Follow these steps:
- Docker support
- One-Click-Deploy on Render.com Deploy
# Troubleshooting
## Support IPv6
In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command
For local docker run
```bash
docker run -it documenso:latest npm run start -- -H ::
```
For k8s or docker-compose
```yaml
containers:
- name: documenso
image: documenso:latest
imagePullPolicy: IfNotPresent
command:
- npm
args:
- run
- start
- --
- -H
- "::"
```

View File

@@ -30,7 +30,7 @@ export default function PDFEditor(props: any) {
movedField.positionY = position.y.toFixed(0);
createOrUpdateField(props.document, movedField);
// no instant redraw neccessary, position information for saving or later rerender is enough
// no instant redraw neccessary, postion information for saving or later rerender is enough
// setFields(newFields);
}

View File

@@ -6,62 +6,53 @@ import prisma from "@documenso/prisma";
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
try {
const user = await getUserFromToken(req, res);
const { id: documentId } = req.query;
const { resendTo: resendTo = [] } = req.body;
const user = await getUserFromToken(req, res);
const { id: documentId } = req.query;
const { resendTo: resendTo = [] } = req.body;
if (!user) {
return res.status(401).send("Unauthorized");
}
if (!user) return;
if (!documentId) {
return res.status(400).send("Missing parameter documentId.");
}
if (!documentId) {
res.status(400).send("Missing parameter documentId.");
return;
}
const document: PrismaDocument = await getDocument(+documentId, req, res);
const document: PrismaDocument = await getDocument(+documentId, req, res);
if (!document) {
res.status(404).end(`No document with id ${documentId} found.`);
}
if (!document) res.status(404).end(`No document with id ${documentId} found.`);
let recipientCondition: any = {
let recipientCondition: any = {
documentId: +documentId,
sendStatus: SendStatus.NOT_SENT,
};
if (resendTo.length) {
recipientCondition = {
documentId: +documentId,
sendStatus: SendStatus.NOT_SENT,
id: { in: resendTo },
};
}
if (resendTo.length) {
recipientCondition = {
documentId: +documentId,
id: { in: resendTo },
};
}
const recipients = await prisma.recipient.findMany({
where: {
...recipientCondition,
},
});
const recipients = await prisma.recipient.findMany({
where: {
...recipientCondition,
},
});
if (!recipients.length) {
return res.status(200).send(recipients.length);
}
let sentRequests = 0;
recipients.forEach(async (recipient) => {
await sendSigningRequest(recipient, document, user);
sentRequests++;
if (!recipients.length) return res.status(200).send(recipients.length);
let sentRequests = 0;
recipients.forEach(async (recipient) => {
await sendSigningRequest(recipient, document, user).catch((err) => {
console.log(err);
return res.status(502).end("Coud not send request for signing.");
});
sentRequests++;
if (sentRequests === recipients.length) {
return res.status(200).send(recipients.length);
}
return res.status(502).end("Coud not send request for signing.");
} catch (err) {
return res.status(502).end("Coud not send request for signing.");
}
});
}
export default defaultHandler({

View File

@@ -22,7 +22,7 @@ echo "Git SHA: $GIT_SHA"
docker build -f "$SCRIPT_DIR/Dockerfile" \
--progress=plain \
-t "documenso:latest" \
-t "documentso:latest" \
-t "documenso:$GIT_SHA" \
-t "documenso:$APP_VERSION" \
"$MONOREPO_ROOT"

150
package-lock.json generated
View File

@@ -3472,7 +3472,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -7486,40 +7485,27 @@
}
},
"node_modules/turbo": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.1.tgz",
"integrity": "sha512-wq0YeSv6P/eEDXOL42jkMUr+T4z34dM8mdHu5u6C6OOAq8JuLJ72F/v4EVR1JmY8icyTkFz10ICLV0haUUYhbQ==",
"version": "1.9.9",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.9.9.tgz",
"integrity": "sha512-+ZS66LOT7ahKHxh6XrIdcmf2Yk9mNpAbPEj4iF2cs0cAeaDU3xLVPZFF0HbSho89Uxwhx7b5HBgPbdcjQTwQkg==",
"dev": true,
"hasInstallScript": true,
"bin": {
"turbo": "bin/turbo"
},
"optionalDependencies": {
"turbo-darwin-64": "1.10.1",
"turbo-darwin-arm64": "1.10.1",
"turbo-linux-64": "1.10.1",
"turbo-linux-arm64": "1.10.1",
"turbo-windows-64": "1.10.1",
"turbo-windows-arm64": "1.10.1"
"turbo-darwin-64": "1.9.9",
"turbo-darwin-arm64": "1.9.9",
"turbo-linux-64": "1.9.9",
"turbo-linux-arm64": "1.9.9",
"turbo-windows-64": "1.9.9",
"turbo-windows-arm64": "1.9.9"
}
},
"node_modules/turbo-darwin-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.10.1.tgz",
"integrity": "sha512-isLLoPuAOMNsYovOq9BhuQOZWQuU13zYsW988KkkaA4OJqOn7qwa9V/KBYCJL8uVQqtG+/Y42J37lO8RJjyXuA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/turbo-darwin-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.1.tgz",
"integrity": "sha512-x1nloPR10fLElNCv17BKr0kCx/O5gse/UXAcVscMZH2tvRUtXrdBmut62uw2YU3J9hli2fszYjUWXkulVpQvFA==",
"version": "1.9.9",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.9.9.tgz",
"integrity": "sha512-VyfkXzTJpYLTAQ9krq2myyEq7RPObilpS04lgJ4OO1piq76RNmSpX9F/t9JCaY9Pj/4TL7i0d8PM7NGhwEA5Ag==",
"cpu": [
"arm64"
],
@@ -7529,58 +7515,6 @@
"darwin"
]
},
"node_modules/turbo-linux-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.10.1.tgz",
"integrity": "sha512-abV+ODCeOlz0503OZlHhPWdy3VwJZc1jObf1VQj7uQM+JqJ/kXbMyqJIMQVz+m7QJUFdferYPRxGhYT/NbYK7Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-linux-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.10.1.tgz",
"integrity": "sha512-zRC3nZbHQ63tofOmbuySzEn1ROISWTkemYYr1L98rpmT5aVa0kERlGiYcfDwZh3cBso/Ylg/wxexRAaPzcCJYQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-windows-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.10.1.tgz",
"integrity": "sha512-Irqz8IU+o7Q/5V44qatZBTunk+FQAOII1hZTsEU54ah62f9Y297K6/LSp+yncmVQOZlFVccXb6MDqcETExIQtA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/turbo-windows-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.10.1.tgz",
"integrity": "sha512-124IT15d2gyjC+NEn11pHOaVFvZDRHpxfF+LDUzV7YxfNIfV0mGkR3R/IyVXtQHOgqOdtQTbC4y411sm31+SEw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -10855,7 +10789,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true,
"peer": true
},
@@ -13703,58 +13636,23 @@
}
},
"turbo": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.1.tgz",
"integrity": "sha512-wq0YeSv6P/eEDXOL42jkMUr+T4z34dM8mdHu5u6C6OOAq8JuLJ72F/v4EVR1JmY8icyTkFz10ICLV0haUUYhbQ==",
"version": "1.9.9",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.9.9.tgz",
"integrity": "sha512-+ZS66LOT7ahKHxh6XrIdcmf2Yk9mNpAbPEj4iF2cs0cAeaDU3xLVPZFF0HbSho89Uxwhx7b5HBgPbdcjQTwQkg==",
"dev": true,
"requires": {
"turbo-darwin-64": "1.10.1",
"turbo-darwin-arm64": "1.10.1",
"turbo-linux-64": "1.10.1",
"turbo-linux-arm64": "1.10.1",
"turbo-windows-64": "1.10.1",
"turbo-windows-arm64": "1.10.1"
"turbo-darwin-64": "1.9.9",
"turbo-darwin-arm64": "1.9.9",
"turbo-linux-64": "1.9.9",
"turbo-linux-arm64": "1.9.9",
"turbo-windows-64": "1.9.9",
"turbo-windows-arm64": "1.9.9"
}
},
"turbo-darwin-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.10.1.tgz",
"integrity": "sha512-isLLoPuAOMNsYovOq9BhuQOZWQuU13zYsW988KkkaA4OJqOn7qwa9V/KBYCJL8uVQqtG+/Y42J37lO8RJjyXuA==",
"dev": true,
"optional": true
},
"turbo-darwin-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.1.tgz",
"integrity": "sha512-x1nloPR10fLElNCv17BKr0kCx/O5gse/UXAcVscMZH2tvRUtXrdBmut62uw2YU3J9hli2fszYjUWXkulVpQvFA==",
"dev": true,
"optional": true
},
"turbo-linux-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.10.1.tgz",
"integrity": "sha512-abV+ODCeOlz0503OZlHhPWdy3VwJZc1jObf1VQj7uQM+JqJ/kXbMyqJIMQVz+m7QJUFdferYPRxGhYT/NbYK7Q==",
"dev": true,
"optional": true
},
"turbo-linux-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.10.1.tgz",
"integrity": "sha512-zRC3nZbHQ63tofOmbuySzEn1ROISWTkemYYr1L98rpmT5aVa0kERlGiYcfDwZh3cBso/Ylg/wxexRAaPzcCJYQ==",
"dev": true,
"optional": true
},
"turbo-windows-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.10.1.tgz",
"integrity": "sha512-Irqz8IU+o7Q/5V44qatZBTunk+FQAOII1hZTsEU54ah62f9Y297K6/LSp+yncmVQOZlFVccXb6MDqcETExIQtA==",
"dev": true,
"optional": true
},
"turbo-windows-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.10.1.tgz",
"integrity": "sha512-124IT15d2gyjC+NEn11pHOaVFvZDRHpxfF+LDUzV7YxfNIfV0mGkR3R/IyVXtQHOgqOdtQTbC4y411sm31+SEw==",
"version": "1.9.9",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.9.9.tgz",
"integrity": "sha512-VyfkXzTJpYLTAQ9krq2myyEq7RPObilpS04lgJ4OO1piq76RNmSpX9F/t9JCaY9Pj/4TL7i0d8PM7NGhwEA5Ag==",
"dev": true,
"optional": true
},

View File

@@ -8,8 +8,9 @@
"db-migrate:dev": "prisma migrate dev",
"db-seed": "prisma db seed",
"db-studio": "prisma studio",
"docker:compose-up": "docker compose -f ./docker/compose-without-app.yml up -d || docker-compose -f ./docker/compose-without-app.yml up -d",
"docker:compose-down": "docker compose -f ./docker/compose-without-app.yml down || docker-compose -f ./docker/compose-without-app.yml down",
"docker:compose": "docker compose -f ./docker/compose-without-app.yml || docker-compose -f ./docker/compose-without-app.yml",
"docker:compose-up": "npm run docker:compose -- up -d",
"docker:compose-down": "npm run docker:compose -- down",
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook",
"dx": "npm install && run-s docker:compose-up db-migrate:dev",
"d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"

View File

@@ -25,9 +25,7 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
});
}
log("constructing body...")
const body = await buffer(req);
log("constructed body")
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
log("event-type:", event.type);
@@ -70,23 +68,38 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
if (event.type === "invoice.payment_succeeded") {
const invoice = event.data.object as Stripe.Invoice;
if (invoice.billing_reason !== "subscription_cycle") {
return res.status(200).json({
success: true,
message: "Webhook received",
});
}
const customerId =
typeof invoice.customer === "string" ? invoice.customer : invoice.customer?.id;
const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string);
await prisma.subscription.update({
const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
data: {
status: SubscriptionStatus.ACTIVE,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
},
});
if (hasSubscription) {
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.ACTIVE,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
return res.status(200).json({
success: true,
message: "Webhook received",
@@ -98,15 +111,23 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
const customerId = failedInvoice.customer as string;
await prisma.subscription.update({
const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
data: {
status: SubscriptionStatus.PAST_DUE,
},
});
if (hasSubscription) {
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.PAST_DUE,
},
});
}
return res.status(200).json({
success: true,
message: "Webhook received",
@@ -118,18 +139,26 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
const customerId = updatedSubscription.customer as string;
await prisma.subscription.update({
const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
data: {
status: SubscriptionStatus.ACTIVE,
planId: updatedSubscription.id,
priceId: updatedSubscription.items.data[0].price.id,
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
},
});
if (hasSubscription) {
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.ACTIVE,
planId: updatedSubscription.id,
priceId: updatedSubscription.items.data[0].price.id,
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
},
});
}
return res.status(200).json({
success: true,
message: "Webhook received",
@@ -141,15 +170,23 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
const customerId = deletedSubscription.customer as string;
await prisma.subscription.update({
const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
data: {
status: SubscriptionStatus.INACTIVE,
},
});
if (hasSubscription) {
await prisma.subscription.update({
where: {
customerId,
},
data: {
status: SubscriptionStatus.INACTIVE,
},
});
}
return res.status(200).json({
success: true,
message: "Webhook received",