2022-09-05 15:55:08 +00:00
|
|
|
|
// request
|
|
|
|
|
package repositories
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"database/sql"
|
2022-09-21 20:24:08 +00:00
|
|
|
|
"errors"
|
2022-09-19 12:46:16 +00:00
|
|
|
|
"fmt"
|
2022-09-26 12:17:25 +00:00
|
|
|
|
"html/template"
|
2022-09-19 12:46:16 +00:00
|
|
|
|
"log"
|
2022-09-05 15:55:08 +00:00
|
|
|
|
"math/big"
|
|
|
|
|
"net/smtp"
|
2022-09-20 10:21:01 +00:00
|
|
|
|
"officeHours/config"
|
|
|
|
|
"officeHours/models"
|
2022-09-24 13:01:33 +00:00
|
|
|
|
"officeHours/templating"
|
2022-09-05 15:55:08 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type RequestRepo struct {
|
|
|
|
|
db *sql.DB
|
|
|
|
|
officeHourRepo models.OfficeHourRepository
|
2022-09-19 12:46:16 +00:00
|
|
|
|
config config.Config
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-19 12:46:16 +00:00
|
|
|
|
func NewRequestRepo(db *sql.DB, officeHourRepo models.OfficeHourRepository, config config.Config) *RequestRepo {
|
2022-09-05 15:55:08 +00:00
|
|
|
|
return &RequestRepo{
|
|
|
|
|
db: db,
|
|
|
|
|
officeHourRepo: officeHourRepo,
|
2022-09-19 12:46:16 +00:00
|
|
|
|
config: config,
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2022-09-28 10:00:45 +00:00
|
|
|
|
return models.Request{}, fmt.Errorf("SQL-error scanning request row: %w", err)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
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 {
|
2022-09-28 10:00:45 +00:00
|
|
|
|
return nil, fmt.Errorf("SQL-error selecting requests by office hour: %w", err)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
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 {
|
2022-09-21 20:24:08 +00:00
|
|
|
|
err = fmt.Errorf("Error scanning request row: %w", err)
|
|
|
|
|
log.Println(err.Error())
|
2022-09-05 15:55:08 +00:00
|
|
|
|
return requests, err
|
|
|
|
|
}
|
2022-09-05 18:10:35 +00:00
|
|
|
|
request.OfficeHour, err = r.officeHourRepo.FindById(officeHourId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return requests, err
|
|
|
|
|
}
|
2022-09-05 15:55:08 +00:00
|
|
|
|
requests = append(requests, request)
|
|
|
|
|
}
|
|
|
|
|
return requests, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-04 20:15:38 +00:00
|
|
|
|
// Add a request to the database if it doesnt already exist.
|
|
|
|
|
// Send a mail with the secret to the confirmation address in any case.
|
2023-01-04 12:06:13 +00:00
|
|
|
|
func (r *RequestRepo) Add(officeHour models.OfficeHour, action models.RequestAction) (int, error) {
|
2022-09-05 15:55:08 +00:00
|
|
|
|
existents, err := r.FindByOfficeHour(officeHour)
|
2022-09-21 20:24:08 +00:00
|
|
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
2022-09-05 15:55:08 +00:00
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
/* Resend confirmation mail if identical request exists,
|
|
|
|
|
* but don't insert new request into database.
|
|
|
|
|
*/
|
|
|
|
|
for _, request := range existents {
|
2022-09-19 12:46:16 +00:00
|
|
|
|
if request.Action == action { // already covered by selection: && request.OfficeHour == officeHour {
|
|
|
|
|
return request.Id, r.sendConfirmationMail(request)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
secret, err := r.newSecret()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
2022-09-19 16:51:51 +00:00
|
|
|
|
request := models.Request{Id: 0, OfficeHour: officeHour, Action: action, Secret: secret}
|
2022-09-05 15:55:08 +00:00
|
|
|
|
_, err = r.db.Exec("INSERT INTO `request` (officeHour, action, secret) VALUES (?,?,?)", officeHour.Id, action, secret)
|
|
|
|
|
if err != nil {
|
2022-09-28 10:00:45 +00:00
|
|
|
|
return 0, fmt.Errorf("SQL-error inserting new request: %w", err)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
request, err = r.FindBySecret(secret)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return request.Id, err
|
|
|
|
|
}
|
2022-09-19 12:46:16 +00:00
|
|
|
|
return request.Id, r.sendConfirmationMail(request)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-04 20:15:38 +00:00
|
|
|
|
// Execute a request and delete it.
|
2022-09-05 15:55:08 +00:00
|
|
|
|
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)
|
2022-09-07 16:26:05 +00:00
|
|
|
|
r.db.Exec("DELETE FROM request WHERE officeHour=?", request.OfficeHour.Id)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
case models.RequestDelete:
|
2022-09-21 20:24:08 +00:00
|
|
|
|
err = r.officeHourRepo.Delete(request.OfficeHour)
|
2022-09-07 16:26:05 +00:00
|
|
|
|
r.db.Exec("DELETE FROM request WHERE officeHour=?", request.OfficeHour.Id)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
default:
|
2022-11-04 20:15:38 +00:00
|
|
|
|
log.Printf("Executing request: Action type %d unknown.", request.Action)
|
2022-09-21 20:24:08 +00:00
|
|
|
|
_, err = r.db.Exec("DELETE FROM request WHERE id=?", request.Id)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-04 20:15:38 +00:00
|
|
|
|
// Find a new secret token with configured length that is currently unused.
|
2022-09-05 15:55:08 +00:00
|
|
|
|
func (r *RequestRepo) newSecret() (string, error) {
|
2022-09-19 12:46:16 +00:00
|
|
|
|
var err error
|
|
|
|
|
var secret string
|
2022-09-05 15:55:08 +00:00
|
|
|
|
// find unused secret
|
2022-09-21 20:24:08 +00:00
|
|
|
|
for !errors.Is(err, sql.ErrNoRows) {
|
2022-09-19 12:46:16 +00:00
|
|
|
|
secret = randomString(r.config.Request.SecretLength)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
_, err = r.FindBySecret(secret)
|
2022-09-24 12:17:36 +00:00
|
|
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
2022-09-19 12:46:16 +00:00
|
|
|
|
return "", err
|
|
|
|
|
}
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
return secret, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-19 12:46:16 +00:00
|
|
|
|
func (r *RequestRepo) sendConfirmationMail(request models.Request) error {
|
2022-09-05 15:55:08 +00:00
|
|
|
|
var message bytes.Buffer
|
2022-09-19 12:46:16 +00:00
|
|
|
|
var data = struct {
|
2022-09-26 12:17:25 +00:00
|
|
|
|
Config config.Config
|
|
|
|
|
Request models.Request
|
|
|
|
|
MessageId template.HTML
|
|
|
|
|
}{r.config, request, template.HTML("<" + randomString(15) + "@" + r.config.Server.Domain + ">")}
|
2022-09-24 13:01:33 +00:00
|
|
|
|
err := templating.WriteTemplate(&message, "confirmationMail", data)
|
2022-09-05 15:55:08 +00:00
|
|
|
|
if err != nil {
|
2022-09-21 20:24:08 +00:00
|
|
|
|
err = fmt.Errorf("Error parsing confirmation Mail: %w", err)
|
|
|
|
|
log.Println(err.Error())
|
2022-09-05 15:55:08 +00:00
|
|
|
|
return err
|
|
|
|
|
}
|
2022-09-24 13:01:33 +00:00
|
|
|
|
|
2022-09-19 12:46:16 +00:00
|
|
|
|
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)
|
|
|
|
|
}
|
2022-09-21 20:24:08 +00:00
|
|
|
|
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
|
2022-09-19 12:46:16 +00:00
|
|
|
|
}
|
|
|
|
|
return nil
|
2022-09-05 15:55:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2022-09-21 20:24:08 +00:00
|
|
|
|
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())
|
|
|
|
|
}
|
2022-09-05 15:55:08 +00:00
|
|
|
|
s[i] = letters[position.Int64()]
|
|
|
|
|
}
|
|
|
|
|
return string(s)
|
|
|
|
|
}
|