Initial project setup for a privacy-focused, self-hosted scheduling app: - Go backend with JWT auth, SQLite storage, and chi router - Calendar integrations via Google OAuth (FreeBusy), CalDAV, and ICS links - Public booking pages with configurable slots and booking requests - AES-256-GCM encryption for stored provider tokens - Vue 3 + TypeScript frontend with Vite, Pinia, and Vue Router - Docs, .gitignore, and .env.example for local development
93 lines
2.8 KiB
Go
93 lines
2.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
)
|
|
|
|
func (s *Server) ListBookings(w http.ResponseWriter, r *http.Request) {
|
|
bookings, err := s.DB.BookingsForUser(userFrom(r).ID)
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "Anfragen nicht lesbar")
|
|
return
|
|
}
|
|
out := make([]BookingJSON, 0, len(bookings))
|
|
for _, b := range bookings {
|
|
out = append(out, bookingToJSON(b))
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (s *Server) AcceptBooking(w http.ResponseWriter, r *http.Request) {
|
|
user := userFrom(r)
|
|
id, err := pathID(r)
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, "Ungültige ID")
|
|
return
|
|
}
|
|
booking, err := s.DB.BookingByID(id, user.ID)
|
|
if err != nil {
|
|
jsonError(w, http.StatusNotFound, "Anfrage nicht gefunden")
|
|
return
|
|
}
|
|
if booking.Status != "pending" {
|
|
jsonError(w, http.StatusConflict, "Diese Anfrage wurde bereits bearbeitet")
|
|
return
|
|
}
|
|
// Kollision mit bestehenden busy-Zeiträumen prüfen (angommene Buchungen
|
|
// und Kalendertermine liegen beide in busy_slots).
|
|
busy, err := s.DB.BusyForUser(user.ID, booking.StartTs-1, booking.EndTs+1)
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "Belegung nicht prüfbar")
|
|
return
|
|
}
|
|
for _, b := range busy {
|
|
if b.BookingID.Valid && b.BookingID.Int64 == booking.ID {
|
|
continue
|
|
}
|
|
if b.StartTs < booking.EndTs && booking.StartTs < b.EndTs {
|
|
jsonError(w, http.StatusConflict, "Der Zeitraum kollidiert inzwischen mit einem anderen Termin")
|
|
return
|
|
}
|
|
}
|
|
if err := s.DB.InsertBusyForBooking(booking); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "Termin konnte nicht eingetragen werden")
|
|
return
|
|
}
|
|
if err := s.DB.SetBookingStatus(booking.ID, "accepted"); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "Status konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
booking.Status = "accepted"
|
|
writeJSON(w, http.StatusOK, bookingToJSON(booking))
|
|
}
|
|
|
|
func (s *Server) DeclineBooking(w http.ResponseWriter, r *http.Request) {
|
|
user := userFrom(r)
|
|
id, err := pathID(r)
|
|
if err != nil {
|
|
jsonError(w, http.StatusBadRequest, "Ungültige ID")
|
|
return
|
|
}
|
|
booking, err := s.DB.BookingByID(id, user.ID)
|
|
if err != nil {
|
|
jsonError(w, http.StatusNotFound, "Anfrage nicht gefunden")
|
|
return
|
|
}
|
|
if booking.Status == "pending" {
|
|
if err := s.DB.SetBookingStatus(booking.ID, "declined"); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "Status konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
} else if booking.Status == "accepted" {
|
|
if err := s.DB.DeleteBusyByBooking(booking.ID); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "Termin konnte nicht entfernt werden")
|
|
return
|
|
}
|
|
if err := s.DB.SetBookingStatus(booking.ID, "declined"); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "Status konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
}
|
|
booking.Status = "declined"
|
|
writeJSON(w, http.StatusOK, bookingToJSON(booking))
|
|
}
|