From d84bff37ec84fd2293c64e07569edf361d0c8c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B4=EB=9D=BC=EC=BF=A0=EB=AA=A8?= Date: Sat, 7 Sep 2024 22:32:45 +0900 Subject: [PATCH] refactor(account): account delete, truncate, auto note removal --- locales/en-US.yml | 44 ++++--- locales/index.d.ts | 72 ++++++++---- locales/ja-JP.yml | 46 +++++--- locales/ko-KR.yml | 34 +++--- .../1725706236633-AutoNoteRemoval.js | 8 +- packages/backend/src/core/RoleService.ts | 10 +- .../backend/src/models/json-schema/role.ts | 16 ++- .../src/queue/QueueProcessorService.ts | 2 +- .../AutoNoteRemovalProcessorService.ts | 14 ++- .../server/api/endpoints/i/delete-account.ts | 12 +- .../api/endpoints/i/truncate-account.ts | 43 ++++--- packages/frontend/src/const.ts | 4 +- .../frontend/src/pages/admin/roles.editor.vue | 84 +++++++++---- packages/frontend/src/pages/admin/roles.vue | 60 ++++++---- .../src/pages/settings/laboratory.vue | 84 +++++++------ .../frontend/src/pages/settings/other.vue | 110 +++++++----------- packages/misskey-js/src/autogen/types.ts | 4 +- 17 files changed, 387 insertions(+), 260 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 4be0c8a09..70df117e7 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1273,6 +1273,8 @@ temporarilySeeThis: "Nevermind, just show me this" sensitiveDoubleClickRequired: "Require double-click to open sensitive media" mutualLink: "Mutual Link" saveThisFile: "Save this file to Drive" +autoRemoval: "Automatic note deletion" +autoRemovalDescription: "You can delete your note when it exceeds period you set." _bubbleGame: howToPlay: "How to play" hold: "Hold" @@ -1331,6 +1333,14 @@ _announcement: dialogAnnouncementUxWarn: "Having two or more dialog-style notifications simultaneously can significantly impact the user experience, so please use them carefully." silence: "No notification" silenceDescription: "Turning this on will skip the notification of this announcement and the user won't need to read it." +_autoRemoval: + use: "Use automatic note deletion" + deleteAfter: "Period threshold" + deleteAfterDescription: "When your notes exceeds this period, notes will be deleted." + noPiningNotes: "Exclude pinned notes" + noPiningNotesDescription: "Delete notes except that are not pinned in profile." + noSpecifiedNotes: "Exclude direct notes" + noSpecifiedNotesDescription: "Delete notes without direct visibility." _initialAccountSetting: accountCreated: "Your account was successfully created!" letsStartAccountSetup: "For starters, let's set up your profile." @@ -1742,29 +1752,31 @@ _role: canCreateContent: "Can create contents" canUpdateContent: "Can edit contents" canDeleteContent: "Can delete contents" + canUseAutoNoteRemoval: "Can use automatic note deletion" canUseAccountRemoval: "Can delete account" + canUseAccountTruncate: "Can truncate account" canPurgeAccount: "Can delete account completely" canUpdateAvatar: "Can change avatar" canUpdateBanner: "Can change banner" - mentionMax: "Maximum number of mentions in a note" + mentionLimit: "Maximum number of mentions in a note" canInvite: "Can create instance invite codes" inviteLimit: "Invite limit" inviteLimitCycle: "Invite limit cooldown" inviteExpirationTime: "Invite expiration interval" canManageCustomEmojis: "Can manage custom emojis" canManageAvatarDecorations: "Manage avatar decorations" - driveCapacity: "Drive capacity" + driveCapacityMb: "Drive capacity (MB)" alwaysMarkNsfw: "Always mark files as NSFW" skipNsfwDetection: "Skip NSFW detection by AI" - pinMax: "Maximum number of pinned notes" - antennaMax: "Maximum number of antennas" - antennaNotesMax: "Maximum number of notes stored in antennas" - wordMuteMax: "Maximum number of characters allowed in word mutes" - webhookMax: "Maximum number of Webhooks" - clipMax: "Maximum number of Clips" - noteEachClipsMax: "Maximum number of notes within a clip" - userListMax: "Maximum number of user lists" - userEachUserListsMax: "Maximum number of users within a user list" + pinLimit: "Maximum number of pinned notes" + antennaLimit: "Maximum number of antennas" + antennaNotesLimit: "Maximum number of notes stored in antennas" + wordMuteLimit: "Maximum number of words allowed in word mutes" + webhookLimit: "Maximum number of Webhooks" + clipLimit: "Maximum number of Clips" + noteEachClipsLimit: "Maximum number of notes within a clip" + userListLimit: "Maximum number of user lists" + userEachUserListsLimit: "Maximum number of users within a user list" rateLimitFactor: "Rate limit" descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " canHideAds: "Can hide ads" @@ -1822,17 +1834,19 @@ _accountDelete: accountDelete: "Delete account" mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded." sendEmail: "Once account deletion has been completed, an email will be sent to the email address registered to this account." - requestAccountDelete: "Request account deletion" + requestAccountDelete: "Request to delete my account" started: "Deletion has been started." - inProgress: "Deletion is currently in progress" + inProgress: "Your account is currently being deleted" youCantUseThisTime: "You can't request account deletion for now." + youAreRootAndCantUseThisTime: "You can't request account deletion 'cause you are root." _accountTruncate: accountTruncate: "Truncate account" - purgeDriveFiles: "Also purge drive's files" - 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." + purgeDriveFiles: "Also truncate drive's files" + mayTakeTime: "As account truncate task 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." requestAccountTruncate: "Request to truncate my account" started: "Truncate task has been started." inProgress: "Your account is currently being truncated" + youCantUseThisTime: "You can't request account truncate task for now." _ad: back: "Back" reduceFrequencyOfThisAd: "Show this ad less" diff --git a/locales/index.d.ts b/locales/index.d.ts index 623bd2c41..4ac7137d4 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5224,9 +5224,21 @@ export interface Locale extends ILocale { */ "youAreOnVacation": string; /** - * 노트 자동 삭제 + * ノート自動削除 */ "autoRemoval": string; + /** + * ノート自動削除は、作成してから一定期間が経過したノートを削除してくれる機能です。 + */ + "autoRemovalDescription": string; + /** + * 危険区域 + */ + "dangerZone": string; + /** + * 以下の機能を利用する際は、特にご注意ください。 + */ + "dangerZoneDescription": string; "_bubbleGame": { /** * 遊び方 @@ -5432,33 +5444,37 @@ export interface Locale extends ILocale { }; "_autoRemoval": { /** - * 노트 자동 삭제를 사용하기 + * ノート自動削除を使用する */ "use": string; /** - * 이 기간이 지난 후에 자동으로 삭제 + * この期間が過ぎたら自動的に削除する */ "deleteAfter": string; /** - * 여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다. + * ここに書かれた日数が過ぎるとノートを自動的に削除します。有効化後は、削除対象のノートも一緒に削除されます。 */ "deleteAfterDescription": string; /** - * 고정된 노트를 제외하기 + * 固定されたノートを除外する */ "noPiningNotes": string; /** - * 프로필에 고정된 노트를 제외하고 삭제합니다. + * プロフィールに固定されたノートを除いて削除します。 */ "noPiningNotesDescription": string; /** - * 다이렉트 노트를 제외하기 + * ダイレクトノートを除外する */ "noSpecifiedNotes": string; /** - * 다이렉트 노트를 제외하고 삭제합니다. + * ダイレクトノートを除いて削除します。 */ "noSpecifiedNotesDescription": string; + /** + * 現在、ノートの自動削除を設定することはできません。 + */ + "youCantUseThisTime": string; }; "_initialTutorial": { /** @@ -6974,10 +6990,18 @@ export interface Locale extends ILocale { * コンテンツの削除 */ "canDeleteContent": string; + /** + * ノート自動削除の利用 + */ + "canUseAutoNoteRemoval": string; /** * アカウントの削除 */ "canUseAccountRemoval": string; + /** + * アカウントの整理 + */ + "canUseAccountTruncate": string; /** * 完全なアカウントの削除 */ @@ -6993,7 +7017,7 @@ export interface Locale extends ILocale { /** * ノート内の最大メンション数 */ - "mentionMax": string; + "mentionLimit": string; /** * サーバー招待コードの発行 */ @@ -7019,9 +7043,9 @@ export interface Locale extends ILocale { */ "canManageAvatarDecorations": string; /** - * ドライブ容量 + * ドライブ容量 (MB) */ - "driveCapacity": string; + "driveCapacityMb": string; /** * ファイルにNSFWを常に付与 */ @@ -7033,39 +7057,39 @@ export interface Locale extends ILocale { /** * ノートのピン留めの最大数 */ - "pinMax": string; + "pinLimit": string; /** * アンテナの作成可能数 */ - "antennaMax": string; + "antennaLimit": string; /** * アンテナに保持する最大ノート数 */ - "antennaNotesMax": string; + "antennaNotesLimit": string; /** * ワードミュートの最大文字数 */ - "wordMuteMax": string; + "wordMuteLimit": string; /** * Webhookの作成可能数 */ - "webhookMax": string; + "webhookLimit": string; /** * クリップの作成可能数 */ - "clipMax": string; + "clipLimit": string; /** * クリップ内のノートの最大数 */ - "noteEachClipsMax": string; + "noteEachClipsLimit": string; /** * ユーザーリストの作成可能数 */ - "userListMax": string; + "userListLimit": string; /** * ユーザーリスト内のユーザーの最大数 */ - "userEachUserListsMax": string; + "userEachUserListsLimit": string; /** * レートリミット */ @@ -7299,6 +7323,10 @@ export interface Locale extends ILocale { * 現在、アカウントの削除はできません。 */ "youCantUseThisTime": string; + /** + * あなたは最高管理者であるため、アカウントを削除することはできません。 + */ + "youAreRootAndCantUseThisTime": string; }; "_accountTruncate": { /** @@ -7325,6 +7353,10 @@ export interface Locale extends ILocale { * 整理が進行中 */ "inProgress": string; + /** + * 現在、アカウントの整理はできません。 + */ + "youCantUseThisTime": string; }; "_ad": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 976205e0c..60a53e41b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1301,7 +1301,10 @@ mindControlDescription: "必要に応じて、Misskeyの疲労感を少なくす hideCounters: "すべてのカウンターを隠す" hideCountersDescription: "ユーザーページのノート、フォロー、フォロワー数、およびこれらの統計をすべて非表示にします。" youAreOnVacation: "現在、休暇モードを使用しています。 このメッセージを閉じるにはここをクリックしてください。" -autoRemoval: "노트 자동 삭제" +autoRemoval: "ノート自動削除" +autoRemovalDescription: "ノート自動削除は、作成してから一定期間が経過したノートを削除してくれる機能です。" +dangerZone: "危険区域" +dangerZoneDescription: "以下の機能を利用する際は、特にご注意ください。" _bubbleGame: howToPlay: "遊び方" @@ -1360,13 +1363,14 @@ _announcement: silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。" _autoRemoval: - use: "노트 자동 삭제를 사용하기" - deleteAfter: "이 기간이 지난 후에 자동으로 삭제" - deleteAfterDescription: "여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다." - noPiningNotes: "고정된 노트를 제외하기" - noPiningNotesDescription: "프로필에 고정된 노트를 제외하고 삭제합니다." - noSpecifiedNotes: "다이렉트 노트를 제외하기" - noSpecifiedNotesDescription: "다이렉트 노트를 제외하고 삭제합니다." + use: "ノート自動削除を使用する" + deleteAfter: "この期間が過ぎたら自動的に削除する" + deleteAfterDescription: "ここに書かれた日数が過ぎるとノートを自動的に削除します。有効化後は、削除対象のノートも一緒に削除されます。" + noPiningNotes: "固定されたノートを除外する" + noPiningNotesDescription: "プロフィールに固定されたノートを除いて削除します。" + noSpecifiedNotes: "ダイレクトノートを除外する" + noSpecifiedNotesDescription: "ダイレクトノートを除いて削除します。" + youCantUseThisTime: "現在、ノートの自動削除を設定することはできません。" _initialTutorial: launchTutorial: "チュートリアルを見る" @@ -1801,29 +1805,31 @@ _role: canCreateContent: "コンテンツの作成" canUpdateContent: "コンテンツの編集" canDeleteContent: "コンテンツの削除" + canUseAutoNoteRemoval: "ノート自動削除の利用" canUseAccountRemoval: "アカウントの削除" + canUseAccountTruncate: "アカウントの整理" canPurgeAccount: "完全なアカウントの削除" canUpdateAvatar: "アイコンの変更" canUpdateBanner: "バナーの変更" - mentionMax: "ノート内の最大メンション数" + mentionLimit: "ノート内の最大メンション数" canInvite: "サーバー招待コードの発行" inviteLimit: "招待コードの作成可能数" inviteLimitCycle: "招待コードの発行間隔" inviteExpirationTime: "招待コードの有効期限" canManageCustomEmojis: "カスタム絵文字の管理" canManageAvatarDecorations: "アバターデコレーションの管理" - driveCapacity: "ドライブ容量" + driveCapacityMb: "ドライブ容量 (MB)" alwaysMarkNsfw: "ファイルにNSFWを常に付与" skipNsfwDetection: "AIによるNSFW検出を無視" - pinMax: "ノートのピン留めの最大数" - antennaMax: "アンテナの作成可能数" - antennaNotesMax: "アンテナに保持する最大ノート数" - wordMuteMax: "ワードミュートの最大文字数" - webhookMax: "Webhookの作成可能数" - clipMax: "クリップの作成可能数" - noteEachClipsMax: "クリップ内のノートの最大数" - userListMax: "ユーザーリストの作成可能数" - userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" + pinLimit: "ノートのピン留めの最大数" + antennaLimit: "アンテナの作成可能数" + antennaNotesLimit: "アンテナに保持する最大ノート数" + wordMuteLimit: "ワードミュートの最大文字数" + webhookLimit: "Webhookの作成可能数" + clipLimit: "クリップの作成可能数" + noteEachClipsLimit: "クリップ内のノートの最大数" + userListLimit: "ユーザーリストの作成可能数" + userEachUserListsLimit: "ユーザーリスト内のユーザーの最大数" rateLimitFactor: "レートリミット" descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" canHideAds: "広告の非表示" @@ -1890,6 +1896,7 @@ _accountDelete: started: "削除処理が開始されました。" inProgress: "削除が進行中" youCantUseThisTime: "現在、アカウントの削除はできません。" + youAreRootAndCantUseThisTime: "あなたは最高管理者であるため、アカウントを削除することはできません。" _accountTruncate: accountDelete: "アカウントの整理" @@ -1898,6 +1905,7 @@ _accountTruncate: requestAccountTruncate: "アカウント整理をリクエスト" started: "整理処理が開始されました。" inProgress: "整理が進行中" + youCantUseThisTime: "現在、アカウントの整理はできません。" _ad: back: "戻る" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index efb293bd6..365442901 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -797,7 +797,7 @@ saveConfirm: "저장하시겠습니까?" deleteConfirm: "삭제하시겠습니까?" invalidValue: "올바른 값이 아닙니다." registry: "레지스트리" -closeAccount: "계정 폐쇄" +closeAccount: "계정 삭제" currentVersion: "현재 버전" latestVersion: "최신 버전" youAreRunningUpToDateClient: "사용 중인 클라이언트는 최신입니다." @@ -1287,11 +1287,14 @@ hideCounters: "모든 카운터를 가리기" hideCountersDescription: "유저 페이지의 노트, 팔로잉, 팔로워 수 및 이러한 통계를 모두 숨깁니다." youAreOnVacation: "현재 휴가 모드를 사용 중입니다. 이 메시지를 닫으려면 여기를 클릭하세요." autoRemoval: "노트 자동 삭제" +autoRemovalDescription: "노트 자동 삭제는 작성한 지 일정 기간이 지난 노트를 삭제해주는 기능입니다." abuseReportCategory: "신고 유형" selectCategory: "카테고리 선택" reportComplete: "신고 완료" blockThisUser: "이 유저 차단하기" muteThisUser: "이 유저 뮤트하기" +dangerZone: "위험한 것들" +dangerZoneDescription: "함부로 실행하면 어딘가 고장날 수 있는 설정들이니, 실행할 때는 주의하세요." _bubbleGame: howToPlay: "설명" hold: "홀드" @@ -1348,11 +1351,12 @@ _announcement: _autoRemoval: use: "노트 자동 삭제를 사용하기" deleteAfter: "이 기간이 지난 후에 자동으로 삭제" - deleteAfterDescription: 여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다. + deleteAfterDescription: "여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다." noPiningNotes: "고정된 노트를 제외하기" noPiningNotesDescription: "프로필에 고정된 노트를 제외하고 삭제합니다." noSpecifiedNotes: "다이렉트 노트를 제외하기" noSpecifiedNotesDescription: "다이렉트 노트를 제외하고 삭제합니다." + youCantUseThisTime: "지금은 노트 자동 삭제를 설정할 수 없습니다." _initialTutorial: launchTutorial: "튜토리얼 보기" title: "튜토리얼" @@ -1777,11 +1781,13 @@ _role: ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" canInitiateConversation: "멘션, 답글, 인용 허용" - mentionMax: "노트에 넣을 수 있는 멘션 수" + mentionLimit: "노트에 넣을 수 있는 멘션 수" canCreateContent: "컨텐츠 생성 허용" canUpdateContent: "컨텐츠 수정 허용" canDeleteContent: "컨텐츠 삭제 허용" + canUseAutoNoteRemoval: "노트 자동 삭제 허용" canUseAccountRemoval: "계정 삭제 허용" + canUseAccountTruncate: "계정 청소 허용" canPurgeAccount: "완전한 계정 삭제 허용" canUpdateAvatar: "아바타 변경 허용" canUpdateBanner: "배너 변경 허용" @@ -1791,18 +1797,18 @@ _role: inviteExpirationTime: "초대장 만료 기간" canManageCustomEmojis: "커스텀 이모지 관리" canManageAvatarDecorations: "아바타 장식 관리" - driveCapacity: "드라이브 용량" + driveCapacityMb: "드라이브 용량 (MB)" alwaysMarkNsfw: "파일을 항상 NSFW로 지정" skipNsfwDetection: "자동 NSFW 감지 기능 미사용" - pinMax: "고정할 수 있는 노트 수" - antennaMax: "만들 수 있는 안테나 수" - antennaNotesMax: "안테나에서 저장할 수 있는 노트 수" - wordMuteMax: "단어 뮤트할 수 있는 문자 수" - webhookMax: "만들 수 있는 웹후크 수" - clipMax: "만들 수 있는 클립 수" - noteEachClipsMax: "클립에 넣을 수 있는 노트 수" - userListMax: "만들 수 있는 유저 리스트 수" - userEachUserListsMax: "유저 리스트에 넣을 수 있는 유저 수" + pinLimit: "고정할 수 있는 노트 수" + antennaLimit: "만들 수 있는 안테나 수" + antennaNotesLimit: "안테나에서 저장할 수 있는 노트 수" + wordMuteLimit: "단어 뮤트할 수 있는 문자 수" + webhookLimit: "만들 수 있는 웹후크 수" + clipLimit: "만들 수 있는 클립 수" + noteEachClipsLimit: "클립에 넣을 수 있는 노트 수" + userListLimit: "만들 수 있는 유저 리스트 수" + userEachUserListsLimit: "유저 리스트에 넣을 수 있는 유저 수" rateLimitFactor: "요청 빈도 제한" descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다." canHideAds: "광고 숨기기" @@ -1859,6 +1865,7 @@ _accountDelete: started: "삭제 작업이 시작되었습니다." inProgress: "삭제 진행 중" youCantUseThisTime: "지금은 계정 삭제를 진행할 수 없습니다." + youAreRootAndCantUseThisTime: "당신은 최고 관리자이므로, 지금은 계정 삭제를 진행할 수 없습니다." _accountTruncate: accountTruncate: "계정 청소" purgeDriveFiles: "드라이브의 파일도 정리하기" @@ -1866,6 +1873,7 @@ _accountTruncate: requestAccountTruncate: "계정 청소를 요청하기" started: "청소 작업이 시작되었습니다." inProgress: "청소 진행 중" + youCantUseThisTime: "지금은 계정 청소를 진행할 수 없습니다." _ad: back: "뒤로" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" diff --git a/packages/backend/migration/1725706236633-AutoNoteRemoval.js b/packages/backend/migration/1725706236633-AutoNoteRemoval.js index 17a8cc8ba..24fabbf25 100644 --- a/packages/backend/migration/1725706236633-AutoNoteRemoval.js +++ b/packages/backend/migration/1725706236633-AutoNoteRemoval.js @@ -2,18 +2,12 @@ export class AutoNoteRemoval1725706236633 { name = 'AutoNoteRemoval1725706236633' async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "auto_removal_condition" ("id" character varying(32) NOT NULL, "deleteAfter" bigint NOT NULL DEFAULT '7', "noPiningNotes" boolean NOT NULL DEFAULT true, "noSpecifiedNotes" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_e6a0b2b5bc8fbc0a07d7e34be7d" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "auto_removal_condition" ("userId" character varying(32) NOT NULL, "deleteAfter" bigint NOT NULL DEFAULT '7', "noPiningNotes" boolean NOT NULL DEFAULT true, "noSpecifiedNotes" boolean NOT NULL DEFAULT true)`); await queryRunner.query(`ALTER TABLE "user" ADD "autoRemoval" boolean NOT NULL DEFAULT false`); await queryRunner.query(`COMMENT ON COLUMN "user"."autoRemoval" IS 'Whether the User is using note auto removal.'`); - await queryRunner.query(`ALTER TABLE "user" ADD "autoRemovalConditionId" character varying(32) NOT NULL`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "UQ_051c1df368299ee71088b7508b8" UNIQUE ("autoRemovalConditionId")`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_051c1df368299ee71088b7508b8" FOREIGN KEY ("autoRemovalConditionId") REFERENCES "auto_removal_condition"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); } async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_051c1df368299ee71088b7508b8"`); - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "UQ_051c1df368299ee71088b7508b8"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "autoRemovalConditionId"`); await queryRunner.query(`COMMENT ON COLUMN "user"."autoRemoval" IS 'Whether the User is using note auto removal.'`); await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "autoRemoval"`); await queryRunner.query(`DROP TABLE "auto_removal_condition"`); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 9664e2537..c0e2a14d8 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -40,6 +40,9 @@ export type RolePolicies = { canCreateContent: boolean; canUpdateContent: boolean; canDeleteContent: boolean; + canUseAutoNoteRemoval: boolean; + canUseAccountRemoval: boolean; + canUseAccountTruncate: boolean; canPurgeAccount: boolean; canUpdateAvatar: boolean; canUpdateBanner: boolean; @@ -69,7 +72,6 @@ export type RolePolicies = { userEachUserListsLimit: number; rateLimitFactor: number; avatarDecorationLimit: number; - canUseAccountRemoval: boolean; mutualLinkSectionLimit: number; mutualLinkLimit: number; }; @@ -82,6 +84,9 @@ export const DEFAULT_POLICIES: RolePolicies = { canCreateContent: true, canUpdateContent: true, canDeleteContent: true, + canUseAutoNoteRemoval: true, + canUseAccountRemoval: true, + canUseAccountTruncate: true, canPurgeAccount: true, canUpdateAvatar: true, canUpdateBanner: true, @@ -111,7 +116,6 @@ export const DEFAULT_POLICIES: RolePolicies = { userEachUserListsLimit: 50, rateLimitFactor: 1, avatarDecorationLimit: 1, - canUseAccountRemoval: true, mutualLinkSectionLimit: 1, mutualLinkLimit: 3, }; @@ -397,7 +401,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)), canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)), canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)), + canUseAutoNoteRemoval: calc('canUseAutoNoteRemoval', vs => vs.some(v => v === true)), canUseAccountRemoval: calc('canUseAccountRemoval', vs => vs.some(v => v === true)), + canUseAccountTruncate: calc('canUseAccountTruncate', vs => vs.some(v => v === true)), canPurgeAccount: calc('canPurgeAccount', vs => vs.some(v => v === true)), canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)), canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)), diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index c92628228..ff651c514 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -196,6 +196,18 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + canUseAutoNoteRemoval: { + type: 'boolean', + optional: false, nullable: false, + }, + canUseAccountRemoval: { + type: 'boolean', + optional: false, nullable: false, + }, + canUseAccountTruncate: { + type: 'boolean', + optional: false, nullable: false, + }, canPurgeAccount: { type: 'boolean', optional: false, nullable: false, @@ -312,10 +324,6 @@ export const packedRolePoliciesSchema = { type: 'integer', optional: false, nullable: false, }, - canUseAccountRemoval: { - type: 'boolean', - optional: false, nullable: false, - }, mutualLinkSectionLimit: { type: 'integer', optional: false, nullable: false, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 1c51d6160..8d1fe884e 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -151,7 +151,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); case 'clean': return this.cleanProcessorService.process(); - case 'autoNoteRemoval': return this.autoNoteRemovalProcessorService.process(); + case 'autoNoteRemoval': return this.autoNoteRemovalProcessorService.process(job); default: throw new Error(`unrecognized job type ${job.name} for system`); } }, { diff --git a/packages/backend/src/queue/processors/AutoNoteRemovalProcessorService.ts b/packages/backend/src/queue/processors/AutoNoteRemovalProcessorService.ts index cb9bb56f0..d9b9445e4 100644 --- a/packages/backend/src/queue/processors/AutoNoteRemovalProcessorService.ts +++ b/packages/backend/src/queue/processors/AutoNoteRemovalProcessorService.ts @@ -12,6 +12,7 @@ import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { IdService } from '@/core/IdService.js'; +import { RoleService } from '@/core/RoleService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -33,6 +34,7 @@ export class AutoNoteRemovalProcessorService { private autoRemovalConditionRepository: AutoRemovalConditionRepository, private idService: IdService, + private roleService: RoleService, private noteDeleteService: NoteDeleteService, private queueLoggerService: QueueLoggerService, ) { @@ -40,7 +42,7 @@ export class AutoNoteRemovalProcessorService { } @bindThis - public async process(): Promise { + public async process(job: Bull.Job>): Promise { this.logger.info('Checking notes that to remove automatically...'); this.logger.info('Checking users that enabled note auto-removal'); const users = await this.usersRepository.find({ where: { autoRemoval: true } }); @@ -51,6 +53,8 @@ export class AutoNoteRemovalProcessorService { const now = Date.now(); for (const user of users) { + const policies = await this.roleService.getUserPolicies(user.id); + if (!policies.canUseAutoNoteRemoval) continue; const autoRemovalCondition = await this.autoRemovalConditionRepository.findOneByOrFail({ userId: user.id }); const pinings: MiUserNotePining[] = await this.userNotePiningsRepository.findBy({ userId: user.id }); const piningNoteIds: string[] = pinings.map(pining => pining.noteId); // pining.note always undefined (bug?) @@ -65,11 +69,11 @@ export class AutoNoteRemovalProcessorService { // Delete notes let cursor: MiNote['id'] | null = null; let condition: string[] = []; - if (autoRemovalCondition.noSpecifiedNotes === true) { + if (autoRemovalCondition.noSpecifiedNotes) { condition = [...condition, ...specifiedNoteIds]; } - if (autoRemovalCondition.noPiningNotes === true) { + if (autoRemovalCondition.noPiningNotes) { condition = [...condition, ...piningNoteIds]; } @@ -99,11 +103,13 @@ export class AutoNoteRemovalProcessorService { const createdAt: number = this.idService.parse(note.id).date.getTime(); const delta: number = now - createdAt; if (delta > deleteAfter) { - await Promise.bind(this.noteDeleteService.delete(user, note, false, user)); + Promise.bind(this.noteDeleteService.delete(user, note, false, user)); } } + await job.updateProgress(100 / users.length * users.indexOf(user)); } + await job.updateProgress(100); this.logger.succ('All of auto-removable notes deleted'); } diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index c659be4a4..b04b9c392 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -17,13 +17,9 @@ export const meta = { requireCredential: true, secure: true, + requireRolePolicy: 'canUseAccountRemoval', errors: { - removalDisabled: { - message: 'Account removal is disabled by your role.', - code: 'REMOVAL_DISABLED', - id: '453d954b-3d8b-4df0-a261-b26ec6660ea3', - }, authenticationFailed: { message: 'Your password or 2FA token is invalid.', code: 'AUTHENTICATION_FAILED', @@ -57,16 +53,10 @@ export default class extends Endpoint { // eslint- private userAuthService: UserAuthService, private deleteAccountService: DeleteAccountService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - const policies = await this.roleService.getUserPolicies(me.id); - if (!policies.canUseAccountRemoval) { - throw new ApiError(meta.errors.removalDisabled); - } - const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); if (userDetailed.isDeleted) { throw new ApiError(meta.errors.alreadyRemoved); diff --git a/packages/backend/src/server/api/endpoints/i/truncate-account.ts b/packages/backend/src/server/api/endpoints/i/truncate-account.ts index fbad69f43..9990984a4 100644 --- a/packages/backend/src/server/api/endpoints/i/truncate-account.ts +++ b/packages/backend/src/server/api/endpoints/i/truncate-account.ts @@ -10,11 +10,26 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { TruncateAccountService } from '@/core/TruncateAccountService.js'; import { DI } from '@/di-symbols.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import {ApiError} from "@/server/api/error.js"; export const meta = { requireCredential: true, secure: true, + requireRolePolicy: 'canUseAccountTruncate', + + errors: { + authenticationFailed: { + message: 'Your password or 2FA token is invalid.', + code: 'AUTHENTICATION_FAILED', + id: 'ea791cff-63e7-4b2a-92fc-646ab641794e', + }, + alreadyRemoved: { + message: 'Your account is removed.', + code: 'ACCOUNT_REMOVED', + id: '59b8f0e6-6eb2-4dc1-a080-1de3108416d0', + }, + }, } as const; export const paramDef = { @@ -40,30 +55,26 @@ export default class extends Endpoint { // eslint- private truncateAccountService: TruncateAccountService, ) { super(meta, paramDef, async (ps, me) => { - const token = ps.token; - const purgeDrive = ps.purgeDrive ? true : false; + const purgeDrive = !!ps.purgeDrive; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - if (profile.twoFactorEnabled) { - if (token == null) { - throw new Error('authentication failed'); - } - - try { - await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (e) { - throw new Error('authentication failed'); - } - } - const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); if (userDetailed.isDeleted) { - return; + throw new ApiError(meta.errors.alreadyRemoved); } const passwordMatched = await bcrypt.compare(ps.password, profile.password!); if (!passwordMatched) { - throw new Error('incorrect password'); + throw new ApiError(meta.errors.authenticationFailed); + } + + if (profile.twoFactorEnabled) { + const token = ps.token; + if (token == null) { + throw new ApiError(meta.errors.authenticationFailed); + } + + await this.userAuthService.twoFactorAuthenticate(profile, token); } await this.truncateAccountService.truncateAccount(me, purgeDrive); diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 7baf51501..71ce53e8e 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -79,6 +79,9 @@ export const ROLE_POLICIES = [ 'canCreateContent', 'canUpdateContent', 'canDeleteContent', + 'canUseAutoNoteRemoval', + 'canUseAccountRemoval', + 'canUseAccountTruncate', 'canPurgeAccount', 'canUpdateAvatar', 'canUpdateBanner', @@ -108,7 +111,6 @@ export const ROLE_POLICIES = [ 'userEachUserListsLimit', 'rateLimitFactor', 'avatarDecorationLimit', - 'canUseAccountRemoval', 'mutualLinkSectionLimit', 'mutualLinkLimit', ] as const; diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index e89d9d837..de231ae1e 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -245,6 +245,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + + +
+
+