From c9bd13f1481c62bd6c0ac562aff464217eca3704 Mon Sep 17 00:00:00 2001 From: Creaous <68230846+Creaous@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:22:08 +1030 Subject: [PATCH] feat: start adding stripe integration --- .env.example | 3 + bun.lockb | Bin 169647 -> 177148 bytes config.json | 9 +++ package.json | 3 +- src/config.ts | 15 ++++ src/drizzle/schema/user/User.ts | 2 + src/lib/logger.ts | 5 +- src/lib/server.ts | 72 +++++++++++++++++--- src/server.ts | 21 +++++- src/types/user/conversation/Conversation.ts | 10 +++ 10 files changed, 128 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 585a1f4..75d23de 100644 --- a/.env.example +++ b/.env.example @@ -34,3 +34,6 @@ UPLOAD_LOCATION= # The location of the config file. CONFIG_FILE=config.json + +# Stripe information. +STRIPE_SECRET_KEY= \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 49a4ac9a38099a6849f62601ef3e0445e1ac875b..b6d8ed45031105bd362dd858b6c1c4ef3d868620 100755 GIT binary patch delta 38135 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 pl09E8tys^OZ4_e*IX$nLy?nQxatugo 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 NtHjn)!>p@){tq_k0M!5h diff --git a/config.json b/config.json index 9dee426..037d6a5 100644 --- a/config.json +++ b/config.json @@ -8,6 +8,15 @@ "enabled": false }, "messaging": { + "direct": { + "enabled": true + }, + "group": { + "enabled": true + } + }, + "subscriptions": { + "_comment": "Subscriptions allow you to monetise your Nova instance.", "enabled": true } } diff --git a/package.json b/package.json index 5244bc3..beec986 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "GPL-3.0-only", "main": "server.ts", "scripts": { - "dev": "bun --watch src/server.ts", + "dev": "NODE_ENV=dev bun --watch src/server.ts", "start": "bun src/server.ts", "db:generate": "drizzle-kit generate", "db:migrate": "bun drizzle/migrate.ts ./drizzle", @@ -35,6 +35,7 @@ "mime-types": "^2.1.35", "pg": "^8.13.1", "redis": "^4.7.0", + "stripe": "^17.5.0", "zod": "^3.24.1" }, "devDependencies": { diff --git a/src/config.ts b/src/config.ts index 6a76e77..31e5753 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,7 @@ import { OIDC, OIDCPluginOptionsBase } from '@nexirift/plugin-oidc'; import { readFileSync } from 'fs'; import { tokenClient } from './redis'; +import Stripe from 'stripe'; const file = (Bun.env.CONFIG_FILE as string) ?? 'config.json'; @@ -13,6 +14,18 @@ type Config = { age_verification: { enabled: boolean; }; + messaging: { + direct: { + enabled: boolean; + }; + group: { + enabled: boolean; + }; + }; + subscription: { + enabled: boolean; + tiers: string[]; + }; }; openid: OIDCPluginOptionsBase; file: string; @@ -43,3 +56,5 @@ export const config: Config = { }, file }; + +export const stripe = new Stripe(Bun.env.STRIPE_SECRET_KEY! || 'no_stripe_key'); diff --git a/src/drizzle/schema/user/User.ts b/src/drizzle/schema/user/User.ts index bc8e4ed..f0ce7a3 100644 --- a/src/drizzle/schema/user/User.ts +++ b/src/drizzle/schema/user/User.ts @@ -33,6 +33,8 @@ export const user = pgTable('user', { profession: citext('profession'), location: citext('location'), website: citext('website'), + stripe_customer_id: citext('stripe_customer_id'), + stripe_subscription_id: citext('stripe_subscription_id'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at') .notNull() diff --git a/src/lib/logger.ts b/src/lib/logger.ts index accd373..ac0c71e 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -3,9 +3,12 @@ import debug from 'debug'; export const log = debug('app:log'); export const guardianLog = debug('lib:guardian'); export const authentikLog = debug('lib:authentik'); +export const stripeLog = debug('lib:stripe'); export const error = debug('app:error'); // Enables all debug namespaces export function enableAll() { - return debug.enable('app:log,lib:guardian,lib:authentik,app:error'); + return debug.enable( + 'app:log,lib:guardian,lib:authentik,lib:stripe,app:error' + ); } diff --git a/src/lib/server.ts b/src/lib/server.ts index 5515526..13e306b 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -6,12 +6,14 @@ import { S3Client } from '@aws-sdk/client-s3'; import { Upload } from '@aws-sdk/lib-storage'; import { mockClient } from 'aws-sdk-client-mock'; import mime from 'mime-types'; -import { config } from '../config'; +import { config, stripe } from '../config'; import { db } from '../drizzle/db'; import { postMedia, user } from '../drizzle/schema'; import { syncClient, tokenClient } from '../redis'; import { authorize } from './authentication'; import { convertModelToUser, getHashedPk, internalUsers } from './authentik'; +import { enableAll, stripeLog } from './logger'; +import { eq } from 'drizzle-orm'; /** * "Legacy" endpoint for uploading media. @@ -179,7 +181,6 @@ async function mediaUploadEndpoint(req: Request) { */ async function webhookEndpoint(req: Request) { const url = new URL(req.url); - console.log(url.pathname, isTestMode); switch (url.pathname) { case `/webhook/${isTestMode ? 'TEST-AUTH' : Bun.env.WEBHOOK_AUTH}`: if ( @@ -274,13 +275,66 @@ async function webhookEndpoint(req: Request) { { status: 404 } ); case `/webhook/${isTestMode ? 'TEST-STRIPE' : Bun.env.WEBHOOK_STRIPE}`: - return Response.json( - { - status: 'WORK_IN_PROGRESS', - message: 'This webhook is not implemented yet' - }, - { status: 404 } - ); + enableAll(); + + const body = await req.json(); + switch (body.type) { + case 'customer.subscription.created': + console.log(body.type, body); + + const customer = await stripe.customers.retrieve( + body.data.object.customer + ); + + const customerId = + 'metadata' in customer + ? (customer.metadata['id'] as string) + : ''; + + const customerEmail = + 'email' in customer ? (customer.email as string) : ''; + + const userRecord = await db.query.user.findFirst({ + where: (user, { eq, or }) => + or( + eq( + user.stripe_customer_id, + body.data.object.customer + ), + eq(user.id, customerId), + eq(user.email, customerEmail) + ) + }); + + if (!userRecord) { + stripeLog( + `customer ${customer.id} has a subscription, but no user was found, bailing out...` + ); + return Response.json({}, { status: 200 }); + } + + stripeLog( + `customer ${customer.id} associated with user ${userRecord.id} has been charged, setting subscription to ${body.id}...` + ); + + await db + .update(user) + .set({ + stripe_customer_id: customer.id, + stripe_subscription_id: body.id + }) + .where(eq(user.id, userRecord.id)) + .execute(); + + return Response.json({}, { status: 200 }); + case 'charge.refunded': + console.log('๐Ÿ’ณ User has been refunded, downgrading...'); + // TODO: Update subscription status for user. + return Response.json({}, { status: 200 }); + default: + //console.log('๐Ÿ’ณ Unknown webhook type'); + return Response.json({}, { status: 200 }); + } default: return Response.json( { diff --git a/src/server.ts b/src/server.ts index 134d66a..a07bb32 100755 --- a/src/server.ts +++ b/src/server.ts @@ -6,7 +6,7 @@ import gradient from 'gradient-string'; import { handleProtocols, makeHandler } from 'graphql-ws/lib/use/bun'; import { createYoga } from 'graphql-yoga'; import { version } from '../package.json'; -import { config } from './config'; +import { config, stripe } from './config'; import { Context } from './context'; import { db, prodDbClient } from './drizzle/db'; import getGitCommitHash from './git'; @@ -179,6 +179,25 @@ export async function startServer() { console.log('๐Ÿงช Running in test mode'); } console.log('\x1b[0m'); + + if (Bun.env.STRIPE_SECRET_KEY) { + if ( + server.hostname === 'localhost' || + server.hostname === '127.0.0.1' + ) { + console.log( + `๐Ÿ’ณ Stripe requires CLI for webhooks due to the hostname being ${server.hostname}.` + ); + } else { + const webhookEndpoint = await stripe.webhookEndpoints.create({ + enabled_events: ['charge.succeeded', 'charge.failed'], + url: new URL( + `/webhook/${Bun.env.WEBHOOK_STRIPE}`, + `http://${server.hostname}:${server.port}` + ).toString() + }); + } + } } if (!isTestMode) { diff --git a/src/types/user/conversation/Conversation.ts b/src/types/user/conversation/Conversation.ts index 0b5f59d..f13c3cf 100644 --- a/src/types/user/conversation/Conversation.ts +++ b/src/types/user/conversation/Conversation.ts @@ -1,4 +1,5 @@ import { builder } from '../../../builder'; +import { config } from '../../../config'; import { db } from '../../../drizzle/db'; import { type UserConversationSchemaType } from '../../../drizzle/schema'; import { UserConversationParticipant } from './Participant'; @@ -12,6 +13,15 @@ export const UserConversation = builder.objectRef('UserConversation'); UserConversation.implement({ + authScopes: (t) => { + if (t.type === 'DIRECT' && !config.features.messaging.direct.enabled) { + return false; + } + if (t.type === 'GROUP' && !config.features.messaging.group.enabled) { + return false; + } + return true; + }, fields: (t) => ({ id: t.exposeString('id', { nullable: false }), name: t.exposeString('name', { nullable: true }),