Fastify (#9106)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix * Update SignupApiService.ts * wip * wip * Update ClientServerService.ts * wip * wip * wip * Update WellKnownServerService.ts * wip * wip * update des * wip * Update ApiServerService.ts * wip * update deps * Update WellKnownServerService.ts * wip * update deps * Update ApiCallService.ts * Update ApiCallService.ts * Update ApiServerService.ts
This commit is contained in:
@@ -1,19 +1,25 @@
|
||||
import { performance } from 'perf_hooks';
|
||||
import { pipeline } from 'node:stream';
|
||||
import * as fs from 'node:fs';
|
||||
import { promisify } from 'node:util';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { CacheableLocalUser, User } from '@/models/entities/User.js';
|
||||
import type { CacheableLocalUser, ILocalUser, User } from '@/models/entities/User.js';
|
||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { UserIpsRepository } from '@/models/index.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { ApiError } from './error.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import type { IEndpointMeta, IEndpoint } from './endpoints.js';
|
||||
import type Koa from 'koa';
|
||||
|
||||
const pump = promisify(pipeline);
|
||||
|
||||
const accessDenied = {
|
||||
message: 'Access denied.',
|
||||
@@ -44,92 +50,149 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}, 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
public handleRequest(endpoint: IEndpoint, exec: any, ctx: Koa.Context) {
|
||||
return new Promise<void>((res) => {
|
||||
const body = ctx.is('multipart/form-data')
|
||||
? (ctx.request as any).body
|
||||
: ctx.method === 'GET'
|
||||
? ctx.query
|
||||
: ctx.request.body;
|
||||
|
||||
const reply = (x?: any, y?: ApiError) => {
|
||||
if (x == null) {
|
||||
ctx.status = 204;
|
||||
} else if (typeof x === 'number' && y) {
|
||||
ctx.status = x;
|
||||
ctx.body = {
|
||||
error: {
|
||||
message: y!.message,
|
||||
code: y!.code,
|
||||
id: y!.id,
|
||||
kind: y!.kind,
|
||||
...(y!.info ? { info: y!.info } : {}),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
|
||||
ctx.body = typeof x === 'string' ? JSON.stringify(x) : x;
|
||||
}
|
||||
res();
|
||||
};
|
||||
|
||||
// Authentication
|
||||
this.authenticateService.authenticate(body['i']).then(([user, app]) => {
|
||||
// API invoking
|
||||
this.call(endpoint, exec, user, app, body, ctx).then((res: any) => {
|
||||
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
|
||||
ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
|
||||
}
|
||||
reply(res);
|
||||
}).catch((e: ApiError) => {
|
||||
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
|
||||
});
|
||||
|
||||
// Log IP
|
||||
if (user) {
|
||||
this.metaService.fetch().then(meta => {
|
||||
if (!meta.enableIpLogging) return;
|
||||
const ip = ctx.ip;
|
||||
const ips = this.userIpHistories.get(user.id);
|
||||
if (ips == null || !ips.has(ip)) {
|
||||
if (ips == null) {
|
||||
this.userIpHistories.set(user.id, new Set([ip]));
|
||||
} else {
|
||||
ips.add(ip);
|
||||
}
|
||||
|
||||
try {
|
||||
this.userIpsRepository.createQueryBuilder().insert().values({
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: ip,
|
||||
}).orIgnore(true).execute();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch(e => {
|
||||
if (e instanceof AuthenticationError) {
|
||||
reply(403, new ApiError({
|
||||
message: 'Authentication failed. Please ensure your token is correct.',
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
}));
|
||||
} else {
|
||||
reply(500, new ApiError());
|
||||
public handleRequest(
|
||||
endpoint: IEndpoint & { exec: any },
|
||||
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const body = request.method === 'GET'
|
||||
? request.query
|
||||
: request.body;
|
||||
|
||||
const token = body['i'];
|
||||
if (token != null && typeof token !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
this.call(endpoint, user, app, body, null, request).then((res) => {
|
||||
if (request.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
|
||||
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
|
||||
}
|
||||
this.send(reply, res);
|
||||
}).catch((err: ApiError) => {
|
||||
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : 500, err);
|
||||
});
|
||||
|
||||
if (user) {
|
||||
this.logIp(request, user);
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err instanceof AuthenticationError) {
|
||||
this.send(reply, 403, new ApiError({
|
||||
message: 'Authentication failed. Please ensure your token is correct.',
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
}));
|
||||
} else {
|
||||
this.send(reply, 500, new ApiError());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async handleMultipartRequest(
|
||||
endpoint: IEndpoint & { exec: any },
|
||||
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const multipartData = await request.file();
|
||||
if (multipartData == null) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const [path] = await createTemp();
|
||||
await pump(multipartData.file, fs.createWriteStream(path));
|
||||
|
||||
const fields = {} as Record<string, string | undefined>;
|
||||
for (const [k, v] of Object.entries(multipartData.fields)) {
|
||||
fields[k] = v.value;
|
||||
}
|
||||
|
||||
const token = fields['i'];
|
||||
if (token != null && typeof token !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
this.call(endpoint, user, app, fields, {
|
||||
name: multipartData.filename,
|
||||
path: path,
|
||||
}, request).then((res) => {
|
||||
this.send(reply, res);
|
||||
}).catch((err: ApiError) => {
|
||||
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : 500, err);
|
||||
});
|
||||
|
||||
if (user) {
|
||||
this.logIp(request, user);
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err instanceof AuthenticationError) {
|
||||
this.send(reply, 403, new ApiError({
|
||||
message: 'Authentication failed. Please ensure your token is correct.',
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
}));
|
||||
} else {
|
||||
this.send(reply, 500, new ApiError());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private send(reply: FastifyReply, x?: any, y?: ApiError) {
|
||||
if (x == null) {
|
||||
reply.code(204);
|
||||
} else if (typeof x === 'number' && y) {
|
||||
reply.code(x);
|
||||
reply.send({
|
||||
error: {
|
||||
message: y!.message,
|
||||
code: y!.code,
|
||||
id: y!.id,
|
||||
kind: y!.kind,
|
||||
...(y!.info ? { info: y!.info } : {}),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
|
||||
reply.send(typeof x === 'string' ? JSON.stringify(x) : x);
|
||||
}
|
||||
}
|
||||
|
||||
private async logIp(request: FastifyRequest, user: ILocalUser) {
|
||||
const meta = await this.metaService.fetch();
|
||||
if (!meta.enableIpLogging) return;
|
||||
const ip = request.ip;
|
||||
const ips = this.userIpHistories.get(user.id);
|
||||
if (ips == null || !ips.has(ip)) {
|
||||
if (ips == null) {
|
||||
this.userIpHistories.set(user.id, new Set([ip]));
|
||||
} else {
|
||||
ips.add(ip);
|
||||
}
|
||||
|
||||
try {
|
||||
this.userIpsRepository.createQueryBuilder().insert().values({
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: ip,
|
||||
}).orIgnore(true).execute();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async call(
|
||||
ep: IEndpoint,
|
||||
exec: any,
|
||||
ep: IEndpoint & { exec: any },
|
||||
user: CacheableLocalUser | null | undefined,
|
||||
token: AccessToken | null | undefined,
|
||||
data: any,
|
||||
ctx?: Koa.Context,
|
||||
file: {
|
||||
name: string;
|
||||
path: string;
|
||||
} | null,
|
||||
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
|
||||
) {
|
||||
const isSecure = user != null && token == null;
|
||||
const isModerator = user != null && (user.isModerator || user.isAdmin);
|
||||
@@ -144,7 +207,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
if (user) {
|
||||
limitActor = user.id;
|
||||
} else {
|
||||
limitActor = getIpHash(ctx!.ip);
|
||||
limitActor = getIpHash(request.ip);
|
||||
}
|
||||
|
||||
const limit = Object.assign({}, ep.meta.limit);
|
||||
@@ -154,7 +217,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
|
||||
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(err => {
|
||||
throw new ApiError({
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
@@ -199,7 +262,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
// Cast non JSON input
|
||||
if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) {
|
||||
if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
|
||||
for (const k of Object.keys(ep.params.properties)) {
|
||||
const param = ep.params.properties![k];
|
||||
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
|
||||
@@ -221,7 +284,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
|
||||
// API invoking
|
||||
const before = performance.now();
|
||||
return await exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((err: Error) => {
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
} else {
|
||||
|
@@ -1,15 +1,13 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import multer from '@koa/multer';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import cors from '@koa/cors';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
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 from './endpoints.js';
|
||||
import endpoints, { IEndpoint } from './endpoints.js';
|
||||
import { ApiCallService } from './ApiCallService.js';
|
||||
import { SignupApiService } from './SignupApiService.js';
|
||||
import { SigninApiService } from './SigninApiService.js';
|
||||
@@ -42,92 +40,107 @@ export class ApiServerService {
|
||||
private discordServerService: DiscordServerService,
|
||||
private twitterServerService: TwitterServerService,
|
||||
) {
|
||||
this.createServer = this.createServer.bind(this);
|
||||
}
|
||||
|
||||
public createApiServer() {
|
||||
const handlers: Record<string, any> = {};
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
handlers[endpoint.name] = this.moduleRef.get('ep:' + endpoint.name, { strict: false }).exec;
|
||||
}
|
||||
|
||||
// Init app
|
||||
const apiServer = new Koa();
|
||||
|
||||
apiServer.use(cors({
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.register(cors, {
|
||||
origin: '*',
|
||||
}));
|
||||
|
||||
// No caching
|
||||
apiServer.use(async (ctx, next) => {
|
||||
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||
await next();
|
||||
});
|
||||
|
||||
apiServer.use(bodyParser({
|
||||
// リクエストが multipart/form-data でない限りはJSONだと見なす
|
||||
detectJSON: ctx => !ctx.is('multipart/form-data'),
|
||||
}));
|
||||
|
||||
// Init multer instance
|
||||
const upload = multer({
|
||||
storage: multer.diskStorage({}),
|
||||
fastify.register(multipart, {
|
||||
limits: {
|
||||
fileSize: this.config.maxFileSize ?? 262144000,
|
||||
files: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
// Prevent cache
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||
done();
|
||||
});
|
||||
|
||||
/**
|
||||
* Register endpoint handlers
|
||||
*/
|
||||
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) {
|
||||
router.post(`/${endpoint.name}`, upload.single('file'), this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
|
||||
} else {
|
||||
// 後方互換性のため
|
||||
if (endpoint.name.includes('-')) {
|
||||
router.post(`/${endpoint.name.replace(/-/g, '_')}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
|
||||
|
||||
if (endpoint.meta.allowGet) {
|
||||
router.get(`/${endpoint.name.replace(/-/g, '_')}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
|
||||
} else {
|
||||
router.get(`/${endpoint.name.replace(/-/g, '_')}`, async ctx => { ctx.status = 405; });
|
||||
fastify.all<{
|
||||
Params: { endpoint: string; },
|
||||
Body: Record<string, unknown>,
|
||||
Querystring: Record<string, unknown>,
|
||||
}>('/' + endpoint.name, (request, reply) => {
|
||||
if (request.method === 'GET' && !endpoint.meta.allowGet) {
|
||||
reply.code(405);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
router.post(`/${endpoint.name}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
|
||||
|
||||
if (endpoint.meta.allowGet) {
|
||||
router.get(`/${endpoint.name}`, this.apiCallService.handleRequest.bind(this.apiCallService, endpoint, handlers[endpoint.name]));
|
||||
} else {
|
||||
router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; });
|
||||
}
|
||||
|
||||
this.apiCallService.handleMultipartRequest(ep, request, reply);
|
||||
});
|
||||
} else {
|
||||
fastify.all<{
|
||||
Params: { endpoint: string; },
|
||||
Body: Record<string, unknown>,
|
||||
Querystring: Record<string, unknown>,
|
||||
}>('/' + endpoint.name, (request, reply) => {
|
||||
if (request.method === 'GET' && !endpoint.meta.allowGet) {
|
||||
reply.code(405);
|
||||
return;
|
||||
}
|
||||
|
||||
this.apiCallService.handleRequest(ep, request, reply);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/signup', ctx => this.signupApiServiceService.signup(ctx));
|
||||
router.post('/signin', ctx => this.signinApiServiceService.signin(ctx));
|
||||
router.post('/signup-pending', ctx => this.signupApiServiceService.signupPending(ctx));
|
||||
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));
|
||||
|
||||
router.use(this.discordServerService.create().routes());
|
||||
router.use(this.githubServerService.create().routes());
|
||||
router.use(this.twitterServerService.create().routes());
|
||||
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));
|
||||
|
||||
router.get('/v1/instance/peers', async ctx => {
|
||||
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'],
|
||||
});
|
||||
|
||||
ctx.body = instances.map(instance => instance.host);
|
||||
return instances.map(instance => instance.host);
|
||||
});
|
||||
|
||||
router.post('/miauth/:session/check', async ctx => {
|
||||
fastify.post<{ Params: { session: string; } }>('/miauth/:session/check', async (request, reply) => {
|
||||
const token = await this.accessTokensRepository.findOneBy({
|
||||
session: ctx.params.session,
|
||||
session: request.params.session,
|
||||
});
|
||||
|
||||
if (token && token.session != null && !token.fetched) {
|
||||
@@ -135,26 +148,18 @@ export class ApiServerService {
|
||||
fetched: true,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
return {
|
||||
ok: true,
|
||||
token: token.token,
|
||||
user: await this.userEntityService.pack(token.userId, null, { detail: true }),
|
||||
};
|
||||
} else {
|
||||
ctx.body = {
|
||||
return {
|
||||
ok: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Return 404 for unknown API
|
||||
router.all('(.*)', async ctx => {
|
||||
ctx.status = 404;
|
||||
});
|
||||
|
||||
// Register router
|
||||
apiServer.use(router.routes());
|
||||
|
||||
return apiServer;
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ export class AuthenticateService {
|
||||
this.appCache = new Cache<App>(Infinity);
|
||||
}
|
||||
|
||||
public async authenticate(token: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> {
|
||||
public async authenticate(token: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> {
|
||||
if (token == null) {
|
||||
return [null, null];
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import * as speakeasy from 'speakeasy';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -12,7 +13,6 @@ import { IdService } from '@/core/IdService.js';
|
||||
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type Koa from 'koa';
|
||||
|
||||
@Injectable()
|
||||
export class SigninApiService {
|
||||
@@ -42,47 +42,60 @@ export class SigninApiService {
|
||||
) {
|
||||
}
|
||||
|
||||
public async signin(ctx: Koa.Context) {
|
||||
ctx.set('Access-Control-Allow-Origin', this.config.url);
|
||||
ctx.set('Access-Control-Allow-Credentials', 'true');
|
||||
public async signin(
|
||||
request: FastifyRequest<{
|
||||
Body: {
|
||||
username: string;
|
||||
password: string;
|
||||
token?: string;
|
||||
signature?: string;
|
||||
authenticatorData?: string;
|
||||
clientDataJSON?: string;
|
||||
credentialId?: string;
|
||||
challengeId?: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
reply.header('Access-Control-Allow-Origin', this.config.url);
|
||||
reply.header('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
const body = ctx.request.body as any;
|
||||
const body = request.body;
|
||||
const username = body['username'];
|
||||
const password = body['password'];
|
||||
const token = body['token'];
|
||||
|
||||
function error(status: number, error: { id: string }) {
|
||||
ctx.status = status;
|
||||
ctx.body = { error };
|
||||
reply.code(status);
|
||||
return { error };
|
||||
}
|
||||
|
||||
try {
|
||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||
await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip));
|
||||
await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||
} catch (err) {
|
||||
ctx.status = 429;
|
||||
ctx.body = {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
ctx.status = 400;
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof password !== 'string') {
|
||||
ctx.status = 400;
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (token != null && typeof token !== 'string') {
|
||||
ctx.status = 400;
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,17 +106,15 @@ export class SigninApiService {
|
||||
}) as ILocalUser;
|
||||
|
||||
if (user == null) {
|
||||
error(404, {
|
||||
return error(404, {
|
||||
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
error(403, {
|
||||
return error(403, {
|
||||
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
@@ -117,32 +128,29 @@ export class SigninApiService {
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: ctx.ip,
|
||||
headers: ctx.headers,
|
||||
ip: request.ip,
|
||||
headers: request.headers,
|
||||
success: false,
|
||||
});
|
||||
|
||||
error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
||||
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
||||
};
|
||||
|
||||
if (!profile.twoFactorEnabled) {
|
||||
if (same) {
|
||||
this.signinService.signin(ctx, user);
|
||||
return;
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (token) {
|
||||
if (!same) {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const verified = (speakeasy as any).totp.verify({
|
||||
@@ -153,20 +161,17 @@ export class SigninApiService {
|
||||
});
|
||||
|
||||
if (verified) {
|
||||
this.signinService.signin(ctx, user);
|
||||
return;
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (body.credentialId) {
|
||||
} else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) {
|
||||
if (!same && !profile.usePasswordLessLogin) {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
|
||||
@@ -179,10 +184,9 @@ export class SigninApiService {
|
||||
});
|
||||
|
||||
if (!challenge) {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: '2715a88a-2125-4013-932f-aa6fe72792da',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.attestationChallengesRepository.delete({
|
||||
@@ -191,10 +195,9 @@ export class SigninApiService {
|
||||
});
|
||||
|
||||
if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: '2715a88a-2125-4013-932f-aa6fe72792da',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const securityKey = await this.userSecurityKeysRepository.findOneBy({
|
||||
@@ -207,10 +210,9 @@ export class SigninApiService {
|
||||
});
|
||||
|
||||
if (!securityKey) {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: '66269679-aeaf-4474-862b-eb761197e046',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = this.twoFactorAuthenticationService.verifySignin({
|
||||
@@ -223,20 +225,17 @@ export class SigninApiService {
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
this.signinService.signin(ctx, user);
|
||||
return;
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: '93b86c4b-72f9-40eb-9815-798928603d1e',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!same && !profile.usePasswordLessLogin) {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = await this.userSecurityKeysRepository.findBy({
|
||||
@@ -244,10 +243,9 @@ export class SigninApiService {
|
||||
});
|
||||
|
||||
if (keys.length === 0) {
|
||||
await fail(403, {
|
||||
return await fail(403, {
|
||||
id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 32 byte challenge
|
||||
@@ -266,15 +264,14 @@ export class SigninApiService {
|
||||
registrationChallenge: false,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
reply.code(200);
|
||||
return {
|
||||
challenge,
|
||||
challengeId,
|
||||
securityKeys: keys.map(key => ({
|
||||
id: key.id,
|
||||
})),
|
||||
};
|
||||
ctx.status = 200;
|
||||
return;
|
||||
}
|
||||
// never get here
|
||||
}
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { SigninsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { SigninsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
|
||||
import type Koa from 'koa';
|
||||
|
||||
@Injectable()
|
||||
export class SigninService {
|
||||
@@ -24,10 +23,25 @@ export class SigninService {
|
||||
) {
|
||||
}
|
||||
|
||||
public signin(ctx: Koa.Context, user: ILocalUser, redirect = false) {
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) {
|
||||
setImmediate(async () => {
|
||||
// Append signin history
|
||||
const record = await this.signinsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: request.ip,
|
||||
headers: request.headers,
|
||||
success: true,
|
||||
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
// Publish signin event
|
||||
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
|
||||
});
|
||||
|
||||
if (redirect) {
|
||||
//#region Cookie
|
||||
ctx.cookies.set('igi', user.token!, {
|
||||
reply.cookies.set('igi', user.token!, {
|
||||
path: '/',
|
||||
// SEE: https://github.com/koajs/koa/issues/974
|
||||
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
|
||||
@@ -36,29 +50,14 @@ export class SigninService {
|
||||
});
|
||||
//#endregion
|
||||
|
||||
ctx.redirect(this.config.url);
|
||||
reply.redirect(this.config.url);
|
||||
} else {
|
||||
ctx.body = {
|
||||
reply.code(200);
|
||||
return {
|
||||
id: user.id,
|
||||
i: user.token,
|
||||
};
|
||||
ctx.status = 200;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Append signin history
|
||||
const record = await this.signinsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: ctx.ip,
|
||||
headers: ctx.headers,
|
||||
success: true,
|
||||
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
// Publish signin event
|
||||
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import rndstr from 'rndstr';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { RegistrationTicketsRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -11,8 +12,8 @@ import { SignupService } from '@/core/SignupService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { ILocalUser } from '@/models/entities/User.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type Koa from 'koa';
|
||||
|
||||
@Injectable()
|
||||
export class SignupApiService {
|
||||
@@ -42,8 +43,22 @@ export class SignupApiService {
|
||||
) {
|
||||
}
|
||||
|
||||
public async signup(ctx: Koa.Context) {
|
||||
const body = ctx.request.body;
|
||||
public async signup(
|
||||
request: FastifyRequest<{
|
||||
Body: {
|
||||
username: string;
|
||||
password: string;
|
||||
host?: string;
|
||||
invitationCode?: string;
|
||||
emailAddress?: string;
|
||||
'hcaptcha-response'?: string;
|
||||
'g-recaptcha-response'?: string;
|
||||
'turnstile-response'?: string;
|
||||
}
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const body = request.body;
|
||||
|
||||
const instance = await this.metaService.fetch(true);
|
||||
|
||||
@@ -51,20 +66,20 @@ export class SignupApiService {
|
||||
// ただしテスト時はこの機構は障害となるため無効にする
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
|
||||
await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
|
||||
ctx.throw(400, e);
|
||||
await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
|
||||
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
|
||||
ctx.throw(400, e);
|
||||
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.enableTurnstile && instance.turnstileSecretKey) {
|
||||
await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(e => {
|
||||
ctx.throw(400, e);
|
||||
await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -77,20 +92,20 @@ export class SignupApiService {
|
||||
|
||||
if (instance.emailRequiredForSignup) {
|
||||
if (emailAddress == null || typeof emailAddress !== 'string') {
|
||||
ctx.status = 400;
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const available = await this.emailService.validateEmailForAccount(emailAddress);
|
||||
if (!available) {
|
||||
ctx.status = 400;
|
||||
const res = await this.emailService.validateEmailForAccount(emailAddress);
|
||||
if (!res.available) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.disableRegistration) {
|
||||
if (invitationCode == null || typeof invitationCode !== 'string') {
|
||||
ctx.status = 400;
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,7 +114,7 @@ export class SignupApiService {
|
||||
});
|
||||
|
||||
if (ticket == null) {
|
||||
ctx.status = 400;
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,18 +132,18 @@ export class SignupApiService {
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
code,
|
||||
email: emailAddress,
|
||||
email: emailAddress!,
|
||||
username: username,
|
||||
password: hash,
|
||||
});
|
||||
|
||||
const link = `${this.config.url}/signup-complete/${code}`;
|
||||
|
||||
this.emailService.sendEmail(emailAddress, 'Signup',
|
||||
this.emailService.sendEmail(emailAddress!, 'Signup',
|
||||
`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
|
||||
`To complete signup, please click this link: ${link}`);
|
||||
|
||||
ctx.status = 204;
|
||||
reply.code(204);
|
||||
} else {
|
||||
try {
|
||||
const { account, secret } = await this.signupService.signup({
|
||||
@@ -140,17 +155,18 @@ export class SignupApiService {
|
||||
includeSecrets: true,
|
||||
});
|
||||
|
||||
(res as any).token = secret;
|
||||
|
||||
ctx.body = res;
|
||||
} catch (e) {
|
||||
ctx.throw(400, e);
|
||||
return {
|
||||
...res,
|
||||
token: secret,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async signupPending(ctx: Koa.Context) {
|
||||
const body = ctx.request.body;
|
||||
public async signupPending(request: FastifyRequest<{ Body: { code: string; } }>, reply: FastifyReply) {
|
||||
const body = request.body;
|
||||
|
||||
const code = body['code'];
|
||||
|
||||
@@ -174,9 +190,9 @@ export class SignupApiService {
|
||||
emailVerifyCode: null,
|
||||
});
|
||||
|
||||
this.signinService.signin(ctx, account as ILocalUser);
|
||||
} catch (e) {
|
||||
ctx.throw(400, e);
|
||||
this.signinService.signin(request, reply, account as ILocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,23 +14,28 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||
|
||||
export type Response = Record<string, any> | void;
|
||||
|
||||
type File = {
|
||||
name: string | null;
|
||||
path: string;
|
||||
};
|
||||
|
||||
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
||||
type executor<T extends IEndpointMeta, Ps extends Schema> =
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||
|
||||
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||
public exec: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||
public exec: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||
|
||||
constructor(meta: T, paramDef: Ps, cb: executor<T, Ps>) {
|
||||
const validate = ajv.compile(paramDef);
|
||||
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
let cleanup: undefined | (() => void) = undefined;
|
||||
|
||||
if (meta.requireFile) {
|
||||
cleanup = () => {
|
||||
fs.unlink(file.path, () => {});
|
||||
if (file) fs.unlink(file.path, () => {});
|
||||
};
|
||||
|
||||
if (file == null) return Promise.reject(new ApiError({
|
||||
|
@@ -78,8 +78,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
|
||||
// Get 'name' parameter
|
||||
let name = ps.name ?? file.originalname;
|
||||
if (name !== undefined && name !== null) {
|
||||
let name = ps.name ?? file!.name ?? null;
|
||||
if (name != null) {
|
||||
name = name.trim();
|
||||
if (name.length === 0) {
|
||||
name = null;
|
||||
@@ -88,8 +88,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
} else if (!this.driveFileEntityService.validateFileName(name)) {
|
||||
throw new ApiError(meta.errors.invalidFileName);
|
||||
}
|
||||
} else {
|
||||
name = null;
|
||||
}
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
@@ -98,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
// Create file
|
||||
const driveFile = await this.driveService.addFile({
|
||||
user: me,
|
||||
path: file.path,
|
||||
path: file!.path,
|
||||
name,
|
||||
comment: ps.comment,
|
||||
folderId: ps.folderId,
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import Router from '@koa/router';
|
||||
import { OAuth2 } from 'oauth';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -12,8 +12,8 @@ import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { SigninService } from '../SigninService.js';
|
||||
import type Koa from 'koa';
|
||||
|
||||
@Injectable()
|
||||
export class DiscordServerService {
|
||||
@@ -36,21 +36,18 @@ export class DiscordServerService {
|
||||
private metaService: MetaService,
|
||||
private signinService: SigninService,
|
||||
) {
|
||||
this.create = this.create.bind(this);
|
||||
}
|
||||
|
||||
public create() {
|
||||
const router = new Router();
|
||||
|
||||
router.get('/disconnect/discord', async ctx => {
|
||||
if (!this.compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.get('/disconnect/discord', async (request, reply) => {
|
||||
if (!this.compareOrigin(request)) {
|
||||
throw new FastifyReplyError(400, 'invalid origin');
|
||||
}
|
||||
|
||||
const userToken = this.getUserToken(ctx);
|
||||
const userToken = this.getUserToken(request);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'signin required');
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneByOrFail({
|
||||
@@ -66,13 +63,13 @@ export class DiscordServerService {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = 'Discordの連携を解除しました :v:';
|
||||
|
||||
// Publish i updated event
|
||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
|
||||
return 'Discordの連携を解除しました :v:';
|
||||
});
|
||||
|
||||
const getOAuth2 = async () => {
|
||||
@@ -90,16 +87,14 @@ export class DiscordServerService {
|
||||
}
|
||||
};
|
||||
|
||||
router.get('/connect/discord', async ctx => {
|
||||
if (!this.compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
fastify.get('/connect/discord', async (request, reply) => {
|
||||
if (!this.compareOrigin(request)) {
|
||||
throw new FastifyReplyError(400, 'invalid origin');
|
||||
}
|
||||
|
||||
const userToken = this.getUserToken(ctx);
|
||||
const userToken = this.getUserToken(request);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'signin required');
|
||||
}
|
||||
|
||||
const params = {
|
||||
@@ -112,10 +107,10 @@ export class DiscordServerService {
|
||||
this.redisClient.set(userToken, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
reply.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get('/signin/discord', async ctx => {
|
||||
fastify.get('/signin/discord', async (request, reply) => {
|
||||
const sessid = uuid();
|
||||
|
||||
const params = {
|
||||
@@ -125,7 +120,7 @@ export class DiscordServerService {
|
||||
response_type: 'code',
|
||||
};
|
||||
|
||||
ctx.cookies.set('signin_with_discord_sid', sessid, {
|
||||
reply.cookies.set('signin_with_discord_sid', sessid, {
|
||||
path: '/',
|
||||
secure: this.config.url.startsWith('https'),
|
||||
httpOnly: true,
|
||||
@@ -134,27 +129,25 @@ export class DiscordServerService {
|
||||
this.redisClient.set(sessid, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
reply.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get('/dc/cb', async ctx => {
|
||||
const userToken = this.getUserToken(ctx);
|
||||
fastify.get('/dc/cb', async (request, reply) => {
|
||||
const userToken = this.getUserToken(request);
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
|
||||
if (!userToken) {
|
||||
const sessid = ctx.cookies.get('signin_with_discord_sid');
|
||||
const sessid = request.cookies.get('signin_with_discord_sid');
|
||||
|
||||
if (!sessid) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const code = ctx.query.code;
|
||||
const code = request.query.code;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
@@ -164,9 +157,8 @@ export class DiscordServerService {
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
if (request.query.state !== state) {
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
|
||||
@@ -192,8 +184,7 @@ export class DiscordServerService {
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.createQueryBuilder()
|
||||
@@ -202,8 +193,7 @@ export class DiscordServerService {
|
||||
.getOne();
|
||||
|
||||
if (profile == null) {
|
||||
ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
|
||||
return;
|
||||
throw new FastifyReplyError(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
|
||||
}
|
||||
|
||||
await this.userProfilesRepository.update(profile.userId, {
|
||||
@@ -220,13 +210,12 @@ export class DiscordServerService {
|
||||
},
|
||||
});
|
||||
|
||||
this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true);
|
||||
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true);
|
||||
} else {
|
||||
const code = ctx.query.code;
|
||||
const code = request.query.code;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
@@ -236,9 +225,8 @@ export class DiscordServerService {
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
if (request.query.state !== state) {
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
|
||||
@@ -263,8 +251,7 @@ export class DiscordServerService {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
})) as Record<string, unknown>;
|
||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneByOrFail({
|
||||
@@ -288,29 +275,29 @@ export class DiscordServerService {
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
|
||||
|
||||
// Publish i updated event
|
||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
|
||||
return `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
done();
|
||||
}
|
||||
|
||||
private getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
||||
private getUserToken(request: FastifyRequest): string | null {
|
||||
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
||||
}
|
||||
|
||||
private compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
private compareOrigin(request: FastifyRequest): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
||||
}
|
||||
|
||||
const referer = ctx.headers['referer'];
|
||||
const referer = request.headers['referer'];
|
||||
|
||||
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import Router from '@koa/router';
|
||||
import { OAuth2 } from 'oauth';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -12,8 +12,8 @@ import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { SigninService } from '../SigninService.js';
|
||||
import type Koa from 'koa';
|
||||
|
||||
@Injectable()
|
||||
export class GithubServerService {
|
||||
@@ -36,21 +36,18 @@ export class GithubServerService {
|
||||
private metaService: MetaService,
|
||||
private signinService: SigninService,
|
||||
) {
|
||||
this.create = this.create.bind(this);
|
||||
}
|
||||
|
||||
public create() {
|
||||
const router = new Router();
|
||||
|
||||
router.get('/disconnect/github', async ctx => {
|
||||
if (!this.compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.get('/disconnect/github', async (request, reply) => {
|
||||
if (!this.compareOrigin(request)) {
|
||||
throw new FastifyReplyError(400, 'invalid origin');
|
||||
}
|
||||
|
||||
const userToken = this.getUserToken(ctx);
|
||||
const userToken = this.getUserToken(request);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'signin required');
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneByOrFail({
|
||||
@@ -66,13 +63,13 @@ export class GithubServerService {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = 'GitHubの連携を解除しました :v:';
|
||||
|
||||
// Publish i updated event
|
||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
|
||||
return 'GitHubの連携を解除しました :v:';
|
||||
});
|
||||
|
||||
const getOath2 = async () => {
|
||||
@@ -90,16 +87,14 @@ export class GithubServerService {
|
||||
}
|
||||
};
|
||||
|
||||
router.get('/connect/github', async ctx => {
|
||||
if (!this.compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
fastify.get('/connect/github', async (request, reply) => {
|
||||
if (!this.compareOrigin(request)) {
|
||||
throw new FastifyReplyError(400, 'invalid origin');
|
||||
}
|
||||
|
||||
const userToken = this.getUserToken(ctx);
|
||||
const userToken = this.getUserToken(request);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'signin required');
|
||||
}
|
||||
|
||||
const params = {
|
||||
@@ -111,10 +106,10 @@ export class GithubServerService {
|
||||
this.redisClient.set(userToken, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
reply.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get('/signin/github', async ctx => {
|
||||
fastify.get('/signin/github', async (request, reply) => {
|
||||
const sessid = uuid();
|
||||
|
||||
const params = {
|
||||
@@ -123,7 +118,7 @@ export class GithubServerService {
|
||||
state: uuid(),
|
||||
};
|
||||
|
||||
ctx.cookies.set('signin_with_github_sid', sessid, {
|
||||
reply.cookies.set('signin_with_github_sid', sessid, {
|
||||
path: '/',
|
||||
secure: this.config.url.startsWith('https'),
|
||||
httpOnly: true,
|
||||
@@ -132,27 +127,25 @@ export class GithubServerService {
|
||||
this.redisClient.set(sessid, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
reply.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get('/gh/cb', async ctx => {
|
||||
const userToken = this.getUserToken(ctx);
|
||||
fastify.get('/gh/cb', async (request, reply) => {
|
||||
const userToken = this.getUserToken(request);
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
|
||||
if (!userToken) {
|
||||
const sessid = ctx.cookies.get('signin_with_github_sid');
|
||||
const sessid = request.cookies.get('signin_with_github_sid');
|
||||
|
||||
if (!sessid) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const code = ctx.query.code;
|
||||
const code = request.query.code;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
@@ -162,9 +155,8 @@ export class GithubServerService {
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
if (request.query.state !== state) {
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
|
||||
@@ -184,8 +176,7 @@ export class GithubServerService {
|
||||
'Authorization': `bearer ${accessToken}`,
|
||||
})) as Record<string, unknown>;
|
||||
if (typeof login !== 'string' || typeof id !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const link = await this.userProfilesRepository.createQueryBuilder()
|
||||
@@ -194,17 +185,15 @@ export class GithubServerService {
|
||||
.getOne();
|
||||
|
||||
if (link == null) {
|
||||
ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
|
||||
return;
|
||||
throw new FastifyReplyError(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
|
||||
}
|
||||
|
||||
this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
|
||||
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
|
||||
} else {
|
||||
const code = ctx.query.code;
|
||||
const code = request.query.code;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
@@ -214,9 +203,8 @@ export class GithubServerService {
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
if (request.query.state !== state) {
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
|
||||
@@ -238,8 +226,7 @@ export class GithubServerService {
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
if (typeof login !== 'string' || typeof id !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneByOrFail({
|
||||
@@ -260,29 +247,29 @@ export class GithubServerService {
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
|
||||
|
||||
// Publish i updated event
|
||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
|
||||
return `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
done();
|
||||
}
|
||||
|
||||
private getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
||||
private getUserToken(request: FastifyRequest): string | null {
|
||||
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
||||
}
|
||||
|
||||
private compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
private compareOrigin(request: FastifyRequest): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
||||
}
|
||||
|
||||
const referer = ctx.headers['referer'];
|
||||
const referer = request.headers['referer'];
|
||||
|
||||
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import Router from '@koa/router';
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull } from 'typeorm';
|
||||
import autwh from 'autwh';
|
||||
@@ -12,8 +12,8 @@ import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { SigninService } from '../SigninService.js';
|
||||
import type Koa from 'koa';
|
||||
|
||||
@Injectable()
|
||||
export class TwitterServerService {
|
||||
@@ -36,21 +36,18 @@ export class TwitterServerService {
|
||||
private metaService: MetaService,
|
||||
private signinService: SigninService,
|
||||
) {
|
||||
this.create = this.create.bind(this);
|
||||
}
|
||||
|
||||
public create() {
|
||||
const router = new Router();
|
||||
|
||||
router.get('/disconnect/twitter', async ctx => {
|
||||
if (!this.compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.get('/disconnect/twitter', async (request, reply) => {
|
||||
if (!this.compareOrigin(request)) {
|
||||
throw new FastifyReplyError(400, 'invalid origin');
|
||||
}
|
||||
|
||||
const userToken = this.getUserToken(ctx);
|
||||
const userToken = this.getUserToken(request);
|
||||
if (userToken == null) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'signin required');
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneByOrFail({
|
||||
@@ -66,13 +63,13 @@ export class TwitterServerService {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = 'Twitterの連携を解除しました :v:';
|
||||
|
||||
// Publish i updated event
|
||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
|
||||
return 'Twitterの連携を解除しました :v:';
|
||||
});
|
||||
|
||||
const getTwAuth = async () => {
|
||||
@@ -89,25 +86,23 @@ export class TwitterServerService {
|
||||
}
|
||||
};
|
||||
|
||||
router.get('/connect/twitter', async ctx => {
|
||||
if (!this.compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
fastify.get('/connect/twitter', async (request, reply) => {
|
||||
if (!this.compareOrigin(request)) {
|
||||
throw new FastifyReplyError(400, 'invalid origin');
|
||||
}
|
||||
|
||||
const userToken = this.getUserToken(ctx);
|
||||
const userToken = this.getUserToken(request);
|
||||
if (userToken == null) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'signin required');
|
||||
}
|
||||
|
||||
const twAuth = await getTwAuth();
|
||||
const twCtx = await twAuth!.begin();
|
||||
this.redisClient.set(userToken, JSON.stringify(twCtx));
|
||||
ctx.redirect(twCtx.url);
|
||||
reply.redirect(twCtx.url);
|
||||
});
|
||||
|
||||
router.get('/signin/twitter', async ctx => {
|
||||
fastify.get('/signin/twitter', async (request, reply) => {
|
||||
const twAuth = await getTwAuth();
|
||||
const twCtx = await twAuth!.begin();
|
||||
|
||||
@@ -115,26 +110,25 @@ export class TwitterServerService {
|
||||
|
||||
this.redisClient.set(sessid, JSON.stringify(twCtx));
|
||||
|
||||
ctx.cookies.set('signin_with_twitter_sid', sessid, {
|
||||
reply.cookies.set('signin_with_twitter_sid', sessid, {
|
||||
path: '/',
|
||||
secure: this.config.url.startsWith('https'),
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
ctx.redirect(twCtx.url);
|
||||
reply.redirect(twCtx.url);
|
||||
});
|
||||
|
||||
router.get('/tw/cb', async ctx => {
|
||||
const userToken = this.getUserToken(ctx);
|
||||
fastify.get('/tw/cb', async (request, reply) => {
|
||||
const userToken = this.getUserToken(request);
|
||||
|
||||
const twAuth = await getTwAuth();
|
||||
|
||||
if (userToken == null) {
|
||||
const sessid = ctx.cookies.get('signin_with_twitter_sid');
|
||||
const sessid = request.cookies.get('signin_with_twitter_sid');
|
||||
|
||||
if (sessid == null) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const get = new Promise<any>((res, rej) => {
|
||||
@@ -145,10 +139,9 @@ export class TwitterServerService {
|
||||
|
||||
const twCtx = await get;
|
||||
|
||||
const verifier = ctx.query.oauth_verifier;
|
||||
const verifier = request.query.oauth_verifier;
|
||||
if (!verifier || typeof verifier !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
|
||||
@@ -159,17 +152,15 @@ export class TwitterServerService {
|
||||
.getOne();
|
||||
|
||||
if (link == null) {
|
||||
ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
|
||||
return;
|
||||
throw new FastifyReplyError(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
|
||||
}
|
||||
|
||||
this.signinService.signin(ctx, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
|
||||
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
|
||||
} else {
|
||||
const verifier = ctx.query.oauth_verifier;
|
||||
const verifier = request.query.oauth_verifier;
|
||||
|
||||
if (!verifier || typeof verifier !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
throw new FastifyReplyError(400, 'invalid session');
|
||||
}
|
||||
|
||||
const get = new Promise<any>((res, rej) => {
|
||||
@@ -201,29 +192,29 @@ export class TwitterServerService {
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
|
||||
|
||||
// Publish i updated event
|
||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
|
||||
return `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
done();
|
||||
}
|
||||
|
||||
private getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
||||
private getUserToken(request: FastifyRequest): string | null {
|
||||
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
||||
}
|
||||
|
||||
private compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
private compareOrigin(request: FastifyRequest): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
||||
}
|
||||
|
||||
const referer = ctx.headers['referer'];
|
||||
const referer = request.headers['referer'];
|
||||
|
||||
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
|
||||
}
|
||||
|
Reference in New Issue
Block a user