// request package repositories import ( "bytes" "crypto/rand" "database/sql" "errors" "fmt" "github.com/wneessen/go-mail" "log" "math/big" "officeHours/config" "officeHours/models" "officeHours/templating" ) type RequestRepo struct { db *sql.DB officeHourRepo models.OfficeHourRepository config config.Config } func NewRequestRepo(db *sql.DB, officeHourRepo models.OfficeHourRepository, config config.Config) *RequestRepo { return &RequestRepo{ db: db, officeHourRepo: officeHourRepo, config: config, } } 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{}, fmt.Errorf("SQL-error scanning request row: %w", 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, fmt.Errorf("SQL-error selecting requests by office hour: %w", 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 { err = fmt.Errorf("error scanning request row: %w", err) log.Println(err.Error()) return requests, err } request.OfficeHour, err = r.officeHourRepo.FindById(officeHourId) if err != nil { return requests, err } requests = append(requests, request) } return requests, nil } // Add a request to the database if it doesnt already exist. // Send a mail with the secret to the confirmation address in any case. func (r *RequestRepo) Add(officeHour models.OfficeHour, action models.RequestAction) (int, error) { existents, err := r.FindByOfficeHour(officeHour) if err != nil && !errors.Is(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, r.sendConfirmationMail(request) } } secret, err := r.newSecret() if err != nil { return 0, err } request := models.Request{Id: 0, OfficeHour: officeHour, Action: action, Secret: secret} _, err = r.db.Exec("INSERT INTO `request` (officeHour, action, secret) VALUES (?,?,?)", officeHour.Id, action, secret) if err != nil { return 0, fmt.Errorf("SQL-error inserting new request: %w", err) } request, err = r.FindBySecret(secret) if err != nil { return request.Id, err } return request.Id, r.sendConfirmationMail(request) } // Execute a request and delete it. 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 officeHour=?", request.OfficeHour.Id) case models.RequestDelete: r.db.Exec("DELETE FROM request WHERE officeHour=?", request.OfficeHour.Id) err = r.officeHourRepo.Delete(request.OfficeHour) default: log.Printf("executing request: Action type %d unknown.", request.Action) _, err = r.db.Exec("DELETE FROM request WHERE id=?", request.Id) } return err } // Find a new secret token with configured length that is currently unused. func (r *RequestRepo) newSecret() (string, error) { var err error var secret string // find unused secret for !errors.Is(err, sql.ErrNoRows) { secret = randomString(r.config.Request.SecretLength) _, err = r.FindBySecret(secret) if err != nil && !errors.Is(err, sql.ErrNoRows) { return "", err } } return secret, nil } func (r *RequestRepo) sendConfirmationMail(request models.Request) error { var messageText bytes.Buffer var data = struct { Config config.Config Request models.Request }{r.config, request} err := templating.WriteTemplate(&messageText, "confirmationMail", data) if err != nil { err = fmt.Errorf("error parsing confirmation Mail: %w", err) log.Println(err.Error()) return err } switch r.config.Mailer.Type { case "Stdout": fmt.Println(messageText.String()) case "Smtp": message := mail.NewMsg() if err := message.From(r.config.Mailer.FromAddress); err != nil { log.Println(err.Error()) return err } if err := message.To(request.OfficeHour.Tutor.Email); err != nil { log.Println(err.Error()) return err } switch request.Action { case models.RequestActivate: message.Subject("Sprechstunde anlegen") case models.RequestDelete: message.Subject("Sprechstunde löschen") } message.SetBodyString(mail.TypeTextPlain, messageText.String()) var options []mail.Option options = append(options, mail.WithPort(r.config.Mailer.SmtpPort)) if r.config.Mailer.SmtpUseAuth { options = append(options, mail.WithSMTPAuth(mail.SMTPAuthPlain)) options = append(options, mail.WithUsername(r.config.Mailer.SmtpIdentity)) options = append(options, mail.WithPassword(r.config.Mailer.SmtpPassword)) } client, err := mail.NewClient(r.config.Mailer.SmtpHost, options...) if err != nil { log.Println(err.Error()) return err } if err := client.DialAndSend(message); err != nil { log.Println(err.Error()) return err } } return nil } 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, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) if err != nil { log.Printf("Error getting random position in randomString(n int): %s", err.Error()) } s[i] = letters[position.Int64()] } return string(s) }