Account approval #17
45 changed files with 931 additions and 51 deletions
|
@ -43,6 +43,7 @@ alreadyFavorited: "Already added to favorites."
|
||||||
cantFavorite: "Couldn't add to favorites."
|
cantFavorite: "Couldn't add to favorites."
|
||||||
pin: "Pin to profile"
|
pin: "Pin to profile"
|
||||||
unpin: "Unpin from profile"
|
unpin: "Unpin from profile"
|
||||||
|
approvals: "Approvals"
|
||||||
copyContent: "Copy contents"
|
copyContent: "Copy contents"
|
||||||
copyLink: "Copy link"
|
copyLink: "Copy link"
|
||||||
copyLinkRenote: "Copy renote link"
|
copyLinkRenote: "Copy renote link"
|
||||||
|
@ -148,6 +149,7 @@ unsuspend: "Unsuspend"
|
||||||
blockConfirm: "Are you sure that you want to block this account?"
|
blockConfirm: "Are you sure that you want to block this account?"
|
||||||
unblockConfirm: "Are you sure that you want to unblock this account?"
|
unblockConfirm: "Are you sure that you want to unblock this account?"
|
||||||
suspendConfirm: "Are you sure that you want to suspend this account?"
|
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?"
|
unsuspendConfirm: "Are you sure that you want to unsuspend this account?"
|
||||||
selectList: "Select a list"
|
selectList: "Select a list"
|
||||||
editList: "Edit list"
|
editList: "Edit list"
|
||||||
|
@ -906,6 +908,7 @@ itsOff: "Disabled"
|
||||||
on: "On"
|
on: "On"
|
||||||
off: "Off"
|
off: "Off"
|
||||||
emailRequiredForSignup: "Require email address for sign-up"
|
emailRequiredForSignup: "Require email address for sign-up"
|
||||||
|
approvalRequiredForSignup: "Require approval for new users"
|
||||||
unread: "Unread"
|
unread: "Unread"
|
||||||
filter: "Filter"
|
filter: "Filter"
|
||||||
controlPanel: "Control Panel"
|
controlPanel: "Control Panel"
|
||||||
|
@ -966,6 +969,12 @@ requireAdminForView: "You must log in with an administrator account to view this
|
||||||
isSystemAccount: "An account created and automatically operated by the system."
|
isSystemAccount: "An account created and automatically operated by the system."
|
||||||
typeToConfirm: "Please enter {x} to confirm"
|
typeToConfirm: "Please enter {x} to confirm"
|
||||||
deleteAccount: "Delete account"
|
deleteAccount: "Delete account"
|
||||||
|
pendingUserApprovals: "There are users awaiting approval."
|
||||||
|
approveAccount: "Approve"
|
||||||
|
denyAccount: "Deny & Delete"
|
||||||
|
approved: "Approved"
|
||||||
|
notApproved: "Not Approved"
|
||||||
|
approvalStatus: "Approval Status"
|
||||||
document: "Documentation"
|
document: "Documentation"
|
||||||
numberOfPageCache: "Number of cached pages"
|
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."
|
numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device."
|
||||||
|
@ -1061,6 +1070,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."
|
disableFederationConfirmWarn: "Even if defederated, posts will continue to be public unless set otherwise. You usually do not need to do this."
|
||||||
disableFederationOk: "Disable"
|
disableFederationOk: "Disable"
|
||||||
invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up."
|
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"
|
emailNotSupported: "This instance does not support sending emails"
|
||||||
postToTheChannel: "Post to channel"
|
postToTheChannel: "Post to channel"
|
||||||
cannotBeChangedLater: "This cannot be changed later."
|
cannotBeChangedLater: "This cannot be changed later."
|
||||||
|
@ -1869,6 +1879,8 @@ _signup:
|
||||||
almostThere: "Almost there"
|
almostThere: "Almost there"
|
||||||
emailAddressInfo: "Please enter your email address. It will not be made public."
|
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."
|
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:
|
||||||
accountDelete: "Delete account"
|
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."
|
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."
|
||||||
|
|
44
locales/index.d.ts
vendored
44
locales/index.d.ts
vendored
|
@ -620,6 +620,10 @@ export interface Locale extends ILocale {
|
||||||
* 凍結しますか?
|
* 凍結しますか?
|
||||||
*/
|
*/
|
||||||
"suspendConfirm": string;
|
"suspendConfirm": string;
|
||||||
|
/**
|
||||||
|
* このアカウントを承認してもよろしいですか?
|
||||||
|
*/
|
||||||
|
"approveConfirm": string;
|
||||||
/**
|
/**
|
||||||
* 解凍しますか?
|
* 解凍しますか?
|
||||||
*/
|
*/
|
||||||
|
@ -3644,6 +3648,10 @@ export interface Locale extends ILocale {
|
||||||
* アカウント登録にメールアドレスを必須にする
|
* アカウント登録にメールアドレスを必須にする
|
||||||
*/
|
*/
|
||||||
"emailRequiredForSignup": string;
|
"emailRequiredForSignup": string;
|
||||||
|
/**
|
||||||
|
* 新規ユーザーの承認が必要
|
||||||
|
*/
|
||||||
|
"approvalRequiredForSignup": string;
|
||||||
/**
|
/**
|
||||||
* 未読
|
* 未読
|
||||||
*/
|
*/
|
||||||
|
@ -3887,6 +3895,30 @@ export interface Locale extends ILocale {
|
||||||
* アカウント削除
|
* アカウント削除
|
||||||
*/
|
*/
|
||||||
"deleteAccount": string;
|
"deleteAccount": string;
|
||||||
|
/**
|
||||||
|
* 承認待ちのユーザーがいます。
|
||||||
|
*/
|
||||||
|
"pendingUserApprovals": string;
|
||||||
|
/**
|
||||||
|
* 承認する
|
||||||
|
*/
|
||||||
|
"approveAccount": string;
|
||||||
|
/**
|
||||||
|
* 拒否と削除
|
||||||
|
*/
|
||||||
|
"denyAccount": string;
|
||||||
|
/**
|
||||||
|
* 承認済み
|
||||||
|
*/
|
||||||
|
"approved": string;
|
||||||
|
/**
|
||||||
|
* 承認されていない
|
||||||
|
*/
|
||||||
|
"notApproved": string;
|
||||||
|
/**
|
||||||
|
* 承認状況
|
||||||
|
*/
|
||||||
|
"approvalStatus": string;
|
||||||
/**
|
/**
|
||||||
* ドキュメント
|
* ドキュメント
|
||||||
*/
|
*/
|
||||||
|
@ -4267,6 +4299,10 @@ export interface Locale extends ILocale {
|
||||||
* 現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。
|
* 現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。
|
||||||
*/
|
*/
|
||||||
"invitationRequiredToRegister": string;
|
"invitationRequiredToRegister": string;
|
||||||
|
/**
|
||||||
|
* このインスタンスは、登録理由を指定したユーザーのみを受け入れています。
|
||||||
|
*/
|
||||||
|
"approvalRequiredToRegister": string;
|
||||||
/**
|
/**
|
||||||
* このサーバーではメール配信はサポートされていません
|
* このサーバーではメール配信はサポートされていません
|
||||||
*/
|
*/
|
||||||
|
@ -7306,6 +7342,14 @@ export interface Locale extends ILocale {
|
||||||
* 入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。
|
* 入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。
|
||||||
*/
|
*/
|
||||||
"emailSent": ParameterizedString<"email">;
|
"emailSent": ParameterizedString<"email">;
|
||||||
|
/**
|
||||||
|
* アカウントが作成され、承認待ちの状態です。
|
||||||
|
*/
|
||||||
|
"approvalPending": string;
|
||||||
|
/**
|
||||||
|
* インスタンスに参加したい理由を入力してください。
|
||||||
|
*/
|
||||||
|
"reasonInfo": string;
|
||||||
};
|
};
|
||||||
"_accountDelete": {
|
"_accountDelete": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -151,6 +151,7 @@ unsuspend: "解凍"
|
||||||
blockConfirm: "ブロックしますか?"
|
blockConfirm: "ブロックしますか?"
|
||||||
unblockConfirm: "ブロック解除しますか?"
|
unblockConfirm: "ブロック解除しますか?"
|
||||||
suspendConfirm: "凍結しますか?"
|
suspendConfirm: "凍結しますか?"
|
||||||
|
approveConfirm: "このアカウントを承認してもよろしいですか?"
|
||||||
unsuspendConfirm: "解凍しますか?"
|
unsuspendConfirm: "解凍しますか?"
|
||||||
selectList: "リストを選択"
|
selectList: "リストを選択"
|
||||||
editList: "リストを編集"
|
editList: "リストを編集"
|
||||||
|
@ -907,6 +908,7 @@ itsOff: "オフになっています"
|
||||||
on: "オン"
|
on: "オン"
|
||||||
off: "オフ"
|
off: "オフ"
|
||||||
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
|
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
|
||||||
|
approvalRequiredForSignup: "新規ユーザーの承認が必要"
|
||||||
unread: "未読"
|
unread: "未読"
|
||||||
filter: "フィルタ"
|
filter: "フィルタ"
|
||||||
controlPanel: "コントロールパネル"
|
controlPanel: "コントロールパネル"
|
||||||
|
@ -967,6 +969,12 @@ requireAdminForView: "閲覧するには管理者アカウントでログイン
|
||||||
isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
|
isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
|
||||||
typeToConfirm: "この操作を行うには {x} と入力してください"
|
typeToConfirm: "この操作を行うには {x} と入力してください"
|
||||||
deleteAccount: "アカウント削除"
|
deleteAccount: "アカウント削除"
|
||||||
|
pendingUserApprovals: "承認待ちのユーザーがいます。"
|
||||||
|
approveAccount: "承認する"
|
||||||
|
denyAccount: "拒否と削除"
|
||||||
|
approved: "承認済み"
|
||||||
|
notApproved: "承認されていない"
|
||||||
|
approvalStatus: "承認状況"
|
||||||
document: "ドキュメント"
|
document: "ドキュメント"
|
||||||
numberOfPageCache: "ページキャッシュ数"
|
numberOfPageCache: "ページキャッシュ数"
|
||||||
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
|
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
|
||||||
|
@ -1062,6 +1070,7 @@ disableFederationConfirm: "連合なしにしますか?"
|
||||||
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
|
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
|
||||||
disableFederationOk: "連合なしにする"
|
disableFederationOk: "連合なしにする"
|
||||||
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
||||||
|
approvalRequiredToRegister: "このインスタンスは、登録理由を指定したユーザーのみを受け入れています。"
|
||||||
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
||||||
postToTheChannel: "チャンネルに投稿"
|
postToTheChannel: "チャンネルに投稿"
|
||||||
cannotBeChangedLater: "後から変更できません。"
|
cannotBeChangedLater: "後から変更できません。"
|
||||||
|
@ -1886,6 +1895,8 @@ _signup:
|
||||||
almostThere: "ほとんど完了です"
|
almostThere: "ほとんど完了です"
|
||||||
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
|
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
|
||||||
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。"
|
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。"
|
||||||
|
approvalPending: "アカウントが作成され、承認待ちの状態です。"
|
||||||
|
reasonInfo: "インスタンスに参加したい理由を入力してください。"
|
||||||
|
|
||||||
_accountDelete:
|
_accountDelete:
|
||||||
accountDelete: "アカウントの削除"
|
accountDelete: "アカウントの削除"
|
||||||
|
|
22
packages/backend/migration/1736372582304-approval.js
Normal file
22
packages/backend/migration/1736372582304-approval.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 true 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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,6 +61,7 @@ export class CreateSystemUserService {
|
||||||
isLocked: true,
|
isLocked: true,
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
isBot: true,
|
isBot: true,
|
||||||
|
approved: true,
|
||||||
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
|
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
|
||||||
|
|
||||||
await transactionalEntityManager.insert(MiUserKeypair, {
|
await transactionalEntityManager.insert(MiUserKeypair, {
|
||||||
|
|
|
@ -58,9 +58,10 @@ export class SignupService {
|
||||||
password?: string | null;
|
password?: string | null;
|
||||||
passwordHash?: MiUserProfile['password'] | null;
|
passwordHash?: MiUserProfile['password'] | null;
|
||||||
host?: string | null;
|
host?: string | null;
|
||||||
|
reason?: string | null;
|
||||||
ignorePreservedUsernames?: boolean;
|
ignorePreservedUsernames?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { username, password, passwordHash, host } = opts;
|
const { username, password, passwordHash, host, reason } = opts;
|
||||||
let hash = passwordHash;
|
let hash = passwordHash;
|
||||||
|
|
||||||
// Validate username
|
// Validate username
|
||||||
|
@ -93,9 +94,9 @@ export class SignupService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
|
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
|
||||||
|
const instance = await this.metaService.fetch(true);
|
||||||
|
|
||||||
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
||||||
const instance = await this.metaService.fetch(true);
|
|
||||||
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
||||||
if (isPreserved) {
|
if (isPreserved) {
|
||||||
throw new Error('USED_USERNAME');
|
throw new Error('USED_USERNAME');
|
||||||
|
@ -128,6 +129,10 @@ export class SignupService {
|
||||||
try {
|
try {
|
||||||
let account!: MiUser;
|
let account!: MiUser;
|
||||||
|
|
||||||
|
let defaultApproval = false;
|
||||||
|
|
||||||
|
if (!instance.approvalRequiredForSignup) defaultApproval = true;
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await this.db.transaction(async transactionalEntityManager => {
|
await this.db.transaction(async transactionalEntityManager => {
|
||||||
const exist = await transactionalEntityManager.findOneBy(MiUser, {
|
const exist = await transactionalEntityManager.findOneBy(MiUser, {
|
||||||
|
@ -144,6 +149,8 @@ export class SignupService {
|
||||||
host: host ? this.utilityService.normalizeHost(host) : null,
|
host: host ? this.utilityService.normalizeHost(host) : null,
|
||||||
token: secret,
|
token: secret,
|
||||||
isRoot: isTheFirstUser,
|
isRoot: isTheFirstUser,
|
||||||
|
approved: defaultApproval,
|
||||||
|
signupReason: reason,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await transactionalEntityManager.save(new MiUserKeypair({
|
await transactionalEntityManager.save(new MiUserKeypair({
|
||||||
|
|
|
@ -328,8 +328,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.logger.error('error occurred while fetching following/followers collection', { error: err });
|
this.logger.error('error occurred while fetching following/followers collection', { error: err });
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
@ -378,6 +378,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
alsoKnownAs: person.alsoKnownAs,
|
alsoKnownAs: person.alsoKnownAs,
|
||||||
isExplorable: person.discoverable,
|
isExplorable: person.discoverable,
|
||||||
username: person.preferredUsername,
|
username: person.preferredUsername,
|
||||||
|
approved: true,
|
||||||
usernameLower: person.preferredUsername?.toLowerCase(),
|
usernameLower: person.preferredUsername?.toLowerCase(),
|
||||||
host,
|
host,
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
|
@ -526,8 +527,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
|
|
@ -68,6 +68,7 @@ export class MetaEntityService {
|
||||||
privacyPolicyUrl: instance.privacyPolicyUrl,
|
privacyPolicyUrl: instance.privacyPolicyUrl,
|
||||||
disableRegistration: instance.disableRegistration,
|
disableRegistration: instance.disableRegistration,
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
|
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
enableMcaptcha: instance.enableMcaptcha,
|
enableMcaptcha: instance.enableMcaptcha,
|
||||||
|
@ -140,6 +141,7 @@ export class MetaEntityService {
|
||||||
globalTimeline: instance.policies.gtlAvailable,
|
globalTimeline: instance.policies.gtlAvailable,
|
||||||
registration: !instance.disableRegistration,
|
registration: !instance.disableRegistration,
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
|
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||||
hCaptcha: instance.enableHcaptcha,
|
hCaptcha: instance.enableHcaptcha,
|
||||||
hcaptcha: instance.enableHcaptcha,
|
hcaptcha: instance.enableHcaptcha,
|
||||||
mCaptcha: instance.enableMcaptcha,
|
mCaptcha: instance.enableMcaptcha,
|
||||||
|
|
|
@ -614,6 +614,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
...(opts.includeSecrets ? {
|
...(opts.includeSecrets ? {
|
||||||
email: profile!.email,
|
email: profile!.email,
|
||||||
emailVerified: profile!.emailVerified,
|
emailVerified: profile!.emailVerified,
|
||||||
|
approved: user!.approved,
|
||||||
|
signupReason: user!.signupReason,
|
||||||
securityKeysList: profile!.twoFactorEnabled
|
securityKeysList: profile!.twoFactorEnabled
|
||||||
? this.userSecurityKeysRepository.find({
|
? this.userSecurityKeysRepository.find({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -184,6 +184,11 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public emailRequiredForSignup: boolean;
|
public emailRequiredForSignup: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public approvalRequiredForSignup: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -261,6 +261,16 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public token: string | null;
|
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>) {
|
constructor(data: Partial<MiUser>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
|
|
@ -38,4 +38,9 @@ export class MiUserPending {
|
||||||
length: 128,
|
length: 128,
|
||||||
})
|
})
|
||||||
public password: string;
|
public password: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1000,
|
||||||
|
})
|
||||||
|
public reason: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,10 @@ export const packedMetaLiteSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
approvalRequiredForSignup: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
enableHcaptcha: {
|
enableHcaptcha: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -275,6 +279,10 @@ export const packedMetaDetailedOnlySchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
approvalRequiredForSignup: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
localTimeline: {
|
localTimeline: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -93,6 +93,9 @@ export class ImportFollowingProcessorService {
|
||||||
// skip myself
|
// skip myself
|
||||||
if (target.id === job.data.user.id) return;
|
if (target.id === job.data.user.id) return;
|
||||||
|
|
||||||
|
// skip follows to not approved accounts
|
||||||
|
if (!target.approved) return;
|
||||||
|
|
||||||
this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`);
|
this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`);
|
||||||
|
|
||||||
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]);
|
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]);
|
||||||
|
|
|
@ -684,6 +684,7 @@ export class ActivityPubServerService {
|
||||||
usernameLower: request.params.user.toLowerCase(),
|
usernameLower: request.params.user.toLowerCase(),
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
|
approved: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.userInfo(request, reply, user);
|
return await this.userInfo(request, reply, user);
|
||||||
|
|
|
@ -82,6 +82,8 @@ import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user
|
||||||
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
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_showUsers from './endpoints/admin/show-users.js';
|
||||||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-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_declineUser from "./endpoints/admin/decline-user.js";
|
||||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-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_updateMeta from './endpoints/admin/update-meta.js';
|
||||||
import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js';
|
import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js';
|
||||||
|
@ -477,6 +479,8 @@ const $admin_showUserAccountMoveLogs: Provider = { provide: 'ep:admin/show-user-
|
||||||
const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default };
|
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_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_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_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_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
|
||||||
const $admin_updateUserName: Provider = { provide: 'ep:admin/update-user-name', useClass: ep___admin_updateUserName.default };
|
const $admin_updateUserName: Provider = { provide: 'ep:admin/update-user-name', useClass: ep___admin_updateUserName.default };
|
||||||
|
@ -876,6 +880,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_showUser,
|
$admin_showUser,
|
||||||
$admin_showUsers,
|
$admin_showUsers,
|
||||||
$admin_suspendUser,
|
$admin_suspendUser,
|
||||||
|
$admin_approveUser,
|
||||||
|
$admin_declineUser,
|
||||||
$admin_unsuspendUser,
|
$admin_unsuspendUser,
|
||||||
$admin_updateMeta,
|
$admin_updateMeta,
|
||||||
$admin_updateUserName,
|
$admin_updateUserName,
|
||||||
|
@ -1269,6 +1275,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_showUser,
|
$admin_showUser,
|
||||||
$admin_showUsers,
|
$admin_showUsers,
|
||||||
$admin_suspendUser,
|
$admin_suspendUser,
|
||||||
|
$admin_approveUser,
|
||||||
$admin_unsuspendUser,
|
$admin_unsuspendUser,
|
||||||
$admin_updateMeta,
|
$admin_updateMeta,
|
||||||
$admin_updateUserName,
|
$admin_updateUserName,
|
||||||
|
|
|
@ -79,6 +79,8 @@ export class SigninApiService {
|
||||||
reply.header('Access-Control-Allow-Origin', this.config.url);
|
reply.header('Access-Control-Allow-Origin', this.config.url);
|
||||||
reply.header('Access-Control-Allow-Credentials', 'true');
|
reply.header('Access-Control-Allow-Credentials', 'true');
|
||||||
|
|
||||||
|
const instance = await this.metaService.fetch(true);
|
||||||
|
|
||||||
const body = request.body;
|
const body = request.body;
|
||||||
const username = body['username'];
|
const username = body['username'];
|
||||||
const password = body['password'];
|
const password = body['password'];
|
||||||
|
@ -132,13 +134,13 @@ export class SigninApiService {
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
user: {
|
user: {
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
}
|
},
|
||||||
} : {
|
} : {
|
||||||
user: {
|
user: {
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const user = (profile?.user as MiLocalUser) ?? null;
|
const user = (profile?.user as MiLocalUser) ?? null;
|
||||||
|
|
||||||
|
@ -163,6 +165,17 @@ export class SigninApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Compare password
|
||||||
const same = await bcrypt.compare(password, profile.password!);
|
const same = await bcrypt.compare(password, profile.password!);
|
||||||
|
|
||||||
|
@ -207,6 +220,7 @@ export class SigninApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (same) {
|
if (same) {
|
||||||
|
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||||
logger.info('Successfully signed in with password.');
|
logger.info('Successfully signed in with password.');
|
||||||
return this.signinService.signin(request, reply, user);
|
return this.signinService.signin(request, reply, user);
|
||||||
} else {
|
} else {
|
||||||
|
@ -234,6 +248,7 @@ export class SigninApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||||
logger.info('Successfully signed in with password and two-factor token.');
|
logger.info('Successfully signed in with password and two-factor token.');
|
||||||
return this.signinService.signin(request, reply, user);
|
return this.signinService.signin(request, reply, user);
|
||||||
} else if (body.credential) {
|
} else if (body.credential) {
|
||||||
|
@ -247,6 +262,7 @@ export class SigninApiService {
|
||||||
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
|
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
|
||||||
|
|
||||||
if (authorized) {
|
if (authorized) {
|
||||||
|
if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
|
||||||
logger.info('Successfully signed in with WebAuthn authentication.');
|
logger.info('Successfully signed in with WebAuthn authentication.');
|
||||||
return this.signinService.signin(request, reply, user);
|
return this.signinService.signin(request, reply, user);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
@ -21,8 +22,8 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
|
import instance from './endpoints/charts/instance.js';
|
||||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import { randomUUID } from 'node:crypto';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SignupApiService {
|
export class SignupApiService {
|
||||||
|
@ -65,6 +66,7 @@ export class SignupApiService {
|
||||||
host?: string;
|
host?: string;
|
||||||
invitationCode?: string;
|
invitationCode?: string;
|
||||||
emailAddress?: string;
|
emailAddress?: string;
|
||||||
|
reason?: string;
|
||||||
'hcaptcha-response'?: string;
|
'hcaptcha-response'?: string;
|
||||||
'g-recaptcha-response'?: string;
|
'g-recaptcha-response'?: string;
|
||||||
'turnstile-response'?: string;
|
'turnstile-response'?: string;
|
||||||
|
@ -117,6 +119,7 @@ export class SignupApiService {
|
||||||
const password = body['password'];
|
const password = body['password'];
|
||||||
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null;
|
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null;
|
||||||
const invitationCode = body['invitationCode'];
|
const invitationCode = body['invitationCode'];
|
||||||
|
const reason = body['reason'];
|
||||||
const emailAddress = body['emailAddress'];
|
const emailAddress = body['emailAddress'];
|
||||||
|
|
||||||
if (instance.emailRequiredForSignup) {
|
if (instance.emailRequiredForSignup) {
|
||||||
|
@ -134,6 +137,13 @@ export class SignupApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (instance.approvalRequiredForSignup) {
|
||||||
|
if (reason == null || typeof reason !== 'string') {
|
||||||
|
reply.code(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ticket: MiRegistrationTicket | null = null;
|
let ticket: MiRegistrationTicket | null = null;
|
||||||
|
|
||||||
if (instance.disableRegistration) {
|
if (instance.disableRegistration) {
|
||||||
|
@ -211,6 +221,7 @@ export class SignupApiService {
|
||||||
email: emailAddress!,
|
email: emailAddress!,
|
||||||
username: username,
|
username: username,
|
||||||
password: hash,
|
password: hash,
|
||||||
|
reason: reason,
|
||||||
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
const link = `${this.config.url}/signup-complete/${code}`;
|
const link = `${this.config.url}/signup-complete/${code}`;
|
||||||
|
@ -233,9 +244,17 @@ export class SignupApiService {
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const { account, secret } = await this.signupService.signup({
|
const { account, secret } = await this.signupService.signup({
|
||||||
username, password, host,
|
username, password, host, reason
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (instance.approvalRequiredForSignup) {
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const res = await this.userEntityService.pack(account, account, {
|
const res = await this.userEntityService.pack(account, account, {
|
||||||
schema: 'MeDetailed',
|
schema: 'MeDetailed',
|
||||||
includeSecrets: true,
|
includeSecrets: true,
|
||||||
|
@ -272,6 +291,8 @@ export class SignupApiService {
|
||||||
|
|
||||||
const code = body['code'];
|
const code = body['code'];
|
||||||
|
|
||||||
|
const instance = await this.metaService.fetch(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code });
|
const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code });
|
||||||
|
|
||||||
|
@ -283,6 +304,7 @@ export class SignupApiService {
|
||||||
const { account, secret } = await this.signupService.signup({
|
const { account, secret } = await this.signupService.signup({
|
||||||
username: pendingUser.username,
|
username: pendingUser.username,
|
||||||
passwordHash: pendingUser.password,
|
passwordHash: pendingUser.password,
|
||||||
|
reason: pendingUser.reason,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userPendingsRepository.delete({
|
this.userPendingsRepository.delete({
|
||||||
|
@ -306,6 +328,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 };
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('Successfully created user.', { userId: account.id });
|
logger.info('Successfully created user.', { userId: account.id });
|
||||||
return this.signinService.signin(request, reply, account as MiLocalUser);
|
return this.signinService.signin(request, reply, account as MiLocalUser);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -82,6 +82,8 @@ import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user
|
||||||
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
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_showUsers from './endpoints/admin/show-users.js';
|
||||||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-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_declineUser from './endpoints/admin/decline-user.js';
|
||||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-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_updateMeta from './endpoints/admin/update-meta.js';
|
||||||
import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js';
|
import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js';
|
||||||
|
@ -475,6 +477,8 @@ const eps = [
|
||||||
['admin/show-user', ep___admin_showUser],
|
['admin/show-user', ep___admin_showUser],
|
||||||
['admin/show-users', ep___admin_showUsers],
|
['admin/show-users', ep___admin_showUsers],
|
||||||
['admin/suspend-user', ep___admin_suspendUser],
|
['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/unsuspend-user', ep___admin_unsuspendUser],
|
||||||
['admin/update-meta', ep___admin_updateMeta],
|
['admin/update-meta', ep___admin_updateMeta],
|
||||||
['admin/update-user-name', ep___admin_updateUserName],
|
['admin/update-user-name', ep___admin_updateUserName],
|
||||||
|
|
|
@ -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,79 @@
|
||||||
|
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 });
|
||||||
|
|
||||||
|
|
||||||
|
//Actually delete it since the last function doesnt actually delete the account
|
||||||
|
//Note: Before approval these accounts wont federate so this is totally fine.
|
||||||
|
await this.usersRepository.delete(user.id);
|
||||||
|
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'decline', {
|
||||||
|
userId: user.id,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
approvalRequiredForSignup: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
enableHcaptcha: {
|
enableHcaptcha: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -552,6 +556,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
privacyPolicyUrl: instance.privacyPolicyUrl,
|
privacyPolicyUrl: instance.privacyPolicyUrl,
|
||||||
disableRegistration: instance.disableRegistration,
|
disableRegistration: instance.disableRegistration,
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
|
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
enableMcaptcha: instance.enableMcaptcha,
|
enableMcaptcha: instance.enableMcaptcha,
|
||||||
|
|
|
@ -180,7 +180,7 @@ export const meta = {
|
||||||
memo: {
|
memo: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -240,6 +240,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return {
|
return {
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
emailVerified: profile.emailVerified,
|
emailVerified: profile.emailVerified,
|
||||||
|
approved: user.approved,
|
||||||
|
signupReason: user.signupReason,
|
||||||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||||
noCrawle: profile.noCrawle,
|
noCrawle: profile.noCrawle,
|
||||||
preventAiLearning: profile.preventAiLearning,
|
preventAiLearning: profile.preventAiLearning,
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const paramDef = {
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
offset: { type: 'integer', default: 0 },
|
offset: { type: 'integer', default: 0 },
|
||||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+lastActiveDate', '-lastActiveDate'] },
|
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' },
|
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
|
||||||
username: { type: 'string', nullable: true, default: null },
|
username: { type: 'string', nullable: true, default: null },
|
||||||
hostname: {
|
hostname: {
|
||||||
|
@ -64,6 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
case 'available': query.where('user.isSuspended = FALSE'); break;
|
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 '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 'suspended': query.where('user.isSuspended = TRUE'); break;
|
||||||
|
case 'approved': query.where('user.approved = FALSE'); break;
|
||||||
case 'admin': {
|
case 'admin': {
|
||||||
const adminIds = await this.roleService.getAdministratorIds();
|
const adminIds = await this.roleService.getAdministratorIds();
|
||||||
if (adminIds.length === 0) return [];
|
if (adminIds.length === 0) return [];
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const paramDef = {
|
||||||
cacheRemoteFiles: { type: 'boolean' },
|
cacheRemoteFiles: { type: 'boolean' },
|
||||||
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
||||||
emailRequiredForSignup: { type: 'boolean' },
|
emailRequiredForSignup: { type: 'boolean' },
|
||||||
|
approvalRequiredForSignup: { type: 'boolean' },
|
||||||
enableHcaptcha: { type: 'boolean' },
|
enableHcaptcha: { type: 'boolean' },
|
||||||
hcaptchaSiteKey: { type: 'string', nullable: true },
|
hcaptchaSiteKey: { type: 'string', nullable: true },
|
||||||
hcaptchaSecretKey: { type: 'string', nullable: true },
|
hcaptchaSecretKey: { type: 'string', nullable: true },
|
||||||
|
@ -323,6 +324,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.approvalRequiredForSignup !== undefined) {
|
||||||
|
set.approvalRequiredForSignup = ps.approvalRequiredForSignup;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.enableHcaptcha !== undefined) {
|
if (ps.enableHcaptcha !== undefined) {
|
||||||
set.enableHcaptcha = ps.enableHcaptcha;
|
set.enableHcaptcha = ps.enableHcaptcha;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,12 @@ export const meta = {
|
||||||
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
|
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
unapprovedUser: {
|
||||||
|
message: 'The user has not been approved yet.',
|
||||||
|
code: 'UNAPPROVED_USER',
|
||||||
|
id: '8d66f136-b3e1-48fd-92c4-30ecfd7fdb7a',
|
||||||
|
},
|
||||||
|
|
||||||
followeeIsYourself: {
|
followeeIsYourself: {
|
||||||
message: 'Followee is yourself.',
|
message: 'Followee is yourself.',
|
||||||
code: 'FOLLOWEE_IS_YOURSELF',
|
code: 'FOLLOWEE_IS_YOURSELF',
|
||||||
|
@ -101,15 +107,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get followee
|
// Get followee
|
||||||
const followee = await this.getterService.getUser(ps.userId).catch(err => {
|
let followee;
|
||||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
try {
|
||||||
|
followee = await this.getterService.getUser(ps.userId);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') {
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
}
|
||||||
|
|
||||||
if ( me.isBot && followee.isBot ) {
|
if ( me.isBot && followee.isBot ) {
|
||||||
throw new ApiError(meta.errors.followingAnotherBot);
|
throw new ApiError(meta.errors.followingAnotherBot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLocalUser = followee.uri == null;
|
||||||
|
if (!followee.approved && isLocalUser) {
|
||||||
|
throw new ApiError(meta.errors.unapprovedUser);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies });
|
await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -54,6 +54,8 @@ export const followersVisibilities = ['public', 'followers', 'private'] as const
|
||||||
export const moderationLogTypes = [
|
export const moderationLogTypes = [
|
||||||
'updateServerSettings',
|
'updateServerSettings',
|
||||||
'suspend',
|
'suspend',
|
||||||
|
'approve',
|
||||||
|
'decline',
|
||||||
'unsuspend',
|
'unsuspend',
|
||||||
'updateUserName',
|
'updateUserName',
|
||||||
'updateUserNote',
|
'updateUserNote',
|
||||||
|
@ -111,6 +113,16 @@ export type ModerationLogPayloads = {
|
||||||
userUsername: string;
|
userUsername: string;
|
||||||
userHost: string | null;
|
userHost: string | null;
|
||||||
};
|
};
|
||||||
|
approve: {
|
||||||
|
userId: string;
|
||||||
|
userUsername: string;
|
||||||
|
userHost: string | null;
|
||||||
|
};
|
||||||
|
decline: {
|
||||||
|
userId: string;
|
||||||
|
userUsername: string;
|
||||||
|
userHost: string | null;
|
||||||
|
}
|
||||||
unsuspend: {
|
unsuspend: {
|
||||||
userId: string;
|
userId: string;
|
||||||
userUsername: string;
|
userUsername: string;
|
||||||
|
|
|
@ -45,6 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</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="ti ti-help-circle"></i></div></template>
|
||||||
|
<template #prefix><i class="ti ti-message-question"></i></template>
|
||||||
|
</MkInput>
|
||||||
<MkNewPassword ref="password" :label="i18n.ts.password"/>
|
<MkNewPassword ref="password" :label="i18n.ts.password"/>
|
||||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
<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.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||||
|
@ -85,6 +89,7 @@ const props = withDefaults(defineProps<{
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'signup', user: Misskey.entities.SigninResponse): void;
|
(ev: 'signup', user: Misskey.entities.SigninResponse): void;
|
||||||
(ev: 'signupEmailPending'): void;
|
(ev: 'signupEmailPending'): void;
|
||||||
|
(ev: 'approvalPending'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const host = toUnicode(config.host);
|
const host = toUnicode(config.host);
|
||||||
|
@ -97,6 +102,7 @@ const turnstile = ref<Captcha | undefined>();
|
||||||
const username = ref<string>('');
|
const username = ref<string>('');
|
||||||
const password = shallowRef<InstanceType<typeof MkNewPassword> | null>(null);
|
const password = shallowRef<InstanceType<typeof MkNewPassword> | null>(null);
|
||||||
const invitationCode = ref<string>('');
|
const invitationCode = ref<string>('');
|
||||||
|
let reason = ref<string>('');
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null);
|
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);
|
const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:banned' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
|
||||||
|
@ -195,6 +201,7 @@ async function onSubmit(): Promise<void> {
|
||||||
password: password.value.password,
|
password: password.value.password,
|
||||||
emailAddress: email.value,
|
emailAddress: email.value,
|
||||||
invitationCode: invitationCode.value,
|
invitationCode: invitationCode.value,
|
||||||
|
reason: reason.value,
|
||||||
'hcaptcha-response': hCaptchaResponse.value,
|
'hcaptcha-response': hCaptchaResponse.value,
|
||||||
'm-captcha-response': mCaptchaResponse.value,
|
'm-captcha-response': mCaptchaResponse.value,
|
||||||
'g-recaptcha-response': reCaptchaResponse.value,
|
'g-recaptcha-response': reCaptchaResponse.value,
|
||||||
|
@ -207,6 +214,13 @@ async function onSubmit(): Promise<void> {
|
||||||
text: i18n.tsx._signup.emailSent({ email: email.value }),
|
text: i18n.tsx._signup.emailSent({ email: email.value }),
|
||||||
});
|
});
|
||||||
emit('signupEmailPending');
|
emit('signupEmailPending');
|
||||||
|
} else if (instance.approvalRequiredForSignup) {
|
||||||
|
os.alert({
|
||||||
|
type: 'success',
|
||||||
|
title: i18n.ts._signup.almostThere,
|
||||||
|
text: i18n.ts._signup.approvalPending,
|
||||||
|
});
|
||||||
|
emit('approvalPending');
|
||||||
} else {
|
} else {
|
||||||
emit('signup', { id: res.id, i: res.token });
|
emit('signup', { id: res.id, i: res.token });
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/>
|
<XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending" @approvalPending="onApprovalPending"/>
|
||||||
</template>
|
</template>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,6 +69,10 @@ function onSignup(res: Misskey.entities.SignupResponse) {
|
||||||
function onSignupEmailPending() {
|
function onSignupEmailPending() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onApprovalPending() {
|
||||||
|
dialog.value?.close();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -113,5 +113,6 @@ onUnmounted(() => {
|
||||||
border: solid 0.5px var(--divider);
|
border: solid 0.5px var(--divider);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,6 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="instance.disableRegistration" :class="$style.mainWarn">
|
<div v-if="instance.disableRegistration" :class="$style.mainWarn">
|
||||||
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="instance.approvalRequiredForSignup" :class="$style.mainWarn">
|
||||||
|
<MkInfo warn>{{ i18n.ts.approvalRequiredToRegister }}</MkInfo>
|
||||||
|
</div>
|
||||||
<div class="_gaps_s" :class="$style.mainActions">
|
<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 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>
|
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||||
|
|
110
packages/frontend/src/components/SkApprovalUser.vue
Normal file
110
packages/frontend/src/components/SkApprovalUser.vue
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: marie and other Sharkey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkFolder :expanded="false">
|
||||||
|
<template #icon><i class="ti ti-user"></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">Reason</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;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let reason = ref('');
|
||||||
|
let 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;
|
||||||
|
|
||||||
|
await os.apiWithDialog('admin/decline-user', {
|
||||||
|
userId: props.user.id,
|
||||||
|
});
|
||||||
|
emits('deleted', props.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function approveAccount() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.approveConfirm,
|
||||||
|
});
|
||||||
|
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>
|
|
@ -15,6 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||||
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||||
<span class="state">
|
<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="admin" class="admin">Admin</span>
|
<span v-if="admin" class="admin">Admin</span>
|
||||||
<span v-if="moderator" class="moderator">Moderator</span>
|
<span v-if="moderator" class="moderator">Moderator</span>
|
||||||
<span v-if="silenced" class="silenced">Silenced</span>
|
<span v-if="silenced" class="silenced">Silenced</span>
|
||||||
|
@ -219,6 +221,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkObjectView tall :value="user">
|
<MkObjectView tall :value="user">
|
||||||
</MkObjectView>
|
</MkObjectView>
|
||||||
</div>
|
</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>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
|
@ -267,10 +283,13 @@ const ap = ref<any>(null);
|
||||||
const admin = ref(false);
|
const admin = ref(false);
|
||||||
const moderator = ref(false);
|
const moderator = ref(false);
|
||||||
const silenced = ref(false);
|
const silenced = ref(false);
|
||||||
|
const approved = ref(false);
|
||||||
const limited = ref(false);
|
const limited = ref(false);
|
||||||
const suspended = ref(false);
|
const suspended = ref(false);
|
||||||
const deleted = ref(false);
|
const deleted = ref(false);
|
||||||
const moderationNote = ref('');
|
const moderationNote = ref('');
|
||||||
|
const signupReason = ref('');
|
||||||
|
|
||||||
const filesPagination = {
|
const filesPagination = {
|
||||||
endpoint: 'admin/drive/files' as const,
|
endpoint: 'admin/drive/files' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
@ -303,10 +322,12 @@ function createFetcher() {
|
||||||
admin.value = info.value.isAdmin;
|
admin.value = info.value.isAdmin;
|
||||||
moderator.value = info.value.isModerator;
|
moderator.value = info.value.isModerator;
|
||||||
silenced.value = info.value.isSilenced;
|
silenced.value = info.value.isSilenced;
|
||||||
|
approved.value = info.value.approved;
|
||||||
limited.value = info.value.isLimited;
|
limited.value = info.value.isLimited;
|
||||||
suspended.value = info.value.isSuspended;
|
suspended.value = info.value.isSuspended;
|
||||||
deleted.value = info.value.isDeleted;
|
deleted.value = info.value.isDeleted;
|
||||||
moderationNote.value = info.value.moderationNote;
|
moderationNote.value = info.value.moderationNote;
|
||||||
|
signupReason.value = info.value.signupReason;
|
||||||
|
|
||||||
watch(moderationNote, async () => {
|
watch(moderationNote, async () => {
|
||||||
await misskeyApi('admin/update-user-note', {
|
await misskeyApi('admin/update-user-note', {
|
||||||
|
@ -470,6 +491,21 @@ async function deleteAccount(soft: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function approveAccount() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.approveConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
|
||||||
|
await os.apiWithDialog('admin/approve-user', {
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.approveConfirm,
|
||||||
|
userId: user.value.id,
|
||||||
|
});
|
||||||
|
await refreshUser();
|
||||||
|
}
|
||||||
|
|
||||||
async function assignRole() {
|
async function assignRole() {
|
||||||
const roles = await misskeyApi('admin/roles/list');
|
const roles = await misskeyApi('admin/roles/list');
|
||||||
|
|
||||||
|
@ -573,35 +609,79 @@ watch(user, () => {
|
||||||
|
|
||||||
const headerActions = computed(() => []);
|
const headerActions = computed(() => []);
|
||||||
|
|
||||||
const headerTabs = computed(() => [{
|
const headerTabs = computed(() => iAmAdmin && !approved.value ?
|
||||||
|
[
|
||||||
|
{
|
||||||
key: 'overview',
|
key: 'overview',
|
||||||
title: i18n.ts.overview,
|
title: i18n.ts.overview,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
key: 'roles',
|
key: 'roles',
|
||||||
title: i18n.ts.roles,
|
title: i18n.ts.roles,
|
||||||
icon: 'ti ti-badges',
|
icon: 'ti ti-badges',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
key: 'announcements',
|
key: 'announcements',
|
||||||
title: i18n.ts.announcements,
|
title: i18n.ts.announcements,
|
||||||
icon: 'ti ti-speakerphone',
|
icon: 'ti ti-speakerphone',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
key: 'drive',
|
key: 'drive',
|
||||||
title: i18n.ts.drive,
|
title: i18n.ts.drive,
|
||||||
icon: 'ti ti-cloud',
|
icon: 'ti ti-cloud',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
key: 'chart',
|
key: 'chart',
|
||||||
title: i18n.ts.charts,
|
title: i18n.ts.charts,
|
||||||
icon: 'ti ti-chart-line',
|
icon: 'ti ti-chart-line',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
key: 'activitypub',
|
key: 'activitypub',
|
||||||
title: 'ActivityPub',
|
title: 'ActivityPub',
|
||||||
icon: 'ti ti-share',
|
icon: 'ti ti-share',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
key: 'raw',
|
key: 'raw',
|
||||||
title: 'Raw',
|
title: 'Raw',
|
||||||
icon: 'ti ti-code',
|
icon: 'ti ti-code',
|
||||||
}]);
|
},
|
||||||
|
{
|
||||||
|
key: 'approval',
|
||||||
|
title: 'Approval',
|
||||||
|
icon: 'ti ti-scan-eye',
|
||||||
|
}
|
||||||
|
] : [
|
||||||
|
{
|
||||||
|
key: 'overview',
|
||||||
|
title: i18n.ts.overview,
|
||||||
|
icon: 'ti ti-info-circle',
|
||||||
|
}, {
|
||||||
|
key: 'roles',
|
||||||
|
title: i18n.ts.roles,
|
||||||
|
icon: 'ti ti-badges',
|
||||||
|
}, {
|
||||||
|
key: 'announcements',
|
||||||
|
title: i18n.ts.announcements,
|
||||||
|
icon: 'ti ti-speakerphone',
|
||||||
|
}, {
|
||||||
|
key: 'drive',
|
||||||
|
title: i18n.ts.drive,
|
||||||
|
icon: 'ti ti-cloud',
|
||||||
|
}, {
|
||||||
|
key: 'chart',
|
||||||
|
title: i18n.ts.charts,
|
||||||
|
icon: 'ti ti-chart-line',
|
||||||
|
}, {
|
||||||
|
key: 'activitypub',
|
||||||
|
title: 'ActivityPub',
|
||||||
|
icon: 'ti ti-share',
|
||||||
|
}, {
|
||||||
|
key: 'raw',
|
||||||
|
title: 'Raw',
|
||||||
|
icon: 'ti ti-code',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
definePageMetadata(() => ({
|
definePageMetadata(() => ({
|
||||||
title: user.value ? acct(user.value) : i18n.ts.userInfo,
|
title: user.value ? acct(user.value) : i18n.ts.userInfo,
|
||||||
|
@ -769,4 +849,16 @@ definePageMetadata(() => ({
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.casdwq {
|
||||||
|
.silenced {
|
||||||
|
color: var(--warn);
|
||||||
|
border-color: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.moderator {
|
||||||
|
color: var(--success);
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
74
packages/frontend/src/pages/admin/approvals.vue
Normal file
74
packages/frontend/src/pages/admin/approvals.vue
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: marie and other Sharkey 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">
|
||||||
|
<SkApprovalUser 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 SkApprovalUser from '@/components/SkApprovalUser.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 = paginationComponent.value.items.filter(
|
||||||
|
(item: any) => item.id !== id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerActions = computed(() => []);
|
||||||
|
|
||||||
|
const headerTabs = computed(() => []);
|
||||||
|
|
||||||
|
definePageMetadata(() => ({
|
||||||
|
title: i18n.ts.approvals,
|
||||||
|
icon: 'ti ti-user-eye',
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||||
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||||
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||||
|
<MkInfo v-if="pendingUserApprovals" warn class="info">{{ i18n.ts.pendingUserApprovals }} <MkA to="/admin/approvals" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
|
||||||
|
|
||||||
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,6 +62,7 @@ let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instan
|
||||||
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
|
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
|
||||||
let noEmailServer = !instance.enableEmail;
|
let noEmailServer = !instance.enableEmail;
|
||||||
const thereIsUnresolvedAbuseReport = ref(false);
|
const thereIsUnresolvedAbuseReport = ref(false);
|
||||||
|
const pendingUserApprovals = ref(false);
|
||||||
const currentPage = computed(() => router.currentRef.value.child);
|
const currentPage = computed(() => router.currentRef.value.child);
|
||||||
|
|
||||||
misskeyApi('admin/abuse-user-reports', {
|
misskeyApi('admin/abuse-user-reports', {
|
||||||
|
@ -70,6 +72,14 @@ misskeyApi('admin/abuse-user-reports', {
|
||||||
if (reports.length > 0) thereIsUnresolvedAbuseReport.value = true;
|
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 NARROW_THRESHOLD = 600;
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
const ro = new ResizeObserver((entries, observer) => {
|
||||||
if (entries.length === 0) return;
|
if (entries.length === 0) return;
|
||||||
|
@ -106,6 +116,11 @@ const menuDef = computed(() => [{
|
||||||
text: i18n.ts.invite,
|
text: i18n.ts.invite,
|
||||||
to: '/admin/invites',
|
to: '/admin/invites',
|
||||||
active: currentPage.value?.route.name === 'invites',
|
active: currentPage.value?.route.name === 'invites',
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-user-scan',
|
||||||
|
text: i18n.ts.approvals,
|
||||||
|
to: '/admin/approvals',
|
||||||
|
active: currentPage.value?.route.name === 'approvals',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-badges',
|
icon: 'ti ti-badges',
|
||||||
text: i18n.ts.roles,
|
text: i18n.ts.roles,
|
||||||
|
|
|
@ -18,6 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
|
<MkSwitch v-model="approvalRequiredForSignup">
|
||||||
|
<template #label>{{ i18n.ts.approvalRequiredForSignup }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
|
||||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||||
|
|
||||||
<MkInput v-model="tosUrl" type="url">
|
<MkInput v-model="tosUrl" type="url">
|
||||||
|
@ -90,6 +94,7 @@ import FormLink from '@/components/form/link.vue';
|
||||||
|
|
||||||
const enableRegistration = ref<boolean>(false);
|
const enableRegistration = ref<boolean>(false);
|
||||||
const emailRequiredForSignup = ref<boolean>(false);
|
const emailRequiredForSignup = ref<boolean>(false);
|
||||||
|
const approvalRequiredForSignup: boolean = ref<boolean>(false);
|
||||||
const sensitiveWords = ref<string>('');
|
const sensitiveWords = ref<string>('');
|
||||||
const prohibitedWords = ref<string>('');
|
const prohibitedWords = ref<string>('');
|
||||||
const hiddenTags = ref<string>('');
|
const hiddenTags = ref<string>('');
|
||||||
|
@ -103,6 +108,7 @@ async function init() {
|
||||||
const meta = await misskeyApi('admin/meta');
|
const meta = await misskeyApi('admin/meta');
|
||||||
enableRegistration.value = !meta.disableRegistration;
|
enableRegistration.value = !meta.disableRegistration;
|
||||||
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
||||||
|
approvalRequiredForSignup.value = meta.approvalRequiredForSignup;
|
||||||
sensitiveWords.value = meta.sensitiveWords.join('\n');
|
sensitiveWords.value = meta.sensitiveWords.join('\n');
|
||||||
prohibitedWords.value = meta.prohibitedWords.join('\n');
|
prohibitedWords.value = meta.prohibitedWords.join('\n');
|
||||||
hiddenTags.value = meta.hiddenTags.join('\n');
|
hiddenTags.value = meta.hiddenTags.join('\n');
|
||||||
|
@ -117,6 +123,7 @@ function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
disableRegistration: !enableRegistration.value,
|
disableRegistration: !enableRegistration.value,
|
||||||
emailRequiredForSignup: emailRequiredForSignup.value,
|
emailRequiredForSignup: emailRequiredForSignup.value,
|
||||||
|
approvalRequiredForSignup: approvalRequiredForSignup.value,
|
||||||
tosUrl: tosUrl.value,
|
tosUrl: tosUrl.value,
|
||||||
privacyPolicyUrl: privacyPolicyUrl.value,
|
privacyPolicyUrl: privacyPolicyUrl.value,
|
||||||
sensitiveWords: sensitiveWords.value.split('\n'),
|
sensitiveWords: sensitiveWords.value.split('\n'),
|
||||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:class="{
|
:class="{
|
||||||
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
|
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
|
||||||
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword', 'regenerateUserToken', 'updateUserName', 'unsetUserAvatar', 'unsetUserBanner', 'unsetUserMutualLink'].includes(log.type),
|
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword', 'regenerateUserToken', 'updateUserName', 'unsetUserAvatar', 'unsetUserBanner', 'unsetUserMutualLink'].includes(log.type),
|
||||||
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
|
[$style.logRed]: ['suspend', 'approve', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
|
||||||
}"
|
}"
|
||||||
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
|
@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="log.type === 'unsetUserBanner'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'unsetUserBanner'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'unsetUserMutualLink'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'unsetUserMutualLink'">: @{{ 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 === '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 === '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 === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'regenerateUserToken'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'regenerateUserToken'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
|
@ -79,6 +80,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-else-if="log.type === 'suspend'">
|
<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>
|
<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>
|
||||||
|
<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'">
|
<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>
|
<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>
|
||||||
|
|
|
@ -46,6 +46,13 @@ function submit() {
|
||||||
misskeyApi('signup-pending', {
|
misskeyApi('signup-pending', {
|
||||||
code: props.code,
|
code: props.code,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
|
if (res.pendingApproval) {
|
||||||
|
return os.alert({
|
||||||
|
type: 'success',
|
||||||
|
title: i18n.ts._signup.almostThere,
|
||||||
|
text: i18n.ts._signup.approvalPending,
|
||||||
|
});
|
||||||
|
}
|
||||||
return login(res.i, '/');
|
return login(res.i, '/');
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
|
|
|
@ -482,11 +482,15 @@ const routes: RouteDef[] = [{
|
||||||
path: '/invites',
|
path: '/invites',
|
||||||
name: 'invites',
|
name: 'invites',
|
||||||
component: page(() => import('@/pages/admin/invites.vue')),
|
component: page(() => import('@/pages/admin/invites.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/approvals',
|
||||||
|
name: 'approvals',
|
||||||
|
component: page(() => import('@/pages/admin/approvals.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/',
|
path: '/',
|
||||||
component: page(() => import('@/pages/_empty_.vue')),
|
component: page(() => import('@/pages/_empty_.vue')),
|
||||||
}],
|
}],
|
||||||
}, {
|
},{
|
||||||
path: '/my/notifications',
|
path: '/my/notifications',
|
||||||
component: page(() => import('@/pages/notifications.vue')),
|
component: page(() => import('@/pages/notifications.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
|
|
@ -116,6 +116,9 @@ type AdminAnnouncementsListResponse = operations['admin___announcements___list']
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json'];
|
type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
|
type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -134,6 +137,9 @@ type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json'];
|
type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminDriveDeleteAllFilesOfAUserRequest = operations['admin___drive___delete-all-files-of-a-user']['requestBody']['content']['application/json'];
|
type AdminDriveDeleteAllFilesOfAUserRequest = operations['admin___drive___delete-all-files-of-a-user']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -1325,6 +1331,8 @@ declare namespace entities {
|
||||||
AdminShowUsersRequest,
|
AdminShowUsersRequest,
|
||||||
AdminShowUsersResponse,
|
AdminShowUsersResponse,
|
||||||
AdminSuspendUserRequest,
|
AdminSuspendUserRequest,
|
||||||
|
AdminApproveUserRequest,
|
||||||
|
AdminDeclineUserRequest,
|
||||||
AdminUnsuspendUserRequest,
|
AdminUnsuspendUserRequest,
|
||||||
AdminUpdateMetaRequest,
|
AdminUpdateMetaRequest,
|
||||||
AdminUpdateUserNameRequest,
|
AdminUpdateUserNameRequest,
|
||||||
|
@ -2544,7 +2552,7 @@ type ModerationLog = {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserName", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "regenerateUserToken", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createSSOServiceProvider", "updateSSOServiceProvider", "deleteSSOServiceProvider", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "unsetUserMutualBanner"];
|
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "approve", "decline", "unsuspend", "updateUserName", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "regenerateUserToken", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createSSOServiceProvider", "updateSSOServiceProvider", "deleteSSOServiceProvider", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "unsetUserMutualBanner"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
|
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
|
||||||
|
@ -2796,7 +2804,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
|
||||||
function parse(acct: string): Acct;
|
function parse(acct: string): Acct;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @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", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "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:indie-auth", "read:admin:indie-auth", "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:sso", "read:admin:sso", "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", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "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:indie-auth", "read:admin:indie-auth", "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:sso", "read:admin:sso", "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)
|
// @public (undocumented)
|
||||||
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
||||||
|
|
|
@ -840,6 +840,28 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): 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.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
|
|
@ -103,6 +103,8 @@ import type {
|
||||||
AdminShowUsersRequest,
|
AdminShowUsersRequest,
|
||||||
AdminShowUsersResponse,
|
AdminShowUsersResponse,
|
||||||
AdminSuspendUserRequest,
|
AdminSuspendUserRequest,
|
||||||
|
AdminApproveUserRequest,
|
||||||
|
AdminDeclineUserRequest,
|
||||||
AdminUnsuspendUserRequest,
|
AdminUnsuspendUserRequest,
|
||||||
AdminUpdateMetaRequest,
|
AdminUpdateMetaRequest,
|
||||||
AdminUpdateUserNameRequest,
|
AdminUpdateUserNameRequest,
|
||||||
|
@ -667,6 +669,8 @@ export type Endpoints = {
|
||||||
'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse };
|
'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse };
|
||||||
'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse };
|
'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse };
|
||||||
'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse };
|
'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/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse };
|
||||||
'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse };
|
'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse };
|
||||||
'admin/update-user-name': { req: AdminUpdateUserNameRequest; res: EmptyResponse };
|
'admin/update-user-name': { req: AdminUpdateUserNameRequest; res: EmptyResponse };
|
||||||
|
|
|
@ -106,6 +106,8 @@ export type AdminShowUserResponse = operations['admin___show-user']['responses']
|
||||||
export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json'];
|
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 AdminShowUsersResponse = operations['admin___show-users']['responses']['200']['content']['application/json'];
|
||||||
export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['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 AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
|
||||||
export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
|
export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
|
||||||
export type AdminUpdateUserNameRequest = operations['admin___update-user-name']['requestBody']['content']['application/json'];
|
export type AdminUpdateUserNameRequest = operations['admin___update-user-name']['requestBody']['content']['application/json'];
|
||||||
|
|
|
@ -697,6 +697,24 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['admin___suspend-user'];
|
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': {
|
||||||
/**
|
/**
|
||||||
* admin/unsuspend-user
|
* admin/unsuspend-user
|
||||||
|
@ -5111,6 +5129,7 @@ export type components = {
|
||||||
defaultLightTheme: string | null;
|
defaultLightTheme: string | null;
|
||||||
disableRegistration: boolean;
|
disableRegistration: boolean;
|
||||||
emailRequiredForSignup: boolean;
|
emailRequiredForSignup: boolean;
|
||||||
|
approvalRequiredForSignup: boolean;
|
||||||
enableHcaptcha: boolean;
|
enableHcaptcha: boolean;
|
||||||
hcaptchaSiteKey: string | null;
|
hcaptchaSiteKey: string | null;
|
||||||
enableMcaptcha: boolean;
|
enableMcaptcha: boolean;
|
||||||
|
@ -5166,6 +5185,7 @@ export type components = {
|
||||||
features?: {
|
features?: {
|
||||||
registration: boolean;
|
registration: boolean;
|
||||||
emailRequiredForSignup: boolean;
|
emailRequiredForSignup: boolean;
|
||||||
|
approvalRequiredForSignup: boolean;
|
||||||
localTimeline: boolean;
|
localTimeline: boolean;
|
||||||
globalTimeline: boolean;
|
globalTimeline: boolean;
|
||||||
hCaptcha: boolean;
|
hCaptcha: boolean;
|
||||||
|
@ -5258,6 +5278,7 @@ export type operations = {
|
||||||
cacheRemoteFiles: boolean;
|
cacheRemoteFiles: boolean;
|
||||||
cacheRemoteSensitiveFiles: boolean;
|
cacheRemoteSensitiveFiles: boolean;
|
||||||
emailRequiredForSignup: boolean;
|
emailRequiredForSignup: boolean;
|
||||||
|
approvalRequiredForSignup: boolean;
|
||||||
enableHcaptcha: boolean;
|
enableHcaptcha: boolean;
|
||||||
hcaptchaSiteKey: string | null;
|
hcaptchaSiteKey: string | null;
|
||||||
enableMcaptcha: boolean;
|
enableMcaptcha: boolean;
|
||||||
|
@ -9887,7 +9908,7 @@ export type operations = {
|
||||||
* @default all
|
* @default all
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended';
|
state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended' | 'approved';
|
||||||
/**
|
/**
|
||||||
* @default combined
|
* @default combined
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
|
@ -9994,6 +10015,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
|
* admin/unsuspend-user
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
@ -10081,6 +10206,7 @@ export type operations = {
|
||||||
cacheRemoteFiles?: boolean;
|
cacheRemoteFiles?: boolean;
|
||||||
cacheRemoteSensitiveFiles?: boolean;
|
cacheRemoteSensitiveFiles?: boolean;
|
||||||
emailRequiredForSignup?: boolean;
|
emailRequiredForSignup?: boolean;
|
||||||
|
approvalRequiredForSignup?: boolean;
|
||||||
enableHcaptcha?: boolean;
|
enableHcaptcha?: boolean;
|
||||||
hcaptchaSiteKey?: string | null;
|
hcaptchaSiteKey?: string | null;
|
||||||
hcaptchaSecretKey?: string | null;
|
hcaptchaSecretKey?: string | null;
|
||||||
|
|
|
@ -62,6 +62,8 @@ export const permissions = [
|
||||||
'read:admin:show-user',
|
'read:admin:show-user',
|
||||||
'read:admin:show-users',
|
'read:admin:show-users',
|
||||||
'write:admin:suspend-user',
|
'write:admin:suspend-user',
|
||||||
|
'write:admin:approve-user',
|
||||||
|
'write:admin:decline-user',
|
||||||
'write:admin:unsuspend-user',
|
'write:admin:unsuspend-user',
|
||||||
'write:admin:meta',
|
'write:admin:meta',
|
||||||
'write:admin:user-name',
|
'write:admin:user-name',
|
||||||
|
@ -106,6 +108,8 @@ export const permissions = [
|
||||||
export const moderationLogTypes = [
|
export const moderationLogTypes = [
|
||||||
'updateServerSettings',
|
'updateServerSettings',
|
||||||
'suspend',
|
'suspend',
|
||||||
|
'approve',
|
||||||
|
'decline',
|
||||||
'unsuspend',
|
'unsuspend',
|
||||||
'updateUserName',
|
'updateUserName',
|
||||||
'updateUserNote',
|
'updateUserNote',
|
||||||
|
@ -163,6 +167,11 @@ export type ModerationLogPayloads = {
|
||||||
userUsername: string;
|
userUsername: string;
|
||||||
userHost: string | null;
|
userHost: string | null;
|
||||||
};
|
};
|
||||||
|
approve: {
|
||||||
|
userId: string;
|
||||||
|
userUsername: string;
|
||||||
|
userHost: string | null;
|
||||||
|
};
|
||||||
unsuspend: {
|
unsuspend: {
|
||||||
userId: string;
|
userId: string;
|
||||||
userUsername: string;
|
userUsername: string;
|
||||||
|
|
Loading…
Reference in a new issue