From 78af58a51d72fe1313e09fa988231ec73ff3a1d3 Mon Sep 17 00:00:00 2001 From: Gonne Date: Mon, 5 Sep 2022 17:55:08 +0200 Subject: [PATCH] =?UTF-8?q?Sprechstunden=20hinzuf=C3=BCgen=20und=20durch?= =?UTF-8?q?=20einen=20E-Mail-Link=20best=C3=A4tigen=20lassen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/addOfficeHourHandlers.go | 17 +++- controllers/baseHandler.go | 5 +- controllers/confirmRequestHandler.go | 24 +++++ controllers/getHandlers.go | 5 +- main.go | 5 +- models/date.go | 17 ++++ models/officeHour.go | 2 +- models/request.go | 20 ++++ models/tutor.go | 2 +- repositories/officeHour.go | 14 +-- repositories/request.go | 141 +++++++++++++++++++++++++++ repositories/tutor.go | 11 ++- sqlite.sql | 4 +- templates/addSuccess.html | 10 ++ templates/confirmationMail | 20 ++++ templates/executeFailure.html | 10 ++ templates/index.html | 5 +- templates/requestNotFound.html | 8 ++ 18 files changed, 291 insertions(+), 29 deletions(-) create mode 100644 controllers/confirmRequestHandler.go create mode 100644 models/request.go create mode 100644 repositories/request.go create mode 100644 templates/addSuccess.html create mode 100644 templates/confirmationMail create mode 100644 templates/executeFailure.html create mode 100644 templates/requestNotFound.html diff --git a/controllers/addOfficeHourHandlers.go b/controllers/addOfficeHourHandlers.go index 9cc9ac0..089abcd 100644 --- a/controllers/addOfficeHourHandlers.go +++ b/controllers/addOfficeHourHandlers.go @@ -142,17 +142,21 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ false, duration, } - err := b.officeHourRepo.Add(officeHour) + id, err := b.officeHourRepo.Add(officeHour) if err != nil { w.WriteHeader(http.StatusInternalServerError) failureTemplate, parseErr := template.ParseFiles("templates/addFailure.html") if parseErr != nil { - w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", string(err.Error())))) + w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", err.Error()))) return } failureTemplate.Execute(w, err) + return } - http.Redirect(w, req, "/", http.StatusTemporaryRedirect) + officeHour, _ = b.officeHourRepo.FindById(id) + b.requestRepo.Add(officeHour, models.RequestActivate) + tmpl, _ := template.ParseFiles("template/addSuccess.html") + tmpl.Execute(w, struct{}{}) } } @@ -160,15 +164,18 @@ func (b *BaseHandler) writeAddOfficeHourMask(w http.ResponseWriter, req *http.Re tmpl, err := template.ParseFiles("templates/addMask.html") if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", string(err.Error())))) + w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", err.Error()))) return } if len(data.Errors) != 0 { w.WriteHeader(http.StatusBadRequest) } + if req.Method == http.MethodGet { + data.Errors = []string{} + } err = tmpl.Execute(w, data) if err != nil { - w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", string(err.Error())))) + w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", err.Error()))) return } } diff --git a/controllers/baseHandler.go b/controllers/baseHandler.go index 2e14d95..6fb38aa 100644 --- a/controllers/baseHandler.go +++ b/controllers/baseHandler.go @@ -8,9 +8,10 @@ type BaseHandler struct { officeHourRepo models.OfficeHourRepository courseRepo models.CourseRepository tutorRepo models.TutorRepository + requestRepo models.RequestRepository } // NewBaseHandler returns a new BaseHandler -func NewBaseHandler(roomRepo models.RoomRepository, officeHourRepo models.OfficeHourRepository, courseRepo models.CourseRepository, tutorRepo models.TutorRepository) *BaseHandler { - return &BaseHandler{roomRepo, officeHourRepo, courseRepo, tutorRepo} +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} } diff --git a/controllers/confirmRequestHandler.go b/controllers/confirmRequestHandler.go new file mode 100644 index 0000000..d5ea2e6 --- /dev/null +++ b/controllers/confirmRequestHandler.go @@ -0,0 +1,24 @@ +package controllers + +import ( + "html/template" + "net/http" +) + +func (b *BaseHandler) ConfirmRequestHandler(w http.ResponseWriter, req *http.Request) { + secret := req.FormValue("code") + request, err := b.requestRepo.FindBySecret(secret) + + if err != nil { + w.WriteHeader(http.StatusNotFound) + tmpl, _ := template.ParseFiles("templates/requestNotFound.html") + tmpl.Execute(w, struct{}{}) + } + + err = b.requestRepo.Execute(request) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + tmpl, err := template.ParseFiles("templates/executeFailure.html") + tmpl.Execute(w, err) + } +} diff --git a/controllers/getHandlers.go b/controllers/getHandlers.go index 160fdc7..6ca0560 100644 --- a/controllers/getHandlers.go +++ b/controllers/getHandlers.go @@ -16,7 +16,6 @@ func (b *BaseHandler) GetByRoomHandler(w http.ResponseWriter, req *http.Request) roomId, _ := strconv.Atoi(req.FormValue("raum")) room, err := b.roomRepo.FindById(roomId) if err != nil { - fmt.Println(err) b.RootHandler(w, req) return } @@ -53,12 +52,12 @@ func (b *BaseHandler) writeTimetablePage(w http.ResponseWriter, req *http.Reques tmpl, err := template.ParseFiles("templates/index.html") if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", string(err.Error())))) + w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", err.Error()))) return } err = tmpl.Execute(w, data) if err != nil { - w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", string(err.Error())))) + w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", err.Error()))) return } } diff --git a/main.go b/main.go index e47336f..7afd916 100644 --- a/main.go +++ b/main.go @@ -15,12 +15,13 @@ func main() { courseRepo := repositories.NewCourseRepo(db) tutorRepo := repositories.NewTutorRepo(db) officeHourRepo := repositories.NewOfficeHourRepo(db, roomRepo, tutorRepo, courseRepo) - - h := controllers.NewBaseHandler(roomRepo, officeHourRepo, courseRepo, tutorRepo) + requestRepo := repositories.NewRequestRepo(db, officeHourRepo) + h := controllers.NewBaseHandler(roomRepo, officeHourRepo, courseRepo, tutorRepo, requestRepo) http.HandleFunc("/getByRoom", h.GetByRoomHandler) http.HandleFunc("/getByCourse", h.GetByCourseHandler) http.HandleFunc("/addOfficeHour", h.AddOfficeHourHandler) + http.HandleFunc("/confirmRequest", h.ConfirmRequestHandler) http.HandleFunc("/", h.RootHandler) http.ListenAndServe(":8080", nil) diff --git a/models/date.go b/models/date.go index f737735..3b04b83 100644 --- a/models/date.go +++ b/models/date.go @@ -9,3 +9,20 @@ type Date struct { } const MinuteGranularity int = 5 + +func DayName(day int) string { + switch day { + case 0: + return "Montag" + case 1: + return "Dienstag" + case 2: + return "Mittwoch" + case 3: + return "Donnerstag" + case 4: + return "Freitag" + default: + return "" + } +} diff --git a/models/officeHour.go b/models/officeHour.go index 0c81f94..eccce3a 100644 --- a/models/officeHour.go +++ b/models/officeHour.go @@ -18,5 +18,5 @@ type OfficeHourRepository interface { FindByRoom(room Room, activatedOnly bool) ([]OfficeHour, error) GetAll(activatedOnly bool) ([]OfficeHour, error) Delete(officeHour OfficeHour) error - Add(officeHour OfficeHour) error + Add(officeHour OfficeHour) (int, error) } diff --git a/models/request.go b/models/request.go new file mode 100644 index 0000000..13bd002 --- /dev/null +++ b/models/request.go @@ -0,0 +1,20 @@ +// request +package models + +type Request struct { + Id int + OfficeHour OfficeHour + Action int + Secret string +} + +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) + Execute(request Request) error +} diff --git a/models/tutor.go b/models/tutor.go index 81fa828..7cf8295 100644 --- a/models/tutor.go +++ b/models/tutor.go @@ -12,5 +12,5 @@ type TutorRepository interface { FindByNameAndEmail(name string, email string) (Tutor, error) FindById(Id int) (Tutor, error) GetAll() ([]Tutor, error) - Add(tutor Tutor) error + Add(tutor Tutor) (int, error) } diff --git a/repositories/officeHour.go b/repositories/officeHour.go index 05bd3b4..f332a43 100644 --- a/repositories/officeHour.go +++ b/repositories/officeHour.go @@ -72,19 +72,18 @@ func (r *OfficeHourRepo) FindById(id int) (models.OfficeHour, error) { } -func (r *OfficeHourRepo) Add(officeHour models.OfficeHour) error { +func (r *OfficeHourRepo) Add(officeHour models.OfficeHour) (id int, err error) { // Find correct tutor or add if not existent r.tutorRepo.Add(officeHour.Tutor) - var err error officeHour.Tutor, err = r.tutorRepo.FindByNameAndEmail(officeHour.Tutor.Name, officeHour.Tutor.Email) if err != nil { - return err + return 0, err } //Don't add identical officeHours officeHours, err := r.FindByCourse(officeHour.Course, false) if err != nil { - return err + return 0, err } for _, oldOfficeHour := range officeHours { if officeHour.Tutor == oldOfficeHour.Tutor && @@ -94,11 +93,11 @@ func (r *OfficeHourRepo) Add(officeHour models.OfficeHour) error { officeHour.Info == oldOfficeHour.Info && officeHour.Active == oldOfficeHour.Active && officeHour.Duration == oldOfficeHour.Duration { - return nil + return officeHour.Id, nil } } - _, err = r.db.Exec("INSERT INTO `officeHour` (tutor, day, hour, minute, room, course, week, info, active, duration) VALUES (?,?,?,?,?,?,?,?,?,?)", + sqlResult, err := r.db.Exec("INSERT INTO `officeHour` (tutor, day, hour, minute, room, course, week, info, active, duration) VALUES (?,?,?,?,?,?,?,?,?,?)", officeHour.Tutor.Id, officeHour.Date.Day, officeHour.Date.Hour, @@ -109,7 +108,8 @@ func (r *OfficeHourRepo) Add(officeHour models.OfficeHour) error { officeHour.Info, officeHour.Active, officeHour.Duration) - return err + id64, _ := sqlResult.LastInsertId() + return int(id64), err } func (r *OfficeHourRepo) Delete(officeHour models.OfficeHour) error { diff --git a/repositories/request.go b/repositories/request.go new file mode 100644 index 0000000..115545d --- /dev/null +++ b/repositories/request.go @@ -0,0 +1,141 @@ +// request +package repositories + +import ( + "bytes" + "crypto/rand" + "database/sql" + "html/template" + "math/big" + "net/smtp" + "sprechstundentool/models" +) + +type RequestRepo struct { + db *sql.DB + officeHourRepo models.OfficeHourRepository +} + +func NewRequestRepo(db *sql.DB, officeHourRepo models.OfficeHourRepository) *RequestRepo { + return &RequestRepo{ + db: db, + officeHourRepo: officeHourRepo, + } +} + +func (r *RequestRepo) FindBySecret(secret string) (models.Request, error) { + // This query is not safe against timing sidechannel attacks – I don't care. + row := r.db.QueryRow("SELECT * FROM request WHERE secret=?", secret) + var request models.Request + var officeHourId int + err := row.Scan(&request.Id, &officeHourId, &request.Action, &request.Secret) + if err != nil { + return models.Request{}, err + } + request.OfficeHour, err = r.officeHourRepo.FindById(officeHourId) + return request, err +} + +func (r *RequestRepo) FindByOfficeHour(officeHour models.OfficeHour) ([]models.Request, error) { + rows, err := r.db.Query("SELECT * FROM request WHERE officeHour=?", officeHour.Id) + if err != nil { + return nil, err + } + defer rows.Close() + + var requests []models.Request + for rows.Next() { + var request models.Request + var officeHourId int + if err := rows.Scan(&request.Id, &officeHourId, &request.Action, &request.Secret); err != nil { + return requests, err + } + requests = append(requests, request) + } + return requests, nil +} + +func (r *RequestRepo) Add(officeHour models.OfficeHour, action int) (int, error) { + existents, err := r.FindByOfficeHour(officeHour) + if err != nil && err != sql.ErrNoRows { + return 0, err + } + /* Resend confirmation mail if identical request exists, + * 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) + } + } + secret, err := r.newSecret() + if err != nil { + return 0, err + } + request := models.Request{0, officeHour, action, secret} + _, err = r.db.Exec("INSERT INTO `request` (officeHour, action, secret) VALUES (?,?,?)", officeHour.Id, action, secret) + if err != nil { + return 0, err + } + + request, err = r.FindBySecret(secret) + if err != nil { + return request.Id, err + } + return request.Id, sendConfirmationMail(request) +} + +func (r *RequestRepo) Execute(request models.Request) error { + var err error + switch request.Action { + case models.RequestActivate: + _, err = r.db.Exec("UPDATE officeHour SET active=true WHERE id=?", request.OfficeHour.Id) + r.db.Exec("DELETE FROM request WHERE id=?", request.Id) + case models.RequestDelete: + _, err = r.db.Exec("DELETE FROM officeHour WHERE id=?", request.OfficeHour.Id) + r.db.Exec("DELETE FROM request WHERE id=?", request.Id) + default: + r.db.Exec("DELETE FROM request WHERE id=?", request.Id) + } + return err +} + +func (r *RequestRepo) newSecret() (string, error) { + secret := randomString(models.SecretLength) + + _, err := r.FindBySecret(secret) + if err != nil && err != sql.ErrNoRows { + return "", err + } + // find unused secret + for err != sql.ErrNoRows { + secret = randomString(models.SecretLength) + _, err = r.FindBySecret(secret) + } + return secret, nil +} + +func sendConfirmationMail(request models.Request) error { + to := []string{request.OfficeHour.Tutor.Email} + tmpl, err := template.New("confirmationMail").Funcs(template.FuncMap{"DayName": models.DayName}).ParseFiles("templates/confirmationMail") + var message bytes.Buffer + err = tmpl.Execute(&message, request) + if err != nil { + return err + } + err = smtp.SendMail("192.168.0.24:25", nil, "Mathebau Sprechstunden ", to, message.Bytes()) + return err +} + +func randomString(n int) string { + // []byte would be faster and also work, but []rune keeps working for UTF8 characters + // if someone would like them. + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + + s := make([]rune, n) + for i := range s { + position, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + s[i] = letters[position.Int64()] + } + return string(s) +} diff --git a/repositories/tutor.go b/repositories/tutor.go index a6bc950..ef49861 100644 --- a/repositories/tutor.go +++ b/repositories/tutor.go @@ -72,13 +72,14 @@ func (r *TutorRepo) GetAll() ([]models.Tutor, error) { func (r *TutorRepo) Save(tutor models.Tutor) error { return nil } -func (r *TutorRepo) Add(tutor models.Tutor) error { +func (r *TutorRepo) Add(tutor models.Tutor) (int, error) { //Don't add identical tutors - _, err := r.FindByNameAndEmail(tutor.Name, tutor.Email) + existentTutor, err := r.FindByNameAndEmail(tutor.Name, tutor.Email) if err == sql.ErrNoRows { - _, err = r.db.Exec("INSERT INTO `tutor` (name, email) VALUES (?,?);", tutor.Name, tutor.Email) - return err + sqlResult, err := r.db.Exec("INSERT INTO `tutor` (name, email) VALUES (?,?)", tutor.Name, tutor.Email) + id, _ := sqlResult.LastInsertId() + return int(id), err } - return err + return existentTutor.Id, err } diff --git a/sqlite.sql b/sqlite.sql index d4569e9..e6cedef 100644 --- a/sqlite.sql +++ b/sqlite.sql @@ -17,8 +17,8 @@ DROP TABLE IF EXISTS `request`; CREATE TABLE `request` ( `id` INTEGER PRIMARY KEY, `officeHour` int DEFAULT NULL, - `type` int DEFAULT NULL, - `code` text DEFAULT NULL + `action` int DEFAULT NULL, + `secret` text DEFAULT NULL ); -- diff --git a/templates/addSuccess.html b/templates/addSuccess.html new file mode 100644 index 0000000..c5c3ce6 --- /dev/null +++ b/templates/addSuccess.html @@ -0,0 +1,10 @@ + + + Sprechstunde anlegen + + + Die Sprechstunde wurde angelegt. Du solltest eine Mail mit einem Aktivierungslink erhalten haben. +
+ {{.}} + + \ No newline at end of file diff --git a/templates/confirmationMail b/templates/confirmationMail new file mode 100644 index 0000000..bf314e7 --- /dev/null +++ b/templates/confirmationMail @@ -0,0 +1,20 @@ +To: {{.OfficeHour.Tutor.Email}} + +Subject: Sprechstunde anlegen + + +Hallo {{.OfficeHour.Tutor.Name}}, + +mit deiner Emailadresse soll eine Sprechstunde mit folgenden Daten angelegt werden: + +{{.OfficeHour.Course.Name}} +{{DayName .OfficeHour.Date.Day}} +ab {{.OfficeHour.Date.Hour}}:{{printf "%02d" .OfficeHour.Date.Minute}} Uhr +{{.OfficeHour.Duration}} Minuten +{{.OfficeHour.Tutor.Name}} + +Wenn diese Daten richtig sind, so bestätige die Sprechstunde durch Abrufen der folgenden URL: +https://sprechstunden.mathebau.de/confirmRequest?code={{.Secret}} +Solltest du diese Email nicht erwartet haben, so kannst du sie einfach ignorieren. + +Deine Fachschaft Mathematik \ No newline at end of file diff --git a/templates/executeFailure.html b/templates/executeFailure.html new file mode 100644 index 0000000..7c20ab0 --- /dev/null +++ b/templates/executeFailure.html @@ -0,0 +1,10 @@ + + + Anfrage ausführen fehlgeschlagen + + + Irgendetwas ist schief gegangen. Bitte sende folgende Daten an sprechstundentool@mathebau.de mit einer Beschreibung, was du tun wolltest. +
+ {{.}} + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index f2609b4..6931aca 100644 --- a/templates/index.html +++ b/templates/index.html @@ -22,6 +22,9 @@ {{.Timetable}} - Technische Fragen an sprechstundentool@mathebau.de + \ No newline at end of file diff --git a/templates/requestNotFound.html b/templates/requestNotFound.html new file mode 100644 index 0000000..7c655a8 --- /dev/null +++ b/templates/requestNotFound.html @@ -0,0 +1,8 @@ + + + Anfrage bestätigen fehlgeschlagen + + + Dieser Bestätigungscode ist nicht verfügbar. + + \ No newline at end of file