feat: 凍結された場合のダイアログを実装 (#7811)

* feat: 凍結された場合のダイアログを実装

* Update CHANGELOG.md

* Update basic.js

* improve error handling

* cypressなんもわからん

* Update basic.js
This commit is contained in:
syuilo 2021-09-19 02:23:12 +09:00 committed by GitHub
parent 6d4e96dea2
commit 54e0a7f8a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 186 additions and 66 deletions

View file

@ -12,6 +12,8 @@
### Improvements ### Improvements
- ActivityPub: リモートユーザーのDeleteアクティビティに対応 - ActivityPub: リモートユーザーのDeleteアクティビティに対応
- ActivityPub: add resolver check for blocked instance - ActivityPub: add resolver check for blocked instance
- アカウントが凍結された場合に、凍結された旨を表示してからログアウトするように
- 凍結されたアカウントにログインしようとしたときに、凍結されている旨を表示するように
- UIの改善 - UIの改善
### Bugfixes ### Bugfixes

View file

@ -1,10 +1,16 @@
describe('Basic', () => { describe('Basic', () => {
before(() => { beforeEach(() => {
cy.request('POST', '/api/reset-db'); cy.request('POST', '/api/reset-db').as('reset');
cy.get('@reset').its('status').should('equal', 204);
cy.clearLocalStorage();
cy.clearCookies();
cy.reload(true);
}); });
beforeEach(() => { afterEach(() => {
cy.reload(true); // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
}); });
it('successfully loads', () => { it('successfully loads', () => {
@ -14,56 +20,130 @@ describe('Basic', () => {
it('setup instance', () => { it('setup instance', () => {
cy.visit('/'); 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-username] input').type('admin');
cy.get('[data-cy-admin-password] input').type('admin1234'); cy.get('[data-cy-admin-password] input').type('admin1234');
cy.get('[data-cy-admin-ok]').click(); cy.get('[data-cy-admin-ok]').click();
// なぜか動かない
//cy.wait('@signup').should('have.property', 'response.statusCode');
cy.wait('@signup');
}); });
it('signup', () => { it('signup', () => {
// インスタンス初期セットアップ
cy.request('POST', '/api/admin/accounts/create', {
username: 'admin',
password: 'pass',
}).as('setup');
cy.get('@setup').then(() => {
cy.visit('/'); cy.visit('/');
cy.intercept('POST', '/api/signup').as('signup');
cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-username] input').type('alice'); cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-password] input').type('alice1234'); cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-password-retype] 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', () => { it('signin', () => {
// インスタンス初期セットアップ
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.visit('/');
cy.intercept('POST', '/api/signin').as('signin');
cy.get('[data-cy-signin]').click(); cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice'); cy.get('[data-cy-signin-username] input').type('alice');
// Enterキーでサインインできるかの確認も兼ねる // Enterキーでサインインできるかの確認も兼ねる
cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.wait('@signin');
});
}); });
it('note', () => { it('note', () => {
cy.visit('/'); cy.visit('/');
//#region TODO: この辺はUI操作ではなくAPI操作でログインする // インスタンス初期セットアップ
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]').click();
cy.get('[data-cy-signin-username] input').type('alice'); cy.get('[data-cy-signin-username] input').type('alice');
// Enterキーでサインインできるかの確認も兼ねる
cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
//#endregion
cy.wait('@signin').as('signinEnd');
});
cy.get('@signinEnd').then(() => {
cy.get('[data-cy-open-post-form]').click(); cy.get('[data-cy-open-post-form]').click();
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
cy.get('[data-cy-open-post-form-submit]').click(); 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('アカウントが凍結されています');
});
}); });
}); });

View file

@ -529,6 +529,8 @@ removeAllFollowing: "フォローを全解除"
removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
userSuspended: "このユーザーは凍結されています。" userSuspended: "このユーザーは凍結されています。"
userSilenced: "このユーザーはサイレンスされています。" userSilenced: "このユーザーはサイレンスされています。"
yourAccountSuspendedTitle: "アカウントが凍結されています"
yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。"
menu: "メニュー" menu: "メニュー"
divider: "分割線" divider: "分割線"
addItem: "項目を追加" addItem: "項目を追加"

View file

@ -3,6 +3,7 @@ import { reactive } from 'vue';
import { apiUrl } from '@client/config'; import { apiUrl } from '@client/config';
import { waiting } from '@client/os'; import { waiting } from '@client/os';
import { unisonReload, reloadChannel } from '@client/scripts/unison-reload'; import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -82,17 +83,20 @@ function fetchAccount(token): Promise<Account> {
i: token i: token
}) })
}) })
.then(res => res.json())
.then(res => { .then(res => {
// When failed to authenticate user if (res.error) {
if (res.status !== 200 && res.status < 500) { if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
return signout(); showSuspendedDialog().then(() => {
} signout();
// Parse response
res.json().then(i => {
i.token = token;
done(i);
}); });
} else {
signout();
}
} else {
res.token = token;
done(res);
}
}) })
.catch(fail); .catch(fail);
}); });

View file

@ -54,6 +54,7 @@ import { apiUrl, host } from '@client/config';
import { byteify, hexify } from '@client/scripts/2fa'; import { byteify, hexify } from '@client/scripts/2fa';
import * as os from '@client/os'; import * as os from '@client/os';
import { login } from '@client/account'; import { login } from '@client/account';
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -169,15 +170,7 @@ export default defineComponent({
this.signing = false; this.signing = false;
this.challengeData = res; this.challengeData = res;
return this.queryKey(); return this.queryKey();
}).catch(() => { }).catch(this.loginFailed);
os.dialog({
type: 'error',
text: this.$ts.signinFailed
});
this.challengeData = null;
this.totpLogin = false;
this.signing = false;
});
} else { } else {
this.totpLogin = true; this.totpLogin = true;
this.signing = false; this.signing = false;
@ -190,14 +183,36 @@ export default defineComponent({
}).then(res => { }).then(res => {
this.$emit('login', res); this.$emit('login', res);
this.onLogin(res); this.onLogin(res);
}).catch(() => { }).catch(this.loginFailed);
}
},
loginFailed(err) {
switch (err.id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.dialog({ os.dialog({
type: 'error', 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() { resetPassword() {

View file

@ -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
});
}

View file

@ -18,4 +18,6 @@ export default define(meta, async (ps, user) => {
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
await resetDb(); await resetDb();
await new Promise(resolve => setTimeout(resolve, 1000));
}); });

View file

@ -18,6 +18,11 @@ export default async (ctx: Koa.Context) => {
const password = body['password']; const password = body['password'];
const token = body['token']; const token = body['token'];
function error(status: number, error: { id: string }) {
ctx.status = status;
ctx.body = { error };
}
if (typeof username != 'string') { if (typeof username != 'string') {
ctx.status = 400; ctx.status = 400;
return; return;
@ -40,15 +45,15 @@ export default async (ctx: Koa.Context) => {
}) as ILocalUser; }) as ILocalUser;
if (user == null) { if (user == null) {
ctx.throw(404, { error(404, {
error: 'user not found' id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
}); });
return; return;
} }
if (user.isSuspended) { if (user.isSuspended) {
ctx.throw(403, { error(403, {
error: 'user is suspended' id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
}); });
return; return;
} }
@ -58,7 +63,7 @@ export default async (ctx: Koa.Context) => {
// Compare password // Compare password
const same = await bcrypt.compare(password, profile.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 // Append signin history
await Signins.insert({ await Signins.insert({
id: genId(), id: genId(),
@ -69,7 +74,7 @@ export default async (ctx: Koa.Context) => {
success: false success: false
}); });
ctx.throw(status || 500, failure || { error: 'someting happened' }); error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
} }
if (!profile.twoFactorEnabled) { if (!profile.twoFactorEnabled) {
@ -78,7 +83,7 @@ export default async (ctx: Koa.Context) => {
return; return;
} else { } else {
await fail(403, { await fail(403, {
error: 'incorrect password' id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
}); });
return; return;
} }
@ -87,7 +92,7 @@ export default async (ctx: Koa.Context) => {
if (token) { if (token) {
if (!same) { if (!same) {
await fail(403, { await fail(403, {
error: 'incorrect password' id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
}); });
return; return;
} }
@ -104,14 +109,14 @@ export default async (ctx: Koa.Context) => {
return; return;
} else { } else {
await fail(403, { await fail(403, {
error: 'invalid token' id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f'
}); });
return; return;
} }
} else if (body.credentialId) { } else if (body.credentialId) {
if (!same && !profile.usePasswordLessLogin) { if (!same && !profile.usePasswordLessLogin) {
await fail(403, { await fail(403, {
error: 'incorrect password' id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
}); });
return; return;
} }
@ -127,7 +132,7 @@ export default async (ctx: Koa.Context) => {
if (!challenge) { if (!challenge) {
await fail(403, { await fail(403, {
error: 'non-existent challenge' id: '2715a88a-2125-4013-932f-aa6fe72792da'
}); });
return; return;
} }
@ -139,7 +144,7 @@ export default async (ctx: Koa.Context) => {
if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
await fail(403, { await fail(403, {
error: 'non-existent challenge' id: '2715a88a-2125-4013-932f-aa6fe72792da'
}); });
return; return;
} }
@ -155,7 +160,7 @@ export default async (ctx: Koa.Context) => {
if (!securityKey) { if (!securityKey) {
await fail(403, { await fail(403, {
error: 'invalid credentialId' id: '66269679-aeaf-4474-862b-eb761197e046'
}); });
return; return;
} }
@ -174,14 +179,14 @@ export default async (ctx: Koa.Context) => {
return; return;
} else { } else {
await fail(403, { await fail(403, {
error: 'invalid challenge data' id: '93b86c4b-72f9-40eb-9815-798928603d1e'
}); });
return; return;
} }
} else { } else {
if (!same && !profile.usePasswordLessLogin) { if (!same && !profile.usePasswordLessLogin) {
await fail(403, { await fail(403, {
error: 'incorrect password' id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
}); });
return; return;
} }
@ -192,7 +197,7 @@ export default async (ctx: Koa.Context) => {
if (keys.length === 0) { if (keys.length === 0) {
await fail(403, { await fail(403, {
error: 'no keys found' id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4'
}); });
return; return;
} }