Browse Source

Implement user settings

- Refactor fields verification mechanism for register and settings
  pages
- Refactor authenticator. Move all authentication functionality to
  authenticator and make it standalone(remove db dependency)
- Add user settings and possibility to modify full name and
  password
- Add stubs for check privilege
Alexey Edelev 5 years ago
parent
commit
9df463a419

+ 175 - 12
auth/authenticator.go

@@ -26,49 +26,212 @@
 package auth
 
 import (
+	"context"
+	"errors"
 	"log"
+	"time"
 
-	db "git.semlanik.org/semlanik/gostfix/db"
+	"git.semlanik.org/semlanik/gostfix/config"
 	utils "git.semlanik.org/semlanik/gostfix/utils"
 	uuid "github.com/google/uuid"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+	"golang.org/x/crypto/bcrypt"
 )
 
 type Authenticator struct {
-	storage *db.Storage
+	db               *mongo.Database
+	usersCollection  *mongo.Collection
+	tokensCollection *mongo.Collection
 }
 
-func NewAuthenticator() (a *Authenticator) {
-	storage, err := db.NewStorage()
+type Privileges int
 
+const (
+	AdminPrivilege = 1 << iota
+	SendMailPrivilege
+)
+
+func NewAuthenticator() (*Authenticator, error) {
+	fullUrl := "mongodb://"
+	if config.ConfigInstance().MongoUser != "" {
+		fullUrl += config.ConfigInstance().MongoUser
+		if config.ConfigInstance().MongoPassword != "" {
+			fullUrl += ":" + config.ConfigInstance().MongoPassword
+		}
+		fullUrl += "@"
+	}
+
+	fullUrl += config.ConfigInstance().MongoAddress
+
+	client, err := mongo.NewClient(options.Client().ApplyURI(fullUrl))
 	if err != nil {
-		log.Fatalf("Unable to intialize user storage %s", err)
-		return nil
+		return nil, err
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+	defer cancel()
+
+	err = client.Connect(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	db := client.Database("gostfix")
+	a := &Authenticator{
+		db:               db,
+		usersCollection:  db.Collection("users"),
+		tokensCollection: db.Collection("tokens"),
+	}
+	return a, nil
+}
+
+func (a *Authenticator) CheckUser(user, password string) error {
+	log.Printf("Check user: %s", user)
+	result := struct {
+		User     string
+		Password string
+	}{}
+	err := a.usersCollection.FindOne(context.Background(), bson.M{"user": user}).Decode(&result)
+	if err != nil {
+		return errors.New("Invalid user or password")
+	}
+
+	if bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(password)) != nil {
+		return errors.New("Invalid user or password")
+	}
+	return nil
+}
+
+func (a *Authenticator) addToken(user, token string) error {
+	log.Printf("Add token: %s\n", user)
+	a.tokensCollection.UpdateOne(context.Background(),
+		bson.M{"user": user},
+		bson.M{
+			"$addToSet": bson.M{
+				"token": bson.M{
+					"token":  token,
+					"expire": time.Now().Add(time.Hour * 24).Unix(),
+				},
+			},
+		},
+		options.Update().SetUpsert(true))
+	a.cleanupTokens(user)
+	return nil
+}
+
+func (a *Authenticator) cleanupTokens(user string) {
+	log.Printf("Cleanup tokens: %s\n", user)
+
+	cur, err := a.tokensCollection.Aggregate(context.Background(),
+		bson.A{
+			bson.M{"$match": bson.M{"user": user}},
+			bson.M{"$unwind": "$token"},
+		})
+
+	if err != nil {
+		log.Fatalln(err)
+	}
+
+	type tokenMetadata struct {
+		Expire int64
+		Token  string
 	}
 
-	a = &Authenticator{
-		storage: storage,
+	tokensToKeep := bson.A{}
+	defer cur.Close(context.Background())
+	for cur.Next(context.Background()) {
+		result := struct {
+			Token *tokenMetadata
+		}{
+			Token: &tokenMetadata{},
+		}
+
+		err = cur.Decode(&result)
+		if err == nil && result.Token.Expire >= time.Now().Unix() {
+			tokensToKeep = append(tokensToKeep, result.Token)
+		} else {
+			log.Printf("Expired token found for %s : %d", user, result.Token.Expire)
+		}
 	}
+
+	_, err = a.tokensCollection.UpdateOne(context.Background(), bson.M{"user": user}, bson.M{"$set": bson.M{"token": tokensToKeep}})
 	return
 }
 
-func (a *Authenticator) Authenticate(user, password string) (string, bool) {
+func (a *Authenticator) Login(user, password string) (string, bool) {
 	if !utils.RegExpUtilsInstance().EmailChecker.MatchString(user) {
 		return "", false
 	}
 
-	if a.storage.CheckUser(user, password) != nil {
+	if a.CheckUser(user, password) != nil {
 		return "", false
 	}
 
 	token := uuid.New().String()
-	a.storage.AddToken(user, token)
+	a.addToken(user, token)
 	return token, true
 }
 
+func (a *Authenticator) Logout(user, token string) error {
+	a.cleanupTokens(user)
+
+	_, err := a.tokensCollection.UpdateOne(context.Background(), bson.M{"user": user}, bson.M{"$pull": bson.M{"token": bson.M{"token": token}}})
+	if err != nil {
+		log.Printf("Unable to remove token %s", err)
+	}
+
+	return err
+}
+
+func (a *Authenticator) checkToken(user, token string) error {
+	if token == "" {
+		return errors.New("Invalid token")
+	}
+
+	cur, err := a.tokensCollection.Aggregate(context.Background(),
+		bson.A{
+			bson.M{"$match": bson.M{"user": user}},
+			bson.M{"$unwind": "$token"},
+			bson.M{"$match": bson.M{"token.token": token}},
+		})
+
+	if err != nil {
+		log.Fatalln(err)
+		return err
+	}
+
+	ok := false
+	defer cur.Close(context.Background())
+	if cur.Next(context.Background()) {
+		result := struct {
+			Token struct {
+				Expire int64
+			}
+		}{}
+
+		err = cur.Decode(&result)
+
+		ok = err == nil && result.Token.Expire >= time.Now().Unix()
+	}
+
+	if ok {
+		//TODO: Renew token
+		return nil
+	}
+
+	return errors.New("Token expired")
+}
+
 func (a *Authenticator) Verify(user, token string) bool {
 	if !utils.RegExpUtilsInstance().EmailChecker.MatchString(user) {
 		return false
 	}
 
-	return a.storage.CheckToken(user, token) == nil
+	return a.checkToken(user, token) == nil
+}
+
+func (a *Authenticator) CheckPrivileges(user string, privilege Privileges) {
+
 }

+ 26 - 122
db/db.go

@@ -137,6 +137,32 @@ func (s *Storage) AddUser(user, password, fullName string) error {
 	return nil
 }
 
+func (s *Storage) UpdateUser(user, password, fullName string) error {
+	userInfo := bson.M{}
+
+	if len(password) > 0 && len(password) < 128 {
+		hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+		if err != nil {
+			return err
+		}
+		hashString := string(hash)
+		userInfo["password"] = hashString
+	}
+
+	if len(fullName) > 0 && len(fullName) < 128 && utils.RegExpUtilsInstance().FullNameChecker.MatchString(fullName) {
+		userInfo["fullName"] = fullName
+	}
+
+	if len(userInfo) > 0 {
+		_, err := s.usersCollection.UpdateOne(context.Background(), bson.M{"user": user}, bson.M{"$set": userInfo})
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (s *Storage) AddEmail(user string, email string) error {
 	return s.addEmail(user, email, false)
 }
@@ -217,128 +243,6 @@ func (s *Storage) RemoveEmail(user string, email string) error {
 	return err
 }
 
-func (s *Storage) CheckUser(user, password string) error {
-	log.Printf("Check user: %s %s", user, password)
-	result := struct {
-		User     string
-		Password string
-	}{}
-	err := s.usersCollection.FindOne(context.Background(), bson.M{"user": user}).Decode(&result)
-	if err != nil {
-		return errors.New("Invalid user or password")
-	}
-
-	if bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(password)) != nil {
-		return errors.New("Invalid user or password")
-	}
-	return nil
-}
-
-func (s *Storage) AddToken(user, token string) error {
-	log.Printf("Add token: %s\n", user)
-	s.tokensCollection.UpdateOne(context.Background(),
-		bson.M{"user": user},
-		bson.M{
-			"$addToSet": bson.M{
-				"token": bson.M{
-					"token":  token,
-					"expire": time.Now().Add(time.Hour * 24).Unix(),
-				},
-			},
-		},
-		options.Update().SetUpsert(true))
-	s.CleanupTokens(user)
-	return nil
-}
-
-func (s *Storage) CheckToken(user, token string) error {
-	if token == "" {
-		return errors.New("Invalid token")
-	}
-
-	cur, err := s.tokensCollection.Aggregate(context.Background(),
-		bson.A{
-			bson.M{"$match": bson.M{"user": user}},
-			bson.M{"$unwind": "$token"},
-			bson.M{"$match": bson.M{"token.token": token}},
-		})
-
-	if err != nil {
-		log.Fatalln(err)
-		return err
-	}
-
-	ok := false
-	defer cur.Close(context.Background())
-	if cur.Next(context.Background()) {
-		result := struct {
-			Token struct {
-				Expire int64
-			}
-		}{}
-
-		err = cur.Decode(&result)
-
-		ok = err == nil && result.Token.Expire >= time.Now().Unix()
-	}
-
-	if ok {
-		//TODO: Renew token
-		return nil
-	}
-
-	return errors.New("Token expired")
-}
-
-func (s *Storage) RemoveToken(user, token string) error {
-	s.CleanupTokens(user)
-
-	_, err := s.tokensCollection.UpdateOne(context.Background(), bson.M{"user": user}, bson.M{"$pull": bson.M{"token": bson.M{"token": token}}})
-	if err != nil {
-		log.Printf("Unable to remove token %s", err)
-	}
-
-	return err
-}
-
-func (s *Storage) CleanupTokens(user string) {
-	log.Printf("Cleanup tokens: %s\n", user)
-
-	cur, err := s.tokensCollection.Aggregate(context.Background(),
-		bson.A{
-			bson.M{"$match": bson.M{"user": user}},
-			bson.M{"$unwind": "$token"},
-		})
-
-	if err != nil {
-		log.Fatalln(err)
-	}
-
-	type tokenMetadata struct {
-		Expire int64
-		Token  string
-	}
-
-	tokensToKeep := bson.A{}
-	defer cur.Close(context.Background())
-	for cur.Next(context.Background()) {
-		result := struct {
-			Token *tokenMetadata
-		}{
-			Token: &tokenMetadata{},
-		}
-
-		err = cur.Decode(&result)
-		if err == nil && result.Token.Expire >= time.Now().Unix() {
-			tokensToKeep = append(tokensToKeep, result.Token)
-		} else {
-			log.Printf("Expired token found for %s : %d", user, result.Token.Expire)
-		}
-	}
-
-	_, err = s.tokensCollection.UpdateOne(context.Background(), bson.M{"user": user}, bson.M{"$set": bson.M{"token": tokensToKeep}})
-	return
-}
 func (s *Storage) SaveMail(email, folder string, m *common.Mail, read bool) error {
 	result := &struct {
 		User string

+ 7 - 1
main.go

@@ -26,6 +26,8 @@
 package main
 
 import (
+	"log"
+
 	sasl "git.semlanik.org/semlanik/gostfix/sasl"
 	scanner "git.semlanik.org/semlanik/gostfix/scanner"
 	web "git.semlanik.org/semlanik/gostfix/web"
@@ -40,10 +42,14 @@ type GofixEngine struct {
 
 func NewGofixEngine() (e *GofixEngine) {
 	mailScanner := scanner.NewMailScanner()
+	saslService, err := sasl.NewSaslServer()
+	if err != nil {
+		log.Fatalf("Unable to intialize sasl server %s\n", err)
+	}
 	e = &GofixEngine{
 		scanner: mailScanner,
 		web:     web.NewServer(mailScanner),
-		sasl:    sasl.NewSaslServer(),
+		sasl:    saslService,
 	}
 
 	e.scanner.RegisterNotifier(e.web.Notifier)

+ 8 - 4
sasl/sasl.go

@@ -68,12 +68,16 @@ const (
 	ContinueStateCredentials
 )
 
-func NewSaslServer() *SaslServer {
+func NewSaslServer() (*SaslServer, error) {
+	authenticator, err := auth.NewAuthenticator()
+	if err != nil {
+		return nil, err
+	}
 	return &SaslServer{
 		pid:           os.Getpid(),
 		cuid:          0,
-		authenticator: auth.NewAuthenticator(),
-	}
+		authenticator: authenticator,
+	}, nil
 }
 
 func (s *SaslServer) Run() {
@@ -202,7 +206,7 @@ func (s *SaslServer) checkCredentials(credentialsBase64 string) (string, error)
 			return login, nil
 		}
 	} else {
-		if _, ok := s.authenticator.Authenticate(login, password); ok {
+		if err := s.authenticator.CheckUser(login, password); err == nil {
 			return login, nil
 		}
 	}

+ 4 - 4
utils/regexp.go

@@ -39,7 +39,7 @@ const (
 	BoundaryEndRegExp   = "^--(.*)--$"
 	BoundaryRegExp      = "boundary=\"(.*)\""
 	MailboxRegExp       = "^/m(\\d+)/?(.*)"
-	FullnameRegExp      = "^[\\w ]*$"
+	FullNameRegExp      = "^[\\w]+[\\w ]*$"
 )
 
 const (
@@ -73,7 +73,7 @@ type regExpUtils struct {
 	BoundaryEndFinder   *regexp.Regexp
 	BoundaryFinder      *regexp.Regexp
 	MailboxFinder       *regexp.Regexp
-	FullnameChecker     *regexp.Regexp
+	FullNameChecker     *regexp.Regexp
 }
 
 func newRegExpUtils() (*regExpUtils, error) {
@@ -131,7 +131,7 @@ func newRegExpUtils() (*regExpUtils, error) {
 		return nil, err
 	}
 
-	fullnameChecker, err := regexp.Compile(FullnameRegExp)
+	fullNameChecker, err := regexp.Compile(FullNameRegExp)
 	if err != nil {
 		log.Fatalf("Invalid regexp %s\n", err)
 		return nil, err
@@ -147,7 +147,7 @@ func newRegExpUtils() (*regExpUtils, error) {
 		BoundaryFinder:      boundaryFinder,
 		DomainChecker:       domainChecker,
 		MailboxFinder:       mailboxFinder,
-		FullnameChecker:     fullnameChecker,
+		FullNameChecker:     fullNameChecker,
 	}
 
 	return ru, nil

+ 11 - 0
web/css/controls.css

@@ -334,3 +334,14 @@
     user-select: none;
 }
 
+.settingsHeader {
+    padding: var(--base-text-padding);
+    font-size: var( --huge-text-size);
+
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}

+ 4 - 4
web/css/index.css

@@ -42,14 +42,14 @@ html, body {
     max-width: 150px;
 }
 
-#contentBox {
+.contentBox {
     display: flex;
     flex-direction: row;
     width: 100%;
     height: 100%;
 }
 
-#foldersBox {
+.leftPanel {
     display: block;
     width: 220px;
     max-width: 220px;
@@ -61,7 +61,7 @@ html, body {
     margin-right: 15px;
 }
 
-#mailInnerBox {
+.innerConentBox {
     position: relative;
     max-width: 100%;
     max-height: 100%;
@@ -102,7 +102,7 @@ html, body {
     overflow: hidden;
     display: none;
     background-color: var(--bg-color);
-    padding: 5px;
+    padding: var(--base-border-padding);
 }
 
 .mailHeader {

+ 103 - 0
web/js/forms.js

@@ -0,0 +1,103 @@
+const passwordRegex = /[A-Z0-6!"\#$%&'()*+,\-./:;<=>?@\[\\\]^_‘{|}~]/
+const fullNameRegex = /^[\w]+[\w ]*$/
+
+function initControls() {
+    $('.inpt, .password').find('.icon').mousedown(function(e) {
+        if ($(e.target).parent().hasClass('password')) {
+            $(e.target).parent().find('input').attr('type', 'text')
+        }
+    })
+
+    $('.inpt, .password').find('.icon').mouseup(function(e) {
+        if ($(e.target).parent().hasClass('password')) {
+            $(e.target).parent().find('input').attr('type', 'password')
+        }
+    })
+}
+
+function addValidation(field, form, func) {
+    func(field)
+    $(field).on('input', function(e) {func(e.target, form)})
+}
+
+function validateForm(form) {
+    if (form == null) {
+        return false
+    }
+
+    if ($(form).find('.inpt').hasClass('bad')) {
+        $(form).find('.btn').addClass('disabled')
+        return false
+    }
+    $(form).find('.btn').removeClass('disabled')
+    return true
+}
+
+function validatePassword(name, form) {
+    var element = $(name)
+    var fieldDiv = element.parent()
+    if (element.val() != '') {
+        fieldDiv.removeClass('bad')
+        if (element.val().length < 8 || !passwordRegex.test(element.val())) {
+            fieldDiv.addClass("weak")
+        } else {
+            fieldDiv.removeClass("weak")
+        }
+    } else {
+        fieldDiv.removeClass('weak')
+        fieldDiv.addClass('bad')
+    }
+    validateForm(form)
+}
+
+function validateFullName(name, form) {
+    var element = $(name)
+    var fieldDiv = element.parent()
+    if (fullNameRegex.test(element.val())) {
+        fieldDiv.removeClass("bad")
+    } else {
+        fieldDiv.addClass("bad")
+    }
+    validateForm(form)
+}
+
+function validateField(element, form) {
+    var fieldDiv = element.parent()
+    if (element.val() != '') {
+        fieldDiv.removeClass('bad')
+    } else {
+        fieldDiv.addClass('bad')
+    }
+    validateForm(form)
+}
+
+//Email validation
+const emailRegex = /^[a-zA-Z]+[\w\d\._-]*$/
+var emailInputTimer = null
+function validateEmail(name, form) {
+    var element = $(name)
+    var fieldDiv = element.parent()
+    fieldDiv.addClass("bad")
+    if (emailRegex.test(element.val())) {
+        clearTimeout(emailInputTimer)
+        emailInputTimer = setTimeout(function(){
+                $.ajax({
+                url: "/checkEmail",
+                data: {
+                    user: element.val()
+                },
+                success: function(result) {
+                    fieldDiv.removeClass("bad")
+                    validateForm(form)
+                },
+                error: function(jqXHR, textStatus, errorThrown) {
+                    fieldDiv.addClass("bad")
+                    validateForm(form)
+                }
+            })
+        }, 200)
+        return
+    } else {
+        validateForm(form)
+    }
+}

+ 4 - 0
web/js/index.js

@@ -493,6 +493,10 @@ function logout() {
     window.location.href = "/logout"
 }
 
+function settings() {
+    window.location.href = "/settings"
+}
+
 function connectNotifier() {
     if (notifierSocket != null) {
         return

+ 92 - 0
web/securezone.go

@@ -0,0 +1,92 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of gostfix project https://git.semlanik.org/semlanik/gostfix
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+package web
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+
+	"git.semlanik.org/semlanik/gostfix/common"
+)
+
+func (s *Server) handleSecureZone(w http.ResponseWriter, r *http.Request) {
+	user, token := s.extractAuth(w, r)
+	if !s.authenticator.Verify(user, token) {
+		s.error(http.StatusUnauthorized, "You are not allowed to access this function", w)
+		return
+	}
+
+	switch r.URL.Path {
+	case "/settings":
+		s.handleSettings(w, user)
+	case "/update":
+		s.handleUpdate(w, r, user)
+		// case "/admin":
+		// case "/addUser":
+		// 	//TODO:
+		// case "/removeUser":
+		// 	//TODO:
+		// case "/changeUser":
+		// 	//TODO:
+	}
+}
+
+func (s *Server) handleSettings(w http.ResponseWriter, user string) {
+	info, err := s.storage.GetUserInfo(user)
+	if err != nil {
+		s.error(http.StatusInternalServerError, "Unable to obtain user information", w)
+		return
+	}
+	fmt.Fprintf(w, s.templater.ExecuteSettings(&struct {
+		Version  string
+		FullName string
+	}{common.Version, info.FullName}))
+}
+
+func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request, user string) {
+	if err := r.ParseForm(); err != nil {
+		s.error(http.StatusUnauthorized, "Password entered is invalid", w)
+		return
+	}
+
+	oldPassword := r.FormValue("oldPassword")
+	if err := s.authenticator.CheckUser(user, oldPassword); err != nil {
+		s.error(http.StatusUnauthorized, "Password entered is invalid", w)
+		return
+	}
+
+	password := r.FormValue("password")
+	fullName := r.FormValue("fullName")
+
+	err := s.storage.UpdateUser(user, password, fullName)
+	if err != nil {
+		log.Println(err.Error())
+		s.error(http.StatusInternalServerError, "Unable to update user data", w)
+		return
+	}
+	w.Write([]byte{0})
+}

+ 23 - 10
web/server.go

@@ -78,8 +78,13 @@ func NewServer(scanner common.Scanner) *Server {
 		return nil
 	}
 
+	authenticator, err := auth.NewAuthenticator()
+	if err != nil {
+		log.Fatalf("Unable to intialize authenticator %s", err)
+		return nil
+	}
 	s := &Server{
-		authenticator: auth.NewAuthenticator(),
+		authenticator: authenticator,
 		templater:     NewTemplater("data/templates"),
 		fileServer:    http.FileServer(http.Dir("data")),
 		sessionStore:  sessions.NewCookieStore(make([]byte, 32)),
@@ -139,6 +144,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			fallthrough
 		case "/delete":
 			s.handleMailRequest(w, r)
+		case "/settings":
+			fallthrough
+		case "/update":
+			fallthrough
+		case "/admin":
+			s.handleSecureZone(w, r)
 		default:
 			http.Redirect(w, r, "/m0", http.StatusTemporaryRedirect)
 		}
@@ -146,6 +157,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 }
 
 func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
+	// if session, err := s.sessionStore.Get(r, CookieSessionToken); err == nil && session.Values["user"] != nil && session.Values["token"] != nil {
+	// 	http.Redirect(w, r, "/m0", http.StatusTemporaryRedirect)
+	// 	return
+	// }
 	if !config.ConfigInstance().RegistrationEnabled {
 		s.error(http.StatusNotImplemented, "Registration is disabled on this server", w)
 		return
@@ -154,11 +169,11 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
 	if err := r.ParseForm(); err == nil {
 		user := r.FormValue("user")
 		password := r.FormValue("password")
-		fullname := r.FormValue("fullname")
-		if user != "" && password != "" && fullname != "" {
+		fullName := r.FormValue("fullName")
+		if user != "" && password != "" && fullName != "" {
 			ok, email := s.checkEmail(user)
-			if ok && len(password) < 128 && len(fullname) < 128 && utils.RegExpUtilsInstance().FullnameChecker.MatchString(fullname) {
-				err := s.storage.AddUser(email, password, fullname)
+			if ok && len(password) < 128 && len(fullName) < 128 && utils.RegExpUtilsInstance().FullNameChecker.MatchString(fullName) {
+				err := s.storage.AddUser(email, password, fullName)
 				if err != nil {
 					log.Println(err.Error())
 					s.error(http.StatusInternalServerError, "Unable to create user", w)
@@ -166,7 +181,7 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
 				}
 
 				s.scanner.Reconfigure()
-				token, _ := s.authenticator.Authenticate(email, password)
+				token, _ := s.authenticator.Login(email, password)
 				s.login(email, token, w, r)
 				return
 			}
@@ -184,7 +199,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
 	if err := r.ParseForm(); err == nil {
 		user := r.FormValue("user")
 		password := r.FormValue("password")
-		token, ok := s.authenticator.Authenticate(user, password)
+		token, ok := s.authenticator.Login(user, password)
 		if ok {
 			s.login(user, token, w, r)
 			return
@@ -236,12 +251,10 @@ func (s *Server) checkEmail(user string) (bool, string) {
 }
 
 func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
-	fmt.Println("logout")
-
 	session, err := s.sessionStore.Get(r, CookieSessionToken)
 	if err == nil {
 		if session.Values["user"] != nil && session.Values["token"] != nil {
-			s.storage.RemoveToken(session.Values["user"].(string), session.Values["token"].(string))
+			s.authenticator.Logout(session.Values["user"].(string), session.Values["token"].(string))
 		}
 		session.Values["user"] = ""
 		session.Values["token"] = ""

+ 12 - 0
web/templater.go

@@ -44,6 +44,7 @@ const (
 	MailTemplateName       = "mailTemplate.eml"
 	SignupTemplateName     = "signup.html"
 	RegisterTemplateName   = "register.html"
+	SettingsTemplateName   = "settings.html"
 )
 
 type Templater struct {
@@ -58,6 +59,7 @@ type Templater struct {
 	foldersTemaplate   *template.Template
 	mailNewTemplate    *template.Template
 	mailTemplate       *template.Template
+	settingsTemplate   *template.Template
 }
 
 func NewTemplater(templatesPath string) (t *Templater) {
@@ -117,6 +119,11 @@ func NewTemplater(templatesPath string) (t *Templater) {
 		log.Fatal(err)
 	}
 
+	settings, err := parseTemplate(templatesPath + "/" + SettingsTemplateName)
+	if err != nil {
+		log.Fatal(err)
+	}
+
 	t = &Templater{
 		indexTemplate:      index,
 		mailListTemplate:   maillist,
@@ -129,6 +136,7 @@ func NewTemplater(templatesPath string) (t *Templater) {
 		mailTemplate:       mail,
 		signupTemplate:     signup,
 		registerTemplate:   register,
+		settingsTemplate:   settings,
 	}
 	return
 }
@@ -186,6 +194,10 @@ func (t *Templater) ExecuteMail(data interface{}) string {
 	return executeTemplateCommon(t.mailTemplate, data)
 }
 
+func (t *Templater) ExecuteSettings(data interface{}) string {
+	return executeTemplateCommon(t.settingsTemplate, data)
+}
+
 func executeTemplateCommon(t *template.Template, values interface{}) string {
 	buffer := &bytes.Buffer{}
 	err := t.Execute(buffer, values)

+ 3 - 3
web/templates/index.html

@@ -26,13 +26,13 @@
                 </div>
             </div>
             <div class="horizontalPaddingBox">
-                <div id="contentBox">
-                    <div id="foldersBox">
+                <div class="contentBox">
+                    <div class="leftPanel">
                         <div id="mailNewButton" class="btn materialLevel1" style="margin-bottom: 15px;">New email</div>
                         <div id="folders"></div>
                     </div>
                     <div class="verticalPaddingBox">
-                        <div id="mailInnerBox" class="materialLevel1">
+                        <div class="innerConentBox materialLevel1">
                             <div id="mailList"></div>
                             <div id="mailDetails"></div>
                             <div id="mailNew">{{.MailNew}}</div>

+ 1 - 1
web/templates/mailnew.html

@@ -17,7 +17,7 @@
             <img class="iconBtn" style="width: 20px; height: 20px; margin-left:10px;" onclick="closeDetails();" src="/assets/back.svg"/>
         </div>
     </div>
-    <div class="horizontalPaddingBox">
+    <div style="display: block; height: 100%;">
         <input type="hidden" id="newMailTo" name="to">
         <textarea id="newMailEditor" name="body" class="contentArea" style="height: 100%; width: 100%; resize: none;"></textarea>
     </div>

+ 12 - 102
web/templates/register.html

@@ -8,112 +8,22 @@
         <link type="text/css" href="/css/styles.css" rel="stylesheet">
         <link type="text/css" href="/css/controls.css" rel="stylesheet">
         <script src="/js/jquery-3.4.1.min.js"></script>
+        <script src="/js/forms.js"></script>
         <script>
-            const emailRegex = /^[a-zA-Z]+[\w\d\._-]*$/
-            const passwordRegex = /[A-Z0-6!"\#$%&'()*+,\-./:;<=>?@\[\\\]^_‘{|}~]/
-            const fullnameRegex = /^[\w ]*$/
-            var emailOk = false
-
             $(document).ready(function() {
-                $('#showPasswordButton').mousedown(function() {
-                    $('#passwordField').attr('type', 'text')
-                })
-
-                $('#showPasswordButton').mouseup(function() {
-                    $('#passwordField').attr('type', 'password')
-                })
+                initControls()
 
-                validateEmail($('#userField'))
-                $('#fullnameField').on('input', validateFields)
-                $('#userField').on('input', function(e) { validateEmail($(e.target)) })
-                $('#passwordField').on('input', validateFields)
+                addValidation('#userField', '#registerForm', validateEmail)
+                addValidation('#fullNameField', '#registerForm', validateFullName)
+                addValidation('#passwordField', '#registerForm', validatePassword)
+                validateForm('#registerForm')
             })
 
-            function submitChanges() {
-                if (emailOk && validatePassword($('#passwordField')) && validateFullname($('#fullnameField'))) {
+            function register() {
+                if (validateForm('#registerForm')) {
                     $('#registerForm').submit()
                 }
             }
-
-            var emailInputTimer = null
-            function validateEmail(element) {
-                emailOk = false
-                validateFields()
-                var fieldDiv = element.parent()
-                if (emailRegex.test(element.val())) {
-                    clearTimeout(emailInputTimer)
-                    emailInputTimer = setTimeout(emailCheck, 200)
-                    return
-                } else {
-                    fieldDiv.addClass("bad")
-                }
-            }
-
-            function emailCheck() {
-                var element = $('#userField')
-                var fieldDiv = element.parent()
-
-                $.ajax({
-                    url: "/checkEmail",
-                    data: {
-                        user: element.val()
-                    },
-                    success: function(result) {
-                        emailOk = true
-                        validateFields()
-                        fieldDiv.removeClass("bad")
-                    },
-                    error: function(jqXHR, textStatus, errorThrown) {
-                        emailOk = false
-                        validateFields()
-                        fieldDiv.addClass("bad")
-                    }
-                })
-            }
-
-            function validatePassword(element) {
-                var fieldDiv = element.parent()
-                if (validateField(element)) {
-                    if (element.val().length < 8 || !passwordRegex.test(element.val())) {
-                        fieldDiv.addClass("weak")
-                    } else {
-                        fieldDiv.removeClass("weak")
-                    }
-                    return true
-                }
-
-                return false
-            }
-
-            function validateFullname(element) {
-                var fieldDiv = element.parent()
-                if (fullnameRegex.test(element.val())) {
-                    fieldDiv.removeClass("bad")
-                    return true
-                }
-                fieldDiv.addClass("bad")
-                return false
-            }
-
-            function validateField(element) {
-                var fieldDiv = element.parent()
-                if (element.val() != '') {
-                    fieldDiv.removeClass('bad')
-                    return true
-                }
-                fieldDiv.addClass('bad')
-                return false
-            }
-
-            function validateFields() {
-                var passwordOk = validatePassword($('#passwordField'))
-                var fullnameOk = validateFullname($('#fullnameField'))
-                if (emailOk && passwordOk && fullnameOk) {
-                    $('#registerButton').removeClass('disabled')
-                } else {
-                    $('#registerButton').addClass('disabled')
-                }
-            }
         </script>
         <title>Gostfix mail {{.Version}}</title>
     </head>
@@ -123,7 +33,7 @@
                 <div style="display: flex; flex-direction: column; width: 100%; height: 100%; justify-content: center;">
                     <form id="registerForm" method="POST" action="/register" style="margin: 0 auto;">
                         <div class="inpt bad">
-                            <input id="fullnameField" name="fullname" type="text" required maxlength="128" autocomplete="off">
+                            <input id="fullNameField" name="fullName" type="text" required maxlength="128" autocomplete="off">
                             <span class="highlight"></span>
                             <span class="bar"></span>
                             <label>Full name</label>
@@ -134,14 +44,14 @@
                             <span class="bar"></span>
                             <label>User @{{.Domain}}</label>
                         </div>
-                        <div class="inpt bad">
+                        <div class="inpt password bad">
                             <input id="passwordField" name="password" type="password" required maxlength="128" autocomplete="off">
                             <span class="highlight"></span>
                             <span class="bar"></span>
                             <label>Password</label>
-                            <img id="showPasswordButton" src="/assets/eye.svg"/>
+                            <img class="icon" src="/assets/eye.svg"/>
                         </div>
-                        <div id="registerButton" class="btn materialLevel1 disabled" style="margin-bottom: 15px;" onclick="submitChanges();">Register</div>
+                        <div id="registerButton" class="btn materialLevel1 disabled" style="margin-bottom: 15px;" onclick="register();">Register</div>
                     </form>
                 </div>
             </div>

+ 84 - 0
web/templates/settings.html

@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8"/>
+        <link rel="icon" href="/assets/logo.png">
+        <link href="https://fonts.googleapis.com/css?family=Titillium+Web&display=swap" rel="stylesheet">
+        <link type="text/css" href="/css/index.css" rel="stylesheet">
+        <link type="text/css" href="/css/styles.css" rel="stylesheet">
+        <link type="text/css" href="/css/controls.css" rel="stylesheet">
+        <script src="/js/jquery-3.4.1.min.js"></script>
+        <script src="/js/forms.js"></script>
+        <script>
+            $(document).ready(function() {
+                initControls()
+
+                addValidation('#fullNameField', null, validateFullName)
+                addValidation('#passwordField', null, validatePassword)
+            })
+
+            function update() {
+                var formValue = $('#updateForm').serialize()
+                $.ajax({
+                    url: "/update",
+                    data: formValue,
+                    success: function(result) {
+                        console.log("update ok")
+                    },
+                    error: function(jqXHR, textStatus, errorThrown) {
+                        console.log("Update failed: " + textStatus)
+                    }
+                })
+            }
+
+            function back() {
+                window.history.back();
+            }
+        </script>
+        <title>Gostfix mail {{.Version}}</title>
+    </head>
+    <body>
+        <div id="main">
+            <div class="horizontalPaddingBox">
+                <div class="contentBox">
+                    <div class="leftPanel">
+                        <div class="folderBtn" onclick="back();">Back</div>
+                        <div class="folderBtn">User settings</div>
+                    </div>
+                    <div class="verticalPaddingBox">
+                        <div class="innerConentBox materialLevel1">
+                            <div style="flex: 1 1 auto; display: flex; flex-direction: column;">
+                                <div class="settingsHeader">
+                                    User settings
+                                </div>
+                                <form id="updateForm" style="margin: 0 auto;">
+                                    <div class="inpt bad">
+                                        <input id="fullNameField" name="fullName" type="text" required maxlength="128" autocomplete="off" value="{{.FullName}}">
+                                        <span class="highlight"></span>
+                                        <span class="bar"></span>
+                                        <label>Full name</label>
+                                    </div>
+                                    <div class="inpt password bad">
+                                        <input id="passwordField" name="password" type="password" required maxlength="128" autocomplete="off">
+                                        <span class="highlight"></span>
+                                        <span class="bar"></span>
+                                        <label>New password</label>
+                                        <img class="icon" src="/assets/eye.svg"/>
+                                    </div>
+                                    <div class="inpt">
+                                        <input id="oldPasswordField" name="oldPassword" type="password" required maxlength="128" autocomplete="off">
+                                        <span class="highlight"></span>
+                                        <span class="bar"></span>
+                                        <label>Old password</label>
+                                    </div>
+                                    <div id="updateButton" class="btn materialLevel1" style="margin-bottom: 30px;" onclick="update();">Update</div>
+                                </form>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/>gostfix {{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
+        </div>
+    </body>
+</html>

+ 2 - 1
web/templates/statusline.html

@@ -4,7 +4,8 @@
         <div style="margin-top: auto; margin-bottom: auto; margin-left: 10px;" class="noselect">
             {{.Name}} {{.Email}}
         </div>
-        <img class="iconBtn" style="width: 30px; margin-left: 20px;" onclick="logout(); event.stopPropagation(); return false;" src="/assets/logout.svg"/>
+        <img class="iconBtn" style="width: 30px; margin-left: 20px;" onclick="settings(); event.stopPropagation(); return false;" src="/assets/settings.svg"/>
+        <img class="iconBtn" style="width: 30px; margin-left: 10px;" onclick="logout(); event.stopPropagation(); return false;" src="/assets/logout.svg"/>
     </div>
     <div id="emailSelector" class="dropdown-content">
         {{range .EmailsIndexes}}