681 lines
20 KiB
Go
681 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/smtp"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Activity struct {
|
|
Name string `json:"name"`
|
|
Icon string `json:"icon"`
|
|
Type string `json:"type"`
|
|
CustomDescription string `json:"customDescription,omitempty"`
|
|
}
|
|
|
|
type SurveyPayload struct {
|
|
Activity string `json:"activity"`
|
|
Activities []Activity `json:"activities"`
|
|
Date string `json:"date"`
|
|
DateType string `json:"dateType"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type Response struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type AvailableDate struct {
|
|
Date string `json:"date"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type AppSettings struct {
|
|
Activities []Activity `json:"activities"`
|
|
AvailableDates []AvailableDate `json:"availableDates"`
|
|
Names []string `json:"names"`
|
|
AdminPassword string `json:"adminPassword"`
|
|
}
|
|
|
|
type PublicSettings struct {
|
|
Activities []Activity `json:"activities"`
|
|
AvailableDates []AvailableDate `json:"availableDates"`
|
|
Names []string `json:"names"`
|
|
}
|
|
|
|
type passwordRequest struct {
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
var settingsMu sync.Mutex
|
|
var sessionsMu sync.Mutex
|
|
var adminSessions = make(map[string]time.Time)
|
|
|
|
func main() {
|
|
loadEnv(".env")
|
|
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
|
|
webDir := os.Getenv("WEB_DIR")
|
|
if webDir == "" {
|
|
webDir = "public"
|
|
}
|
|
|
|
http.HandleFunc("/api/submit", handleSubmit)
|
|
http.HandleFunc("/api/booked-dates", handleBookedDates)
|
|
http.HandleFunc("/api/settings", handlePublicSettings)
|
|
http.HandleFunc("/api/admin/login", handleAdminLogin)
|
|
http.HandleFunc("/api/admin/logout", handleAdminLogout)
|
|
http.HandleFunc("/api/admin/dates", handleAdminDates)
|
|
http.HandleFunc("/admin", handleAdminPage)
|
|
|
|
// Serve static files from webDir directory
|
|
fs := http.FileServer(http.Dir(webDir))
|
|
http.Handle("/", fs)
|
|
|
|
log.Printf("Server starting on port %s (serving static files from %s)...", port, webDir)
|
|
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
|
log.Fatalf("Server failed to start: %v", err)
|
|
}
|
|
}
|
|
|
|
func handleAdminPage(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/admin" && r.URL.Path != "/admin/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
http.ServeFile(w, r, "public/admin.html")
|
|
}
|
|
|
|
func handlePublicSettings(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
settings, err := readSettings()
|
|
if err != nil {
|
|
log.Printf("Error reading app settings: %v", err)
|
|
http.Error(w, "Settings could not be loaded", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, PublicSettings{
|
|
Activities: settings.Activities,
|
|
AvailableDates: availableDatesFromToday(settings.AvailableDates),
|
|
Names: settings.Names,
|
|
})
|
|
}
|
|
|
|
func handleAdminLogin(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var request passwordRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
settings, err := readSettings()
|
|
if err != nil {
|
|
log.Printf("Error reading app settings: %v", err)
|
|
http.Error(w, "Settings could not be loaded", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if settings.AdminPassword == "" || subtle.ConstantTimeCompare([]byte(request.Password), []byte(settings.AdminPassword)) != 1 {
|
|
writeJSON(w, http.StatusUnauthorized, Response{Success: false, Message: "Ungültiges Passwort."})
|
|
return
|
|
}
|
|
|
|
sessionID, err := createAdminSession()
|
|
if err != nil {
|
|
log.Printf("Error creating admin session: %v", err)
|
|
http.Error(w, "Session could not be created", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "date_wizard_admin",
|
|
Value: sessionID,
|
|
Path: "/",
|
|
MaxAge: int((8 * time.Hour).Seconds()),
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
Secure: r.TLS != nil,
|
|
})
|
|
writeJSON(w, http.StatusOK, Response{Success: true, Message: "Angemeldet."})
|
|
}
|
|
|
|
func handleAdminLogout(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
if cookie, err := r.Cookie("date_wizard_admin"); err == nil {
|
|
sessionsMu.Lock()
|
|
delete(adminSessions, cookie.Value)
|
|
sessionsMu.Unlock()
|
|
}
|
|
http.SetCookie(w, &http.Cookie{Name: "date_wizard_admin", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteStrictMode, Secure: r.TLS != nil})
|
|
writeJSON(w, http.StatusOK, Response{Success: true, Message: "Abgemeldet."})
|
|
}
|
|
|
|
func handleAdminDates(w http.ResponseWriter, r *http.Request) {
|
|
if !isAdminRequest(r) {
|
|
writeJSON(w, http.StatusUnauthorized, Response{Success: false, Message: "Anmeldung erforderlich."})
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
settings, err := readSettings()
|
|
if err != nil {
|
|
log.Printf("Error reading app settings: %v", err)
|
|
http.Error(w, "Settings could not be loaded", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, availableDatesFromToday(settings.AvailableDates))
|
|
case http.MethodPost:
|
|
var availableDate AvailableDate
|
|
if err := json.NewDecoder(r.Body).Decode(&availableDate); err != nil || !isValidAvailableDate(availableDate) || !isTodayOrFuture(availableDate.Date) {
|
|
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Bitte wähle ein gültiges Datum ab heute und einen Tagestyp."})
|
|
return
|
|
}
|
|
if err := updateAvailableDates(func(dates []AvailableDate) ([]AvailableDate, error) {
|
|
for _, date := range dates {
|
|
if date.Date == availableDate.Date {
|
|
return nil, fmt.Errorf("date already exists")
|
|
}
|
|
}
|
|
return append(dates, availableDate), nil
|
|
}); err != nil {
|
|
if err.Error() == "date already exists" {
|
|
writeJSON(w, http.StatusConflict, Response{Success: false, Message: "Dieses Datum ist bereits verfügbar."})
|
|
return
|
|
}
|
|
log.Printf("Error adding available date: %v", err)
|
|
http.Error(w, "Date could not be saved", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, Response{Success: true, Message: "Datum hinzugefügt."})
|
|
case http.MethodPut:
|
|
var availableDate AvailableDate
|
|
if err := json.NewDecoder(r.Body).Decode(&availableDate); err != nil || !isValidAvailableDate(availableDate) || !isTodayOrFuture(availableDate.Date) {
|
|
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Bitte wähle ein Datum ab heute und einen gültigen Tagestyp."})
|
|
return
|
|
}
|
|
updated := false
|
|
if err := updateAvailableDates(func(dates []AvailableDate) ([]AvailableDate, error) {
|
|
for index := range dates {
|
|
if dates[index].Date == availableDate.Date {
|
|
dates[index].Type = availableDate.Type
|
|
updated = true
|
|
break
|
|
}
|
|
}
|
|
if !updated {
|
|
return nil, fmt.Errorf("date does not exist")
|
|
}
|
|
return dates, nil
|
|
}); err != nil {
|
|
if err.Error() == "date does not exist" {
|
|
writeJSON(w, http.StatusNotFound, Response{Success: false, Message: "Datum wurde nicht gefunden."})
|
|
return
|
|
}
|
|
log.Printf("Error updating available date: %v", err)
|
|
http.Error(w, "Date could not be saved", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, Response{Success: true, Message: "Tagestyp aktualisiert."})
|
|
case http.MethodDelete:
|
|
var availableDate AvailableDate
|
|
if err := json.NewDecoder(r.Body).Decode(&availableDate); err != nil || !isValidAvailableDate(AvailableDate{Date: availableDate.Date, Type: "short"}) || !isTodayOrFuture(availableDate.Date) {
|
|
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Bitte wähle ein Datum ab heute."})
|
|
return
|
|
}
|
|
deleted := false
|
|
if err := updateAvailableDates(func(dates []AvailableDate) ([]AvailableDate, error) {
|
|
filtered := make([]AvailableDate, 0, len(dates))
|
|
for _, date := range dates {
|
|
if date.Date == availableDate.Date {
|
|
deleted = true
|
|
continue
|
|
}
|
|
filtered = append(filtered, date)
|
|
}
|
|
if !deleted {
|
|
return nil, fmt.Errorf("date does not exist")
|
|
}
|
|
return filtered, nil
|
|
}); err != nil {
|
|
if err.Error() == "date does not exist" {
|
|
writeJSON(w, http.StatusNotFound, Response{Success: false, Message: "Datum wurde nicht gefunden."})
|
|
return
|
|
}
|
|
log.Printf("Error deleting available date: %v", err)
|
|
http.Error(w, "Date could not be saved", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, Response{Success: true, Message: "Datum gelöscht."})
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func getSettingsFilePath() string {
|
|
path := os.Getenv("SETTINGS_FILE")
|
|
if path == "" {
|
|
path = "appsettings.json"
|
|
}
|
|
return path
|
|
}
|
|
|
|
func readSettings() (AppSettings, error) {
|
|
settingsMu.Lock()
|
|
defer settingsMu.Unlock()
|
|
return readSettingsFile()
|
|
}
|
|
|
|
func readSettingsFile() (AppSettings, error) {
|
|
var settings AppSettings
|
|
data, err := os.ReadFile(getSettingsFilePath())
|
|
if err != nil {
|
|
return settings, err
|
|
}
|
|
if err := json.Unmarshal(data, &settings); err != nil {
|
|
return settings, err
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
func updateAvailableDates(update func([]AvailableDate) ([]AvailableDate, error)) error {
|
|
settingsMu.Lock()
|
|
defer settingsMu.Unlock()
|
|
|
|
settings, err := readSettingsFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dates, err := update(settings.AvailableDates)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sort.Slice(dates, func(i, j int) bool { return dates[i].Date < dates[j].Date })
|
|
settings.AvailableDates = dates
|
|
|
|
data, err := json.MarshalIndent(settings, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
temporaryPath := getSettingsFilePath() + ".tmp"
|
|
if err := os.WriteFile(temporaryPath, data, 0600); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(temporaryPath, getSettingsFilePath())
|
|
}
|
|
|
|
func isValidAvailableDate(availableDate AvailableDate) bool {
|
|
if availableDate.Type != "short" && availableDate.Type != "long" {
|
|
return false
|
|
}
|
|
parsedDate, err := time.Parse("2006-01-02", availableDate.Date)
|
|
return err == nil && parsedDate.Format("2006-01-02") == availableDate.Date
|
|
}
|
|
|
|
func isTodayOrFuture(date string) bool {
|
|
return date >= time.Now().Format("2006-01-02")
|
|
}
|
|
|
|
func availableDatesFromToday(dates []AvailableDate) []AvailableDate {
|
|
upcomingDates := make([]AvailableDate, 0, len(dates))
|
|
for _, availableDate := range dates {
|
|
if isTodayOrFuture(availableDate.Date) {
|
|
upcomingDates = append(upcomingDates, availableDate)
|
|
}
|
|
}
|
|
return upcomingDates
|
|
}
|
|
|
|
func createAdminSession() (string, error) {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
sessionID := fmt.Sprintf("%x", bytes)
|
|
sessionsMu.Lock()
|
|
adminSessions[sessionID] = time.Now().Add(8 * time.Hour)
|
|
sessionsMu.Unlock()
|
|
return sessionID, nil
|
|
}
|
|
|
|
func isAdminRequest(r *http.Request) bool {
|
|
cookie, err := r.Cookie("date_wizard_admin")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
sessionsMu.Lock()
|
|
defer sessionsMu.Unlock()
|
|
expiresAt, found := adminSessions[cookie.Value]
|
|
if !found || time.Now().After(expiresAt) {
|
|
delete(adminSessions, cookie.Value)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, value interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(value)
|
|
}
|
|
|
|
func handleSubmit(w http.ResponseWriter, r *http.Request) {
|
|
if enableCORS(w, r) {
|
|
return
|
|
}
|
|
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var payload SurveyPayload
|
|
err := json.NewDecoder(r.Body).Decode(&payload)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(Response{
|
|
Success: false,
|
|
Message: "Ungültige Anfragedaten.",
|
|
})
|
|
return
|
|
}
|
|
|
|
log.Printf("Received survey submission from %s for date %s (%s)", payload.Name, payload.Date, payload.Activity)
|
|
|
|
if err := recordBookedDate(payload.Date); err != nil {
|
|
log.Printf("Error recording booked date: %v", err)
|
|
}
|
|
|
|
err = sendEmail(payload)
|
|
if err != nil {
|
|
log.Printf("Warning: Email could not be sent: %v", err)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(Response{
|
|
Success: true,
|
|
Message: "Reservierung erfolgreich übermittelt!",
|
|
})
|
|
}
|
|
|
|
func handleBookedDates(w http.ResponseWriter, r *http.Request) {
|
|
if enableCORS(w, r) {
|
|
return
|
|
}
|
|
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
dates, err := getBookedDates()
|
|
if err != nil {
|
|
log.Printf("Error reading booked dates: %v", err)
|
|
dates = []string{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(dates)
|
|
}
|
|
|
|
func getBookedDatesFilePath() string {
|
|
path := os.Getenv("BOOKED_DATES_FILE")
|
|
if path == "" {
|
|
path = "booked_dates.json"
|
|
}
|
|
return path
|
|
}
|
|
|
|
func getBookedDates() ([]string, error) {
|
|
filePath := getBookedDatesFilePath()
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return []string{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var rawDates []string
|
|
if err := json.NewDecoder(file).Decode(&rawDates); err != nil {
|
|
return []string{}, nil
|
|
}
|
|
|
|
dates := make([]string, 0, len(rawDates))
|
|
for _, d := range rawDates {
|
|
trimmed := strings.TrimSpace(d)
|
|
if trimmed != "" {
|
|
dates = append(dates, trimmed)
|
|
}
|
|
}
|
|
return dates, nil
|
|
}
|
|
|
|
func recordBookedDate(dateStr string) error {
|
|
trimmedDate := strings.TrimSpace(dateStr)
|
|
if trimmedDate == "" {
|
|
return nil
|
|
}
|
|
|
|
dates, err := getBookedDates()
|
|
if err != nil {
|
|
dates = []string{}
|
|
}
|
|
|
|
for _, d := range dates {
|
|
if d == trimmedDate {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
dates = append(dates, trimmedDate)
|
|
|
|
filePath := getBookedDatesFilePath()
|
|
data, err := json.MarshalIndent(dates, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(filePath, data, 0644)
|
|
}
|
|
|
|
func enableCORS(w http.ResponseWriter, r *http.Request) bool {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusOK)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func sendEmail(payload SurveyPayload) error {
|
|
smtpHost := os.Getenv("SMTP_HOST")
|
|
smtpPort := os.Getenv("SMTP_PORT")
|
|
if smtpPort == "" {
|
|
smtpPort = "587"
|
|
}
|
|
smtpUser := os.Getenv("SMTP_USER")
|
|
smtpPass := os.Getenv("SMTP_PASS")
|
|
toEmail := os.Getenv("TO_EMAIL")
|
|
fromEmail := os.Getenv("FROM_EMAIL")
|
|
if fromEmail == "" {
|
|
fromEmail = smtpUser
|
|
}
|
|
|
|
if smtpHost == "" || toEmail == "" {
|
|
log.Println("HINWEIS: SMTP_HOST oder TO_EMAIL nicht konfiguriert. E-Mail-Versand übersprungen.")
|
|
return nil
|
|
}
|
|
|
|
boundary := fmt.Sprintf("BOUNDARY_%d", time.Now().UnixNano())
|
|
|
|
var bodyBuilder bytes.Buffer
|
|
bodyBuilder.WriteString(fmt.Sprintf("From: %s\r\n", fromEmail))
|
|
bodyBuilder.WriteString(fmt.Sprintf("To: %s\r\n", toEmail))
|
|
bodyBuilder.WriteString(fmt.Sprintf("Subject: Neue Date-Reservierung von %s! 💕\r\n", payload.Name))
|
|
bodyBuilder.WriteString("MIME-Version: 1.0\r\n")
|
|
bodyBuilder.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\r\n", boundary))
|
|
|
|
// Text-Teil der E-Mail
|
|
bodyBuilder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
|
bodyBuilder.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
|
|
bodyBuilder.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
|
|
|
|
bodyBuilder.WriteString("Hallo!\r\n\r\n")
|
|
bodyBuilder.WriteString("Es gibt eine neue Date-Reservierung über deinen Date-Planer:\r\n\r\n")
|
|
bodyBuilder.WriteString(fmt.Sprintf("👤 Name: %s\r\n", payload.Name))
|
|
bodyBuilder.WriteString(fmt.Sprintf("📅 Datum: %s (%s)\r\n", payload.Date, payload.DateType))
|
|
bodyBuilder.WriteString(fmt.Sprintf("💌 Aktivität(en): %s\r\n\r\n", cleanActivityName(payload.Activity)))
|
|
|
|
if len(payload.Activities) > 0 {
|
|
bodyBuilder.WriteString("Details der Aktivitäten:\r\n")
|
|
for _, act := range payload.Activities {
|
|
actName := cleanActivityName(act.Name)
|
|
if act.CustomDescription != "" {
|
|
cleanDesc := cleanActivityName(act.CustomDescription)
|
|
if !strings.Contains(actName, cleanDesc) {
|
|
actName = cleanDesc
|
|
}
|
|
}
|
|
bodyBuilder.WriteString(fmt.Sprintf(" - %s %s (%s)\r\n", act.Icon, actName, act.Type))
|
|
}
|
|
bodyBuilder.WriteString("\r\n")
|
|
}
|
|
|
|
bodyBuilder.WriteString("Im Anhang findest du einen Kalendereintrag (.ics), den du direkt auf deinem Smartphone/Kalender importieren kannst. 💕\r\n\r\n")
|
|
|
|
// iCalendar-Anhang (.ics)
|
|
icsData := generateICS(payload)
|
|
bodyBuilder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
|
bodyBuilder.WriteString("Content-Type: text/calendar; charset=UTF-8; method=REQUEST; name=\"date-event.ics\"\r\n")
|
|
bodyBuilder.WriteString("Content-Disposition: attachment; filename=\"date-event.ics\"\r\n")
|
|
bodyBuilder.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
|
|
bodyBuilder.WriteString(icsData)
|
|
bodyBuilder.WriteString("\r\n")
|
|
|
|
// Abschluss-Boundary
|
|
bodyBuilder.WriteString(fmt.Sprintf("--%s--\r\n", boundary))
|
|
|
|
auth := smtp.PlainAuth("", smtpUser, smtpPass, smtpHost)
|
|
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
|
|
|
|
return smtp.SendMail(addr, auth, fromEmail, []string{toEmail}, bodyBuilder.Bytes())
|
|
}
|
|
|
|
func cleanActivityName(s string) string {
|
|
s = strings.TrimPrefix(s, "Etwas eigenes: ")
|
|
s = strings.TrimPrefix(s, "Etwas eigenes:")
|
|
s = strings.TrimPrefix(s, "Sonstiges: ")
|
|
s = strings.TrimPrefix(s, "Sonstiges:")
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
func generateICS(payload SurveyPayload) string {
|
|
parsedDate, err := time.Parse("2006-01-02", payload.Date)
|
|
dtStart := strings.ReplaceAll(payload.Date, "-", "")
|
|
dtEnd := dtStart
|
|
if err == nil {
|
|
dtStart = parsedDate.Format("20060102")
|
|
dtEnd = parsedDate.AddDate(0, 0, 1).Format("20060102")
|
|
}
|
|
|
|
dtStamp := time.Now().UTC().Format("20060102T150405Z")
|
|
uid := fmt.Sprintf("date-wizard-%s-%d@date-wizard", dtStart, time.Now().UnixNano())
|
|
|
|
activityText := cleanActivityName(payload.Activity)
|
|
for _, act := range payload.Activities {
|
|
if act.CustomDescription != "" {
|
|
cleanDesc := cleanActivityName(act.CustomDescription)
|
|
if !strings.Contains(activityText, cleanDesc) {
|
|
activityText = cleanDesc
|
|
}
|
|
}
|
|
}
|
|
|
|
summary := fmt.Sprintf("Date: %s", activityText)
|
|
description := fmt.Sprintf("Date mit %s\\nAktivität(en): %s", payload.Name, activityText)
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("BEGIN:VCALENDAR\r\n")
|
|
sb.WriteString("VERSION:2.0\r\n")
|
|
sb.WriteString("PRODID:-//Date-Wizard//DE\r\n")
|
|
sb.WriteString("CALSCALE:GREGORIAN\r\n")
|
|
sb.WriteString("METHOD:REQUEST\r\n")
|
|
sb.WriteString("BEGIN:VEVENT\r\n")
|
|
sb.WriteString(fmt.Sprintf("UID:%s\r\n", uid))
|
|
sb.WriteString(fmt.Sprintf("DTSTAMP:%s\r\n", dtStamp))
|
|
sb.WriteString(fmt.Sprintf("DTSTART;VALUE=DATE:%s\r\n", dtStart))
|
|
sb.WriteString(fmt.Sprintf("DTEND;VALUE=DATE:%s\r\n", dtEnd))
|
|
sb.WriteString(fmt.Sprintf("SUMMARY:%s\r\n", summary))
|
|
sb.WriteString(fmt.Sprintf("DESCRIPTION:%s\r\n", description))
|
|
sb.WriteString("STATUS:CONFIRMED\r\n")
|
|
sb.WriteString("END:VEVENT\r\n")
|
|
sb.WriteString("END:VCALENDAR\r\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func loadEnv(filename string) {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) == 2 {
|
|
key := strings.TrimSpace(parts[0])
|
|
val := strings.TrimSpace(parts[1])
|
|
val = strings.Trim(val, `"'`)
|
|
if os.Getenv(key) == "" {
|
|
os.Setenv(key, val)
|
|
}
|
|
}
|
|
}
|
|
}
|