فهرست منبع

Add service watcher and admin panel

Alexey Edelev 2 سال پیش
والد
کامیت
9bfee89ced
21فایلهای تغییر یافته به همراه440 افزوده شده و 116 حذف شده
  1. 3 1
      .gitignore
  2. 23 9
      auth/authenticator.go
  3. 6 2
      build.sh
  4. 2 2
      common/gostfix.proto
  5. 1 2
      go.mod
  6. 0 4
      go.sum
  7. 56 17
      main.go
  8. 1 24
      sasl/sasl.go
  9. 66 0
      sasl/service.go
  10. 6 48
      scanner/mailscanner.go
  11. 91 0
      scanner/service.go
  12. 37 0
      service/service.go
  13. 16 0
      service/service.proto
  14. 1 1
      setup/setup.go
  15. 1 0
      utils/regexp.go
  16. 18 0
      web/js/admin.js
  17. 11 0
      web/securezone.go
  18. 11 6
      web/server.go
  19. 45 0
      web/service.go
  20. 1 0
      web/templater.go
  21. 44 0
      web/templates/admin.html

+ 3 - 1
.gitignore

@@ -25,5 +25,7 @@ _testmain.go
 *.prof
 
 data/
+bin/
 
-common/gostfix.pb.go
+common/gostfix.pb.go
+service/service.pb.go

+ 23 - 9
auth/authenticator.go

@@ -46,8 +46,6 @@ type Authenticator struct {
 	tokensCollection *mongo.Collection
 }
 
-type Privileges int
-
 const (
 	AdminPrivilege = 1 << iota
 	SendMailPrivilege
@@ -105,7 +103,7 @@ func (a *Authenticator) CheckUser(user, password string) error {
 }
 
 func (a *Authenticator) addToken(user, token string) error {
-	log.Printf("Add token: %s\n", user)
+	log.Printf("Add token: %s", user)
 	a.tokensCollection.UpdateOne(context.Background(),
 		bson.M{"user": user},
 		bson.M{
@@ -122,7 +120,11 @@ func (a *Authenticator) addToken(user, token string) error {
 }
 
 func (a *Authenticator) cleanupTokens(user string) {
-	log.Printf("Cleanup tokens: %s\n", user)
+	if len(user) == 0 {
+		return
+	}
+
+	log.Printf("Cleanup tokens: %s", user)
 
 	cur, err := a.tokensCollection.Aggregate(context.Background(),
 		bson.A{
@@ -210,9 +212,9 @@ func (a *Authenticator) checkToken(user, token string) error {
 				Expire int64
 			}
 		}{}
-		
+
 		err = cur.Decode(&result)
-		
+
 		ok = err == nil && (config.ConfigInstance().WebSessionExpireTime <= 0 || result.Token.Expire >= time.Now().Unix())
 	}
 
@@ -223,7 +225,7 @@ func (a *Authenticator) checkToken(user, token string) error {
 				Filters: bson.A{
 					bson.M{"element.token": token},
 				}})
-			a.tokensCollection.UpdateOne(context.Background(),
+			_, err = a.tokensCollection.UpdateOne(context.Background(),
 				bson.M{
 					"user": user,
 				},
@@ -233,6 +235,9 @@ func (a *Authenticator) checkToken(user, token string) error {
 					},
 				},
 				opts)
+			if err != nil {
+				log.Printf("Unable to update token expiration time for user %s", user)
+			}
 		}
 		return nil
 	}
@@ -248,6 +253,15 @@ func (a *Authenticator) Verify(user, token string) bool {
 	return a.checkToken(user, token) == nil
 }
 
-func (a *Authenticator) CheckPrivileges(user string, privilege Privileges) {
-
+func (a *Authenticator) CheckPrivileges(user string, privilege int) (error, bool) {
+	// TODO: check if privelege is a signle value but not bitmask already
+	log.Printf("Check privileges %d for user %s", privilege, user)
+	result := struct {
+		Privileges int
+	}{}
+	err := a.usersCollection.FindOne(context.Background(), bson.M{"user": user}).Decode(&result)
+	if err != nil {
+		return errors.New("Invalid user"), false
+	}
+	return nil, result.Privileges&privilege != 0
 }

+ 6 - 2
build.sh

@@ -1,17 +1,21 @@
 export GOBIN=$(go env GOPATH)/bin
 export PATH=$PATH:$GOBIN
-export RPC_PATH=common
 
 go env
 go install google.golang.org/protobuf/compiler/protogen
 go install github.com/amsokol/protoc-gen-gotag
 
 # mkdir -p $RPC_PATH
+export RPC_PATH=common
 rm -f $RPC_PATH/*.pb.go
 protoc -I$RPC_PATH --go_out=plugins=grpc:$PWD $RPC_PATH/gostfix.proto
-
 protoc -I$RPC_PATH --gotag_out=xxx="bson+\"-\"",output_path=$RPC_PATH:. $RPC_PATH/gostfix.proto
 
+export RPC_PATH=service
+rm -f $RPC_PATH/*.pb.go
+protoc -I$RPC_PATH --go_out=plugins=grpc:$PWD $RPC_PATH/service.proto
+protoc -I$RPC_PATH --gotag_out=xxx="bson+\"-\"",output_path=$RPC_PATH:. $RPC_PATH/service.proto
+
 #echo "Installing data"
 #rm -rf data
 #mkdir data

+ 2 - 2
common/gostfix.proto

@@ -18,8 +18,8 @@ message MailHeader {
 }
 
 message Mail {
-    MailHeader header = 1;
-    MailBody body = 2;
+	MailHeader header = 1;
+	MailBody body = 2;
 }
 
 message Attachment {

+ 1 - 2
go.mod

@@ -4,7 +4,6 @@ go 1.14
 
 require (
 	github.com/fsnotify/fsnotify v1.4.9
-	github.com/golang/protobuf v1.5.2
 	github.com/google/uuid v1.1.2
 	github.com/gorilla/sessions v1.2.0
 	github.com/gorilla/websocket v1.4.2
@@ -19,7 +18,7 @@ require (
 	golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect
 	golang.org/x/sys v0.0.0-20220422013727-9388b58f7150
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
-	google.golang.org/protobuf v1.28.0 // indirect
+	google.golang.org/protobuf v1.28.0
 	gopkg.in/go-ini/ini.v1 v1.57.0
 	gopkg.in/ini.v1 v1.57.0 // indirect
 )

+ 0 - 4
go.sum

@@ -39,10 +39,7 @@ github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/V
 github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
 github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
 github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
-github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -181,7 +178,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 56 - 17
main.go

@@ -26,44 +26,83 @@
 package main
 
 import (
+	"fmt"
 	"log"
+	"os"
+	"os/signal"
+	"syscall"
 
 	sasl "git.semlanik.org/semlanik/gostfix/sasl"
 	scanner "git.semlanik.org/semlanik/gostfix/scanner"
+	service "git.semlanik.org/semlanik/gostfix/service"
 	web "git.semlanik.org/semlanik/gostfix/web"
 	"github.com/pkg/profile"
 )
 
-type GofixEngine struct {
-	scanner *scanner.MailScanner
-	web     *web.Server
-	sasl    *sasl.SaslServer
+type GostfixEngine struct {
+	services []service.NanoService
+	stats    []*service.NanoServiceStats
 }
 
-func NewGofixEngine() (e *GofixEngine) {
+func NewGostfixEngine() (e *GostfixEngine) {
 	mailScanner := scanner.NewMailScanner()
 	saslService, err := sasl.NewSaslServer()
-	webServer := web.NewServer(mailScanner)
 	if err != nil {
 		log.Fatalf("Unable to intialize sasl server %s\n", err)
 	}
-	e = &GofixEngine{
-		scanner: mailScanner,
-		web:     webServer,
-		sasl:    saslService,
-	}
+
+	webServer := web.NewServer(mailScanner, e)
+
+	e = &GostfixEngine{}
+	e.services = append(e.services, saslService, mailScanner, webServer)
+	e.stats = make([]*service.NanoServiceStats, len(e.services))
 	return
 }
 
-func (e *GofixEngine) Run() {
-	defer e.scanner.Stop()
-	e.sasl.Run()
-	e.scanner.Run()
-	e.web.Run()
+func (e *GostfixEngine) Run() {
+	done := make(chan bool, 1)
+	for i, s := range e.services {
+		if e.stats[i] == nil {
+			e.stats[i] = &service.NanoServiceStats{}
+		}
+		go func(s service.NanoService, stats *service.NanoServiceStats) {
+			stats.Name = s.ServiceName()
+			stats.Status = service.NanoServiceStatus_NanoServiceRunning
+			log.Printf("Running %s", s.ServiceName())
+			s.Run()
+			stats.Status = service.NanoServiceStatus_NanoServiceStopped
+			log.Printf("%s is stopped", s.ServiceName())
+			done <- true
+		}(s, e.stats[i])
+	}
+
+	sigs := make(chan os.Signal, 1)
+	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+	go func() {
+		sig := <-sigs
+		log.Printf("Exit by signal %v", sig)
+		log.Printf("Server is stopping")
+		for i, s := range e.services {
+			if e.stats[i] != nil && e.stats[i].Status == service.NanoServiceStatus_NanoServiceRunning {
+				s.Stop()
+			}
+		}
+	}()
+
+	log.Printf("Server is running succesfully")
+	for i := 0; i < len(e.services); i++ {
+		<-done
+	}
+	fmt.Printf("Server shutdown!!!!")
+	log.Printf("Server shutdown")
+}
+
+func (e *GostfixEngine) ReadStats() []*service.NanoServiceStats {
+	return e.stats
 }
 
 func main() {
 	defer profile.Start().Stop()
-	engine := NewGofixEngine()
+	engine := NewGostfixEngine()
 	engine.Run()
 }

+ 1 - 24
sasl/sasl.go

@@ -41,7 +41,6 @@ import (
 	"time"
 
 	"git.semlanik.org/semlanik/gostfix/auth"
-	"git.semlanik.org/semlanik/gostfix/config"
 	"github.com/google/uuid"
 )
 
@@ -49,6 +48,7 @@ type SaslServer struct {
 	pid           int
 	cuid          int
 	authenticator *auth.Authenticator
+	listener      net.Listener
 }
 
 const (
@@ -82,29 +82,6 @@ func NewSaslServer() (*SaslServer, error) {
 	}, nil
 }
 
-func (s *SaslServer) Run() {
-	go func() {
-		l, err := net.Listen("tcp", "127.0.0.1:"+config.ConfigInstance().SASLPort)
-		if err != nil {
-			log.Fatalf("Coulf not start SASL server: %s\n", err)
-			return
-		}
-		defer l.Close()
-
-		log.Printf("Listen sasl on: %s\n", l.Addr().String())
-
-		for {
-			conn, err := l.Accept()
-			s.cuid++
-			if err != nil {
-				log.Println("Error accepting: ", err.Error())
-				continue
-			}
-			go s.handleRequest(conn)
-		}
-	}()
-}
-
 func (s *SaslServer) handleRequest(conn net.Conn) {
 	conn.SetReadDeadline(time.Time{})
 	defer conn.Close()

+ 66 - 0
sasl/service.go

@@ -0,0 +1,66 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 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 sasl
+
+import (
+	"log"
+	"net"
+
+	"git.semlanik.org/semlanik/gostfix/config"
+)
+
+func (s *SaslServer) ServiceName() string {
+	return "SASL Server"
+}
+
+func (s *SaslServer) Run() {
+	l, err := net.Listen("tcp", "127.0.0.1:"+config.ConfigInstance().SASLPort)
+	if err != nil {
+		log.Fatalf("Coulf not start SASL server: %s\n", err)
+		return
+	}
+	defer l.Close()
+
+	log.Printf("Listen sasl on: %s\n", l.Addr().String())
+
+	for {
+		conn, err := l.Accept()
+		s.cuid++
+		if err != nil {
+			log.Println("Error accepting: ", err.Error())
+			continue
+		}
+		go s.handleRequest(conn)
+	}
+}
+
+func (s *SaslServer) Stop() {
+	// TODO: Make possible to stop SASL
+}
+
+func (s *SaslServer) Error() string {
+	return "" // TODO: return last error in the service
+}

+ 6 - 48
scanner/mailscanner.go

@@ -39,6 +39,7 @@ import (
 
 const (
 	SignalReconfigure = iota
+	SignalStop
 )
 
 type MailScanner struct {
@@ -98,6 +99,7 @@ func (ms *MailScanner) checkEmailRegistred(email string) bool {
 }
 
 func (ms *MailScanner) reconfigure() {
+	log.Printf("Reconfiguring mail scanner")
 	var err error
 	ms.emailMaps, err = ms.storage.ReadEmailMaps()
 	if err != nil {
@@ -129,55 +131,11 @@ func (ms *MailScanner) reconfigure() {
 	}
 }
 
-func (ms *MailScanner) Run() {
-	go func() {
+func (ms *MailScanner) handleSignal(signal int) {
+	switch signal {
+	case SignalReconfigure:
 		ms.reconfigure()
-
-		for {
-			select {
-			case signal := <-ms.signalChannel:
-				switch signal {
-				case SignalReconfigure:
-					ms.reconfigure()
-				}
-			case event, ok := <-ms.watcher.Events:
-				if !ok {
-					return
-				}
-				if event.Op&fsnotify.Write == fsnotify.Write {
-					log.Println("iNotify write")
-
-					mailPath := event.Name
-					mailbox := ""
-					for k, v := range ms.emailMaps {
-						if v == mailPath {
-							mailbox = k
-						}
-					}
-
-					if mailbox != "" {
-						mails := ms.readMailFile(mailPath)
-						for _, mail := range mails {
-							ms.storage.SaveMail(mailbox, common.Inbox, mail, false)
-						}
-						log.Printf("New email for %s, emails read %d", mailPath, len(mails))
-					} else {
-						log.Printf("Invalid path update triggered: %s", mailPath)
-					}
-
-				}
-			case err, ok := <-ms.watcher.Errors:
-				if !ok {
-					return
-				}
-				log.Println("error:", err)
-			}
-		}
-	}()
-}
-
-func (ms *MailScanner) Stop() {
-	defer ms.watcher.Close()
+	}
 }
 
 func (ms *MailScanner) readMailFile(mailPath string) (mails []*common.Mail) {

+ 91 - 0
scanner/service.go

@@ -0,0 +1,91 @@
+/*
+ * 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 scanner
+
+import (
+	"log"
+
+	"git.semlanik.org/semlanik/gostfix/common"
+	"github.com/fsnotify/fsnotify"
+)
+
+func (ms *MailScanner) ServiceName() string {
+	return "Mail Scanner"
+}
+
+func (ms *MailScanner) Run() {
+	ms.reconfigure()
+	defer ms.watcher.Close()
+	for {
+		select {
+		case signal := <-ms.signalChannel:
+			if signal == SignalStop {
+				return
+			}
+			ms.handleSignal(signal)
+		case event, ok := <-ms.watcher.Events:
+			if !ok {
+				log.Printf("Unable to read mail watcher event. Mail scanner restart is needed.")
+				return
+			}
+			if event.Op&fsnotify.Write == fsnotify.Write {
+				log.Println("iNotify write")
+
+				mailPath := event.Name
+				mailbox := ""
+				for k, v := range ms.emailMaps {
+					if v == mailPath {
+						mailbox = k
+					}
+				}
+
+				if mailbox != "" {
+					mails := ms.readMailFile(mailPath)
+					for _, mail := range mails {
+						ms.storage.SaveMail(mailbox, common.Inbox, mail, false)
+					}
+					log.Printf("New email for %s, emails read %d", mailPath, len(mails))
+				} else {
+					log.Printf("Invalid path update triggered: %s", mailPath)
+				}
+			}
+		case err, ok := <-ms.watcher.Errors:
+			if !ok {
+				log.Printf("Unable to read mail watcher errors. Mail scanner restart is needed.")
+				return
+			}
+			log.Println("Mail watcher error:", err)
+		}
+	}
+}
+
+func (ms *MailScanner) Stop() {
+	ms.signalChannel <- SignalStop
+}
+
+func (ms *MailScanner) Error() string {
+	return ""
+}

+ 37 - 0
service/service.go

@@ -0,0 +1,37 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 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 service
+
+type NanoService interface {
+	ServiceName() string
+	Run()
+	Stop()
+	Error() string
+}
+
+type NanoServiceWatcher interface {
+	ReadStats() []*NanoServiceStats
+}

+ 16 - 0
service/service.proto

@@ -0,0 +1,16 @@
+syntax = "proto3";
+option go_package = "./service";
+package service;
+
+enum NanoServiceStatus {
+	NanoServiceStopped = 0;
+	NanoServiceRunning = 1;
+	NanoServiceError = 2;
+}
+
+message NanoServiceStats {
+	string name = 1;
+	NanoServiceStatus status = 2;
+	string error = 3;
+	uint64 upTime = 4;
+}

+ 1 - 1
setup/setup.go

@@ -1,7 +1,7 @@
 /*
  * MIT License
  *
- * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
+ * Copyright (c) 2022 Alexey Edelev <semlanik@gmail.com>
  *
  * This file is part of gostfix project https://git.semlanik.org/semlanik/gostfix
  *

+ 1 - 0
utils/regexp.go

@@ -1,3 +1,4 @@
+
 /*
  * MIT License
  *

+ 18 - 0
web/js/admin.js

@@ -0,0 +1,18 @@
+function updateServiceStats() {
+    $.ajax({
+        url: '/s',
+        success: function(result) {
+            var data = jQuery.parseJSON(result);
+            pageMax = Math.floor(data.total/50);
+
+            if ($('#mailList')) {
+                $('#mailList').html(data.html);
+            }
+        },
+        error: function(jqXHR, textStatus, errorThrown) {
+            if ($('#mailList')) {
+                $('#mailList').html('Unable to load service list');
+            }
+        }
+    });
+}

+ 11 - 0
web/securezone.go

@@ -30,6 +30,7 @@ import (
 	"log"
 	"net/http"
 
+	"git.semlanik.org/semlanik/gostfix/auth"
 	"git.semlanik.org/semlanik/gostfix/common"
 )
 
@@ -38,6 +39,16 @@ func (s *Server) handleSecureZone(w http.ResponseWriter, r *http.Request, user s
 		log.Printf("User could not be empty. Invalid usage of handleMailRequest")
 		panic(nil)
 	}
+
+	err, ok := s.authenticator.CheckPrivileges(user, auth.AdminPrivilege)
+	if err != nil {
+		log.Printf("Unable to fetch priveleges %s for user %s", err, user)
+	}
+
+	if !ok {
+		s.error(http.StatusUnauthorized, "Administrator permissions required", w)
+		return
+	}
 	s.error(http.StatusNotImplemented, "Admin panel is not implemented", w)
 }
 

+ 11 - 6
web/server.go

@@ -35,6 +35,7 @@ import (
 	common "git.semlanik.org/semlanik/gostfix/common"
 	"git.semlanik.org/semlanik/gostfix/config"
 	db "git.semlanik.org/semlanik/gostfix/db"
+	service "git.semlanik.org/semlanik/gostfix/service"
 
 	sessions "github.com/gorilla/sessions"
 )
@@ -66,9 +67,11 @@ type Server struct {
 	storage           *db.Storage
 	notifier          *webNotifier
 	scanner           common.Scanner
+	watcher           service.NanoServiceWatcher
+	httpServer        *http.Server
 }
 
-func NewServer(scanner common.Scanner) *Server {
+func NewServer(scanner common.Scanner, watcher service.NanoServiceWatcher) *Server {
 
 	storage, err := db.NewStorage()
 
@@ -91,19 +94,19 @@ func NewServer(scanner common.Scanner) *Server {
 		storage:           storage,
 		notifier:          NewWebNotifier(),
 		scanner:           scanner,
+		watcher:           watcher,
 	}
 
 	s.notifier.server = s
 	s.storage.RegisterNotifier(s.notifier)
+	s.httpServer = &http.Server{
+		Addr:    ":" + config.ConfigInstance().WebPort,
+		Handler: s,
+	}
 
 	return s
 }
 
-func (s *Server) Run() {
-	http.Handle("/", s)
-	log.Fatal(http.ListenAndServe(":"+config.ConfigInstance().WebPort, nil))
-}
-
 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	log.Printf("%s %s", r.Method, r.URL.Path)
 	urlParts := strings.Split(r.URL.Path, "/")[1:]
@@ -160,6 +163,8 @@ func (s *Server) handleSecure(w http.ResponseWriter, r *http.Request, urlParts [
 		s.handleSettings(w, r, user)
 	case "admin":
 		s.handleSecureZone(w, r, user)
+	case "s":
+		s.handleSecureZone(w, r, user)
 	default:
 		http.Redirect(w, r, "/m/0", http.StatusTemporaryRedirect)
 	}

+ 45 - 0
web/service.go

@@ -0,0 +1,45 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 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 "context"
+
+func (s *Server) ServiceName() string {
+	return "Web Server"
+}
+
+func (s *Server) Run() {
+	s.httpServer.ListenAndServe()
+}
+
+func (s *Server) Stop() {
+	s.httpServer.Shutdown(context.Background())
+	// TODO: terminate the service
+}
+
+func (s *Server) Error() string {
+	return "" // TODO: return last error from "ListenAndServe" call
+}

+ 1 - 0
web/templater.go

@@ -45,6 +45,7 @@ const (
 	SignupTemplateName     = "signup.html"
 	RegisterTemplateName   = "register.html"
 	SettingsTemplateName   = "settings.html"
+	AdminTemplateName      = "admin.html"
 )
 
 type Templater struct {

+ 44 - 0
web/templates/admin.html

@@ -0,0 +1,44 @@
+<!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 src="/js/notifications.js"></script>
+        <script src="/js/admin.js"></script>
+        <script>
+             $(document).ready(function() {
+                 updateServiceStats()
+             }
+        </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">Service list</div>
+                    </div>
+                    <div class="verticalPaddingBox">
+                        <div class="innerConentBox materialLevel1">
+                            <div id="adminContent" style="flex: 1 1 auto; display: flex; flex-direction: column;">
+                                <div class="serviceListHeader">
+                                    Service list
+                                </div>
+                                
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/><a href="https://github.com/semlanik/gostfix" target="_blank">gostfix</a>&nbsp;{{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
+        </div>
+    </body>
+</html>