sprechstunden-go/repositories/request.go

182 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// request
package repositories
import (
"bytes"
"crypto/rand"
"database/sql"
"errors"
"fmt"
"html/template"
"log"
"math/big"
"net/smtp"
"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 int) (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:
err = r.officeHourRepo.Delete(request.OfficeHour)
r.db.Exec("DELETE FROM request WHERE officeHour=?", request.OfficeHour.Id)
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 message bytes.Buffer
var data = struct {
Config config.Config
Request models.Request
MessageId template.HTML
}{r.config, request, template.HTML("<" + randomString(15) + "@" + r.config.Server.Domain + ">")}
err := templating.WriteTemplate(&message, "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(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)
}
err = smtp.SendMail(fmt.Sprintf("%s:%d", r.config.Mailer.SmtpHost, r.config.Mailer.SmtpPort), auth, string(r.config.Mailer.FromName), to, message.Bytes())
if err != nil {
err = fmt.Errorf("Error sending mail by smtp: %w", err)
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)
}