176 lines
6.1 KiB
Go
176 lines
6.1 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/concurrency"
|
||
|
|
"data-terminal-cloud/internal/sharing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func CreateSharingGrantHandler(service *sharing.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, ok := webPrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
key := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
||
|
|
if len(key) < 16 || len(key) > 128 {
|
||
|
|
writeValidation(w, r, "缺少有效的重复请求识别码", FieldError{Field: "Idempotency-Key", Message: "长度必须为16至128字符"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body struct {
|
||
|
|
ResourceType string `json:"resource_type"`
|
||
|
|
ResourceID string `json:"resource_id"`
|
||
|
|
GranteeUserID string `json:"grantee_user_id"`
|
||
|
|
Permission string `json:"permission"`
|
||
|
|
Purpose string `json:"purpose"`
|
||
|
|
ExpiresAt time.Time `json:"expires_at"`
|
||
|
|
LoginRequired *bool `json:"login_required"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &body, "分享授权参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
loginRequired := true
|
||
|
|
if body.LoginRequired != nil {
|
||
|
|
loginRequired = *body.LoginRequired
|
||
|
|
}
|
||
|
|
input := sharing.CreateInput{ResourceType: body.ResourceType, ResourceID: body.ResourceID, GranteeUserID: body.GranteeUserID, Permission: body.Permission, Purpose: body.Purpose, ExpiresAt: body.ExpiresAt, LoginRequired: loginRequired}
|
||
|
|
if !validUUID(input.ResourceID) || !validUUID(input.GranteeUserID) || input.ExpiresAt.IsZero() {
|
||
|
|
writeValidation(w, r, "分享授权参数不正确", FieldError{Field: "grant", Message: "资源、被分享用户和到期时间均为必填"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, created, err := service.Create(r.Context(), principal, key, RequestID(r.Context()), input)
|
||
|
|
if err != nil {
|
||
|
|
writeSharingError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
status := http.StatusCreated
|
||
|
|
if !created {
|
||
|
|
status = http.StatusOK
|
||
|
|
w.Header().Set("Idempotency-Replayed", "true")
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, status, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ListSharingGrantsHandler(service *sharing.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, ok := webPrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
subjectID := strings.TrimSpace(r.URL.Query().Get("subject_id"))
|
||
|
|
if !validUUID(subjectID) {
|
||
|
|
writeValidation(w, r, "分享授权查询参数不正确", FieldError{Field: "subject_id", Message: "必须是有效UUID"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := service.List(r.Context(), principal, subjectID)
|
||
|
|
if err != nil {
|
||
|
|
writeSharingError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func RevokeSharingGrantHandler(service *sharing.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.Revoke(r.Context(), principal, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSharingError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func AccessSharedResourceHandler(service *sharing.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.Access(r.Context(), principal, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSharingError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func AccessPublicSharedResourceHandler(service *sharing.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
||
|
|
token := strings.TrimSpace(r.Header.Get("X-Share-Token"))
|
||
|
|
if !validUUID(id) || len(token) < 32 || len(token) > 256 {
|
||
|
|
writeValidation(w, r, "分享链接参数不正确", FieldError{Field: "share", Message: "分享编号或访问口令无效"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.AccessByToken(r.Context(), id, token, RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSharingError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func EditSharedStoryHandler(service *sharing.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
expected, err := concurrency.ParseIfMatch(r)
|
||
|
|
if err != nil {
|
||
|
|
writeETagError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var input struct {
|
||
|
|
Content string `json:"content"`
|
||
|
|
Reason string `json:"reason"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &input, "共享故事修改参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.EditStory(r.Context(), principal, r.PathValue("id"), input.Content, input.Reason, RequestID(r.Context()), expected)
|
||
|
|
if err != nil {
|
||
|
|
writeSharingError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("ETag", concurrency.FormatETag(item.Version))
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func writeSharingError(w http.ResponseWriter, r *http.Request, err error) {
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, sharing.ErrInvalid):
|
||
|
|
writeValidation(w, r, "分享授权参数不正确", FieldError{Field: "grant", Message: "资源类型、权限、用途或到期时间不符合要求"})
|
||
|
|
case errors.Is(err, sharing.ErrAccessDenied):
|
||
|
|
WriteError(w, r, http.StatusForbidden, "SHARING_ACCESS_DENIED", "当前账号没有创建、管理或访问该分享的权限")
|
||
|
|
case errors.Is(err, sharing.ErrExpired):
|
||
|
|
WriteError(w, r, http.StatusGone, "SHARING_GRANT_INACTIVE", "分享已经撤销或过期")
|
||
|
|
case errors.Is(err, sharing.ErrNotFound):
|
||
|
|
WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "分享授权或目标资源不存在")
|
||
|
|
case errors.Is(err, sharing.ErrVersionConflict):
|
||
|
|
WriteError(w, r, http.StatusPreconditionFailed, "VERSION_CONFLICT", "共享资源已经被其他用户修改,请刷新后重试")
|
||
|
|
default:
|
||
|
|
WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "分享授权处理失败")
|
||
|
|
}
|
||
|
|
}
|