refactor: update api and client to reference humanIds
This commit is contained in:
+249
-67
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/flowy-live/llink/internal/auth"
|
||||
@@ -39,12 +38,10 @@ func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc net
|
||||
// Response DTOs
|
||||
|
||||
type Human struct {
|
||||
// Id will be nil if this human is not registered
|
||||
Id *string `json:"id"`
|
||||
Id string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
EmailPrefix string `json:"email_prefix"`
|
||||
// CreatedAt will be nil if this human is not registered
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
@@ -89,6 +86,20 @@ type MembersRequest struct {
|
||||
Emails []string `json:"emails"`
|
||||
}
|
||||
|
||||
type Invitation struct {
|
||||
NetworkId string `json:"network_id"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type AcceptInvitationRequest struct {
|
||||
NetworkId string `json:"network_id"`
|
||||
}
|
||||
|
||||
type RevokeInvitationRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// Depot DTOs
|
||||
|
||||
type PrepareUploadRequest struct {
|
||||
@@ -162,7 +173,19 @@ func (h *Handler) SignIn(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.authSvc.VerifySignInCode(r.Context(), req.Email, req.Code)
|
||||
// Look up human first so we can store humanId in the session
|
||||
hum, err := h.humanSvc.GetByEmail(r.Context(), req.Email)
|
||||
if err != nil {
|
||||
if errors.Is(err, human.ErrNotFound) {
|
||||
http.Error(w, "human not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
slog.Error("failed to get human for sign-in", "error", err, "email", req.Email)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.authSvc.VerifySignInCode(r.Context(), req.Email, req.Code, hum.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrInvalidCode) {
|
||||
http.Error(w, "invalid code", http.StatusUnauthorized)
|
||||
@@ -173,14 +196,6 @@ func (h *Handler) SignIn(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the human
|
||||
hum, err := h.humanSvc.GetByEmail(r.Context(), req.Email)
|
||||
if err != nil {
|
||||
slog.Error("failed to get human after sign-in", "error", err, "email", req.Email)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := SignInResponse{
|
||||
Human: humanToDTO(hum),
|
||||
Token: token,
|
||||
@@ -237,7 +252,7 @@ func (h *Handler) GetCurrentHuman(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// CreateNetwork creates a new network
|
||||
func (h *Handler) CreateNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
email, ok := middleware.EmailFromContext(r.Context())
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -249,13 +264,13 @@ func (h *Handler) CreateNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
net, err := h.networkSvc.Create(r.Context(), req.Name, email)
|
||||
net, err := h.networkSvc.Create(r.Context(), req.Name, humanId)
|
||||
if err != nil {
|
||||
if errors.Is(err, network.ErrInvalidName) {
|
||||
http.Error(w, "name cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
slog.Error("failed to create network", "error", err, "email", email, "name", req.Name)
|
||||
slog.Error("failed to create network", "error", err, "humanId", humanId, "name", req.Name)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -274,15 +289,15 @@ func (h *Handler) CreateNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ListNetworks retrieves networks for the authenticated human
|
||||
func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
|
||||
email, ok := middleware.EmailFromContext(r.Context())
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
networks, err := h.networkSvc.ListForEmail(r.Context(), email)
|
||||
networks, err := h.networkSvc.ListForHuman(r.Context(), humanId)
|
||||
if err != nil {
|
||||
slog.Error("failed to list networks", "error", err, "email", email)
|
||||
slog.Error("failed to list networks", "error", err, "humanId", humanId)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -303,7 +318,7 @@ func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetNetwork retrieves a specific network
|
||||
func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
email, ok := middleware.EmailFromContext(r.Context())
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -315,10 +330,9 @@ func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check membership
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email)
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
|
||||
if err != nil {
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email)
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -349,9 +363,10 @@ func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// AddMembersToNetwork adds members to a network
|
||||
// AddMembersToNetwork adds members to a network. Registered users are added as members,
|
||||
// unregistered users receive email invitations.
|
||||
func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
email, ok := middleware.EmailFromContext(r.Context())
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -363,10 +378,9 @@ func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check membership
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email)
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
|
||||
if err != nil {
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email)
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -386,10 +400,42 @@ func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.networkSvc.AddMembers(r.Context(), networkID, req.EmailAddresses); err != nil {
|
||||
slog.Error("failed to add members to network", "error", err, "network_id", networkID)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
// Resolve emails: registered users become members, unregistered get invitations
|
||||
var memberHumanIds []string
|
||||
var inviteEmails []string
|
||||
for _, email := range req.EmailAddresses {
|
||||
normalized, err := utils.NormalizeEmail(email)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid email: "+email, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hum, err := h.humanSvc.GetByEmail(r.Context(), normalized)
|
||||
if err != nil {
|
||||
if errors.Is(err, human.ErrNotFound) {
|
||||
inviteEmails = append(inviteEmails, normalized)
|
||||
continue
|
||||
}
|
||||
slog.Error("failed to look up human by email", "error", err, "email", normalized)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
memberHumanIds = append(memberHumanIds, hum.ID)
|
||||
}
|
||||
|
||||
if len(memberHumanIds) > 0 {
|
||||
if err := h.networkSvc.AddMembers(r.Context(), networkID, memberHumanIds); err != nil {
|
||||
slog.Error("failed to add members to network", "error", err, "network_id", networkID)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(inviteEmails) > 0 {
|
||||
if err := h.networkSvc.InviteByEmail(r.Context(), networkID, inviteEmails); err != nil {
|
||||
slog.Error("failed to invite members to network", "error", err, "network_id", networkID)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Return updated network
|
||||
@@ -413,23 +459,22 @@ func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// RemoveMemberFromNetwork removes a member from a network
|
||||
func (h *Handler) RemoveMemberFromNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
email, ok := middleware.EmailFromContext(r.Context())
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
networkID := r.PathValue("id")
|
||||
memberEmail := r.PathValue("email")
|
||||
if networkID == "" || memberEmail == "" {
|
||||
http.Error(w, "network id and member email are required", http.StatusBadRequest)
|
||||
memberHumanId := r.PathValue("humanId")
|
||||
if networkID == "" || memberHumanId == "" {
|
||||
http.Error(w, "network id and member humanId are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check membership
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email)
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
|
||||
if err != nil {
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email)
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -438,8 +483,159 @@ func (h *Handler) RemoveMemberFromNetwork(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.networkSvc.RemoveMember(r.Context(), networkID, memberEmail); err != nil {
|
||||
slog.Error("failed to remove member from network", "error", err, "network_id", networkID, "member_email", memberEmail)
|
||||
if err := h.networkSvc.RemoveMember(r.Context(), networkID, memberHumanId); err != nil {
|
||||
slog.Error("failed to remove member from network", "error", err, "network_id", networkID, "memberHumanId", memberHumanId)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListInvitationsForNetwork returns pending invitations for a network
|
||||
func (h *Handler) ListInvitationsForNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
networkID := r.PathValue("id")
|
||||
if networkID == "" {
|
||||
http.Error(w, "network id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
|
||||
if err != nil {
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !isMember {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
invitations, err := h.networkSvc.ListInvitationsForNetwork(r.Context(), networkID)
|
||||
if err != nil {
|
||||
slog.Error("failed to list invitations", "error", err, "network_id", networkID)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := make([]Invitation, 0, len(invitations))
|
||||
for _, inv := range invitations {
|
||||
resp = append(resp, Invitation{
|
||||
NetworkId: inv.NetworkID,
|
||||
Email: inv.Email,
|
||||
CreatedAt: inv.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// ListMyInvitations returns pending invitations for the authenticated user
|
||||
func (h *Handler) ListMyInvitations(w http.ResponseWriter, r *http.Request) {
|
||||
email, ok := middleware.EmailFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
invitations, err := h.networkSvc.ListInvitationsForEmail(r.Context(), email)
|
||||
if err != nil {
|
||||
slog.Error("failed to list invitations for email", "error", err, "email", email)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := make([]Invitation, 0, len(invitations))
|
||||
for _, inv := range invitations {
|
||||
resp = append(resp, Invitation{
|
||||
NetworkId: inv.NetworkID,
|
||||
Email: inv.Email,
|
||||
CreatedAt: inv.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// AcceptInvitation accepts a pending network invitation for the authenticated user
|
||||
func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
|
||||
email, ok := middleware.EmailFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req AcceptInvitationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.NetworkId == "" {
|
||||
http.Error(w, "network_id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.networkSvc.AcceptInvitation(r.Context(), req.NetworkId, email, humanId); err != nil {
|
||||
slog.Error("failed to accept invitation", "error", err, "network_id", req.NetworkId, "email", email)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RevokeInvitation revokes a pending invitation from a network
|
||||
func (h *Handler) RevokeInvitation(w http.ResponseWriter, r *http.Request) {
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
networkID := r.PathValue("id")
|
||||
if networkID == "" {
|
||||
http.Error(w, "network id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
|
||||
if err != nil {
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !isMember {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var req RevokeInvitationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == "" {
|
||||
http.Error(w, "email is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.networkSvc.RevokeInvitation(r.Context(), networkID, req.Email); err != nil {
|
||||
slog.Error("failed to revoke invitation", "error", err, "network_id", networkID, "email", req.Email)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -476,7 +672,7 @@ func (h *Handler) DownloadParticleMedia(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// PrepareUpload prepares an upload and returns a signed URL for direct upload to GCS
|
||||
func (h *Handler) PrepareUpload(w http.ResponseWriter, r *http.Request) {
|
||||
email, ok := middleware.EmailFromContext(r.Context())
|
||||
humanId, ok := middleware.HumanIdFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -493,10 +689,9 @@ func (h *Handler) PrepareUpload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check network membership
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), req.NetworkId, email)
|
||||
isMember, err := h.networkSvc.IsMember(r.Context(), req.NetworkId, humanId)
|
||||
if err != nil {
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", req.NetworkId, "email", email)
|
||||
slog.Error("failed to check network membership", "error", err, "network_id", req.NetworkId, "humanId", humanId)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -582,40 +777,27 @@ func (h *Handler) ConfirmUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func humanToDTO(h *human.Human) Human {
|
||||
return Human{
|
||||
Id: utils.CreateOptionalString(h.ID),
|
||||
Id: h.ID,
|
||||
Email: h.Email,
|
||||
EmailPrefix: h.EmailPrefix,
|
||||
CreatedAt: &h.CreatedAt,
|
||||
CreatedAt: h.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func emailPrefix(email string) string {
|
||||
return strings.Split(email, "@")[0]
|
||||
}
|
||||
|
||||
func (h *Handler) networkToDTO(ctx context.Context, n *network.Network) (Network, error) {
|
||||
adminHuman, err := h.humanSvc.GetByEmail(ctx, n.AdminEmail)
|
||||
adminHuman, err := h.humanSvc.GetByID(ctx, n.AdminHumanId)
|
||||
if err != nil {
|
||||
return Network{}, err
|
||||
}
|
||||
|
||||
// Get member humans
|
||||
humans := make([]Human, 0, len(n.MemberEmails))
|
||||
for _, email := range n.MemberEmails {
|
||||
hum, err := h.humanSvc.GetByEmail(ctx, email)
|
||||
humans := make([]Human, 0, len(n.MemberHumanIds))
|
||||
for _, memberHumanId := range n.MemberHumanIds {
|
||||
hum, err := h.humanSvc.GetByID(ctx, memberHumanId)
|
||||
if err != nil {
|
||||
if err == human.ErrNotFound {
|
||||
humans = append(humans, Human{
|
||||
Id: nil,
|
||||
CreatedAt: nil,
|
||||
Email: email,
|
||||
EmailPrefix: emailPrefix(email),
|
||||
})
|
||||
}
|
||||
slog.Warn("failed to look up network member", "humanId", memberHumanId, "error", err)
|
||||
continue
|
||||
} else {
|
||||
humans = append(humans, humanToDTO(hum))
|
||||
}
|
||||
humans = append(humans, humanToDTO(hum))
|
||||
}
|
||||
|
||||
return Network{
|
||||
|
||||
Reference in New Issue
Block a user