Add login page
This commit is contained in:
parent
7cfb961e2b
commit
718ec2f7b4
173
resources/assets/src/components/auth/Login.vue
Normal file
173
resources/assets/src/components/auth/Login.vue
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="form-group has-feedback">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
v-model="identification"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('auth.identification')"
|
||||||
|
ref="identification"
|
||||||
|
>
|
||||||
|
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group has-feedback">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
v-model="password"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('auth.password')"
|
||||||
|
ref="password"
|
||||||
|
>
|
||||||
|
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="tooManyFails" class="row">
|
||||||
|
<div class="col-xs-8">
|
||||||
|
<div class="form-group has-feedback">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="captcha"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('auth.captcha')"
|
||||||
|
ref="captcha"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<img
|
||||||
|
class="pull-right captcha"
|
||||||
|
:src="`${baseUrl}/auth/captcha?v=${time}`"
|
||||||
|
alt="CAPTCHA"
|
||||||
|
:title="$t('auth.change-captcha')"
|
||||||
|
@click="refreshCaptcha"
|
||||||
|
data-placement="top"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout callout-info" :class="{ hide: !infoMsg }">{{ infoMsg }}</div>
|
||||||
|
<div class="callout callout-warning" :class="{ hide: !warningMsg }">{{ warningMsg }}</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<div class="checkbox icheck" style="margin-top: 0;">
|
||||||
|
<label>
|
||||||
|
<input v-model="remember" type="checkbox"> {{ $t('auth.keep') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<a class="pull-right" :href="`${baseUrl}/auth/forgot`" v-t="'auth.forgot-link'"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<button v-if="pending" disabled class="btn btn-primary btn-block btn-flat">
|
||||||
|
<i class="fa fa-spinner fa-spin"></i> {{ $t('auth.loggingIn') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
@click.prevent="login"
|
||||||
|
class="btn btn-primary btn-block btn-flat"
|
||||||
|
>{{ $t('auth.login') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { swal } from '../../js/notify';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Login',
|
||||||
|
props: {
|
||||||
|
baseUrl: {
|
||||||
|
default: blessing.base_url
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
identification: '',
|
||||||
|
password: '',
|
||||||
|
captcha: '',
|
||||||
|
remember: false,
|
||||||
|
time: Date.now(),
|
||||||
|
tooManyFails: __bs_data__.tooManyFails,
|
||||||
|
infoMsg: '',
|
||||||
|
warningMsg: '',
|
||||||
|
pending: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async login() {
|
||||||
|
const {
|
||||||
|
identification, password, captcha, remember
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
if (!identification) {
|
||||||
|
this.infoMsg = this.$t('auth.emptyIdentification');
|
||||||
|
this.$refs.identification.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
this.infoMsg = this.$t('auth.emptyPassword');
|
||||||
|
this.$refs.password.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tooManyFails && !captcha) {
|
||||||
|
this.infoMsg = this.$t('auth.emptyCaptcha');
|
||||||
|
this.$refs.captcha.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pending = true;
|
||||||
|
const { errno, msg, login_fails } = await this.$http.post(
|
||||||
|
'/auth/login',
|
||||||
|
{
|
||||||
|
identification,
|
||||||
|
password,
|
||||||
|
keep: remember,
|
||||||
|
captcha: this.tooManyFails ? captcha : undefined
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (errno === 0) {
|
||||||
|
swal({ type: 'success', html: msg });
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location = `${blessing.base_url}/${blessing.redirect_to || 'user'}`;
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
if (login_fails > 3 && !this.tooManyFails) {
|
||||||
|
swal({ type: 'error', html: this.$t('auth.tooManyFails') });
|
||||||
|
this.tooManyFails = true;
|
||||||
|
}
|
||||||
|
this.refreshCaptcha();
|
||||||
|
this.infoMsg = '';
|
||||||
|
this.warningMsg = msg;
|
||||||
|
this.pending = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshCaptcha() {
|
||||||
|
this.time = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.login-logo a {
|
||||||
|
font-family: Minecraft, Ubuntu, 'Segoe UI', 'Microsoft Yahei', 'Microsoft Jhenghei', sans-serif;
|
||||||
|
transition: all .2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo a:hover {
|
||||||
|
color: #42a5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-button {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -34,4 +34,9 @@ export default [
|
||||||
component: () => import('./admin/Customization'),
|
component: () => import('./admin/Customization'),
|
||||||
el: '#change-color'
|
el: '#change-color'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/login',
|
||||||
|
component: () => import('./auth/Login'),
|
||||||
|
el: 'form'
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
69
resources/assets/tests/components/auth/Login.test.js
Normal file
69
resources/assets/tests/components/auth/Login.test.js
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import Login from '@/components/auth/Login';
|
||||||
|
import { swal } from '@/js/notify';
|
||||||
|
|
||||||
|
jest.mock('@/js/notify');
|
||||||
|
|
||||||
|
test('show captcha if too many login fails', () => {
|
||||||
|
window.__bs_data__ = { tooManyFails: true };
|
||||||
|
const wrapper = mount(Login);
|
||||||
|
expect(wrapper.find('img').attributes().src).toMatch(/\/auth\/captcha\?v=\d+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('click to refresh captcha', () => {
|
||||||
|
window.__bs_data__ = { tooManyFails: true };
|
||||||
|
jest.spyOn(Date, 'now');
|
||||||
|
const wrapper = mount(Login);
|
||||||
|
wrapper.find('img').trigger('click');
|
||||||
|
expect(Date.now).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login', async () => {
|
||||||
|
window.__bs_data__ = { tooManyFails: false };
|
||||||
|
Vue.prototype.$http.post
|
||||||
|
.mockResolvedValueOnce({ errno: 1, msg: 'fail' })
|
||||||
|
.mockResolvedValueOnce({ errno: 1, login_fails: 4 })
|
||||||
|
.mockResolvedValueOnce({ errno: 0, msg: 'ok' });
|
||||||
|
const wrapper = mount(Login);
|
||||||
|
const button = wrapper.find('button');
|
||||||
|
const info = wrapper.find('.callout-info');
|
||||||
|
const warning = wrapper.find('.callout-warning');
|
||||||
|
|
||||||
|
button.trigger('click');
|
||||||
|
expect(Vue.prototype.$http.post).not.toBeCalled();
|
||||||
|
expect(info.text()).toBe('auth.emptyIdentification');
|
||||||
|
|
||||||
|
wrapper.find('[type="email"]').setValue('a@b.c');
|
||||||
|
button.trigger('click');
|
||||||
|
expect(Vue.prototype.$http.post).not.toBeCalled();
|
||||||
|
expect(info.text()).toBe('auth.emptyPassword');
|
||||||
|
|
||||||
|
wrapper.find('[type="password"]').setValue('123');
|
||||||
|
button.trigger('click');
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||||
|
'/auth/login',
|
||||||
|
{ identification: 'a@b.c', password: '123', keep: false }
|
||||||
|
);
|
||||||
|
expect(warning.text()).toBe('fail');
|
||||||
|
|
||||||
|
button.trigger('click');
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
expect(swal).toBeCalledWith({ type: 'error', html: 'auth.tooManyFails' });
|
||||||
|
expect(wrapper.find('img').exists()).toBeTrue();
|
||||||
|
|
||||||
|
button.trigger('click');
|
||||||
|
expect(info.text()).toBe('auth.emptyCaptcha');
|
||||||
|
|
||||||
|
wrapper.find('[type="text"]').setValue('a');
|
||||||
|
wrapper.find('[type="checkbox"]').setChecked();
|
||||||
|
button.trigger('click');
|
||||||
|
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||||
|
'/auth/login',
|
||||||
|
{ identification: 'a@b.c', password: '123', keep: true, captcha: 'a' }
|
||||||
|
);
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(swal).toBeCalledWith({ type: 'success', html: 'ok' });
|
||||||
|
});
|
||||||
|
|
@ -17,6 +17,15 @@ auth:
|
||||||
sending: Sending
|
sending: Sending
|
||||||
reset: Reset
|
reset: Reset
|
||||||
resetting: Resetting
|
resetting: Resetting
|
||||||
|
nickname: Nickname
|
||||||
|
email: Email
|
||||||
|
identification: Email or player name
|
||||||
|
password: Password
|
||||||
|
captcha: CAPTCHA
|
||||||
|
change-captcha: Click to change CAPTCHA image.
|
||||||
|
login-link: Already registered? Log in here.
|
||||||
|
forgot-link: Forgot password?
|
||||||
|
keep: Remember me
|
||||||
|
|
||||||
skinlib:
|
skinlib:
|
||||||
addToCloset: Add to closet
|
addToCloset: Add to closet
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,15 @@ auth:
|
||||||
sending: 发送中
|
sending: 发送中
|
||||||
reset: 重置
|
reset: 重置
|
||||||
resetting: 重置中
|
resetting: 重置中
|
||||||
|
nickname: 昵称
|
||||||
|
email: Email
|
||||||
|
identification: Email 或角色名
|
||||||
|
password: 密码
|
||||||
|
captcha: 请输入验证码
|
||||||
|
change-captcha: 点击以更换图片
|
||||||
|
login-link: 已经有账号了?登录
|
||||||
|
forgot-link: 忘记密码?
|
||||||
|
keep: 记住我
|
||||||
|
|
||||||
skinlib:
|
skinlib:
|
||||||
addToCloset: 添加至衣柜
|
addToCloset: 添加至衣柜
|
||||||
|
|
|
||||||
|
|
@ -16,50 +16,7 @@
|
||||||
<div class="callout callout-warning">{{ Session::pull('msg') }}</div>
|
<div class="callout callout-warning">{{ Session::pull('msg') }}</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<form id="login-form">
|
<form></form>
|
||||||
<div class="form-group has-feedback">
|
|
||||||
<input id="identification" type="email" class="form-control" placeholder="@lang('auth.identification')">
|
|
||||||
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
|
||||||
</div>
|
|
||||||
<div class="form-group has-feedback">
|
|
||||||
<input id="password" type="password" class="form-control" placeholder="@lang('auth.password')">
|
|
||||||
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" id="captcha-form" style="{{ (session('login_fails') > 3) ? '' : 'display: none;' }}">
|
|
||||||
<div class="col-xs-8">
|
|
||||||
<div class="form-group has-feedback">
|
|
||||||
<input id="captcha" type="text" class="form-control" placeholder="@lang('auth.captcha')">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /.col -->
|
|
||||||
<div class="col-xs-4">
|
|
||||||
<img class="pull-right captcha" src="{{ url('auth/captcha?v='.time()) }}" alt="CAPTCHA" title="@lang('auth.change-captcha')" data-placement="top" data-toggle="tooltip">
|
|
||||||
</div>
|
|
||||||
<!-- /.col -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="msg" class="callout hide"></div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-6">
|
|
||||||
<div class="checkbox icheck" style="margin-top: 0;">
|
|
||||||
<label for="keep">
|
|
||||||
<input id="keep" type="checkbox"> @lang('auth.login.keep')
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.col -->
|
|
||||||
<div class="col-xs-6">
|
|
||||||
<a class="pull-right" href="{{ url('auth/forgot') }}">@lang('auth.forgot-link')</a>
|
|
||||||
</div><!-- /.col -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
<button id="login-button" class="btn btn-primary btn-block btn-flat">@lang('auth.login.button')</button>
|
|
||||||
</div><!-- /.col -->
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<br>
|
<br>
|
||||||
<a href="{{ url('auth/register') }}" class="pull-left" style="margin-top: -10px;">@lang('auth.register-link')</a>
|
<a href="{{ url('auth/register') }}" class="pull-left" style="margin-top: -10px;">@lang('auth.register-link')</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -67,4 +24,12 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- /.login-box -->
|
<!-- /.login-box -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Object.defineProperty(window, '__bs_data__', {
|
||||||
|
get: function () {
|
||||||
|
return Object.freeze({ tooManyFails: {{ session('login_fails') > 3 ? 'true' : 'false' }} })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user