🎨 2FA設定のデザイン向上 / セキュリティキーの名前を変更できるように (#9985)
* wip * fix * wip * wip * ✌️ * rename key * 🎨 * update CHANGELOG.md * パスワードレスログインの判断はサーバーで * 日本語 * 日本語 * 日本語 * 日本語 * ✌️ * fix * refactor * トークン→確認コード * fix password-less / qr click * use otpauth * 日本語 * autocomplete * パスワードレス設定は外に出す * 🎨 * 🎨 --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
		@@ -170,6 +170,7 @@ import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
 | 
			
		||||
import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
 | 
			
		||||
import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
 | 
			
		||||
import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
 | 
			
		||||
import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
 | 
			
		||||
import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
 | 
			
		||||
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
 | 
			
		||||
import * as ep___i_apps from './endpoints/i/apps.js';
 | 
			
		||||
@@ -486,6 +487,7 @@ const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___
 | 
			
		||||
const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default };
 | 
			
		||||
const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default };
 | 
			
		||||
const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default };
 | 
			
		||||
const $i_2fa_updateKey: Provider = { provide: 'ep:i/2fa/update-key', useClass: ep___i_2fa_updateKey.default };
 | 
			
		||||
const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default };
 | 
			
		||||
const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
 | 
			
		||||
const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
 | 
			
		||||
@@ -806,6 +808,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
			
		||||
		$i_2fa_passwordLess,
 | 
			
		||||
		$i_2fa_registerKey,
 | 
			
		||||
		$i_2fa_register,
 | 
			
		||||
		$i_2fa_updateKey,
 | 
			
		||||
		$i_2fa_removeKey,
 | 
			
		||||
		$i_2fa_unregister,
 | 
			
		||||
		$i_apps,
 | 
			
		||||
@@ -1120,6 +1123,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
			
		||||
		$i_2fa_passwordLess,
 | 
			
		||||
		$i_2fa_registerKey,
 | 
			
		||||
		$i_2fa_register,
 | 
			
		||||
		$i_2fa_updateKey,
 | 
			
		||||
		$i_2fa_removeKey,
 | 
			
		||||
		$i_2fa_unregister,
 | 
			
		||||
		$i_apps,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { randomBytes } from 'node:crypto';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import bcrypt from 'bcryptjs';
 | 
			
		||||
import * as speakeasy from 'speakeasy';
 | 
			
		||||
import * as OTPAuth from "otpauth";
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
@@ -155,19 +155,19 @@ export class SigninApiService {
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const verified = (speakeasy as any).totp.verify({
 | 
			
		||||
				secret: profile.twoFactorSecret,
 | 
			
		||||
				encoding: 'base32',
 | 
			
		||||
				token: token,
 | 
			
		||||
				window: 2,
 | 
			
		||||
			const delta = OTPAuth.TOTP.validate({
 | 
			
		||||
				secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret),
 | 
			
		||||
				digits: 6,
 | 
			
		||||
				token,
 | 
			
		||||
				window: 1,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (verified) {
 | 
			
		||||
				return this.signinService.signin(request, reply, user);
 | 
			
		||||
			} else {
 | 
			
		||||
			if (delta === null) {
 | 
			
		||||
				return await fail(403, {
 | 
			
		||||
					id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				return this.signinService.signin(request, reply, user);
 | 
			
		||||
			}
 | 
			
		||||
		} else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) {
 | 
			
		||||
			if (!same && !profile.usePasswordLessLogin) {
 | 
			
		||||
 
 | 
			
		||||
@@ -170,6 +170,7 @@ import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
 | 
			
		||||
import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
 | 
			
		||||
import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
 | 
			
		||||
import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
 | 
			
		||||
import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
 | 
			
		||||
import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
 | 
			
		||||
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
 | 
			
		||||
import * as ep___i_apps from './endpoints/i/apps.js';
 | 
			
		||||
@@ -484,6 +485,7 @@ const eps = [
 | 
			
		||||
	['i/2fa/password-less', ep___i_2fa_passwordLess],
 | 
			
		||||
	['i/2fa/register-key', ep___i_2fa_registerKey],
 | 
			
		||||
	['i/2fa/register', ep___i_2fa_register],
 | 
			
		||||
	['i/2fa/update-key', ep___i_2fa_updateKey],
 | 
			
		||||
	['i/2fa/remove-key', ep___i_2fa_removeKey],
 | 
			
		||||
	['i/2fa/unregister', ep___i_2fa_unregister],
 | 
			
		||||
	['i/apps', ep___i_apps],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
import * as speakeasy from 'speakeasy';
 | 
			
		||||
import * as OTPAuth from 'otpauth';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import type { UserProfilesRepository } from '@/models/index.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
@@ -22,8 +25,14 @@ export const paramDef = {
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userProfilesRepository)
 | 
			
		||||
		private userProfilesRepository: UserProfilesRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const token = ps.token.replace(/\s/g, '');
 | 
			
		||||
@@ -34,13 +43,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
				throw new Error('二段階認証の設定が開始されていません');
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const verified = (speakeasy as any).totp.verify({
 | 
			
		||||
				secret: profile.twoFactorTempSecret,
 | 
			
		||||
				encoding: 'base32',
 | 
			
		||||
				token: token,
 | 
			
		||||
			const delta = OTPAuth.TOTP.validate({
 | 
			
		||||
				secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret),
 | 
			
		||||
				digits: 6,
 | 
			
		||||
				token,
 | 
			
		||||
				window: 1,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (!verified) {
 | 
			
		||||
			if (delta === null) {
 | 
			
		||||
				throw new Error('not verified');
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -48,6 +58,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
				twoFactorSecret: profile.twoFactorTempSecret,
 | 
			
		||||
				twoFactorEnabled: true,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Publish meUpdated event
 | 
			
		||||
			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
				includeSecrets: true,
 | 
			
		||||
			}));
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ export const paramDef = {
 | 
			
		||||
		attestationObject: { type: 'string' },
 | 
			
		||||
		password: { type: 'string' },
 | 
			
		||||
		challengeId: { type: 'string' },
 | 
			
		||||
		name: { type: 'string' },
 | 
			
		||||
		name: { type: 'string', minLength: 1, maxLength: 30 },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'],
 | 
			
		||||
} as const;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,23 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { UserProfilesRepository } from '@/models/index.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	secure: true,
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noKey: {
 | 
			
		||||
			message: 'No security key.',
 | 
			
		||||
			code: 'NO_SECURITY_KEY',
 | 
			
		||||
			id: 'f9c54d7f-d4c2-4d3c-9a8g-a70daac86512',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
@@ -23,11 +34,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.userProfilesRepository)
 | 
			
		||||
		private userProfilesRepository: UserProfilesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userSecurityKeysRepository)
 | 
			
		||||
		private userSecurityKeysRepository: UserSecurityKeysRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			if (ps.value === true) {
 | 
			
		||||
				// セキュリティキーがなければパスワードレスを有効にはできない
 | 
			
		||||
				const keyCount = await this.userSecurityKeysRepository.count({
 | 
			
		||||
					where: {
 | 
			
		||||
						userId: me.id,
 | 
			
		||||
					},
 | 
			
		||||
					select: {
 | 
			
		||||
						id: true,
 | 
			
		||||
						name: true,
 | 
			
		||||
						lastUsed: true,
 | 
			
		||||
					},
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (keyCount === 0) {
 | 
			
		||||
					await this.userProfilesRepository.update(me.id, {
 | 
			
		||||
						usePasswordLessLogin: false,
 | 
			
		||||
					});
 | 
			
		||||
 | 
			
		||||
					throw new ApiError(meta.errors.noKey);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await this.userProfilesRepository.update(me.id, {
 | 
			
		||||
				usePasswordLessLogin: ps.value,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Publish meUpdated event
 | 
			
		||||
			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
				includeSecrets: true,
 | 
			
		||||
			}));
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import bcrypt from 'bcryptjs';
 | 
			
		||||
import * as speakeasy from 'speakeasy';
 | 
			
		||||
import * as OTPAuth from 'otpauth';
 | 
			
		||||
import * as QRCode from 'qrcode';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { UserProfilesRepository } from '@/models/index.js';
 | 
			
		||||
@@ -42,25 +42,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Generate user's secret key
 | 
			
		||||
			const secret = speakeasy.generateSecret({
 | 
			
		||||
				length: 32,
 | 
			
		||||
			});
 | 
			
		||||
			const secret = new OTPAuth.Secret();
 | 
			
		||||
 | 
			
		||||
			await this.userProfilesRepository.update(me.id, {
 | 
			
		||||
				twoFactorTempSecret: secret.base32,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Get the data URL of the authenticator URL
 | 
			
		||||
			const url = speakeasy.otpauthURL({
 | 
			
		||||
				secret: secret.base32,
 | 
			
		||||
				encoding: 'base32',
 | 
			
		||||
			const totp = new OTPAuth.TOTP({
 | 
			
		||||
				secret,
 | 
			
		||||
				digits: 6,
 | 
			
		||||
				label: me.username,
 | 
			
		||||
				issuer: this.config.host,
 | 
			
		||||
			});
 | 
			
		||||
			const dataUrl = await QRCode.toDataURL(url);
 | 
			
		||||
			const url = totp.toString();
 | 
			
		||||
			const qr = await QRCode.toDataURL(url);
 | 
			
		||||
 | 
			
		||||
			return {
 | 
			
		||||
				qr: dataUrl,
 | 
			
		||||
				qr,
 | 
			
		||||
				url,
 | 
			
		||||
				secret: secret.base32,
 | 
			
		||||
				label: me.username,
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
				id: ps.credentialId,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// 使われているキーがなくなったらパスワードレスログインをやめる
 | 
			
		||||
			const keyCount = await this.userSecurityKeysRepository.count({
 | 
			
		||||
				where: {
 | 
			
		||||
					userId: me.id,
 | 
			
		||||
				},
 | 
			
		||||
				select: {
 | 
			
		||||
					id: true,
 | 
			
		||||
					name: true,
 | 
			
		||||
					lastUsed: true,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (keyCount === 0) {
 | 
			
		||||
				await this.userProfilesRepository.update(me.id, {
 | 
			
		||||
					usePasswordLessLogin: false,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Publish meUpdated event
 | 
			
		||||
			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import bcrypt from 'bcryptjs';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import type { UserProfilesRepository } from '@/models/index.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
@@ -24,6 +26,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.userProfilesRepository)
 | 
			
		||||
		private userProfilesRepository: UserProfilesRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
 | 
			
		||||
@@ -38,7 +43,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
			await this.userProfilesRepository.update(me.id, {
 | 
			
		||||
				twoFactorSecret: null,
 | 
			
		||||
				twoFactorEnabled: false,
 | 
			
		||||
				usePasswordLessLogin: false,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Publish meUpdated event
 | 
			
		||||
			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
				includeSecrets: true,
 | 
			
		||||
			}));
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,78 @@
 | 
			
		||||
import bcrypt from 'bcryptjs';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	secure: true,
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchKey: {
 | 
			
		||||
			message: 'No such key.',
 | 
			
		||||
			code: 'NO_SUCH_KEY',
 | 
			
		||||
			id: 'f9c5467f-d492-4d3c-9a8g-a70dacc86512',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		accessDenied: {
 | 
			
		||||
			message: 'You do not have edit privilege of the channel.',
 | 
			
		||||
			code: 'ACCESS_DENIED',
 | 
			
		||||
			id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		name: { type: 'string', minLength: 1, maxLength: 30 },
 | 
			
		||||
		credentialId: { type: 'string' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['name', 'credentialId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.userSecurityKeysRepository)
 | 
			
		||||
		private userSecurityKeysRepository: UserSecurityKeysRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userProfilesRepository)
 | 
			
		||||
		private userProfilesRepository: UserProfilesRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const key = await this.userSecurityKeysRepository.findOneBy({
 | 
			
		||||
				id: ps.credentialId,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (key == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchKey);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (key.userId !== me.id) {
 | 
			
		||||
				throw new ApiError(meta.errors.accessDenied);
 | 
			
		||||
			}
 | 
			
		||||
	
 | 
			
		||||
			await this.userSecurityKeysRepository.update(key.id, {
 | 
			
		||||
				name: ps.name,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Publish meUpdated event
 | 
			
		||||
			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
				includeSecrets: true,
 | 
			
		||||
			}));
 | 
			
		||||
 | 
			
		||||
			return {};
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user