Add email validation after registration
This commit is contained in:
parent
c56bd75abb
commit
fc64e8d40d
|
@ -174,7 +174,13 @@ func checkAuth(opts *config.Options, _ httprouter.Params, body io.Reader) Respon
|
|||
}
|
||||
} else if !user.CheckAuth(lf.Password) {
|
||||
return APIErrorResponse{
|
||||
err: errors.New(`Invalid username or password`),
|
||||
err: errors.New(`Invalid username or password.`),
|
||||
status: http.StatusUnauthorized,
|
||||
}
|
||||
} else if user.EmailValidated == nil {
|
||||
return APIErrorResponse{
|
||||
err: errors.New(`Please validate your e-mail address before your first login.`),
|
||||
href: "/email-validation",
|
||||
status: http.StatusUnauthorized,
|
||||
}
|
||||
} else {
|
||||
|
|
182
api/users.go
182
api/users.go
|
@ -34,17 +34,28 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"git.happydns.org/happydns/config"
|
||||
"git.happydns.org/happydns/model"
|
||||
"git.happydns.org/happydns/storage"
|
||||
"git.happydns.org/happydns/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
router.POST("/api/users", apiHandler(registerUser))
|
||||
router.PATCH("/api/users", apiHandler(resendValidationLink))
|
||||
router.GET("/api/users/:uid", apiAuthHandler(sameUserHandler(getUser)))
|
||||
router.POST("/api/users/:uid/email", apiHandler(userHandler(validateUserAddress)))
|
||||
}
|
||||
|
||||
type UploadedUser struct {
|
||||
|
@ -52,21 +63,76 @@ type UploadedUser struct {
|
|||
Password string
|
||||
}
|
||||
|
||||
func sendValidationLink(opts *config.Options, user *happydns.User) error {
|
||||
var toName string
|
||||
if n := strings.Index(user.Email, "+"); n > 0 {
|
||||
toName = user.Email[0:n]
|
||||
} else {
|
||||
toName = user.Email[0:strings.Index(user.Email, "@")]
|
||||
}
|
||||
if len(toName) > 1 {
|
||||
toNameCopy := strings.Replace(toName, ".", " ", -1)
|
||||
toName = ""
|
||||
lastRuneIsSpace := true
|
||||
for _, runeValue := range toNameCopy {
|
||||
if lastRuneIsSpace {
|
||||
lastRuneIsSpace = false
|
||||
toName += string(unicode.ToTitle(runeValue))
|
||||
} else {
|
||||
toName += string(runeValue)
|
||||
}
|
||||
|
||||
if unicode.IsSpace(runeValue) || unicode.IsPunct(runeValue) || unicode.IsSymbol(runeValue) {
|
||||
lastRuneIsSpace = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("test to", user.Email, toName)
|
||||
return utils.SendMail(
|
||||
&mail.Address{Name: toName, Address: user.Email},
|
||||
"Your new account on happyDNS",
|
||||
`Welcome to happyDNS!
|
||||
--------------------
|
||||
|
||||
Hi `+toName+`,
|
||||
|
||||
We are pleased that you created an account on our great domain name
|
||||
management platform!
|
||||
|
||||
In order to validate your account, please follow this link now:
|
||||
|
||||
[Validate my account](`+opts.GetRegistrationURL(user)+`)`,
|
||||
)
|
||||
}
|
||||
|
||||
func registerUser(opts *config.Options, p httprouter.Params, body io.Reader) Response {
|
||||
var uu UploadedUser
|
||||
err := json.NewDecoder(body).Decode(&uu)
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
err: fmt.Errorf("Something is wrong in received data: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
if len(uu.Email) <= 3 {
|
||||
if len(uu.Email) <= 3 || strings.Index(uu.Email, "@") == -1 {
|
||||
return APIErrorResponse{
|
||||
err: errors.New("The given email is invalid."),
|
||||
}
|
||||
}
|
||||
|
||||
if len(uu.Password) <= 7 {
|
||||
return APIErrorResponse{
|
||||
err: errors.New("The given email is invalid."),
|
||||
}
|
||||
}
|
||||
|
||||
if storage.MainStore.UserExists(uu.Email) {
|
||||
return APIErrorResponse{
|
||||
err: errors.New("An account already exists with the given address. Try login now."),
|
||||
}
|
||||
}
|
||||
|
||||
if user, err := happydns.NewUser(uu.Email, uu.Password); err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
|
@ -75,9 +141,121 @@ func registerUser(opts *config.Options, p httprouter.Params, body io.Reader) Res
|
|||
return APIErrorResponse{
|
||||
err: err,
|
||||
}
|
||||
} else if sendValidationLink(opts, user); err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
}
|
||||
} else {
|
||||
return APIResponse{
|
||||
response: user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resendValidationLink(opts *config.Options, p httprouter.Params, body io.Reader) Response {
|
||||
var uu UploadedUser
|
||||
err := json.NewDecoder(body).Decode(&uu)
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
err: fmt.Errorf("Something is wrong in received data: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
if user, err := storage.MainStore.GetUserByEmail(uu.Email); err != nil {
|
||||
log.Println(err)
|
||||
return APIErrorResponse{
|
||||
err: errors.New("If this address exists in our database, you'll receive a new validation link."),
|
||||
status: http.StatusOK,
|
||||
}
|
||||
} else if user.EmailValidated != nil {
|
||||
return APIErrorResponse{
|
||||
err: errors.New("If this address exists in our database, you'll receive a new validation link."),
|
||||
status: http.StatusOK,
|
||||
}
|
||||
} else if err = sendValidationLink(opts, user); err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
}
|
||||
} else {
|
||||
return APIErrorResponse{
|
||||
err: errors.New("If this address exists in our database, you'll receive a new validation link."),
|
||||
status: http.StatusOK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sameUserHandler(f func(*config.Options, *happydns.User, io.Reader) Response) func(*config.Options, *happydns.User, httprouter.Params, io.Reader) Response {
|
||||
return func(opts *config.Options, u *happydns.User, ps httprouter.Params, body io.Reader) Response {
|
||||
if uid, err := strconv.ParseInt(ps.ByName("uid"), 16, 64); err != nil {
|
||||
return APIErrorResponse{
|
||||
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 != u.Id {
|
||||
return APIErrorResponse{
|
||||
status: http.StatusNotFound,
|
||||
err: errors.New("User not found"),
|
||||
}
|
||||
} else {
|
||||
return f(opts, user, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getUser(opts *config.Options, user *happydns.User, _ io.Reader) Response {
|
||||
return APIResponse{
|
||||
response: user,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return APIErrorResponse{
|
||||
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 {
|
||||
return f(opts, user, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type UploadedAddressValidation struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func validateUserAddress(opts *config.Options, user *happydns.User, body io.Reader) Response {
|
||||
var uav UploadedAddressValidation
|
||||
err := json.NewDecoder(body).Decode(&uav)
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
err: fmt.Errorf("Something is wrong in received data: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
if err := user.ValidateEmail(uav.Key); err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
}
|
||||
} else if err := storage.MainStore.UpdateUser(user); err != nil {
|
||||
return APIErrorResponse{
|
||||
status: http.StatusNotFound,
|
||||
err: errors.New("User not found"),
|
||||
}
|
||||
} else {
|
||||
return APIResponse{
|
||||
response: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
42
config/user.go
Normal file
42
config/user.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
// 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.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.happydns.org/happydns/model"
|
||||
)
|
||||
|
||||
func (o *Options) GetRegistrationURL(u *happydns.User) string {
|
||||
return fmt.Sprintf("%s%s/email-validation?u=%x&k=%s", o.ExternalURL, o.BaseURL, u.Id, u.GenRegistrationHash(false))
|
||||
}
|
|
@ -70,6 +70,13 @@ const routes = [
|
|||
return import(/* webpackChunkName: "signup" */ '../views/signup.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/email-validation',
|
||||
name: 'email-validation',
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "signup" */ '../views/email-validation.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/domains',
|
||||
name: 'domains',
|
||||
|
|
160
htdocs/src/views/email-validation.vue
Normal file
160
htdocs/src/views/email-validation.vue
Normal file
|
@ -0,0 +1,160 @@
|
|||
<!--
|
||||
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 style="margin-top: 10vh; margin-bottom: 10vh;">
|
||||
<b-alert v-if="error !== null" variant="danger" :show="error.length > 0">
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
|
||||
<div v-if="isLoading" class="text-center">
|
||||
<b-spinner variant="primary" label="Spinning" class="mr-3" /> Please wait
|
||||
</div>
|
||||
|
||||
<b-form v-else ref="form" class="mt-2" @submit.stop.prevent="goResend">
|
||||
<p>
|
||||
In order to validate your e-mail address, please check your inbox, and follow the link contained in the message.
|
||||
</p>
|
||||
<p>
|
||||
If you need a new confirmation e-mail, just enter your address in the form below.
|
||||
</p>
|
||||
<b-form-row>
|
||||
<label for="email-input" class="col-md-4 col-form-label text-truncate text-md-right font-weight-bold">Email address</label>
|
||||
<b-col md="6">
|
||||
<b-form-input
|
||||
id="email-input"
|
||||
ref="signupemail"
|
||||
v-model="email"
|
||||
:state="emailState"
|
||||
required
|
||||
autofocus
|
||||
type="email"
|
||||
placeholder="jPostel@isi.edu"
|
||||
autocomplete="username"
|
||||
/>
|
||||
</b-col>
|
||||
</b-form-row>
|
||||
<b-form-row class="mt-3">
|
||||
<b-button class="offset-sm-4 col-sm-4" type="submit" variant="primary">
|
||||
Re-send the confirmation e-mail
|
||||
</b-button>
|
||||
</b-form-row>
|
||||
</b-form>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
email: '',
|
||||
emailState: null,
|
||||
error: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isLoading () {
|
||||
return this.error === null
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.$route.query.u) {
|
||||
axios
|
||||
.post('/api/users/' + encodeURIComponent(this.$route.query.u) + '/email', {
|
||||
key: this.$route.query.k
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.error = ''
|
||||
this.$root.$bvToast.toast(
|
||||
'Ready to login!', {
|
||||
title: 'Your new e-mail address is now validated!',
|
||||
autoHideDelay: 5000,
|
||||
variant: 'success',
|
||||
toaster: 'b-toaster-content-right'
|
||||
}
|
||||
)
|
||||
this.$router.push('/login')
|
||||
},
|
||||
(error) => {
|
||||
this.error = error.response.data.errmsg
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.error = ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
goResend () {
|
||||
const valid = this.$refs.form.checkValidity()
|
||||
this.emailState = valid
|
||||
|
||||
if (valid) {
|
||||
axios
|
||||
.patch('/api/users', {
|
||||
email: this.email
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.$root.$bvToast.toast(
|
||||
'Please check your inbox in order to valiate your e-mail address.', {
|
||||
title: 'Confirmation e-mail sent!',
|
||||
autoHideDelay: 5000,
|
||||
variant: 'success',
|
||||
toaster: 'b-toaster-content-right'
|
||||
}
|
||||
)
|
||||
this.$router.push('/email-validation')
|
||||
},
|
||||
(error) => {
|
||||
this.$bvToast.toast(
|
||||
error.response.data.errmsg, {
|
||||
title: 'Registration problem',
|
||||
autoHideDelay: 5000,
|
||||
variant: 'danger',
|
||||
toaster: 'b-toaster-content-right'
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -32,6 +32,10 @@
|
|||
package happydns
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -42,6 +46,7 @@ type User struct {
|
|||
Email string
|
||||
Password []byte
|
||||
RegistrationTime *time.Time
|
||||
EmailValidated *time.Time
|
||||
}
|
||||
|
||||
type Users []*User
|
||||
|
@ -69,3 +74,30 @@ func (u *User) DefinePassword(password string) (err error) {
|
|||
func (u *User) CheckAuth(password string) bool {
|
||||
return bcrypt.CompareHashAndPassword(u.Password, []byte(password)) == nil
|
||||
}
|
||||
|
||||
const RegistrationHashValidity = 24 * time.Hour
|
||||
|
||||
func (u *User) GenRegistrationHash(previous bool) string {
|
||||
date := time.Now()
|
||||
if previous {
|
||||
date = date.Add(RegistrationHashValidity * -1)
|
||||
}
|
||||
date = date.Truncate(RegistrationHashValidity)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(
|
||||
hmac.New(
|
||||
sha512.New,
|
||||
[]byte(u.RegistrationTime.Format(time.RFC3339Nano)),
|
||||
).Sum(date.AppendFormat([]byte{}, time.RFC3339)),
|
||||
)
|
||||
}
|
||||
|
||||
func (u *User) ValidateEmail(key string) error {
|
||||
if key == u.GenRegistrationHash(false) || key == u.GenRegistrationHash(true) {
|
||||
now := time.Now()
|
||||
u.EmailValidated = &now
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("The validation address link you follow is invalid or has expired (it is valid during %d hours)", RegistrationHashValidity/time.Hour)
|
||||
}
|
||||
|
|
14
static.go
14
static.go
|
@ -137,6 +137,20 @@ func init() {
|
|||
fwd_request(w, r, opts.DevProxy)
|
||||
}
|
||||
})
|
||||
api.Router().GET("/email-validation", 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("/join", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
opts := r.Context().Value("opts").(*config.Options)
|
||||
|
||||
|
|
|
@ -164,6 +164,7 @@ const mailHTMLTpl = `
|
|||
</html>`
|
||||
|
||||
const mailTXTTpl = `{{ .Content }}
|
||||
--
|
||||
|
||||
--
|
||||
Fred - customer support @ happyDNS
|
||||
Legal notice: https://www.happydns.org/en/legal-notice/`
|
||||
|
|
Loading…
Reference in New Issue
Block a user