104 lines
3.6 KiB
Go
104 lines
3.6 KiB
Go
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})
|
||
}
|