diff --git a/.config/ci.yml b/.config/ci.yml index da4672cb33..fcf15a293d 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -223,7 +223,9 @@ checkActivityPubGetSignature: false # maxFileSize: 262144000 # enable stripe identity for ID verification -#stripeverify: true +# stripeVerify: true +# stripeKey: sk_ +# stripeHookKey: whsec_ # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/docker_example.yml b/.config/docker_example.yml index d0e372a9eb..03bfa1b7de 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -298,8 +298,9 @@ checkActivityPubGetSignature: false # maxFileSize: 262144000 # enable stripe identity for ID verification -#stripeverify: true -#stripekey: productionkey +# stripeVerify: true +# stripeKey: sk_ +# stripeHookKey: whsec_ # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/example.yml b/.config/example.yml index 692bd30d99..c92191e4e2 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -313,8 +313,9 @@ checkActivityPubGetSignature: false # maxFileSize: 262144000 # enable stripe identity for ID verification -#stripeverify: true -#stripekey: productionkey +# stripeVerify: true +# stripeKey: sk_ +# stripeHookKey: whsec_ # PID File of master process #pidFile: /tmp/misskey.pid diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 33486da0f2..cebd46ea85 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -108,8 +108,9 @@ type Source = { maxFileSize: number; }; - stripeverify?: boolean; - stripekey?: string; + stripeVerify?: boolean; + stripeKey?: string; + stripeHookKey?: string; pidFile: string; }; @@ -200,8 +201,9 @@ export type Config = { maxFileSize: number; } | undefined; - stripeverify: boolean | undefined; - stripekey: string; + stripeVerify: boolean | undefined; + stripeKey: string; + stripeHookKey: string; pidFile: string; }; @@ -325,8 +327,9 @@ export function loadConfig(): Config { perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), import: config.import, - stripeverify: config.stripeverify ?? false, - stripekey: config.stripekey ?? '', + stripeVerify: config.stripeVerify ?? false, + stripeKey: config.stripeKey ?? '', + stripeHookKey: config.stripeHookKey ?? '', pidFile: config.pidFile, }; } diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 7ca09628e0..95675dc30f 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -49,7 +49,7 @@ import { MastodonApiServerService } from './api/mastodon/MastodonApiServerServic import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; -import { StripeHookApiService } from './api/StripeHookApiService.js'; +import { StripeHookServerService } from './StripeHookServerService.js'; @Module({ imports: [ @@ -99,7 +99,7 @@ import { StripeHookApiService } from './api/StripeHookApiService.js'; MastodonApiServerService, OAuth2ProviderService, MastoConverters, - StripeHookApiService, + StripeHookServerService, ], exports: [ ServerService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 30c133d9ec..2ab2e7152e 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -32,6 +32,7 @@ import { HealthServerService } from './HealthServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js'; +import { StripeHookServerService } from './StripeHookServerService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; const _dirname = fileURLToPath(new URL('.', import.meta.url)); @@ -66,6 +67,7 @@ export class ServerService implements OnApplicationShutdown { private fileServerService: FileServerService, private healthServerService: HealthServerService, private clientServerService: ClientServerService, + private stripeHookServerService: StripeHookServerService, private globalEventService: GlobalEventService, private loggerService: LoggerService, private oauth2ProviderService: OAuth2ProviderService, @@ -109,6 +111,8 @@ export class ServerService implements OnApplicationShutdown { fastify.register(this.mastodonApiServerService.createServer, { prefix: '/api' }); fastify.register(this.fileServerService.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.wellKnownServerService.createServer); fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' }); diff --git a/packages/backend/src/server/StripeHookServerService.ts b/packages/backend/src/server/StripeHookServerService.ts new file mode 100644 index 0000000000..93bb664850 --- /dev/null +++ b/packages/backend/src/server/StripeHookServerService.ts @@ -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 = 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(); + } +} \ No newline at end of file diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 9346c93d8b..4a5935f930 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -17,7 +17,6 @@ import endpoints from './endpoints.js'; import { ApiCallService } from './ApiCallService.js'; import { SignupApiService } from './SignupApiService.js'; import { SigninApiService } from './SigninApiService.js'; -import { StripeHookApiService } from './StripeHookApiService.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; @Injectable() @@ -38,7 +37,6 @@ export class ApiServerService { private apiCallService: ApiCallService, private signupApiService: SignupApiService, private signinApiService: SigninApiService, - private stripeHookApiService: StripeHookApiService, ) { //this.createServer = this.createServer.bind(this); } @@ -133,13 +131,6 @@ export class ApiServerService { }; }>('/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.get('/v1/instance/peers', async (request, reply) => { diff --git a/packages/backend/src/server/api/StripeHookApiService.ts b/packages/backend/src/server/api/StripeHookApiService.ts deleted file mode 100644 index b6ac76ee1b..0000000000 --- a/packages/backend/src/server/api/StripeHookApiService.ts +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/packages/backend/src/server/api/endpoints/stripe/create-verify-session.ts b/packages/backend/src/server/api/endpoints/stripe/create-verify-session.ts index 51afb27b0b..edb3a532ea 100644 --- a/packages/backend/src/server/api/endpoints/stripe/create-verify-session.ts +++ b/packages/backend/src/server/api/endpoints/stripe/create-verify-session.ts @@ -29,6 +29,11 @@ export const meta = { id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a', kind: 'permission', }, + stripeIsDisabled: { + message: 'Stripe is disabled.', + code: 'STRIPE_IS_DISABLED', + id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1b', + }, }, } as const; @@ -48,13 +53,15 @@ export default class extends Endpoint { // eslint- private config: Config, ) { super(meta, paramDef, async (ps, me) => { + if (!this.config.stripeVerify) throw new ApiError(meta.errors.stripeIsDisabled); + const userProfile = await this.usersRepository.findOne({ where: { id: me.id, } }); - const stripe = new Stripe(config.stripekey); + const stripe = new Stripe(this.config.stripeKey); if (userProfile == null) { throw new ApiError(meta.errors.userIsDeleted); diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts index 07cf3b4a69..6fdd7daf72 100644 --- a/packages/frontend/vite.config.local-dev.ts +++ b/packages/frontend/vite.config.local-dev.ts @@ -63,6 +63,7 @@ const devConfig: UserConfig = { '/bios': httpUrl, '/cli': httpUrl, '/inbox': httpUrl, + '/stripe': httpUrl, '/emoji/': httpUrl, '/notes': { target: httpUrl,