From 07bb8067ae62fffe8857c91c7d9f75ccba16789e Mon Sep 17 00:00:00 2001 From: sousuke0422 Date: Sat, 18 Sep 2021 13:30:28 +0900 Subject: fix: アンテナの既読 (#7803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit from: https://gitlab.com/xianon/misskey/-/commit/a89742319caea378f9cdd70c8ebd83bdf2178ff6 --- src/server/api/endpoints/antennas/notes.ts | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/server/api/endpoints') diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts index 3c8a4fbdae..1759e95b4c 100644 --- a/src/server/api/endpoints/antennas/notes.ts +++ b/src/server/api/endpoints/antennas/notes.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; +import readNote from '@/services/note/read'; import { Antennas, Notes, AntennaNotes } from '@/models/index'; import { makePaginationQuery } from '../../common/make-pagination-query'; import { generateVisibilityQuery } from '../../common/generate-visibility-query'; @@ -84,5 +85,9 @@ export default define(meta, async (ps, user) => { .take(ps.limit!) .getMany(); + if (notes.length > 0) { + readNote(user.id, notes); + } + return await Notes.packMany(notes, user); }); -- cgit v1.2.3-freya From 54e0a7f8a8d977c7befc255cc4950a86ac2e72fb Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 19 Sep 2021 02:23:12 +0900 Subject: feat: 凍結された場合のダイアログを実装 (#7811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 凍結された場合のダイアログを実装 * Update CHANGELOG.md * Update basic.js * improve error handling * cypressなんもわからん * Update basic.js --- CHANGELOG.md | 2 + cypress/integration/basic.js | 142 ++++++++++++++++++++++------ locales/ja-JP.yml | 2 + src/client/account.ts | 22 +++-- src/client/components/signin.vue | 41 +++++--- src/client/scripts/show-suspended-dialog.ts | 10 ++ src/server/api/endpoints/reset-db.ts | 2 + src/server/api/private/signin.ts | 37 ++++---- 8 files changed, 189 insertions(+), 69 deletions(-) create mode 100644 src/client/scripts/show-suspended-dialog.ts (limited to 'src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index dce25340f9..8a15faf6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ ### Improvements - ActivityPub: リモートユーザーのDeleteアクティビティに対応 - ActivityPub: add resolver check for blocked instance +- アカウントが凍結された場合に、凍結された旨を表示してからログアウトするように +- 凍結されたアカウントにログインしようとしたときに、凍結されている旨を表示するように - UIの改善 ### Bugfixes diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js index 69d59bc2c6..52bcdb58d0 100644 --- a/cypress/integration/basic.js +++ b/cypress/integration/basic.js @@ -1,12 +1,18 @@ describe('Basic', () => { - before(() => { - cy.request('POST', '/api/reset-db'); - }); - beforeEach(() => { + cy.request('POST', '/api/reset-db').as('reset'); + cy.get('@reset').its('status').should('equal', 204); + cy.clearLocalStorage(); + cy.clearCookies(); cy.reload(true); }); + afterEach(() => { + // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 + // waitを入れることでそれを防止できる + cy.wait(1000); + }); + it('successfully loads', () => { cy.visit('/'); }); @@ -14,56 +20,130 @@ describe('Basic', () => { it('setup instance', () => { cy.visit('/'); + cy.intercept('POST', '/api/admin/accounts/create').as('signup'); + cy.get('[data-cy-admin-username] input').type('admin'); - cy.get('[data-cy-admin-password] input').type('admin1234'); - cy.get('[data-cy-admin-ok]').click(); + + // なぜか動かない + //cy.wait('@signup').should('have.property', 'response.statusCode'); + cy.wait('@signup'); }); it('signup', () => { - cy.visit('/'); + // インスタンス初期セットアップ + cy.request('POST', '/api/admin/accounts/create', { + username: 'admin', + password: 'pass', + }).as('setup'); - cy.get('[data-cy-signup]').click(); + cy.get('@setup').then(() => { + cy.visit('/'); - cy.get('[data-cy-signup-username] input').type('alice'); + cy.intercept('POST', '/api/signup').as('signup'); - cy.get('[data-cy-signup-password] input').type('alice1234'); - - cy.get('[data-cy-signup-password-retype] input').type('alice1234'); + cy.get('[data-cy-signup]').click(); + cy.get('[data-cy-signup-username] input').type('alice'); + cy.get('[data-cy-signup-password] input').type('alice1234'); + cy.get('[data-cy-signup-password-retype] input').type('alice1234'); + cy.get('[data-cy-signup-submit]').click(); - cy.get('[data-cy-signup-submit]').click(); + cy.wait('@signup'); + }); }); it('signin', () => { - cy.visit('/'); - - cy.get('[data-cy-signin]').click(); - - cy.get('[data-cy-signin-username] input').type('alice'); - - // Enterキーでサインインできるかの確認も兼ねる - cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); + // インスタンス初期セットアップ + cy.request('POST', '/api/admin/accounts/create', { + username: 'admin', + password: 'pass', + }).as('setup'); + + cy.get('@setup').then(() => { + // ユーザー作成 + cy.request('POST', '/api/signup', { + username: 'alice', + password: 'alice1234', + }).as('signup'); + }); + + cy.get('@signup').then(() => { + cy.visit('/'); + + cy.intercept('POST', '/api/signin').as('signin'); + + cy.get('[data-cy-signin]').click(); + cy.get('[data-cy-signin-username] input').type('alice'); + // Enterキーでサインインできるかの確認も兼ねる + cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); + + cy.wait('@signin'); + }); }); it('note', () => { cy.visit('/'); - //#region TODO: この辺はUI操作ではなくAPI操作でログインする - cy.get('[data-cy-signin]').click(); + // インスタンス初期セットアップ + cy.request('POST', '/api/admin/accounts/create', { + username: 'admin', + password: 'pass', + }).as('setup'); + + cy.get('@setup').then(() => { + // ユーザー作成 + cy.request('POST', '/api/signup', { + username: 'alice', + password: 'alice1234', + }).as('signup'); + }); - cy.get('[data-cy-signin-username] input').type('alice'); + cy.get('@signup').then(() => { + cy.visit('/'); - // Enterキーでサインインできるかの確認も兼ねる - cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); - //#endregion + cy.intercept('POST', '/api/signin').as('signin'); - cy.get('[data-cy-open-post-form]').click(); + cy.get('[data-cy-signin]').click(); + cy.get('[data-cy-signin-username] input').type('alice'); + cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); - cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); + cy.wait('@signin').as('signinEnd'); + }); - cy.get('[data-cy-open-post-form-submit]').click(); + cy.get('@signinEnd').then(() => { + cy.get('[data-cy-open-post-form]').click(); + cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); + cy.get('[data-cy-open-post-form-submit]').click(); - // TODO: 投稿した文字列が画面内にあるか(=タイムラインに流れてきたか)のテスト + cy.contains('Hello, Misskey!'); + }); }); + + it('suspend', function() { + cy.request('POST', '/api/admin/accounts/create', { + username: 'admin', + password: 'pass', + }).its('body').as('admin'); + + cy.request('POST', '/api/signup', { + username: 'alice', + password: 'pass', + }).its('body').as('alice'); + + cy.then(() => { + cy.request('POST', '/api/admin/suspend-user', { + i: this.admin.token, + userId: this.alice.id, + }); + + cy.visit('/'); + + cy.get('[data-cy-signin]').click(); + cy.get('[data-cy-signin-username] input').type('alice'); + cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); + + cy.contains('アカウントが凍結されています'); + }); + }); }); diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b9623ef0d0..2c0663cf87 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -529,6 +529,8 @@ removeAllFollowing: "フォローを全解除" removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" userSuspended: "このユーザーは凍結されています。" userSilenced: "このユーザーはサイレンスされています。" +yourAccountSuspendedTitle: "アカウントが凍結されています" +yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。" menu: "メニュー" divider: "分割線" addItem: "項目を追加" diff --git a/src/client/account.ts b/src/client/account.ts index e469bae5a2..6e26ac1f7d 100644 --- a/src/client/account.ts +++ b/src/client/account.ts @@ -3,6 +3,7 @@ import { reactive } from 'vue'; import { apiUrl } from '@client/config'; import { waiting } from '@client/os'; import { unisonReload, reloadChannel } from '@client/scripts/unison-reload'; +import { showSuspendedDialog } from './scripts/show-suspended-dialog'; // TODO: 他のタブと永続化されたstateを同期 @@ -82,17 +83,20 @@ function fetchAccount(token): Promise { i: token }) }) + .then(res => res.json()) .then(res => { - // When failed to authenticate user - if (res.status !== 200 && res.status < 500) { - return signout(); + if (res.error) { + if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { + showSuspendedDialog().then(() => { + signout(); + }); + } else { + signout(); + } + } else { + res.token = token; + done(res); } - - // Parse response - res.json().then(i => { - i.token = token; - done(i); - }); }) .catch(fail); }); diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue index c051288d0a..69f527b7d6 100755 --- a/src/client/components/signin.vue +++ b/src/client/components/signin.vue @@ -54,6 +54,7 @@ import { apiUrl, host } from '@client/config'; import { byteify, hexify } from '@client/scripts/2fa'; import * as os from '@client/os'; import { login } from '@client/account'; +import { showSuspendedDialog } from '../scripts/show-suspended-dialog'; export default defineComponent({ components: { @@ -169,15 +170,7 @@ export default defineComponent({ this.signing = false; this.challengeData = res; return this.queryKey(); - }).catch(() => { - os.dialog({ - type: 'error', - text: this.$ts.signinFailed - }); - this.challengeData = null; - this.totpLogin = false; - this.signing = false; - }); + }).catch(this.loginFailed); } else { this.totpLogin = true; this.signing = false; @@ -190,14 +183,36 @@ export default defineComponent({ }).then(res => { this.$emit('login', res); this.onLogin(res); - }).catch(() => { + }).catch(this.loginFailed); + } + }, + + loginFailed(err) { + switch (err.id) { + case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { os.dialog({ type: 'error', - text: this.$ts.loginFailed + title: this.$ts.loginFailed, + text: this.$ts.noSuchUser }); - this.signing = false; - }); + break; + } + case 'e03a5f46-d309-4865-9b69-56282d94e1eb': { + showSuspendedDialog(); + break; + } + default: { + os.dialog({ + type: 'error', + title: this.$ts.loginFailed, + text: JSON.stringify(err) + }); + } } + + this.challengeData = null; + this.totpLogin = false; + this.signing = false; }, resetPassword() { diff --git a/src/client/scripts/show-suspended-dialog.ts b/src/client/scripts/show-suspended-dialog.ts new file mode 100644 index 0000000000..dde829cdae --- /dev/null +++ b/src/client/scripts/show-suspended-dialog.ts @@ -0,0 +1,10 @@ +import * as os from '@client/os'; +import { i18n } from '@client/i18n'; + +export function showSuspendedDialog() { + return os.dialog({ + type: 'error', + title: i18n.locale.yourAccountSuspendedTitle, + text: i18n.locale.yourAccountSuspendedDescription + }); +} diff --git a/src/server/api/endpoints/reset-db.ts b/src/server/api/endpoints/reset-db.ts index f430869302..f0a9dae4ff 100644 --- a/src/server/api/endpoints/reset-db.ts +++ b/src/server/api/endpoints/reset-db.ts @@ -18,4 +18,6 @@ export default define(meta, async (ps, user) => { if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; await resetDb(); + + await new Promise(resolve => setTimeout(resolve, 1000)); }); diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index fff1037ff9..83c3dfee94 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -18,6 +18,11 @@ export default async (ctx: Koa.Context) => { const password = body['password']; const token = body['token']; + function error(status: number, error: { id: string }) { + ctx.status = status; + ctx.body = { error }; + } + if (typeof username != 'string') { ctx.status = 400; return; @@ -40,15 +45,15 @@ export default async (ctx: Koa.Context) => { }) as ILocalUser; if (user == null) { - ctx.throw(404, { - error: 'user not found' + error(404, { + id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', }); return; } if (user.isSuspended) { - ctx.throw(403, { - error: 'user is suspended' + error(403, { + id: 'e03a5f46-d309-4865-9b69-56282d94e1eb', }); return; } @@ -58,7 +63,7 @@ export default async (ctx: Koa.Context) => { // Compare password const same = await bcrypt.compare(password, profile.password!); - async function fail(status?: number, failure?: { error: string }) { + async function fail(status?: number, failure?: { id: string }) { // Append signin history await Signins.insert({ id: genId(), @@ -69,7 +74,7 @@ export default async (ctx: Koa.Context) => { success: false }); - ctx.throw(status || 500, failure || { error: 'someting happened' }); + error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); } if (!profile.twoFactorEnabled) { @@ -78,7 +83,7 @@ export default async (ctx: Koa.Context) => { return; } else { await fail(403, { - error: 'incorrect password' + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c' }); return; } @@ -87,7 +92,7 @@ export default async (ctx: Koa.Context) => { if (token) { if (!same) { await fail(403, { - error: 'incorrect password' + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c' }); return; } @@ -104,14 +109,14 @@ export default async (ctx: Koa.Context) => { return; } else { await fail(403, { - error: 'invalid token' + id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f' }); return; } } else if (body.credentialId) { if (!same && !profile.usePasswordLessLogin) { await fail(403, { - error: 'incorrect password' + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c' }); return; } @@ -127,7 +132,7 @@ export default async (ctx: Koa.Context) => { if (!challenge) { await fail(403, { - error: 'non-existent challenge' + id: '2715a88a-2125-4013-932f-aa6fe72792da' }); return; } @@ -139,7 +144,7 @@ export default async (ctx: Koa.Context) => { if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { await fail(403, { - error: 'non-existent challenge' + id: '2715a88a-2125-4013-932f-aa6fe72792da' }); return; } @@ -155,7 +160,7 @@ export default async (ctx: Koa.Context) => { if (!securityKey) { await fail(403, { - error: 'invalid credentialId' + id: '66269679-aeaf-4474-862b-eb761197e046' }); return; } @@ -174,14 +179,14 @@ export default async (ctx: Koa.Context) => { return; } else { await fail(403, { - error: 'invalid challenge data' + id: '93b86c4b-72f9-40eb-9815-798928603d1e' }); return; } } else { if (!same && !profile.usePasswordLessLogin) { await fail(403, { - error: 'incorrect password' + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c' }); return; } @@ -192,7 +197,7 @@ export default async (ctx: Koa.Context) => { if (keys.length === 0) { await fail(403, { - error: 'no keys found' + id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4' }); return; } -- cgit v1.2.3-freya From 9208825975f56bab8aca7ae8d6507f6cfe0f599a Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 22 Sep 2021 17:34:48 +0900 Subject: feat(server): 管理者用アカウント削除API実装 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 動作確認済み Resolve #7735 --- CHANGELOG.md | 2 + src/queue/index.ts | 5 +- src/queue/processors/db/delete-account.ts | 11 +++-- src/queue/types.ts | 7 ++- src/server/api/endpoints/admin/accounts/delete.ts | 58 +++++++++++++++++++++++ src/server/api/endpoints/i/delete-account.ts | 4 +- 6 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 src/server/api/endpoints/admin/accounts/delete.ts (limited to 'src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index cf5621fc05..e034c4e553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ - ActivityPub: リモートユーザーのDeleteアクティビティに対応 - ActivityPub: add resolver check for blocked instance - ActivityPub: deliverキューのメモリ使用量を削減 +- API: 管理者用アカウント削除APIを実装(/admin/accounts/delete) + - リモートユーザーの削除も可能に - アカウントが凍結された場合に、凍結された旨を表示してからログアウトするように - 凍結されたアカウントにログインしようとしたときに、凍結されている旨を表示するように - リスト、アンテナタイムラインを個別ページとして分割 diff --git a/src/queue/index.ts b/src/queue/index.ts index ee34ed47e4..0ce10a4c60 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -173,9 +173,10 @@ export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id'] }); } -export function createDeleteAccountJob(user: ThinUser) { +export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; }) { return dbQueue.add('deleteAccount', { - user: user + user: user, + soft: opts.soft }, { removeOnComplete: true, removeOnFail: true diff --git a/src/queue/processors/db/delete-account.ts b/src/queue/processors/db/delete-account.ts index 65327754c2..e54f38e35e 100644 --- a/src/queue/processors/db/delete-account.ts +++ b/src/queue/processors/db/delete-account.ts @@ -1,7 +1,7 @@ import * as Bull from 'bull'; import { queueLogger } from '../../logger'; import { DriveFiles, Notes, UserProfiles, Users } from '@/models/index'; -import { DbUserJobData } from '@/queue/types'; +import { DbUserDeleteJobData } from '@/queue/types'; import { Note } from '@/models/entities/note'; import { DriveFile } from '@/models/entities/drive-file'; import { MoreThan } from 'typeorm'; @@ -10,7 +10,7 @@ import { sendEmail } from '@/services/send-email'; const logger = queueLogger.createSubLogger('delete-account'); -export async function deleteAccount(job: Bull.Job): Promise { +export async function deleteAccount(job: Bull.Job): Promise { logger.info(`Deleting account of ${job.data.user.id} ...`); const user = await Users.findOne(job.data.user.id); @@ -83,7 +83,12 @@ export async function deleteAccount(job: Bull.Job): Promise { + const user = await Users.findOne(ps.userId); + + if (user == null) { + throw new Error('user not found'); + } + + if (user.isAdmin) { + throw new Error('cannot suspend admin'); + } + + if (user.isModerator) { + throw new Error('cannot suspend moderator'); + } + + if (Users.isLocalUser(user)) { + // 物理削除する前にDelete activityを送信する + await doPostSuspend(user).catch(e => {}); + + createDeleteAccountJob(user, { + soft: false + }); + } else { + createDeleteAccountJob(user, { + soft: true // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する + }); + } + + await Users.update(user.id, { + isDeleted: true, + }); + + if (Users.isLocalUser(user)) { + // Terminate streaming + publishUserEvent(user.id, 'terminate', {}); + } +}); diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts index 77f11925cd..10e5adf64a 100644 --- a/src/server/api/endpoints/i/delete-account.ts +++ b/src/server/api/endpoints/i/delete-account.ts @@ -35,7 +35,9 @@ export default define(meta, async (ps, user) => { // 物理削除する前にDelete activityを送信する await doPostSuspend(user).catch(e => {}); - createDeleteAccountJob(user); + createDeleteAccountJob(user, { + soft: false + }); await Users.update(user.id, { isDeleted: true, -- cgit v1.2.3-freya