parent
e8177ee311
commit
0ad7869249
11 changed files with 68 additions and 6 deletions
|
@ -26,6 +26,7 @@
|
||||||
- ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。
|
- ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。
|
||||||
* デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。
|
* デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。
|
||||||
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
||||||
|
- 管理者が予約ユーザー名を設定できるようになりました。
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- 通知の表示をカスタマイズできるように
|
- 通知の表示をカスタマイズできるように
|
||||||
|
|
|
@ -1022,6 +1022,8 @@ serverRules: "サーバールール"
|
||||||
pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。"
|
pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。"
|
||||||
pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。"
|
pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。"
|
||||||
continue: "続ける"
|
continue: "続ける"
|
||||||
|
preservedUsernames: "予約ユーザー名"
|
||||||
|
preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成自はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
|
||||||
|
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||||
|
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,9 @@ import { UsedUsername } from '@/models/entities/UsedUsername.js';
|
||||||
import generateUserToken from '@/misc/generate-native-user-token.js';
|
import generateUserToken from '@/misc/generate-native-user-token.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import UsersChart from './chart/charts/users.js';
|
import UsersChart from '@/core/chart/charts/users.js';
|
||||||
import { UtilityService } from './UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SignupService {
|
export class SignupService {
|
||||||
|
@ -34,6 +35,7 @@ export class SignupService {
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private metaService: MetaService,
|
||||||
private usersChart: UsersChart,
|
private usersChart: UsersChart,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -44,6 +46,7 @@ export class SignupService {
|
||||||
password?: string | null;
|
password?: string | null;
|
||||||
passwordHash?: UserProfile['password'] | null;
|
passwordHash?: UserProfile['password'] | null;
|
||||||
host?: string | null;
|
host?: string | null;
|
||||||
|
ignorePreservedUsernames?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { username, password, passwordHash, host } = opts;
|
const { username, password, passwordHash, host } = opts;
|
||||||
let hash = passwordHash;
|
let hash = passwordHash;
|
||||||
|
@ -76,6 +79,14 @@ export class SignupService {
|
||||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
||||||
throw new Error('USED_USERNAME');
|
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<string[]>((res, rej) =>
|
const keyPair = await new Promise<string[]>((res, rej) =>
|
||||||
generateKeyPair('rsa', {
|
generateKeyPair('rsa', {
|
||||||
|
|
|
@ -412,4 +412,9 @@ export class Meta {
|
||||||
default: '{}',
|
default: '{}',
|
||||||
})
|
})
|
||||||
public serverRules: string[];
|
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[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import rndstr from 'rndstr';
|
import rndstr from 'rndstr';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.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 { bindThis } from '@/decorators.js';
|
||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import { IsNull } from 'typeorm';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SignupApiService {
|
export class SignupApiService {
|
||||||
|
@ -137,6 +137,11 @@ export class SignupApiService {
|
||||||
throw new FastifyReplyError(400, 'USED_USERNAME');
|
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);
|
const code = rndstr('a-z0-9', 16);
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
|
|
|
@ -52,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
const { account, secret } = await this.signupService.signup({
|
const { account, secret } = await this.signupService.signup({
|
||||||
username: ps.username,
|
username: ps.username,
|
||||||
password: ps.password,
|
password: ps.password,
|
||||||
|
ignorePreservedUsernames: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await this.userEntityService.pack(account, account, {
|
const res = await this.userEntityService.pack(account, account, {
|
||||||
|
|
|
@ -118,6 +118,14 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
preservedUsernames: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
hcaptchaSecretKey: {
|
hcaptchaSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
@ -311,6 +319,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
hiddenTags: instance.hiddenTags,
|
hiddenTags: instance.hiddenTags,
|
||||||
blockedHosts: instance.blockedHosts,
|
blockedHosts: instance.blockedHosts,
|
||||||
sensitiveWords: instance.sensitiveWords,
|
sensitiveWords: instance.sensitiveWords,
|
||||||
|
preservedUsernames: instance.preservedUsernames,
|
||||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||||
turnstileSecretKey: instance.turnstileSecretKey,
|
turnstileSecretKey: instance.turnstileSecretKey,
|
||||||
|
|
|
@ -95,6 +95,7 @@ export const paramDef = {
|
||||||
enableChartsForRemoteUser: { type: 'boolean' },
|
enableChartsForRemoteUser: { type: 'boolean' },
|
||||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||||
serverRules: { type: 'array', items: { type: 'string' } },
|
serverRules: { type: 'array', items: { type: 'string' } },
|
||||||
|
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -392,6 +393,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
set.serverRules = ps.serverRules;
|
set.serverRules = ps.serverRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.preservedUsernames !== undefined) {
|
||||||
|
set.preservedUsernames = ps.preservedUsernames;
|
||||||
|
}
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
this.moderationLogService.insertModerationLog(me, 'updateMeta');
|
this.moderationLogService.insertModerationLog(me, 'updateMeta');
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { UsedUsernamesRepository, UsersRepository } from '@/models/index.js
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { localUsernameSchema } from '@/models/entities/User.js';
|
import { localUsernameSchema } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
|
@ -39,9 +40,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
@Inject(DI.usedUsernamesRepository)
|
@Inject(DI.usedUsernamesRepository)
|
||||||
private usedUsernamesRepository: UsedUsernamesRepository,
|
private usedUsernamesRepository: UsedUsernamesRepository,
|
||||||
|
|
||||||
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Get exist
|
|
||||||
const exist = await this.usersRepository.countBy({
|
const exist = await this.usersRepository.countBy({
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
usernameLower: ps.username.toLowerCase(),
|
usernameLower: ps.username.toLowerCase(),
|
||||||
|
@ -49,8 +51,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() });
|
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 {
|
return {
|
||||||
available: exist === 0 && exist2 === 0,
|
available: exist === 0 && exist2 === 0 && !isPreserved,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
<MkTextarea v-model="preservedUsernames">
|
||||||
|
<template #label>{{ i18n.ts.preservedUsernames }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
|
||||||
|
</MkTextarea>
|
||||||
<MkTextarea v-model="sensitiveWords">
|
<MkTextarea v-model="sensitiveWords">
|
||||||
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
||||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template>
|
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template>
|
||||||
|
@ -46,14 +50,16 @@ import { fetchInstance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
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 sensitiveWords: string = $ref('');
|
||||||
|
let preservedUsernames: string = $ref('');
|
||||||
let tosUrl: string | null = $ref(null);
|
let tosUrl: string | null = $ref(null);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
sensitiveWords = meta.sensitiveWords.join('\n');
|
sensitiveWords = meta.sensitiveWords.join('\n');
|
||||||
|
preservedUsernames = meta.preservedUsernames.join('\n');
|
||||||
tosUrl = meta.tosUrl;
|
tosUrl = meta.tosUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +67,7 @@ function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
tosUrl,
|
tosUrl,
|
||||||
sensitiveWords: sensitiveWords.split('\n'),
|
sensitiveWords: sensitiveWords.split('\n'),
|
||||||
|
preservedUsernames: preservedUsernames.split('\n'),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue