dana-medical-site/internal/api/share.go
2026-10-14 13:33:13 +00:00

104 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"net/http"
"strings"
"time"
"dana/internal/storage"
)
// Ссылка «только просмотр» на дашборд клиники для человека без входа в админку (маркетолога).
// Токен стоит в адресе после «#»: эта часть не уходит на сервер, в журналы и в Referer.
// Страница передаёт его в заголовке Authorization — Caddy этот заголовок в журнал не пишет.
// В базе хранится только SHA-256 токена; новый выпуск отключает прежнюю ссылку.
const sharePagePath = "/admin/share.html"
func shareTokenHash(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
func (a *API) adminShareLink(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
clinic, valid := requestClinic(w, r)
if !valid {
return
}
info, err := a.store.ShareLinkInfo(r.Context(), clinic.Key)
if err != nil {
handleErr(w, err)
return
}
ok(w, info, nil)
}
// adminCreateShareLink выпускает новую ссылку; токен отдаётся только в этом ответе.
func (a *API) adminCreateShareLink(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
clinic, valid := requestClinic(w, r)
if !valid {
return
}
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
handleErr(w, err)
return
}
token := base64.RawURLEncoding.EncodeToString(b)
if err := a.store.SetShareLink(r.Context(), clinic.Key, shareTokenHash(token), u.ID); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "create", "share_link_"+clinic.Key, 0, clientIP(r))
w.Header().Set("Cache-Control", "no-store")
ok(w, map[string]any{"path": sharePagePath + "#" + token}, nil)
}
func (a *API) adminDeleteShareLink(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
clinic, valid := requestClinic(w, r)
if !valid {
return
}
if err := a.store.DeleteShareLink(r.Context(), clinic.Key); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "delete", "share_link_"+clinic.Key, 0, clientIP(r))
ok(w, map[string]any{"clinic": clinic.Key}, nil)
}
// shareWeeklyReports — дашборд клиники по ссылке: те же периоды, что во вкладке «По неделям»,
// только чтение. Данных пациентов там нет — только итоги по каналам и колл-центру.
func (a *API) shareWeeklyReports(w http.ResponseWriter, r *http.Request) {
if a.tooMany(w, "share:"+clientIP(r), 60, time.Minute) {
return
}
w.Header().Set("Cache-Control", "no-store")
token := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
if len(token) < 40 || len(token) > 64 {
fail(w, http.StatusUnauthorized, "share_invalid", "Ссылка недействительна")
return
}
key, err := a.store.ShareLinkClinic(r.Context(), shareTokenHash(token))
clinic := clinicByKey[key]
if errors.Is(err, storage.ErrNotFound) || (err == nil && clinic == nil) {
fail(w, http.StatusUnauthorized, "share_invalid", "Ссылка недействительна или её отключили — попросите новую")
return
}
if err != nil {
handleErr(w, err)
return
}
list, err := a.store.WeeklyReports(r.Context(), clinic.Key)
if err != nil {
handleErr(w, err)
return
}
ok(w, list, map[string]any{"clinic": clinic.Key, "title": clinic.Title})
}