upd: move stripe webhook into its own service

This commit is contained in:
Marie 2024-08-23 14:11:39 +02:00
parent 03039d110a
commit 0627f84e30
No known key found for this signature in database
GPG Key ID: 56569BBE47D2C828
11 changed files with 183 additions and 117 deletions

View File

@ -223,7 +223,9 @@ checkActivityPubGetSignature: false
# maxFileSize: 262144000 # maxFileSize: 262144000
# enable stripe identity for ID verification # enable stripe identity for ID verification
#stripeverify: true # stripeVerify: true
# stripeKey: sk_
# stripeHookKey: whsec_
# Upload or download file size limits (bytes) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View File

@ -298,8 +298,9 @@ checkActivityPubGetSignature: false
# maxFileSize: 262144000 # maxFileSize: 262144000
# enable stripe identity for ID verification # enable stripe identity for ID verification
#stripeverify: true # stripeVerify: true
#stripekey: productionkey # stripeKey: sk_
# stripeHookKey: whsec_
# Upload or download file size limits (bytes) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View File

@ -313,8 +313,9 @@ checkActivityPubGetSignature: false
# maxFileSize: 262144000 # maxFileSize: 262144000
# enable stripe identity for ID verification # enable stripe identity for ID verification
#stripeverify: true # stripeVerify: true
#stripekey: productionkey # stripeKey: sk_
# stripeHookKey: whsec_
# PID File of master process # PID File of master process
#pidFile: /tmp/misskey.pid #pidFile: /tmp/misskey.pid

View File

@ -108,8 +108,9 @@ type Source = {
maxFileSize: number; maxFileSize: number;
}; };
stripeverify?: boolean; stripeVerify?: boolean;
stripekey?: string; stripeKey?: string;
stripeHookKey?: string;
pidFile: string; pidFile: string;
}; };
@ -200,8 +201,9 @@ export type Config = {
maxFileSize: number; maxFileSize: number;
} | undefined; } | undefined;
stripeverify: boolean | undefined; stripeVerify: boolean | undefined;
stripekey: string; stripeKey: string;
stripeHookKey: string;
pidFile: string; pidFile: string;
}; };
@ -325,8 +327,9 @@ export function loadConfig(): Config {
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
import: config.import, import: config.import,
stripeverify: config.stripeverify ?? false, stripeVerify: config.stripeVerify ?? false,
stripekey: config.stripekey ?? '', stripeKey: config.stripeKey ?? '',
stripeHookKey: config.stripeHookKey ?? '',
pidFile: config.pidFile, pidFile: config.pidFile,
}; };
} }

View File

@ -49,7 +49,7 @@ import { MastodonApiServerService } from './api/mastodon/MastodonApiServerServic
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
import { StripeHookApiService } from './api/StripeHookApiService.js'; import { StripeHookServerService } from './StripeHookServerService.js';
@Module({ @Module({
imports: [ imports: [
@ -99,7 +99,7 @@ import { StripeHookApiService } from './api/StripeHookApiService.js';
MastodonApiServerService, MastodonApiServerService,
OAuth2ProviderService, OAuth2ProviderService,
MastoConverters, MastoConverters,
StripeHookApiService, StripeHookServerService,
], ],
exports: [ exports: [
ServerService, ServerService,

View File

@ -32,6 +32,7 @@ import { HealthServerService } from './HealthServerService.js';
import { ClientServerService } from './web/ClientServerService.js'; import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js'; import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js';
import { StripeHookServerService } from './StripeHookServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
const _dirname = fileURLToPath(new URL('.', import.meta.url)); const _dirname = fileURLToPath(new URL('.', import.meta.url));
@ -66,6 +67,7 @@ export class ServerService implements OnApplicationShutdown {
private fileServerService: FileServerService, private fileServerService: FileServerService,
private healthServerService: HealthServerService, private healthServerService: HealthServerService,
private clientServerService: ClientServerService, private clientServerService: ClientServerService,
private stripeHookServerService: StripeHookServerService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private loggerService: LoggerService, private loggerService: LoggerService,
private oauth2ProviderService: OAuth2ProviderService, private oauth2ProviderService: OAuth2ProviderService,
@ -109,6 +111,8 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.mastodonApiServerService.createServer, { prefix: '/api' }); fastify.register(this.mastodonApiServerService.createServer, { prefix: '/api' });
fastify.register(this.fileServerService.createServer); fastify.register(this.fileServerService.createServer);
fastify.register(this.activityPubServerService.createServer); fastify.register(this.activityPubServerService.createServer);
// only enable stripe webhook if verification is enabled
if (this.config.stripeVerify) fastify.register(this.stripeHookServerService.createServer, { prefix: '/stripe' });
fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer); fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' }); fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });

View File

@ -0,0 +1,150 @@
/*
* SPDX-FileCopyrightText: marie and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type {
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiLocalUser } from '@/models/User.js';
import { bindThis } from '@/decorators.js';
import cors from '@fastify/cors';
import secureJson from 'secure-json-parse';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import Stripe from 'stripe';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
@Injectable()
export class StripeHookServerService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private loggerService: LoggerService,
) {
//this.createServer = this.createServer.bind(this);
this.logger = this.loggerService.getLogger('stripe', 'gray');
}
@bindThis
private async stripehook(
request: FastifyRequest,
reply: FastifyReply,
) {
const stripe = new Stripe(this.config.stripeKey);
if (request.rawBody == null) {
// Bad request
reply.code(400);
return;
}
const body = request.rawBody;
const headers = request.headers;
function error(status: number, error: { id: string }) {
reply.code(status);
return { error };
}
let event;
// Verify the event came from Stripe
try {
const sig = headers['stripe-signature']!;
event = stripe.webhooks.constructEvent(body, sig, this.config.stripeHookKey);
} catch (err: any) {
// On error, log and return the error message
console.log(`❌ Error message: ${err.message}`);
reply.code(400)
return `Webhook Error: ${err.message}`;
}
// Successfully constructed event
switch (event.type) {
case 'identity.verification_session.verified': {
// All the verification checks passed
const verificationSession = event.data.object;
const user = await this.usersRepository.findOneBy({
id: verificationSession.metadata.user_id,
host: IsNull(),
}) as MiLocalUser;
if (user == null) {
return error(404, {
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
});
}
if (user.isSuspended) {
return error(403, {
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
});
}
this.logger.succ(`${user.username} has succesfully approved their ID via Session ${user.idSession}`);
await this.usersRepository.update(user.id, { idCheckRequired: false, idVerified: true });
break;
}
}
reply.code(200)
return { received: true };
// never get here
}
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const almostDefaultJsonParser: FastifyBodyParser<Buffer> = function (request, rawBody, done) {
if (rawBody.length === 0) {
const err = new Error('Body cannot be empty!') as any;
err.statusCode = 400;
return done(err);
}
try {
const json = secureJson.parse(rawBody.toString('utf8'), null, {
protoAction: 'ignore',
constructorAction: 'ignore',
});
done(null, json);
} catch (err: any) {
err.statusCode = 400;
return done(err);
}
};
fastify.register(cors, {
origin: '*',
});
fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, almostDefaultJsonParser);
fastify.addHook('onRequest', (request, reply, done) => {
reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
done();
});
fastify.post<{
Body: any,
Headers: any,
}>('/hook', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.stripehook(request, reply));
done();
}
}

View File

@ -17,7 +17,6 @@ import endpoints from './endpoints.js';
import { ApiCallService } from './ApiCallService.js'; import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.js'; import { SignupApiService } from './SignupApiService.js';
import { SigninApiService } from './SigninApiService.js'; import { SigninApiService } from './SigninApiService.js';
import { StripeHookApiService } from './StripeHookApiService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@Injectable() @Injectable()
@ -38,7 +37,6 @@ export class ApiServerService {
private apiCallService: ApiCallService, private apiCallService: ApiCallService,
private signupApiService: SignupApiService, private signupApiService: SignupApiService,
private signinApiService: SigninApiService, private signinApiService: SigninApiService,
private stripeHookApiService: StripeHookApiService,
) { ) {
//this.createServer = this.createServer.bind(this); //this.createServer = this.createServer.bind(this);
} }
@ -133,13 +131,6 @@ export class ApiServerService {
}; };
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply)); }>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
if (this.config.stripeverify) {
fastify.post<{
Headers: any;
Body: any;
}>('/stripe/hook', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, (request, reply) => this.stripeHookApiService.stripehook(request, reply));
}
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply)); fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
fastify.get('/v1/instance/peers', async (request, reply) => { fastify.get('/v1/instance/peers', async (request, reply) => {

View File

@ -1,94 +0,0 @@
/*
* SPDX-FileCopyrightText: marie and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type {
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiLocalUser } from '@/models/User.js';
import { bindThis } from '@/decorators.js';
import type { FastifyReply, FastifyRequest } from 'fastify';
import Stripe from 'stripe';
@Injectable()
export class StripeHookApiService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
) {
}
@bindThis
public async stripehook(
request: FastifyRequest,
reply: FastifyReply,
) {
const stripe = new Stripe(this.config.stripekey);
if (!request.rawBody) return reply.code(400).send('error');
const body: any = request.rawBody
const headers: any = request.headers;
function error(status: number, error: { id: string }) {
reply.code(status);
return { error };
}
let event;
console.log(body);
// Verify the event came from Stripe
try {
const sig = headers['stripe-signature'];
event = stripe.webhooks.constructEvent(body, sig, 'webhooksecretherewillbereplacedlaterwithconfig');
} catch (err: any) {
// On error, log and return the error message
console.log(`❌ Error message: ${err.message}`);
reply.code(400)
return `Webhook Error: ${err.message}`;
}
// Successfully constructed event
switch (event.type) {
case 'identity.verification_session.verified': {
// All the verification checks passed
const verificationSession = event.data.object;
const user = await this.usersRepository.findOneBy({
id: verificationSession.metadata.user_id,
host: IsNull(),
}) as MiLocalUser;
if (user == null) {
return error(404, {
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
});
}
if (user.isSuspended) {
return error(403, {
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
});
}
await this.usersRepository.update(user.id, { idCheckRequired: false, idVerified: true });
break;
}
}
reply.code(200)
return { received: true };
// never get here
}
}

View File

@ -29,6 +29,11 @@ export const meta = {
id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a', id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a',
kind: 'permission', kind: 'permission',
}, },
stripeIsDisabled: {
message: 'Stripe is disabled.',
code: 'STRIPE_IS_DISABLED',
id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1b',
},
}, },
} as const; } as const;
@ -48,13 +53,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private config: Config, private config: Config,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
if (!this.config.stripeVerify) throw new ApiError(meta.errors.stripeIsDisabled);
const userProfile = await this.usersRepository.findOne({ const userProfile = await this.usersRepository.findOne({
where: { where: {
id: me.id, id: me.id,
} }
}); });
const stripe = new Stripe(config.stripekey); const stripe = new Stripe(this.config.stripeKey);
if (userProfile == null) { if (userProfile == null) {
throw new ApiError(meta.errors.userIsDeleted); throw new ApiError(meta.errors.userIsDeleted);

View File

@ -63,6 +63,7 @@ const devConfig: UserConfig = {
'/bios': httpUrl, '/bios': httpUrl,
'/cli': httpUrl, '/cli': httpUrl,
'/inbox': httpUrl, '/inbox': httpUrl,
'/stripe': httpUrl,
'/emoji/': httpUrl, '/emoji/': httpUrl,
'/notes': { '/notes': {
target: httpUrl, target: httpUrl,