4b66d8e185
* chore: only set visibility for container particles * create reusable controls indicator for reply or new * compress the size of top bar * refactor: restructure state, routing, and more * introduce stream compose flow * feat: compose new stream full flow * implement stream player * fix: prevent redirect for signed object urls * fix: implement stream playback cleaner structure * refactor: layout file name * feat: show stream name in breadcrumbs * chore: tweak padding * chore: adjust position of audio bars * feat: show latest particle preview in stream list * fix: remove console log * refactor: reorder classes * fix: avoid passing in updated_at to firestore particle * refactor: extract properties for container particles to flat fields in firestore * make the stream previews look alive * feat: show audio bars during audio clip playback * feat: order streams by last child creation * feat: playback where I left off * chore: remove unused store * fix: recording mode not using shared state * chore: clean unused variable * remove unused imports * fix: improve controls indicator immersion * feat: show playback progress in bar & auto-play text * feat: auto-exit stream on playback completion * fix: jittery media playback progress * fix: navigate during state change is invalid with react router * fix: buggy exit progress when changing clips * feat: add app icon * update package.json info * feat: only show streams visible to me * feat: show seen indicator on particles * fix: prevent unnecessary effects * fix: play new particle after playback is ended * use contols indicator for exit timer
642 lines
18 KiB
Go
642 lines
18 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/flowy-live/llink/internal/auth"
|
|
"github.com/flowy-live/llink/internal/depot"
|
|
"github.com/flowy-live/llink/internal/human"
|
|
"github.com/flowy-live/llink/internal/middleware"
|
|
"github.com/flowy-live/llink/internal/network"
|
|
"github.com/flowy-live/llink/internal/particle"
|
|
"github.com/flowy-live/llink/internal/utils"
|
|
)
|
|
|
|
type Handler struct {
|
|
authSvc auth.AuthService
|
|
humanSvc human.Service
|
|
networkSvc network.Service
|
|
particleSvc particle.Service
|
|
depotSvc depot.Service
|
|
}
|
|
|
|
func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc network.Service, particleSvc particle.Service, depotSvc depot.Service) *Handler {
|
|
return &Handler{
|
|
authSvc: authSvc,
|
|
humanSvc: humanSvc,
|
|
networkSvc: networkSvc,
|
|
particleSvc: particleSvc,
|
|
depotSvc: depotSvc,
|
|
}
|
|
}
|
|
|
|
// Response DTOs
|
|
|
|
type Human struct {
|
|
// Id will be nil if this human is not registered
|
|
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"`
|
|
}
|
|
|
|
type Network struct {
|
|
Id string `json:"id"`
|
|
Name string `json:"name"`
|
|
AdminHuman Human `json:"admin_human"`
|
|
Humans []Human `json:"humans"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Auth Request/Response DTOs
|
|
|
|
type RequestSignInCodeRequest struct {
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
type SignInRequest struct {
|
|
Email string `json:"email"`
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type SignInResponse struct {
|
|
Human Human `json:"human"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// Network Request DTOs
|
|
|
|
type CreateNetworkRequest struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type AddMembersToNetworkRequest struct {
|
|
EmailAddresses []string `json:"email_addresses"`
|
|
}
|
|
|
|
type SetOpenStreamCapacityRequest struct {
|
|
Capacity int `json:"capacity"`
|
|
}
|
|
|
|
type MembersRequest struct {
|
|
Emails []string `json:"emails"`
|
|
}
|
|
|
|
// Depot DTOs
|
|
|
|
type PrepareUploadRequest struct {
|
|
NetworkId string `json:"network_id"`
|
|
Name string `json:"name"`
|
|
ContentType string `json:"content_type"`
|
|
ContentLength int64 `json:"content_length"`
|
|
}
|
|
|
|
type PrepareUploadResponse struct {
|
|
ObjectID string `json:"object_id"`
|
|
UploadURL string `json:"upload_url"`
|
|
UploadHeaders map[string]string `json:"upload_headers"`
|
|
}
|
|
|
|
type DepotObject struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
ContentType string `json:"content_type"`
|
|
ContentLength int64 `json:"content_length"`
|
|
ContainsContent bool `json:"contains_content"`
|
|
DownloadURL string `json:"download_url,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// ============================================================================
|
|
// Auth Handlers
|
|
// ============================================================================
|
|
|
|
// RequestSignInCode creates a human account if not already existent and sends a sign-in code
|
|
func (h *Handler) RequestSignInCode(w http.ResponseWriter, r *http.Request) {
|
|
var req RequestSignInCodeRequest
|
|
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
|
|
}
|
|
|
|
// Auto-create human if doesn't exist
|
|
_, err := h.humanSvc.GetOrCreateByEmail(r.Context(), req.Email)
|
|
if err != nil {
|
|
slog.Error("failed to get or create human", "error", err, "email", req.Email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Request sign-in code
|
|
if err := h.authSvc.RequestSignInCode(r.Context(), req.Email); err != nil {
|
|
slog.Error("failed to request sign-in code", "error", err, "email", req.Email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// SignIn verifies the code and returns a session token
|
|
func (h *Handler) SignIn(w http.ResponseWriter, r *http.Request) {
|
|
var req SignInRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.Email == "" || req.Code == "" {
|
|
http.Error(w, "email and code are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
token, err := h.authSvc.VerifySignInCode(r.Context(), req.Email, req.Code)
|
|
if err != nil {
|
|
if errors.Is(err, auth.ErrInvalidCode) {
|
|
http.Error(w, "invalid code", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
slog.Error("failed to verify sign-in code", "error", err, "email", req.Email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
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,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// SignOut deletes the session from the token in headers
|
|
func (h *Handler) SignOut(w http.ResponseWriter, r *http.Request) {
|
|
token := extractBearerToken(r)
|
|
if token == "" {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if err := h.authSvc.SignOut(r.Context(), token); err != nil {
|
|
slog.Error("failed to sign out", "error", err)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// GetCurrentHuman returns the authenticated human
|
|
func (h *Handler) GetCurrentHuman(w http.ResponseWriter, r *http.Request) {
|
|
email, ok := middleware.EmailFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
hum, err := h.humanSvc.GetByEmail(r.Context(), email)
|
|
if err != nil {
|
|
if errors.Is(err, human.ErrNotFound) {
|
|
http.Error(w, "human not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
slog.Error("failed to get current human", "error", err, "email", email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
dto := humanToDTO(hum)
|
|
json.NewEncoder(w).Encode(dto)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Network Handlers
|
|
// ============================================================================
|
|
|
|
// CreateNetwork creates a new network
|
|
func (h *Handler) CreateNetwork(w http.ResponseWriter, r *http.Request) {
|
|
email, ok := middleware.EmailFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var req CreateNetworkRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
net, err := h.networkSvc.Create(r.Context(), req.Name, email)
|
|
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)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp, err := h.networkToDTO(r.Context(), net)
|
|
if err != nil {
|
|
slog.Error("failed to convert network to DTO", "error", err, "network_id", net.ID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// ListNetworks retrieves networks for the authenticated human
|
|
func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
|
|
email, ok := middleware.EmailFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
networks, err := h.networkSvc.ListForEmail(r.Context(), email)
|
|
if err != nil {
|
|
slog.Error("failed to list networks", "error", err, "email", email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp := make([]Network, 0, len(networks))
|
|
for _, net := range networks {
|
|
dto, err := h.networkToDTO(r.Context(), net)
|
|
if err != nil {
|
|
slog.Warn("failed to convert network to DTO in list", "error", err, "network_id", net.ID)
|
|
continue
|
|
}
|
|
resp = append(resp, dto)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// GetNetwork retrieves a specific network
|
|
func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) {
|
|
email, ok := middleware.EmailFromContext(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
|
|
}
|
|
|
|
// Check membership
|
|
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email)
|
|
if err != nil {
|
|
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if !isMember {
|
|
http.Error(w, "access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
net, err := h.networkSvc.GetByID(r.Context(), networkID)
|
|
if err != nil {
|
|
if errors.Is(err, network.ErrNotFound) {
|
|
http.Error(w, "network not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
slog.Error("failed to get network", "error", err, "network_id", networkID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp, err := h.networkToDTO(r.Context(), net)
|
|
if err != nil {
|
|
slog.Error("failed to convert network to DTO", "error", err, "network_id", networkID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// AddMembersToNetwork adds members to a network
|
|
func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
|
|
email, ok := middleware.EmailFromContext(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
|
|
}
|
|
|
|
// Check membership
|
|
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email)
|
|
if err != nil {
|
|
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if !isMember {
|
|
http.Error(w, "access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
var req AddMembersToNetworkRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if len(req.EmailAddresses) == 0 {
|
|
http.Error(w, "email addresses are required", http.StatusBadRequest)
|
|
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
|
|
}
|
|
|
|
// Return updated network
|
|
net, err := h.networkSvc.GetByID(r.Context(), networkID)
|
|
if err != nil {
|
|
slog.Error("failed to get network after adding members", "error", err, "network_id", networkID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp, err := h.networkToDTO(r.Context(), net)
|
|
if err != nil {
|
|
slog.Error("failed to convert network to DTO", "error", err, "network_id", networkID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// RemoveMemberFromNetwork removes a member from a network
|
|
func (h *Handler) RemoveMemberFromNetwork(w http.ResponseWriter, r *http.Request) {
|
|
email, ok := middleware.EmailFromContext(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)
|
|
return
|
|
}
|
|
|
|
// Check membership
|
|
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email)
|
|
if err != nil {
|
|
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if !isMember {
|
|
http.Error(w, "access denied", http.StatusForbidden)
|
|
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)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// DownloadParticleMedia returns a fresh signed download URL for media/file particles
|
|
func (h *Handler) DownloadParticleMedia(w http.ResponseWriter, r *http.Request) {
|
|
_, ok := middleware.EmailFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// TODO: integrate firebase to fetch particle, and verify visibility for this particle's media
|
|
|
|
objectID := r.PathValue("id")
|
|
|
|
downloadURL, err := h.depotSvc.GetDownloadURL(r.Context(), objectID)
|
|
if err != nil {
|
|
slog.Error("failed to get download URL", "error", err, "object_id", objectID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"url": downloadURL})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Depot Handlers
|
|
// ============================================================================
|
|
|
|
// 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())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var req PrepareUploadRequest
|
|
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
|
|
}
|
|
|
|
// Check network membership
|
|
isMember, err := h.networkSvc.IsMember(r.Context(), req.NetworkId, email)
|
|
if err != nil {
|
|
slog.Error("failed to check network membership", "error", err, "network_id", req.NetworkId, "email", email)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if !isMember {
|
|
http.Error(w, "access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
input := depot.PrepareUploadInput{
|
|
Prefix: req.NetworkId,
|
|
Name: req.Name,
|
|
ContentType: req.ContentType,
|
|
ContentLength: req.ContentLength,
|
|
}
|
|
|
|
result, err := h.depotSvc.PrepareUpload(r.Context(), input)
|
|
if err != nil {
|
|
if errors.Is(err, depot.ErrInvalidInput) {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
slog.Error("failed to prepare upload", "error", err, "network_id", req.NetworkId, "name", req.Name)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp := PrepareUploadResponse{
|
|
ObjectID: result.ObjectID,
|
|
UploadURL: result.UploadURL,
|
|
UploadHeaders: result.UploadHeaders,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// ConfirmUpload confirms that an upload has been completed
|
|
func (h *Handler) ConfirmUpload(w http.ResponseWriter, r *http.Request) {
|
|
_, ok := middleware.EmailFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
objectID := r.PathValue("id")
|
|
if objectID == "" {
|
|
http.Error(w, "object id is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
obj, err := h.depotSvc.ConfirmUpload(r.Context(), objectID)
|
|
if err != nil {
|
|
if errors.Is(err, depot.ErrNotFound) {
|
|
http.Error(w, "object not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if errors.Is(err, depot.ErrInvalidInput) {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
slog.Error("failed to confirm upload", "error", err, "object_id", objectID)
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp := DepotObject{
|
|
ID: obj.ID,
|
|
Name: obj.Name,
|
|
ContentType: obj.ContentType,
|
|
ContentLength: obj.ContentLength,
|
|
ContainsContent: obj.ContainsContent,
|
|
DownloadURL: "",
|
|
CreatedAt: obj.CreatedAt,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
func humanToDTO(h *human.Human) Human {
|
|
return Human{
|
|
Id: utils.CreateOptionalString(h.ID),
|
|
Email: h.Email,
|
|
EmailPrefix: h.EmailPrefix,
|
|
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)
|
|
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)
|
|
if err != nil {
|
|
if err == human.ErrNotFound {
|
|
humans = append(humans, Human{
|
|
Id: nil,
|
|
CreatedAt: nil,
|
|
Email: email,
|
|
EmailPrefix: emailPrefix(email),
|
|
})
|
|
}
|
|
continue
|
|
} else {
|
|
humans = append(humans, humanToDTO(hum))
|
|
}
|
|
}
|
|
|
|
return Network{
|
|
Id: n.ID,
|
|
Name: n.Name,
|
|
AdminHuman: humanToDTO(adminHuman),
|
|
Humans: humans,
|
|
CreatedAt: n.CreatedAt,
|
|
}, nil
|
|
}
|
|
|
|
func extractBearerToken(r *http.Request) string {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
return ""
|
|
}
|
|
|
|
const prefix = "Bearer "
|
|
if len(authHeader) > len(prefix) && authHeader[:len(prefix)] == prefix {
|
|
return authHeader[len(prefix):]
|
|
}
|
|
return ""
|
|
}
|