support update password and send password reset email

This commit is contained in:
zema1 2017-09-10 18:29:01 +08:00
parent 69c3178a58
commit f6b833594e
19 changed files with 497 additions and 104 deletions

View File

@ -18,7 +18,7 @@ module.exports = {
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
"space-before-function-paren": ["warn", {
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
@ -26,6 +26,7 @@ module.exports = {
"no-irregular-whitespace": ["error", {
"skipComments": true,
"skipTemplates": true
}]
}],
"no-unused-vars": ["warn"]
}
}

View File

@ -35,9 +35,6 @@ export default {
getCaptcha() {
return ajax('captcha', 'get')
},
getTwoFactorQrcode() {
return ajax('two_factor_auth', 'get')
},
// 获取自身信息
getUserInfo(username = undefined) {
return ajax('profile', 'get', {
@ -46,13 +43,25 @@ export default {
}
})
},
// 保存用户资料设置
updateProfile(profile) {
return ajax('profile', 'put', {
data: profile
})
},
getTwoFactorQrcode() {
return ajax('two_factor_auth', 'get')
},
apply_reset_password(data) {
return ajax('apply_reset_password', 'post', {
data
})
},
changePassword(data) {
return ajax('change_password', 'post', {
data
})
},
getLanguages() {
return ajax('languages', 'get')
},

View File

@ -56,7 +56,7 @@
</Dropdown>
</template>
</Menu>
<Register :visible.sync="registerModalVisible" :mode.sync="modalMode"></Register>
<LoginOrRegister :visible.sync="registerModalVisible" :mode.sync="modalMode"></LoginOrRegister>
</div>
</template>
@ -65,11 +65,11 @@
import api from '@/api'
import auth from '../utils/auth'
import Register from '@/views/user/LoginORRegister'
import LoginOrRegister from '@/views/user/LoginOrRegister'
export default {
components: {
Register
LoginOrRegister
},
data() {
return {

View File

@ -0,0 +1,23 @@
import api from '@/api'
export default {
methods: {
validateForm(formName) {
return new Promise((resolve, reject) => {
this.$refs[formName].validate(valid => {
if (!valid) {
this.$error('please validate the error fields')
reject(valid)
} else {
resolve(valid)
}
})
})
},
getCaptchaSrc() {
api.getCaptcha().then(res => {
this.captchaSrc = res.data.data
})
}
}
}

View File

@ -1,5 +1,6 @@
import Emitter from './emitter'
import ProblemMixin from './problem'
import SettingMixin from './setting'
import FormMixin from './form'
export {Emitter, ProblemMixin, SettingMixin}
export {Emitter, ProblemMixin, SettingMixin, FormMixin}

View File

@ -50,6 +50,9 @@ Vue.component(Panel.name, Panel)
// Vue.use(VueI18n)
// 注册全局消息提示
Vue.prototype.$Message.config({
duration: 1.8
})
Vue.prototype.$error = Vue.prototype.$Message.error
Vue.prototype.$info = Vue.prototype.$Message.info
Vue.prototype.$success = Vue.prototype.$Message.success

View File

@ -1,8 +1,10 @@
// all routes here.
import Test from '../views/test'
import {
ProblemList, ContestList, ContestDetails, ContestProblemList, ContestAnnouncement, ContestRank,
Logout, ACMRank, Settings, ProfileSetting
Logout, ApplyResetPassword, ResetPassword,
ProfileSetting, SecuritySetting, Settings,
ContestAnnouncement, ContestDetails, ContestList, ContestProblemList, ContestRank,
ProblemList, ACMRank
} from '../views'
export default [
@ -18,6 +20,16 @@ export default [
path: '/logout',
component: Logout
},
{
name: 'apply-reset-password',
path: '/apply-reset-password',
component: ApplyResetPassword
},
{
name: 'reset-password',
path: '/reset-password/:token',
component: ResetPassword
},
{
name: 'problem-list',
path: '/problems',
@ -93,6 +105,11 @@ export default [
name: 'profile-setting',
path: 'profile',
component: ProfileSetting
},
{
name: 'security-setting',
path: 'security',
component: SecuritySetting
}
]
},
@ -100,8 +117,8 @@ export default [
path: '/test',
name: 'Test',
component: Test
},
{
path: '*', redirect: '/problems'
}
// {
// path: '*', redirect: '/problems'
// }
]

View File

@ -8,8 +8,8 @@
.section-title {
font-size: 21px;
font-weight: 400;
padding: 20px 20px;
font-weight: 500;
padding: 20px 25px;
line-height: 30px;
}

View File

@ -28,3 +28,11 @@ table {
color: #a94442;
background-color: #f2dede;
}
.ivu-modal-footer {
border-top-width: 0;
padding: 0 18px 20px 18px;
}
.ivu-modal-body {
padding: 18px;
}

View File

@ -148,7 +148,7 @@
this.applyToTable(res.data.data.results)
})
},
getContestAndProblems(contestID) {
getContestAndProblems() {
// localStorage
this.contest = utils.loadContest(this.contestID)
let problems = storage.get(STORAGE_KEY.contestProblems + this.contestID)

View File

@ -1,12 +1,15 @@
import ProblemList from './problem/ProblemList.vue'
import ACMRank from './rank/ACMRank.vue'
import Logout from './user/Logout.vue'
import ApplyResetPassword from './user/ApplyResetPassword.vue'
import ResetPassword from './user/ResetPassword.vue'
export {
ProblemList, ACMRank, Logout
Logout, ResetPassword, ApplyResetPassword,
ProblemList, ACMRank
}
export {ContestRank, ContestProblemList, ContestList, ContestDetails, ContestAnnouncement} from './contest'
export {Settings, ProfileSetting} from './setting'
export {Settings, ProfileSetting, SecuritySetting} from './setting'
/* , Login, Logout,
* 在对应的route内加载
* 见https://router.vuejs.org/en/advanced/lazy-loading.html

View File

@ -2,7 +2,7 @@
<Row type="flex" :gutter="18">
<Col :span=20>
<Panel shadow>
<div slot="title">Problems List</div>
<div slot="title">Problem List</div>
<div slot="extra">
<ul class="filter">
<li>

View File

@ -1,21 +1,25 @@
<template>
<Card :padding="0" id="settings-card">
<div class="flex-container">
<div class="menu">
<Menu accordion @on-select="goRoute" activeName="/setting/profile" style="text-align: center;">
<div>
<img class="avatar" src="../../assets/profile.jpg"/>
</div>
<Menu-item name="/setting/profile">Profile</Menu-item>
<Menu-item name="/setting/1">Security</Menu-item>
<Menu-item name="/setting/2">Perference</Menu-item>
</Menu>
<Row type="flex" justify="space-around">
<Col :span="22">
<Card :padding="0" id="settings-card">
<div class="flex-container">
<div class="menu">
<Menu accordion @on-select="goRoute" activeName="/setting/profile" style="text-align: center;">
<div>
<img class="avatar" src="../../assets/profile.jpg"/>
</div>
<Menu-item name="/setting/profile">Profile</Menu-item>
<Menu-item name="/setting/security">Security</Menu-item>
<Menu-item name="/setting/2">Perference</Menu-item>
</Menu>
</div>
<div class="panel">
<router-view></router-view>
</div>
</div>
<div class="panel">
<router-view></router-view>
</div>
</div>
</Card>
</Card>
</Col>
</Row>
</template>
<script>
export default {
@ -65,3 +69,10 @@
width: 0;
}
</style>
<style>
.setting-main {
margin: 10px 40px;
padding-bottom: 40px;
}
</style>

View File

@ -1,48 +1,49 @@
<template>
<div>
<panel :padding="0" :bordered="false" dis-hover>
<div slot="title">Profile Settings</div>
<div slot="extra">
<Button type="primary" @click="updateProfile">Save All</Button>
</div>
<Form ref="formProfile" :model="formProfile">
<Row type="flex" :gutter="30" justify="space-around">
<Col :span="10">
<FormItem label="Real Name">
<Input v-model="formProfile.real_name"/>
</FormItem>
<Form-item label="Phone">
<Input v-model="formProfile.phone_number"/>
</Form-item>
<Form-item label="Mood">
<Input v-model="formProfile.mood"/>
</Form-item>
</Col>
<panel :padding="0" :bordered="false" dis-hover>
<div slot="title">Profile Setting</div>
<div slot="extra">
<Button type="primary" @click="updateProfile" :loading="btnLoading">Save All</Button>
</div>
<Form ref="formProfile" :model="formProfile">
<Row type="flex" :gutter="30" justify="space-around">
<Col :span="10">
<FormItem label="Real Name">
<Input v-model="formProfile.real_name"/>
</FormItem>
<Form-item label="Phone">
<Input v-model="formProfile.phone_number"/>
</Form-item>
<Form-item label="Mood">
<Input v-model="formProfile.mood"/>
</Form-item>
</Col>
<Col :span="10">
<Form-item label="Major">
<Input v-model="formProfile.major" />
</Form-item>
<Form-item label="Blog">
<Input v-model="formProfile.blog"/>
</Form-item>
<Form-item label="Language">
<Input v-model="formProfile.language"/>
</Form-item>
</Col>
</Row>
</Form>
</panel>
</div>
<Col :span="10">
<Form-item label="Major">
<Input v-model="formProfile.major"/>
</Form-item>
<Form-item label="Blog">
<Input v-model="formProfile.blog"/>
</Form-item>
<Form-item label="Language">
<Input v-model="formProfile.language"/>
</Form-item>
</Col>
</Row>
</Form>
</panel>
</template>
<script>
import api from '@/api.js'
import auth from '@/utils/auth'
import {SettingMixin} from '~/mixins'
export default {
mixins: [SettingMixin],
data() {
return {
btnLoading: false,
formProfile: {
real_name: '',
mood: '',
@ -58,19 +59,23 @@
},
methods: {
getProfile() {
if (!auth.isAuthicated()) {
this.$error('please login first.')
} else {
let profile = auth.getUser()
let profile = this.loadProfile()
if (profile !== null && profile !== undefined) {
Object.keys(this.formProfile).forEach(element => {
this.formProfile[element] = profile[element]
if (profile[element] !== undefined) {
this.formProfile[element] = profile[element]
}
})
}
},
updateProfile() {
this.btnLoading = true
api.updateProfile(this.formProfile).then(res => {
this.$success('Success')
this.btnLoading = false
auth.setUser(res.data.data)
}, _ => {
this.btnLoading = false
})
}
}

View File

@ -0,0 +1,159 @@
<template>
<Card :padding="0" :bordered="false" dis-hover>
<div class="flex-container">
<div class="left">
<p class="section-title">Change Password</p>
<Form class="setting-main" ref="formPassword" :model="formPassword" :rules="rulePassword">
<FormItem label="Old password" prop="old_password">
<Input v-model="formPassword.old_password" type="password"/>
</FormItem>
<FormItem label="New password" prop="new_password">
<Input v-model="formPassword.new_password" type="password"/>
</FormItem>
<FormItem label="Confirm new password" prop="again_password">
<Input v-model="formPassword.again_password" type="password"/>
</FormItem>
<FormItem v-if="visible.passwordAlert">
<Alert type="success">Password successfully updated, you have to login again after 3 seconds..</Alert>
</FormItem>
<Button type="primary" @click="changePassword">Update password</Button>
</Form>
</div>
<div class="middle separator"></div>
<div class="right">
<p class="section-title">Change Email</p>
<Form class="setting-main" ref="formEmail" :model="formEmail">
<FormItem label="Current password">
<Input v-model="formEmail.password"/>
</FormItem>
<FormItem label="Old Email">
<Input v-model="formEmail.old_email" disabled/>
</FormItem>
<FormItem label="New Email">
<Input v-model="formEmail.new_email"/>
</FormItem>
<Button type="primary">Change Email</Button>
</Form>
</div>
</div>
<!--<img :src="qrcodeSrc" id="qr-img"/>-->
</Card>
</template>
<script>
import api from '@/api.js'
import {SettingMixin} from '~/mixins'
export default {
mixins: [SettingMixin],
data() {
const validatePass = (rule, value, callback) => {
if (this.formPassword.old_password !== '') {
if (this.formPassword.old_password === this.formPassword.new_password) {
callback(new Error('The new password doesn\'t change'))
} else {
//
this.$refs.formPassword.validateField('again_password')
}
}
callback()
}
const validatePassAgain = (rule, value, callback) => {
if (value !== this.formPassword.new_password) {
callback(new Error('password does not match'))
}
callback()
}
return {
qrcodeSrc: '',
loading: {
btnPassword: false,
btnEmail: false
},
visible: {
passwordAlert: false,
emailAlert: false
},
formPassword: {
old_password: '',
new_password: '',
again_password: ''
},
formEmail: {
password: '',
old_email: '',
new_email: ''
},
rulePassword: {
old_password: [
{required: true, trigger: 'blur', min: 6, max: 20}
],
new_password: [
{required: true, trigger: 'blur', min: 6, max: 20},
{validator: validatePass, trigger: 'blur'}
],
again_password: [
{required: true, validator: validatePassAgain, trigger: 'change'}
]
},
ruleEmail: {}
}
},
mounted() {
this.getProfile()
},
methods: {
getProfile() {
let profile = this.loadProfile()
if (profile !== null && profile !== undefined) {
this.formEmail.old_email = profile.user.email
}
},
changePassword() {
this.loading.btnPassword = true
let data = Object.assign({}, this.formPassword)
delete data.again_password
api.changePassword(data).then(res => {
this.loading.btnPassword = false
this.visible.passwordAlert = true
setTimeout(() => {
this.visible.passwordAlert = false
this.$router.push({name: 'logout'})
}, 3000)
}, _ => {
this.loading.btnPassword = false
})
},
changeEmail() {
this.btnEmailLoading = true
// todo
},
getAuthImg() {
api.getTwoFactorQrcode().then(res => {
this.qrcodeSrc = res.data.data
})
}
}
}
</script>
<style lang="less" scoped>
.flex-container {
justify-content: flex-start;
.left {
flex: 1 1;
padding-right: 10%;
}
.middle {
flex: none;
}
.right {
flex: 1 1;
padding-right: 10%;
}
}
</style>

View File

@ -1,4 +1,5 @@
import Settings from './Settings.vue'
import ProfileSetting from './children/ProfileSetting.vue'
import SecuritySetting from './children/SecuritySetting.vue'
export {Settings, ProfileSetting}
export {Settings, ProfileSetting, SecuritySetting}

View File

@ -0,0 +1,131 @@
<template>
<Panel :padding="30" class="container">
<div slot="title" class="center">Lost Password</div>
<template v-if="!successApply">
<Form :rules="ruleResetPassword" :model=formResetPassword ref="formResetPassword">
<Form-item prop="email">
<Input v-model="formResetPassword.email" placeholder="Your Email Address" size="large">
<Icon type="ios-email-outline" slot="prepend"></Icon>
</Input>
</Form-item>
<Form-item prop="captcha" style="margin-bottom:10px">
<div id="captcha">
<div id="captchaCode">
<Input v-model="formResetPassword.captcha" placeholder="Captcha" size="large">
<Icon type="ios-lightbulb-outline" slot="prepend"></Icon>
</Input>
</div>
<div id="captchaImg">
<Tooltip content="Click to refresh" placement="top">
<img :src="captchaSrc" @click="getCaptchaSrc"/>
</Tooltip>
</div>
</div>
</Form-item>
</Form>
<Button type="primary"
@click="sendEmail"
class="btn" long
:loading="btnLoading">Send Password Reset Email
</Button>
</template>
<template v-else>
<Alert type="success" show-icon>
Success
<span slot="desc">Password reset mail has been sent to your email.</span>
</Alert>
</template>
</Panel>
</template>
<script>
import api from '@/api'
import {FormMixin} from '~/mixins'
export default {
mixins: [FormMixin],
data() {
const validateEmail = (rule, value, callback) => {
if (value !== '') {
api.checkUsernameOrEmail(undefined, value).then(res => {
if (res.data.data.email === false) {
callback(new Error('This email doesn\'t exist'))
} else {
callback()
}
}, _ => callback())
} else {
callback()
}
}
return {
captchaSrc: '',
successApply: false,
btnLoading: false,
formResetPassword: {
email: '',
captcha: ''
},
ruleResetPassword: {
email: [
{required: true, type: 'email', trigger: 'blur'},
{validator: validateEmail, trigger: 'blur'}
],
captcha: [
{required: true, trigger: 'blur', min: 1, max: 10}
]
}
}
},
mounted() {
this.getCaptchaSrc()
},
methods: {
sendEmail() {
this.validateForm('formResetPassword').then(() => {
this.btnLoading = true
api.apply_reset_password(this.formResetPassword).then(res => {
//
setTimeout(() => {
this.btnLoading = false
this.successApply = true
}, 2000)
}, _ => {
this.btnLoading = false
this.formResetPassword.captcha = ''
this.getCaptchaSrc()
})
}, _ => {
})
}
}
}
</script>
<style scoped lang="less">
.container {
width: 450px;
margin: auto;
.center {
text-align: center;
}
#captcha {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
width: 100%;
height: 36px;
#captchaCode {
flex: auto;
}
#captchaImg {
margin-left: 10px;
padding: 3px;
flex: initial;
}
}
.btn {
margin-top: 18px;
text-align: center;
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<Modal :value="visible" @on-cancel="handleUpdateProp('update:visible', false)" :width="350">
<Modal :value="visible" @on-cancel="handleUpdateProp('update:visible', false)" :width="400" className="modal">
<div slot="header">
<span class="title">Welcome to OJ</span>
</div>
@ -42,7 +42,7 @@
<Form-item prop="captcha" style="margin-bottom:10px">
<div id="captcha">
<div id="captchaCode">
<Input v-model="formRegister.captcha" placeholder="Capacha" size="large">
<Input v-model="formRegister.captcha" placeholder="Captcha" size="large">
<Icon type="ios-lightbulb-outline" slot="prepend"></Icon>
</Input>
</div>
@ -57,12 +57,30 @@
</template>
<div slot="footer" class="footer">
<template v-if="mode === 'login'">
<Button type="primary" @click="handleLogin()" class="btn" long>Login</Button>
<Button
type="primary"
@click="handleLogin()"
class="btn" long
:loading="btnLoginLoading">
Login
</Button>
<a @click.stop="handleUpdateProp('update:mode', 'register')">No account? Register now!</a>
<a @click.stop="goResetPassword" style="float: right">Forget Password</a>
</template>
<template v-else>
<Button type="primary" @click="handleRegister()" class="btn" long>Register Now</Button>
<a @click.stop="handleUpdateProp('update:mode', 'login')">Already registed? Login now!</a>
<Button
type="primary"
@click="handleRegister()"
class="btn" long
:loading="btnRegisterLoading">
Register
</Button>
<Button
type="ghost"
@click="handleUpdateProp('update:mode', 'login')"
class="btn" long>
Already registed? Login now!
</Button>
</template>
</div>
</Modal>
@ -71,8 +89,10 @@
<script>
import api from '@/api'
import auth from '@/utils/auth'
import {FormMixin} from '~/mixins'
export default {
mixins: [FormMixin],
props: {
visible: {
required: true,
@ -90,7 +110,7 @@
const validateUsername = (rule, value, callback) => {
if (value !== '') {
api.checkUsernameOrEmail(value, undefined).then(res => {
if (res.data.data.username === false) {
if (res.data.data.username === true) {
callback(new Error('username already exists.'))
} else {
callback()
@ -103,8 +123,8 @@
const validateEmail = (rule, value, callback) => {
if (value !== '') {
api.checkUsernameOrEmail(undefined, value).then(res => {
if (res.data.data.email === false) {
callback(new Error('email already exists'))
if (res.data.data.email === true) {
callback(new Error('email already exist'))
} else {
callback()
}
@ -129,6 +149,8 @@
return {
captchaSrc: '',
btnRegisterLoading: false,
btnLoginLoading: false,
formRegister: {
username: '',
password: '',
@ -171,32 +193,30 @@
}
},
methods: {
validateForm(formName) {
let isValid = false
this.$refs[formName].validate(valid => {
if (!valid) {
this.$error('please validate the error fields')
}
isValid = valid
})
return isValid
handleUpdateProp(eventName, value) {
this.$emit(eventName, value)
},
handleRegister() {
if (this.validateForm('formRegister')) {
let formData = Object.assign({}, this.formRegister)
delete formData['passwordAgain']
this.btnRegisterLoading = true
api.register(formData).then(res => {
this.$success('Register successed, go to login')
this.handleUpdateProp('update:mode', 'login')
this.btnRegisterLoading = false
}, _ => {
this.getCaptchaSrc()
this.formRegister.captcha = ''
this.btnRegisterLoading = false
})
}
},
handleLogin() {
if (this.validateForm('formLogin')) {
this.btnLoginLoading = true
api.login(this.formLogin.uname, this.formLogin.passwd).then(res => {
this.btnLoginLoading = false
api.getUserInfo().then(res => {
auth.setUser(res.data.data)
this.$bus.$emit('login-success', res.data.data)
@ -204,16 +224,14 @@
this.handleUpdateProp('update:visible', false)
})
}, _ => {
this.btnLoginLoading = false
})
}
},
handleUpdateProp(eventName, value) {
this.$emit(eventName, value)
},
getCaptchaSrc() {
api.getCaptcha().then(res => {
this.captchaSrc = res.data.data
})
goResetPassword() {
this.handleUpdateProp('update:visible', false)
this.$router.push({name: 'apply-reset-password'})
}
},
watch: {
@ -255,15 +273,18 @@
}
.title {
font-size: 16px;
font-size: 18px;
font-weight: 600;
}
.footer {
text-align: center;
margin: 0;
text-align: left;
.btn {
margin: 0 0 10px 0;
margin: 0 0 15px 0;
&:last-child {
margin: 0;
}
}
}
</style>

View File