upd: move stripe webhook into its own service
This commit is contained in:
parent
03039d110a
commit
0627f84e30
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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' });
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue