diff --git a/resources/db/migration/V1.2.0.1__create_reports.sql b/resources/db/migration/V1.2.0.1__create_reports.sql
new file mode 100644
index 0000000..d74143e
--- /dev/null
+++ b/resources/db/migration/V1.2.0.1__create_reports.sql
@@ -0,0 +1,19 @@
+create table bug_reports
+(
+ uuid varchar(36) not null primary key,
+ created timestamptz not null,
+ operator_uuid varchar(36) not null,
+ email varchar not null,
+ center_uuid varchar not null,
+ center_name varchar not null,
+ center_address varchar not null,
+ subject text not null,
+ message text
+);
+
+alter table operators
+ add email varchar,
+ add bug_reports_receiver varchar;
+
+alter table centers
+ add email varchar;
\ No newline at end of file
diff --git a/resources/db/migration/V1.2.0.2__add_system_settings.sql b/resources/db/migration/V1.2.0.2__add_system_settings.sql
new file mode 100644
index 0000000..2b47927
--- /dev/null
+++ b/resources/db/migration/V1.2.0.2__add_system_settings.sql
@@ -0,0 +1,22 @@
+create table system_settings
+(
+ config_key varchar not null primary key,
+ config_value text
+);
+
+INSERT INTO public.system_settings (config_key, config_value) VALUES ('reports.email.default', 'dirk.reske@t-systems.com');
+INSERT INTO public.system_settings (config_key, config_value) VALUES ('reports.email.subject', 'Meldungen zur Ihren Teststellen');
+INSERT INTO public.system_settings (config_key, config_value) VALUES ('reports.email.template', 'Guten Tag,
+
+für Ihre Teststellen wurden folgende Fehler gemeldet.
+
+{{range $center, $reports := .Centers}}Meldungen für die Teststelle {{(index $reports 0).CenterName}} ({{(index $reports 0).CenterAddress}})
+
+{{range $reportUUID, $report := $reports}}
+- {{$report.Subject}} {{if $report.Message}}(Hinweis: {{$report.Message}}){{end}} (Gemeldet am {{$report.Created}}){{end}}
+
+
+{{end}}
+
+Viele Grüße
+Ihr Schnelltestportal.de Team');
\ No newline at end of file
diff --git a/src/api/centers.go b/src/api/centers.go
index c4db103..0b8fa31 100644
--- a/src/api/centers.go
+++ b/src/api/centers.go
@@ -10,13 +10,12 @@ import (
"com.t-systems-mms.cwa/repositories"
"com.t-systems-mms.cwa/services"
"encoding/csv"
- "encoding/json"
"github.com/go-chi/chi"
"github.com/go-chi/jwtauth"
+ "github.com/go-playground/validator"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/sirupsen/logrus"
- "io"
"net/http"
"strconv"
"strings"
@@ -55,9 +54,12 @@ type Centers struct {
geocoder geocoding.Geocoder
operatorsService services.Operators
centersRepository repositories.Centers
+ bugReportsService services.BugReports
+ validate *validator.Validate
}
func NewCentersAPI(centersService services.Centers, centersRepository repositories.Centers,
+ bugReportsService services.BugReports,
operatorsService services.Operators, geocoder geocoding.Geocoder, auth *jwtauth.JWTAuth) *Centers {
centers := &Centers{
Router: chi.NewRouter(),
@@ -65,11 +67,14 @@ func NewCentersAPI(centersService services.Centers, centersRepository repositori
centersRepository: centersRepository,
operatorsService: operatorsService,
geocoder: geocoder,
+ bugReportsService: bugReportsService,
+ validate: validator.New(),
}
// public endpoints
centers.Get("/", api.Handle(centers.FindCenters))
centers.Get("/bounds", api.Handle(centers.Geocode))
+ centers.Post("/{uuid}/report", api.Handle(centers.createBugReport))
centers.Group(func(r chi.Router) {
r.Use(jwtauth.Verifier(auth))
@@ -226,11 +231,7 @@ func (c *Centers) AdminGetCentersCSV(w http.ResponseWriter, r *http.Request) {
func (c *Centers) ImportCenters(_ http.ResponseWriter, r *http.Request) (interface{}, error) {
var importData model.ImportCenterRequest
- buffer, err := io.ReadAll(r.Body)
- if err != nil {
- return nil, err
- }
- if err := json.Unmarshal(buffer, &importData); err != nil {
+ if err := api.ParseRequestBody(r, c.validate, &importData); err != nil {
return nil, err
}
@@ -276,6 +277,18 @@ func (c *Centers) deleteCenterByReference(_ http.ResponseWriter, r *http.Request
return nil, c.centersRepository.Delete(r.Context(), center)
}
+// createBugReport creates a bug report for the specified center
+func (c *Centers) createBugReport(_ http.ResponseWriter, r *http.Request) (interface{}, error) {
+ uuid := chi.URLParam(r, "uuid")
+ var request model.CreateBugReportRequestDTO
+ if err := api.ParseRequestBody(r, c.validate, &request); err != nil {
+ return nil, err
+ }
+
+ _, err := c.bugReportsService.CreateBugReport(r.Context(), uuid, request.Subject, request.Message)
+ return nil, err
+}
+
func (c *Centers) DeleteCenter(_ http.ResponseWriter, r *http.Request) (interface{}, error) {
centerUUID := chi.URLParam(r, "uuid")
logrus.WithField("uuid", centerUUID).Trace("DeleteCenter")
diff --git a/src/api/model/bugreport.go b/src/api/model/bugreport.go
new file mode 100644
index 0000000..c29d80a
--- /dev/null
+++ b/src/api/model/bugreport.go
@@ -0,0 +1,6 @@
+package model
+
+type CreateBugReportRequestDTO struct {
+ Subject string `json:"subject" validate:"required,max=160"`
+ Message *string `json:"message" validate:"omitempty,max=160"`
+}
diff --git a/src/api/model/centers.go b/src/api/model/centers.go
index b57cde3..31c02d6 100644
--- a/src/api/model/centers.go
+++ b/src/api/model/centers.go
@@ -17,7 +17,7 @@ type FindCentersResult struct {
}
type ImportCenterRequest struct {
- Centers []EditCenterDTO `json:"centers"`
+ Centers []EditCenterDTO `json:"centers" validate:"dive"`
DeleteAll bool `json:"deleteAll"`
}
@@ -30,6 +30,7 @@ type ImportCenterResult struct {
type CenterSummaryDTO struct {
UUID string `json:"uuid"`
Name string `json:"name"`
+ Email *string `json:"email"`
Website *string `json:"website"`
Coordinates *CoordinatesDTO `json:"coordinates"`
Logo *string `json:"logo"`
@@ -58,6 +59,7 @@ func (CenterSummaryDTO) MapFromDomain(center *domain.Center) *CenterSummaryDTO {
return &CenterSummaryDTO{
UUID: center.UUID,
Name: center.Name,
+ Email: center.Email,
Website: center.Website,
Coordinates: CoordinatesDTO{}.MapFromModel(¢er.Coordinates),
Logo: getCenterLogo(center),
@@ -121,12 +123,13 @@ func MapToCenterDTOs(centers []domain.Center) []CenterDTO {
type EditCenterDTO struct {
UserReference *string `json:"userReference"`
Name string `json:"name" validate:"required"`
+ Email *string `json:"email" validate:"omitempty,email"`
Website *string `json:"website"`
Address string `json:"address" validate:"required"`
- OpeningHours []string `json:"openingHours"`
+ OpeningHours []string `json:"openingHours" validate:"dive,max=64"`
AddressNote *string `json:"addressNote"`
- Appointment *string `json:"appointment"`
- TestKinds []string `json:"testKinds"`
+ Appointment *string `json:"appointment" validate:"omitempty,oneof=Required NotRequired Possible"`
+ TestKinds []string `json:"testKinds" validate:"dive,oneof=Antigen PCR Vaccination Antibody"`
DCC *bool `json:"dcc"`
EnterDate *string `json:"enterDate"`
LeaveDate *string `json:"leaveDate"`
@@ -160,6 +163,7 @@ func (c EditCenterDTO) MapToDomain() domain.Center {
DCC: c.DCC,
EnterDate: enterDate,
LeaveDate: leaveDate,
+ Email: c.Email,
}
}
diff --git a/src/api/model/operators.go b/src/api/model/operators.go
index 31ef684..d55dbf8 100644
--- a/src/api/model/operators.go
+++ b/src/api/model/operators.go
@@ -6,6 +6,7 @@ type OperatorDTO struct {
UUID string `json:"uuid"`
OperatorNumber *string `json:"operatorNumber"`
Name string `json:"name"`
+ Email *string `json:"email"`
Logo *string `json:"logo"`
MarkerIcon *string `json:"markerIcon"`
}
@@ -33,5 +34,6 @@ func MapToOperatorDTO(operator *domain.Operator) *OperatorDTO {
Name: operator.Name,
Logo: logo,
MarkerIcon: markerIcon,
+ Email: operator.Email,
}
}
diff --git a/src/cmd/backend/config.go b/src/cmd/backend/config.go
index 16c89c0..f8c8438 100644
--- a/src/cmd/backend/config.go
+++ b/src/cmd/backend/config.go
@@ -2,6 +2,7 @@ package main
import (
"com.t-systems-mms.cwa/external/geocoding"
+ "com.t-systems-mms.cwa/services"
"errors"
"fmt"
"github.com/hashicorp/vault/api"
@@ -17,6 +18,8 @@ type Config struct {
Database DatabaseConfig
Google geocoding.GoogleGeocoderConfig
Authentication AuthenticationConfig
+ BugReports services.BugReportConfig
+ Email services.EmailConfig
}
type DatabaseConfig struct {
@@ -105,6 +108,34 @@ func LoadConfig() error {
appConfig.Database.IdlePoolSize = 10
}
+ // E-Mail
+ if err := readStringSecret(logicalClient, backend+"/data/email", "smtp-host",
+ &appConfig.Email.SmtpHost); err != nil {
+ return err
+ }
+ if err := readIntSecret(logicalClient, backend+"/data/email", "smtp-port",
+ &appConfig.Email.SmtpPort); err != nil {
+ return err
+ }
+ if err := readStringSecret(logicalClient, backend+"/data/email", "smtp-user",
+ &appConfig.Email.SmtpUser); err != nil {
+ return err
+ }
+ if err := readStringSecret(logicalClient, backend+"/data/email", "smtp-password",
+ &appConfig.Email.SmtpPassword); err != nil {
+ return err
+ }
+ if err := readStringSecret(logicalClient, backend+"/data/email", "from",
+ &appConfig.Email.From); err != nil {
+ return err
+ }
+
+ // Bug reports
+ if err := readIntSecret(logicalClient, backend+"/data/reports", "interval",
+ &appConfig.BugReports.Interval); err != nil {
+ appConfig.BugReports.Interval = 24 * 60
+ }
+
// Authentication
if err := readStringSecret(logicalClient, backend+"/data/authentication", "jwks-url",
&appConfig.Authentication.JwksUrl); err != nil {
diff --git a/src/cmd/backend/main.go b/src/cmd/backend/main.go
index b427c4d..3f5cc38 100644
--- a/src/cmd/backend/main.go
+++ b/src/cmd/backend/main.go
@@ -69,11 +69,19 @@ func main() {
sqlDB.SetMaxOpenConns(appConfig.Database.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(appConfig.Database.ConnMaxLifetime) * time.Minute)
+ settingsRepository := repositories.NewSystemSettingsRepository(db)
+
centersRepository := repositories.NewCentersRepository(db)
operatorsRepository := repositories.NewOperatorsRepository(db)
operatorsService := services.NewOperatorsService(operatorsRepository)
centersService := services.NewCentersService(centersRepository, operatorsRepository, operatorsService, geocoder)
+ mailService := services.NewMailService(appConfig.Email)
+
+ bugReportsRepository := repositories.NewBugReportsRepository(db)
+ bugReportsService := services.NewBugReportsService(appConfig.BugReports,
+ mailService, centersRepository, bugReportsRepository, settingsRepository)
+
// configure authentication
jwksSource := jwks.NewWebSource(appConfig.Authentication.JwksUrl)
jwksClient := jwks.NewDefaultClient(jwksSource, time.Hour, 12*time.Hour)
@@ -88,7 +96,7 @@ func main() {
router := chi.NewRouter()
router.Use(middleware.DefaultLogger)
router.Handle("/metrics", initMetricsHandler(centersRepository, operatorsRepository))
- router.Mount("/api/centers", api.NewCentersAPI(centersService, centersRepository, operatorsService, geocoder, tokenAuth))
+ router.Mount("/api/centers", api.NewCentersAPI(centersService, centersRepository, bugReportsService, operatorsService, geocoder, tokenAuth))
router.Mount("/api/operators", api.NewOperatorsAPI(operatorsRepository, operatorsService, tokenAuth))
server := &http.Server{
@@ -109,6 +117,8 @@ func main() {
serverWaitHandle.Done()
}()
+ go bugReportsService.PublishScheduler()
+
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
<-signals
diff --git a/src/core/api/errors.go b/src/core/api/errors.go
index b1aa724..fcbe57b 100644
--- a/src/core/api/errors.go
+++ b/src/core/api/errors.go
@@ -38,7 +38,7 @@ func createValidationErrorResponse(errors validator.ValidationErrors) Validation
for _, fieldError := range errors {
fields = append(fields, ValidationFieldError{
Field: fieldError.Field(),
- Validation: fieldError.Tag(),
+ Validation: fieldError.Tag() + "=" + fieldError.Param(),
})
}
return ValidationErrorResponse{
diff --git a/src/core/api/requesthandler.go b/src/core/api/requesthandler.go
index 9897603..3d9b0b8 100644
--- a/src/core/api/requesthandler.go
+++ b/src/core/api/requesthandler.go
@@ -7,6 +7,7 @@ import (
"github.com/go-playground/validator"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
+ "io"
"net/http"
"time"
)
@@ -78,3 +79,19 @@ func WriteResponse(writer http.ResponseWriter, code int, body interface{}) {
}
}
}
+
+// ParseRequestBody parses the request body, unmarshals the json into target and validates it
+func ParseRequestBody(r *http.Request, validate *validator.Validate, target interface{}) error {
+ if data, err := io.ReadAll(r.Body); err == nil {
+ if err := json.Unmarshal(data, target); err == nil {
+ if vErr := validate.Struct(target); vErr != nil {
+ return vErr
+ }
+ } else {
+ return err
+ }
+ } else {
+ return err
+ }
+ return nil
+}
diff --git a/src/domain/bugreports.go b/src/domain/bugreports.go
new file mode 100644
index 0000000..705fcef
--- /dev/null
+++ b/src/domain/bugreports.go
@@ -0,0 +1,21 @@
+package domain
+
+import "time"
+
+const (
+ ReportReceiverCenter = "center"
+ ReportReceiverOperator = "operator"
+)
+
+type BugReport struct {
+ UUID string `gorm:"primaryKey"`
+ Created time.Time
+ Email string
+ OperatorUUID string
+ Operator Operator
+ CenterUUID string
+ CenterName string
+ CenterAddress string
+ Subject string
+ Message *string
+}
diff --git a/src/domain/centers.go b/src/domain/centers.go
index b2ad67f..e034830 100644
--- a/src/domain/centers.go
+++ b/src/domain/centers.go
@@ -81,6 +81,7 @@ type Center struct {
Ranking float64
Zip *string
Region *string
+ Email *string `validate:"omitempty,email"`
}
type CenterWithDistance struct {
diff --git a/src/domain/operator.go b/src/domain/operator.go
index 2df0478..54f44da 100644
--- a/src/domain/operator.go
+++ b/src/domain/operator.go
@@ -1,10 +1,12 @@
package domain
type Operator struct {
- UUID string `gorm:"primaryKey"`
- Subject *string
- OperatorNumber *string
- Name string
- Logo *string
- MarkerIcon *string
+ UUID string `gorm:"primaryKey"`
+ Subject *string
+ OperatorNumber *string
+ Name string
+ Logo *string
+ MarkerIcon *string
+ Email *string
+ BugReportsReceiver *string
}
diff --git a/src/domain/systemsettings.go b/src/domain/systemsettings.go
new file mode 100644
index 0000000..c99ca88
--- /dev/null
+++ b/src/domain/systemsettings.go
@@ -0,0 +1,6 @@
+package domain
+
+type SystemSetting struct {
+ ConfigKey string
+ ConfigValue *string
+}
diff --git a/src/go.mod b/src/go.mod
index fb34dfe..0698d29 100644
--- a/src/go.mod
+++ b/src/go.mod
@@ -20,6 +20,7 @@ require (
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50
+ github.com/xhit/go-simple-mail/v2 v2.10.0 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/text v0.3.6 // indirect
googlemaps.github.io/maps v1.3.2
diff --git a/src/go.sum b/src/go.sum
index 5e9e532..994d968 100644
--- a/src/go.sum
+++ b/src/go.sum
@@ -470,6 +470,10 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 h1:uxE3GYdXIOfhMv3unJKETJEhw78gvzuQqRX/rVirc2A=
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
+github.com/xhit/go-simple-mail v2.2.2+incompatible h1:Hm2VGfLqiQJ/NnC8SYsrPOPyVYIlvP2kmnotP4RIV74=
+github.com/xhit/go-simple-mail v2.2.2+incompatible/go.mod h1:I8Ctg6vIJZ+Sv7k/22M6oeu/tbFumDY0uxBuuLbtU7Y=
+github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8=
+github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
diff --git a/src/repositories/bugreports.go b/src/repositories/bugreports.go
new file mode 100644
index 0000000..a86bba2
--- /dev/null
+++ b/src/repositories/bugreports.go
@@ -0,0 +1,57 @@
+package repositories
+
+import (
+ "com.t-systems-mms.cwa/core/util"
+ "com.t-systems-mms.cwa/domain"
+ "context"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+ "time"
+)
+
+type BugReports interface {
+ Repository
+ Save(ctx context.Context, center *domain.BugReport) error
+ FindAll(ctx context.Context) ([]domain.BugReport, error)
+ DeleteAll(ctx context.Context) error
+ DeleteByReceiver(ctx context.Context, receiver string) error
+}
+
+type bugReportsRepository struct {
+ postgresqlRepository
+ db *gorm.DB
+ // Save persists the given center
+}
+
+func NewBugReportsRepository(db *gorm.DB) BugReports {
+ return &bugReportsRepository{
+ postgresqlRepository: postgresqlRepository{db: db},
+ db: db,
+ }
+}
+
+func (b *bugReportsRepository) Save(ctx context.Context, report *domain.BugReport) error {
+ if util.IsNilOrEmpty(&report.UUID) {
+ if id, err := uuid.NewUUID(); err != nil {
+ return err
+ } else {
+ report.UUID = id.String()
+ report.Created = time.Now()
+ }
+ }
+ return b.db.Save(report).Error
+}
+
+func (b *bugReportsRepository) DeleteAll(ctx context.Context) error {
+ return b.GetTX(ctx).Exec("DELETE FROM bug_reports").Error
+}
+
+func (b *bugReportsRepository) DeleteByReceiver(ctx context.Context, receiver string) error {
+ return b.GetTX(ctx).Exec("DELETE FROM bug_reports where email = ?", receiver).Error
+}
+
+func (b *bugReportsRepository) FindAll(ctx context.Context) ([]domain.BugReport, error) {
+ var reports []domain.BugReport
+ err := b.GetTX(ctx).Find(&reports).Error
+ return reports, err
+}
diff --git a/src/repositories/centers.go b/src/repositories/centers.go
index 908de81..030a53d 100644
--- a/src/repositories/centers.go
+++ b/src/repositories/centers.go
@@ -30,8 +30,7 @@ type CenterStatistics struct {
}
type Centers interface {
- UseTransaction(ctx context.Context, fn func(ctx context.Context) error) error
-
+ Repository
FindByUUID(ctx context.Context, uuid string) (domain.Center, error)
Delete(ctx context.Context, center domain.Center) error
@@ -80,7 +79,10 @@ func (r *centersRepository) Delete(ctx context.Context, center domain.Center) er
func (r *centersRepository) FindByUUID(ctx context.Context, uuid string) (domain.Center, error) {
var center domain.Center
- err := r.db.Model(&domain.Center{}).Where("uuid = ?", uuid).First(¢er).Error
+ err := r.db.Model(&domain.Center{}).
+ Preload("Operator").
+ Where("uuid = ?", uuid).
+ First(¢er).Error
return center, err
}
diff --git a/src/repositories/operators.go b/src/repositories/operators.go
index af5c5c1..55d9455 100644
--- a/src/repositories/operators.go
+++ b/src/repositories/operators.go
@@ -72,21 +72,28 @@ func (r *operatorsRepository) GetOrCreateByToken(ctx context.Context, token jwt.
if errors.Is(err, gorm.ErrRecordNotFound) {
// Create the operator with default settings
name := ""
- nameEntry, ok := token.Get("name")
- if ok {
- name = nameEntry.(string)
+ if entry, ok := token.Get("name"); ok {
+ name = entry.(string)
}
- username := ""
- usernameEntry, ok := token.Get("preferred_username")
- if ok {
- username = usernameEntry.(string)
+
+ operatorNumber := ""
+ if entry, ok := token.Get("preferred_username"); ok {
+ operatorNumber = entry.(string)
+ }
+
+ email := ""
+ if entry, ok := token.Get("email"); ok {
+ email = entry.(string)
}
+ receiver := domain.ReportReceiverOperator
return r.Save(ctx, domain.Operator{
- UUID: id.String(),
- OperatorNumber: &username,
- Name: name,
- Subject: &subject,
+ UUID: id.String(),
+ OperatorNumber: &operatorNumber,
+ Name: name,
+ Subject: &subject,
+ BugReportsReceiver: &receiver,
+ Email: &email,
})
}
return operator, nil
diff --git a/src/repositories/postgresql.go b/src/repositories/postgresql.go
index 74efcdc..1a1444e 100644
--- a/src/repositories/postgresql.go
+++ b/src/repositories/postgresql.go
@@ -7,6 +7,10 @@ import (
const transactionKey = "transactionKey"
+type Repository interface {
+ UseTransaction(ctx context.Context, fn func(ctx context.Context) error) error
+}
+
type postgresqlRepository struct {
db *gorm.DB
}
diff --git a/src/repositories/systemsettings.go b/src/repositories/systemsettings.go
new file mode 100644
index 0000000..5beeac7
--- /dev/null
+++ b/src/repositories/systemsettings.go
@@ -0,0 +1,53 @@
+package repositories
+
+import (
+ "com.t-systems-mms.cwa/domain"
+ "context"
+ "errors"
+ "gorm.io/gorm"
+)
+
+type SystemSettings interface {
+ FindValue(ctx context.Context, key string) (*string, error)
+ FindValueWithDefault(ctx context.Context, key, value string) (string, error)
+}
+
+type systemSettingsRepository struct {
+ postgresqlRepository
+ db *gorm.DB
+ // Save persists the given center
+}
+
+func NewSystemSettingsRepository(db *gorm.DB) SystemSettings {
+ return &systemSettingsRepository{
+ postgresqlRepository: postgresqlRepository{db: db},
+ db: db,
+ }
+}
+
+func (s *systemSettingsRepository) FindValue(ctx context.Context, key string) (*string, error) {
+ var setting domain.SystemSetting
+ err := s.GetTX(ctx).
+ Model(domain.SystemSetting{}).
+ Where("config_key = ?", key).
+ First(&setting).
+ Error
+
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, nil
+ }
+
+ return setting.ConfigValue, err
+}
+
+func (s *systemSettingsRepository) FindValueWithDefault(ctx context.Context, key, value string) (string, error) {
+ val, err := s.FindValue(ctx, key)
+ if err != nil {
+ return "", err
+ }
+
+ if val == nil || *val == "" {
+ return value, nil
+ }
+ return *val, nil
+}
diff --git a/src/services/bugreports.go b/src/services/bugreports.go
new file mode 100644
index 0000000..3865e10
--- /dev/null
+++ b/src/services/bugreports.go
@@ -0,0 +1,187 @@
+package services
+
+import (
+ "com.t-systems-mms.cwa/domain"
+ "com.t-systems-mms.cwa/repositories"
+ "context"
+ "errors"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+ "github.com/sirupsen/logrus"
+ "strings"
+ "text/template"
+ "time"
+)
+
+var (
+ createdBugReportsCount = promauto.NewCounter(prometheus.CounterOpts{
+ Name: "cwa_map_bug_reports_count",
+ Help: "The total count of created bug reports",
+ })
+)
+
+const (
+ ConfigDefaultReportsEmail = "reports.email.default"
+ ConfigReportsEmailTemplate = "reports.email.template"
+ ConfigReportsEmailSubject = "reports.email.subject"
+)
+
+type BugReportConfig struct {
+ Interval int
+}
+
+type BugReports interface {
+ CreateBugReport(ctx context.Context, centerUUID, subject string, message *string) (domain.BugReport, error)
+
+ //PublishBugReports sends all pending bug reports the appropriate receivers.
+ //After sending the reports will be permanently deleted
+ PublishBugReports(ctx context.Context) error
+
+ //PublishScheduler starts the scheduler for regularly sending bug reports
+ PublishScheduler()
+}
+
+type bugReportsService struct {
+ config BugReportConfig
+ mailService MailService
+ centersRepository repositories.Centers
+ bugReportsRepository repositories.BugReports
+ settingsRepository repositories.SystemSettings
+}
+
+func NewBugReportsService(config BugReportConfig,
+ mailService MailService,
+ centersRepository repositories.Centers,
+ bugReportsRepository repositories.BugReports,
+ settingsRepository repositories.SystemSettings) BugReports {
+
+ return &bugReportsService{
+ config: config,
+ mailService: mailService,
+ centersRepository: centersRepository,
+ bugReportsRepository: bugReportsRepository,
+ settingsRepository: settingsRepository,
+ }
+}
+
+func (s *bugReportsService) CreateBugReport(ctx context.Context, centerUUID, subject string, message *string) (domain.BugReport, error) {
+ // check if center exists
+ center, err := s.centersRepository.FindByUUID(ctx, centerUUID)
+ if err != nil {
+ return domain.BugReport{}, err
+ }
+
+ email := center.Operator.Email
+ if center.Operator.BugReportsReceiver != nil && *center.Operator.BugReportsReceiver == "center" {
+ email = center.Email
+ }
+
+ if email == nil {
+ if value, err := s.settingsRepository.FindValue(ctx, ConfigDefaultReportsEmail); err != nil {
+ logrus.WithError(err).WithField("key", ConfigDefaultReportsEmail).Error("Error getting config value")
+ return domain.BugReport{}, errors.New("invalid config")
+ } else if value == nil {
+ logrus.WithField("key", ConfigDefaultReportsEmail).Error("Config not set")
+ return domain.BugReport{}, errors.New("invalid config")
+ } else {
+ email = value
+ }
+ }
+
+ report := domain.BugReport{
+ Created: time.Now(),
+ Email: *email,
+ CenterUUID: centerUUID,
+ OperatorUUID: center.OperatorUUID,
+ CenterName: center.Name,
+ CenterAddress: center.Address,
+ Subject: subject,
+ Message: message,
+ }
+
+ err = s.bugReportsRepository.Save(ctx, &report)
+ createdBugReportsCount.Inc()
+ return report, err
+}
+
+func (s *bugReportsService) PublishBugReports(ctx context.Context) error {
+ reports, err := s.bugReportsRepository.FindAll(ctx)
+ if err != nil {
+ return err
+ }
+
+ // getting template
+ mailTemplateString, err := s.settingsRepository.FindValue(ctx, ConfigReportsEmailTemplate)
+ if err != nil {
+ return err
+ } else if mailTemplateString == nil {
+ return errors.New("missing template")
+ }
+
+ mailSubject, err := s.settingsRepository.FindValue(ctx, ConfigReportsEmailSubject)
+ if err != nil {
+ return err
+ } else if mailSubject == nil {
+ return errors.New("missing subject")
+ }
+
+ mailTemplate, err := template.New("report").Parse(*mailTemplateString)
+ if err != nil {
+ return err
+ }
+
+ // collect reports by receiver
+ receivers := make(map[string][]domain.BugReport)
+ for _, report := range reports {
+ if _, ok := receivers[report.Email]; !ok {
+ receivers[report.Email] = make([]domain.BugReport, 0)
+ }
+ receivers[report.Email] = append(receivers[report.Email], report)
+ }
+
+ for receiver, reports := range receivers {
+ // collect reports by center
+ centers := make(map[string][]domain.BugReport)
+ for _, report := range reports {
+ if _, ok := centers[report.CenterUUID]; !ok {
+ centers[report.CenterUUID] = make([]domain.BugReport, 0)
+ }
+ centers[report.CenterUUID] = append(centers[report.CenterUUID], report)
+ }
+
+ // render template
+ buffer := strings.Builder{}
+ err := mailTemplate.Execute(&buffer, struct {
+ Centers map[string][]domain.BugReport
+ }{
+ Centers: centers,
+ })
+ if err != nil {
+ return err
+ }
+
+ // sending mail
+ err = s.mailService.SendMail(ctx, receiver, *mailSubject, "text/html", buffer.String())
+ if err != nil {
+ return err
+ }
+
+ // delete reports for this receiver
+ err = s.bugReportsRepository.DeleteByReceiver(ctx, receiver)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (s *bugReportsService) PublishScheduler() {
+ logrus.WithFields(logrus.Fields{"interval": s.config.Interval}).Info("PublishScheduler started")
+ for {
+ if err := s.PublishBugReports(context.Background()); err != nil {
+ logrus.WithError(err).Error("Error publishing reports")
+ }
+ time.Sleep(time.Duration(s.config.Interval) * time.Minute)
+ }
+}
diff --git a/src/services/csvparser.go b/src/services/csvparser.go
index 25b409b..de5aba0 100644
--- a/src/services/csvparser.go
+++ b/src/services/csvparser.go
@@ -23,6 +23,7 @@ const (
cityIndex = 6
enterDateIndex = 8
leaveDateIndex = 9
+ emailIndex = 12
openingHoursIndex = 13
appointmentIndex = 14
testKindsIndex = 15
@@ -143,6 +144,13 @@ func (c *CsvParser) parseCsvRow(entry []string) ImportCenterResult {
var website *string
if entry := strings.TrimSpace(entry[websiteIndex]); entry != "" && strings.ToLower(entry) != "null" {
website = &entry
+ } else {
+ result.Errors = append(result.Errors, "invalid email address: "+entry)
+ }
+
+ var email *string
+ if entry := strings.TrimSpace(entry[emailIndex]); entry != "" && strings.ToLower(entry) != "null" {
+ email = &entry
}
dcc := strings.ToLower(strings.TrimSpace(entry[dccIndex])) == "ja"
@@ -173,6 +181,7 @@ func (c *CsvParser) parseCsvRow(entry []string) ImportCenterResult {
result.Center = domain.Center{
UserReference: userReference,
Name: strings.TrimSpace(entry[nameIndex]),
+ Email: email,
Website: website,
Coordinates: domain.Coordinates{
Longitude: 0,
diff --git a/src/services/mail.go b/src/services/mail.go
new file mode 100644
index 0000000..a29c27c
--- /dev/null
+++ b/src/services/mail.go
@@ -0,0 +1,69 @@
+package services
+
+import (
+ "context"
+ mail "github.com/xhit/go-simple-mail/v2"
+ "net/smtp"
+)
+
+type EmailConfig struct {
+ SmtpHost string
+ SmtpPort int
+ SmtpUser string
+ SmtpPassword string
+ From string
+}
+
+type MailService interface {
+ // SendMail send the mail with the given receiver, subject and body to the configured mail server
+ SendMail(ctx context.Context, receiver, subject, contentType, body string) error
+}
+
+type mailService struct {
+ config EmailConfig
+ auth smtp.Auth
+}
+
+func NewMailService(config EmailConfig) MailService {
+ return &mailService{
+ config: config,
+ auth: smtp.PlainAuth("", config.SmtpUser, config.SmtpPassword, config.SmtpHost),
+ }
+}
+
+func (m *mailService) SendMail(ctx context.Context, receiver, subject, contentType, body string) error {
+ server := mail.NewSMTPClient()
+ server.Host = m.config.SmtpHost
+ server.Port = m.config.SmtpPort
+ server.Username = m.config.SmtpUser
+ server.Password = m.config.SmtpPassword
+ server.Encryption = mail.EncryptionSTARTTLS
+ server.Authentication = mail.AuthLogin
+ server.KeepAlive = true
+
+ smtpClient, err := server.Connect()
+ if err != nil {
+ return err
+ }
+
+ contentTypeArg := mail.TextPlain
+ if contentType == "text/html" {
+ contentTypeArg = mail.TextHTML
+ }
+
+ email := mail.NewMSG()
+ email.SetFrom(m.config.From).
+ AddTo(receiver).
+ SetSubject(subject).
+ SetBody(contentTypeArg, body)
+
+ if email.Error != nil {
+ return err
+ }
+
+ if err := email.Send(smtpClient); err != nil {
+ return err
+ }
+
+ return smtpClient.Quit()
+}