forked from mirrors/misskey
* fix(backend): `text: null`だけのノートは投稿できないように * add test * Update CHANGELOG.md * chore: bump OpenAPI Specification from 3.0.0 to 3.1.0 * chore: テストがすでにコメントで記述されていたのでそっちを使うことにする * fix test * fix(backend): prohibit posting whitespace-only notes * Update CHANGELOG.md * fix(backend): `renoteId`または`fileIds`(`mediaIds`)または`poll`が`null`でない場合に、`text が空白文字のみで構成されたリクエストになることを許可して、結果は`text: null`を返すように * test(backend): 引用renoteで空白文字のみで構成されたtextにするとレスポンスが`text: null`になることをチェックするテストを追加 * fix(frontend): `text`が`null`であって`renoteId`と`replyId`が`null`でないようなノートは引用リノートとして表示するように * fix(misskey-js): OpenAPI 3.1に対応 * fix(misskey-js): 型生成をOpenAPI Specification 3.1.0に対応 * fix(ci): `validate-api.json`をOpenAPI Specification 3.1.0に対応 * fix(ci): スキーマ書き換えの際のミスを修正 * Revert "fix(frontend): `text`が`null`であって`renoteId`と`replyId`が`null`でないようなノートは引用リノートとして表示するように" This reverts commit a9ca55343df6ea1679599acbc4801f78aa3a242b. * fix(misskey-js): `build-misskey-js-with-types`時は`api.json`のGETをスキップするように * Revert "fix(misskey-js): `build-misskey-js-with-types`時は`api.json`のGETをスキップするように" This reverts commit 865458989f9ddacc38d1bb3743a41ea828dbf324. * fix(misskey-js): `openapi-parser`で`validate`のかわりに`parse`を用いるように * Update CHANGELOG.md
237 lines
4.5 KiB
TypeScript
237 lines
4.5 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
import bcrypt from 'bcryptjs';
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
|
import type { UserProfilesRepository } from '@/models/_.js';
|
|
import { DI } from '@/di-symbols.js';
|
|
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
|
import { ApiError } from '@/server/api/error.js';
|
|
import { UserAuthService } from '@/core/UserAuthService.js';
|
|
|
|
export const meta = {
|
|
requireCredential: true,
|
|
|
|
secure: true,
|
|
|
|
errors: {
|
|
userNotFound: {
|
|
message: 'User not found.',
|
|
code: 'USER_NOT_FOUND',
|
|
id: '652f899f-66d4-490e-993e-6606c8ec04c3',
|
|
},
|
|
|
|
incorrectPassword: {
|
|
message: 'Incorrect password.',
|
|
code: 'INCORRECT_PASSWORD',
|
|
id: '38769596-efe2-4faf-9bec-abbb3f2cd9ba',
|
|
},
|
|
|
|
twoFactorNotEnabled: {
|
|
message: '2fa not enabled.',
|
|
code: 'TWO_FACTOR_NOT_ENABLED',
|
|
id: 'bf32b864-449b-47b8-974e-f9a5468546f1',
|
|
},
|
|
},
|
|
|
|
res: {
|
|
type: 'object',
|
|
nullable: false,
|
|
optional: false,
|
|
properties: {
|
|
rp: {
|
|
type: 'object',
|
|
properties: {
|
|
id: {
|
|
type: 'string',
|
|
nullable: true,
|
|
},
|
|
},
|
|
},
|
|
user: {
|
|
type: 'object',
|
|
properties: {
|
|
id: {
|
|
type: 'string',
|
|
},
|
|
name: {
|
|
type: 'string',
|
|
},
|
|
displayName: {
|
|
type: 'string',
|
|
},
|
|
},
|
|
},
|
|
challenge: {
|
|
type: 'string',
|
|
},
|
|
pubKeyCredParams: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
type: {
|
|
type: 'string',
|
|
},
|
|
alg: {
|
|
type: 'number',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
timeout: {
|
|
type: 'number',
|
|
nullable: true,
|
|
},
|
|
excludeCredentials: {
|
|
type: 'array',
|
|
nullable: true,
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
id: {
|
|
type: 'string',
|
|
},
|
|
type: {
|
|
type: 'string',
|
|
},
|
|
transports: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'string',
|
|
enum: [
|
|
'ble',
|
|
'cable',
|
|
'hybrid',
|
|
'internal',
|
|
'nfc',
|
|
'smart-card',
|
|
'usb',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
authenticatorSelection: {
|
|
type: 'object',
|
|
nullable: true,
|
|
properties: {
|
|
authenticatorAttachment: {
|
|
type: 'string',
|
|
enum: [
|
|
'cross-platform',
|
|
'platform',
|
|
],
|
|
},
|
|
requireResidentKey: {
|
|
type: 'boolean',
|
|
},
|
|
userVerification: {
|
|
type: 'string',
|
|
enum: [
|
|
'discouraged',
|
|
'preferred',
|
|
'required',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
attestation: {
|
|
type: 'string',
|
|
nullable: true,
|
|
enum: [
|
|
'direct',
|
|
'enterprise',
|
|
'indirect',
|
|
'none',
|
|
null,
|
|
],
|
|
},
|
|
extensions: {
|
|
type: 'object',
|
|
nullable: true,
|
|
properties: {
|
|
appid: {
|
|
type: 'string',
|
|
nullable: true,
|
|
},
|
|
credProps: {
|
|
type: 'boolean',
|
|
nullable: true,
|
|
},
|
|
hmacCreateSecret: {
|
|
type: 'boolean',
|
|
nullable: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as const;
|
|
|
|
export const paramDef = {
|
|
type: 'object',
|
|
properties: {
|
|
password: { type: 'string' },
|
|
token: { type: 'string', nullable: true },
|
|
},
|
|
required: ['password'],
|
|
} as const;
|
|
|
|
// eslint-disable-next-line import/no-default-export
|
|
@Injectable()
|
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|
constructor(
|
|
@Inject(DI.userProfilesRepository)
|
|
private userProfilesRepository: UserProfilesRepository,
|
|
|
|
private webAuthnService: WebAuthnService,
|
|
private userAuthService: UserAuthService,
|
|
) {
|
|
super(meta, paramDef, async (ps, me) => {
|
|
const token = ps.token;
|
|
const profile = await this.userProfilesRepository.findOne({
|
|
where: {
|
|
userId: me.id,
|
|
},
|
|
relations: ['user'],
|
|
});
|
|
|
|
if (profile == null) {
|
|
throw new ApiError(meta.errors.userNotFound);
|
|
}
|
|
|
|
if (profile.twoFactorEnabled) {
|
|
if (token == null) {
|
|
throw new Error('authentication failed');
|
|
}
|
|
|
|
try {
|
|
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
|
} catch (e) {
|
|
throw new Error('authentication failed');
|
|
}
|
|
}
|
|
|
|
const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? '');
|
|
if (!passwordMatched) {
|
|
throw new ApiError(meta.errors.incorrectPassword);
|
|
}
|
|
|
|
if (!profile.twoFactorEnabled) {
|
|
throw new ApiError(meta.errors.twoFactorNotEnabled);
|
|
}
|
|
|
|
return await this.webAuthnService.initiateRegistration(
|
|
me.id,
|
|
profile.user?.username ?? me.id,
|
|
profile.user?.name ?? undefined,
|
|
);
|
|
});
|
|
}
|
|
}
|