Add My account page to change password and delete account

This commit is contained in:
nemunaire 2020-07-21 04:02:18 +02:00
parent b014ef1af2
commit 4f3110cddc
5 changed files with 330 additions and 6 deletions

View File

@ -57,7 +57,9 @@ func init() {
router.POST("/api/users", ApiHandler(registerUser))
router.PATCH("/api/users", ApiHandler(specialUserOperations))
router.GET("/api/users/:uid", apiAuthHandler(sameUserHandler(getUser)))
router.POST("/api/users/:uid/delete", apiAuthHandler(sameUserHandler(deleteUser)))
router.POST("/api/users/:uid/email", ApiHandler(userHandler(validateUserAddress)))
router.POST("/api/users/:uid/new_password", apiAuthHandler(sameUserHandler(changePassword)))
router.POST("/api/users/:uid/recovery", ApiHandler(userHandler(recoverUserAccount)))
}
@ -231,12 +233,7 @@ func sameUserHandler(f func(*config.Options, *RequestResources, io.Reader) Respo
status: http.StatusNotFound,
err: fmt.Errorf("Invalid user identifier given: %w", err),
}
} else if user, err := storage.MainStore.GetUser(uid); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("User not found"),
}
} else if user.Id != req.User.Id {
} else if uid != req.User.Id {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("User not found"),
@ -253,6 +250,72 @@ func getUser(opts *config.Options, req *RequestResources, _ io.Reader) Response
}
}
type passwordForm struct {
Current string
Password string
PasswordConfirm string
}
func changePassword(opts *config.Options, req *RequestResources, body io.Reader) Response {
var lf passwordForm
if err := json.NewDecoder(body).Decode(&lf); err != nil {
return APIErrorResponse{
err: err,
}
}
if !req.User.CheckAuth(lf.Current) {
return APIErrorResponse{
err: errors.New(`Invalid password.`),
status: http.StatusForbidden,
}
}
if lf.Password != lf.PasswordConfirm {
return APIErrorResponse{
err: errors.New(`The new password and its confirmation are different.`),
}
}
if err := req.User.DefinePassword(lf.Password); err != nil {
return APIErrorResponse{
err: err,
}
}
if err := storage.MainStore.UpdateUser(req.User); err != nil {
return APIErrorResponse{
err: err,
}
}
return logout(opts, req.Ps, body)
}
func deleteUser(opts *config.Options, req *RequestResources, body io.Reader) Response {
var lf passwordForm
if err := json.NewDecoder(body).Decode(&lf); err != nil {
return APIErrorResponse{
err: err,
}
}
if !req.User.CheckAuth(lf.Password) {
return APIErrorResponse{
err: errors.New(`Invalid password.`),
status: http.StatusForbidden,
}
}
if err := storage.MainStore.DeleteUser(req.User); err != nil {
return APIErrorResponse{
err: err,
}
}
return logout(opts, req.Ps, body)
}
func userHandler(f func(*config.Options, *happydns.User, io.Reader) Response) func(*config.Options, httprouter.Params, io.Reader) Response {
return func(opts *config.Options, ps httprouter.Params, body io.Reader) Response {
if uid, err := strconv.ParseInt(ps.ByName("uid"), 16, 64); err != nil {

View File

@ -62,6 +62,10 @@
DNS client
</b-dropdown-item>
<b-dropdown-divider />
<b-dropdown-item to="/me">
My account
</b-dropdown-item>
<b-dropdown-divider />
<b-dropdown-item @click="logout()">
Logout
</b-dropdown-item>

View File

@ -91,6 +91,13 @@ const routes = [
return import(/* webpackChunkName: "home" */ '../views/onboarding.vue')
}
},
{
path: '/me',
name: 'me',
component: function () {
return import(/* webpackChunkName: "me" */ '../views/me.vue')
}
},
{
path: '/domains',
name: 'domains',

236
htdocs/src/views/me.vue Normal file
View File

@ -0,0 +1,236 @@
<!--
Copyright or © or Copr. happyDNS (2020)
contact@happydns.org
This software is a computer program whose purpose is to provide a modern
interface to interact with DNS systems.
This software is governed by the CeCILL license under French law and abiding
by the rules of distribution of free software. You can use, modify and/or
redistribute the software under the terms of the CeCILL license as
circulated by CEA, CNRS and INRIA at the following URL
"http://www.cecill.info".
As a counterpart to the access to the source code and rights to copy, modify
and redistribute granted by the license, users are provided only with a
limited warranty and the software's author, the holder of the economic
rights, and the successive licensors have only limited liability.
In this respect, the user's attention is drawn to the risks associated with
loading, using, modifying and/or developing or reproducing the software by
the user in light of its specific status of free software, that may mean
that it is complicated to manipulate, and that also therefore means that it
is reserved for developers and experienced professionals having in-depth
computer knowledge. Users are therefore encouraged to load and test the
software's suitability as regards their requirements in conditions enabling
the security of their systems and/or data to be ensured and, more generally,
to use and operate it in the same conditions as regards security.
The fact that you are presently reading this means that you have had
knowledge of the CeCILL license and that you accept its terms.
-->
<template>
<b-container class="my-4">
<h2 id="password-change">
Change my password
</h2>
<b-row>
<b-card class="offset-md-2 col-8">
<b-form @submit.stop.prevent="sendChPassword">
<b-form-group
label="Enter your current password"
label-for="currentPassword-input"
>
<b-form-input
id="currentPassword-input"
v-model="signupForm.current"
type="password"
required
placeholder="xXxXxXxXxX"
autocomplete="current-password"
/>
</b-form-group>
<b-form-group
:state="passwordState"
label="Enter your new password"
label-for="password-input"
invalid-feedback="Password has to be strong enough: at least 8 characters with numbers, low case characters and high case characters."
>
<b-form-input
id="password-input"
ref="signuppassword"
v-model="signupForm.password"
type="password"
:state="passwordState"
required
placeholder="xXxXxXxXxX"
autocomplete="new-password"
/>
</b-form-group>
<b-form-group
:state="passwordConfirmState"
label="Confirm your new password"
label-for="passwordconfirm-input"
invalid-feedback="Password and its confirmation doesn't match."
>
<b-form-input
id="passwordconfirm-input"
ref="signuppasswordconfirm"
v-model="signupForm.passwordConfirm"
type="password"
:state="passwordConfirmState"
required
placeholder="xXxXxXxXxX"
/>
</b-form-group>
<div class="d-flex justify-content-around">
<b-button type="submit" variant="primary">
Change password
</b-button>
</div>
</b-form>
</b-card>
</b-row>
<hr>
<h2 id="delete-account">
Delete my account
</h2>
<b-row>
<b-card class="offset-md-2 col-8">
<p>
If you want to delete your account and all data associated with it, press the button below:
</p>
<b-button type="button" variant="danger" @click="askAccountDeletion">
Delete my account
</b-button>
<p class="mt-2 text-muted" style="line-height: 1.1">
<small>
Your domains owned on others platforms will not be affected by the deletion, they'll continue to respond with the current dataset.
</small>
</p>
</b-card>
</b-row>
<b-modal id="delete-account-modal" title="Delete Your Account" ok-variant="danger" ok-title="Delete my account" cancel-variant="primary" @ok="deleteMyAccount">
<p>
By confirming the deletion, you'll permanently and irrevocably delete your account from our database and will loose easy access to our easy management interface for your domains.
</p>
<b-form-group
label="To ensure this is really you, please enter your password:"
label-for="currentPassword-forDeletion"
>
<b-form-input
id="currentPassword-forDeletion"
v-model="deletePassword"
autocomplete="off"
autofocus
required
placeholder="xXxXxXxXxX"
style="border-color:#c92052"
type="password"
/>
</b-form-group>
<p class="text-muted" style="line-height: 1.1">
<small>
For technical reason, your account will be deleted right after your validation, but some data from your account will persist until the next database clean up.
</small>
</p>
</b-modal>
</b-container>
</template>
<script>
import axios from 'axios'
import PasswordChecks from '@/mixins/passwordChecks'
export default {
mixins: [PasswordChecks],
data () {
return {
deletePassword: '',
loggedUser: null,
signupForm: {
current: '',
password: '',
passwordConfirm: ''
}
}
},
computed: {
isLoading () {
return this.loggedUser != null
}
},
created () {
axios.get('/api/auth')
.then(
(response) => {
this.loggedUser = response.data
})
},
methods: {
askAccountDeletion () {
this.deletePassword = ''
this.$bvModal.show('delete-account-modal')
},
deleteMyAccount () {
axios
.post('/api/users/' + encodeURIComponent(this.loggedUser.id.toString(16)) + '/delete', { password: this.deletePassword })
.then(
response => {
this.$root.$bvToast.toast(
'Your account have been successfully deleted. We hope to see you back soon.', {
title: 'Account Deleted',
variant: 'primary',
toaster: 'b-toaster-content-right'
}
)
this.$router.push('/login')
},
error => {
this.$bvToast.toast(
error.response.data.errmsg, {
title: 'An error occurs when trying to delete your account',
autoHideDelay: 5000,
variant: 'danger',
toaster: 'b-toaster-content-right'
}
)
})
},
sendChPassword () {
axios
.post('/api/users/' + encodeURIComponent(this.loggedUser.id.toString(16)) + '/new_password', this.signupForm)
.then(
response => {
this.$root.$bvToast.toast(
'Your account\'s password has been changed with success.', {
title: 'Password Successfully Changed',
autoHideDelay: 5000,
variant: 'success',
toaster: 'b-toaster-content-right'
}
)
this.$router.push('/')
},
error => {
this.$bvToast.toast(
error.response.data.errmsg, {
title: 'Unable to change your password account',
autoHideDelay: 5000,
variant: 'danger',
toaster: 'b-toaster-content-right'
}
)
})
}
}
}
</script>

View File

@ -193,6 +193,20 @@ func init() {
fwd_request(w, r, opts.DevProxy)
}
})
api.Router().GET("/me", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
opts := r.Context().Value("opts").(*config.Options)
if opts.DevProxy == "" {
if data, err := Asset("htdocs/dist/index.html"); err != nil {
fmt.Fprintf(w, "{\"errmsg\":%q}", err)
} else {
w.Write(data)
}
} else {
r.URL.Path = "/"
fwd_request(w, r, opts.DevProxy)
}
})
api.Router().GET("/onboarding/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
opts := r.Context().Value("opts").(*config.Options)