feat: 凍結された場合のダイアログを実装 (#7811)
* feat: 凍結された場合のダイアログを実装 * Update CHANGELOG.md * Update basic.js * improve error handling * cypressなんもわからん * Update basic.js
This commit is contained in:
		
							parent
							
								
									6d4e96dea2
								
							
						
					
					
						commit
						54e0a7f8a8
					
				
					 8 changed files with 186 additions and 66 deletions
				
			
		|  | @ -12,6 +12,8 @@ | |||
| ### Improvements | ||||
| - ActivityPub: リモートユーザーのDeleteアクティビティに対応 | ||||
| - ActivityPub: add resolver check for blocked instance | ||||
| - アカウントが凍結された場合に、凍結された旨を表示してからログアウトするように | ||||
| - 凍結されたアカウントにログインしようとしたときに、凍結されている旨を表示するように | ||||
| - UIの改善 | ||||
| 
 | ||||
| ### Bugfixes | ||||
|  |  | |||
|  | @ -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]').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-password-retype] input').type('alice1234'); | ||||
| 
 | ||||
| 		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('アカウントが凍結されています'); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
|  |  | |||
|  | @ -529,6 +529,8 @@ removeAllFollowing: "フォローを全解除" | |||
| removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" | ||||
| userSuspended: "このユーザーは凍結されています。" | ||||
| userSilenced: "このユーザーはサイレンスされています。" | ||||
| yourAccountSuspendedTitle: "アカウントが凍結されています" | ||||
| yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。" | ||||
| menu: "メニュー" | ||||
| divider: "分割線" | ||||
| addItem: "項目を追加" | ||||
|  |  | |||
|  | @ -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<Account> { | |||
| 				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); | ||||
| 	}); | ||||
|  |  | |||
|  | @ -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() { | ||||
|  |  | |||
							
								
								
									
										10
									
								
								src/client/scripts/show-suspended-dialog.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/client/scripts/show-suspended-dialog.ts
									
										
									
									
									
										Normal 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 | ||||
| 	}); | ||||
| } | ||||
|  | @ -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)); | ||||
| }); | ||||
|  |  | |||
|  | @ -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; | ||||
| 		} | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue