Browse Source

Task/improve Stripe checkout session verification (#6872)

* Improve verification of stripeCheckoutSessionId

* Update changelog
pull/6919/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
8e3778cc66
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 2
      apps/api/src/app/subscription/subscription.controller.ts
  3. 32
      apps/api/src/app/subscription/subscription.service.ts
  4. 5
      prisma/migrations/20260516074856_added_stripe_checkout_session_to_subscription/migration.sql
  5. 1
      prisma/schema.prisma

1
CHANGELOG.md

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Randomized the placeholder in the assistant
- Filtered out sectors with zero weight for ETF and mutual fund assets in the _Yahoo Finance_ data enhancer
- Enabled the _Bull Dashboard_ in the admin control panel without requiring an environment variable (experimental)
- Improved the verification of the _Stripe_ checkout session when creating a subscription
- Relaxed the URL validation in the asset profile DTOs to accept both `HTTP` and `HTTPS` protocols
- Relaxed the URL validation in the platform DTOs to accept both `HTTP` and `HTTPS` protocols
- Extracted the page tabs to a reusable component

2
apps/api/src/app/subscription/subscription.controller.ts

@ -100,10 +100,12 @@ export class SubscriptionController {
request.query.checkoutSessionId as string
);
if (userId) {
Logger.log(
`Subscription for user '${userId}' has been created via Stripe`,
'SubscriptionController'
);
}
response.redirect(
`${this.configurationService.get(

32
apps/api/src/app/subscription/subscription.service.ts

@ -17,7 +17,7 @@ import {
} from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { Subscription } from '@prisma/client';
import { Prisma, Subscription } from '@prisma/client';
import { addMilliseconds, isBefore } from 'date-fns';
import ms, { StringValue } from 'ms';
import Stripe from 'stripe';
@ -108,11 +108,13 @@ export class SubscriptionService {
duration = '1 year',
durationExtension,
price,
stripeCheckoutSessionId,
userId
}: {
duration?: StringValue;
durationExtension?: StringValue;
price: number;
stripeCheckoutSessionId?: string;
userId: string;
}) {
let expiresAt = addMilliseconds(new Date(), ms(duration));
@ -125,6 +127,7 @@ export class SubscriptionService {
data: {
expiresAt,
price,
stripeCheckoutSessionId,
user: {
connect: {
id: userId
@ -136,24 +139,41 @@ export class SubscriptionService {
public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
try {
let durationExtension: StringValue;
const session =
await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId);
if (session.payment_status !== 'paid' || session.status !== 'complete') {
throw new Error(
`Stripe Checkout Session '${aCheckoutSessionId}' has not been paid (status=${session.status}, payment_status=${session.payment_status})`
);
}
const subscriptionOffer: SubscriptionOffer = JSON.parse(
session.metadata.subscriptionOffer ?? '{}'
);
if (subscriptionOffer) {
durationExtension = subscriptionOffer.durationExtension;
}
const durationExtension = subscriptionOffer?.durationExtension;
try {
await this.createSubscription({
durationExtension,
price: session.amount_total / 100,
stripeCheckoutSessionId: session.id,
userId: session.client_reference_id
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
Logger.log(
`Stripe Checkout Session '${session.id}' has already been redeemed`,
'SubscriptionService'
);
} else {
throw error;
}
}
return session.client_reference_id;
} catch (error) {

5
prisma/migrations/20260516074856_added_stripe_checkout_session_to_subscription/migration.sql

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Subscription" ADD COLUMN "stripeCheckoutSessionId" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_stripeCheckoutSessionId_key" ON "Subscription"("stripeCheckoutSessionId");

1
prisma/schema.prisma

@ -240,6 +240,7 @@ model Subscription {
expiresAt DateTime
id String @id @default(uuid())
price Float?
stripeCheckoutSessionId String? @unique
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], onDelete: Cascade, references: [id])
userId String

Loading…
Cancel
Save