Skip to content

Commit

Permalink
feat: start adding stripe integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Creaous committed Jan 13, 2025
1 parent c18f0eb commit c9bd13f
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ UPLOAD_LOCATION=

# The location of the config file.
CONFIG_FILE=config.json

# Stripe information.
STRIPE_SECRET_KEY=
Binary file modified bun.lockb
Binary file not shown.
9 changes: 9 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
15 changes: 15 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
Expand Down Expand Up @@ -43,3 +56,5 @@ export const config: Config = {
},
file
};

export const stripe = new Stripe(Bun.env.STRIPE_SECRET_KEY! || 'no_stripe_key');
2 changes: 2 additions & 0 deletions src/drizzle/schema/user/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
}
72 changes: 63 additions & 9 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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();

Check failure on line 280 in src/lib/server.ts

View workflow job for this annotation

GitHub Actions / Coverage

SyntaxError: Unexpected end of JSON input

at <anonymous> (/home/runner/work/nova/nova/src/lib/server.ts:280:27) at webhookEndpoint (/home/runner/work/nova/nova/src/lib/server.ts:182:32) at <anonymous> (/home/runner/work/nova/nova/src/lib/server.test.ts:46:25) at <anonymous> (/home/runner/work/nova/nova/src/lib/server.test.ts:44:72)

Check failure on line 280 in src/lib/server.ts

View workflow job for this annotation

GitHub Actions / Test

SyntaxError: Unexpected end of JSON input

at <anonymous> (/home/runner/work/nova/nova/src/lib/server.ts:280:27) at webhookEndpoint (/home/runner/work/nova/nova/src/lib/server.ts:182:32) at <anonymous> (/home/runner/work/nova/nova/src/lib/server.test.ts:46:25) at <anonymous> (/home/runner/work/nova/nova/src/lib/server.test.ts:44:72)
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(
{
Expand Down
21 changes: 20 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions src/types/user/conversation/Conversation.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,6 +13,15 @@ export const UserConversation =
builder.objectRef<UserConversationSchemaType>('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 }),
Expand Down

0 comments on commit c9bd13f

Please sign in to comment.