Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 5 additions & 14 deletions cmd/lunogram/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import (
"github.com/lunogram/platform/internal/cluster/leader"
"github.com/lunogram/platform/internal/cluster/scheduler"
"github.com/lunogram/platform/internal/config"
managementv1 "github.com/lunogram/platform/internal/http/controllers/v1/management"
publicv1 "github.com/lunogram/platform/internal/http/controllers/v1/public"
v1 "github.com/lunogram/platform/internal/http/controllers/v1"
"github.com/lunogram/platform/internal/providers"
"github.com/lunogram/platform/internal/pubsub"
"github.com/lunogram/platform/internal/pubsub/consumer"
Expand Down Expand Up @@ -124,23 +123,15 @@ func run() error {
return err
}

logger.Info("starting http servers")
logger.Info("starting http server")

mgmt, err := managementv1.NewServer(ctx, logger, conf, managementDB, bucket, pub, registry)
server, err := v1.NewServer(ctx, logger, conf, managementDB, bucket, pub, registry)
if err != nil {
return err
}

logger.Info("serving management http server")
go mgmt.Serve(ctx, conf.ManagementServiceAddress)

public, err := publicv1.NewServer(ctx, logger, conf, managementDB, managementStore, usersStore, bucket, pub)
if err != nil {
return err
}

logger.Info("serving public http server")
go public.Serve(ctx, conf.PublicServiceAddress)
logger.Info("serving http server")
go server.Serve(ctx, conf.HTTPAddress)

logger.Info("service up and running!")
ctx.AwaitKillSignal()
Expand Down
4 changes: 3 additions & 1 deletion internal/claim/rbac/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ type contextKey string

const scopeKey contextKey = "admin"

// Scope represents an authenticated admin user in the context
// Scope represents an authenticated user or API key in the context.
// It is used for both the management API (JWT authentication) and the client API (API key authentication).
type Scope struct {
OrganizationID uuid.UUID
ProjectID uuid.UUID
}

// WithScope stores the admin object in the context
Expand Down
23 changes: 11 additions & 12 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,17 @@ import (
)

type Node struct {
NodeID string `env:"NODE_ID" envDefault:""`
ManagementServiceAddress string `env:"ADMIN_SERVICE_ADDRESS" envDefault:":8080"`
PublicServiceAddress string `env:"PUBLIC_SERVICE_ADDRESS" envDefault:":8081"`
DatabaseMigrate bool `env:"DATABASE_MIGRATE" envDefault:"true"`
Redis Redis `envPrefix:"REDIS_"`
Cluster Cluster `envPrefix:"CLUSTER_"`
Auth Auth `envPrefix:"AUTH_"`
Nats Nats `envPrefix:"NATS_"`
WASM WASM `envPrefix:"WASM_"`
HTTP http.Config
Store store.Config
Storage storage.Config
NodeID string `env:"NODE_ID" envDefault:""`
HTTPAddress string `env:"HTTP_ADDRESS" envDefault:":8080"`
DatabaseMigrate bool `env:"DATABASE_MIGRATE" envDefault:"true"`
Redis Redis `envPrefix:"REDIS_"`
Cluster Cluster `envPrefix:"CLUSTER_"`
Auth Auth `envPrefix:"AUTH_"`
Nats Nats `envPrefix:"NATS_"`
WASM WASM `envPrefix:"WASM_"`
HTTP http.Config
Store store.Config
Storage storage.Config
}

type Auth struct {
Expand Down
1 change: 1 addition & 0 deletions internal/http/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func WithKey(mgmt *management.State) Handler {

ctx = rbac.WithScope(ctx, &rbac.Scope{
OrganizationID: key.OrganizationID,
ProjectID: key.ProjectID,
})

return ctx, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/lunogram/platform/internal/claim/rbac"
"github.com/lunogram/platform/internal/http/controllers/v1/public/oapi"
"github.com/lunogram/platform/internal/http/controllers/v1/client/oapi"
"github.com/lunogram/platform/internal/http/json"
"github.com/lunogram/platform/internal/http/problem"
"github.com/lunogram/platform/internal/pubsub"
Expand All @@ -31,7 +31,7 @@ type ClientController struct {
pubsub pubsub.Publisher
}

func (srv *ClientController) PostEvents(w http.ResponseWriter, r *http.Request, projectID uuid.UUID) {
func (srv *ClientController) PostEvents(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

scope := rbac.FromContext(ctx)
Expand All @@ -41,6 +41,7 @@ func (srv *ClientController) PostEvents(w http.ResponseWriter, r *http.Request,
return
}

projectID := scope.ProjectID
if projectID == uuid.Nil {
srv.logger.Error("project_id is required")
oapi.WriteProblem(w, problem.ErrUnauthorized())
Expand Down Expand Up @@ -78,7 +79,7 @@ func (srv *ClientController) PostEvents(w http.ResponseWriter, r *http.Request,
w.WriteHeader(http.StatusAccepted)
}

func (srv *ClientController) IdentifyUser(w http.ResponseWriter, r *http.Request, projectID uuid.UUID) {
func (srv *ClientController) IdentifyUserClient(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

scope := rbac.FromContext(ctx)
Expand All @@ -88,6 +89,7 @@ func (srv *ClientController) IdentifyUser(w http.ResponseWriter, r *http.Request
return
}

projectID := scope.ProjectID
if projectID == uuid.Nil {
srv.logger.Error("project_id is required")
oapi.WriteProblem(w, problem.ErrUnauthorized())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"testing"

"github.com/cloudproud/graceful"
"github.com/google/uuid"
"github.com/lunogram/platform/internal/claim/rbac"
"github.com/lunogram/platform/internal/config"
"github.com/lunogram/platform/internal/container"
Expand Down Expand Up @@ -153,10 +152,11 @@ func TestPostEvents(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w := httptest.NewRecorder()

controller.PostEvents(w, req, projectID)
controller.PostEvents(w, req)

assert.Equal(t, tc.statusCode, w.Code)
})
Expand All @@ -183,10 +183,11 @@ func TestPostEventsInvalidRequest(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w := httptest.NewRecorder()

controller.PostEvents(w, req, projectID)
controller.PostEvents(w, req)

assert.Equal(t, 400, w.Code)
}
Expand All @@ -210,7 +211,7 @@ func TestPostEventsMissingRBACScope(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

controller.PostEvents(w, req, uuid.Nil)
controller.PostEvents(w, req)

assert.Equal(t, 401, w.Code)
}
Expand All @@ -237,10 +238,11 @@ func TestPostEventsMissingProjectID(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
// ProjectID is intentionally left as uuid.Nil
}))
w := httptest.NewRecorder()

controller.PostEvents(w, req, uuid.Nil)
controller.PostEvents(w, req)

assert.Equal(t, 401, w.Code)
}
Expand Down Expand Up @@ -288,10 +290,11 @@ func TestPostEventsWithNestedData(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w := httptest.NewRecorder()

controller.PostEvents(w, req, projectID)
controller.PostEvents(w, req)

assert.Equal(t, 202, w.Code)
}
Expand Down Expand Up @@ -321,10 +324,11 @@ func TestPostEventsEmptyArray(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w := httptest.NewRecorder()

controller.PostEvents(w, req, projectID)
controller.PostEvents(w, req)

assert.Equal(t, 202, w.Code)
}
Expand Down Expand Up @@ -399,10 +403,11 @@ func TestClientIdentifyUser(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w := httptest.NewRecorder()

controller.IdentifyUser(w, req, projectID)
controller.IdentifyUserClient(w, req)

assert.Equal(t, tc.statusCode, w.Code)
if w.Code == 200 {
Expand Down Expand Up @@ -465,10 +470,11 @@ func TestClientIdentifyUserInvalidRequest(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w := httptest.NewRecorder()

controller.IdentifyUser(w, req, projectID)
controller.IdentifyUserClient(w, req)

assert.Equal(t, tc.statusCode, w.Code)
})
Expand All @@ -490,7 +496,7 @@ func TestClientIdentifyUserMissingRBACScope(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

controller.IdentifyUser(w, req, uuid.Nil)
controller.IdentifyUserClient(w, req)

assert.Equal(t, 401, w.Code)
}
Expand All @@ -513,10 +519,11 @@ func TestClientIdentifyUserMissingProjectID(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
// ProjectID is intentionally left as uuid.Nil
}))
w := httptest.NewRecorder()

controller.IdentifyUser(w, req, uuid.Nil)
controller.IdentifyUserClient(w, req)

assert.Equal(t, 401, w.Code)
}
Expand Down Expand Up @@ -552,10 +559,11 @@ func TestClientIdentifyUserUpdateExisting(t *testing.T) {
req1.Header.Set("Content-Type", "application/json")
req1 = req1.WithContext(rbac.WithScope(req1.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w1 := httptest.NewRecorder()

controller.IdentifyUser(w1, req1, projectID)
controller.IdentifyUserClient(w1, req1)

assert.Equal(t, 200, w1.Code)

Expand All @@ -580,10 +588,11 @@ func TestClientIdentifyUserUpdateExisting(t *testing.T) {
req2.Header.Set("Content-Type", "application/json")
req2 = req2.WithContext(rbac.WithScope(req2.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w2 := httptest.NewRecorder()

controller.IdentifyUser(w2, req2, projectID)
controller.IdentifyUserClient(w2, req2)

assert.Equal(t, 200, w2.Code)

Expand Down Expand Up @@ -627,10 +636,11 @@ func TestClientIdentifyUserWithBothIdentifiers(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(rbac.WithScope(req.Context(), &rbac.Scope{
OrganizationID: orgID,
ProjectID: projectID,
}))
w := httptest.NewRecorder()

controller.IdentifyUser(w, req, projectID)
controller.IdentifyUserClient(w, req)

assert.Equal(t, 200, w.Code)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/lunogram/platform/internal/http/problem"
"github.com/lunogram/platform/internal/http/scalar"
middleware "github.com/oapi-codegen/nethttp-middleware"
)

Expand All @@ -21,20 +20,3 @@ func Validator(spec *openapi3.T, options openapi3filter.Options) func(next http.
},
})
}

func Scalar() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/openapi.yaml" {
scalar.HandleOAPI(oapi).ServeHTTP(w, req)
return
}
if req.URL.Path == "/" {
http.FileServer(http.FS(scalar.FS)).ServeHTTP(w, req)
return
}

next.ServeHTTP(w, req)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import (
//go:generate oapi-codegen -o ./resources_gen.go -generate types,client,chi-server -package oapi ./resources.yml

//go:embed resources.yml
var oapi []byte
var OAPI []byte

func Spec() (*openapi3.T, error) {
return openapi3.NewLoader().LoadFromData(oapi)
return openapi3.NewLoader().LoadFromData(OAPI)
}

// WriteProblem writes a JSON v1 problem message to the given response writer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,15 @@ info:
version: 1.0.0

paths:
/api/client/projects/{projectID}/identify:
/api/client/identify:
post:
summary: Identify user
description: Used by client libraries to create or update a user profile
operationId: identifyUser
description: Used by client libraries to create or update a user profile. The project is determined from the API key.
operationId: identifyUserClient
tags:
- Client
security:
- HttpBearerAuth: []
parameters:
- name: projectID
in: path
required: true
schema:
type: string
format: uuid
description: The project ID
requestBody:
required: true
content:
Expand All @@ -38,27 +30,20 @@ paths:
default:
$ref: '#/components/responses/Error'

/api/client/projects/{projectID}/events:
/api/client/events:
post:
summary: Post events
description: |
Used by client libraries to trigger events that can be used to execute a step in a journey or update a virtual list.
Multiple events can be sent in a single request and are processed asynchronously.
Each event is handled independently: if one event fails to be accepted or processed, other events in the same request may still be published successfully.
Clients requiring deterministic retry or recovery behavior must submit events individually.
The project is determined from the API key.
operationId: postEvents
tags:
- Client
security:
- HttpBearerAuth: []
parameters:
- name: projectID
in: path
required: true
schema:
type: string
format: uuid
description: The project ID
requestBody:
required: true
content:
Expand Down
Loading
Loading