package organization import ( "context" "encoding/json" "errors" "net/http" "strings" "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/example/remote-access-platform/backend/internal/platform/authority" "github.com/example/remote-access-platform/backend/internal/platform/httpx" "github.com/example/remote-access-platform/backend/internal/platform/module" ) const ( RoleOrgOwner = "org_owner" RoleOrgAdmin = "org_admin" RoleOrgOperator = "org_operator" RoleOrgMember = "org_member" RoleOrgViewer = "org_viewer" ) type Module struct { db *pgxpool.Pool authority *authority.Verifier } type Organization struct { ID string `json:"id"` Slug string `json:"slug"` Name string `json:"name"` Status string `json:"status"` Metadata json.RawMessage `json:"metadata"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Membership struct { ID string `json:"id"` OrganizationID string `json:"organization_id"` UserID string `json:"user_id"` RoleID string `json:"role_id"` Status string `json:"status"` InvitedByUser *string `json:"invited_by_user_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type AdminSummary struct { OrganizationID string `json:"organization_id"` ResourceCount int64 `json:"resource_count"` ActiveSessionCount int64 `json:"active_session_count"` ServiceEndpoints []ServiceSummary `json:"service_endpoints"` ConnectorStatus map[string]any `json:"connector_status"` RecentAudit []OrgAuditEvent `json:"recent_audit"` TopologyExposure string `json:"topology_exposure"` } type ServiceSummary struct { Protocol string `json:"protocol"` Count int64 `json:"count"` } type OrgAuditEvent struct { ID string `json:"id"` EventType string `json:"event_type"` TargetType string `json:"target_type"` TargetID string `json:"target_id"` Payload json.RawMessage `json:"payload"` CreatedAt time.Time `json:"created_at"` } type createOrganizationRequest struct { ActorUserID string `json:"actor_user_id"` Slug string `json:"slug"` Name string `json:"name"` Metadata json.RawMessage `json:"metadata"` } type addMembershipRequest struct { ActorUserID string `json:"actor_user_id"` UserID string `json:"user_id"` RoleID string `json:"role_id"` } func NewModule(deps module.Dependencies) *Module { authorityVerifier, _ := authority.NewVerifier(deps.Config.Installation) return &Module{db: deps.Infra.DB, authority: authorityVerifier} } func (m *Module) Name() string { return "organization" } func (m *Module) RegisterRoutes(router chi.Router) { router.Route("/organizations", func(r chi.Router) { r.Get("/", m.listOrganizations) r.Post("/", m.createOrganization) r.Get("/{organizationID}", m.getOrganization) r.Get("/{organizationID}/admin-summary", m.getAdminSummary) r.Get("/{organizationID}/memberships", m.listMemberships) r.Post("/{organizationID}/memberships", m.addMembership) }) } func (m *Module) listOrganizations(w http.ResponseWriter, r *http.Request) { userID := r.URL.Query().Get("user_id") if userID == "" { httpx.WriteError(w, http.StatusBadRequest, "user_id is required") return } platformRole, err := m.getPlatformRole(r.Context(), userID) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } var rows pgx.Rows if isPlatformAdmin(platformRole) { rows, err = m.db.Query(r.Context(), ` SELECT id, slug, name, status, metadata, created_at, updated_at FROM organizations ORDER BY created_at DESC `) } else { rows, err = m.db.Query(r.Context(), ` SELECT o.id, o.slug, o.name, o.status, o.metadata, o.created_at, o.updated_at FROM organizations o INNER JOIN organization_memberships om ON om.organization_id = o.id WHERE om.user_id = $1 AND om.status = 'active' ORDER BY o.created_at DESC `, userID) } if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } defer rows.Close() var organizations []Organization for rows.Next() { org, err := scanOrganization(rows) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } organizations = append(organizations, org) } httpx.WriteJSON(w, http.StatusOK, map[string]any{"organizations": organizations}) } func (m *Module) getOrganization(w http.ResponseWriter, r *http.Request) { orgID := chi.URLParam(r, "organizationID") userID := r.URL.Query().Get("user_id") if userID == "" { httpx.WriteError(w, http.StatusBadRequest, "user_id is required") return } if err := m.ensureOrgAccess(r.Context(), orgID, userID, false); err != nil { status := http.StatusInternalServerError if errors.Is(err, pgx.ErrNoRows) || errors.Is(err, errForbidden) { status = http.StatusForbidden } httpx.WriteError(w, status, err.Error()) return } org, err := m.getOrganizationByID(r.Context(), orgID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { httpx.WriteError(w, http.StatusNotFound, "organization not found") return } httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } httpx.WriteJSON(w, http.StatusOK, map[string]any{"organization": org}) } func (m *Module) getAdminSummary(w http.ResponseWriter, r *http.Request) { orgID := chi.URLParam(r, "organizationID") actorUserID := r.URL.Query().Get("actor_user_id") if actorUserID == "" { httpx.WriteError(w, http.StatusBadRequest, "actor_user_id is required") return } if err := m.ensureOrgAccess(r.Context(), orgID, actorUserID, true); err != nil { httpx.WriteError(w, http.StatusForbidden, err.Error()) return } summary, err := m.loadAdminSummary(r.Context(), orgID) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } httpx.WriteJSON(w, http.StatusOK, map[string]any{"admin_summary": summary}) } func (m *Module) loadAdminSummary(ctx context.Context, orgID string) (AdminSummary, error) { var resourceCount int64 if err := m.db.QueryRow(ctx, ` SELECT COUNT(*) FROM resources WHERE organization_id = $1::uuid `, orgID).Scan(&resourceCount); err != nil { return AdminSummary{}, err } var activeSessionCount int64 if err := m.db.QueryRow(ctx, ` SELECT COUNT(*) FROM remote_sessions WHERE organization_id = $1::uuid AND state = 'active' `, orgID).Scan(&activeSessionCount); err != nil { return AdminSummary{}, err } rows, err := m.db.Query(ctx, ` SELECT protocol, COUNT(*) FROM resources WHERE organization_id = $1::uuid GROUP BY protocol ORDER BY protocol `, orgID) if err != nil { return AdminSummary{}, err } defer rows.Close() var services []ServiceSummary for rows.Next() { var item ServiceSummary if err := rows.Scan(&item.Protocol, &item.Count); err != nil { return AdminSummary{}, err } services = append(services, item) } if err := rows.Err(); err != nil { return AdminSummary{}, err } auditRows, err := m.db.Query(ctx, ` SELECT ae.id::text, ae.event_type, ae.target_type, ae.target_id, ae.payload, ae.created_at FROM audit_events ae LEFT JOIN remote_sessions rs ON rs.id = ae.remote_session_id WHERE rs.organization_id = $1::uuid ORDER BY ae.created_at DESC LIMIT 20 `, orgID) if err != nil { return AdminSummary{}, err } defer auditRows.Close() var audit []OrgAuditEvent for auditRows.Next() { var item OrgAuditEvent if err := auditRows.Scan(&item.ID, &item.EventType, &item.TargetType, &item.TargetID, &item.Payload, &item.CreatedAt); err != nil { return AdminSummary{}, err } audit = append(audit, item) } if err := auditRows.Err(); err != nil { return AdminSummary{}, err } return AdminSummary{ OrganizationID: orgID, ResourceCount: resourceCount, ActiveSessionCount: activeSessionCount, ServiceEndpoints: services, ConnectorStatus: map[string]any{ "vpn": "not_implemented", "connector": "not_implemented", }, RecentAudit: audit, TopologyExposure: tenantSafeTopologyExposure(), }, nil } func tenantSafeTopologyExposure() string { return "tenant_safe_no_core_mesh_topology" } func (m *Module) createOrganization(w http.ResponseWriter, r *http.Request) { var req createOrganizationRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { httpx.WriteError(w, http.StatusBadRequest, "invalid organization payload") return } if req.ActorUserID == "" || req.Name == "" || req.Slug == "" { httpx.WriteError(w, http.StatusBadRequest, "actor_user_id, slug, and name are required") return } role, err := m.getPlatformRole(r.Context(), req.ActorUserID) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } if !isPlatformAdmin(role) { httpx.WriteError(w, http.StatusForbidden, "platform admin role is required") return } if len(req.Metadata) == 0 { req.Metadata = json.RawMessage(`{}`) } if !json.Valid(req.Metadata) { httpx.WriteError(w, http.StatusBadRequest, "metadata must be valid json") return } now := time.Now().UTC() org := Organization{ ID: uuid.NewString(), Slug: normalizeSlug(req.Slug), Name: req.Name, Status: "active", Metadata: req.Metadata, CreatedAt: now, UpdatedAt: now, } membership := Membership{ ID: uuid.NewString(), OrganizationID: org.ID, UserID: req.ActorUserID, RoleID: RoleOrgOwner, Status: "active", CreatedAt: now, UpdatedAt: now, } tx, err := m.db.Begin(r.Context()) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } defer tx.Rollback(r.Context()) if _, err := tx.Exec(r.Context(), ` INSERT INTO organizations (id, slug, name, status, metadata, created_at, updated_at) VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7) `, org.ID, org.Slug, org.Name, org.Status, []byte(org.Metadata), org.CreatedAt, org.UpdatedAt); err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } if _, err := tx.Exec(r.Context(), ` INSERT INTO organization_memberships ( id, organization_id, user_id, role_id, status, invited_by_user_id, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) `, membership.ID, membership.OrganizationID, membership.UserID, membership.RoleID, membership.Status, req.ActorUserID, membership.CreatedAt, membership.UpdatedAt); err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } if err := tx.Commit(r.Context()); err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } httpx.WriteJSON(w, http.StatusCreated, map[string]any{ "organization": org, "membership": membership, }) } func (m *Module) listMemberships(w http.ResponseWriter, r *http.Request) { orgID := chi.URLParam(r, "organizationID") userID := r.URL.Query().Get("user_id") if userID == "" { httpx.WriteError(w, http.StatusBadRequest, "user_id is required") return } if err := m.ensureOrgAccess(r.Context(), orgID, userID, true); err != nil { httpx.WriteError(w, http.StatusForbidden, err.Error()) return } rows, err := m.db.Query(r.Context(), ` SELECT id, organization_id, user_id, role_id, status, invited_by_user_id, created_at, updated_at FROM organization_memberships WHERE organization_id = $1 ORDER BY created_at DESC `, orgID) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } defer rows.Close() var memberships []Membership for rows.Next() { membership, err := scanMembership(rows) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } memberships = append(memberships, membership) } httpx.WriteJSON(w, http.StatusOK, map[string]any{"memberships": memberships}) } func (m *Module) addMembership(w http.ResponseWriter, r *http.Request) { orgID := chi.URLParam(r, "organizationID") var req addMembershipRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { httpx.WriteError(w, http.StatusBadRequest, "invalid membership payload") return } if req.ActorUserID == "" || req.UserID == "" || req.RoleID == "" { httpx.WriteError(w, http.StatusBadRequest, "actor_user_id, user_id, and role_id are required") return } if err := m.ensureOrgAccess(r.Context(), orgID, req.ActorUserID, true); err != nil { httpx.WriteError(w, http.StatusForbidden, err.Error()) return } now := time.Now().UTC() membership := Membership{ ID: uuid.NewString(), OrganizationID: orgID, UserID: req.UserID, RoleID: req.RoleID, Status: "active", InvitedByUser: &req.ActorUserID, CreatedAt: now, UpdatedAt: now, } if _, err := m.db.Exec(r.Context(), ` INSERT INTO organization_memberships ( id, organization_id, user_id, role_id, status, invited_by_user_id, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (organization_id, user_id) DO UPDATE SET role_id = EXCLUDED.role_id, status = 'active', invited_by_user_id = EXCLUDED.invited_by_user_id, updated_at = EXCLUDED.updated_at `, membership.ID, membership.OrganizationID, membership.UserID, membership.RoleID, membership.Status, membership.InvitedByUser, membership.CreatedAt, membership.UpdatedAt); err != nil { httpx.WriteError(w, http.StatusInternalServerError, err.Error()) return } httpx.WriteJSON(w, http.StatusCreated, map[string]any{"membership": membership}) } var errForbidden = errors.New("forbidden") func (m *Module) ensureOrgAccess(ctx context.Context, orgID, userID string, adminRequired bool) error { role, err := m.getPlatformRole(ctx, userID) if err != nil { return err } if isPlatformAdmin(role) { return nil } query := ` SELECT role_id FROM organization_memberships WHERE organization_id = $1 AND user_id = $2 AND status = 'active' ` var roleID string if err := m.db.QueryRow(ctx, query, orgID, userID).Scan(&roleID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return errForbidden } return err } if adminRequired && roleID != RoleOrgOwner && roleID != RoleOrgAdmin { return errForbidden } return nil } func (m *Module) getPlatformRole(ctx context.Context, userID string) (string, error) { return authority.EffectivePlatformRole(ctx, m.db, m.authority, userID) } func isPlatformAdmin(role string) bool { return role == "platform_admin" || role == "platform_recovery_admin" } func (m *Module) getOrganizationByID(ctx context.Context, orgID string) (Organization, error) { row := m.db.QueryRow(ctx, ` SELECT id, slug, name, status, metadata, created_at, updated_at FROM organizations WHERE id = $1 `, orgID) return scanOrganization(row) } type rowScanner interface { Scan(dest ...any) error } func scanOrganization(row rowScanner) (Organization, error) { var org Organization if err := row.Scan(&org.ID, &org.Slug, &org.Name, &org.Status, &org.Metadata, &org.CreatedAt, &org.UpdatedAt); err != nil { return Organization{}, err } if len(org.Metadata) == 0 { org.Metadata = json.RawMessage(`{}`) } return org, nil } func scanMembership(row rowScanner) (Membership, error) { var membership Membership if err := row.Scan( &membership.ID, &membership.OrganizationID, &membership.UserID, &membership.RoleID, &membership.Status, &membership.InvitedByUser, &membership.CreatedAt, &membership.UpdatedAt, ); err != nil { return Membership{}, err } return membership, nil } func normalizeSlug(in string) string { return strings.ToLower(strings.TrimSpace(in)) }