package httpapi import ( "crypto/rand" "encoding/base64" "net/http" "strings" "time" "golang.org/x/crypto/bcrypt" "wannpassts/internal/db" ) const tokenTTL = 7 * 24 * time.Hour func (s *Server) Register(w http.ResponseWriter, r *http.Request) { var in struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name"` Timezone string `json:"timezone"` } if err := decodeBody(r, &in); err != nil { jsonError(w, http.StatusBadRequest, "Anfrage unlesbar") return } in.Email = strings.ToLower(strings.TrimSpace(in.Email)) in.Name = strings.TrimSpace(in.Name) if !validEmail(in.Email) { jsonError(w, http.StatusBadRequest, "Bitte eine gültige E-Mail-Adresse angeben") return } if len(in.Password) < 8 || len(in.Password) > 200 { jsonError(w, http.StatusBadRequest, "Das Passwort muss 8–200 Zeichen lang sein") return } if in.Name == "" || len(in.Name) > 80 { jsonError(w, http.StatusBadRequest, "Bitte einen Namen (max. 80 Zeichen) angeben") return } tz := in.Timezone if tz == "" { tz = "Europe/Berlin" } if _, err := time.LoadLocation(tz); err != nil { jsonError(w, http.StatusBadRequest, "Unbekannte Zeitzone") return } if _, err := s.DB.UserByEmail(in.Email); err == nil { jsonError(w, http.StatusConflict, "Diese E-Mail-Adresse ist bereits registriert") return } hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), 10) if err != nil { jsonError(w, http.StatusInternalServerError, "Passwort konnte nicht verarbeitet werden") return } slug, err := s.uniqueSlug() if err != nil { jsonError(w, http.StatusInternalServerError, "Interner Fehler") return } u := &db.User{Email: in.Email, Name: in.Name, PasswordHash: string(hash), Slug: slug, Timezone: tz} if err := s.DB.CreateUser(u); err != nil { jsonError(w, http.StatusInternalServerError, "Konto konnte nicht erstellt werden") return } token, err := s.signJWT(u.ID, tokenTTL) if err != nil { jsonError(w, http.StatusInternalServerError, "Token konnte nicht erzeugt werden") return } writeJSON(w, http.StatusCreated, map[string]any{"token": token, "user": userToJSON(u)}) } func (s *Server) Login(w http.ResponseWriter, r *http.Request) { var in struct { Email string `json:"email"` Password string `json:"password"` } if err := decodeBody(r, &in); err != nil { jsonError(w, http.StatusBadRequest, "Anfrage unlesbar") return } u, err := s.DB.UserByEmail(strings.ToLower(strings.TrimSpace(in.Email))) if err != nil { jsonError(w, http.StatusUnauthorized, "E-Mail oder Passwort falsch") return } if bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(in.Password)) != nil { jsonError(w, http.StatusUnauthorized, "E-Mail oder Passwort falsch") return } token, err := s.signJWT(u.ID, tokenTTL) if err != nil { jsonError(w, http.StatusInternalServerError, "Token konnte nicht erzeugt werden") return } writeJSON(w, http.StatusOK, map[string]any{"token": token, "user": userToJSON(u)}) } func (s *Server) Me(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"user": userToJSON(userFrom(r))}) } func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) { u := userFrom(r) var in struct { Name *string `json:"name"` Timezone *string `json:"timezone"` SlotMinutes *int `json:"slot_minutes"` DayStartMin *int `json:"day_start_min"` DayEndMin *int `json:"day_end_min"` HorizonDays *int `json:"horizon_days"` Durations []int `json:"durations"` Weekdays []int `json:"weekdays"` } if err := decodeBody(r, &in); err != nil { jsonError(w, http.StatusBadRequest, "Anfrage unlesbar") return } if in.Name != nil { name := strings.TrimSpace(*in.Name) if name == "" || len(name) > 80 { jsonError(w, http.StatusBadRequest, "Name muss 1–80 Zeichen haben") return } u.Name = name } if in.Timezone != nil { if _, err := time.LoadLocation(*in.Timezone); err != nil { jsonError(w, http.StatusBadRequest, "Unbekannte Zeitzone") return } u.Timezone = *in.Timezone } if in.SlotMinutes != nil { if *in.SlotMinutes < 5 || *in.SlotMinutes > 180 { jsonError(w, http.StatusBadRequest, "Slot-Größe muss 5–180 Minuten sein") return } u.SlotMinutes = *in.SlotMinutes } if in.DayStartMin != nil { if *in.DayStartMin < 0 || *in.DayStartMin > 1439 { jsonError(w, http.StatusBadRequest, "Tagesbeginn ungültig") return } u.DayStartMin = *in.DayStartMin } if in.DayEndMin != nil { if *in.DayEndMin < 1 || *in.DayEndMin > 1440 { jsonError(w, http.StatusBadRequest, "Tagesende ungültig") return } u.DayEndMin = *in.DayEndMin } if u.DayStartMin >= u.DayEndMin { jsonError(w, http.StatusBadRequest, "Tagesbeginn muss vor dem Tagesende liegen") return } if in.HorizonDays != nil { if *in.HorizonDays < 1 || *in.HorizonDays > 120 { jsonError(w, http.StatusBadRequest, "Buchungshorizont muss 1–120 Tage sein") return } u.HorizonDays = *in.HorizonDays } if in.Durations != nil { durations, err := cleanDurations(in.Durations) if err != nil { jsonError(w, http.StatusBadRequest, err.Error()) return } u.Durations = intsCSV(durations) } if in.Weekdays != nil { weekdays, err := cleanWeekdays(in.Weekdays) if err != nil { jsonError(w, http.StatusBadRequest, err.Error()) return } u.Weekdays = intsCSV(weekdays) } if err := s.DB.UpdateUserSettings(u); err != nil { jsonError(w, http.StatusInternalServerError, "Speichern fehlgeschlagen") return } writeJSON(w, http.StatusOK, map[string]any{"user": userToJSON(u)}) } func (s *Server) RegenSlug(w http.ResponseWriter, r *http.Request) { u := userFrom(r) slug, err := s.uniqueSlug() if err != nil { jsonError(w, http.StatusInternalServerError, "Interner Fehler") return } if err := s.DB.UpdateUserSlug(u.ID, slug); err != nil { jsonError(w, http.StatusInternalServerError, "Speichern fehlgeschlagen") return } u.Slug = slug writeJSON(w, http.StatusOK, map[string]any{"user": userToJSON(u)}) } func (s *Server) uniqueSlug() (string, error) { for i := 0; i < 8; i++ { b := make([]byte, 9) if _, err := rand.Read(b); err != nil { return "", err } slug := base64.RawURLEncoding.EncodeToString(b) exists, err := s.DB.SlugExists(slug) if err != nil { return "", err } if !exists { return slug, nil } } return "", http.ErrBodyNotAllowed } func validEmail(s string) bool { if len(s) < 5 || len(s) > 254 || strings.Count(s, "@") != 1 { return false } at := strings.IndexByte(s, '@') local, domain := s[:at], s[at+1:] if local == "" || !strings.Contains(domain, ".") || strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") { return false } return !strings.ContainsAny(s, " \t") } func cleanDurations(list []int) ([]int, error) { if len(list) == 0 || len(list) > 8 { return nil, errNew("1–8 Dauerangaben benötigt") } seen := map[int]bool{} var out []int for _, v := range list { if v < 5 || v > 480 { return nil, errNew("Dauern müssen 5–480 Minuten sein") } if !seen[v] { seen[v] = true out = append(out, v) } } for i := 1; i < len(out); i++ { for j := i; j > 0 && out[j] < out[j-1]; j-- { out[j], out[j-1] = out[j-1], out[j] } } return out, nil } func cleanWeekdays(list []int) ([]int, error) { if len(list) == 0 || len(list) > 7 { return nil, errNew("Mindestens ein Buchungstag nötig") } seen := map[int]bool{} var out []int for _, v := range list { if v < 1 || v > 7 { return nil, errNew("Wochentage müssen 1 (Mo) – 7 (So) sein") } if !seen[v] { seen[v] = true out = append(out, v) } } for i := 1; i < len(out); i++ { for j := i; j > 0 && out[j] < out[j-1]; j-- { out[j], out[j-1] = out[j-1], out[j] } } return out, nil } func errNew(msg string) error { return &simpleError{msg} } type simpleError struct{ msg string } func (e *simpleError) Error() string { return e.msg }