From 54e0a7f8a8d977c7befc255cc4950a86ac2e72fb Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 19 Sep 2021 02:23:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=87=8D=E7=B5=90=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=9F=E5=A0=B4=E5=90=88=E3=81=AE=E3=83=80=E3=82=A4=E3=82=A2?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=82=92=E5=AE=9F=E8=A3=85=20(#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 | 136 ++++++++++++++++---- 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, 186 insertions(+), 66 deletions(-) create mode 100644 src/client/scripts/show-suspended-dialog.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dce25340f..8a15faf6a 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 69d59bc2c..52bcdb58d 100644 --- a/cypress/integration/basic.js +++ b/cypress/integration/basic.js @@ -1,10 +1,16 @@ 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); }); - beforeEach(() => { - cy.reload(true); + afterEach(() => { + // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 + // waitを入れることでそれを防止できる + cy.wait(1000); }); it('successfully loads', () => { @@ -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.request('POST', '/api/admin/accounts/create', { + username: 'admin', + password: 'pass', + }).as('setup'); - cy.get('[data-cy-signin]').click(); + 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}'); + 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('[data-cy-signin-username] input').type('alice'); + cy.get('@setup').then(() => { + // ユーザー作成 + cy.request('POST', '/api/signup', { + username: 'alice', + password: 'alice1234', + }).as('signup'); + }); - // Enterキーでサインインできるかの確認も兼ねる - cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); - //#endregion + cy.get('@signup').then(() => { + cy.visit('/'); - cy.get('[data-cy-open-post-form]').click(); + cy.intercept('POST', '/api/signin').as('signin'); - cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); + 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-open-post-form-submit]').click(); + cy.wait('@signin').as('signinEnd'); + }); - // TODO: 投稿した文字列が画面内にあるか(=タイムラインに流れてきたか)のテスト + 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(); + + 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 b9623ef0d..2c0663cf8 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 e469bae5a..6e26ac1f7 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 c051288d0..69f527b7d 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 000000000..dde829cda --- /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 f43086930..f0a9dae4f 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 fff1037ff..83c3dfee9 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; }