From 0ad7869249c8594277afc0aa707c05ac2ed633cf Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 29 Apr 2023 17:03:14 +0900 Subject: [PATCH] feat: preserved usernames Resolve #10704 --- CHANGELOG.md | 1 + locales/ja-JP.yml | 2 ++ .../migration/1682754135458-preservedUsernames.js | 11 +++++++++++ packages/backend/src/core/SignupService.ts | 15 +++++++++++++-- packages/backend/src/models/entities/Meta.ts | 5 +++++ .../backend/src/server/api/SignupApiService.ts | 7 ++++++- .../server/api/endpoints/admin/accounts/create.ts | 1 + .../src/server/api/endpoints/admin/meta.ts | 9 +++++++++ .../src/server/api/endpoints/admin/update-meta.ts | 5 +++++ .../server/api/endpoints/username/available.ts | 9 +++++++-- packages/frontend/src/pages/admin/moderation.vue | 9 ++++++++- 11 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 packages/backend/migration/1682754135458-preservedUsernames.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 210b1ed5f..4704c8050 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。 * デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。 - カスタム絵文字のライセンスを複数でセットできるようになりました。 +- 管理者が予約ユーザー名を設定できるようになりました。 ### Client - 通知の表示をカスタマイズできるように diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1e2019b8f..5e2e83fb1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1022,6 +1022,8 @@ serverRules: "サーバールール" pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。" pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。" continue: "続ける" +preservedUsernames: "予約ユーザー名" +preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成自はこの制限を受けません。また、既に存在するアカウントも影響を受けません。" _serverRules: description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。" diff --git a/packages/backend/migration/1682754135458-preservedUsernames.js b/packages/backend/migration/1682754135458-preservedUsernames.js new file mode 100644 index 000000000..46a0826f4 --- /dev/null +++ b/packages/backend/migration/1682754135458-preservedUsernames.js @@ -0,0 +1,11 @@ +export class PreservedUsernames1682754135458 { + name = 'PreservedUsernames1682754135458' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "preservedUsernames" character varying(1024) array NOT NULL DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "preservedUsernames"`); + } +} diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index d7bc05b8b..2b8387f89 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -13,8 +13,9 @@ import { UsedUsername } from '@/models/entities/UsedUsername.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import UsersChart from './chart/charts/users.js'; -import { UtilityService } from './UtilityService.js'; +import UsersChart from '@/core/chart/charts/users.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { MetaService } from '@/core/MetaService.js'; @Injectable() export class SignupService { @@ -34,6 +35,7 @@ export class SignupService { private utilityService: UtilityService, private userEntityService: UserEntityService, private idService: IdService, + private metaService: MetaService, private usersChart: UsersChart, ) { } @@ -44,6 +46,7 @@ export class SignupService { password?: string | null; passwordHash?: UserProfile['password'] | null; host?: string | null; + ignorePreservedUsernames?: boolean; }) { const { username, password, passwordHash, host } = opts; let hash = passwordHash; @@ -76,6 +79,14 @@ export class SignupService { if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { throw new Error('USED_USERNAME'); } + + if (!opts.ignorePreservedUsernames) { + const instance = await this.metaService.fetch(true); + const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + if (isPreserved) { + throw new Error('USED_USERNAME'); + } + } const keyPair = await new Promise((res, rej) => generateKeyPair('rsa', { diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index c8df141a0..6d44e4edc 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -412,4 +412,9 @@ export class Meta { default: '{}', }) public serverRules: string[]; + + @Column('varchar', { + length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', + }) + public preservedUsernames: string[]; } diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index fbabf47af..f44e71771 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import rndstr from 'rndstr'; import bcrypt from 'bcryptjs'; +import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -15,7 +16,6 @@ import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { bindThis } from '@/decorators.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; -import { IsNull } from 'typeorm'; @Injectable() export class SignupApiService { @@ -137,6 +137,11 @@ export class SignupApiService { throw new FastifyReplyError(400, 'USED_USERNAME'); } + const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + if (isPreserved) { + throw new FastifyReplyError(400, 'USED_USERNAME'); + } + const code = rndstr('a-z0-9', 16); // Generate hash of password diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index bac8ae16e..8a3541dff 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -52,6 +52,7 @@ export default class extends Endpoint { const { account, secret } = await this.signupService.signup({ username: ps.username, password: ps.password, + ignorePreservedUsernames: true, }); const res = await this.userEntityService.pack(account, account, { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index fc318a621..87a2d22ac 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -118,6 +118,14 @@ export const meta = { optional: false, nullable: false, }, }, + preservedUsernames: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, hcaptchaSecretKey: { type: 'string', optional: true, nullable: true, @@ -311,6 +319,7 @@ export default class extends Endpoint { hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, sensitiveWords: instance.sensitiveWords, + preservedUsernames: instance.preservedUsernames, hcaptchaSecretKey: instance.hcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index ae2fc84b5..0e94f56cf 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -95,6 +95,7 @@ export const paramDef = { enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, serverRules: { type: 'array', items: { type: 'string' } }, + preservedUsernames: { type: 'array', items: { type: 'string' } }, }, required: [], } as const; @@ -392,6 +393,10 @@ export default class extends Endpoint { set.serverRules = ps.serverRules; } + if (ps.preservedUsernames !== undefined) { + set.preservedUsernames = ps.preservedUsernames; + } + await this.metaService.update(set); this.moderationLogService.insertModerationLog(me, 'updateMeta'); }); diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index c80b6efdc..6293c5cb5 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -4,6 +4,7 @@ import type { UsedUsernamesRepository, UsersRepository } from '@/models/index.js import { Endpoint } from '@/server/api/endpoint-base.js'; import { localUsernameSchema } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; +import { MetaService } from '@/core/MetaService.js'; export const meta = { tags: ['users'], @@ -39,9 +40,10 @@ export default class extends Endpoint { @Inject(DI.usedUsernamesRepository) private usedUsernamesRepository: UsedUsernamesRepository, + + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { - // Get exist const exist = await this.usersRepository.countBy({ host: IsNull(), usernameLower: ps.username.toLowerCase(), @@ -49,8 +51,11 @@ export default class extends Endpoint { const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() }); + const meta = await this.metaService.fetch(); + const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase()); + return { - available: exist === 0 && exist2 === 0, + available: exist === 0 && exist2 === 0 && !isPreserved, }; }); } diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index e7e3cb536..1ee07d383 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -12,6 +12,10 @@ + + + + @@ -46,14 +50,16 @@ import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkButton from '@/components/MkButton.vue'; -import FormLink from "@/components/form/link.vue"; +import FormLink from '@/components/form/link.vue'; let sensitiveWords: string = $ref(''); +let preservedUsernames: string = $ref(''); let tosUrl: string | null = $ref(null); async function init() { const meta = await os.api('admin/meta'); sensitiveWords = meta.sensitiveWords.join('\n'); + preservedUsernames = meta.preservedUsernames.join('\n'); tosUrl = meta.tosUrl; } @@ -61,6 +67,7 @@ function save() { os.apiWithDialog('admin/update-meta', { tosUrl, sensitiveWords: sensitiveWords.split('\n'), + preservedUsernames: preservedUsernames.split('\n'), }).then(() => { fetchInstance(); });