merge: approve signup system (#93)
This commit is contained in:
commit
887f422511
33 changed files with 378 additions and 31 deletions
|
@ -145,6 +145,7 @@ unblockConfirm: "Are you sure that you want to unblock this account?"
|
|||
nsfwConfirm: "Are you sure that you want to mark all media from this account as NSFW?"
|
||||
unNsfwConfirm: "Are you sure that you want to unmark all media from this account as NSFW?"
|
||||
suspendConfirm: "Are you sure that you want to suspend this account?"
|
||||
approveConfirm: "Are you sure that you want to approve this account?"
|
||||
unsuspendConfirm: "Are you sure that you want to unsuspend this account?"
|
||||
selectList: "Select a list"
|
||||
editList: "Edit list"
|
||||
|
@ -870,6 +871,7 @@ itsOff: "Disabled"
|
|||
on: "On"
|
||||
off: "Off"
|
||||
emailRequiredForSignup: "Require email address for sign-up"
|
||||
approvalRequiredForSignup: "Require approval for new users"
|
||||
unread: "Unread"
|
||||
filter: "Filter"
|
||||
controlPanel: "Control Panel"
|
||||
|
@ -928,6 +930,11 @@ requireAdminForView: "You must log in with an administrator account to view this
|
|||
isSystemAccount: "An account created and automatically operated by the system."
|
||||
typeToConfirm: "Please enter {x} to confirm"
|
||||
deleteAccount: "Delete account"
|
||||
approveAccount: "Approve"
|
||||
denyAccount: "Deny & Delete"
|
||||
approved: "Approved"
|
||||
notApproved: "Not Approved"
|
||||
approvalStatus: "Approval Status"
|
||||
document: "Documentation"
|
||||
numberOfPageCache: "Number of cached pages"
|
||||
numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device."
|
||||
|
@ -1019,6 +1026,7 @@ disableFederationConfirm: "Really disable federation?"
|
|||
disableFederationConfirmWarn: "Even if defederated, posts will continue to be public unless set otherwise. You usually do not need to do this."
|
||||
disableFederationOk: "Disable"
|
||||
invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up."
|
||||
approvalRequiredToRegister: "This instance is only accepting users who specify a reason for registration."
|
||||
emailNotSupported: "This instance does not support sending emails"
|
||||
postToTheChannel: "Post to channel"
|
||||
cannotBeChangedLater: "This cannot be changed later."
|
||||
|
@ -1544,6 +1552,8 @@ _signup:
|
|||
almostThere: "Almost there"
|
||||
emailAddressInfo: "Please enter your email address. It will not be made public."
|
||||
emailSent: "A confirmation email has been sent to your email address ({email}). Please click the included link to complete account creation."
|
||||
approvalPending: "Your account has been created and is awaiting approval."
|
||||
reasonInfo: "Please enter a reason as to why you want to join the instance."
|
||||
_accountDelete:
|
||||
accountDelete: "Delete account"
|
||||
mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded."
|
||||
|
|
10
locales/index.d.ts
vendored
10
locales/index.d.ts
vendored
|
@ -148,6 +148,7 @@ export interface Locale {
|
|||
"nsfwConfirm": string;
|
||||
"unNsfwConfirm": string;
|
||||
"suspendConfirm": string;
|
||||
"approveConfirm": string;
|
||||
"unsuspendConfirm": string;
|
||||
"selectList": string;
|
||||
"editList": string;
|
||||
|
@ -873,6 +874,7 @@ export interface Locale {
|
|||
"on": string;
|
||||
"off": string;
|
||||
"emailRequiredForSignup": string;
|
||||
"approvalRequiredForSignup": string;
|
||||
"unread": string;
|
||||
"filter": string;
|
||||
"controlPanel": string;
|
||||
|
@ -931,6 +933,11 @@ export interface Locale {
|
|||
"isSystemAccount": string;
|
||||
"typeToConfirm": string;
|
||||
"deleteAccount": string;
|
||||
"approveAccount": string;
|
||||
"denyAccount": string;
|
||||
"approved": string;
|
||||
"notApproved": string;
|
||||
"approvalStatus": string;
|
||||
"document": string;
|
||||
"numberOfPageCache": string;
|
||||
"numberOfPageCacheDescription": string;
|
||||
|
@ -1022,6 +1029,7 @@ export interface Locale {
|
|||
"disableFederationConfirmWarn": string;
|
||||
"disableFederationOk": string;
|
||||
"invitationRequiredToRegister": string;
|
||||
"approvalRequiredToRegister": string;
|
||||
"emailNotSupported": string;
|
||||
"postToTheChannel": string;
|
||||
"cannotBeChangedLater": string;
|
||||
|
@ -1636,6 +1644,8 @@ export interface Locale {
|
|||
"almostThere": string;
|
||||
"emailAddressInfo": string;
|
||||
"emailSent": string;
|
||||
"approvalPending": string;
|
||||
"reasonInfo": string;
|
||||
};
|
||||
"_accountDelete": {
|
||||
"accountDelete": string;
|
||||
|
|
|
@ -145,6 +145,7 @@ unblockConfirm: "ブロック解除しますか?"
|
|||
nsfwConfirm: "このアカウントからのすべてのメディアをNSFWとしてマークしてもよろしいですか?"
|
||||
unNsfwConfirm: "このアカウントのすべてのメディアをNSFWとしてマーク解除してもよろしいですか?"
|
||||
suspendConfirm: "凍結しますか?"
|
||||
approveConfirm: "このアカウントを承認してもよろしいですか?"
|
||||
unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
editList: "リストを編集"
|
||||
|
@ -870,6 +871,7 @@ itsOff: "オフになっています"
|
|||
on: "オン"
|
||||
off: "オフ"
|
||||
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
|
||||
approvalRequiredForSignup: "新規ユーザーの承認が必要"
|
||||
unread: "未読"
|
||||
filter: "フィルタ"
|
||||
controlPanel: "コントロールパネル"
|
||||
|
@ -928,6 +930,11 @@ requireAdminForView: "閲覧するには管理者アカウントでログイン
|
|||
isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
|
||||
typeToConfirm: "この操作を行うには {x} と入力してください"
|
||||
deleteAccount: "アカウント削除"
|
||||
approveAccount: "承認する"
|
||||
denyAccount: "拒否と削除"
|
||||
approved: "承認済み"
|
||||
notApproved: "承認されていない"
|
||||
approvalStatus: "承認状況"
|
||||
document: "ドキュメント"
|
||||
numberOfPageCache: "ページキャッシュ数"
|
||||
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
|
||||
|
@ -1019,6 +1026,7 @@ disableFederationConfirm: "連合なしにしますか?"
|
|||
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
|
||||
disableFederationOk: "連合なしにする"
|
||||
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
||||
approvalRequiredToRegister: "このインスタンスは、登録理由を指定したユーザーのみを受け入れています。"
|
||||
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
||||
postToTheChannel: "チャンネルに投稿"
|
||||
cannotBeChangedLater: "後から変更できません。"
|
||||
|
@ -1555,6 +1563,8 @@ _signup:
|
|||
almostThere: "ほとんど完了です"
|
||||
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
|
||||
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。"
|
||||
approvalPending: "アカウントが作成され、承認待ちの状態です。"
|
||||
reasonInfo: "インスタンスに参加したい理由を入力してください。"
|
||||
|
||||
_accountDelete:
|
||||
accountDelete: "アカウントの削除"
|
||||
|
|
22
packages/backend/migration/1697580470000-approvalSignup.js
Normal file
22
packages/backend/migration/1697580470000-approvalSignup.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ApprovalSignup1697580470000 {
|
||||
name = 'ApprovalSignup1697580470000'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "approvalRequiredForSignup" boolean DEFAULT false NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "approved" boolean DEFAULT false NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "signupReason" character varying(1000) NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "user_pending" ADD "reason" character varying(1000) NULL`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "approvalRequiredForSignup"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "approved"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "signupReason"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_pending" DROP COLUMN "reason"`);
|
||||
}
|
||||
}
|
|
@ -48,10 +48,12 @@ export class SignupService {
|
|||
password?: string | null;
|
||||
passwordHash?: MiUserProfile['password'] | null;
|
||||
host?: string | null;
|
||||
reason?: string | null;
|
||||
ignorePreservedUsernames?: boolean;
|
||||
}) {
|
||||
const { username, password, passwordHash, host } = opts;
|
||||
const { username, password, passwordHash, host, reason } = opts;
|
||||
let hash = passwordHash;
|
||||
const instance = await this.metaService.fetch(true);
|
||||
|
||||
// Validate username
|
||||
if (!this.userEntityService.validateLocalUsername(username)) {
|
||||
|
@ -85,7 +87,6 @@ export class SignupService {
|
|||
const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0;
|
||||
|
||||
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
||||
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');
|
||||
|
@ -110,6 +111,9 @@ export class SignupService {
|
|||
));
|
||||
|
||||
let account!: MiUser;
|
||||
let defaultApproval = false;
|
||||
|
||||
if (!instance.approvalRequiredForSignup) defaultApproval = true;
|
||||
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
|
@ -127,6 +131,8 @@ export class SignupService {
|
|||
host: this.utilityService.toPunyNullable(host),
|
||||
token: secret,
|
||||
isRoot: isTheFirstUser,
|
||||
approved: defaultApproval,
|
||||
signupReason: reason,
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new MiUserKeypair({
|
||||
|
|
|
@ -316,6 +316,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
alsoKnownAs: person.alsoKnownAs,
|
||||
isExplorable: person.discoverable,
|
||||
username: person.preferredUsername,
|
||||
approved: true,
|
||||
usernameLower: person.preferredUsername?.toLowerCase(),
|
||||
host,
|
||||
inbox: person.inbox,
|
||||
|
|
|
@ -489,6 +489,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
...(opts.includeSecrets ? {
|
||||
email: profile!.email,
|
||||
emailVerified: profile!.emailVerified,
|
||||
approved: user.approved,
|
||||
signupReason: user.signupReason,
|
||||
securityKeysList: profile!.twoFactorEnabled
|
||||
? this.userSecurityKeysRepository.find({
|
||||
where: {
|
||||
|
|
|
@ -174,6 +174,11 @@ export class MiMeta {
|
|||
})
|
||||
public emailRequiredForSignup: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public approvalRequiredForSignup: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
@ -278,6 +278,16 @@ export class MiUser {
|
|||
})
|
||||
public token: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public approved: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1000, nullable: true,
|
||||
})
|
||||
public signupReason: string | null;
|
||||
|
||||
constructor(data: Partial<MiUser>) {
|
||||
if (data == null) return;
|
||||
|
||||
|
|
|
@ -31,4 +31,9 @@ export class MiUserPending {
|
|||
length: 128,
|
||||
})
|
||||
public password: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1000,
|
||||
})
|
||||
public reason: string;
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
|
|||
import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
|
||||
import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
|
||||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
||||
import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
|
||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||
|
@ -423,6 +424,7 @@ const $admin_unnsfwUser: Provider = { provide: 'ep:admin/unnsfw-user', useClass:
|
|||
const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClass: ep___admin_silenceUser.default };
|
||||
const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default };
|
||||
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
|
||||
const $admin_approveUser: Provider = { provide: 'ep:admin/approve-user', useClass: ep___admin_approveUser.default };
|
||||
const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
|
||||
const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
|
||||
const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default };
|
||||
|
@ -784,6 +786,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
|||
$admin_silenceUser,
|
||||
$admin_unsilenceUser,
|
||||
$admin_suspendUser,
|
||||
$admin_approveUser,
|
||||
$admin_unsuspendUser,
|
||||
$admin_updateMeta,
|
||||
$admin_deleteAccount,
|
||||
|
@ -1139,6 +1142,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
|||
$admin_silenceUser,
|
||||
$admin_unsilenceUser,
|
||||
$admin_suspendUser,
|
||||
$admin_approveUser,
|
||||
$admin_unsuspendUser,
|
||||
$admin_updateMeta,
|
||||
$admin_deleteAccount,
|
||||
|
|
|
@ -21,6 +21,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
|
||||
|
@ -46,6 +47,7 @@ export class SigninApiService {
|
|||
private signinService: SigninService,
|
||||
private userAuthService: UserAuthService,
|
||||
private webAuthnService: WebAuthnService,
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -64,6 +66,8 @@ export class SigninApiService {
|
|||
reply.header('Access-Control-Allow-Origin', this.config.url);
|
||||
reply.header('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
const instance = await this.metaService.fetch(true);
|
||||
|
||||
const body = request.body;
|
||||
const username = body['username'];
|
||||
const password = body['password'];
|
||||
|
@ -123,6 +127,17 @@ export class SigninApiService {
|
|||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
if (!user.approved && instance.approvalRequiredForSignup) {
|
||||
reply.code(403);
|
||||
return {
|
||||
error: {
|
||||
message: 'The account has not been approved by an admin yet. Try again later.',
|
||||
code: 'NOT_APPROVED',
|
||||
id: '22d05606-fbcf-421a-a2db-b32241faft1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Compare password
|
||||
const same = await argon2.verify(profile.password!, password) || bcrypt.compareSync(password, profile.password!);
|
||||
|
||||
|
@ -147,6 +162,8 @@ export class SigninApiService {
|
|||
password: newHash
|
||||
});
|
||||
}
|
||||
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else {
|
||||
return await fail(403, {
|
||||
|
@ -176,6 +193,8 @@ export class SigninApiService {
|
|||
});
|
||||
}
|
||||
|
||||
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else if (body.credential) {
|
||||
if (!same && !profile.usePasswordLessLogin) {
|
||||
|
@ -187,6 +206,7 @@ export class SigninApiService {
|
|||
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
|
||||
|
||||
if (authorized) {
|
||||
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else {
|
||||
return await fail(403, {
|
||||
|
|
|
@ -22,6 +22,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import instance from './endpoints/charts/instance.js';
|
||||
|
||||
@Injectable()
|
||||
export class SignupApiService {
|
||||
|
@ -63,6 +64,7 @@ export class SignupApiService {
|
|||
host?: string;
|
||||
invitationCode?: string;
|
||||
emailAddress?: string;
|
||||
reason?: string;
|
||||
'hcaptcha-response'?: string;
|
||||
'g-recaptcha-response'?: string;
|
||||
'turnstile-response'?: string;
|
||||
|
@ -100,6 +102,7 @@ export class SignupApiService {
|
|||
const password = body['password'];
|
||||
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null;
|
||||
const invitationCode = body['invitationCode'];
|
||||
const reason = body['reason'];
|
||||
const emailAddress = body['emailAddress'];
|
||||
|
||||
if (instance.emailRequiredForSignup) {
|
||||
|
@ -115,6 +118,13 @@ export class SignupApiService {
|
|||
}
|
||||
}
|
||||
|
||||
if (instance.approvalRequiredForSignup) {
|
||||
if (reason == null || typeof reason !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let ticket: MiRegistrationTicket | null = null;
|
||||
|
||||
if (instance.disableRegistration) {
|
||||
|
@ -170,6 +180,7 @@ export class SignupApiService {
|
|||
email: emailAddress!,
|
||||
username: username,
|
||||
password: hash,
|
||||
reason: reason,
|
||||
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const link = `${this.config.url}/signup-complete/${code}`;
|
||||
|
@ -185,6 +196,19 @@ export class SignupApiService {
|
|||
});
|
||||
}
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else if (instance.approvalRequiredForSignup) {
|
||||
await this.signupService.signup({
|
||||
username, password, host, reason,
|
||||
});
|
||||
|
||||
if (emailAddress) {
|
||||
this.emailService.sendEmail(emailAddress, 'Approval pending',
|
||||
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.',
|
||||
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.');
|
||||
}
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else {
|
||||
|
@ -222,12 +246,15 @@ export class SignupApiService {
|
|||
|
||||
const code = body['code'];
|
||||
|
||||
const instance = await this.metaService.fetch(true);
|
||||
|
||||
try {
|
||||
const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code });
|
||||
|
||||
const { account, secret } = await this.signupService.signup({
|
||||
username: pendingUser.username,
|
||||
passwordHash: pendingUser.password,
|
||||
reason: pendingUser.reason,
|
||||
});
|
||||
|
||||
this.userPendingsRepository.delete({
|
||||
|
@ -251,6 +278,15 @@ export class SignupApiService {
|
|||
});
|
||||
}
|
||||
|
||||
if (instance.approvalRequiredForSignup) {
|
||||
if (pendingUser.email) {
|
||||
this.emailService.sendEmail(pendingUser.email, 'Approval pending',
|
||||
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.',
|
||||
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.');
|
||||
}
|
||||
return { pendingApproval: true };
|
||||
}
|
||||
|
||||
return this.signinService.signin(request, reply, account as MiLocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
|
|
|
@ -66,6 +66,7 @@ import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
|
|||
import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
|
||||
import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
|
||||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
||||
import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
|
||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||
|
@ -421,6 +422,7 @@ const eps = [
|
|||
['admin/silence-user', ep___admin_silenceUser],
|
||||
['admin/unsilence-user', ep___admin_unsilenceUser],
|
||||
['admin/suspend-user', ep___admin_suspendUser],
|
||||
['admin/approve-user', ep___admin_approveUser],
|
||||
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
||||
['admin/update-meta', ep___admin_updateMeta],
|
||||
['admin/delete-account', ep___admin_deleteAccount],
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private emailService: EmailService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
||||
if (user == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
approved: true,
|
||||
});
|
||||
|
||||
if (profile?.email) {
|
||||
this.emailService.sendEmail(profile.email, 'Account Approved',
|
||||
'Your Account has been approved have fun socializing!',
|
||||
'Your Account has been approved have fun socializing!');
|
||||
}
|
||||
|
||||
this.moderationLogService.log(me, 'approve', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -32,6 +32,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
approvalRequiredForSignup: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableHcaptcha: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -357,6 +361,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
privacyPolicyUrl: instance.privacyPolicyUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableRecaptcha: instance.enableRecaptcha,
|
||||
|
|
|
@ -73,6 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return {
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
approved: user.approved,
|
||||
signupReason: user.signupReason,
|
||||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||
noCrawle: profile.noCrawle,
|
||||
preventAiLearning: profile.preventAiLearning,
|
||||
|
|
|
@ -34,7 +34,7 @@ export const paramDef = {
|
|||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+lastActiveDate', '-lastActiveDate'] },
|
||||
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended'], default: 'all' },
|
||||
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended', 'approved'], default: 'all' },
|
||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
|
||||
username: { type: 'string', nullable: true, default: null },
|
||||
hostname: {
|
||||
|
@ -63,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
case 'available': query.where('user.isSuspended = FALSE'); break;
|
||||
case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
|
||||
case 'suspended': query.where('user.isSuspended = TRUE'); break;
|
||||
case 'approved': query.where('user.approved = FALSE'); break;
|
||||
case 'admin': {
|
||||
const adminIds = await this.roleService.getAdministratorIds();
|
||||
if (adminIds.length === 0) return [];
|
||||
|
|
|
@ -59,6 +59,7 @@ export const paramDef = {
|
|||
cacheRemoteFiles: { type: 'boolean' },
|
||||
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
||||
emailRequiredForSignup: { type: 'boolean' },
|
||||
approvalRequiredForSignup: { type: 'boolean' },
|
||||
enableHcaptcha: { type: 'boolean' },
|
||||
hcaptchaSiteKey: { type: 'string', nullable: true },
|
||||
hcaptchaSecretKey: { type: 'string', nullable: true },
|
||||
|
@ -250,6 +251,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
||||
}
|
||||
|
||||
if (ps.approvalRequiredForSignup !== undefined) {
|
||||
set.approvalRequiredForSignup = ps.approvalRequiredForSignup;
|
||||
}
|
||||
|
||||
if (ps.enableHcaptcha !== undefined) {
|
||||
set.enableHcaptcha = ps.enableHcaptcha;
|
||||
}
|
||||
|
|
|
@ -100,6 +100,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
approvalRequiredForSignup: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableHcaptcha: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -308,6 +312,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
privacyPolicyUrl: instance.privacyPolicyUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableRecaptcha: instance.enableRecaptcha,
|
||||
|
|
|
@ -28,7 +28,7 @@ export async function getInstance(
|
|||
thumbnail: meta.backgroundImageUrl || '/static-assets/transparent.png',
|
||||
languages: meta.langs,
|
||||
registrations: !meta.disableRegistration || response.registrations,
|
||||
approval_required: !response.registrations,
|
||||
approval_required: meta.approvalRequiredForSignup,
|
||||
invites_enabled: response.registrations,
|
||||
configuration: {
|
||||
accounts: {
|
||||
|
|
|
@ -30,6 +30,7 @@ export const ffVisibility = ['public', 'followers', 'private'] as const;
|
|||
export const moderationLogTypes = [
|
||||
'updateServerSettings',
|
||||
'suspend',
|
||||
'approve',
|
||||
'unsuspend',
|
||||
'updateUserNote',
|
||||
'addCustomEmoji',
|
||||
|
@ -72,6 +73,11 @@ export type ModerationLogPayloads = {
|
|||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
approve: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
unsuspend: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
|
|
|
@ -61,6 +61,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="instance.approvalRequiredForSignup" v-model="reason" type="text" :spellcheck="false" required data-cy-signup-reason>
|
||||
<template #label>Reason <div v-tooltip:dialog="i18n.ts._signup.reasonInfo" class="_button _help"><i class="ph-question ph-bold ph-lg"></i></div></template>
|
||||
<template #prefix><i class="ph-chalkboard-teacher ph-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
|
@ -97,6 +101,7 @@ const props = withDefaults(defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'signup', user: Record<string, any>): void;
|
||||
(ev: 'signupEmailPending'): void;
|
||||
(ev: 'approvalPending'): void;
|
||||
}>();
|
||||
|
||||
const host = toUnicode(config.host);
|
||||
|
@ -109,6 +114,7 @@ let username: string = $ref('');
|
|||
let password: string = $ref('');
|
||||
let retypedPassword: string = $ref('');
|
||||
let invitationCode: string = $ref('');
|
||||
let reason: string = $ref('');
|
||||
let email = $ref('');
|
||||
let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
|
||||
let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
|
||||
|
@ -249,6 +255,7 @@ async function onSubmit(): Promise<void> {
|
|||
password,
|
||||
emailAddress: email,
|
||||
invitationCode,
|
||||
reason,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
'turnstile-response': turnstileResponse,
|
||||
|
@ -260,6 +267,13 @@ async function onSubmit(): Promise<void> {
|
|||
text: i18n.t('_signup.emailSent', { email }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else if (instance.approvalRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.ts._signup.approvalPending,
|
||||
});
|
||||
emit('approvalPending');
|
||||
} else {
|
||||
const res = await os.api('signin', {
|
||||
username,
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending" @approvalPending="onApprovalPending"/>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@ -64,6 +64,9 @@ function onSignup(res) {
|
|||
function onSignupEmailPending() {
|
||||
dialog.close();
|
||||
}
|
||||
function onApprovalPending() {
|
||||
dialog.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -21,6 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="instance.disableRegistration" :class="$style.mainWarn">
|
||||
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
||||
</div>
|
||||
<div v-if="instance.approvalRequiredForSignup" :class="$style.mainWarn">
|
||||
<MkInfo warn>{{ i18n.ts.approvalRequiredToRegister }}</MkInfo>
|
||||
</div>
|
||||
<div class="_gaps_s" :class="$style.mainActions">
|
||||
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||
|
|
|
@ -15,6 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||
<span class="state">
|
||||
<span v-if="!approved" class="silenced">{{ i18n.ts.notApproved }}</span>
|
||||
<span v-if="approved" class="moderator">{{ i18n.ts.approved }}</span>
|
||||
<span v-if="suspended" class="suspended">Suspended</span>
|
||||
<span v-if="silenced" class="silenced">Silenced</span>
|
||||
<span v-if="moderator" class="moderator">Moderator</span>
|
||||
|
@ -178,6 +180,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkObjectView tall :value="user">
|
||||
</MkObjectView>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'approval'" class="_gaps_m">
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.approvalStatus }}</template>
|
||||
<template #value><span class="_monospace">{{ approved ? i18n.ts.approved : i18n.ts.notApproved }}</span></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<MkTextarea v-model="signupReason" readonly>
|
||||
<template #label>Reason</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkButton v-if="$i.isAdmin" inline success @click="approveAccount">{{ i18n.ts.approveAccount }}</MkButton>
|
||||
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.denyAccount }}</MkButton>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
@ -224,9 +240,12 @@ let ips = $ref(null);
|
|||
let ap = $ref(null);
|
||||
let moderator = $ref(false);
|
||||
let silenced = $ref(false);
|
||||
let approved = $ref(false);
|
||||
let suspended = $ref(false);
|
||||
let markedAsNSFW = $ref(false);
|
||||
let moderationNote = $ref('');
|
||||
let signupReason = $ref('');
|
||||
|
||||
const filesPagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
limit: 10,
|
||||
|
@ -256,8 +275,10 @@ function createFetcher() {
|
|||
ips = _ips;
|
||||
moderator = info.isModerator;
|
||||
silenced = info.isSilenced;
|
||||
approved = info.approved;
|
||||
suspended = info.isSuspended;
|
||||
moderationNote = info.moderationNote;
|
||||
signupReason = info.signupReason;
|
||||
markedAsNSFW = info.alwaysMarkNsfw;
|
||||
|
||||
watch($$(moderationNote), async () => {
|
||||
|
@ -376,6 +397,16 @@ async function deleteAccount() {
|
|||
}
|
||||
}
|
||||
|
||||
async function approveAccount() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.approveConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
await os.api('admin/approve-user', { userId: user.id });
|
||||
await refreshUser();
|
||||
}
|
||||
|
||||
async function assignRole() {
|
||||
const roles = await os.api('admin/roles/list');
|
||||
|
||||
|
@ -462,31 +493,60 @@ watch($$(user), () => {
|
|||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
const headerTabs = $computed(() => iAmAdmin && !approved ?
|
||||
[{
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'ph-info ph-bold ph-lg',
|
||||
}, {
|
||||
}, {
|
||||
key: 'roles',
|
||||
title: i18n.ts.roles,
|
||||
icon: 'ph-seal-check ph-bold pg-lg',
|
||||
}, {
|
||||
}, {
|
||||
key: 'announcements',
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ph-megaphone ph-bold ph-lg',
|
||||
}, {
|
||||
}, {
|
||||
key: 'drive',
|
||||
title: i18n.ts.drive,
|
||||
icon: 'ph-cloud ph-bold ph-lg',
|
||||
}, {
|
||||
}, {
|
||||
key: 'chart',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'ph-chart-line ph-bold pg-lg',
|
||||
}, {
|
||||
}, {
|
||||
key: 'raw',
|
||||
title: 'Raw',
|
||||
icon: 'ph-code ph-bold pg-lg',
|
||||
}]);
|
||||
}, {
|
||||
key: 'approval',
|
||||
title: 'Approval',
|
||||
icon: 'ph-eye ph-bold pg-lg',
|
||||
}] : [{
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'ph-info ph-bold ph-lg',
|
||||
}, {
|
||||
key: 'roles',
|
||||
title: i18n.ts.roles,
|
||||
icon: 'ph-seal-check ph-bold pg-lg',
|
||||
}, {
|
||||
key: 'announcements',
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ph-megaphone ph-bold ph-lg',
|
||||
}, {
|
||||
key: 'drive',
|
||||
title: i18n.ts.drive,
|
||||
icon: 'ph-cloud ph-bold ph-lg',
|
||||
}, {
|
||||
key: 'chart',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'ph-chart-line ph-bold pg-lg',
|
||||
}, {
|
||||
key: 'raw',
|
||||
title: 'Raw',
|
||||
icon: 'ph-code ph-bold pg-lg',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: user ? acct(user) : i18n.ts.userInfo,
|
||||
|
@ -577,6 +637,18 @@ definePageMetadata(computed(() => ({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.casdwq {
|
||||
.silenced {
|
||||
color: var(--warn);
|
||||
border-color: var(--warn);
|
||||
}
|
||||
|
||||
.moderator {
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -18,6 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="approvalRequiredForSignup">
|
||||
<template #label>{{ i18n.ts.approvalRequiredForSignup }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||
|
||||
<MkInput v-model="tosUrl">
|
||||
|
@ -71,6 +75,7 @@ import FormLink from '@/components/form/link.vue';
|
|||
|
||||
let enableRegistration: boolean = $ref(false);
|
||||
let emailRequiredForSignup: boolean = $ref(false);
|
||||
let approvalRequiredForSignup: boolean = $ref(false);
|
||||
let sensitiveWords: string = $ref('');
|
||||
let preservedUsernames: string = $ref('');
|
||||
let tosUrl: string | null = $ref(null);
|
||||
|
@ -80,6 +85,7 @@ async function init() {
|
|||
const meta = await os.api('admin/meta');
|
||||
enableRegistration = !meta.disableRegistration;
|
||||
emailRequiredForSignup = meta.emailRequiredForSignup;
|
||||
approvalRequiredForSignup = meta.approvalRequiredForSignup;
|
||||
sensitiveWords = meta.sensitiveWords.join('\n');
|
||||
preservedUsernames = meta.preservedUsernames.join('\n');
|
||||
tosUrl = meta.tosUrl;
|
||||
|
@ -90,6 +96,7 @@ function save() {
|
|||
os.apiWithDialog('admin/update-meta', {
|
||||
disableRegistration: !enableRegistration,
|
||||
emailRequiredForSignup,
|
||||
approvalRequiredForSignup,
|
||||
tosUrl,
|
||||
privacyPolicyUrl,
|
||||
sensitiveWords: sensitiveWords.split('\n'),
|
||||
|
|
|
@ -10,11 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:class="{
|
||||
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation'].includes(log.type),
|
||||
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
|
||||
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd'].includes(log.type)
|
||||
[$style.logRed]: ['suspend', 'approve', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd'].includes(log.type)
|
||||
}"
|
||||
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'approve'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
|
||||
|
@ -65,6 +66,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="log.type === 'suspend'">
|
||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'approve'">
|
||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'unsuspend'">
|
||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||
</template>
|
||||
|
|
|
@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="available">{{ i18n.ts.normal }}</option>
|
||||
<option value="approved">{{ i18n.ts.notApproved }}</option>
|
||||
<option value="admin">{{ i18n.ts.administrator }}</option>
|
||||
<option value="moderator">{{ i18n.ts.moderator }}</option>
|
||||
<option value="suspended">{{ i18n.ts.suspend }}</option>
|
||||
|
|
|
@ -45,6 +45,13 @@ function submit() {
|
|||
os.api('signup-pending', {
|
||||
code: props.code,
|
||||
}).then(res => {
|
||||
if (res.pendingApproval) {
|
||||
return os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.ts._signup.approvalPending,
|
||||
});
|
||||
}
|
||||
return login(res.i, '/');
|
||||
}).catch(() => {
|
||||
submitting = false;
|
||||
|
|
|
@ -33,6 +33,7 @@ export type Endpoints = {
|
|||
'admin/suspend-user': { req: TODO; res: TODO; };
|
||||
'admin/nsfw-user': { req: TODO; res: TODO; };
|
||||
'admin/unnsfw-user': { req: TODO; res: TODO; };
|
||||
'admin/approve-user': { req: TODO; res: TODO; };
|
||||
'admin/unsilence-user': { req: TODO; res: TODO; };
|
||||
'admin/unsuspend-user': { req: TODO; res: TODO; };
|
||||
'admin/update-meta': { req: TODO; res: TODO; };
|
||||
|
|
|
@ -48,6 +48,7 @@ export const permissions = [
|
|||
export const moderationLogTypes = [
|
||||
'updateServerSettings',
|
||||
'suspend',
|
||||
'approve',
|
||||
'unsuspend',
|
||||
'updateUserNote',
|
||||
'addCustomEmoji',
|
||||
|
@ -87,6 +88,11 @@ export type ModerationLogPayloads = {
|
|||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
approve: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
unsuspend: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
|
|
|
@ -348,6 +348,7 @@ export type LiteInstanceMetadata = {
|
|||
driveCapacityPerLocalUserMb: number;
|
||||
driveCapacityPerRemoteUserMb: number;
|
||||
emailRequiredForSignup: boolean;
|
||||
approvalRequiredForSignup: boolean;
|
||||
enableHcaptcha: boolean;
|
||||
hcaptchaSiteKey: string | null;
|
||||
enableRecaptcha: boolean;
|
||||
|
|
Loading…
Reference in a new issue