mirror of
https://github.com/paricafe/misskey.git
synced 2025-01-17 19:10:49 -06:00
feat: feat: Sign-Up Approval
This commit is contained in:
parent
9c6cebffcc
commit
a3873344bf
47 changed files with 875 additions and 7 deletions
|
@ -1914,6 +1914,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: "The account has been created and is pending approval."
|
||||
reasonInfo: "Please enter the reason you want to join this server."
|
||||
_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."
|
||||
|
@ -2617,6 +2619,7 @@ _moderationLogTypes:
|
|||
updateRole: "Role updated"
|
||||
assignRole: "Assigned to role"
|
||||
unassignRole: "Removed from role"
|
||||
approve: "Approved"
|
||||
suspend: "Suspended"
|
||||
unsuspend: "Unsuspended"
|
||||
addCustomEmoji: "Custom emoji added"
|
||||
|
@ -2819,6 +2822,21 @@ _selfXssPrevention:
|
|||
_followRequest:
|
||||
recieved: "Requests received"
|
||||
sent: "Requests sent"
|
||||
approvals: "Approvals"
|
||||
registerApproveConfirm: "Do you approve the registration?"
|
||||
registerApproveConfirmDescription: "This action cannot be undone. After approval, an email will be sent to this user confirming their registration."
|
||||
approvalRequiredForSignup: "Make account registration approval-based"
|
||||
signupPendingApprovals: "Account registration approvals"
|
||||
pendingUserApprovals: "There are users awaiting approval."
|
||||
approveAccount: "Approve"
|
||||
denyAccount: "Deny and delete account"
|
||||
approved: "Approved"
|
||||
notApproved: "Not approved"
|
||||
approvalStatus: "Approval status"
|
||||
approvalRequiredToRegister: "This server is currently approval-based. Please provide a reason for joining, and only those who are approved will be able to register."
|
||||
registerReason: "Reason for registration"
|
||||
registerHasNotBeenApprovedYet: "Your registration to the server has not been approved yet. Please try again later. If you provided an email address during registration, you will be notified by email once your registration is approved."
|
||||
registerApprovalEmailRecommended: "It is highly recommended to make email addresses mandatory for account registration, to notify users about the approval of their server registration."
|
||||
pariMisskeyAbout: "Pari Cafe uses a branch that has been customized for the original version."
|
||||
timeTravel: "Time Travel"
|
||||
timeTravelDescription: "Show posts before this date."
|
||||
|
|
16
locales/index.d.ts
vendored
16
locales/index.d.ts
vendored
|
@ -7173,6 +7173,14 @@ export interface Locale extends ILocale {
|
|||
* 入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。
|
||||
*/
|
||||
"emailSent": ParameterizedString<"email">;
|
||||
/**
|
||||
* Your account has been created and is awaiting approval.
|
||||
*/
|
||||
"approvalPending": string;
|
||||
/**
|
||||
* Please enter a reason as to why you want to join the instance.
|
||||
*/
|
||||
"reasonInfo": string;
|
||||
};
|
||||
"_accountDelete": {
|
||||
/**
|
||||
|
@ -10031,6 +10039,10 @@ export interface Locale extends ILocale {
|
|||
* ギャラリーの投稿を削除
|
||||
*/
|
||||
"deleteGalleryPost": string;
|
||||
/**
|
||||
* Approved
|
||||
*/
|
||||
"approve": string;
|
||||
};
|
||||
"_fileViewer": {
|
||||
/**
|
||||
|
@ -10578,6 +10590,10 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"codeGeneratedDescription": string;
|
||||
};
|
||||
/**
|
||||
* Approvals
|
||||
*/
|
||||
"approvals": string;
|
||||
"_selfXssPrevention": {
|
||||
/**
|
||||
* 警告
|
||||
|
|
|
@ -1858,6 +1858,8 @@ _signup:
|
|||
almostThere: "ほとんど完了です"
|
||||
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
|
||||
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。"
|
||||
approvalPending: "アカウントが作成され、承認待ちの状態です。"
|
||||
reasonInfo: "このサーバーに参加したい理由を入力してください。"
|
||||
|
||||
_accountDelete:
|
||||
accountDelete: "アカウントの削除"
|
||||
|
@ -2614,6 +2616,7 @@ _moderationLogTypes:
|
|||
updateRole: "ロールを更新"
|
||||
assignRole: "ロールへアサイン"
|
||||
unassignRole: "ロールのアサイン解除"
|
||||
approve: "承認済み"
|
||||
suspend: "凍結"
|
||||
unsuspend: "凍結解除"
|
||||
addCustomEmoji: "カスタム絵文字追加"
|
||||
|
@ -2829,6 +2832,22 @@ _followRequest:
|
|||
recieved: "受け取った申請"
|
||||
sent: "送った申請"
|
||||
|
||||
approvals: "承認"
|
||||
registerApproveConfirm: "登録を承認しますか?"
|
||||
registerApproveConfirmDescription: "この操作は取り消せません。承認後、このユーザーに登録が承認された旨が記載されたメールが送信されます。"
|
||||
approvalRequiredForSignup: "アカウント登録を承認制にする"
|
||||
signupPendingApprovals: "アカウント登録の承認"
|
||||
pendingUserApprovals: "承認待ちのユーザーがいます。"
|
||||
approveAccount: "承認する"
|
||||
denyAccount: "拒否してアカウント削除"
|
||||
approved: "承認済み"
|
||||
notApproved: "未承認"
|
||||
approvalStatus: "承認状況"
|
||||
approvalRequiredToRegister: "現在このサーバーは承認制です。参加したい理由を記入し、承認された方のみ登録できます。"
|
||||
registerReason: "登録理由"
|
||||
registerHasNotBeenApprovedYet: "サーバーへの登録はまだ承認されていません。しばらくしてから再度お試しください。登録時にメールアドレスを記入した場合は、登録が承認されたらメールでお知らせします。"
|
||||
registerApprovalEmailRecommended: "サーバーへの登録が承認されたかどうかの通知を行うために、併せてアカウント登録にメールアドレスを必須にすることを強く推奨します。"
|
||||
|
||||
pariMisskeyAbout: "Pari Cafe使用のブランチは、原版に対してカスタマイズを行っています。"
|
||||
timeTravel: "Time Travel"
|
||||
timeTravelDescription: "Show posts before this date."
|
||||
|
|
|
@ -1918,6 +1918,8 @@ _signup:
|
|||
almostThere: "即将完成"
|
||||
emailAddressInfo: "请输入您所使用的电子邮件地址"
|
||||
emailSent: "已将确认邮件发送至您输入的电子邮件地址 ({email})。请访问电子邮件中的链接以完成帐户创建。"
|
||||
approvalPending: "账户已创建,正在等待批准。"
|
||||
reasonInfo: "请输入您想加入此服务器的理由。"
|
||||
_accountDelete:
|
||||
accountDelete: "删除帐户"
|
||||
mayTakeTime: "删除账号是一个性能损耗较大的处理,如果账号持有的内容数量和上传的文件数量较多的话,完成需要花费一段时间。"
|
||||
|
@ -2621,6 +2623,7 @@ _moderationLogTypes:
|
|||
updateRole: "更新角色"
|
||||
assignRole: "分配角色"
|
||||
unassignRole: "取消分配角色"
|
||||
approve: "已批准"
|
||||
suspend: "冻结"
|
||||
unsuspend: "解除冻结"
|
||||
addCustomEmoji: "添加自定义表情符号"
|
||||
|
@ -2823,6 +2826,21 @@ _selfXssPrevention:
|
|||
_followRequest:
|
||||
recieved: "已收到申请"
|
||||
sent: "已发送申请"
|
||||
approvals: "审批"
|
||||
registerApproveConfirm: "您是否批准注册?"
|
||||
registerApproveConfirmDescription: "此操作无法撤销。批准后,将向该用户发送一封确认其注册的电子邮件。"
|
||||
approvalRequiredForSignup: "将账户注册设为审批制"
|
||||
signupPendingApprovals: "账户注册审批"
|
||||
pendingUserApprovals: "有用户等待审批。"
|
||||
approveAccount: "批准"
|
||||
denyAccount: "拒绝并删除账户"
|
||||
approved: "已批准"
|
||||
notApproved: "未批准"
|
||||
approvalStatus: "审批状态"
|
||||
approvalRequiredToRegister: "该服务器目前为审批制。请提供加入理由,只有获得批准的人才能注册。"
|
||||
registerReason: "注册理由"
|
||||
registerHasNotBeenApprovedYet: "您的注册尚未获得批准,请稍后再试。如果您在注册时提供了电子邮件地址,注册批准后我们会通过电子邮件通知您。"
|
||||
registerApprovalEmailRecommended: "强烈建议将电子邮件地址设为账户注册必填项,以便通知用户其服务器注册是否已获批准。"
|
||||
pariMisskeyAbout: "Pari Cafe使用的分支基于原版进行了定制。"
|
||||
timeTravel: "时光机"
|
||||
timeTravelDescription: "显示该日期以前的帖子"
|
||||
|
|
|
@ -1917,6 +1917,8 @@ _signup:
|
|||
almostThere: "即將完成"
|
||||
emailAddressInfo: "請輸入您所使用的電子郵件地址。電子郵件地址不會被公開。"
|
||||
emailSent: "已發送確認郵件至您輸入的電子郵件地址({email})。請開啟電子郵件中的連結完成註冊。"
|
||||
approvalPending: "帳號已建立,正在等待審核。"
|
||||
reasonInfo: "請輸入您想加入此伺服器的理由。"
|
||||
_accountDelete:
|
||||
accountDelete: "刪除帳戶"
|
||||
mayTakeTime: "刪除帳戶的處理負荷較大,如果帳戶發佈的內容以及上傳的檔案數量較多,則需要一段時間才能完成。"
|
||||
|
@ -2620,6 +2622,7 @@ _moderationLogTypes:
|
|||
updateRole: "更新角色設定"
|
||||
assignRole: "指派角色"
|
||||
unassignRole: "撤銷角色"
|
||||
approve: "已批准"
|
||||
suspend: "凍結"
|
||||
unsuspend: "解除凍結"
|
||||
addCustomEmoji: "新增自訂表情符號"
|
||||
|
@ -2822,6 +2825,21 @@ _selfXssPrevention:
|
|||
_followRequest:
|
||||
recieved: "收到的請求"
|
||||
sent: "送出的請求"
|
||||
approvals: "審核"
|
||||
registerApproveConfirm: "您是否批准註冊?"
|
||||
registerApproveConfirmDescription: "此操作無法撤銷。批准後,將向該用戶發送一封確認其註冊的電子郵件。"
|
||||
approvalRequiredForSignup: "將帳號註冊設為審核制"
|
||||
signupPendingApprovals: "帳號註冊審核"
|
||||
pendingUserApprovals: "有用戶等待審核。"
|
||||
approveAccount: "批准"
|
||||
denyAccount: "拒絕並刪除帳號"
|
||||
approved: "已批准"
|
||||
notApproved: "未批准"
|
||||
approvalStatus: "審核狀態"
|
||||
approvalRequiredToRegister: "該伺服器目前為審核制。請提供加入理由,只有獲得批准的人才能註冊。"
|
||||
registerReason: "註冊理由"
|
||||
registerHasNotBeenApprovedYet: "您的註冊尚未獲得批准,請稍後再試。如果您在註冊時提供了電子郵件地址,註冊批准後我們會通過電子郵件通知您。"
|
||||
registerApprovalEmailRecommended: "強烈建議將電子郵件地址設為帳號註冊必填項,以便通知用戶其伺服器註冊是否已獲批准。"
|
||||
pariMisskeyAbout: "Pari Cafe使用的分支是基於原版進行了定制。"
|
||||
timeTravel: "時光機"
|
||||
timeTravelDescription: "回到指定的日期"
|
||||
|
|
23
packages/backend/migration/1697580470000-approvalSignup.js
Normal file
23
packages/backend/migration/1697580470000-approvalSignup.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: marie and other Sharkey 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(`UPDATE "user" SET "approved" = true`);
|
||||
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"`);
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ export class CreateSystemUserService {
|
|||
isRoot: false,
|
||||
isLocked: true,
|
||||
isExplorable: false,
|
||||
approved: true,
|
||||
isBot: true,
|
||||
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
|
||||
|
||||
|
|
|
@ -52,9 +52,11 @@ export class SignupService {
|
|||
password?: string | null;
|
||||
passwordHash?: MiUserProfile['password'] | null;
|
||||
host?: string | null;
|
||||
reason?: string | null;
|
||||
ignorePreservedUsernames?: boolean;
|
||||
approved?: boolean;
|
||||
}) {
|
||||
const { username, password, passwordHash, host } = opts;
|
||||
const { username, password, passwordHash, host, reason } = opts;
|
||||
let hash = passwordHash;
|
||||
|
||||
// Validate username
|
||||
|
@ -130,6 +132,8 @@ export class SignupService {
|
|||
host: this.utilityService.toPunyNullable(host),
|
||||
token: secret,
|
||||
isRoot: isTheFirstUser,
|
||||
approved: isTheFirstUser || (opts.approved ?? !this.meta.approvalRequiredForSignup),
|
||||
signupReason: reason,
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new MiUserKeypair({
|
||||
|
|
|
@ -89,6 +89,8 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
|||
uri: null,
|
||||
followersUri: null,
|
||||
token: null,
|
||||
approved: true,
|
||||
signupReason: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -387,6 +387,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,
|
||||
|
@ -575,6 +576,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
emojis: emojiNames,
|
||||
name: truncate(person.name, nameLength),
|
||||
tags,
|
||||
approved: true,
|
||||
isBot: getApType(object) === 'Service' || getApType(object) === 'Application',
|
||||
isCat: (person as any).isCat === true,
|
||||
isLocked: person.manuallyApprovesFollowers,
|
||||
|
|
|
@ -87,6 +87,7 @@ export class MetaEntityService {
|
|||
inquiryUrl: instance.inquiryUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableMcaptcha: instance.enableMcaptcha,
|
||||
|
|
|
@ -521,6 +521,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
|
||||
makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
|
||||
makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
|
||||
approved: user.approved,
|
||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
|
@ -645,6 +646,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
...(opts.includeSecrets ? {
|
||||
email: profile!.email,
|
||||
emailVerified: profile!.emailVerified,
|
||||
signupReason: user.signupReason,
|
||||
securityKeysList: profile!.twoFactorEnabled
|
||||
? this.userSecurityKeysRepository.find({
|
||||
where: {
|
||||
|
|
|
@ -189,6 +189,11 @@ export class MiMeta {
|
|||
})
|
||||
public emailRequiredForSignup: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public approvalRequiredForSignup: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
@ -277,6 +277,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;
|
||||
}
|
||||
|
|
|
@ -76,6 +76,8 @@ import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderatio
|
|||
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
||||
import * as ep___admin_showUsers from './endpoints/admin/show-users.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_declineUser from './endpoints/admin/decline-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';
|
||||
|
@ -467,6 +469,8 @@ const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation
|
|||
const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default };
|
||||
const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.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_declineUser: Provider = { provide: 'ep:admin/decline-user', useClass: ep___admin_declineUser.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 };
|
||||
|
@ -862,6 +866,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_showUser,
|
||||
$admin_showUsers,
|
||||
$admin_suspendUser,
|
||||
$admin_approveUser,
|
||||
$admin_declineUser,
|
||||
$admin_unsuspendUser,
|
||||
$admin_updateMeta,
|
||||
$admin_deleteAccount,
|
||||
|
@ -1251,6 +1257,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_showUser,
|
||||
$admin_showUsers,
|
||||
$admin_suspendUser,
|
||||
$admin_approveUser,
|
||||
$admin_declineUser,
|
||||
$admin_unsuspendUser,
|
||||
$admin_updateMeta,
|
||||
$admin_deleteAccount,
|
||||
|
|
|
@ -130,10 +130,20 @@ export class SigninApiService {
|
|||
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
|
||||
});
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
|
||||
|
||||
if (!user.approved && this.meta.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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (password == null) {
|
||||
reply.code(200);
|
||||
if (profile.twoFactorEnabled) {
|
||||
|
@ -204,6 +214,7 @@ export class SigninApiService {
|
|||
}
|
||||
|
||||
if (same) {
|
||||
if (!this.meta.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else {
|
||||
return await fail(403, {
|
||||
|
@ -227,6 +238,8 @@ export class SigninApiService {
|
|||
});
|
||||
}
|
||||
|
||||
if (!this.meta.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) {
|
||||
|
@ -238,6 +251,7 @@ export class SigninApiService {
|
|||
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
|
||||
|
||||
if (authorized) {
|
||||
if (!this.meta.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||
return this.signinService.signin(request, reply, user);
|
||||
} else {
|
||||
return await fail(403, {
|
||||
|
|
|
@ -20,6 +20,8 @@ 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';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
@Injectable()
|
||||
export class SignupApiService {
|
||||
|
@ -51,6 +53,7 @@ export class SignupApiService {
|
|||
private signupService: SignupService,
|
||||
private signinService: SigninService,
|
||||
private emailService: EmailService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -63,6 +66,7 @@ export class SignupApiService {
|
|||
host?: string;
|
||||
invitationCode?: string;
|
||||
emailAddress?: string;
|
||||
reason?: string;
|
||||
'hcaptcha-response'?: string;
|
||||
'g-recaptcha-response'?: string;
|
||||
'turnstile-response'?: string;
|
||||
|
@ -112,6 +116,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 (this.meta.emailRequiredForSignup) {
|
||||
|
@ -127,6 +132,13 @@ export class SignupApiService {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.meta.approvalRequiredForSignup) {
|
||||
if (reason == null || typeof reason !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let ticket: MiRegistrationTicket | null = null;
|
||||
|
||||
if (this.meta.disableRegistration) {
|
||||
|
@ -195,6 +207,7 @@ export class SignupApiService {
|
|||
email: emailAddress!,
|
||||
username: username,
|
||||
password: hash,
|
||||
reason: reason,
|
||||
});
|
||||
|
||||
const link = `${this.config.url}/signup-complete/${code}`;
|
||||
|
@ -210,6 +223,39 @@ export class SignupApiService {
|
|||
});
|
||||
}
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else if (this.meta.approvalRequiredForSignup) {
|
||||
const { account } = 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.');
|
||||
}
|
||||
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedAt: new Date(),
|
||||
usedBy: account,
|
||||
usedById: account.id,
|
||||
});
|
||||
}
|
||||
|
||||
const moderators = await this.roleService.getModerators();
|
||||
|
||||
for (const moderator of moderators) {
|
||||
const profile = await this.userProfilesRepository.findOneBy({ userId: moderator.id });
|
||||
|
||||
if (profile?.email) {
|
||||
this.emailService.sendEmail(profile.email, 'New user awaiting approval',
|
||||
`A new user called ${account.username} is awaiting approval with the following reason: "${reason}"`,
|
||||
`A new user called ${account.username} is awaiting approval with the following reason: "${reason}"`);
|
||||
}
|
||||
}
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else {
|
||||
|
@ -257,6 +303,7 @@ export class SignupApiService {
|
|||
const { account, secret } = await this.signupService.signup({
|
||||
username: pendingUser.username,
|
||||
passwordHash: pendingUser.password,
|
||||
reason: pendingUser.reason,
|
||||
});
|
||||
|
||||
this.userPendingsRepository.delete({
|
||||
|
@ -280,6 +327,28 @@ export class SignupApiService {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.meta.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.');
|
||||
}
|
||||
|
||||
const moderators = await this.roleService.getModerators();
|
||||
|
||||
for (const moderator of moderators) {
|
||||
const profile = await this.userProfilesRepository.findOneBy({ userId: moderator.id });
|
||||
|
||||
if (profile?.email) {
|
||||
this.emailService.sendEmail(profile.email, 'New user awaiting approval',
|
||||
`A new user called ${pendingUser.username} is awaiting approval with the following reason: "${pendingUser.reason}"`,
|
||||
`A new user called ${pendingUser.username} is awaiting approval with the following reason: "${pendingUser.reason}"`);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
|
|
@ -82,6 +82,8 @@ import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderatio
|
|||
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
||||
import * as ep___admin_showUsers from './endpoints/admin/show-users.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_declineUser from './endpoints/admin/decline-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';
|
||||
|
@ -471,6 +473,8 @@ const eps = [
|
|||
['admin/show-user', ep___admin_showUser],
|
||||
['admin/show-users', ep___admin_showUsers],
|
||||
['admin/suspend-user', ep___admin_suspendUser],
|
||||
['admin/approve-user', ep___admin_approveUser],
|
||||
['admin/decline-user', ep___admin_declineUser],
|
||||
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
||||
['admin/update-meta', ep___admin_updateMeta],
|
||||
['admin/delete-account', ep___admin_deleteAccount],
|
||||
|
|
|
@ -94,6 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
username: ps.username,
|
||||
password: ps.password,
|
||||
ignorePreservedUsernames: true,
|
||||
approved: true,
|
||||
});
|
||||
|
||||
const res = await this.userEntityService.pack(account, account, {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
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,
|
||||
kind: 'write:admin:approve-user',
|
||||
} 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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsedUsernamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:decline-user',
|
||||
} 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,
|
||||
|
||||
@Inject(DI.usedUsernamesRepository)
|
||||
private usedUsernamesRepository: UsedUsernamesRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private emailService: EmailService,
|
||||
private deleteAccountService: DeleteAccountService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
||||
if (user == null || user.isDeleted) {
|
||||
throw new Error('user not found or already deleted');
|
||||
}
|
||||
|
||||
if (user.approved) {
|
||||
throw new Error('user is already approved');
|
||||
}
|
||||
|
||||
if (user.host) {
|
||||
throw new Error('user is not local');
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
|
||||
|
||||
if (profile?.email) {
|
||||
this.emailService.sendEmail(profile.email, 'Account Declined',
|
||||
'Your Account has been declined!',
|
||||
'Your Account has been declined!');
|
||||
}
|
||||
|
||||
await this.usedUsernamesRepository.delete({ username: user.username });
|
||||
|
||||
await this.deleteAccountService.deleteAccount(user);
|
||||
|
||||
this.moderationLogService.log(me, 'decline', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -33,6 +33,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
approvalRequiredForSignup: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableHcaptcha: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -561,6 +565,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
inquiryUrl: instance.inquiryUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableMcaptcha: instance.enableMcaptcha,
|
||||
|
|
|
@ -31,6 +31,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
approved: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
followedMessage: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
@ -230,6 +234,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return {
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
approved: user.approved,
|
||||
signupReason: user.signupReason,
|
||||
followedMessage: profile.followedMessage,
|
||||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||
noCrawle: profile.noCrawle,
|
||||
|
|
|
@ -35,7 +35,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: {
|
||||
|
@ -64,6 +64,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 [];
|
||||
|
|
|
@ -70,6 +70,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 },
|
||||
|
@ -315,6 +316,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;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,8 @@ export const userImportableEntities = ['antenna', 'blocking', 'customEmoji', 'fo
|
|||
export const moderationLogTypes = [
|
||||
'updateServerSettings',
|
||||
'suspend',
|
||||
'approve',
|
||||
'decline',
|
||||
'unsuspend',
|
||||
'updateUserNote',
|
||||
'addCustomEmoji',
|
||||
|
@ -132,6 +134,16 @@ export type ModerationLogPayloads = {
|
|||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
approve: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
decline: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
unsuspend: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
|
|
112
packages/frontend/src/components/MkApprovalUser.vue
Normal file
112
packages/frontend/src/components/MkApprovalUser.vue
Normal file
|
@ -0,0 +1,112 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkFolder :expanded="false">
|
||||
<template #icon><i class="ti ti-user-check"></i></template>
|
||||
<template #label>{{ i18n.ts.user }}: {{ user.username }}</template>
|
||||
|
||||
<div class="_gaps_s" :class="$style.root">
|
||||
<div :class="$style.items">
|
||||
<div>
|
||||
<div :class="$style.label">{{ i18n.ts.createdAt }}</div>
|
||||
<div><MkTime :time="user.createdAt" mode="absolute"/></div>
|
||||
</div>
|
||||
<div v-if="email">
|
||||
<div :class="$style.label">{{ i18n.ts.emailAddress }}</div>
|
||||
<div>{{ email }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.label">{{ i18n.ts.registerReason }}</div>
|
||||
<div>{{ reason }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton inline success @click="approveAccount()">{{ i18n.ts.approveAccount }}</MkButton>
|
||||
<MkButton inline danger @click="deleteAccount()">{{ i18n.ts.denyAccount }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
const props = defineProps<{
|
||||
user: Misskey.entities.User;
|
||||
}>();
|
||||
const reason = ref('');
|
||||
const email = ref('');
|
||||
function getReason() {
|
||||
return misskeyApi('admin/show-user', {
|
||||
userId: props.user.id,
|
||||
}).then(info => {
|
||||
reason.value = info.signupReason;
|
||||
email.value = info.email;
|
||||
});
|
||||
}
|
||||
getReason();
|
||||
const emits = defineEmits<{
|
||||
(event: 'deleted', value: string): void;
|
||||
}>();
|
||||
async function deleteAccount() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteAccountConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
const typed = await os.inputText({
|
||||
text: i18n.t('typeToConfirm', { x: props.user.username }),
|
||||
});
|
||||
if (typed.canceled) return;
|
||||
if (typed.result === props.user.username) {
|
||||
await os.apiWithDialog('admin/delete-account', {
|
||||
userId: props.user.id,
|
||||
});
|
||||
emits('deleted', props.user.id);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'input not match',
|
||||
});
|
||||
}
|
||||
}
|
||||
async function approveAccount() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts.registerApproveConfirm,
|
||||
text: i18n.ts.registerApproveConfirmDescription,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
await misskeyApi('admin/approve-user', { userId: props.user.id });
|
||||
emits('deleted', props.user.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
text-align: left;
|
||||
}
|
||||
.items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 0;
|
||||
user-select: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
|
@ -306,6 +306,14 @@ function onSigninApiError(err?: any): void {
|
|||
showSuspendedDialog();
|
||||
break;
|
||||
}
|
||||
case '2fe70810-0ed2-47db-a70b-dc3ecbf5f069': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: i18n.ts.registerHasNotBeenApprovedYet,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
|
|
|
@ -62,6 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkTextarea 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="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix><i class="ti ti-chalkboard"></i></template>
|
||||
</MkTextarea>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
|
@ -85,6 +89,7 @@ import * as Misskey from 'misskey-js';
|
|||
import * as config from '@@/js/config.js';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkTextarea from './MkTextarea.vue';
|
||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
@ -101,6 +106,7 @@ const props = withDefaults(defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'signup', user: Misskey.entities.SignupResponse): void;
|
||||
(ev: 'signupEmailPending'): void;
|
||||
(ev: 'approvalPending'): void;
|
||||
}>();
|
||||
|
||||
const host = toUnicode(config.host);
|
||||
|
@ -115,6 +121,7 @@ const username = ref<string>('');
|
|||
const password = ref<string>('');
|
||||
const retypedPassword = ref<string>('');
|
||||
const invitationCode = ref<string>('');
|
||||
const reason = ref<string>('');
|
||||
const email = ref('');
|
||||
const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null);
|
||||
const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:banned' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
|
||||
|
@ -259,6 +266,7 @@ async function onSubmit(): Promise<void> {
|
|||
password: password.value,
|
||||
emailAddress: email.value,
|
||||
invitationCode: invitationCode.value,
|
||||
reason: reason.value,
|
||||
'hcaptcha-response': hCaptchaResponse.value,
|
||||
'm-captcha-response': mCaptchaResponse.value,
|
||||
'g-recaptcha-response': reCaptchaResponse.value,
|
||||
|
@ -285,6 +293,13 @@ async function onSubmit(): Promise<void> {
|
|||
text: i18n.tsx._signup.emailSent({ email: email.value }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else if (instance.approvalRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.ts._signup.approvalPending,
|
||||
});
|
||||
emit('approvalPending');
|
||||
} else {
|
||||
const resJson = (await res.json()) as Misskey.entities.SignupResponse;
|
||||
if (_DEV_) console.log(resJson);
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending" @approvalPending="onApprovalPending"/>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@ -69,6 +69,10 @@ function onSignup(res: Misskey.entities.SignupResponse) {
|
|||
function onSignupEmailPending() {
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function onApprovalPending() {
|
||||
dialog.value.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 && !user.host" 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>
|
||||
|
@ -251,6 +253,7 @@ const ips = ref<Misskey.entities.AdminGetUserIpsResponse | null>(null);
|
|||
const ap = ref<any>(null);
|
||||
const moderator = ref(false);
|
||||
const silenced = ref(false);
|
||||
const approved = ref(false);
|
||||
const suspended = ref(false);
|
||||
const moderationNote = ref('');
|
||||
const filesPagination = {
|
||||
|
@ -286,6 +289,7 @@ function createFetcher() {
|
|||
ips.value = _ips;
|
||||
moderator.value = info.value.isModerator;
|
||||
silenced.value = info.value.isSilenced;
|
||||
approved.value = info.value.approved;
|
||||
suspended.value = info.value.isSuspended;
|
||||
moderationNote.value = info.value.moderationNote;
|
||||
|
||||
|
@ -622,6 +626,18 @@ definePageMetadata(() => ({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.casdwq {
|
||||
.silenced {
|
||||
color: var(--MI_THEME-warn);
|
||||
border-color: var(--warn);
|
||||
}
|
||||
|
||||
.moderator {
|
||||
color: var(--MI_THEME-success);
|
||||
border-color: var(--MI_THEME-success);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
65
packages/frontend/src/pages/admin/approvals.vue
Normal file
65
packages/frontend/src/pages/admin/approvals.vue
Normal file
|
@ -0,0 +1,65 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps_m">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<MkApprovalUser v-for="item in items" :key="item.id" :user="(item as any)" :onDeleted="deleted"/>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkApprovalUser from '@/components/MkApprovalUser.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
let paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const pagination = {
|
||||
endpoint: 'admin/show-users' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
sort: '+createdAt',
|
||||
state: 'approved',
|
||||
origin: 'local',
|
||||
})),
|
||||
offsetMode: true,
|
||||
};
|
||||
function deleted(id: string) {
|
||||
if (paginationComponent.value) {
|
||||
paginationComponent.value.items.delete(id);
|
||||
}
|
||||
}
|
||||
const headerActions = computed(() => []);
|
||||
const headerTabs = computed(() => []);
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.signupPendingApprovals,
|
||||
icon: 'ti ti-user-check',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.inputs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.input {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
|
@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInfo v-if="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noBotProtection" warn>{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="pendingUserApprovals" warn>{{ i18n.ts.pendingUserApprovals }} <MkA to="/admin/approvals" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
|
||||
</div>
|
||||
|
||||
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
||||
|
@ -66,6 +67,7 @@ const noBotProtection = computed(() => !instance.disableRegistration && !instanc
|
|||
const noEmailServer = computed(() => !instance.enableEmail);
|
||||
const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl));
|
||||
const thereIsUnresolvedAbuseReport = ref(false);
|
||||
const pendingUserApprovals = ref(false);
|
||||
const currentPage = computed(() => router.currentRef.value.child);
|
||||
|
||||
misskeyApi('admin/abuse-user-reports', {
|
||||
|
@ -75,6 +77,14 @@ misskeyApi('admin/abuse-user-reports', {
|
|||
if (reports.length > 0) thereIsUnresolvedAbuseReport.value = true;
|
||||
});
|
||||
|
||||
misskeyApi('admin/show-users', {
|
||||
state: 'approved',
|
||||
origin: 'local',
|
||||
limit: 1,
|
||||
}).then(approvals => {
|
||||
if (approvals.length > 0) pendingUserApprovals.value = true;
|
||||
});
|
||||
|
||||
const NARROW_THRESHOLD = 600;
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
if (entries.length === 0) return;
|
||||
|
@ -111,6 +121,11 @@ const menuDef = computed(() => [{
|
|||
text: i18n.ts.invite,
|
||||
to: '/admin/invites',
|
||||
active: currentPage.value?.route.name === 'invites',
|
||||
}, {
|
||||
icon: 'ti ti-user-check',
|
||||
text: i18n.ts.approvals,
|
||||
to: '/admin/approvals',
|
||||
active: currentPage.value?.route.name === 'approvals',
|
||||
}, {
|
||||
icon: 'ti ti-badges',
|
||||
text: i18n.ts.roles,
|
||||
|
|
|
@ -22,6 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="approvalRequiredForSignup" @change="onChange_approvalRequiredForSignup">
|
||||
<template #label>{{ i18n.ts.approvalRequiredForSignup }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||
|
||||
<MkFolder>
|
||||
|
@ -144,6 +148,7 @@ import MkFolder from '@/components/MkFolder.vue';
|
|||
|
||||
const enableRegistration = ref<boolean>(false);
|
||||
const emailRequiredForSignup = ref<boolean>(false);
|
||||
const approvalRequiredForSignup = ref<boolean>(false);
|
||||
const sensitiveWords = ref<string>('');
|
||||
const prohibitedWords = ref<string>('');
|
||||
const prohibitedWordsForNameOfUser = ref<string>('');
|
||||
|
@ -157,6 +162,7 @@ async function init() {
|
|||
const meta = await misskeyApi('admin/meta');
|
||||
enableRegistration.value = !meta.disableRegistration;
|
||||
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
||||
approvalRequiredForSignup.value = meta.approvalRequiredForSignup;
|
||||
sensitiveWords.value = meta.sensitiveWords.join('\n');
|
||||
prohibitedWords.value = meta.prohibitedWords.join('\n');
|
||||
prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
|
||||
|
@ -193,6 +199,14 @@ function onChange_emailRequiredForSignup(value: boolean) {
|
|||
});
|
||||
}
|
||||
|
||||
function onChange_approvalRequiredForSignup(value: boolean) {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
approvalRequiredForSignup: value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
function save_preservedUsernames() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||
|
|
|
@ -26,6 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
].includes(log.type),
|
||||
[$style.logRed]: [
|
||||
'suspend',
|
||||
'approve',
|
||||
'decline',
|
||||
'deleteRole',
|
||||
'deleteGlobalAnnouncement',
|
||||
'deleteUserAnnouncement',
|
||||
|
@ -45,6 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>{{ 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 === 'decline'">: @{{ 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>
|
||||
|
@ -108,6 +112,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>
|
||||
|
@ -43,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination">
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<div :class="$style.users">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/admin/user/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
|
|
|
@ -46,6 +46,13 @@ function submit() {
|
|||
misskeyApi('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.value = false;
|
||||
|
|
|
@ -482,6 +482,10 @@ const routes: RouteDef[] = [{
|
|||
path: '/invites',
|
||||
name: 'invites',
|
||||
component: page(() => import('@/pages/admin/invites.vue')),
|
||||
}, {
|
||||
path: '/approvals',
|
||||
name: 'approvals',
|
||||
component: page(() => import('@/pages/admin/approvals.vue')),
|
||||
}, {
|
||||
path: '/abuse-report-notification-recipient',
|
||||
name: 'abuse-report-notification-recipient',
|
||||
|
|
|
@ -118,6 +118,12 @@ type AdminAnnouncementsListResponse = operations['admin___announcements___list']
|
|||
// @public (undocumented)
|
||||
type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -1320,6 +1326,8 @@ declare namespace entities {
|
|||
AdminShowUsersRequest,
|
||||
AdminShowUsersResponse,
|
||||
AdminSuspendUserRequest,
|
||||
AdminApproveUserRequest,
|
||||
AdminDeclineUserRequest,
|
||||
AdminUnsuspendUserRequest,
|
||||
AdminUpdateMetaRequest,
|
||||
AdminDeleteAccountRequest,
|
||||
|
@ -1561,6 +1569,7 @@ declare namespace entities {
|
|||
IGalleryPostsResponse,
|
||||
IImportBlockingRequest,
|
||||
IImportFollowingRequest,
|
||||
IImportNotesRequest,
|
||||
IImportMutingRequest,
|
||||
IImportUserListsRequest,
|
||||
IImportAntennasRequest,
|
||||
|
@ -2231,6 +2240,9 @@ type IImportFollowingRequest = operations['i___import-following']['requestBody']
|
|||
// @public (undocumented)
|
||||
type IImportMutingRequest = operations['i___import-muting']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type IImportNotesRequest = operations['i___import-notes']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type IImportUserListsRequest = operations['i___import-user-lists']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -2463,6 +2475,12 @@ type ModerationLog = {
|
|||
} & ({
|
||||
type: 'updateServerSettings';
|
||||
info: ModerationLogPayloads['updateServerSettings'];
|
||||
} | {
|
||||
type: 'approve';
|
||||
info: ModerationLogPayloads['approve'];
|
||||
} | {
|
||||
type: 'decline';
|
||||
info: ModerationLogPayloads['decline'];
|
||||
} | {
|
||||
type: 'suspend';
|
||||
info: ModerationLogPayloads['suspend'];
|
||||
|
@ -2884,7 +2902,7 @@ type PartialRolePolicyOverride = Partial<{
|
|||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
||||
|
||||
// @public (undocumented)
|
||||
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
||||
|
|
|
@ -779,6 +779,28 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
|
||||
*/
|
||||
request<E extends 'admin/approve-user', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
|
||||
*/
|
||||
request<E extends 'admin/decline-user', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -95,6 +95,8 @@ import type {
|
|||
AdminShowUsersRequest,
|
||||
AdminShowUsersResponse,
|
||||
AdminSuspendUserRequest,
|
||||
AdminApproveUserRequest,
|
||||
AdminDeclineUserRequest,
|
||||
AdminUnsuspendUserRequest,
|
||||
AdminUpdateMetaRequest,
|
||||
AdminDeleteAccountRequest,
|
||||
|
@ -654,6 +656,8 @@ export type Endpoints = {
|
|||
'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse };
|
||||
'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse };
|
||||
'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse };
|
||||
'admin/approve-user': { req: AdminApproveUserRequest; res: EmptyResponse };
|
||||
'admin/decline-user': { req: AdminDeclineUserRequest; res: EmptyResponse };
|
||||
'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse };
|
||||
'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse };
|
||||
'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse };
|
||||
|
|
|
@ -98,6 +98,8 @@ export type AdminShowUserResponse = operations['admin___show-user']['responses']
|
|||
export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json'];
|
||||
export type AdminShowUsersResponse = operations['admin___show-users']['responses']['200']['content']['application/json'];
|
||||
export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json'];
|
||||
export type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json'];
|
||||
export type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json'];
|
||||
export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
|
||||
export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
|
||||
export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json'];
|
||||
|
|
|
@ -648,6 +648,24 @@ export type paths = {
|
|||
*/
|
||||
post: operations['admin___suspend-user'];
|
||||
};
|
||||
'/admin/approve-user': {
|
||||
/**
|
||||
* admin/approve-user
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
|
||||
*/
|
||||
post: operations['admin___approve-user'];
|
||||
};
|
||||
'/admin/decline-user': {
|
||||
/**
|
||||
* admin/decline-user
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
|
||||
*/
|
||||
post: operations['admin___decline-user'];
|
||||
};
|
||||
'/admin/unsuspend-user': {
|
||||
/**
|
||||
* admin/unsuspend-user
|
||||
|
@ -5013,6 +5031,7 @@ export type components = {
|
|||
defaultLightTheme: string | null;
|
||||
disableRegistration: boolean;
|
||||
emailRequiredForSignup: boolean;
|
||||
approvalRequiredForSignup: boolean;
|
||||
enableHcaptcha: boolean;
|
||||
hcaptchaSiteKey: string | null;
|
||||
enableMcaptcha: boolean;
|
||||
|
@ -5144,6 +5163,7 @@ export type operations = {
|
|||
cacheRemoteFiles: boolean;
|
||||
cacheRemoteSensitiveFiles: boolean;
|
||||
emailRequiredForSignup: boolean;
|
||||
approvalRequiredForSignup: boolean;
|
||||
enableHcaptcha: boolean;
|
||||
hcaptchaSiteKey: string | null;
|
||||
enableMcaptcha: boolean;
|
||||
|
@ -9143,6 +9163,7 @@ export type operations = {
|
|||
'application/json': {
|
||||
email: string | null;
|
||||
emailVerified: boolean;
|
||||
approved: boolean;
|
||||
followedMessage: string | null;
|
||||
autoAcceptFollowed: boolean;
|
||||
noCrawle: boolean;
|
||||
|
@ -9351,7 +9372,7 @@ export type operations = {
|
|||
* @default all
|
||||
* @enum {string}
|
||||
*/
|
||||
state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended';
|
||||
state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended' | 'approved';
|
||||
/**
|
||||
* @default combined
|
||||
* @enum {string}
|
||||
|
@ -9458,6 +9479,110 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/approve-user
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
|
||||
*/
|
||||
'admin___approve-user': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/decline-user
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
|
||||
*/
|
||||
'admin___decline-user': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/unsuspend-user
|
||||
* @description No description provided.
|
||||
|
@ -9546,6 +9671,7 @@ export type operations = {
|
|||
cacheRemoteFiles?: boolean;
|
||||
cacheRemoteSensitiveFiles?: boolean;
|
||||
emailRequiredForSignup?: boolean;
|
||||
approvalRequiredForSignup?: boolean;
|
||||
enableHcaptcha?: boolean;
|
||||
hcaptchaSiteKey?: string | null;
|
||||
hcaptchaSecretKey?: string | null;
|
||||
|
|
|
@ -77,6 +77,8 @@ export const permissions = [
|
|||
'read:admin:show-moderation-log',
|
||||
'read:admin:show-user',
|
||||
'write:admin:suspend-user',
|
||||
'write:admin:approve-user',
|
||||
'write:admin:decline-user',
|
||||
'write:admin:unset-user-avatar',
|
||||
'write:admin:unset-user-banner',
|
||||
'write:admin:unsuspend-user',
|
||||
|
@ -115,6 +117,7 @@ export const permissions = [
|
|||
export const moderationLogTypes = [
|
||||
'updateServerSettings',
|
||||
'suspend',
|
||||
'approve',
|
||||
'unsuspend',
|
||||
'updateUserNote',
|
||||
'addCustomEmoji',
|
||||
|
@ -195,6 +198,16 @@ export type ModerationLogPayloads = {
|
|||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
approve: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
decline: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
unsuspend: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
|
|
|
@ -48,6 +48,12 @@ export type ModerationLog = {
|
|||
} & ({
|
||||
type: 'updateServerSettings';
|
||||
info: ModerationLogPayloads['updateServerSettings'];
|
||||
} | {
|
||||
type: 'approve';
|
||||
info: ModerationLogPayloads['approve'];
|
||||
} | {
|
||||
type: 'decline';
|
||||
info: ModerationLogPayloads['decline'];
|
||||
} | {
|
||||
type: 'suspend';
|
||||
info: ModerationLogPayloads['suspend'];
|
||||
|
|
Loading…
Reference in a new issue