diff --git a/config/dev.env.js b/config/dev.env.js
index 13ea61f7b754ded74198bc89d08714ccca08b798..f2b2a130357120c83937f6bdd4da4ca24326ef86 100644
--- a/config/dev.env.js
+++ b/config/dev.env.js
@@ -1,5 +1,4 @@
 module.exports = {
   NODE_ENV: '"development"',
-  ENV_CONFIG: '"dev"',
-  BASE_API: '"http://localhost:4000"'
+  ENV_CONFIG: '"dev"'
 }
diff --git a/package.json b/package.json
index 81dff90cf905718205dacf0cf381d48f2f433828..097c291fb983b0ee4f3a7a71346c0ed1521d1204 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,7 @@
     "eslint-loader": "2.0.0",
     "eslint-plugin-vue": "4.7.1",
     "file-loader": "1.1.11",
+    "flush-promises": "^1.0.2",
     "friendly-errors-webpack-plugin": "1.7.0",
     "hash-sum": "1.0.2",
     "html-webpack-plugin": "^3.2.0",
diff --git a/src/api/__mocks__/login.js b/src/api/__mocks__/login.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f1bfeb1d68b6f38e41626451fc4319442e5a459
--- /dev/null
+++ b/src/api/__mocks__/login.js
@@ -0,0 +1,59 @@
+const users = [
+  { username: 'bob', password: '123456' }
+]
+
+export async function loginByUsername(username, password) {
+  const user = users.find(user => user.username === username)
+  const verifyPassword = user.password === password
+  const data = {
+    'token_type': 'Bearer',
+    'scope': 'read write follow',
+    'refresh_token': 'foo123',
+    'me': 'bob',
+    'expires_in': 600,
+    'access_token': 'bar123'
+  }
+
+  return verifyPassword
+    ? Promise.resolve({ data })
+    : Promise.reject({ message: 'Invalid credentials' })
+}
+
+export function getUserInfo(token, authHost) {
+  const userInfo = {
+    'name_html': 'bob',
+    'background_image': null,
+    'friends_count': 0,
+    'description_html': '',
+    'followers_count': 0,
+    'locked': false,
+    'follows_you': true,
+    'statusnet_blocking': false,
+    'statusnet_profile_url': '',
+    'following': true,
+    'id': '10',
+    'is_local': true,
+    'profile_image_url': '',
+    'role': 'admin',
+    'profile_image_url_profile_size': '',
+    'rights': { 'admin': true, 'delete_others_notice': true },
+    'token': 'foo123456',
+    'no_rich_text': false,
+    'statuses_count': 0,
+    'cover_photo': '',
+    'hide_follows': false,
+    'pleroma': { 'confirmation_pending': false, 'deactivated': false, 'tags': ['force_nsfw'] },
+    'profile_image_url_original': '',
+    'created_at': 'Fri Mar 01 15:15:19 +0000 2019',
+    'fields': [],
+    'name': 'bob',
+    'description': '',
+    'favourites_count': 0,
+    'default_scope': 'public',
+    'profile_image_url_https': '',
+    'hide_followers': false,
+    'show_role': true,
+    'screen_name': 'bob' }
+
+  return Promise.resolve({ data: userInfo })
+}
diff --git a/src/api/__mocks__/users.js b/src/api/__mocks__/users.js
index c8e6fe9459feb478b7a7507d6ebae1c985876bf5..73a93174b9b8aef7867511c1958c0b1a5b20241d 100644
--- a/src/api/__mocks__/users.js
+++ b/src/api/__mocks__/users.js
@@ -4,7 +4,7 @@ const users = [
   { deactivated: true, id: 'abc', nickname: 'john', local: true, roles: { admin: false, moderator: false }, tags: ['strip_media'] }
 ]
 
-export async function fetchUsers(showLocalUsersOnly, token, page = 1) {
+export async function fetchUsers(showLocalUsersOnly, authHost, token, page = 1) {
   const filteredUsers = showLocalUsersOnly ? users.filter(user => user.local) : users
   return Promise.resolve({ data: {
     users: filteredUsers,
@@ -13,12 +13,12 @@ export async function fetchUsers(showLocalUsersOnly, token, page = 1) {
   }})
 }
 
-export async function toggleUserActivation(nickname, token) {
+export async function toggleUserActivation(nickname, authHost, token) {
   const response = users.find(user => user.nickname === nickname)
   return Promise.resolve({ data: { ...response, deactivated: !response.deactivated }})
 }
 
-export async function searchUsers(query, showLocalUsersOnly, token, page = 1) {
+export async function searchUsers(query, showLocalUsersOnly, authHost, token, page = 1) {
   const filteredUsers = showLocalUsersOnly ? users.filter(user => user.local) : users
   const response = filteredUsers.filter(user => user.nickname === query)
   return Promise.resolve({ data: {
@@ -28,28 +28,28 @@ export async function searchUsers(query, showLocalUsersOnly, token, page = 1) {
   }})
 }
 
-export async function addRight(nickname, right, token) {
+export async function addRight(nickname, right, authHost, token) {
   return Promise.resolve({ data:
     { [`is_${right}`]: true }
   })
 }
 
-export async function deleteRight(nickname, right, token) {
+export async function deleteRight(nickname, right, authHost, token) {
   return Promise.resolve({ data:
     { [`is_${right}`]: false }
   })
 }
 
-export async function deleteUser(nickname, token) {
+export async function deleteUser(nickname, authHost, token) {
   return Promise.resolve({ data:
     nickname
   })
 }
 
-export async function tagUser(nickname, tag, token) {
+export async function tagUser(nickname, tag, authHost, token) {
   return Promise.resolve()
 }
 
-export async function untagUser(nickname, tag, token) {
+export async function untagUser(nickname, tag, authHost, token) {
   return Promise.resolve()
 }
diff --git a/src/api/login.js b/src/api/login.js
index e3d990d1b06c794f5d218bac9dc5c853df66b1d7..adaabd54832fa1374a31adc28ed354bec14417b8 100644
--- a/src/api/login.js
+++ b/src/api/login.js
@@ -1,7 +1,9 @@
 import request from '@/utils/request'
+import { baseName } from './utils'
 
-export async function loginByUsername(username, password) {
+export async function loginByUsername(username, password, authHost) {
   const appsRequest = await request({
+    baseURL: baseName(authHost),
     url: '/api/v1/apps',
     method: 'post',
     data: {
@@ -14,6 +16,7 @@ export async function loginByUsername(username, password) {
   const app = appsRequest.data
 
   return request({
+    baseURL: baseName(authHost),
     url: '/oauth/token',
     method: 'post',
     data: {
@@ -26,8 +29,9 @@ export async function loginByUsername(username, password) {
   })
 }
 
-export function getUserInfo(token) {
+export function getUserInfo(token, authHost) {
   return request({
+    baseURL: baseName(authHost),
     url: '/api/account/verify_credentials',
     method: 'post',
     headers: token ? { 'Authorization': `Bearer ${token}` } : {}
diff --git a/src/api/users.js b/src/api/users.js
index 43ebe5e1572ec36d633eb04fbc30be9f12909123..2ec6a2abfb7c2a66189502279f41bd5ac6ae3614 100644
--- a/src/api/users.js
+++ b/src/api/users.js
@@ -1,56 +1,64 @@
 import request from '@/utils/request'
 import { getToken } from '@/utils/auth'
+import { baseName } from './utils'
 
-export async function fetchUsers(showLocalUsersOnly, token, page = 1) {
+export async function fetchUsers(showLocalUsersOnly, authHost, token, page = 1) {
   return await request({
+    baseURL: baseName(authHost),
     url: `/api/pleroma/admin/users?page=${page}&local_only=${showLocalUsersOnly}`,
     method: 'get',
     headers: authHeaders(token)
   })
 }
 
-export async function toggleUserActivation(nickname, token) {
+export async function toggleUserActivation(nickname, authHost, token) {
   return await request({
+    baseURL: baseName(authHost),
     url: `/api/pleroma/admin/users/${nickname}/toggle_activation`,
     method: 'patch',
     headers: authHeaders(token)
   })
 }
 
-export async function searchUsers(query, showLocalUsersOnly, token, page = 1) {
+export async function searchUsers(query, showLocalUsersOnly, authHost, token, page = 1) {
   return await request({
+    baseURL: baseName(authHost),
     url: `/api/pleroma/admin/users?query=${query}&page=${page}&local_only=${showLocalUsersOnly}`,
     method: 'get',
     headers: authHeaders(token)
   })
 }
 
-export async function addRight(nickname, right, token) {
+export async function addRight(nickname, right, authHost, token) {
   return await request({
+    baseURL: baseName(authHost),
     url: `/api/pleroma/admin/permission_group/${nickname}/${right}`,
     method: 'post',
     headers: authHeaders(token)
   })
 }
 
-export async function deleteRight(nickname, right, token) {
+export async function deleteRight(nickname, right, authHost, token) {
   return await request({
+    baseURL: baseName(authHost),
     url: `/api/pleroma/admin/permission_group/${nickname}/${right}`,
     method: 'delete',
     headers: authHeaders(token)
   })
 }
 
-export async function deleteUser(nickname, token) {
+export async function deleteUser(nickname, authHost, token) {
   return await request({
+    baseURL: baseName(authHost),
     url: `/api/pleroma/admin/user.json?nickname=${nickname}`,
     method: 'delete',
     headers: authHeaders(token)
   })
 }
 
-export async function tagUser(nickname, tag, token) {
+export async function tagUser(nickname, tag, authHost, token) {
   return await request({
+    baseURL: baseName(authHost),
     url: '/api/pleroma/admin/users/tag',
     method: 'put',
     headers: authHeaders(token),
@@ -58,8 +66,9 @@ export async function tagUser(nickname, tag, token) {
   })
 }
 
-export async function untagUser(nickname, tag, token) {
+export async function untagUser(nickname, tag, authHost, token) {
   return await request({
+    baseURL: baseName(authHost),
     url: '/api/pleroma/admin/users/tag',
     method: 'delete',
     headers: authHeaders(token),
diff --git a/src/api/utils.js b/src/api/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..4943f650835910a4bc6412bfcfd3cf7618334ed9
--- /dev/null
+++ b/src/api/utils.js
@@ -0,0 +1,5 @@
+const isLocalhost = (instanceName) =>
+  instanceName.startsWith('localhost:') || instanceName.startsWith('127.0.0.1:')
+
+export const baseName = (instanceName) =>
+  isLocalhost(instanceName) ? `http://${instanceName}` : `https://${instanceName}`
diff --git a/src/lang/en.js b/src/lang/en.js
index 655fb0ffdcfe14e8cca6aceabfd1142383cabaef..1c4097e73a01d7bcf8edc237bdc9a04d6c7c8f8b 100644
--- a/src/lang/en.js
+++ b/src/lang/en.js
@@ -76,8 +76,9 @@ export default {
   login: {
     title: 'Login Form',
     logIn: 'Log in',
-    username: 'Username',
+    username: 'Username@Host',
     password: 'Password',
+    errorMessage: 'Username must contain username and host, e.g. john@pleroma.social',
     any: 'any',
     thirdparty: 'Or connect with',
     thirdpartyTips: 'Can not be simulated on local, so please combine you own business simulation! ! !'
diff --git a/src/permission.js b/src/permission.js
index acd01b74f92d5cf3cb4b3369f95c8ff0e468785b..1439b1b976de50d83758947ce98ec0d4eab63416 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -16,7 +16,7 @@ function hasPermission(roles, permissionRoles) {
 
 const whiteList = ['/login', '/auth-redirect']// no redirect whitelist
 
-router.beforeEach((to, from, next) => {
+export const beforeEachRoute = (to, from, next) => {
   NProgress.start() // start progress bar
   if (getToken()) { // determine if there has token
     /* has token*/
@@ -24,12 +24,12 @@ router.beforeEach((to, from, next) => {
       next({ path: '/' })
       NProgress.done() // if current page is dashboard will not trigger	afterEach hook, so manually handle it
     } else {
-      if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
-        store.dispatch('GetUserInfo').then(res => { // 拉取user_info
+      if (store.getters.roles.length === 0) {
+        store.dispatch('GetUserInfo').then(res => {
           const roles = res.data.rights.admin ? ['admin'] : []
-          store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
-            router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
-            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
+          store.dispatch('GenerateRoutes', { roles }).then(() => {
+            router.addRoutes(store.getters.addRouters)
+            next({ ...to, replace: true })
           })
         }).catch((err) => {
           store.dispatch('FedLogOut').then(() => {
@@ -38,25 +38,24 @@ router.beforeEach((to, from, next) => {
           })
         })
       } else {
-        // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
         if (hasPermission(store.getters.roles, to.meta.roles)) {
           next()
         } else {
           next({ path: '/401', replace: true, query: { noGoBack: true }})
         }
-        // 可删 ↑
       }
     }
   } else {
     /* has no token*/
-    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
+    if (whiteList.indexOf(to.path) !== -1) {
       next()
     } else {
-      next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
+      next(`/login?redirect=${to.path}`)
       NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
     }
   }
-})
+}
+router.beforeEach(beforeEachRoute)
 
 router.afterEach(() => {
   NProgress.done() // finish progress bar
diff --git a/src/store/getters.js b/src/store/getters.js
index b20f2a1c3394d3f2867461cc9482bd6c9ba441fc..cec3bce11d27c686b9a8aa1f63c221754de0c47f 100644
--- a/src/store/getters.js
+++ b/src/store/getters.js
@@ -15,6 +15,7 @@ const getters = {
   permission_routers: state => state.permission.routers,
   addRouters: state => state.permission.addRouters,
   errorLogs: state => state.errorLog.logs,
-  users: state => state.users.fetchedUsers
+  users: state => state.users.fetchedUsers,
+  authHost: state => state.user.authHost
 }
 export default getters
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
index 1da451b667a371d7211a0ea673d9287f3cf11d4d..8c282e71c225612baecc03d2c6c384640b04bfd5 100644
--- a/src/store/modules/user.js
+++ b/src/store/modules/user.js
@@ -1,5 +1,5 @@
 import { loginByUsername, getUserInfo } from '@/api/login'
-import { getToken, setToken, removeToken } from '@/utils/auth'
+import { getToken, setToken, removeToken, getAuthHost, setAuthHost, removeAuthHost } from '@/utils/auth'
 
 const user = {
   state: {
@@ -8,6 +8,7 @@ const user = {
     status: '',
     code: '',
     token: getToken(),
+    authHost: getAuthHost(),
     name: '',
     avatar: '',
     introduction: '',
@@ -44,19 +45,24 @@ const user = {
     },
     SET_ID: (state, id) => {
       state.id = id
+    },
+    SET_AUTH_HOST: (state, authHost) => {
+      state.authHost = authHost
     }
   },
 
   actions: {
-    LoginByUsername({ commit }, userInfo) {
-      const username = userInfo.username.trim()
+    LoginByUsername({ commit, dispatch }, { username, authHost, password }) {
       return new Promise((resolve, reject) => {
-        loginByUsername(username, userInfo.password).then(response => {
+        loginByUsername(username, password, authHost).then(response => {
           const data = response.data
           commit('SET_TOKEN', data.access_token)
+          commit('SET_AUTH_HOST', authHost)
           setToken(data.access_token)
+          setAuthHost(authHost)
           resolve()
         }).catch(error => {
+          dispatch('addErrorLog', { message: error.message })
           reject(error)
         })
       })
@@ -64,7 +70,7 @@ const user = {
 
     GetUserInfo({ commit, state }) {
       return new Promise((resolve, reject) => {
-        getUserInfo(state.token).then(response => {
+        getUserInfo(state.token, state.authHost).then(response => {
           const data = response.data
 
           if (!data) {
@@ -91,11 +97,13 @@ const user = {
       commit('SET_TOKEN', '')
       commit('SET_ROLES', [])
       removeToken()
+      removeAuthHost()
     },
     FedLogOut({ commit }) {
       return new Promise(resolve => {
         commit('SET_TOKEN', '')
         removeToken()
+        removeAuthHost()
         resolve()
       })
     }
diff --git a/src/store/modules/users.js b/src/store/modules/users.js
index e3bf6e638427f5e117d4374f9db74b0c4325e1ac..09854c596a884c65784d6ab2c4013e7e60ca10d9 100644
--- a/src/store/modules/users.js
+++ b/src/store/modules/users.js
@@ -43,14 +43,14 @@ const users = {
   },
   actions: {
     async FetchUsers({ commit, state, getters }, { page }) {
-      const response = await fetchUsers(state.showLocalUsersOnly, getters.token, page)
+      const response = await fetchUsers(state.showLocalUsersOnly, getters.authHost, getters.token, page)
 
       commit('SET_LOADING', true)
 
       loadUsers(commit, page, response.data)
     },
     async ToggleUserActivation({ commit, getters }, nickname) {
-      const response = await toggleUserActivation(nickname, getters.token)
+      const response = await toggleUserActivation(nickname, getters.authHost, getters.token)
 
       commit('SWAP_USER', response.data)
     },
@@ -62,7 +62,7 @@ const users = {
         commit('SET_LOADING', true)
         commit('SET_SEARCH_QUERY', query)
 
-        const response = await searchUsers(query, state.showLocalUsersOnly, getters.token, page)
+        const response = await searchUsers(query, state.showLocalUsersOnly, getters.authHost, getters.token, page)
 
         loadUsers(commit, page, response.data)
       }
@@ -73,24 +73,24 @@ const users = {
     },
     async ToggleRight({ commit, getters }, { user, right }) {
       user.roles[right]
-        ? await deleteRight(user.nickname, right, getters.token)
-        : await addRight(user.nickname, right, getters.token)
+        ? await deleteRight(user.nickname, right, getters.authHost, getters.token)
+        : await addRight(user.nickname, right, getters.authHost, getters.token)
 
       const updatedUser = { ...user, roles: { ...user.roles, [right]: !user.roles[right] }}
       commit('SWAP_USER', updatedUser)
     },
     async DeleteUser({ commit, getters }, user) {
-      await deleteUser(user.nickname, getters.token)
+      await deleteUser(user.nickname, getters.authHost, getters.token)
       const updatedUser = { ...user, deactivated: true }
       commit('SWAP_USER', updatedUser)
     },
     async ToggleTag({ commit, getters }, { user, tag }) {
       if (user.tags.includes(tag)) {
-        await untagUser(user.nickname, tag, getters.token)
+        await untagUser(user.nickname, tag, getters.authHost, getters.token)
         const updatedUser = { ...user, tags: user.tags.filter(userTag => userTag !== tag) }
         commit('SWAP_USER', updatedUser)
       } else {
-        await tagUser(user.nickname, tag, getters.token)
+        await tagUser(user.nickname, tag, getters.authHost, getters.token)
         const updatedUser = { ...user, tags: [...user.tags, tag] }
         commit('SWAP_USER', updatedUser)
       }
diff --git a/src/utils/auth.js b/src/utils/auth.js
index 08a43d6e299227a695497dd2d8a312096315958b..f8939a9ae1c3d7ea488b8a1762880719468aedae 100644
--- a/src/utils/auth.js
+++ b/src/utils/auth.js
@@ -1,6 +1,7 @@
 import Cookies from 'js-cookie'
 
 const TokenKey = 'Admin-Token'
+const AuthHostKey = 'Auth-Host'
 
 export function getToken() {
   return Cookies.get(TokenKey)
@@ -13,3 +14,15 @@ export function setToken(token) {
 export function removeToken() {
   return Cookies.remove(TokenKey)
 }
+
+export function getAuthHost() {
+  return Cookies.get(AuthHostKey)
+}
+
+export function setAuthHost(token) {
+  return Cookies.set(AuthHostKey, token)
+}
+
+export function removeAuthHost() {
+  return Cookies.remove(AuthHostKey)
+}
diff --git a/src/utils/request.js b/src/utils/request.js
index 0bf34b092fbf389ea3e2a4ba280e5343e460e0f3..5fdfef8016271a712d3b8bf42381fed1c75ba91d 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -3,7 +3,6 @@ import { Message } from 'element-ui'
 
 // create an axios instance
 const service = axios.create({
-  baseURL: process.env.BASE_API, // api çš„ base_url
   timeout: 5000 // request timeout
 })
 
diff --git a/src/views/i18n-demo/index.vue b/src/views/i18n-demo/index.vue
deleted file mode 100644
index 08861bca278112b66a28cb163055180e992a31df..0000000000000000000000000000000000000000
--- a/src/views/i18n-demo/index.vue
+++ /dev/null
@@ -1,143 +0,0 @@
-<template>
-  <div>
-    <el-card class="box-card" style="margin-top:40px;">
-      <div slot="header" class="clearfix">
-        <svg-icon icon-class="international" />
-        <span style="margin-left:10px;">{{ $t('i18nView.title') }}</span>
-      </div>
-      <div>
-        <el-radio-group v-model="lang" size="small">
-          <el-radio label="zh" border>简体中文</el-radio>
-          <el-radio label="en" border>English</el-radio>
-          <el-radio label="es" border>Español</el-radio>
-        </el-radio-group>
-        <el-tag style="margin-top:15px;display:block;" type="info">{{ $t('i18nView.note') }}</el-tag>
-      </div>
-    </el-card>
-
-    <el-row :gutter="20" style="margin:100px 15px 50px;">
-      <el-col :span="12" :xs="24">
-        <div class="block">
-          <el-date-picker v-model="date" :placeholder="$t('i18nView.datePlaceholder')" type="date"/>
-        </div>
-        <div class="block">
-          <el-select v-model="value" :placeholder="$t('i18nView.selectPlaceholder')">
-            <el-option
-              v-for="item in options"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value"/>
-          </el-select>
-        </div>
-        <div class="block">
-          <el-button class="item-btn" size="small">{{ $t('i18nView.default') }}</el-button>
-          <el-button class="item-btn" size="small" type="primary">{{ $t('i18nView.primary') }}</el-button>
-          <el-button class="item-btn" size="small" type="success">{{ $t('i18nView.success') }}</el-button>
-          <el-button class="item-btn" size="small" type="info">{{ $t('i18nView.info') }}</el-button>
-          <el-button class="item-btn" size="small" type="warning">{{ $t('i18nView.warning') }}</el-button>
-          <el-button class="item-btn" size="small" type="danger">{{ $t('i18nView.danger') }}</el-button>
-        </div>
-      </el-col>
-      <el-col :span="12" :xs="24">
-        <el-table :data="tableData" fit highlight-current-row border style="width: 100%">
-          <el-table-column :label="$t('i18nView.tableName')" prop="name" width="100" align="center"/>
-          <el-table-column :label="$t('i18nView.tableDate')" prop="date" width="120" align="center"/>
-          <el-table-column :label="$t('i18nView.tableAddress')" prop="address"/>
-        </el-table>
-      </el-col>
-    </el-row>
-  </div>
-</template>
-
-<script>
-import local from './local'
-const viewName = 'i18nView'
-
-export default {
-  name: 'I18n',
-  data: function() {
-    return {
-      date: '',
-      tableData: [{
-        date: '2016-05-03',
-        name: 'Tom',
-        address: 'No. 189, Grove St, Los Angeles'
-      },
-      {
-        date: '2016-05-02',
-        name: 'Tom',
-        address: 'No. 189, Grove St, Los Angeles'
-      },
-      {
-        date: '2016-05-04',
-        name: 'Tom',
-        address: 'No. 189, Grove St, Los Angeles'
-      },
-      {
-        date: '2016-05-01',
-        name: 'Tom',
-        address: 'No. 189, Grove St, Los Angeles'
-      }],
-      options: [],
-      value: ''
-    }
-  },
-  computed: {
-    lang: {
-      get() {
-        return this.$store.state.app.language
-      },
-      set(lang) {
-        this.$i18n.locale = lang
-        this.$store.dispatch('setLanguage', lang)
-      }
-    }
-  },
-  watch: {
-    lang() {
-      this.setOptions()
-    }
-  },
-  created() {
-    if (!this.$i18n.getLocaleMessage('en')[viewName]) {
-      this.$i18n.mergeLocaleMessage('en', local.en)
-      this.$i18n.mergeLocaleMessage('zh', local.zh)
-      this.$i18n.mergeLocaleMessage('es', local.es)
-    }
-    this.setOptions() // set default select options
-  },
-  methods: {
-    setOptions() {
-      this.options = [
-        {
-          value: '1',
-          label: this.$t('i18nView.one')
-        },
-        {
-          value: '2',
-          label: this.$t('i18nView.two')
-        },
-        {
-          value: '3',
-          label: this.$t('i18nView.three')
-        }
-      ]
-    }
-  }
-}
-</script>
-
-<style scoped>
-.box-card {
-  width: 600px;
-  max-width: 100%;
-  margin: 20px auto;
-}
-.item-btn{
-  margin-bottom: 15px;
-  margin-left: 0px;
-}
-.block {
-  padding: 25px;
-}
-</style>
diff --git a/src/views/i18n-demo/local.js b/src/views/i18n-demo/local.js
deleted file mode 100644
index 9b43e60533b3f4225a3de332583531bf59a997e6..0000000000000000000000000000000000000000
--- a/src/views/i18n-demo/local.js
+++ /dev/null
@@ -1,63 +0,0 @@
-
-export default {
-  zh: {
-    i18nView: {
-      title: '切换语言',
-      note: '本项目国际化基于 vue-i18n',
-      datePlaceholder: '请选择日期',
-      selectPlaceholder: '请选择',
-      tableDate: '日期',
-      tableName: '姓名',
-      tableAddress: '地址',
-      default: '默认按钮',
-      primary: '主要按钮',
-      success: '成功按钮',
-      info: '信息按钮',
-      warning: '警告按钮',
-      danger: '危险按钮',
-      one: '一',
-      two: '二',
-      three: '三'
-    }
-  },
-  en: {
-    i18nView: {
-      title: 'Switch Language',
-      note: 'The internationalization of this project is based on vue-i18n',
-      datePlaceholder: 'Pick a day',
-      selectPlaceholder: 'Select',
-      tableDate: 'tableDate',
-      tableName: 'tableName',
-      tableAddress: 'tableAddress',
-      default: 'default:',
-      primary: 'primary',
-      success: 'success',
-      info: 'info',
-      warning: 'warning',
-      danger: 'danger',
-      one: 'One',
-      two: 'Two',
-      three: 'Three'
-    }
-  },
-  es: {
-    i18nView: {
-      title: 'Switch Language',
-      note: 'The internationalization of this project is based on vue-i18n',
-      datePlaceholder: 'Pick a day',
-      selectPlaceholder: 'Select',
-      tableDate: 'tableDate',
-      tableName: 'tableName',
-      tableAddress: 'tableAddress',
-      default: 'default:',
-      primary: 'primary',
-      success: 'success',
-      info: 'info',
-      warning: 'warning',
-      danger: 'danger',
-      one: 'One',
-      two: 'Two',
-      three: 'Three'
-    }
-  }
-}
diff --git a/src/views/login/index.vue b/src/views/login/index.vue
index c2a5d2028969a7224a44f54da683589dbcbb50d2..5ff5b857edb4ca2315fccd4a3105235a8a97d5b0 100644
--- a/src/views/login/index.vue
+++ b/src/views/login/index.vue
@@ -45,8 +45,13 @@
 </template>
 
 <script>
+import { Message } from 'element-ui'
+import SvgIcon from '@/components/SvgIcon'
+import i18n from '@/lang'
+
 export default {
   name: 'Login',
+  components: { 'svg-icon': SvgIcon },
   data: function() {
     return {
       loginForm: {
@@ -77,12 +82,35 @@ export default {
     },
     handleLogin() {
       this.loading = true
-      this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
-        this.loading = false
-        this.$router.push({ path: this.redirect || '/users/index' })
-      }).catch(() => {
+      if (this.checkUsername()) {
+        const loginData = this.getLoginData()
+        this.$store.dispatch('LoginByUsername', loginData).then(() => {
+          this.loading = false
+          this.$router.push({ path: this.redirect || '/users/index' })
+        }).catch(() => {
+          this.loading = false
+        })
+      } else {
+        Message({
+          message: i18n.t('login.errorMessage'),
+          type: 'error',
+          duration: 7000
+        })
+        this.$store.dispatch('addErrorLog', { message: i18n.t('login.errorMessage') })
         this.loading = false
-      })
+      }
+    },
+    checkUsername() {
+      return this.loginForm.username.includes('@')
+    },
+    getLoginData() {
+      const [username, authHost] = this.loginForm.username.split('@')
+
+      return {
+        username: username.trim(),
+        authHost: authHost.trim(),
+        password: this.loginForm.password
+      }
     }
   }
 }
diff --git a/test/views/layout/index.test.js b/test/views/layout/index.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..8e12783e6eeb95d24cc842162e44d8d40d0d6c3f
--- /dev/null
+++ b/test/views/layout/index.test.js
@@ -0,0 +1,47 @@
+import Vuex from 'vuex'
+import VueRouter from 'vue-router'
+import { mount, createLocalVue, config } from '@vue/test-utils'
+import Element from 'element-ui'
+import Layout from '@/views/layout/Layout'
+import storeConfig from './store.conf'
+import routerConfig from './router.conf'
+import { cloneDeep } from 'lodash'
+import { beforeEachRoute } from '@/permission'
+
+config.mocks["$t"] = () => {}
+
+const localVue = createLocalVue()
+localVue.use(Vuex)
+localVue.use(VueRouter)
+localVue.use(Element)
+
+describe('Log out', () => {
+  let store
+  let router
+
+  beforeEach(() => {
+    store = new Vuex.Store(cloneDeep(storeConfig))
+    router = new VueRouter(cloneDeep(routerConfig))
+    router.beforeEach(beforeEachRoute)
+    window.location.reload = jest.fn()
+  })
+
+  it('logs out user', async (done) => {
+    const wrapper = mount(Layout, {
+      store,
+      router,
+      localVue
+    })
+
+    const logoutButton = wrapper.find('span')
+    expect(store.state.user.roles.length).toBe(1)
+    expect(store.state.user.token).toBe('foo123')
+
+    logoutButton.trigger('click')
+    await wrapper.vm.$nextTick()
+
+    expect(store.state.user.roles.length).toBe(0)
+    expect(store.state.user.token).toBe('')
+    done()
+  })
+})
diff --git a/test/views/layout/router.conf.js b/test/views/layout/router.conf.js
new file mode 100644
index 0000000000000000000000000000000000000000..fcc4c4130b4f34017c8bc5499e8baf1df75f0d8d
--- /dev/null
+++ b/test/views/layout/router.conf.js
@@ -0,0 +1,40 @@
+import Layout from '@/views/layout/Layout'
+
+export const constantRouterMap = [
+  {
+    path: '/redirect',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: '/redirect/:path*',
+        component: () => import('@/views/redirect/index')
+      }
+    ]
+  },
+  {
+    path: '/login',
+    component: () => import('@/views/login/index'),
+    hidden: true
+  },
+  {
+    path: '/auth-redirect',
+    component: () => import('@/views/login/authredirect'),
+    hidden: true
+  },
+  {
+    path: '/404',
+    component: () => import('@/views/errorPage/404'),
+    hidden: true
+  },
+  {
+    path: '/401',
+    component: () => import('@/views/errorPage/401'),
+    hidden: true
+  },
+  {
+    path: '',
+    component: Layout,
+    redirect: '/users/index'
+  }
+]
diff --git a/test/views/layout/store.conf.js b/test/views/layout/store.conf.js
new file mode 100644
index 0000000000000000000000000000000000000000..1bc8bd04821e714b3ab28a647dd3239b847adda5
--- /dev/null
+++ b/test/views/layout/store.conf.js
@@ -0,0 +1,34 @@
+import app from '@/store/modules/app'
+import errorLog from '@/store/modules/errorLog'
+import permission from '@/store/modules/permission'
+import tagsView from '@/store/modules/tagsView'
+import user from '@/store/modules/user'
+import users from '@/store/modules/users'
+import getters from '@/store/getters'
+
+export default {
+  modules: {
+    app,
+    errorLog,
+    permission,
+    tagsView,
+    user: {
+      ...user,
+      state: {
+        id:"10",
+        status: '',
+        code: '',
+        token: "foo123",
+        authHost:"apple",
+        name:"bob",
+        avatar: '',
+        introduction: '',
+        roles: ['admin'],
+        setting: {
+          articlePlatform: []
+        }
+      }
+    }
+  },
+  getters
+}
diff --git a/test/views/login/index.test.js b/test/views/login/index.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..17ac438f301562da39c715184279917f6c58f09b
--- /dev/null
+++ b/test/views/login/index.test.js
@@ -0,0 +1,106 @@
+import Vuex from 'vuex'
+import VueRouter from 'vue-router'
+import { mount, createLocalVue, config } from '@vue/test-utils'
+import flushPromises from 'flush-promises'
+import Element from 'element-ui'
+import Login from '@/views/login/index'
+import storeConfig from './store.conf'
+import routerConfig from './router.conf'
+import { cloneDeep } from 'lodash'
+import { beforeEachRoute } from '@/permission'
+
+config.mocks["$t"] = () => {}
+
+const localVue = createLocalVue()
+localVue.use(Vuex)
+localVue.use(VueRouter)
+localVue.use(Element)
+
+jest.mock('@/api/login')
+
+describe('Login', () => {
+  let store
+  let router
+
+  const usernameInput = 'input[name="username"]'
+  const passwordInput = 'input[name="password"]'
+
+  beforeEach(() => {
+    store = new Vuex.Store(cloneDeep(storeConfig))
+    router = new VueRouter(cloneDeep(routerConfig))
+    router.beforeEach(beforeEachRoute)
+  })
+
+  it('throws error if username does not contain authHost', () => {
+    const wrapper = mount(Login, {
+      store,
+      router,
+      localVue
+    })
+
+    const errorLog = store.state.errorLog.logs
+    expect(errorLog.length).toBe(0)
+    const submitButton = wrapper.find('button')
+
+    wrapper.find(usernameInput).element.value = 'bob'
+    wrapper.find(usernameInput).trigger('input')
+    wrapper.find(passwordInput).element.value = '1234'
+    wrapper.find(passwordInput).trigger('input')
+    submitButton.trigger('click')
+
+    const updatedErrorLog = store.state.errorLog.logs
+    expect(updatedErrorLog.length).toBe(1)
+    expect(updatedErrorLog[0].message).toEqual(
+      'Username must contain username and host, e.g. john@pleroma.social'
+    )
+  })
+
+  it('throws error if password is incorrect', async (done) => {
+    const wrapper = mount(Login, {
+      store,
+      router,
+      localVue
+    })
+
+    const errorLog = store.state.errorLog.logs
+    expect(errorLog.length).toBe(0)
+    const submitButton = wrapper.find('button')
+
+    wrapper.find(usernameInput).element.value = 'bob@apple'
+    wrapper.find(usernameInput).trigger('input')
+    wrapper.find(passwordInput).element.value = '1234'
+    wrapper.find(passwordInput).trigger('input')
+    submitButton.trigger('click')
+    await flushPromises()
+
+    const updatedErrorLog = store.state.errorLog.logs
+    expect(updatedErrorLog.length).toBe(1)
+    expect(updatedErrorLog[0].message).toEqual(
+      'Invalid credentials'
+    )
+    done()
+  })
+
+  it('logs user in', async (done) => {
+    const wrapper = mount(Login, {
+      store,
+      router,
+      localVue
+    })
+
+    const errorLog = store.state.errorLog.logs
+    const submitButton = wrapper.find('button')
+    expect(wrapper.vm.$route.path).toBe('/login')
+
+    wrapper.find(usernameInput).element.value = 'bob@apple'
+    wrapper.find(usernameInput).trigger('input')
+    wrapper.find(passwordInput).element.value = '123456'
+    wrapper.find(passwordInput).trigger('input')
+    submitButton.trigger('click')
+    await flushPromises()
+
+    expect(errorLog.length).toBe(0)
+    expect(wrapper.vm.$route.path).toBe('/')
+    done()
+  })
+})
diff --git a/test/views/login/router.conf.js b/test/views/login/router.conf.js
new file mode 100644
index 0000000000000000000000000000000000000000..fcc4c4130b4f34017c8bc5499e8baf1df75f0d8d
--- /dev/null
+++ b/test/views/login/router.conf.js
@@ -0,0 +1,40 @@
+import Layout from '@/views/layout/Layout'
+
+export const constantRouterMap = [
+  {
+    path: '/redirect',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: '/redirect/:path*',
+        component: () => import('@/views/redirect/index')
+      }
+    ]
+  },
+  {
+    path: '/login',
+    component: () => import('@/views/login/index'),
+    hidden: true
+  },
+  {
+    path: '/auth-redirect',
+    component: () => import('@/views/login/authredirect'),
+    hidden: true
+  },
+  {
+    path: '/404',
+    component: () => import('@/views/errorPage/404'),
+    hidden: true
+  },
+  {
+    path: '/401',
+    component: () => import('@/views/errorPage/401'),
+    hidden: true
+  },
+  {
+    path: '',
+    component: Layout,
+    redirect: '/users/index'
+  }
+]
diff --git a/test/views/login/store.conf.js b/test/views/login/store.conf.js
new file mode 100644
index 0000000000000000000000000000000000000000..69bde7603f10733efc1fa3138fd0a77d019cf098
--- /dev/null
+++ b/test/views/login/store.conf.js
@@ -0,0 +1,16 @@
+import app from '@/store/modules/app'
+import errorLog from '@/store/modules/errorLog'
+import user from '@/store/modules/user'
+import users from '@/store/modules/users'
+import getters from '@/store/getters'
+
+export default {
+  modules: {
+    app,
+    errorLog,
+    user,
+    users
+  },
+  getters
+}
+
diff --git a/test/views/users/store.conf.js b/test/views/users/store.conf.js
index be9aabb7f6e59813b4a57632068d08df44dd55b0..77e2b7c8b7e2265c486d6c67e0ae07150fac76d9 100644
--- a/test/views/users/store.conf.js
+++ b/test/views/users/store.conf.js
@@ -1,28 +1,13 @@
-import getters  from '@/store/getters'
 import app from '@/store/modules/app'
 import user from '@/store/modules/user'
 import users from '@/store/modules/users'
+import getters from '@/store/getters'
 
 export default {
   modules: {
     app,
-    users,
-    user: {
-      state: {
-        user: '',
-        id: '1',
-        status: '',
-        code: '',
-        token: 'MmwSkMgBW6lAkWCFspIjX9icmLfqSCohSi-GReAZrQw',
-        name: 'john',
-        avatar: '',
-        introduction: '',
-        roles: ["admin"],
-        setting: {
-          articlePlatform: []
-        }
-      }, ...user
-    }
+    user,
+    users
   },
   getters
 }
diff --git a/yarn.lock b/yarn.lock
index c5460595d4eb08ff7ffc355a836b8a7085f68f5b..a4c90db284ee6944511b1d130a5f67d76fd8dcf5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4181,6 +4181,11 @@ flat-cache@^1.2.1:
     rimraf "~2.6.2"
     write "^0.2.1"
 
+flush-promises@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/flush-promises/-/flush-promises-1.0.2.tgz#4948fd58f15281fed79cbafc86293d5bb09b2ced"
+  integrity sha512-G0sYfLQERwKz4+4iOZYQEZVpOt9zQrlItIxQAAYAWpfby3gbHrx0osCHz5RLl/XoXevXk0xoN4hDFky/VV9TrA==
+
 flush-write-stream@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@@ -10164,18 +10169,6 @@ vue-jest@4.0.0-beta.2:
     source-map "^0.5.6"
     ts-jest "^23.10.5"
 
-vue-jest@4.0.0-beta.2:
-  version "4.0.0-beta.2"
-  resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-4.0.0-beta.2.tgz#f2120ea9d24224aad3a100c2010b0760d47ee6fe"
-  integrity sha512-SywBIciuIfqsCb8Eb9UQ02s06+NV8Ry8KnbyhAfnvnyFFulIuh7ujtga9eJYq720nCS4Hz4TpVtS4pD1ZbUILQ==
-  dependencies:
-    "@babel/plugin-transform-modules-commonjs" "^7.2.0"
-    "@vue/component-compiler-utils" "^2.4.0"
-    chalk "^2.1.0"
-    extract-from-css "^0.4.4"
-    source-map "^0.5.6"
-    ts-jest "^23.10.5"
-
 vue-loader@15.3.0:
   version "15.3.0"
   resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.3.0.tgz#b474d10a4e93d934a78c147fc3e314b370e9fc54"