Speichere Konfiguration in config/config.json

This commit is contained in:
Gonne 2022-09-19 14:46:16 +02:00
parent 43b3631da2
commit c38286bcc5
14 changed files with 249 additions and 78 deletions

88
config/config.go Normal file
View File

@ -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
}

33
config/config.json Normal file
View File

@ -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 <sprechstunden@localhost>",
"smtpHost": "localhost",
"smtpPort": 25,
"smtpUseAuth": false,
"smtpIdentity": "",
"smtpPassword": ""
},
"SQL": {
"type": "SQLite",
"SQLiteFile": "officeHours.db",
"mysqlUser": "officeHours",
"mysqlPassword": "",
"mysqlHost": "localhost",
"mysqlPort": 3306,
"mysqlDatabase": "officeHours"
}
}

View File

@ -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,

View File

@ -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}
}

View File

@ -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))
}
}

View File

@ -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) {

View File

@ -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 += "<tr>"
if minute == 0 {
tableBody += fmt.Sprintf("<td>%d Uhr</td>\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()
}

40
main.go
View File

@ -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())
}

View File

@ -8,8 +8,6 @@ type Date struct {
Minute int
}
const MinuteGranularity int = 5
func DayName(day int) string {
switch day {
case 0:

View File

@ -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)

View File

@ -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) {

View File

@ -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 <sprechstunden@mathebau.de>", 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 {

View File

@ -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
}

View File

@ -1,19 +1,19 @@
From: Mathebau Sprechstunden <sprechstunden@mathebau.de>
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