diff --git a/.gitignore b/.gitignore
index 0786295cf5..f8baa43848 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,10 @@
 /node_modules
 report.*.json
 
+# Cypress
+cypress/screenshots
+cypress/videos
+
 # config
 /.config/*
 !/.config/example.yml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2d6a3e8c1..914c5ac7a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,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..182f70ff68 100644
--- a/cypress/integration/basic.js
+++ b/cypress/integration/basic.js
@@ -1,10 +1,14 @@
-describe('Basic', () => {
-	before(() => {
-		cy.request('POST', '/api/reset-db');
+describe('Before setup instance', () => {
+	beforeEach(() => {
+		cy.request('POST', '/api/reset-db').as('reset');
+		cy.get('@reset').its('status').should('equal', 204);
+		cy.reload(true);
 	});
 
-	beforeEach(() => {
-		cy.reload(true);
+	afterEach(() => {
+		// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
+		// waitを入れることでそれを防止できる
+		cy.wait(1000);
 	});
 
   it('successfully loads', () => {
@@ -14,56 +18,172 @@ 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');
+  });
+});
+
+describe('After setup instance', () => {
+	beforeEach(() => {
+		cy.request('POST', '/api/reset-db').as('reset');
+		cy.get('@reset').its('status').should('equal', 204);
+		cy.reload(true);
+
+		// インスタンス初期セットアップ
+		cy.request('POST', '/api/admin/accounts/create', {
+			username: 'admin',
+			password: 'pass',
+		}).its('body').as('admin');
+
+		cy.get('@admin');
+	});
+
+	afterEach(() => {
+		// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
+		// waitを入れることでそれを防止できる
+		cy.wait(1000);
+	});
+
+  it('successfully loads', () => {
+    cy.visit('/');
   });
 
 	it('signup', () => {
-    cy.visit('/');
+		cy.visit('/');
+
+		cy.intercept('POST', '/api/signup').as('signup');
 
 		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.wait('@signup');
+  });
+});
+
+describe('After user signup', () => {
+	beforeEach(() => {
+		cy.request('POST', '/api/reset-db').as('reset');
+		cy.get('@reset').its('status').should('equal', 204);
+		cy.reload(true);
+
+		// インスタンス初期セットアップ
+		cy.request('POST', '/api/admin/accounts/create', {
+			username: 'admin',
+			password: 'pass',
+		}).its('body').as('admin');
+
+		cy.get('@admin').then(() => {
+			// ユーザー作成
+			cy.request('POST', '/api/signup', {
+				username: 'alice',
+				password: 'alice1234',
+			}).its('body').as('alice');
+		});
+
+		cy.get('@alice');
+	});
+
+	afterEach(() => {
+		// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
+		// waitを入れることでそれを防止できる
+		cy.wait(1000);
+	});
+
+  it('successfully loads', () => {
+    cy.visit('/');
   });
 
 	it('signin', () => {
-    cy.visit('/');
+		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('suspend', function() {
+		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('アカウントが凍結されています');
+	});
+});
+
+describe('After user singed in', () => {
+	beforeEach(() => {
+		cy.request('POST', '/api/reset-db').as('reset');
+		cy.get('@reset').its('status').should('equal', 204);
+		cy.reload(true);
+
+		// インスタンス初期セットアップ
+		cy.request('POST', '/api/admin/accounts/create', {
+			username: 'admin',
+			password: 'pass',
+		}).its('body').as('admin');
+
+		cy.get('@admin').then(() => {
+			// ユーザー作成
+			cy.request('POST', '/api/signup', {
+				username: 'alice',
+				password: 'alice1234',
+			}).its('body').as('alice');
+		});
+
+		cy.get('@alice').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');
+			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+
+			cy.wait('@signin').as('signedIn');
+		});
+
+		cy.get('@signedIn');
+	});
+
+	afterEach(() => {
+		// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
+		// waitを入れることでそれを防止できる
+		cy.wait(1000);
+	});
+
+  it('successfully loads', () => {
+    cy.visit('/');
   });
 
 	it('note', () => {
     cy.visit('/');
 
-		//#region TODO: この辺はUI操作ではなくAPI操作でログインする
-		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}');
-		//#endregion
-
 		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!');
   });
 });
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1097226b38..a3d516b631 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 46868b3d65..9ca01340ea 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<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);
 	});
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;
 		}