import { Inject, Injectable } from '@nestjs/common'; import { FastifyInstance, FastifyPluginOptions } from 'fastify'; import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; import { ModuleRef, repl } from '@nestjs/core'; import type { Config } from '@/config.js'; import type { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import endpoints, { IEndpoint } from './endpoints.js'; import { ApiCallService } from './ApiCallService.js'; import { SignupApiService } from './SignupApiService.js'; import { SigninApiService } from './SigninApiService.js'; import { GithubServerService } from './integration/GithubServerService.js'; import { DiscordServerService } from './integration/DiscordServerService.js'; import { TwitterServerService } from './integration/TwitterServerService.js'; @Injectable() export class ApiServerService { constructor( private moduleRef: ModuleRef, @Inject(DI.config) private config: Config, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, private userEntityService: UserEntityService, private apiCallService: ApiCallService, private signupApiServiceService: SignupApiService, private signinApiServiceService: SigninApiService, private githubServerService: GithubServerService, private discordServerService: DiscordServerService, private twitterServerService: TwitterServerService, ) { this.createServer = this.createServer.bind(this); } public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.register(cors, { origin: '*', }); fastify.register(multipart, { limits: { fileSize: this.config.maxFileSize ?? 262144000, files: 1, }, }); // Prevent cache fastify.addHook('onRequest', (request, reply, done) => { reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); done(); }); for (const endpoint of endpoints) { const ep = { name: endpoint.name, meta: endpoint.meta, params: endpoint.params, exec: this.moduleRef.get('ep:' + endpoint.name, { strict: false }).exec, }; if (endpoint.meta.requireFile) { fastify.all<{ Params: { endpoint: string; }, Body: Record, Querystring: Record, }>('/' + endpoint.name, (request, reply) => { if (request.method === 'GET' && !endpoint.meta.allowGet) { reply.code(405); return; } this.apiCallService.handleMultipartRequest(ep, request, reply); }); } else { fastify.all<{ Params: { endpoint: string; }, Body: Record, Querystring: Record, }>('/' + endpoint.name, (request, reply) => { if (request.method === 'GET' && !endpoint.meta.allowGet) { reply.code(405); return; } this.apiCallService.handleRequest(ep, request, reply); }); } } fastify.post<{ Body: { username: string; password: string; host?: string; invitationCode?: string; emailAddress?: string; 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; } }>('/signup', (request, reply) => this.signupApiServiceService.signup(request, reply)); fastify.post<{ Body: { username: string; password: string; token?: string; signature?: string; authenticatorData?: string; clientDataJSON?: string; credentialId?: string; challengeId?: string; }; }>('/signin', (request, reply) => this.signinApiServiceService.signin(request, reply)); fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiServiceService.signupPending(request, reply)); fastify.register(this.discordServerService.create); fastify.register(this.githubServerService.create); fastify.register(this.twitterServerService.create); fastify.get('/v1/instance/peers', async (request, reply) => { const instances = await this.instancesRepository.find({ select: ['host'], }); return instances.map(instance => instance.host); }); fastify.post<{ Params: { session: string; } }>('/miauth/:session/check', async (request, reply) => { const token = await this.accessTokensRepository.findOneBy({ session: request.params.session, }); if (token && token.session != null && !token.fetched) { this.accessTokensRepository.update(token.id, { fetched: true, }); return { ok: true, token: token.token, user: await this.userEntityService.pack(token.userId, null, { detail: true }), }; } else { return { ok: false, }; } }); done(); } }