Initial project snapshot

This commit is contained in:
2026-04-28 22:29:50 +03:00
commit 8ba0561f4f
365 changed files with 91832 additions and 0 deletions
@@ -0,0 +1,344 @@
package identitysource
import (
"context"
"encoding/json"
"errors"
"net/http"
"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/httpx"
"github.com/example/remote-access-platform/backend/internal/platform/module"
)
type Module struct {
db *pgxpool.Pool
}
type IdentitySource struct {
ID string `json:"id"`
OrganizationID string `json:"organization_id"`
Kind string `json:"kind"`
Name string `json:"name"`
Status string `json:"status"`
Config json.RawMessage `json:"config"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type IdentityMapping struct {
ID string `json:"id"`
IdentitySourceID string `json:"identity_source_id"`
MappingType string `json:"mapping_type"`
ExternalSelector json.RawMessage `json:"external_selector"`
InternalTarget json.RawMessage `json:"internal_target"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type upsertIdentitySourceRequest struct {
ActorUserID string `json:"actor_user_id"`
OrganizationID string `json:"organization_id"`
Kind string `json:"kind"`
Name string `json:"name"`
Status string `json:"status"`
Config json.RawMessage `json:"config"`
IdentityMappings []struct {
MappingType string `json:"mapping_type"`
ExternalSelector json.RawMessage `json:"external_selector"`
InternalTarget json.RawMessage `json:"internal_target"`
} `json:"identity_mappings"`
}
func NewModule(deps module.Dependencies) *Module {
return &Module{db: deps.Infra.DB}
}
func (m *Module) Name() string {
return "identitysource"
}
func (m *Module) RegisterRoutes(router chi.Router) {
router.Route("/identity-sources", func(r chi.Router) {
r.Get("/", m.listIdentitySources)
r.Post("/", m.createIdentitySource)
r.Get("/{identitySourceID}", m.getIdentitySource)
r.Put("/{identitySourceID}", m.updateIdentitySource)
})
}
func (m *Module) listIdentitySources(w http.ResponseWriter, r *http.Request) {
orgID := r.URL.Query().Get("organization_id")
if orgID == "" {
httpx.WriteError(w, http.StatusBadRequest, "organization_id is required")
return
}
rows, err := m.db.Query(r.Context(), `
SELECT id, organization_id, kind, name, status, config, created_at, updated_at
FROM identity_sources
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 items []IdentitySource
for rows.Next() {
item, err := scanIdentitySource(rows)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
items = append(items, item)
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"identity_sources": items})
}
func (m *Module) getIdentitySource(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "identitySourceID")
item, err := m.getByID(r.Context(), id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
httpx.WriteError(w, http.StatusNotFound, "identity source not found")
return
}
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
mappings, err := m.listMappings(r.Context(), id)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{
"identity_source": item,
"identity_mappings": mappings,
})
}
func (m *Module) createIdentitySource(w http.ResponseWriter, r *http.Request) {
req, err := decodeRequest(r)
if err != nil {
httpx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
now := time.Now().UTC()
item := IdentitySource{
ID: uuid.NewString(),
OrganizationID: req.OrganizationID,
Kind: req.Kind,
Name: req.Name,
Status: req.Status,
Config: req.Config,
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 identity_sources (id, organization_id, kind, name, status, config, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8)
`, item.ID, item.OrganizationID, item.Kind, item.Name, item.Status, []byte(item.Config), item.CreatedAt, item.UpdatedAt); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
mappings, err := upsertMappings(r.Context(), tx, item.ID, req.IdentityMappings)
if 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{
"identity_source": item,
"identity_mappings": mappings,
})
}
func (m *Module) updateIdentitySource(w http.ResponseWriter, r *http.Request) {
req, err := decodeRequest(r)
if err != nil {
httpx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
id := chi.URLParam(r, "identitySourceID")
tx, err := m.db.Begin(r.Context())
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
defer tx.Rollback(r.Context())
tag, err := tx.Exec(r.Context(), `
UPDATE identity_sources
SET organization_id = $2, kind = $3, name = $4, status = $5, config = $6::jsonb, updated_at = $7
WHERE id = $1
`, id, req.OrganizationID, req.Kind, req.Name, req.Status, []byte(req.Config), time.Now().UTC())
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
if tag.RowsAffected() == 0 {
httpx.WriteError(w, http.StatusNotFound, "identity source not found")
return
}
if _, err := tx.Exec(r.Context(), `DELETE FROM identity_mappings WHERE identity_source_id = $1`, id); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
mappings, err := upsertMappings(r.Context(), tx, id, req.IdentityMappings)
if 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
}
item, err := m.getByID(r.Context(), id)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{
"identity_source": item,
"identity_mappings": mappings,
})
}
func decodeRequest(r *http.Request) (*upsertIdentitySourceRequest, error) {
var req upsertIdentitySourceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.New("invalid identity source payload")
}
if req.ActorUserID == "" || req.OrganizationID == "" || req.Kind == "" || req.Name == "" {
return nil, errors.New("actor_user_id, organization_id, kind, and name are required")
}
if req.Status == "" {
req.Status = "active"
}
if len(req.Config) == 0 {
req.Config = json.RawMessage(`{}`)
}
if !json.Valid(req.Config) {
return nil, errors.New("config must be valid json")
}
for _, mapping := range req.IdentityMappings {
if len(mapping.ExternalSelector) == 0 {
mapping.ExternalSelector = json.RawMessage(`{}`)
}
if len(mapping.InternalTarget) == 0 {
mapping.InternalTarget = json.RawMessage(`{}`)
}
}
return &req, nil
}
func (m *Module) getByID(ctx context.Context, id string) (IdentitySource, error) {
row := m.db.QueryRow(ctx, `
SELECT id, organization_id, kind, name, status, config, created_at, updated_at
FROM identity_sources
WHERE id = $1
`, id)
return scanIdentitySource(row)
}
func (m *Module) listMappings(ctx context.Context, sourceID string) ([]IdentityMapping, error) {
rows, err := m.db.Query(ctx, `
SELECT id, identity_source_id, mapping_type, external_selector, internal_target, created_at, updated_at
FROM identity_mappings
WHERE identity_source_id = $1
ORDER BY created_at ASC
`, sourceID)
if err != nil {
return nil, err
}
defer rows.Close()
var mappings []IdentityMapping
for rows.Next() {
item, err := scanIdentityMapping(rows)
if err != nil {
return nil, err
}
mappings = append(mappings, item)
}
return mappings, rows.Err()
}
func upsertMappings(ctx context.Context, tx pgx.Tx, sourceID string, requested []struct {
MappingType string `json:"mapping_type"`
ExternalSelector json.RawMessage `json:"external_selector"`
InternalTarget json.RawMessage `json:"internal_target"`
}) ([]IdentityMapping, error) {
now := time.Now().UTC()
items := make([]IdentityMapping, 0, len(requested))
for _, mapping := range requested {
external := mapping.ExternalSelector
if len(external) == 0 {
external = json.RawMessage(`{}`)
}
internal := mapping.InternalTarget
if len(internal) == 0 {
internal = json.RawMessage(`{}`)
}
item := IdentityMapping{
ID: uuid.NewString(),
IdentitySourceID: sourceID,
MappingType: mapping.MappingType,
ExternalSelector: external,
InternalTarget: internal,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := tx.Exec(ctx, `
INSERT INTO identity_mappings (
id, identity_source_id, mapping_type, external_selector, internal_target, created_at, updated_at
) VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, $6, $7)
`, item.ID, item.IdentitySourceID, item.MappingType, []byte(item.ExternalSelector), []byte(item.InternalTarget), item.CreatedAt, item.UpdatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}
type rowScanner interface {
Scan(dest ...any) error
}
func scanIdentitySource(row rowScanner) (IdentitySource, error) {
var item IdentitySource
if err := row.Scan(&item.ID, &item.OrganizationID, &item.Kind, &item.Name, &item.Status, &item.Config, &item.CreatedAt, &item.UpdatedAt); err != nil {
return IdentitySource{}, err
}
if len(item.Config) == 0 {
item.Config = json.RawMessage(`{}`)
}
return item, nil
}
func scanIdentityMapping(row rowScanner) (IdentityMapping, error) {
var item IdentityMapping
if err := row.Scan(&item.ID, &item.IdentitySourceID, &item.MappingType, &item.ExternalSelector, &item.InternalTarget, &item.CreatedAt, &item.UpdatedAt); err != nil {
return IdentityMapping{}, err
}
if len(item.ExternalSelector) == 0 {
item.ExternalSelector = json.RawMessage(`{}`)
}
if len(item.InternalTarget) == 0 {
item.InternalTarget = json.RawMessage(`{}`)
}
return item, nil
}