From c38286bcc576f58167ceed11076a7a95b061ffe6 Mon Sep 17 00:00:00 2001 From: Gonne Date: Mon, 19 Sep 2022 14:46:16 +0200 Subject: [PATCH] Speichere Konfiguration in config/config.json --- config/config.go | 88 ++++++++++++++++++++++++++ config/config.json | 33 ++++++++++ controllers/addOfficeHourHandler.go | 12 ++-- controllers/baseHandler.go | 15 ++++- controllers/deleteOfficeHourHandler.go | 4 +- controllers/getHandlers.go | 8 +-- controllers/timetable.go | 18 +++--- main.go | 40 +++++++----- models/date.go | 2 - models/request.go | 2 - repositories/officeHour.go | 5 +- repositories/request.go | 50 ++++++++++----- sqldb/sqldb.go | 28 ++++++-- templates/confirmationMail | 22 +++---- 14 files changed, 249 insertions(+), 78 deletions(-) create mode 100644 config/config.go create mode 100644 config/config.json diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..3b144b6 --- /dev/null +++ b/config/config.go @@ -0,0 +1,88 @@ +package config + +import ( + "encoding/json" + "fmt" + "html/template" + "io/ioutil" + "log" +) + +type Config struct { + Server struct { + ListenAddress string + ListenPort int + Protocol string + Domain string + } + Date struct { + MinuteGranularity int + } + Request struct { + SecretLength int + } + Mailer struct { + Type string + FromAddress string + FromName template.HTML + SmtpHost string + SmtpPort int + SmtpUseAuth bool + SmtpIdentity string + SmtpPassword string + } + SQL struct { + Type string + SQLiteFile string + MysqlUser string + MysqlPassword string + MysqlHost string + MysqlPort int + MysqlDatabase string + } +} + +// ReadConfigFile takes a file path as an argument and attempts to +// unmarshal the content of the file into a struct containing a +// configuration. +func ReadConfigFile(filename string, conf *Config) error { + configData, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + err = json.Unmarshal(configData, conf) + if err != nil { + return err + } + return validateConfig(conf) +} + +func validateConfig(conf *Config) error { + var err error + if !(conf.Server.ListenPort >= 1 && conf.Server.ListenPort <= 65535) { + err = fmt.Errorf("Validating config: Server port must be between 1 and 65535, but is %d.", conf.Server.ListenPort) + log.Println(err.Error()) + } + if !(conf.Server.Protocol == "http" || conf.Server.Protocol == "https") { + err = fmt.Errorf("Validating config: Server protocol must be http or https, but is '%s'.", conf.Server.Protocol) + log.Println(err.Error()) + } + if !(conf.Date.MinuteGranularity >= 1 && conf.Date.MinuteGranularity <= 60) { + err = fmt.Errorf("Validating config: Minute granularity must be between 1 and 60, but is %d.", conf.Date.MinuteGranularity) + log.Println(err.Error()) + } + if !(conf.Request.SecretLength >= 5 && conf.Request.SecretLength <= 50) { + err = fmt.Errorf("Validating config: Requests' secret length must be between 5 and 50, but is %d.", conf.Request.SecretLength) + log.Println(err.Error()) + } + if !(conf.Mailer.Type == "Stdout" || conf.Mailer.Type == "Smtp") { + err = fmt.Errorf("Validating config: Mailer type must be 'stdout' or 'smtp', but is '%s'.", conf.Mailer.Type) + log.Println(err.Error()) + } + if !(conf.SQL.Type == "SQLite" || conf.SQL.Type == "Mysql") { + err = fmt.Errorf("Validating config: SQL type must be 'SQLite' or 'Mysql', but is '%s'.", conf.SQL.Type) + log.Println(err.Error()) + } + return err + +} diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..2e64ad1 --- /dev/null +++ b/config/config.json @@ -0,0 +1,33 @@ +{ + "server": { + "listenAddress": "", + "listenPort": 8080, + "protocol": "https", + "domain": "localhost:8080" + }, + "date": { + "minuteGranularity": 5 + }, + "request": { + "secretLength": 15 + }, + "mailer": { + "type": "Stdout", + "fromAddress": "sprechstunden@localhost", + "fromName": "Mathebau Sprechstunden ", + "smtpHost": "localhost", + "smtpPort": 25, + "smtpUseAuth": false, + "smtpIdentity": "", + "smtpPassword": "" + }, + "SQL": { + "type": "SQLite", + "SQLiteFile": "officeHours.db", + "mysqlUser": "officeHours", + "mysqlPassword": "", + "mysqlHost": "localhost", + "mysqlPort": 3306, + "mysqlDatabase": "officeHours" + } +} \ No newline at end of file diff --git a/controllers/addOfficeHourHandler.go b/controllers/addOfficeHourHandler.go index 3e70f47..7573d9c 100644 --- a/controllers/addOfficeHourHandler.go +++ b/controllers/addOfficeHourHandler.go @@ -77,14 +77,14 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ errors = append(errors, "Die Stunde muss eine ganze Zahl sein.") } if !(hour >= 8 && hour <= 17) { - errors = append(errors, fmt.Sprintf("Sprechstunden müssen zwischen 08:00 Uhr und 17:%d starten.", 60-models.MinuteGranularity)) + errors = append(errors, fmt.Sprintf("Sprechstunden müssen zwischen 08:00 Uhr und 17:%d starten.", 60-b.config.Date.MinuteGranularity)) } minute, err = strconv.Atoi(time[1]) if err != nil { errors = append(errors, "Die Minute muss eine ganze Zahl sein.") } - if !(minute >= 0 && minute <= 60-models.MinuteGranularity && minute%models.MinuteGranularity == 0) { - errors = append(errors, fmt.Sprintf("Sprechstunden dürfen nur alle %d Minuten starten.", models.MinuteGranularity)) + if !(minute >= 0 && minute <= 60-b.config.Date.MinuteGranularity && minute%b.config.Date.MinuteGranularity == 0) { + errors = append(errors, fmt.Sprintf("Sprechstunden dürfen nur alle %d Minuten starten.", b.config.Date.MinuteGranularity)) } } date := models.Date{week, day, hour, minute} @@ -92,8 +92,8 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ if err != nil { errors = append(errors, "Die Dauer muss eine ganze Zahl sein.") } - if !(duration >= models.MinuteGranularity && duration <= 120 && duration%models.MinuteGranularity == 0) { - errors = append(errors, fmt.Sprintf("Sprechstunden müssen zwischen %d und 120 Minuten lang sein.", models.MinuteGranularity)) + if !(duration >= b.config.Date.MinuteGranularity && duration <= 120 && duration%b.config.Date.MinuteGranularity == 0) { + errors = append(errors, fmt.Sprintf("Sprechstunden müssen zwischen %d und 120 Minuten lang sein.", b.config.Date.MinuteGranularity)) } roomname := req.FormValue("raumname") @@ -120,7 +120,7 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ var data maskData = maskData{ courses, rooms, - models.MinuteGranularity, + b.config.Date.MinuteGranularity, courseid, roomid, date, diff --git a/controllers/baseHandler.go b/controllers/baseHandler.go index 6fb38aa..99b217b 100644 --- a/controllers/baseHandler.go +++ b/controllers/baseHandler.go @@ -1,6 +1,9 @@ package controllers -import "sprechstundentool/models" +import ( + "sprechstundentool/config" + "sprechstundentool/models" +) // BaseHandler will hold everything that controller needs type BaseHandler struct { @@ -9,9 +12,15 @@ type BaseHandler struct { courseRepo models.CourseRepository tutorRepo models.TutorRepository requestRepo models.RequestRepository + config config.Config } // NewBaseHandler returns a new BaseHandler -func NewBaseHandler(roomRepo models.RoomRepository, officeHourRepo models.OfficeHourRepository, courseRepo models.CourseRepository, tutorRepo models.TutorRepository, requestRepo models.RequestRepository) *BaseHandler { - return &BaseHandler{roomRepo, officeHourRepo, courseRepo, tutorRepo, requestRepo} +func NewBaseHandler(roomRepo models.RoomRepository, + officeHourRepo models.OfficeHourRepository, + courseRepo models.CourseRepository, + tutorRepo models.TutorRepository, + requestRepo models.RequestRepository, + config config.Config) *BaseHandler { + return &BaseHandler{roomRepo, officeHourRepo, courseRepo, tutorRepo, requestRepo, config} } diff --git a/controllers/deleteOfficeHourHandler.go b/controllers/deleteOfficeHourHandler.go index 41d9be6..391981e 100644 --- a/controllers/deleteOfficeHourHandler.go +++ b/controllers/deleteOfficeHourHandler.go @@ -21,7 +21,7 @@ func (b *BaseHandler) DeleteOfficeHourHandler(w http.ResponseWriter, req *http.R Templates.ExecuteTemplate(w, "deleteSuccess.html", struct{}{}) } else { officeHours, _ := b.officeHourRepo.GetAll(true) - timetable, slots := GetTimetable(officeHours) - b.writeTimetablePage(w, req, printTimetable(timetable, slots, true)) + timetable, slots := b.GetTimetable(officeHours) + b.writeTimetablePage(w, req, b.printTimetable(timetable, slots, true)) } } diff --git a/controllers/getHandlers.go b/controllers/getHandlers.go index d59a73c..8575008 100644 --- a/controllers/getHandlers.go +++ b/controllers/getHandlers.go @@ -20,8 +20,8 @@ func (b *BaseHandler) GetByRoomHandler(w http.ResponseWriter, req *http.Request) return } officeHours, _ := b.officeHourRepo.FindByRoom(room, true) - timetable, slots := GetTimetable(officeHours) - b.writeTimetablePage(w, req, printTimetable(timetable, slots, false)) + timetable, slots := b.GetTimetable(officeHours) + b.writeTimetablePage(w, req, b.printTimetable(timetable, slots, false)) } func (b *BaseHandler) GetByCourseHandler(w http.ResponseWriter, req *http.Request) { @@ -35,8 +35,8 @@ func (b *BaseHandler) GetByCourseHandler(w http.ResponseWriter, req *http.Reques return } officeHours, _ := b.officeHourRepo.FindByCourse(course, true) - timetable, slots := GetTimetable(officeHours) - b.writeTimetablePage(w, req, printTimetable(timetable, slots, false)) + timetable, slots := b.GetTimetable(officeHours) + b.writeTimetablePage(w, req, b.printTimetable(timetable, slots, false)) } func (b *BaseHandler) writeTimetablePage(w http.ResponseWriter, req *http.Request, timetable template.HTML) { diff --git a/controllers/timetable.go b/controllers/timetable.go index 5c15748..d49773c 100644 --- a/controllers/timetable.go +++ b/controllers/timetable.go @@ -8,11 +8,11 @@ import ( "sprechstundentool/models" ) -func GetTimetable(officeHours []models.OfficeHour) (timetable map[models.Date]map[int]models.OfficeHour, slots []int) { +func (b *BaseHandler) GetTimetable(officeHours []models.OfficeHour) (timetable map[models.Date]map[int]models.OfficeHour, slots []int) { var fullTimetable = make(map[models.Date]map[int]models.OfficeHour) for _, officeHour := range officeHours { var slot int = 0 - for minute := 0; minute < officeHour.Duration; minute += models.MinuteGranularity { // find slot id + for minute := 0; minute < officeHour.Duration; minute += b.config.Date.MinuteGranularity { // find slot id _, exists := fullTimetable[models.GetEndDate(officeHour.Date, minute, true)] if exists { _, exists := fullTimetable[models.GetEndDate(officeHour.Date, minute, true)][slot] @@ -25,7 +25,7 @@ func GetTimetable(officeHours []models.OfficeHour) (timetable map[models.Date]ma fullTimetable[models.GetEndDate(officeHour.Date, minute, true)] = make(map[int]models.OfficeHour) } } - for minute := 0; minute < officeHour.Duration; minute += models.MinuteGranularity { // write officeHour id to timetable + for minute := 0; minute < officeHour.Duration; minute += b.config.Date.MinuteGranularity { // write officeHour id to timetable fullTimetable[models.GetEndDate(officeHour.Date, minute, true)][slot] = officeHour } } @@ -47,10 +47,10 @@ func GetTimetable(officeHours []models.OfficeHour) (timetable map[models.Date]ma return fullTimetable, slots } -func printTimetable(timetable map[models.Date]map[int]models.OfficeHour, slots []int, deleteIcons bool) template.HTML { +func (b *BaseHandler) printTimetable(timetable map[models.Date]map[int]models.OfficeHour, slots []int, deleteIcons bool) template.HTML { var tableBody string for hour := 8; hour < 19; hour += 1 { - for minute := 0; minute < 60; minute += models.MinuteGranularity { + for minute := 0; minute < 60; minute += b.config.Date.MinuteGranularity { tableBody += "" if minute == 0 { tableBody += fmt.Sprintf("%d Uhr\n", hour) @@ -65,10 +65,10 @@ func printTimetable(timetable map[models.Date]map[int]models.OfficeHour, slots [ var continued bool = false // is this slot occupied by the same office hour the previous minute? var predecessorExists bool var predecessor models.OfficeHour - if hour > 0 && minute < models.MinuteGranularity { - predecessor, predecessorExists = timetable[models.Date{0, day, hour - 1, 60 - models.MinuteGranularity}][slot] + if hour > 0 && minute < b.config.Date.MinuteGranularity { + predecessor, predecessorExists = timetable[models.Date{0, day, hour - 1, 60 - b.config.Date.MinuteGranularity}][slot] } else { - predecessor, predecessorExists = timetable[models.Date{0, day, hour, minute - models.MinuteGranularity}][slot] + predecessor, predecessorExists = timetable[models.Date{0, day, hour, minute - b.config.Date.MinuteGranularity}][slot] } if predecessorExists { continued = (predecessor == current) @@ -83,7 +83,7 @@ func printTimetable(timetable map[models.Date]map[int]models.OfficeHour, slots [ OfficeHour models.OfficeHour MinuteGranularity int DeleteIcons bool - }{current, models.MinuteGranularity, deleteIcons} + }{current, b.config.Date.MinuteGranularity, deleteIcons} Templates.ExecuteTemplate(&celldata, "td.html", data) tableBody += celldata.String() } diff --git a/main.go b/main.go index 6f99c9a..703f98c 100644 --- a/main.go +++ b/main.go @@ -1,42 +1,47 @@ package main import ( - "database/sql" + "flag" + "fmt" "log" "log/syslog" "net/http" "os" + "sprechstundentool/config" "sprechstundentool/controllers" "sprechstundentool/repositories" "sprechstundentool/sqldb" - "strings" ) func main() { - logwriter, e := syslog.New(syslog.LOG_ERR, "sprechstunden") + logwriter, e := syslog.New(syslog.LOG_ERR, "office hours") if e == nil { log.SetOutput(logwriter) } - - var db *sql.DB - switch os.Getenv("ohtDbType") { - case "mysql": - db = sqldb.ConnectMysql(os.Getenv("ohtDbMysqlConnection")) - default: - if os.Getenv("ohtDbFile") != "" && !strings.Contains(os.Getenv("ohtDbFile"), "/") { - db = sqldb.ConnectSQLite(os.Getenv("ohtDbFile")) - } else { - db = sqldb.ConnectSQLite("sprechstunden.db") - } + configFile := flag.String( + "config", + "config/config.json", + "File path to the configuration file") + flag.Parse() + if *configFile == "" { + flag.Usage() + os.Exit(1) } + var conf config.Config + err := config.ReadConfigFile(*configFile, &conf) + if err != nil { + log.Fatalf("%s: %s", "Reading JSON config file into config structure", err) + } + + db := sqldb.Connect(conf) // Create repos roomRepo := repositories.NewRoomRepo(db) courseRepo := repositories.NewCourseRepo(db) tutorRepo := repositories.NewTutorRepo(db) officeHourRepo := repositories.NewOfficeHourRepo(db, roomRepo, tutorRepo, courseRepo) - requestRepo := repositories.NewRequestRepo(db, officeHourRepo) - h := controllers.NewBaseHandler(roomRepo, officeHourRepo, courseRepo, tutorRepo, requestRepo) + requestRepo := repositories.NewRequestRepo(db, officeHourRepo, conf) + h := controllers.NewBaseHandler(roomRepo, officeHourRepo, courseRepo, tutorRepo, requestRepo, conf) http.HandleFunc("/getByRoom", h.GetByRoomHandler) http.HandleFunc("/getByCourse", h.GetByCourseHandler) @@ -45,5 +50,6 @@ func main() { http.HandleFunc("/deleteOfficeHour", h.DeleteOfficeHourHandler) http.HandleFunc("/", h.RootHandler) - http.ListenAndServe(":8080", nil) + err = http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Server.ListenAddress, conf.Server.ListenPort), nil) + fmt.Println(err.Error()) } diff --git a/models/date.go b/models/date.go index 5667378..65000f9 100644 --- a/models/date.go +++ b/models/date.go @@ -8,8 +8,6 @@ type Date struct { Minute int } -const MinuteGranularity int = 5 - func DayName(day int) string { switch day { case 0: diff --git a/models/request.go b/models/request.go index 13bd002..640428a 100644 --- a/models/request.go +++ b/models/request.go @@ -11,8 +11,6 @@ type Request struct { const RequestActivate int = 1 const RequestDelete int = 2 -const SecretLength int = 15 - type RequestRepository interface { Add(officeHour OfficeHour, action int) (int, error) FindBySecret(secret string) (Request, error) diff --git a/repositories/officeHour.go b/repositories/officeHour.go index 146115c..ff04b82 100644 --- a/repositories/officeHour.go +++ b/repositories/officeHour.go @@ -114,7 +114,10 @@ func (r *OfficeHourRepo) Add(officeHour models.OfficeHour) (id int, err error) { func (r *OfficeHourRepo) Delete(officeHour models.OfficeHour) error { _, err := r.db.Exec("DELETE FROM officeHour WHERE id=?", officeHour.Id) - return fmt.Errorf("Error deleting officeHour from database: %s", err.Error()) + if err != nil { + return fmt.Errorf("Error deleting officeHour from database: %s", err.Error()) + } + return nil } func (r *OfficeHourRepo) getFromRow(row *sql.Row) (models.OfficeHour, error) { diff --git a/repositories/request.go b/repositories/request.go index 3204deb..8e19915 100644 --- a/repositories/request.go +++ b/repositories/request.go @@ -5,8 +5,11 @@ import ( "bytes" "crypto/rand" "database/sql" + "fmt" + "log" "math/big" "net/smtp" + "sprechstundentool/config" "sprechstundentool/controllers" "sprechstundentool/models" ) @@ -14,12 +17,14 @@ import ( type RequestRepo struct { db *sql.DB officeHourRepo models.OfficeHourRepository + config config.Config } -func NewRequestRepo(db *sql.DB, officeHourRepo models.OfficeHourRepository) *RequestRepo { +func NewRequestRepo(db *sql.DB, officeHourRepo models.OfficeHourRepository, config config.Config) *RequestRepo { return &RequestRepo{ db: db, officeHourRepo: officeHourRepo, + config: config, } } @@ -68,8 +73,8 @@ func (r *RequestRepo) Add(officeHour models.OfficeHour, action int) (int, error) * but don't insert new request into database. */ for _, request := range existents { - if request.Action == action { // already covered by selection && request.OfficeHour == officeHour { - return request.Id, sendConfirmationMail(request) + if request.Action == action { // already covered by selection: && request.OfficeHour == officeHour { + return request.Id, r.sendConfirmationMail(request) } } secret, err := r.newSecret() @@ -85,7 +90,7 @@ func (r *RequestRepo) Add(officeHour models.OfficeHour, action int) (int, error) if err != nil { return request.Id, err } - return request.Id, sendConfirmationMail(request) + return request.Id, r.sendConfirmationMail(request) } func (r *RequestRepo) Execute(request models.Request) error { @@ -104,29 +109,42 @@ func (r *RequestRepo) Execute(request models.Request) error { } func (r *RequestRepo) newSecret() (string, error) { - secret := randomString(models.SecretLength) - - _, err := r.FindBySecret(secret) - if err != nil && err != sql.ErrNoRows { - return "", err - } + var err error + var secret string // find unused secret for err != sql.ErrNoRows { - secret = randomString(models.SecretLength) + secret = randomString(r.config.Request.SecretLength) _, err = r.FindBySecret(secret) + if err != nil && err != sql.ErrNoRows { + return "", err + } } return secret, nil } -func sendConfirmationMail(request models.Request) error { - to := []string{request.OfficeHour.Tutor.Email} +func (r *RequestRepo) sendConfirmationMail(request models.Request) error { var message bytes.Buffer - err := controllers.Templates.ExecuteTemplate(&message, "confirmationMail", request) + var data = struct { + Config config.Config + Request models.Request + }{r.config, request} + err := controllers.Templates.ExecuteTemplate(&message, "confirmationMail", data) if err != nil { + log.Printf("Error parsing confirmation Mail: %s", err.Error()) return err } - err = smtp.SendMail("192.168.0.24:25", nil, "Mathebau Sprechstunden ", to, message.Bytes()) - return err + switch r.config.Mailer.Type { + case "Stdout": + fmt.Println(message.String()) + case "Smtp": + to := []string{request.OfficeHour.Tutor.Email} + var auth smtp.Auth + if r.config.Mailer.SmtpUseAuth { + auth = smtp.PlainAuth(r.config.Mailer.SmtpIdentity, r.config.Mailer.FromAddress, r.config.Mailer.SmtpPassword, r.config.Mailer.SmtpHost) + } + return smtp.SendMail(fmt.Sprintf("%s:%d", r.config.Mailer.SmtpHost, r.config.Mailer.SmtpPort), auth, string(r.config.Mailer.FromName), to, message.Bytes()) + } + return nil } func randomString(n int) string { diff --git a/sqldb/sqldb.go b/sqldb/sqldb.go index c53c804..e556870 100644 --- a/sqldb/sqldb.go +++ b/sqldb/sqldb.go @@ -2,13 +2,31 @@ package sqldb import ( "database/sql" + "fmt" "log" + "sprechstundentool/config" _ "github.com/go-sql-driver/mysql" _ "github.com/mattn/go-sqlite3" ) -func ConnectSQLite(file string) *sql.DB { +func Connect(config config.Config) *sql.DB { + switch config.SQL.Type { + case "SQLite": + return connectSQLite(config.SQL.SQLiteFile) + case "Mysql": + return connectMysql(config.SQL.MysqlUser, + config.SQL.MysqlPassword, + config.SQL.MysqlHost, + config.SQL.MysqlPort, + config.SQL.MysqlDatabase) + default: + log.Fatal("Type of database not recognised.") + } + return nil +} + +func connectSQLite(file string) *sql.DB { db, err := sql.Open("sqlite3", file) if err != nil { log.Fatal(err) @@ -17,16 +35,16 @@ func ConnectSQLite(file string) *sql.DB { return db } -func ConnectMysql(connection string) *sql.DB { - db, err := sql.Open("mysql", connection) +func connectMysql(user string, password string, address string, port int, database string) *sql.DB { + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", user, password, address, port, database)) if err != nil { - log.Fatal(err) + log.Fatalf("Error connecting to database: %s", err) } err = db.Ping() // handle error if err != nil { - log.Fatal(err) + log.Fatalf("Error pinging database: %s", err) } return db } diff --git a/templates/confirmationMail b/templates/confirmationMail index 72596d3..6a14e50 100644 --- a/templates/confirmationMail +++ b/templates/confirmationMail @@ -1,19 +1,19 @@ -From: Mathebau Sprechstunden -To: {{.OfficeHour.Tutor.Email}} -Subject: Sprechstunde {{if eq .Action 1}}anlegen{{end}}{{if eq .Action 2}}löschen{{end}} +From: {{.Config.Mailer.FromName}} +To: {{.Request.OfficeHour.Tutor.Email}} +Subject: Sprechstunde {{if eq .Request.Action 1}}anlegen{{end}}{{if eq .Request.Action 2}}löschen{{end}} -Hallo {{.OfficeHour.Tutor.Name}}, +Hallo {{.Request.OfficeHour.Tutor.Name}}, -mit deiner Emailadresse soll eine Sprechstunde mit folgenden Daten {{if eq .Action 1}}angelegt werden{{end}}{{if eq .Action 2}}gelöscht werden{{end}}: +mit deiner Emailadresse soll eine Sprechstunde mit folgenden Daten {{if eq .Request.Action 1}}angelegt werden{{end}}{{if eq .Request.Action 2}}gelöscht werden{{end}}: -{{.OfficeHour.Course.Name}} -{{DayName .OfficeHour.Date.Day}} -{{printf "%02d" .OfficeHour.Date.Hour}}:{{printf "%02d" .OfficeHour.Date.Minute}} Uhr bis {{printf "%02d" .OfficeHour.EndDate.Hour}}:{{printf "%02d" .OfficeHour.EndDate.Minute}} Uhr -{{.OfficeHour.Tutor.Name}} -{{.OfficeHour.Room.Name}} +{{.Request.OfficeHour.Course.Name}} +{{DayName .Request.OfficeHour.Date.Day}} +{{printf "%02d" .Request.OfficeHour.Date.Hour}}:{{printf "%02d" .Request.OfficeHour.Date.Minute}} Uhr bis {{printf "%02d" .Request.OfficeHour.EndDate.Hour}}:{{printf "%02d" .Request.OfficeHour.EndDate.Minute}} Uhr +{{.Request.OfficeHour.Tutor.Name}} +{{.Request.OfficeHour.Room.Name}} Falls dies richtig ist, so bestätige die Sprechstunde durch Abrufen der folgenden URL: -https://sprechstunden.mathebau.de/confirmRequest?code={{.Secret}} +{{.Config.Server.Protocol}}://{{.Config.Server.Domain}}/confirmRequest?code={{.Request.Secret}} Solltest du diese Email nicht erwartet haben, so kannst du sie einfach ignorieren. Deine Fachschaft Mathematik \ No newline at end of file