82 lines
2.6 KiB
Go
82 lines
2.6 KiB
Go
package api
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"crypto/subtle"
|
||
"encoding/json"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
const maxOnecEventBody = 8 << 10
|
||
|
||
func (a *API) WithOneCWebhook(key string) { a.onecHook = strings.TrimSpace(key) }
|
||
func (a *API) onecAutonomousEnabled() bool { return a.onecHook != "" }
|
||
|
||
type onecEvent struct {
|
||
Event string `json:"event"`
|
||
DoctorID string `json:"doctor_id"`
|
||
AppointmentID string `json:"appointment_id"`
|
||
Status string `json:"status"`
|
||
BranchID string `json:"branch_id"`
|
||
Start *time.Time `json:"start"`
|
||
End *time.Time `json:"end"`
|
||
}
|
||
|
||
func sameKey(got, want string) bool {
|
||
g, w := sha256.Sum256([]byte(got)), sha256.Sum256([]byte(want))
|
||
return subtle.ConstantTimeCompare(g[:], w[:]) == 1
|
||
}
|
||
func (a *API) onecEvents(w http.ResponseWriter, r *http.Request) {
|
||
if !a.onecAutonomousEnabled() {
|
||
fail(w, 404, "not_found", "Не найдено")
|
||
return
|
||
}
|
||
ip := clientIP(r)
|
||
if a.tooMany(w, "onec-ev:"+ip, 300, time.Minute) {
|
||
return
|
||
}
|
||
if !a.checkOnecKey(r) {
|
||
if a.tooMany(w, "onec-bad:"+ip, 20, 10*time.Minute) {
|
||
return
|
||
}
|
||
fail(w, 401, "unauthorized", "Неверный ключ")
|
||
return
|
||
}
|
||
var ev onecEvent
|
||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxOnecEventBody)).Decode(&ev); err != nil {
|
||
fail(w, 400, "bad_json", "Не удалось разобрать событие")
|
||
return
|
||
}
|
||
switch ev.Event {
|
||
case "schedule_changed", "directory_changed":
|
||
if err := a.store.RequestOnecSnapshot(r.Context()); err != nil {
|
||
handleErr(w, err)
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusAccepted, envelope{Data: map[string]any{"ok": true, "queued": "push"}})
|
||
case "appointment_status":
|
||
if ev.AppointmentID == "" || (ev.Status != "booked" && ev.Status != "arrived" && ev.Status != "cancelled") {
|
||
fail(w, 400, "validation", "Нужны appointment_id и status: booked, arrived или cancelled")
|
||
return
|
||
}
|
||
if (ev.Start == nil) != (ev.End == nil) || (ev.Start != nil && !ev.Start.Before(*ev.End)) {
|
||
fail(w, 400, "validation", "Некорректный интервал события")
|
||
return
|
||
}
|
||
_, _, known, err := a.store.ApplyOnecAppointmentEvent(r.Context(), ev.AppointmentID, ev.Status, ev.DoctorID, ev.BranchID, ev.Start, ev.End)
|
||
if err != nil {
|
||
handleErr(w, err)
|
||
return
|
||
}
|
||
if err = a.store.RequestOnecSnapshot(r.Context()); err != nil {
|
||
handleErr(w, err)
|
||
return
|
||
}
|
||
ok(w, map[string]any{"ok": true, "known": known}, nil)
|
||
default:
|
||
fail(w, 400, "validation", "Неизвестное событие: допустимы schedule_changed, directory_changed, appointment_status")
|
||
}
|
||
}
|