diff --git a/auth/credential.go b/auth/credential.go index c7b9d96383..fd0568fcb0 100644 --- a/auth/credential.go +++ b/auth/credential.go @@ -17,6 +17,7 @@ type Credential struct { Username string `json:"username"` Password string `json:"password"` APIKey string `json:"api_key"` + Token string `json:"token"` } func (c *Credential) String() string { @@ -28,6 +29,7 @@ type CredentialType string const ( CredentialTypeBasic = CredentialType("BASIC") CredentialTypeAPIKey = CredentialType("BEARER") + CredentialTypeJWT = CredentialType("JWT") ) func (c CredentialType) String() string { diff --git a/auth/realm/jwt/jwt.go b/auth/realm/jwt/jwt.go new file mode 100644 index 0000000000..7d493637ec --- /dev/null +++ b/auth/realm/jwt/jwt.go @@ -0,0 +1,201 @@ +package jwt + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/frain-dev/convoy" + "github.com/frain-dev/convoy/cache" + "github.com/frain-dev/convoy/config" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/util" + "github.com/golang-jwt/jwt" +) + +var ( + ErrInvalidToken = errors.New("invalid token") + ErrTokenExpired = errors.New("expired token") +) + +type Token struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +type VerifiedToken struct { + UserID string + Expiry int64 +} + +const ( + JwtDefaultSecret string = "convoy-jwt" + JwtDefaultRefreshSecret string = "convoy-refresh-jwt" + JwtDefaultExpiry int = 1800 //seconds + JwtDefaultRefreshExpiry int = 86400 //seconds +) + +type Jwt struct { + Secret string + Expiry int + RefreshSecret string + RefreshExpiry int + cache cache.Cache +} + +func NewJwt(opts *config.JwtRealmOptions, cache cache.Cache) *Jwt { + + j := &Jwt{ + Secret: opts.Secret, + Expiry: opts.Expiry, + RefreshSecret: opts.RefreshSecret, + RefreshExpiry: opts.RefreshExpiry, + cache: cache, + } + + if util.IsStringEmpty(j.Secret) { + j.Secret = JwtDefaultSecret + } + + if util.IsStringEmpty(j.RefreshSecret) { + j.RefreshSecret = JwtDefaultRefreshSecret + } + + if j.Expiry == 0 { + j.Expiry = JwtDefaultExpiry + } + + if j.RefreshExpiry == 0 { + j.RefreshExpiry = JwtDefaultRefreshExpiry + } + + return j +} + +func (j *Jwt) GenerateToken(user *datastore.User) (Token, error) { + tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": user.UID, + "exp": time.Now().Add(time.Second * time.Duration(j.Expiry)).Unix(), + }) + + token := Token{} + + accessToken, err := tok.SignedString([]byte(j.Secret)) + if err != nil { + return token, err + } + + refreshToken, err := j.generateRefreshToken(user) + if err != nil { + return token, err + } + + token.AccessToken = accessToken + token.RefreshToken = refreshToken + + return token, nil + +} + +func (j *Jwt) ValidateAccessToken(accessToken string) (*VerifiedToken, error) { + return j.validateToken(accessToken, j.Secret) +} + +func (j *Jwt) ValidateRefreshToken(refreshToken string) (*VerifiedToken, error) { + return j.validateToken(refreshToken, j.RefreshSecret) +} + +// A token is considered blacklisted if the base64 encoding +// of the token exists as a key within the cache +func (j *Jwt) isTokenBlacklisted(token string) (bool, error) { + var exists *string + + key := convoy.TokenCacheKey.Get(j.EncodeToken(token)).String() + err := j.cache.Get(context.Background(), key, &exists) + + if err != nil { + return false, err + } + + if exists == nil { + return false, nil + } + + return true, nil + +} + +func (j *Jwt) BlacklistToken(verified *VerifiedToken, token string) error { + // Calculate the remaining valid time for the token + ttl := time.Until(time.Unix(verified.Expiry, 0)) + key := convoy.TokenCacheKey.Get(j.EncodeToken(token)).String() + err := j.cache.Set(context.Background(), key, &verified.UserID, ttl) + + if err != nil { + return err + } + + return nil +} + +func (j *Jwt) EncodeToken(token string) string { + return base64.StdEncoding.EncodeToString([]byte(token)) +} + +func (j *Jwt) generateRefreshToken(user *datastore.User) (string, error) { + refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": user.UID, + "exp": time.Now().Add(time.Second * time.Duration(j.RefreshExpiry)).Unix(), + }) + + return refreshToken.SignedString([]byte(j.RefreshSecret)) +} + +func (j *Jwt) validateToken(accessToken, secret string) (*VerifiedToken, error) { + var userId string + var expiry float64 + + isBlacklisted, err := j.isTokenBlacklisted(accessToken) + if err != nil { + return nil, err + } + + if isBlacklisted { + return nil, ErrInvalidToken + } + + token, err := jwt.Parse(accessToken, func(token *jwt.Token) (interface{}, error) { + _, ok := token.Method.(*jwt.SigningMethodHMAC) + if !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(secret), nil + }) + + if err != nil { + v, ok := err.(*jwt.ValidationError) + if ok && v.Errors == jwt.ValidationErrorExpired { + if payload, ok := token.Claims.(jwt.MapClaims); ok { + expiry = payload["exp"].(float64) + } + + return &VerifiedToken{Expiry: int64(expiry)}, ErrTokenExpired + } + + return nil, err + } + + payload, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + userId = payload["sub"].(string) + expiry = payload["exp"].(float64) + + v := &VerifiedToken{UserID: userId, Expiry: int64(expiry)} + return v, nil + } + + return nil, err +} diff --git a/auth/realm/jwt/jwt_realm.go b/auth/realm/jwt/jwt_realm.go new file mode 100644 index 0000000000..9da95cede8 --- /dev/null +++ b/auth/realm/jwt/jwt_realm.go @@ -0,0 +1,49 @@ +package jwt + +import ( + "context" + "fmt" + + "github.com/frain-dev/convoy/auth" + "github.com/frain-dev/convoy/cache" + "github.com/frain-dev/convoy/config" + "github.com/frain-dev/convoy/datastore" +) + +type JwtRealm struct { + userRepo datastore.UserRepository + jwt *Jwt +} + +func NewJwtRealm(userRepo datastore.UserRepository, opts *config.JwtRealmOptions, cache cache.Cache) *JwtRealm { + return &JwtRealm{userRepo: userRepo, jwt: NewJwt(opts, cache)} +} + +func (j *JwtRealm) Authenticate(ctx context.Context, cred *auth.Credential) (*auth.AuthenticatedUser, error) { + if cred.Type != auth.CredentialTypeJWT { + return nil, fmt.Errorf("%s only authenticates credential type %s", j.GetName(), auth.CredentialTypeJWT.String()) + } + + verified, err := j.jwt.ValidateAccessToken(cred.Token) + if err != nil { + return nil, ErrInvalidToken + } + + user, err := j.userRepo.FindUserByID(ctx, verified.UserID) + if err != nil { + return nil, ErrInvalidToken + } + + authUser := &auth.AuthenticatedUser{ + AuthenticatedByRealm: j.GetName(), + Credential: *cred, + Role: user.Role, + } + + return authUser, nil + +} + +func (j *JwtRealm) GetName() string { + return "jwt" +} diff --git a/auth/realm/jwt/jwt_realm_test.go b/auth/realm/jwt/jwt_realm_test.go new file mode 100644 index 0000000000..70cc657d9b --- /dev/null +++ b/auth/realm/jwt/jwt_realm_test.go @@ -0,0 +1,138 @@ +package jwt + +import ( + "context" + "fmt" + "testing" + + "github.com/frain-dev/convoy/auth" + "github.com/frain-dev/convoy/cache" + "github.com/frain-dev/convoy/config" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/mocks" + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func TestJwtRealm_Authenticate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + userRepo := mocks.NewMockUserRepository(ctrl) + cache, err := cache.NewCache(config.CacheConfiguration{}) + + require.Nil(t, err) + + jr := NewJwtRealm(userRepo, &config.JwtRealmOptions{}, cache) + + user := &datastore.User{UID: "123456"} + token, err := jr.jwt.GenerateToken(user) + + require.Nil(t, err) + + type args struct { + cred *auth.Credential + } + + tests := []struct { + name string + args args + dbFn func(userRepo *mocks.MockUserRepository) + want *auth.AuthenticatedUser + blacklist bool + wantErr bool + wantErrMsg string + }{ + { + name: "should_authenticate_successfully", + args: args{ + cred: &auth.Credential{ + Type: auth.CredentialTypeJWT, + Token: token.AccessToken, + }, + }, + dbFn: func(userRepo *mocks.MockUserRepository) { + userRepo.EXPECT().FindUserByID(gomock.Any(), gomock.Any()).Times(1).Return(&datastore.User{ + UID: "123456", + FirstName: "test", + LastName: "test", + Role: auth.Role{Type: auth.RoleAdmin}, + }, nil) + }, + want: &auth.AuthenticatedUser{ + AuthenticatedByRealm: jr.GetName(), + Credential: auth.Credential{ + Type: auth.CredentialTypeJWT, + Token: token.AccessToken, + }, + Role: auth.Role{Type: auth.RoleAdmin}, + }, + }, + + { + name: "should_error_for_wrong_cred_type", + args: args{ + cred: &auth.Credential{ + Type: auth.CredentialTypeAPIKey, + }, + }, + dbFn: nil, + want: nil, + wantErr: true, + wantErrMsg: fmt.Sprintf("%s only authenticates credential type JWT", jr.GetName()), + }, + + { + name: "should_error_for_invalid_token", + args: args{ + cred: &auth.Credential{ + Type: auth.CredentialTypeJWT, + Token: uuid.NewString(), + }, + }, + dbFn: nil, + want: nil, + wantErr: true, + wantErrMsg: "invalid token", + }, + + { + name: "should_error_for_blacklisted_token", + args: args{ + cred: &auth.Credential{ + Type: auth.CredentialTypeJWT, + Token: token.AccessToken, + }, + }, + dbFn: nil, + want: nil, + blacklist: true, + wantErr: true, + wantErrMsg: "invalid token", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.dbFn != nil { + tc.dbFn(userRepo) + } + + if tc.blacklist { + err := jr.jwt.BlacklistToken(&VerifiedToken{UserID: user.UID, Expiry: 10}, token.AccessToken) + require.Nil(t, err) + } + + got, err := jr.Authenticate(context.Background(), tc.args.cred) + if tc.wantErr { + require.Equal(t, tc.wantErrMsg, err.Error()) + return + } + + require.Nil(t, err) + require.Equal(t, tc.want, got) + }) + } + +} diff --git a/auth/realm/jwt/jwt_test.go b/auth/realm/jwt/jwt_test.go new file mode 100644 index 0000000000..a80ded4d8f --- /dev/null +++ b/auth/realm/jwt/jwt_test.go @@ -0,0 +1,80 @@ +package jwt + +import ( + "testing" + + "github.com/frain-dev/convoy/cache" + "github.com/frain-dev/convoy/config" + "github.com/frain-dev/convoy/datastore" + "github.com/stretchr/testify/require" +) + +func provideJwt(t *testing.T) *Jwt { + cache, err := cache.NewCache(config.CacheConfiguration{}) + + require.Nil(t, err) + + jwt := NewJwt(&config.JwtRealmOptions{}, cache) + return jwt +} + +func TestJwt_GenerateToken(t *testing.T) { + user := &datastore.User{UID: "123456"} + jwt := provideJwt(t) + + token, err := jwt.GenerateToken(user) + require.Nil(t, err) + + require.NotEmpty(t, token.AccessToken) + require.NotEmpty(t, token.RefreshToken) +} + +func TestJwt_ValidateToken(t *testing.T) { + user := &datastore.User{UID: "123456"} + jwt := provideJwt(t) + + token, err := jwt.GenerateToken(user) + require.Nil(t, err) + + require.NotEmpty(t, token.AccessToken) + require.NotEmpty(t, token.RefreshToken) + + verified, err := jwt.ValidateAccessToken(token.AccessToken) + require.Nil(t, err) + + require.Equal(t, user.UID, verified.UserID) +} + +func TestJwt_ValidateRefreshToken(t *testing.T) { + user := &datastore.User{UID: "123456"} + jwt := provideJwt(t) + + token, err := jwt.GenerateToken(user) + require.Nil(t, err) + + require.NotEmpty(t, token.AccessToken) + require.NotEmpty(t, token.RefreshToken) + + verified, err := jwt.ValidateRefreshToken(token.RefreshToken) + require.Nil(t, err) + + require.Equal(t, user.UID, verified.UserID) +} + +func TestJwt_BlacklistToken(t *testing.T) { + user := &datastore.User{UID: "123456"} + jwt := provideJwt(t) + + token, err := jwt.GenerateToken(user) + require.Nil(t, err) + + verified, err := jwt.ValidateAccessToken(token.AccessToken) + require.Nil(t, err) + + err = jwt.BlacklistToken(verified, token.AccessToken) + require.Nil(t, err) + + isBlacklist, err := jwt.isTokenBlacklisted(token.AccessToken) + require.Nil(t, err) + require.True(t, isBlacklist) +} diff --git a/auth/realm_chain/realm_chain.go b/auth/realm_chain/realm_chain.go index 2eeef138a1..55c0543538 100644 --- a/auth/realm_chain/realm_chain.go +++ b/auth/realm_chain/realm_chain.go @@ -8,8 +8,10 @@ import ( "github.com/frain-dev/convoy/auth" "github.com/frain-dev/convoy/auth/realm/file" + "github.com/frain-dev/convoy/auth/realm/jwt" "github.com/frain-dev/convoy/auth/realm/native" "github.com/frain-dev/convoy/auth/realm/noop" + "github.com/frain-dev/convoy/cache" "github.com/frain-dev/convoy/config" "github.com/frain-dev/convoy/datastore" log "github.com/sirupsen/logrus" @@ -39,7 +41,7 @@ func Get() (*RealmChain, error) { return rc, nil } -func Init(authConfig *config.AuthConfiguration, apiKeyRepo datastore.APIKeyRepository) error { +func Init(authConfig *config.AuthConfiguration, apiKeyRepo datastore.APIKeyRepository, userRepo datastore.UserRepository, cache cache.Cache) error { rc := newRealmChain() // validate authentication realms @@ -61,6 +63,15 @@ func Init(authConfig *config.AuthConfiguration, apiKeyRepo datastore.APIKeyRepos return errors.New("failed to register file realm in realm chain") } } + + if authConfig.Jwt.Enabled { + jr := jwt.NewJwtRealm(userRepo, &authConfig.Jwt, cache) + err = rc.RegisterRealm(jr) + if err != nil { + return errors.New("failed to register jwt realm in realm chain") + } + } + } else { log.Warnf("using noop realm for authentication: all requests will be authenticated with super_user role") err := rc.RegisterRealm(noop.NewNoopRealm()) diff --git a/auth/realm_chain/realm_chain_test.go b/auth/realm_chain/realm_chain_test.go index 894d7942c4..46fc724e5b 100644 --- a/auth/realm_chain/realm_chain_test.go +++ b/auth/realm_chain/realm_chain_test.go @@ -318,8 +318,13 @@ func TestInit(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockAPIKeyRepo := mocks.NewMockAPIKeyRepository(gomock.NewController(t)) - err := Init(tt.args.authConfig, mockAPIKeyRepo) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockAPIKeyRepo := mocks.NewMockAPIKeyRepository(ctrl) + userRepo := mocks.NewMockUserRepository(ctrl) + cache := mocks.NewMockCache(ctrl) + err := Init(tt.args.authConfig, mockAPIKeyRepo, userRepo, cache) if tt.wantErr { require.Equal(t, tt.wantErrMsg, err.Error()) return diff --git a/cmd/main.go b/cmd/main.go index 80ba482712..bc8a337364 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "fmt" "os" "time" _ "time/tzdata" @@ -11,6 +12,8 @@ import ( "github.com/frain-dev/convoy/datastore/badger" "github.com/frain-dev/convoy/searcher" "github.com/go-redis/redis/v8" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson/primitive" "github.com/frain-dev/convoy/logger" memqueue "github.com/frain-dev/convoy/queue/memqueue" @@ -73,6 +76,47 @@ func NewQueue(opts queue.QueueOptions, name string) queue.Queuer { return convoyQueue } +func ensureDefaultUser(ctx context.Context, a *app) error { + pageable := datastore.Pageable{} + + users, _, err := a.userRepo.LoadUsersPaged(ctx, pageable) + + if err != nil { + return fmt.Errorf("failed to load users - %w", err) + } + + if len(users) > 0 { + return nil + } + + p := datastore.Password{Plaintext: "default"} + err = p.GenerateHash() + + if err != nil { + return err + } + + defaultUser := &datastore.User{ + UID: uuid.NewString(), + FirstName: "default", + LastName: "default", + Email: "superuser@default.com", + Password: string(p.Hash), + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + DocumentStatus: datastore.ActiveDocumentStatus, + } + + err = a.userRepo.CreateUser(ctx, defaultUser) + if err != nil { + return fmt.Errorf("failed to create user - %w", err) + } + + log.Infof("Created Superuser with username: %s and password: %s", defaultUser.Email, p.Plaintext) + + return nil +} + type app struct { apiKeyRepo datastore.APIKeyRepository groupRepo datastore.GroupRepository @@ -80,7 +124,9 @@ type app struct { eventRepo datastore.EventRepository eventDeliveryRepo datastore.EventDeliveryRepository subRepo datastore.SubscriptionRepository + orgRepo datastore.OrganisationRepository sourceRepo datastore.SourceRepository + userRepo datastore.UserRepository eventQueue queue.Queuer createEventQueue queue.Queuer logger logger.Logger @@ -212,6 +258,7 @@ func preRun(app *app, db datastore.DatabaseClient) func(cmd *cobra.Command, args app.applicationRepo = db.AppRepo() app.eventDeliveryRepo = db.EventDeliveryRepo() app.sourceRepo = db.SourceRepo() + app.userRepo = db.UserRepo() app.eventQueue = NewQueue(opts, "EventQueue") app.createEventQueue = NewQueue(opts, "CreateEventQueue") @@ -222,7 +269,7 @@ func preRun(app *app, db datastore.DatabaseClient) func(cmd *cobra.Command, args app.limiter = li app.searcher = se - return nil + return ensureDefaultUser(context.Background(), app) } } diff --git a/cmd/retry.go b/cmd/retry.go index 00d620601f..ddac710d54 100644 --- a/cmd/retry.go +++ b/cmd/retry.go @@ -2,7 +2,8 @@ package main import ( "github.com/frain-dev/convoy/config" - "github.com/frain-dev/convoy/worker" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/worker/task" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -23,10 +24,8 @@ func addRetryCommand(a *app) *cobra.Command { log.WithError(err).Fatalf("Queue type error: Command is available for redis queue only.") } - err = worker.RequeueEventDeliveries(status, timeInterval, a.eventDeliveryRepo, a.groupRepo, a.eventQueue) - if err != nil { - log.WithError(err).Fatalf("Error requeue event deliveries.") - } + statuses := []datastore.EventDeliveryStatus{datastore.EventDeliveryStatus(status)} + task.RetryEventDeliveries(statuses, timeInterval, a.eventDeliveryRepo, a.groupRepo, a.eventQueue) }, } diff --git a/cmd/scheduler.go b/cmd/scheduler.go index 54d872d34f..c927920ed3 100644 --- a/cmd/scheduler.go +++ b/cmd/scheduler.go @@ -1,9 +1,6 @@ package main import ( - "context" - "time" - "github.com/frain-dev/convoy/config" "github.com/frain-dev/convoy/worker" log "github.com/sirupsen/logrus" @@ -24,40 +21,16 @@ func addSchedulerCommand(a *app) *cobra.Command { if cfg.Queue.Type != config.RedisQueueProvider { log.WithError(err).Fatalf("Queue type error: Command is available for redis queue only.") } - d, err := time.ParseDuration(timer) - if err != nil { - log.WithError(err).Fatalf("failed to parse time duration") - } - ticker := time.NewTicker(d) - ctx := context.Background() + s := worker.NewScheduler(&a.eventQueue) - for { - select { - case <-ticker.C: - go func() { - err := worker.RequeueEventDeliveries("Processing", timeInterval, a.eventDeliveryRepo, a.groupRepo, a.eventQueue) - if err != nil { - log.WithError(err).Errorf("Error requeuing status processing: %v", err) - } - }() - go func() { - err := worker.RequeueEventDeliveries("Scheduled", timeInterval, a.eventDeliveryRepo, a.groupRepo, a.eventQueue) - if err != nil { - log.WithError(err).Errorf("Error requeuing status Scheduled: %v", err) - } - }() - go func() { - err := worker.RequeueEventDeliveries("Retry", timeInterval, a.eventDeliveryRepo, a.groupRepo, a.eventQueue) - if err != nil { - log.WithError(err).Errorf("Error requeuing status Retry: %v", err) - } - }() - case <-ctx.Done(): - ticker.Stop() - return - } - } + // Register tasks. + // s.AddTask("retry events", 30, func() { + // task.RetryEventDeliveries(nil, "", a.eventDeliveryRepo, a.groupRepo, a.eventQueue) + // }) + + // Start Processing + s.Start() }, } diff --git a/cmd/server.go b/cmd/server.go index 8cb5b42aed..a480d0b616 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -144,7 +144,7 @@ func StartConvoyServer(a *app, cfg config.Configuration, withWorkers bool) error start := time.Now() log.Info("Starting Convoy server...") - err := realm_chain.Init(&cfg.Auth, a.apiKeyRepo) + err := realm_chain.Init(&cfg.Auth, a.apiKeyRepo, a.userRepo, a.cache) if err != nil { log.WithError(err).Fatal("failed to initialize realm chain") } @@ -159,9 +159,11 @@ func StartConvoyServer(a *app, cfg config.Configuration, withWorkers bool) error a.eventDeliveryRepo, a.applicationRepo, a.apiKeyRepo, - a.groupRepo, a.subRepo, + a.groupRepo, + a.orgRepo, a.sourceRepo, + a.userRepo, a.eventQueue, a.createEventQueue, a.logger, diff --git a/config/config.go b/config/config.go index 0f774a83f4..7bb7f443b5 100644 --- a/config/config.go +++ b/config/config.go @@ -60,12 +60,21 @@ type AuthConfiguration struct { RequireAuth bool `json:"require_auth" envconfig:"CONVOY_REQUIRE_AUTH"` File FileRealmOption `json:"file"` Native NativeRealmOptions `json:"native"` + Jwt JwtRealmOptions `json:"jwt"` } type NativeRealmOptions struct { Enabled bool `json:"enabled" envconfig:"CONVOY_NATIVE_REALM_ENABLED"` } +type JwtRealmOptions struct { + Enabled bool `json:"enabled"` + Secret string `json:"secret"` + Expiry int `json:"expiry"` + RefreshSecret string `json:"refresh_secret"` + RefreshExpiry int `json:"refresh_expiry"` +} + type SMTPConfiguration struct { Provider string `json:"provider" envconfig:"CONVOY_SMTP_PROVIDER"` URL string `json:"url" envconfig:"CONVOY_SMTP_URL"` diff --git a/convoy.json.example b/convoy.json.example index fabc068fa9..62b6ae9f8d 100644 --- a/convoy.json.example +++ b/convoy.json.example @@ -20,12 +20,12 @@ } }, "tracer": { - "type": "new_relic" + "type": "new_relic", "new_relic": { - "license_key": "", - "app_name": "convoy", - "config_enabled": true, - "distributed_tracer_enabled": true + "license_key": "", + "app_name": "convoy", + "config_enabled": true, + "distributed_tracer_enabled": true } }, "server": { diff --git a/datastore/badger/badger.go b/datastore/badger/badger.go index f8e6a20b1b..3b89ea1824 100644 --- a/datastore/badger/badger.go +++ b/datastore/badger/badger.go @@ -22,6 +22,8 @@ type Client struct { applicationRepo datastore.ApplicationRepository eventDeliveryRepo datastore.EventDeliveryRepository sourceRepo datastore.SourceRepository + orgRepo datastore.OrganisationRepository + userRepo datastore.UserRepository } func New(cfg config.Configuration) (datastore.DatabaseClient, error) { @@ -52,6 +54,8 @@ func New(cfg config.Configuration) (datastore.DatabaseClient, error) { subRepo: NewSubscriptionRepo(st), eventDeliveryRepo: NewEventDeliveryRepository(st), sourceRepo: NewSourceRepo(st), + orgRepo: NewOrgRepo(st), + userRepo: NewUserRepo(st), } return c, nil @@ -96,3 +100,11 @@ func (c *Client) SubRepo() datastore.SubscriptionRepository { func (c *Client) SourceRepo() datastore.SourceRepository { return c.sourceRepo } + +func (c *Client) OrganisationRepo() datastore.OrganisationRepository { + return c.orgRepo +} + +func (c *Client) UserRepo() datastore.UserRepository { + return c.userRepo +} diff --git a/datastore/badger/organisation.go b/datastore/badger/organisation.go new file mode 100644 index 0000000000..73452e6d04 --- /dev/null +++ b/datastore/badger/organisation.go @@ -0,0 +1,83 @@ +package badger + +import ( + "context" + "errors" + "github.com/frain-dev/convoy/datastore" + "github.com/timshannon/badgerhold/v4" + "math" +) + +type orgRepo struct { + db *badgerhold.Store +} + +func NewOrgRepo(db *badgerhold.Store) *orgRepo { + return &orgRepo{db: db} +} + +func (o *orgRepo) LoadOrganisationsPaged(ctx context.Context, pageable datastore.Pageable) ([]datastore.Organisation, datastore.PaginationData, error) { + var organisations = make([]datastore.Organisation, 0) + + page := pageable.Page + perPage := pageable.PerPage + data := datastore.PaginationData{} + + if pageable.Page < 1 { + page = 1 + } + + if pageable.PerPage < 1 { + perPage = 10 + } + + prevPage := page - 1 + lowerBound := perPage * prevPage + + qry := (&badgerhold.Query{}).Skip(lowerBound).Limit(perPage).SortBy("CreatedAt") + if pageable.Sort == -1 { + qry.Reverse() + } + + err := o.db.Find(&organisations, qry) + if err != nil { + return nil, datastore.PaginationData{}, err + } + + total, err := o.db.Count(&datastore.Organisation{}, nil) + if err != nil { + return nil, datastore.PaginationData{}, err + } + + data.TotalPage = int64(math.Ceil(float64(total) / float64(perPage))) + data.Total = int64(total) + data.PerPage = int64(perPage) + data.Next = int64(page + 1) + data.Page = int64(page) + data.Prev = int64(prevPage) + + return organisations, data, err +} + +func (o *orgRepo) CreateOrganisation(ctx context.Context, org *datastore.Organisation) error { + return o.db.Upsert(org.UID, org) +} + +func (o *orgRepo) UpdateOrganisation(ctx context.Context, org *datastore.Organisation) error { + return o.db.Update(org.UID, org) +} + +func (o *orgRepo) DeleteOrganisation(ctx context.Context, uid string) error { + return o.db.Delete(uid, &datastore.Organisation{}) +} + +func (o *orgRepo) FetchOrganisationByID(ctx context.Context, id string) (*datastore.Organisation, error) { + var organisation *datastore.Organisation + + err := o.db.Get(id, &organisation) + if err != nil && errors.Is(err, badgerhold.ErrNotFound) { + return organisation, datastore.ErrOrgNotFound + } + + return organisation, err +} diff --git a/datastore/badger/user.go b/datastore/badger/user.go new file mode 100644 index 0000000000..541e9ccac8 --- /dev/null +++ b/datastore/badger/user.go @@ -0,0 +1,32 @@ +package badger + +import ( + "context" + + "github.com/frain-dev/convoy/datastore" + "github.com/timshannon/badgerhold/v4" +) + +type userRepo struct { + db *badgerhold.Store +} + +func NewUserRepo(db *badgerhold.Store) datastore.UserRepository { + return &userRepo{db: db} +} + +func (u *userRepo) CreateUser(ctx context.Context, user *datastore.User) error { + return nil +} + +func (u *userRepo) FindUserByEmail(ctx context.Context, email string) (*datastore.User, error) { + return nil, nil +} + +func (u *userRepo) FindUserByID(ctx context.Context, id string) (*datastore.User, error) { + return nil, nil +} + +func (u *userRepo) LoadUsersPaged(ctx context.Context, pageable datastore.Pageable) ([]datastore.User, datastore.PaginationData, error) { + return nil, datastore.PaginationData{}, nil +} diff --git a/datastore/datastore.go b/datastore/datastore.go index d52b964921..1ceb69b523 100644 --- a/datastore/datastore.go +++ b/datastore/datastore.go @@ -16,4 +16,6 @@ type DatabaseClient interface { SubRepo() SubscriptionRepository EventDeliveryRepo() EventDeliveryRepository SourceRepo() SourceRepository + OrganisationRepo() OrganisationRepository + UserRepo() UserRepository } diff --git a/datastore/models.go b/datastore/models.go index e49307d46c..b4b879863f 100644 --- a/datastore/models.go +++ b/datastore/models.go @@ -10,6 +10,7 @@ import ( "github.com/frain-dev/convoy/auth" "github.com/frain-dev/convoy/config" "go.mongodb.org/mongo-driver/bson/primitive" + "golang.org/x/crypto/bcrypt" ) type Pageable struct { @@ -118,6 +119,10 @@ var ( } ) +var ( + ErrUserNotFound = errors.New("user not found") +) + const ( ActiveSubscriptionStatus SubscriptionStatus = "active" InactiveSubscriptionStatus SubscriptionStatus = "inactive" @@ -162,6 +167,8 @@ type Endpoint struct { DocumentStatus DocumentStatus `json:"-" bson:"document_status"` } +var ErrOrgNotFound = errors.New("organisation not found") + type Group struct { ID primitive.ObjectID `json:"-" bson:"_id"` UID string `json:"uid" bson:"uid"` @@ -353,11 +360,6 @@ type EventInterval struct { Count uint64 `json:"count" bson:"count"` } -// type EventMetadata struct { -// UID string `json:"uid" bson:"uid"` -// EventType EventType `json:"name" bson:"name"` -// } - type DeliveryAttempt struct { ID primitive.ObjectID `json:"-" bson:"_id"` UID string `json:"uid" bson:"uid"` @@ -440,9 +442,9 @@ type Subscription struct { RetryConfig *RetryConfiguration `json:"retry_config,omitempty" bson:"retry_config,omitempty"` FilterConfig *FilterConfiguration `json:"filter_config,omitempty" bson:"filter_config,omitempty"` - CreatedAt primitive.DateTime `json:"created_at,omitempty" bson:"created_at"` - UpdatedAt primitive.DateTime `json:"updated_at,omitempty" bson:"updated_at"` - DeletedAt primitive.DateTime `json:"deleted_at,omitempty" bson:"deleted_at"` + CreatedAt primitive.DateTime `json:"created_at,omitempty" bson:"created_at" swaggertype:"string"` + UpdatedAt primitive.DateTime `json:"updated_at,omitempty" bson:"updated_at" swaggertype:"string"` + DeletedAt primitive.DateTime `json:"delted_at,omitempty" bson:"deleted_at" swaggertype:"string"` DocumentStatus DocumentStatus `json:"-" bson:"document_status"` } @@ -457,6 +459,22 @@ type Source struct { IsDisabled bool `json:"is_disabled" bson:"is_disabled"` Verifier *VerifierConfig `json:"verifier" bson:"verifier"` + CreatedAt primitive.DateTime `json:"created_at,omitempty" bson:"created_at" swaggertype:"string"` + UpdatedAt primitive.DateTime `json:"updated_at,omitempty" bson:"updated_at" swaggertype:"string"` + DeletedAt primitive.DateTime `json:"deleted_at,omitempty" bson:"deleted_at" swaggertype:"string"` + + DocumentStatus DocumentStatus `json:"-" bson:"document_status"` +} + +type User struct { + ID primitive.ObjectID `json:"-" bson:"_id"` + UID string `json:"uid" bson:"uid"` + FirstName string `json:"first_name" bson:"first_name"` + LastName string `json:"last_name" bson:"last_name"` + Email string `json:"email" bson:"email"` + Password string `json:"-" bson:"password"` + Role auth.Role `json:"role" bson:"role"` + CreatedAt primitive.DateTime `json:"created_at,omitempty" bson:"created_at"` UpdatedAt primitive.DateTime `json:"updated_at,omitempty" bson:"updated_at"` DeletedAt primitive.DateTime `json:"deleted_at,omitempty" bson:"deleted_at"` @@ -502,3 +520,43 @@ type ApiKey struct { APIKey string `json:"key,omitempty" bson:"key"` APIKeyHeader string `json:"header,omitempty" bson:"header"` } + +type Organisation struct { + ID primitive.ObjectID `json:"-" bson:"_id"` + UID string `json:"uid" bson:"uid"` + OwnerID string `json:"owner_id" bson:"owner_id"` + Name string `json:"name" bson:"name"` + DocumentStatus DocumentStatus `json:"-" bson:"document_status"` + CreatedAt primitive.DateTime `json:"created_at,omitempty" bson:"created_at,omitempty" swaggertype:"string"` + UpdatedAt primitive.DateTime `json:"updated_at,omitempty" bson:"updated_at,omitempty" swaggertype:"string"` + DeletedAt primitive.DateTime `json:"deleted_at,omitempty" bson:"deleted_at,omitempty" swaggertype:"string"` +} + +type Password struct { + Plaintext string + Hash []byte +} + +func (p *Password) GenerateHash() error { + hash, err := bcrypt.GenerateFromPassword([]byte(p.Plaintext), 12) + if err != nil { + return err + } + + p.Hash = hash + return nil +} + +func (p *Password) Matches() (bool, error) { + err := bcrypt.CompareHashAndPassword(p.Hash, []byte(p.Plaintext)) + if err != nil { + switch { + case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword): + return false, nil + default: + return false, err + } + } + + return true, err +} diff --git a/datastore/mongo/mongo.go b/datastore/mongo/mongo.go index 159d8afb11..0c671e1c40 100644 --- a/datastore/mongo/mongo.go +++ b/datastore/mongo/mongo.go @@ -18,10 +18,12 @@ import ( const ( SubscriptionCollection = "subscriptions" - AppCollections = "applications" GroupCollection = "groups" + OrganisationCollection = "organisations" + AppCollections = "applications" EventCollection = "events" SourceCollection = "sources" + UserCollection = "users" ) type Client struct { @@ -33,6 +35,8 @@ type Client struct { subscriptionRepo datastore.SubscriptionRepository eventDeliveryRepo datastore.EventDeliveryRepository sourceRepo datastore.SourceRepository + orgRepo datastore.OrganisationRepository + userRepo datastore.UserRepository } func New(cfg config.Configuration) (datastore.DatabaseClient, error) { @@ -72,6 +76,8 @@ func New(cfg config.Configuration) (datastore.DatabaseClient, error) { eventRepo: NewEventRepository(conn), eventDeliveryRepo: NewEventDeliveryRepository(conn), sourceRepo: NewSourceRepo(conn), + orgRepo: NewOrgRepo(conn), + userRepo: NewUserRepo(conn), } c.ensureMongoIndices() @@ -119,6 +125,14 @@ func (c *Client) SourceRepo() datastore.SourceRepository { return c.sourceRepo } +func (c *Client) OrganisationRepo() datastore.OrganisationRepository { + return c.orgRepo +} + +func (c *Client) UserRepo() datastore.UserRepository { + return c.userRepo +} + func (c *Client) ensureMongoIndices() { c.ensureIndex(GroupCollection, "uid", true, nil) c.ensureIndex(GroupCollection, "name", true, bson.M{"document_status": datastore.ActiveDocumentStatus}) diff --git a/datastore/mongo/organisation.go b/datastore/mongo/organisation.go new file mode 100644 index 0000000000..3f4db3c31f --- /dev/null +++ b/datastore/mongo/organisation.go @@ -0,0 +1,81 @@ +package mongo + +import ( + "context" + "errors" + "time" + + "github.com/frain-dev/convoy/datastore" + pager "github.com/gobeam/mongo-go-pagination" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type orgRepo struct { + innerDB *mongo.Database + inner *mongo.Collection +} + +func NewOrgRepo(db *mongo.Database) datastore.OrganisationRepository { + return &orgRepo{ + innerDB: db, + inner: db.Collection(OrganisationCollection), + } +} + +func (db *orgRepo) LoadOrganisationsPaged(ctx context.Context, pageable datastore.Pageable) ([]datastore.Organisation, datastore.PaginationData, error) { + filter := bson.M{"document_status": datastore.ActiveDocumentStatus} + + organisations := make([]datastore.Organisation, 0) + paginatedData, err := pager.New(db.inner).Context(ctx).Limit(int64(pageable.PerPage)).Page(int64(pageable.Page)).Sort("created_at", pageable.Sort).Filter(filter).Decode(&organisations).Find() + if err != nil { + return organisations, datastore.PaginationData{}, err + } + + return organisations, datastore.PaginationData(paginatedData.Pagination), nil +} + +func (db *orgRepo) CreateOrganisation(ctx context.Context, org *datastore.Organisation) error { + org.ID = primitive.NewObjectID() + _, err := db.inner.InsertOne(ctx, org) + return err +} + +func (db *orgRepo) UpdateOrganisation(ctx context.Context, org *datastore.Organisation) error { + org.UpdatedAt = primitive.NewDateTimeFromTime(time.Now()) + update := bson.D{primitive.E{Key: "$set", Value: bson.D{ + primitive.E{Key: "name", Value: org.Name}, + primitive.E{Key: "updated_at", Value: org.UpdatedAt}, + }}} + + _, err := db.inner.UpdateOne(ctx, bson.M{"uid": org.UID}, update) + return err +} + +func (db *orgRepo) DeleteOrganisation(ctx context.Context, uid string) error { + update := bson.M{ + "$set": bson.M{ + "deleted_at": primitive.NewDateTimeFromTime(time.Now()), + "document_status": datastore.DeletedDocumentStatus, + }, + } + + _, err := db.inner.UpdateOne(ctx, bson.M{"uid": uid}, update) + if err != nil { + return err + } + + return nil +} + +func (db *orgRepo) FetchOrganisationByID(ctx context.Context, id string) (*datastore.Organisation, error) { + org := new(datastore.Organisation) + + err := db.inner.FindOne(ctx, bson.M{"uid": id}).Decode(&org) + if errors.Is(err, mongo.ErrNoDocuments) { + err = datastore.ErrOrgNotFound + } + + return org, err +} diff --git a/datastore/mongo/organisation_test.go b/datastore/mongo/organisation_test.go new file mode 100644 index 0000000000..3c7dc12fdf --- /dev/null +++ b/datastore/mongo/organisation_test.go @@ -0,0 +1,134 @@ +//go:build integration +// +build integration + +package mongo + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/frain-dev/convoy/datastore" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestLoadOrganisationsPaged(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + orgRepo := NewOrgRepo(db) + + for i := 1; i < 6; i++ { + org := &datastore.Organisation{ + UID: uuid.NewString(), + Name: fmt.Sprintf("org%d", i), + DocumentStatus: datastore.ActiveDocumentStatus, + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + } + + err := orgRepo.CreateOrganisation(context.Background(), org) + require.NoError(t, err) + } + + organisations, _, err := orgRepo.LoadOrganisationsPaged(context.Background(), datastore.Pageable{ + Page: 2, + PerPage: 2, + Sort: -1, + }) + + require.NoError(t, err) + require.Equal(t, 2, len(organisations)) +} + +func TestCreateOrganisation(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + orgRepo := NewOrgRepo(db) + org := &datastore.Organisation{ + UID: uuid.NewString(), + Name: fmt.Sprintf("new org"), + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + } + + err := orgRepo.CreateOrganisation(context.Background(), org) + require.NoError(t, err) +} + +func TestUpdateOrganisation(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + orgRepo := NewOrgRepo(db) + org := &datastore.Organisation{ + UID: uuid.NewString(), + Name: fmt.Sprintf("new org"), + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + } + + err := orgRepo.CreateOrganisation(context.Background(), org) + require.NoError(t, err) + + name := "organisation update" + org.Name = name + + err = orgRepo.UpdateOrganisation(context.Background(), org) + require.NoError(t, err) + + org, err = orgRepo.FetchOrganisationByID(context.Background(), org.UID) + require.NoError(t, err) + + require.Equal(t, name, org.Name) +} + +func TestFetchOrganisationByID(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + orgRepo := NewOrgRepo(db) + org := &datastore.Organisation{ + UID: uuid.NewString(), + Name: fmt.Sprintf("new org"), + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + } + + err := orgRepo.CreateOrganisation(context.Background(), org) + require.NoError(t, err) + + organisation, err := orgRepo.FetchOrganisationByID(context.Background(), org.UID) + require.NoError(t, err) + + require.Equal(t, org.UID, organisation.UID) +} + +func TestDeleteOrganisation(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + orgRepo := NewOrgRepo(db) + org := &datastore.Organisation{ + UID: uuid.NewString(), + Name: fmt.Sprintf("new org"), + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + } + + err := orgRepo.CreateOrganisation(context.Background(), org) + require.NoError(t, err) + + err = orgRepo.DeleteOrganisation(context.Background(), org.UID) + require.NoError(t, err) + + organisation, err := orgRepo.FetchOrganisationByID(context.Background(), org.UID) + require.NoError(t, err) + + require.True(t, organisation.DeletedAt > 0) + require.Equal(t, datastore.DeletedDocumentStatus, organisation.DocumentStatus) +} diff --git a/datastore/mongo/user.go b/datastore/mongo/user.go new file mode 100644 index 0000000000..4a7b647c4a --- /dev/null +++ b/datastore/mongo/user.go @@ -0,0 +1,76 @@ +package mongo + +import ( + "context" + "errors" + + "github.com/frain-dev/convoy/datastore" + pager "github.com/gobeam/mongo-go-pagination" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type userRepo struct { + innerDB *mongo.Database + client *mongo.Collection +} + +func NewUserRepo(db *mongo.Database) datastore.UserRepository { + return &userRepo{ + innerDB: db, + client: db.Collection(UserCollection), + } +} + +func (u *userRepo) CreateUser(ctx context.Context, user *datastore.User) error { + user.ID = primitive.NewObjectID() + + _, err := u.client.InsertOne(ctx, user) + return err +} + +func (u *userRepo) FindUserByEmail(ctx context.Context, email string) (*datastore.User, error) { + user := &datastore.User{} + + filter := bson.M{"email": email, "document_status": datastore.ActiveDocumentStatus} + + err := u.client.FindOne(ctx, filter).Decode(&user) + + if errors.Is(err, mongo.ErrNoDocuments) { + return user, datastore.ErrUserNotFound + } + + return user, nil +} + +func (u *userRepo) FindUserByID(ctx context.Context, id string) (*datastore.User, error) { + user := &datastore.User{} + + filter := bson.M{"uid": id, "document_status": datastore.ActiveDocumentStatus} + + err := u.client.FindOne(ctx, filter).Decode(&user) + + if errors.Is(err, mongo.ErrNoDocuments) { + return user, datastore.ErrUserNotFound + } + + return user, nil +} + +func (u *userRepo) LoadUsersPaged(ctx context.Context, pageable datastore.Pageable) ([]datastore.User, datastore.PaginationData, error) { + var users []datastore.User + + filter := bson.M{"document_status": datastore.ActiveDocumentStatus} + + paginatedData, err := pager.New(u.client).Context(ctx).Limit(int64(pageable.PerPage)).Page(int64(pageable.Page)).Sort("created_at", -1).Filter(filter).Decode(&users).Find() + if err != nil { + return users, datastore.PaginationData{}, err + } + + if users == nil { + users = make([]datastore.User, 0) + } + + return users, datastore.PaginationData(paginatedData.Pagination), nil +} diff --git a/datastore/mongo/user_test.go b/datastore/mongo/user_test.go new file mode 100644 index 0000000000..4e93502d42 --- /dev/null +++ b/datastore/mongo/user_test.go @@ -0,0 +1,173 @@ +//go:build integration +// +build integration + +package mongo + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/frain-dev/convoy/datastore" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func Test_CreateUser(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + userRepo := NewUserRepo(db) + user := generateUser(t) + + require.NoError(t, userRepo.CreateUser(context.Background(), user)) + newUser, err := userRepo.FindUserByID(context.Background(), user.UID) + require.NoError(t, err) + + require.Equal(t, user.UID, newUser.UID) + require.Equal(t, user.FirstName, newUser.FirstName) + require.Equal(t, user.LastName, newUser.LastName) +} + +func Test_FindUserByEmail(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + userRepo := NewUserRepo(db) + user := generateUser(t) + + _, err := userRepo.FindUserByEmail(context.Background(), user.Email) + + require.Error(t, err) + require.True(t, errors.Is(err, datastore.ErrUserNotFound)) + + require.NoError(t, userRepo.CreateUser(context.Background(), user)) + + newUser, err := userRepo.FindUserByEmail(context.Background(), user.Email) + require.NoError(t, err) + + require.Equal(t, user.UID, newUser.UID) + require.Equal(t, user.FirstName, newUser.FirstName) + require.Equal(t, user.Email, newUser.Email) +} + +func Test_FindUserByID(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + userRepo := NewUserRepo(db) + user := generateUser(t) + + _, err := userRepo.FindUserByID(context.Background(), user.UID) + + require.Error(t, err) + require.True(t, errors.Is(err, datastore.ErrUserNotFound)) + + require.NoError(t, userRepo.CreateUser(context.Background(), user)) + + newUser, err := userRepo.FindUserByID(context.Background(), user.UID) + require.NoError(t, err) + + require.Equal(t, user.UID, newUser.UID) + require.Equal(t, user.FirstName, newUser.FirstName) + require.Equal(t, user.Email, newUser.Email) +} + +func Test_LoadUsersPaged(t *testing.T) { + type Expected struct { + paginationData datastore.PaginationData + } + + tests := []struct { + name string + pageData datastore.Pageable + count int + expected Expected + }{ + { + name: "Load Users Paged - 10 records", + pageData: datastore.Pageable{Page: 1, PerPage: 3}, + count: 10, + expected: Expected{ + paginationData: datastore.PaginationData{ + Total: 10, + TotalPage: 4, + Page: 1, + PerPage: 3, + Prev: 0, + Next: 2, + }, + }, + }, + + { + name: "Load Users Paged - 12 records", + pageData: datastore.Pageable{Page: 2, PerPage: 4}, + count: 12, + expected: Expected{ + paginationData: datastore.PaginationData{ + Total: 12, + TotalPage: 3, + Page: 2, + PerPage: 4, + Prev: 1, + Next: 3, + }, + }, + }, + + { + name: "Load Users Paged - 5 records", + pageData: datastore.Pageable{Page: 1, PerPage: 3}, + count: 5, + expected: Expected{ + paginationData: datastore.PaginationData{ + Total: 5, + TotalPage: 2, + Page: 1, + PerPage: 3, + Prev: 0, + Next: 2, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + db, closeFn := getDB(t) + defer closeFn() + + userRepo := NewUserRepo(db) + for i := 0; i < tc.count; i++ { + user := &datastore.User{ + UID: uuid.NewString(), + FirstName: "test", + LastName: "test", + Email: fmt.Sprintf("%s@test.com", uuid.NewString()), + DocumentStatus: datastore.ActiveDocumentStatus, + } + require.NoError(t, userRepo.CreateUser(context.Background(), user)) + } + + _, pageable, err := userRepo.LoadUsersPaged(context.Background(), tc.pageData) + + require.NoError(t, err) + require.Equal(t, tc.expected.paginationData.Page, pageable.Page) + require.Equal(t, tc.expected.paginationData.PerPage, pageable.PerPage) + require.Equal(t, tc.expected.paginationData.Prev, pageable.Prev) + require.Equal(t, tc.expected.paginationData.Next, pageable.Next) + }) + } +} + +func generateUser(t *testing.T) *datastore.User { + return &datastore.User{ + UID: uuid.NewString(), + FirstName: "test", + LastName: "test", + Email: fmt.Sprintf("%s@test.com", uuid.NewString()), + DocumentStatus: datastore.ActiveDocumentStatus, + } +} diff --git a/datastore/repository.go b/datastore/repository.go index b0f3f18379..d4ef401878 100644 --- a/datastore/repository.go +++ b/datastore/repository.go @@ -48,6 +48,14 @@ type GroupRepository interface { FillGroupsStatistics(ctx context.Context, groups []*Group) error } +type OrganisationRepository interface { + LoadOrganisationsPaged(context.Context, Pageable) ([]Organisation, PaginationData, error) + CreateOrganisation(context.Context, *Organisation) error + UpdateOrganisation(context.Context, *Organisation) error + DeleteOrganisation(context.Context, string) error + FetchOrganisationByID(context.Context, string) (*Organisation, error) +} + type ApplicationRepository interface { CreateApplication(context.Context, *Application, string) error LoadApplicationsPaged(context.Context, string, string, Pageable) ([]Application, PaginationData, error) @@ -80,3 +88,10 @@ type SourceRepository interface { DeleteSourceByID(ctx context.Context, groupID string, id string) error LoadSourcesPaged(ctx context.Context, groupID string, filter *SourceFilter, pageable Pageable) ([]Source, PaginationData, error) } + +type UserRepository interface { + CreateUser(context.Context, *User) error + FindUserByEmail(context.Context, string) (*User, error) + FindUserByID(context.Context, string) (*User, error) + LoadUsersPaged(context.Context, Pageable) ([]User, PaginationData, error) +} diff --git a/docs/docs.go b/docs/docs.go index a5eef8bcbe..efc3e800db 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,6 +1,6 @@ // Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // This file was generated by swaggo/swag at -// 2022-06-06 11:38:37.857024107 +0100 WAT m=+90.788952526 +// 2022-06-06 14:28:50.662472048 +0100 WAT m=+88.731874597 package docs import ( @@ -1206,14 +1206,9 @@ var doc = `{ } } }, - "/eventdeliveries": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "This endpoint fetch event deliveries.", + "/auth/login": { + "post": { + "description": "This endpoint logs in a user", "consumes": [ "application/json" ], @@ -1221,67 +1216,18 @@ var doc = `{ "application/json" ], "tags": [ - "EventDelivery" + "User" ], - "summary": "Get event deliveries", + "summary": "Login a user", "parameters": [ { - "type": "string", - "description": "application id", - "name": "appId", - "in": "query" - }, - { - "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "event id", - "name": "eventId", - "in": "query" - }, - { - "type": "string", - "description": "start date", - "name": "startDate", - "in": "query" - }, - { - "type": "string", - "description": "end date", - "name": "endDate", - "in": "query" - }, - { - "type": "string", - "description": "results per page", - "name": "perPage", - "in": "query" - }, - { - "type": "string", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "string", - "description": "sort order", - "name": "sort", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "description": "status", - "name": "status", - "in": "query" + "description": "User Details", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginUser" + } } ], "responses": { @@ -1296,34 +1242,7 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/server.pagedResponse" - }, - { - "type": "object", - "properties": { - "content": { - "type": "array", - "items": { - "allOf": [ - { - "$ref": "#/definitions/datastore.EventDelivery" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] - } - } - } - } - ] + "$ref": "#/definitions/models.LoginUserResponse" } } } @@ -1387,14 +1306,14 @@ var doc = `{ } } }, - "/eventdeliveries/batchretry": { + "/auth/logout": { "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint resends multiple app events", + "description": "This endpoint logs out a user", "consumes": [ "application/json" ], @@ -1402,41 +1321,108 @@ var doc = `{ "application/json" ], "tags": [ - "EventDelivery" + "User" ], - "summary": "Batch Resend app events", - "parameters": [ - { - "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", - "required": true + "summary": "Logs out a user", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } }, - { - "description": "event delivery ids", - "name": "ids", - "in": "body", - "required": true, + "400": { + "description": "Bad Request", "schema": { "allOf": [ { - "$ref": "#/definitions/server.Stub" + "$ref": "#/definitions/server.serverResponse" }, { "type": "object", "properties": { - "ids": { - "type": "array", - "items": { - "type": "string" - } + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" } } } ] } } + } + } + }, + "/auth/token/refresh": { + "post": { + "description": "This endpoint refreshes an access token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Refresh an access token", + "parameters": [ + { + "description": "Token Details", + "name": "token", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Token" + } + } ], "responses": { "200": { @@ -1450,7 +1436,7 @@ var doc = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/server.Stub" + "$ref": "#/definitions/models.Token" } } } @@ -1514,14 +1500,14 @@ var doc = `{ } } }, - "/eventdeliveries/countbatchretryevents": { + "/eventdeliveries": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint counts app events that will be affected by a batch retry operation", + "description": "This endpoint fetch event deliveries.", "consumes": [ "application/json" ], @@ -1531,7 +1517,7 @@ var doc = `{ "tags": [ "EventDelivery" ], - "summary": "Count affected eventDeliveries", + "summary": "Get event deliveries", "parameters": [ { "type": "string", @@ -1541,11 +1527,17 @@ var doc = `{ }, { "type": "string", - "description": "group Id", + "description": "group id", "name": "groupId", "in": "query", "required": true }, + { + "type": "string", + "description": "event id", + "name": "eventId", + "in": "query" + }, { "type": "string", "description": "start date", @@ -1575,6 +1567,15 @@ var doc = `{ "description": "sort order", "name": "sort", "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "status", + "name": "status", + "in": "query" } ], "responses": { @@ -1591,13 +1592,28 @@ var doc = `{ "data": { "allOf": [ { - "$ref": "#/definitions/server.Stub" + "$ref": "#/definitions/server.pagedResponse" }, { "type": "object", "properties": { - "num": { - "type": "integer" + "content": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/datastore.EventDelivery" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } } } } @@ -1665,14 +1681,14 @@ var doc = `{ } } }, - "/eventdeliveries/forceresend": { + "/eventdeliveries/batchretry": { "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint force resends multiple app events", + "description": "This endpoint resends multiple app events", "consumes": [ "application/json" ], @@ -1682,11 +1698,11 @@ var doc = `{ "tags": [ "EventDelivery" ], - "summary": "Force Resend app events", + "summary": "Batch Resend app events", "parameters": [ { "type": "string", - "description": "group Id", + "description": "group id", "name": "groupId", "in": "query", "required": true @@ -1728,7 +1744,939 @@ var doc = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/server.Stub" + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/eventdeliveries/countbatchretryevents": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint counts app events that will be affected by a batch retry operation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EventDelivery" + ], + "summary": "Count affected eventDeliveries", + "parameters": [ + { + "type": "string", + "description": "application id", + "name": "appId", + "in": "query" + }, + { + "type": "string", + "description": "group Id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "start date", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "description": "end date", + "name": "endDate", + "in": "query" + }, + { + "type": "string", + "description": "results per page", + "name": "perPage", + "in": "query" + }, + { + "type": "string", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "string", + "description": "sort order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/server.Stub" + }, + { + "type": "object", + "properties": { + "num": { + "type": "integer" + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/eventdeliveries/forceresend": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint force resends multiple app events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EventDelivery" + ], + "summary": "Force Resend app events", + "parameters": [ + { + "type": "string", + "description": "group Id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "description": "event delivery ids", + "name": "ids", + "in": "body", + "required": true, + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.Stub" + }, + { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/eventdeliveries/{eventDeliveryID}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint fetches an event delivery.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EventDelivery" + ], + "summary": "Get event delivery", + "parameters": [ + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "event delivery id", + "name": "eventDeliveryID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/eventdeliveries/{eventDeliveryID}/resend": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint resends an app event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EventDelivery" + ], + "summary": "Resend an app event", + "parameters": [ + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "event delivery id", + "name": "eventDeliveryID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/events": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint fetches app events with pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Events" + ], + "summary": "Get app events with pagination", + "parameters": [ + { + "type": "string", + "description": "application id", + "name": "appId", + "in": "query" + }, + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "start date", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "description": "end date", + "name": "endDate", + "in": "query" + }, + { + "type": "string", + "description": "results per page", + "name": "perPage", + "in": "query" + }, + { + "type": "string", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "string", + "description": "sort order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/server.pagedResponse" + }, + { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint creates an app event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Events" + ], + "summary": "Create app event", + "parameters": [ + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "description": "Event Details", + "name": "event", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Event" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/events/{eventID}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint fetches an app event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Events" + ], + "summary": "Get app event", + "parameters": [ + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "event id", + "name": "eventID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] } } } @@ -1792,14 +2740,14 @@ var doc = `{ } } }, - "/eventdeliveries/{eventDeliveryID}": { + "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches an event delivery.", + "description": "This endpoint fetches an app message's delivery attempts", "consumes": [ "application/json" ], @@ -1807,15 +2755,15 @@ var doc = `{ "application/json" ], "tags": [ - "EventDelivery" + "DeliveryAttempts" ], - "summary": "Get event delivery", + "summary": "Get delivery attempts", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", + "description": "event id", + "name": "eventID", + "in": "path", "required": true }, { @@ -1838,19 +2786,10 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/datastore.DeliveryAttempt" + } } } } @@ -1914,14 +2853,14 @@ var doc = `{ } } }, - "/eventdeliveries/{eventDeliveryID}/resend": { - "put": { + "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts/{deliveryAttemptID}": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint resends an app event", + "description": "This endpoint fetches an app event delivery attempt", "consumes": [ "application/json" ], @@ -1929,15 +2868,15 @@ var doc = `{ "application/json" ], "tags": [ - "EventDelivery" + "DeliveryAttempts" ], - "summary": "Resend an app event", + "summary": "Get delivery attempt", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", + "description": "event id", + "name": "eventID", + "in": "path", "required": true }, { @@ -1946,6 +2885,13 @@ var doc = `{ "name": "eventDeliveryID", "in": "path", "required": true + }, + { + "type": "string", + "description": "delivery attempt id", + "name": "deliveryAttemptID", + "in": "path", + "required": true } ], "responses": { @@ -1960,19 +2906,7 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] + "$ref": "#/definitions/datastore.DeliveryAttempt" } } } @@ -2036,14 +2970,14 @@ var doc = `{ } } }, - "/events": { - "get": { + "/events/{eventID}/replay": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches app events with pagination", + "description": "This endpoint replays an app event", "consumes": [ "application/json" ], @@ -2053,14 +2987,8 @@ var doc = `{ "tags": [ "Events" ], - "summary": "Get app events with pagination", + "summary": "Replay app event", "parameters": [ - { - "type": "string", - "description": "application id", - "name": "appId", - "in": "query" - }, { "type": "string", "description": "group id", @@ -2070,33 +2998,10 @@ var doc = `{ }, { "type": "string", - "description": "start date", - "name": "startDate", - "in": "query" - }, - { - "type": "string", - "description": "end date", - "name": "endDate", - "in": "query" - }, - { - "type": "string", - "description": "results per page", - "name": "perPage", - "in": "query" - }, - { - "type": "string", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "string", - "description": "sort order", - "name": "sort", - "in": "query" + "description": "event id", + "name": "eventID", + "in": "path", + "required": true } ], "responses": { @@ -2113,28 +3018,13 @@ var doc = `{ "data": { "allOf": [ { - "$ref": "#/definitions/server.pagedResponse" + "$ref": "#/definitions/datastore.Event" }, { "type": "object", "properties": { - "content": { - "type": "array", - "items": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] - } + "data": { + "$ref": "#/definitions/server.Stub" } } } @@ -2200,14 +3090,16 @@ var doc = `{ } } } - }, - "post": { + } + }, + "/groups": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint creates an app event", + "description": "This endpoint fetches groups", "consumes": [ "application/json" ], @@ -2215,25 +3107,15 @@ var doc = `{ "application/json" ], "tags": [ - "Events" + "Group" ], - "summary": "Create app event", + "summary": "Get groups", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", - "required": true - }, - { - "description": "Event Details", - "name": "event", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Event" - } + "description": "group name", + "name": "name", + "in": "query" } ], "responses": { @@ -2248,19 +3130,10 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/datastore.Group" + } } } } @@ -2322,16 +3195,14 @@ var doc = `{ } } } - } - }, - "/events/{eventID}": { - "get": { + }, + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches an app event", + "description": "This endpoint creates a group", "consumes": [ "application/json" ], @@ -2339,23 +3210,18 @@ var doc = `{ "application/json" ], "tags": [ - "Events" + "Group" ], - "summary": "Get app event", + "summary": "Create a group", "parameters": [ { - "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "event id", - "name": "eventID", - "in": "path", - "required": true + "description": "Group Details", + "name": "group", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Group" + } } ], "responses": { @@ -2366,23 +3232,11 @@ var doc = `{ { "$ref": "#/definitions/server.serverResponse" }, - { - "type": "object", - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/datastore.Group" } } } @@ -2446,14 +3300,14 @@ var doc = `{ } } }, - "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts": { + "/groups/{groupID}": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches an app message's delivery attempts", + "description": "This endpoint fetches a group by its id", "consumes": [ "application/json" ], @@ -2461,21 +3315,14 @@ var doc = `{ "application/json" ], "tags": [ - "DeliveryAttempts" + "Group" ], - "summary": "Get delivery attempts", + "summary": "Get a group", "parameters": [ { "type": "string", - "description": "event id", - "name": "eventID", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "event delivery id", - "name": "eventDeliveryID", + "description": "group id", + "name": "groupID", "in": "path", "required": true } @@ -2492,10 +3339,7 @@ var doc = `{ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/datastore.DeliveryAttempt" - } + "$ref": "#/definitions/datastore.Group" } } } @@ -2557,16 +3401,14 @@ var doc = `{ } } } - } - }, - "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts/{deliveryAttemptID}": { - "get": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches an app event delivery attempt", + "description": "This endpoint updates a group", "consumes": [ "application/json" ], @@ -2574,30 +3416,25 @@ var doc = `{ "application/json" ], "tags": [ - "DeliveryAttempts" + "Group" ], - "summary": "Get delivery attempt", + "summary": "Update a group", "parameters": [ { "type": "string", - "description": "event id", - "name": "eventID", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "event delivery id", - "name": "eventDeliveryID", + "description": "group id", + "name": "groupID", "in": "path", "required": true }, { - "type": "string", - "description": "delivery attempt id", - "name": "deliveryAttemptID", - "in": "path", - "required": true + "description": "Group Details", + "name": "group", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Group" + } } ], "responses": { @@ -2612,7 +3449,7 @@ var doc = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/datastore.DeliveryAttempt" + "$ref": "#/definitions/datastore.Group" } } } @@ -2674,16 +3511,14 @@ var doc = `{ } } } - } - }, - "/groups": { - "get": { + }, + "delete": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches groups", + "description": "This endpoint deletes a group using its id", "consumes": [ "application/json" ], @@ -2693,13 +3528,14 @@ var doc = `{ "tags": [ "Group" ], - "summary": "Get groups", + "summary": "Delete a group", "parameters": [ { "type": "string", - "description": "group name", - "name": "name", - "in": "query" + "description": "group id", + "name": "groupID", + "in": "path", + "required": true } ], "responses": { @@ -2714,10 +3550,7 @@ var doc = `{ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/datastore.Group" - } + "$ref": "#/definitions/server.Stub" } } } @@ -2779,14 +3612,16 @@ var doc = `{ } } } - }, - "post": { + } + }, + "/organisations": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint creates a group", + "description": "This endpoint fetches multiple organisations", "consumes": [ "application/json" ], @@ -2794,20 +3629,9 @@ var doc = `{ "application/json" ], "tags": [ - "Group" - ], - "summary": "Create a group", - "parameters": [ - { - "description": "Group Details", - "name": "group", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Group" - } - } + "Organisation" ], + "summary": "Get organisations", "responses": { "200": { "description": "OK", @@ -2820,7 +3644,22 @@ var doc = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/datastore.Group" + "allOf": [ + { + "$ref": "#/definitions/server.pagedResponse" + }, + { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/datastore.Organisation" + } + } + } + } + ] } } } @@ -2882,16 +3721,14 @@ var doc = `{ } } } - } - }, - "/groups/{groupID}": { - "get": { + }, + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches a group by its id", + "description": "This endpoint creates an organisation", "consumes": [ "application/json" ], @@ -2899,16 +3736,18 @@ var doc = `{ "application/json" ], "tags": [ - "Group" + "Application" ], - "summary": "Get a group", + "summary": "Create an organisation", "parameters": [ { - "type": "string", - "description": "group id", - "name": "groupID", - "in": "path", - "required": true + "description": "Organisation Details", + "name": "application", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Organisation" + } } ], "responses": { @@ -2923,7 +3762,7 @@ var doc = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/datastore.Group" + "$ref": "#/definitions/datastore.Organisation" } } } @@ -2985,14 +3824,16 @@ var doc = `{ } } } - }, - "put": { + } + }, + "/organisations/{orgID}": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint updates a group", + "description": "This endpoint fetches an organisation by its id", "consumes": [ "application/json" ], @@ -3000,25 +3841,16 @@ var doc = `{ "application/json" ], "tags": [ - "Group" + "Organisation" ], - "summary": "Update a group", + "summary": "Get an organisation", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupID", + "description": "organisation id", + "name": "orgID", "in": "path", "required": true - }, - { - "description": "Group Details", - "name": "group", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Group" - } } ], "responses": { @@ -3033,7 +3865,7 @@ var doc = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/datastore.Group" + "$ref": "#/definitions/datastore.Organisation" } } } @@ -3096,13 +3928,13 @@ var doc = `{ } } }, - "delete": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint deletes a group using its id", + "description": "This endpoint deletes an organisation", "consumes": [ "application/json" ], @@ -3110,14 +3942,14 @@ var doc = `{ "application/json" ], "tags": [ - "Group" + "Organisation" ], - "summary": "Delete a group", + "summary": "Delete organisation", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupID", + "description": "organisation id", + "name": "orgID", "in": "path", "required": true } @@ -5446,6 +6278,29 @@ var doc = `{ } } }, + "datastore.Organisation": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "deleted_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "datastore.PaginationData": { "type": "object", "properties": { @@ -5509,10 +6364,10 @@ var doc = `{ "type": "object", "properties": { "created_at": { - "type": "integer" + "type": "string" }, "deleted_at": { - "type": "integer" + "type": "string" }, "group_id": { "type": "string" @@ -5533,7 +6388,7 @@ var doc = `{ "type": "string" }, "updated_at": { - "type": "integer" + "type": "string" }, "verifier": { "$ref": "#/definitions/datastore.VerifierConfig" @@ -5562,10 +6417,10 @@ var doc = `{ "$ref": "#/definitions/datastore.AlertConfiguration" }, "created_at": { - "type": "integer" + "type": "string" }, - "deleted_at": { - "type": "integer" + "delted_at": { + "type": "string" }, "endpoint": { "$ref": "#/definitions/datastore.Endpoint" @@ -5592,7 +6447,7 @@ var doc = `{ "type": "string" }, "updated_at": { - "type": "integer" + "type": "string" } } }, @@ -5743,6 +6598,57 @@ var doc = `{ } } }, + "models.LoginUser": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.LoginUserResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "integer" + }, + "deleted_at": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/auth.Role" + }, + "token": { + "$ref": "#/definitions/models.Token" + }, + "uid": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + } + }, + "models.Organisation": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, "models.PortalAPIKeyResponse": { "type": "object", "properties": { @@ -5850,6 +6756,17 @@ var doc = `{ } } }, + "models.Token": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, "server.Stub": { "type": "object" }, diff --git a/docs/swagger.json b/docs/swagger.json index 5899f1d180..612374f14a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1194,14 +1194,9 @@ } } }, - "/eventdeliveries": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "This endpoint fetch event deliveries.", + "/auth/login": { + "post": { + "description": "This endpoint logs in a user", "consumes": [ "application/json" ], @@ -1209,67 +1204,18 @@ "application/json" ], "tags": [ - "EventDelivery" + "User" ], - "summary": "Get event deliveries", + "summary": "Login a user", "parameters": [ { - "type": "string", - "description": "application id", - "name": "appId", - "in": "query" - }, - { - "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "event id", - "name": "eventId", - "in": "query" - }, - { - "type": "string", - "description": "start date", - "name": "startDate", - "in": "query" - }, - { - "type": "string", - "description": "end date", - "name": "endDate", - "in": "query" - }, - { - "type": "string", - "description": "results per page", - "name": "perPage", - "in": "query" - }, - { - "type": "string", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "string", - "description": "sort order", - "name": "sort", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "description": "status", - "name": "status", - "in": "query" + "description": "User Details", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginUser" + } } ], "responses": { @@ -1284,34 +1230,7 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/server.pagedResponse" - }, - { - "type": "object", - "properties": { - "content": { - "type": "array", - "items": { - "allOf": [ - { - "$ref": "#/definitions/datastore.EventDelivery" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] - } - } - } - } - ] + "$ref": "#/definitions/models.LoginUserResponse" } } } @@ -1375,14 +1294,14 @@ } } }, - "/eventdeliveries/batchretry": { + "/auth/logout": { "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint resends multiple app events", + "description": "This endpoint logs out a user", "consumes": [ "application/json" ], @@ -1390,41 +1309,108 @@ "application/json" ], "tags": [ - "EventDelivery" + "User" ], - "summary": "Batch Resend app events", - "parameters": [ - { - "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", - "required": true + "summary": "Logs out a user", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } }, - { - "description": "event delivery ids", - "name": "ids", - "in": "body", - "required": true, + "400": { + "description": "Bad Request", "schema": { "allOf": [ { - "$ref": "#/definitions/server.Stub" + "$ref": "#/definitions/server.serverResponse" }, { "type": "object", "properties": { - "ids": { - "type": "array", - "items": { - "type": "string" - } + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" } } } ] } } + } + } + }, + "/auth/token/refresh": { + "post": { + "description": "This endpoint refreshes an access token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Refresh an access token", + "parameters": [ + { + "description": "Token Details", + "name": "token", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Token" + } + } ], "responses": { "200": { @@ -1438,7 +1424,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/server.Stub" + "$ref": "#/definitions/models.Token" } } } @@ -1502,14 +1488,14 @@ } } }, - "/eventdeliveries/countbatchretryevents": { + "/eventdeliveries": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint counts app events that will be affected by a batch retry operation", + "description": "This endpoint fetch event deliveries.", "consumes": [ "application/json" ], @@ -1519,7 +1505,7 @@ "tags": [ "EventDelivery" ], - "summary": "Count affected eventDeliveries", + "summary": "Get event deliveries", "parameters": [ { "type": "string", @@ -1529,11 +1515,17 @@ }, { "type": "string", - "description": "group Id", + "description": "group id", "name": "groupId", "in": "query", "required": true }, + { + "type": "string", + "description": "event id", + "name": "eventId", + "in": "query" + }, { "type": "string", "description": "start date", @@ -1563,6 +1555,15 @@ "description": "sort order", "name": "sort", "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "status", + "name": "status", + "in": "query" } ], "responses": { @@ -1579,13 +1580,28 @@ "data": { "allOf": [ { - "$ref": "#/definitions/server.Stub" + "$ref": "#/definitions/server.pagedResponse" }, { "type": "object", "properties": { - "num": { - "type": "integer" + "content": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/datastore.EventDelivery" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } } } } @@ -1653,14 +1669,14 @@ } } }, - "/eventdeliveries/forceresend": { + "/eventdeliveries/batchretry": { "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint force resends multiple app events", + "description": "This endpoint resends multiple app events", "consumes": [ "application/json" ], @@ -1670,11 +1686,11 @@ "tags": [ "EventDelivery" ], - "summary": "Force Resend app events", + "summary": "Batch Resend app events", "parameters": [ { "type": "string", - "description": "group Id", + "description": "group id", "name": "groupId", "in": "query", "required": true @@ -1716,7 +1732,939 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/server.Stub" + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/eventdeliveries/countbatchretryevents": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint counts app events that will be affected by a batch retry operation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EventDelivery" + ], + "summary": "Count affected eventDeliveries", + "parameters": [ + { + "type": "string", + "description": "application id", + "name": "appId", + "in": "query" + }, + { + "type": "string", + "description": "group Id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "start date", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "description": "end date", + "name": "endDate", + "in": "query" + }, + { + "type": "string", + "description": "results per page", + "name": "perPage", + "in": "query" + }, + { + "type": "string", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "string", + "description": "sort order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/server.Stub" + }, + { + "type": "object", + "properties": { + "num": { + "type": "integer" + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/eventdeliveries/forceresend": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint force resends multiple app events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EventDelivery" + ], + "summary": "Force Resend app events", + "parameters": [ + { + "type": "string", + "description": "group Id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "description": "event delivery ids", + "name": "ids", + "in": "body", + "required": true, + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.Stub" + }, + { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/eventdeliveries/{eventDeliveryID}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint fetches an event delivery.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EventDelivery" + ], + "summary": "Get event delivery", + "parameters": [ + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "event delivery id", + "name": "eventDeliveryID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/eventdeliveries/{eventDeliveryID}/resend": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint resends an app event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EventDelivery" + ], + "summary": "Resend an app event", + "parameters": [ + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "event delivery id", + "name": "eventDeliveryID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/events": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint fetches app events with pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Events" + ], + "summary": "Get app events with pagination", + "parameters": [ + { + "type": "string", + "description": "application id", + "name": "appId", + "in": "query" + }, + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "start date", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "description": "end date", + "name": "endDate", + "in": "query" + }, + { + "type": "string", + "description": "results per page", + "name": "perPage", + "in": "query" + }, + { + "type": "string", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "string", + "description": "sort order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/server.pagedResponse" + }, + { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint creates an app event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Events" + ], + "summary": "Create app event", + "parameters": [ + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "description": "Event Details", + "name": "event", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Event" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] + } + } + } + } + }, + "/events/{eventID}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint fetches an app event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Events" + ], + "summary": "Get app event", + "parameters": [ + { + "type": "string", + "description": "group id", + "name": "groupId", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "event id", + "name": "eventID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.serverResponse" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/datastore.Event" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/server.Stub" + } + } + } + ] } } } @@ -1780,14 +2728,14 @@ } } }, - "/eventdeliveries/{eventDeliveryID}": { + "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches an event delivery.", + "description": "This endpoint fetches an app message's delivery attempts", "consumes": [ "application/json" ], @@ -1795,15 +2743,15 @@ "application/json" ], "tags": [ - "EventDelivery" + "DeliveryAttempts" ], - "summary": "Get event delivery", + "summary": "Get delivery attempts", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", + "description": "event id", + "name": "eventID", + "in": "path", "required": true }, { @@ -1826,19 +2774,10 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/datastore.DeliveryAttempt" + } } } } @@ -1902,14 +2841,14 @@ } } }, - "/eventdeliveries/{eventDeliveryID}/resend": { - "put": { + "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts/{deliveryAttemptID}": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint resends an app event", + "description": "This endpoint fetches an app event delivery attempt", "consumes": [ "application/json" ], @@ -1917,15 +2856,15 @@ "application/json" ], "tags": [ - "EventDelivery" + "DeliveryAttempts" ], - "summary": "Resend an app event", + "summary": "Get delivery attempt", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", + "description": "event id", + "name": "eventID", + "in": "path", "required": true }, { @@ -1934,6 +2873,13 @@ "name": "eventDeliveryID", "in": "path", "required": true + }, + { + "type": "string", + "description": "delivery attempt id", + "name": "deliveryAttemptID", + "in": "path", + "required": true } ], "responses": { @@ -1948,19 +2894,7 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] + "$ref": "#/definitions/datastore.DeliveryAttempt" } } } @@ -2024,14 +2958,14 @@ } } }, - "/events": { - "get": { + "/events/{eventID}/replay": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches app events with pagination", + "description": "This endpoint replays an app event", "consumes": [ "application/json" ], @@ -2041,14 +2975,8 @@ "tags": [ "Events" ], - "summary": "Get app events with pagination", + "summary": "Replay app event", "parameters": [ - { - "type": "string", - "description": "application id", - "name": "appId", - "in": "query" - }, { "type": "string", "description": "group id", @@ -2058,33 +2986,10 @@ }, { "type": "string", - "description": "start date", - "name": "startDate", - "in": "query" - }, - { - "type": "string", - "description": "end date", - "name": "endDate", - "in": "query" - }, - { - "type": "string", - "description": "results per page", - "name": "perPage", - "in": "query" - }, - { - "type": "string", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "string", - "description": "sort order", - "name": "sort", - "in": "query" + "description": "event id", + "name": "eventID", + "in": "path", + "required": true } ], "responses": { @@ -2101,28 +3006,13 @@ "data": { "allOf": [ { - "$ref": "#/definitions/server.pagedResponse" + "$ref": "#/definitions/datastore.Event" }, { "type": "object", "properties": { - "content": { - "type": "array", - "items": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] - } + "data": { + "$ref": "#/definitions/server.Stub" } } } @@ -2188,14 +3078,16 @@ } } } - }, - "post": { + } + }, + "/groups": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint creates an app event", + "description": "This endpoint fetches groups", "consumes": [ "application/json" ], @@ -2203,25 +3095,15 @@ "application/json" ], "tags": [ - "Events" + "Group" ], - "summary": "Create app event", + "summary": "Get groups", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", - "required": true - }, - { - "description": "Event Details", - "name": "event", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Event" - } + "description": "group name", + "name": "name", + "in": "query" } ], "responses": { @@ -2236,19 +3118,10 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/datastore.Group" + } } } } @@ -2310,16 +3183,14 @@ } } } - } - }, - "/events/{eventID}": { - "get": { + }, + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches an app event", + "description": "This endpoint creates a group", "consumes": [ "application/json" ], @@ -2327,23 +3198,18 @@ "application/json" ], "tags": [ - "Events" + "Group" ], - "summary": "Get app event", + "summary": "Create a group", "parameters": [ { - "type": "string", - "description": "group id", - "name": "groupId", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "event id", - "name": "eventID", - "in": "path", - "required": true + "description": "Group Details", + "name": "group", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Group" + } } ], "responses": { @@ -2354,23 +3220,11 @@ { "$ref": "#/definitions/server.serverResponse" }, - { - "type": "object", - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/definitions/datastore.Event" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.Stub" - } - } - } - ] + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/datastore.Group" } } } @@ -2434,14 +3288,14 @@ } } }, - "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts": { + "/groups/{groupID}": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches an app message's delivery attempts", + "description": "This endpoint fetches a group by its id", "consumes": [ "application/json" ], @@ -2449,21 +3303,14 @@ "application/json" ], "tags": [ - "DeliveryAttempts" + "Group" ], - "summary": "Get delivery attempts", + "summary": "Get a group", "parameters": [ { "type": "string", - "description": "event id", - "name": "eventID", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "event delivery id", - "name": "eventDeliveryID", + "description": "group id", + "name": "groupID", "in": "path", "required": true } @@ -2480,10 +3327,7 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/datastore.DeliveryAttempt" - } + "$ref": "#/definitions/datastore.Group" } } } @@ -2545,16 +3389,14 @@ } } } - } - }, - "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts/{deliveryAttemptID}": { - "get": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches an app event delivery attempt", + "description": "This endpoint updates a group", "consumes": [ "application/json" ], @@ -2562,30 +3404,25 @@ "application/json" ], "tags": [ - "DeliveryAttempts" + "Group" ], - "summary": "Get delivery attempt", + "summary": "Update a group", "parameters": [ { "type": "string", - "description": "event id", - "name": "eventID", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "event delivery id", - "name": "eventDeliveryID", + "description": "group id", + "name": "groupID", "in": "path", "required": true }, { - "type": "string", - "description": "delivery attempt id", - "name": "deliveryAttemptID", - "in": "path", - "required": true + "description": "Group Details", + "name": "group", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Group" + } } ], "responses": { @@ -2600,7 +3437,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/datastore.DeliveryAttempt" + "$ref": "#/definitions/datastore.Group" } } } @@ -2662,16 +3499,14 @@ } } } - } - }, - "/groups": { - "get": { + }, + "delete": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches groups", + "description": "This endpoint deletes a group using its id", "consumes": [ "application/json" ], @@ -2681,13 +3516,14 @@ "tags": [ "Group" ], - "summary": "Get groups", + "summary": "Delete a group", "parameters": [ { "type": "string", - "description": "group name", - "name": "name", - "in": "query" + "description": "group id", + "name": "groupID", + "in": "path", + "required": true } ], "responses": { @@ -2702,10 +3538,7 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/datastore.Group" - } + "$ref": "#/definitions/server.Stub" } } } @@ -2767,14 +3600,16 @@ } } } - }, - "post": { + } + }, + "/organisations": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint creates a group", + "description": "This endpoint fetches multiple organisations", "consumes": [ "application/json" ], @@ -2782,20 +3617,9 @@ "application/json" ], "tags": [ - "Group" - ], - "summary": "Create a group", - "parameters": [ - { - "description": "Group Details", - "name": "group", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Group" - } - } + "Organisation" ], + "summary": "Get organisations", "responses": { "200": { "description": "OK", @@ -2808,7 +3632,22 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/datastore.Group" + "allOf": [ + { + "$ref": "#/definitions/server.pagedResponse" + }, + { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/datastore.Organisation" + } + } + } + } + ] } } } @@ -2870,16 +3709,14 @@ } } } - } - }, - "/groups/{groupID}": { - "get": { + }, + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint fetches a group by its id", + "description": "This endpoint creates an organisation", "consumes": [ "application/json" ], @@ -2887,16 +3724,18 @@ "application/json" ], "tags": [ - "Group" + "Application" ], - "summary": "Get a group", + "summary": "Create an organisation", "parameters": [ { - "type": "string", - "description": "group id", - "name": "groupID", - "in": "path", - "required": true + "description": "Organisation Details", + "name": "application", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Organisation" + } } ], "responses": { @@ -2911,7 +3750,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/datastore.Group" + "$ref": "#/definitions/datastore.Organisation" } } } @@ -2973,14 +3812,16 @@ } } } - }, - "put": { + } + }, + "/organisations/{orgID}": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint updates a group", + "description": "This endpoint fetches an organisation by its id", "consumes": [ "application/json" ], @@ -2988,25 +3829,16 @@ "application/json" ], "tags": [ - "Group" + "Organisation" ], - "summary": "Update a group", + "summary": "Get an organisation", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupID", + "description": "organisation id", + "name": "orgID", "in": "path", "required": true - }, - { - "description": "Group Details", - "name": "group", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Group" - } } ], "responses": { @@ -3021,7 +3853,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/datastore.Group" + "$ref": "#/definitions/datastore.Organisation" } } } @@ -3084,13 +3916,13 @@ } } }, - "delete": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "This endpoint deletes a group using its id", + "description": "This endpoint deletes an organisation", "consumes": [ "application/json" ], @@ -3098,14 +3930,14 @@ "application/json" ], "tags": [ - "Group" + "Organisation" ], - "summary": "Delete a group", + "summary": "Delete organisation", "parameters": [ { "type": "string", - "description": "group id", - "name": "groupID", + "description": "organisation id", + "name": "orgID", "in": "path", "required": true } @@ -5434,6 +6266,29 @@ } } }, + "datastore.Organisation": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "deleted_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "datastore.PaginationData": { "type": "object", "properties": { @@ -5497,10 +6352,10 @@ "type": "object", "properties": { "created_at": { - "type": "integer" + "type": "string" }, "deleted_at": { - "type": "integer" + "type": "string" }, "group_id": { "type": "string" @@ -5521,7 +6376,7 @@ "type": "string" }, "updated_at": { - "type": "integer" + "type": "string" }, "verifier": { "$ref": "#/definitions/datastore.VerifierConfig" @@ -5550,10 +6405,10 @@ "$ref": "#/definitions/datastore.AlertConfiguration" }, "created_at": { - "type": "integer" + "type": "string" }, - "deleted_at": { - "type": "integer" + "delted_at": { + "type": "string" }, "endpoint": { "$ref": "#/definitions/datastore.Endpoint" @@ -5580,7 +6435,7 @@ "type": "string" }, "updated_at": { - "type": "integer" + "type": "string" } } }, @@ -5731,6 +6586,57 @@ } } }, + "models.LoginUser": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.LoginUserResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "integer" + }, + "deleted_at": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/auth.Role" + }, + "token": { + "$ref": "#/definitions/models.Token" + }, + "uid": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + } + }, + "models.Organisation": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, "models.PortalAPIKeyResponse": { "type": "object", "properties": { @@ -5838,6 +6744,17 @@ } } }, + "models.Token": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, "server.Stub": { "type": "object" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9776c59ece..289a8a7cb0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -298,6 +298,21 @@ definitions: strategy: type: string type: object + datastore.Organisation: + properties: + created_at: + type: string + deleted_at: + type: string + name: + type: string + owner_id: + type: string + uid: + type: string + updated_at: + type: string + type: object datastore.PaginationData: properties: next: @@ -339,9 +354,9 @@ definitions: datastore.Source: properties: created_at: - type: integer + type: string deleted_at: - type: integer + type: string group_id: type: string is_disabled: @@ -355,7 +370,7 @@ definitions: uid: type: string updated_at: - type: integer + type: string verifier: $ref: '#/definitions/datastore.VerifierConfig' type: object @@ -374,9 +389,9 @@ definitions: $ref: '#/definitions/datastore.AlertConfiguration' description: subscription config created_at: - type: integer - deleted_at: - type: integer + type: string + delted_at: + type: string endpoint: $ref: '#/definitions/datastore.Endpoint' filter_config: @@ -394,7 +409,7 @@ definitions: uid: type: string updated_at: - type: integer + type: string type: object datastore.VerifierConfig: properties: @@ -494,6 +509,39 @@ definitions: type: type: string type: object + models.LoginUser: + properties: + password: + type: string + username: + type: string + type: object + models.LoginUserResponse: + properties: + created_at: + type: integer + deleted_at: + type: integer + email: + type: string + first_name: + type: string + last_name: + type: string + role: + $ref: '#/definitions/auth.Role' + token: + $ref: '#/definitions/models.Token' + uid: + type: string + updated_at: + type: integer + type: object + models.Organisation: + properties: + name: + type: string + type: object models.PortalAPIKeyResponse: properties: app_id: @@ -564,6 +612,13 @@ definitions: type: type: string type: object + models.Token: + properties: + access_token: + type: string + refresh_token: + type: string + type: object server.Stub: type: object server.pagedResponse: @@ -1246,6 +1301,163 @@ paths: summary: Update an application endpoint tags: - Application Endpoints + /auth/login: + post: + consumes: + - application/json + description: This endpoint logs in a user + parameters: + - description: User Details + in: body + name: user + required: true + schema: + $ref: '#/definitions/models.LoginUser' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/models.LoginUserResponse' + type: object + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + summary: Login a user + tags: + - User + /auth/logout: + post: + consumes: + - application/json + description: This endpoint logs out a user + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + security: + - ApiKeyAuth: [] + summary: Logs out a user + tags: + - User + /auth/token/refresh: + post: + consumes: + - application/json + description: This endpoint refreshes an access token + parameters: + - description: Token Details + in: body + name: token + required: true + schema: + $ref: '#/definitions/models.Token' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/models.Token' + type: object + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + summary: Refresh an access token + tags: + - User /eventdeliveries: get: consumes: @@ -2046,6 +2258,71 @@ paths: summary: Get delivery attempt tags: - DeliveryAttempts + /events/{eventID}/replay: + put: + consumes: + - application/json + description: This endpoint replays an app event + parameters: + - description: group id + in: query + name: groupId + required: true + type: string + - description: event id + in: path + name: eventID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + allOf: + - $ref: '#/definitions/datastore.Event' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + type: object + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + security: + - ApiKeyAuth: [] + summary: Replay app event + tags: + - Events /groups: get: consumes: @@ -2326,6 +2603,226 @@ paths: summary: Update a group tags: - Group + /organisations: + get: + consumes: + - application/json + description: This endpoint fetches multiple organisations + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + allOf: + - $ref: '#/definitions/server.pagedResponse' + - properties: + content: + items: + $ref: '#/definitions/datastore.Organisation' + type: array + type: object + type: object + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + security: + - ApiKeyAuth: [] + summary: Get organisations + tags: + - Organisation + post: + consumes: + - application/json + description: This endpoint creates an organisation + parameters: + - description: Organisation Details + in: body + name: application + required: true + schema: + $ref: '#/definitions/models.Organisation' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/datastore.Organisation' + type: object + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + security: + - ApiKeyAuth: [] + summary: Create an organisation + tags: + - Application + /organisations/{orgID}: + get: + consumes: + - application/json + description: This endpoint fetches an organisation by its id + parameters: + - description: organisation id + in: path + name: orgID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/datastore.Organisation' + type: object + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + security: + - ApiKeyAuth: [] + summary: Get an organisation + tags: + - Organisation + put: + consumes: + - application/json + description: This endpoint deletes an organisation + parameters: + - description: organisation id + in: path + name: orgID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "401": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/server.serverResponse' + - properties: + data: + $ref: '#/definitions/server.Stub' + type: object + security: + - ApiKeyAuth: [] + summary: Delete organisation + tags: + - Organisation /security/applications/{appID}/keys: post: consumes: diff --git a/docs/v3/openapi3.json b/docs/v3/openapi3.json index ca25bb9911..c9669e07cf 100644 --- a/docs/v3/openapi3.json +++ b/docs/v3/openapi3.json @@ -438,6 +438,29 @@ }, "type": "object" }, + "datastore.Organisation": { + "properties": { + "created_at": { + "type": "string" + }, + "deleted_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "type": "object" + }, "datastore.PaginationData": { "properties": { "next": { @@ -500,10 +523,10 @@ "datastore.Source": { "properties": { "created_at": { - "type": "integer" + "type": "string" }, "deleted_at": { - "type": "integer" + "type": "string" }, "group_id": { "type": "string" @@ -524,7 +547,7 @@ "type": "string" }, "updated_at": { - "type": "integer" + "type": "string" }, "verifier": { "$ref": "#/components/schemas/datastore.VerifierConfig" @@ -552,10 +575,10 @@ "$ref": "#/components/schemas/datastore.AlertConfiguration" }, "created_at": { - "type": "integer" + "type": "string" }, - "deleted_at": { - "type": "integer" + "delted_at": { + "type": "string" }, "endpoint": { "$ref": "#/components/schemas/datastore.Endpoint" @@ -582,7 +605,7 @@ "type": "string" }, "updated_at": { - "type": "integer" + "type": "string" } }, "type": "object" @@ -734,6 +757,57 @@ }, "type": "object" }, + "models.LoginUser": { + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "models.LoginUserResponse": { + "properties": { + "created_at": { + "type": "integer" + }, + "deleted_at": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/auth.Role" + }, + "token": { + "$ref": "#/components/schemas/models.Token" + }, + "uid": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + }, + "type": "object" + }, + "models.Organisation": { + "properties": { + "name": { + "type": "string" + } + }, + "type": "object" + }, "models.PortalAPIKeyResponse": { "properties": { "app_id": { @@ -841,6 +915,17 @@ }, "type": "object" }, + "models.Token": { + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + }, + "type": "object" + }, "server.Stub": { "type": "object" }, @@ -2231,87 +2316,21 @@ ] } }, - "/eventdeliveries": { - "get": { - "description": "This endpoint fetch event deliveries.", - "parameters": [ - { - "description": "application id", - "in": "query", - "name": "appId", - "schema": { - "type": "string" - } - }, - { - "description": "group id", - "in": "query", - "name": "groupId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "event id", - "in": "query", - "name": "eventId", - "schema": { - "type": "string" - } - }, - { - "description": "start date", - "in": "query", - "name": "startDate", - "schema": { - "type": "string" - } - }, - { - "description": "end date", - "in": "query", - "name": "endDate", - "schema": { - "type": "string" - } - }, - { - "description": "results per page", - "in": "query", - "name": "perPage", - "schema": { - "type": "string" - } - }, - { - "description": "page number", - "in": "query", - "name": "page", - "schema": { - "type": "string" - } - }, - { - "description": "sort order", - "in": "query", - "name": "sort", - "schema": { - "type": "string" + "/auth/login": { + "post": { + "description": "This endpoint logs in a user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/models.LoginUser" + } } }, - { - "description": "status", - "in": "query", - "name": "status", - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - } - ], + "description": "User Details", + "required": true, + "x-originalParamName": "user" + }, "responses": { "200": { "content": { @@ -2324,34 +2343,7 @@ { "properties": { "data": { - "allOf": [ - { - "$ref": "#/components/schemas/server.pagedResponse" - }, - { - "properties": { - "content": { - "items": { - "allOf": [ - { - "$ref": "#/components/schemas/datastore.EventDelivery" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/server.Stub" - } - }, - "type": "object" - } - ] - }, - "type": "array" - } - }, - "type": "object" - } - ] + "$ref": "#/components/schemas/models.LoginUserResponse" } }, "type": "object" @@ -2429,58 +2421,15 @@ "description": "Internal Server Error" } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "summary": "Get event deliveries", + "summary": "Login a user", "tags": [ - "EventDelivery" + "User" ] } }, - "/eventdeliveries/batchretry": { + "/auth/logout": { "post": { - "description": "This endpoint resends multiple app events", - "parameters": [ - { - "description": "group id", - "in": "query", - "name": "groupId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/server.Stub" - }, - { - "properties": { - "ids": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - } - ] - } - } - }, - "description": "event delivery ids", - "required": true, - "x-originalParamName": "ids" - }, + "description": "This endpoint logs out a user", "responses": { "200": { "content": { @@ -2576,26 +2525,1077 @@ "ApiKeyAuth": [] } ], - "summary": "Batch Resend app events", + "summary": "Logs out a user", "tags": [ - "EventDelivery" + "User" ] } }, - "/eventdeliveries/countbatchretryevents": { - "get": { - "description": "This endpoint counts app events that will be affected by a batch retry operation", - "parameters": [ - { - "description": "application id", - "in": "query", + "/auth/token/refresh": { + "post": { + "description": "This endpoint refreshes an access token", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/models.Token" + } + } + }, + "description": "Token Details", + "required": true, + "x-originalParamName": "token" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/models.Token" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Refresh an access token", + "tags": [ + "User" + ] + } + }, + "/eventdeliveries": { + "get": { + "description": "This endpoint fetch event deliveries.", + "parameters": [ + { + "description": "application id", + "in": "query", + "name": "appId", + "schema": { + "type": "string" + } + }, + { + "description": "group id", + "in": "query", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "event id", + "in": "query", + "name": "eventId", + "schema": { + "type": "string" + } + }, + { + "description": "start date", + "in": "query", + "name": "startDate", + "schema": { + "type": "string" + } + }, + { + "description": "end date", + "in": "query", + "name": "endDate", + "schema": { + "type": "string" + } + }, + { + "description": "results per page", + "in": "query", + "name": "perPage", + "schema": { + "type": "string" + } + }, + { + "description": "page number", + "in": "query", + "name": "page", + "schema": { + "type": "string" + } + }, + { + "description": "sort order", + "in": "query", + "name": "sort", + "schema": { + "type": "string" + } + }, + { + "description": "status", + "in": "query", + "name": "status", + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/server.pagedResponse" + }, + { + "properties": { + "content": { + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/datastore.EventDelivery" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + }, + "type": "array" + } + }, + "type": "object" + } + ] + } + }, + "type": "object" + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "summary": "Get event deliveries", + "tags": [ + "EventDelivery" + ] + } + }, + "/eventdeliveries/batchretry": { + "post": { + "description": "This endpoint resends multiple app events", + "parameters": [ + { + "description": "group id", + "in": "query", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.Stub" + }, + { + "properties": { + "ids": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "event delivery ids", + "required": true, + "x-originalParamName": "ids" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "summary": "Batch Resend app events", + "tags": [ + "EventDelivery" + ] + } + }, + "/eventdeliveries/countbatchretryevents": { + "get": { + "description": "This endpoint counts app events that will be affected by a batch retry operation", + "parameters": [ + { + "description": "application id", + "in": "query", + "name": "appId", + "schema": { + "type": "string" + } + }, + { + "description": "group Id", + "in": "query", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "start date", + "in": "query", + "name": "startDate", + "schema": { + "type": "string" + } + }, + { + "description": "end date", + "in": "query", + "name": "endDate", + "schema": { + "type": "string" + } + }, + { + "description": "results per page", + "in": "query", + "name": "perPage", + "schema": { + "type": "string" + } + }, + { + "description": "page number", + "in": "query", + "name": "page", + "schema": { + "type": "string" + } + }, + { + "description": "sort order", + "in": "query", + "name": "sort", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/server.Stub" + }, + { + "properties": { + "num": { + "type": "integer" + } + }, + "type": "object" + } + ] + } + }, + "type": "object" + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "summary": "Count affected eventDeliveries", + "tags": [ + "EventDelivery" + ] + } + }, + "/eventdeliveries/forceresend": { + "post": { + "description": "This endpoint force resends multiple app events", + "parameters": [ + { + "description": "group Id", + "in": "query", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.Stub" + }, + { + "properties": { + "ids": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "event delivery ids", + "required": true, + "x-originalParamName": "ids" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "summary": "Force Resend app events", + "tags": [ + "EventDelivery" + ] + } + }, + "/eventdeliveries/{eventDeliveryID}": { + "get": { + "description": "This endpoint fetches an event delivery.", + "parameters": [ + { + "description": "group id", + "in": "query", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "event delivery id", + "in": "path", + "name": "eventDeliveryID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/datastore.Event" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + }, + "type": "object" + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "summary": "Get event delivery", + "tags": [ + "EventDelivery" + ] + } + }, + "/eventdeliveries/{eventDeliveryID}/resend": { + "put": { + "description": "This endpoint resends an app event", + "parameters": [ + { + "description": "group id", + "in": "query", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "event delivery id", + "in": "path", + "name": "eventDeliveryID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/datastore.Event" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + }, + "type": "object" + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "summary": "Resend an app event", + "tags": [ + "EventDelivery" + ] + } + }, + "/events": { + "get": { + "description": "This endpoint fetches app events with pagination", + "parameters": [ + { + "description": "application id", + "in": "query", "name": "appId", "schema": { "type": "string" } }, { - "description": "group Id", + "description": "group id", "in": "query", "name": "groupId", "required": true, @@ -2658,12 +3658,164 @@ "data": { "allOf": [ { - "$ref": "#/components/schemas/server.Stub" + "$ref": "#/components/schemas/server.pagedResponse" + }, + { + "properties": { + "content": { + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/datastore.Event" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + }, + "type": "array" + } + }, + "type": "object" + } + ] + } + }, + "type": "object" + } + ] + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "summary": "Get app events with pagination", + "tags": [ + "Events" + ] + }, + "post": { + "description": "This endpoint creates an app event", + "parameters": [ + { + "description": "group id", + "in": "query", + "name": "groupId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/models.Event" + } + } + }, + "description": "Event Details", + "required": true, + "x-originalParamName": "event" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/server.serverResponse" + }, + { + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/datastore.Event" }, { "properties": { - "num": { - "type": "integer" + "data": { + "$ref": "#/components/schemas/server.Stub" } }, "type": "object" @@ -2751,53 +3903,35 @@ "ApiKeyAuth": [] } ], - "summary": "Count affected eventDeliveries", + "summary": "Create app event", "tags": [ - "EventDelivery" + "Events" ] } }, - "/eventdeliveries/forceresend": { - "post": { - "description": "This endpoint force resends multiple app events", + "/events/{eventID}": { + "get": { + "description": "This endpoint fetches an app event", "parameters": [ { - "description": "group Id", + "description": "group id", "in": "query", "name": "groupId", "required": true, "schema": { "type": "string" } + }, + { + "description": "event id", + "in": "path", + "name": "eventID", + "required": true, + "schema": { + "type": "string" + } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/server.Stub" - }, - { - "properties": { - "ids": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - } - ] - } - } - }, - "description": "event delivery ids", - "required": true, - "x-originalParamName": "ids" - }, "responses": { "200": { "content": { @@ -2810,7 +3944,19 @@ { "properties": { "data": { - "$ref": "#/components/schemas/server.Stub" + "allOf": [ + { + "$ref": "#/components/schemas/datastore.Event" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/server.Stub" + } + }, + "type": "object" + } + ] } }, "type": "object" @@ -2893,20 +4039,20 @@ "ApiKeyAuth": [] } ], - "summary": "Force Resend app events", + "summary": "Get app event", "tags": [ - "EventDelivery" + "Events" ] } }, - "/eventdeliveries/{eventDeliveryID}": { + "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts": { "get": { - "description": "This endpoint fetches an event delivery.", + "description": "This endpoint fetches an app message's delivery attempts", "parameters": [ { - "description": "group id", - "in": "query", - "name": "groupId", + "description": "event id", + "in": "path", + "name": "eventID", "required": true, "schema": { "type": "string" @@ -2934,19 +4080,10 @@ { "properties": { "data": { - "allOf": [ - { - "$ref": "#/components/schemas/datastore.Event" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/server.Stub" - } - }, - "type": "object" - } - ] + "items": { + "$ref": "#/components/schemas/datastore.DeliveryAttempt" + }, + "type": "array" } }, "type": "object" @@ -3029,20 +4166,20 @@ "ApiKeyAuth": [] } ], - "summary": "Get event delivery", + "summary": "Get delivery attempts", "tags": [ - "EventDelivery" + "DeliveryAttempts" ] } }, - "/eventdeliveries/{eventDeliveryID}/resend": { - "put": { - "description": "This endpoint resends an app event", + "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts/{deliveryAttemptID}": { + "get": { + "description": "This endpoint fetches an app event delivery attempt", "parameters": [ { - "description": "group id", - "in": "query", - "name": "groupId", + "description": "event id", + "in": "path", + "name": "eventID", "required": true, "schema": { "type": "string" @@ -3056,6 +4193,15 @@ "schema": { "type": "string" } + }, + { + "description": "delivery attempt id", + "in": "path", + "name": "deliveryAttemptID", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { @@ -3070,19 +4216,7 @@ { "properties": { "data": { - "allOf": [ - { - "$ref": "#/components/schemas/datastore.Event" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/server.Stub" - } - }, - "type": "object" - } - ] + "$ref": "#/components/schemas/datastore.DeliveryAttempt" } }, "type": "object" @@ -3165,24 +4299,16 @@ "ApiKeyAuth": [] } ], - "summary": "Resend an app event", + "summary": "Get delivery attempt", "tags": [ - "EventDelivery" + "DeliveryAttempts" ] } }, - "/events": { - "get": { - "description": "This endpoint fetches app events with pagination", + "/events/{eventID}/replay": { + "put": { + "description": "This endpoint replays an app event", "parameters": [ - { - "description": "application id", - "in": "query", - "name": "appId", - "schema": { - "type": "string" - } - }, { "description": "group id", "in": "query", @@ -3193,41 +4319,10 @@ } }, { - "description": "start date", - "in": "query", - "name": "startDate", - "schema": { - "type": "string" - } - }, - { - "description": "end date", - "in": "query", - "name": "endDate", - "schema": { - "type": "string" - } - }, - { - "description": "results per page", - "in": "query", - "name": "perPage", - "schema": { - "type": "string" - } - }, - { - "description": "page number", - "in": "query", - "name": "page", - "schema": { - "type": "string" - } - }, - { - "description": "sort order", - "in": "query", - "name": "sort", + "description": "event id", + "in": "path", + "name": "eventID", + "required": true, "schema": { "type": "string" } @@ -3247,27 +4342,12 @@ "data": { "allOf": [ { - "$ref": "#/components/schemas/server.pagedResponse" + "$ref": "#/components/schemas/datastore.Event" }, { "properties": { - "content": { - "items": { - "allOf": [ - { - "$ref": "#/components/schemas/datastore.Event" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/server.Stub" - } - }, - "type": "object" - } - ] - }, - "type": "array" + "data": { + "$ref": "#/components/schemas/server.Stub" } }, "type": "object" @@ -3355,36 +4435,25 @@ "ApiKeyAuth": [] } ], - "summary": "Get app events with pagination", + "summary": "Replay app event", "tags": [ "Events" ] - }, - "post": { - "description": "This endpoint creates an app event", + } + }, + "/groups": { + "get": { + "description": "This endpoint fetches groups", "parameters": [ { - "description": "group id", + "description": "group name", "in": "query", - "name": "groupId", - "required": true, + "name": "name", "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/models.Event" - } - } - }, - "description": "Event Details", - "required": true, - "x-originalParamName": "event" - }, "responses": { "200": { "content": { @@ -3393,23 +4462,14 @@ "allOf": [ { "$ref": "#/components/schemas/server.serverResponse" - }, - { - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/components/schemas/datastore.Event" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/server.Stub" - } - }, - "type": "object" - } - ] + }, + { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/datastore.Group" + }, + "type": "array" } }, "type": "object" @@ -3492,35 +4552,25 @@ "ApiKeyAuth": [] } ], - "summary": "Create app event", + "summary": "Get groups", "tags": [ - "Events" + "Group" ] - } - }, - "/events/{eventID}": { - "get": { - "description": "This endpoint fetches an app event", - "parameters": [ - { - "description": "group id", - "in": "query", - "name": "groupId", - "required": true, - "schema": { - "type": "string" + }, + "post": { + "description": "This endpoint creates a group", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/models.Group" + } } }, - { - "description": "event id", - "in": "path", - "name": "eventID", - "required": true, - "schema": { - "type": "string" - } - } - ], + "description": "Group Details", + "required": true, + "x-originalParamName": "group" + }, "responses": { "200": { "content": { @@ -3533,19 +4583,7 @@ { "properties": { "data": { - "allOf": [ - { - "$ref": "#/components/schemas/datastore.Event" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/server.Stub" - } - }, - "type": "object" - } - ] + "$ref": "#/components/schemas/datastore.Group" } }, "type": "object" @@ -3628,29 +4666,20 @@ "ApiKeyAuth": [] } ], - "summary": "Get app event", + "summary": "Create a group", "tags": [ - "Events" + "Group" ] } }, - "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts": { - "get": { - "description": "This endpoint fetches an app message's delivery attempts", + "/groups/{groupID}": { + "delete": { + "description": "This endpoint deletes a group using its id", "parameters": [ { - "description": "event id", - "in": "path", - "name": "eventID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "event delivery id", + "description": "group id", "in": "path", - "name": "eventDeliveryID", + "name": "groupID", "required": true, "schema": { "type": "string" @@ -3669,10 +4698,7 @@ { "properties": { "data": { - "items": { - "$ref": "#/components/schemas/datastore.DeliveryAttempt" - }, - "type": "array" + "$ref": "#/components/schemas/server.Stub" } }, "type": "object" @@ -3755,38 +4781,18 @@ "ApiKeyAuth": [] } ], - "summary": "Get delivery attempts", + "summary": "Delete a group", "tags": [ - "DeliveryAttempts" + "Group" ] - } - }, - "/events/{eventID}/eventdeliveries/{eventDeliveryID}/deliveryattempts/{deliveryAttemptID}": { + }, "get": { - "description": "This endpoint fetches an app event delivery attempt", + "description": "This endpoint fetches a group by its id", "parameters": [ { - "description": "event id", - "in": "path", - "name": "eventID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "event delivery id", - "in": "path", - "name": "eventDeliveryID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "delivery attempt id", + "description": "group id", "in": "path", - "name": "deliveryAttemptID", + "name": "groupID", "required": true, "schema": { "type": "string" @@ -3805,7 +4811,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/datastore.DeliveryAttempt" + "$ref": "#/components/schemas/datastore.Group" } }, "type": "object" @@ -3888,25 +4894,36 @@ "ApiKeyAuth": [] } ], - "summary": "Get delivery attempt", + "summary": "Get a group", "tags": [ - "DeliveryAttempts" + "Group" ] - } - }, - "/groups": { - "get": { - "description": "This endpoint fetches groups", + }, + "put": { + "description": "This endpoint updates a group", "parameters": [ { - "description": "group name", - "in": "query", - "name": "name", + "description": "group id", + "in": "path", + "name": "groupID", + "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/models.Group" + } + } + }, + "description": "Group Details", + "required": true, + "x-originalParamName": "group" + }, "responses": { "200": { "content": { @@ -3919,10 +4936,7 @@ { "properties": { "data": { - "items": { - "$ref": "#/components/schemas/datastore.Group" - }, - "type": "array" + "$ref": "#/components/schemas/datastore.Group" } }, "type": "object" @@ -4005,25 +5019,15 @@ "ApiKeyAuth": [] } ], - "summary": "Get groups", + "summary": "Update a group", "tags": [ "Group" ] - }, - "post": { - "description": "This endpoint creates a group", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/models.Group" - } - } - }, - "description": "Group Details", - "required": true, - "x-originalParamName": "group" - }, + } + }, + "/organisations": { + "get": { + "description": "This endpoint fetches multiple organisations", "responses": { "200": { "content": { @@ -4036,7 +5040,22 @@ { "properties": { "data": { - "$ref": "#/components/schemas/datastore.Group" + "allOf": [ + { + "$ref": "#/components/schemas/server.pagedResponse" + }, + { + "properties": { + "content": { + "items": { + "$ref": "#/components/schemas/datastore.Organisation" + }, + "type": "array" + } + }, + "type": "object" + } + ] } }, "type": "object" @@ -4119,26 +5138,25 @@ "ApiKeyAuth": [] } ], - "summary": "Create a group", + "summary": "Get organisations", "tags": [ - "Group" + "Organisation" ] - } - }, - "/groups/{groupID}": { - "delete": { - "description": "This endpoint deletes a group using its id", - "parameters": [ - { - "description": "group id", - "in": "path", - "name": "groupID", - "required": true, - "schema": { - "type": "string" + }, + "post": { + "description": "This endpoint creates an organisation", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/models.Organisation" + } } - } - ], + }, + "description": "Organisation Details", + "required": true, + "x-originalParamName": "application" + }, "responses": { "200": { "content": { @@ -4151,7 +5169,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/server.Stub" + "$ref": "#/components/schemas/datastore.Organisation" } }, "type": "object" @@ -4234,18 +5252,20 @@ "ApiKeyAuth": [] } ], - "summary": "Delete a group", + "summary": "Create an organisation", "tags": [ - "Group" + "Application" ] - }, + } + }, + "/organisations/{orgID}": { "get": { - "description": "This endpoint fetches a group by its id", + "description": "This endpoint fetches an organisation by its id", "parameters": [ { - "description": "group id", + "description": "organisation id", "in": "path", - "name": "groupID", + "name": "orgID", "required": true, "schema": { "type": "string" @@ -4264,7 +5284,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/datastore.Group" + "$ref": "#/components/schemas/datastore.Organisation" } }, "type": "object" @@ -4347,36 +5367,24 @@ "ApiKeyAuth": [] } ], - "summary": "Get a group", + "summary": "Get an organisation", "tags": [ - "Group" + "Organisation" ] }, "put": { - "description": "This endpoint updates a group", + "description": "This endpoint deletes an organisation", "parameters": [ { - "description": "group id", + "description": "organisation id", "in": "path", - "name": "groupID", + "name": "orgID", "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/models.Group" - } - } - }, - "description": "Group Details", - "required": true, - "x-originalParamName": "group" - }, "responses": { "200": { "content": { @@ -4389,7 +5397,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/datastore.Group" + "$ref": "#/components/schemas/server.Stub" } }, "type": "object" @@ -4472,9 +5480,9 @@ "ApiKeyAuth": [] } ], - "summary": "Update a group", + "summary": "Delete organisation", "tags": [ - "Group" + "Organisation" ] } }, diff --git a/docs/v3/openapi3.yaml b/docs/v3/openapi3.yaml index 7d4bb94563..1889a80158 100644 --- a/docs/v3/openapi3.yaml +++ b/docs/v3/openapi3.yaml @@ -298,6 +298,21 @@ components: strategy: type: string type: object + datastore.Organisation: + properties: + created_at: + type: string + deleted_at: + type: string + name: + type: string + owner_id: + type: string + uid: + type: string + updated_at: + type: string + type: object datastore.PaginationData: properties: next: @@ -339,9 +354,9 @@ components: datastore.Source: properties: created_at: - type: integer + type: string deleted_at: - type: integer + type: string group_id: type: string is_disabled: @@ -355,7 +370,7 @@ components: uid: type: string updated_at: - type: integer + type: string verifier: $ref: '#/components/schemas/datastore.VerifierConfig' type: object @@ -373,9 +388,9 @@ components: alert_config: $ref: '#/components/schemas/datastore.AlertConfiguration' created_at: - type: integer - deleted_at: - type: integer + type: string + delted_at: + type: string endpoint: $ref: '#/components/schemas/datastore.Endpoint' filter_config: @@ -393,7 +408,7 @@ components: uid: type: string updated_at: - type: integer + type: string type: object datastore.VerifierConfig: properties: @@ -493,6 +508,39 @@ components: type: type: string type: object + models.LoginUser: + properties: + password: + type: string + username: + type: string + type: object + models.LoginUserResponse: + properties: + created_at: + type: integer + deleted_at: + type: integer + email: + type: string + first_name: + type: string + last_name: + type: string + role: + $ref: '#/components/schemas/auth.Role' + token: + $ref: '#/components/schemas/models.Token' + uid: + type: string + updated_at: + type: integer + type: object + models.Organisation: + properties: + name: + type: string + type: object models.PortalAPIKeyResponse: properties: app_id: @@ -563,6 +611,13 @@ components: type: type: string type: object + models.Token: + properties: + access_token: + type: string + refresh_token: + type: string + type: object server.Stub: type: object server.pagedResponse: @@ -1323,6 +1378,177 @@ paths: summary: Update an application endpoint tags: - Application Endpoints + /auth/login: + post: + description: This endpoint logs in a user + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/models.LoginUser' + description: User Details + required: true + x-originalParamName: user + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/models.LoginUserResponse' + type: object + description: OK + "400": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Bad Request + "401": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Unauthorized + "500": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Internal Server Error + summary: Login a user + tags: + - User + /auth/logout: + post: + description: This endpoint logs out a user + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: OK + "400": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Bad Request + "401": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Unauthorized + "500": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Internal Server Error + security: + - ApiKeyAuth: [] + summary: Logs out a user + tags: + - User + /auth/token/refresh: + post: + description: This endpoint refreshes an access token + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/models.Token' + description: Token Details + required: true + x-originalParamName: token + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/models.Token' + type: object + description: OK + "400": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Bad Request + "401": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Unauthorized + "500": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Internal Server Error + summary: Refresh an access token + tags: + - User /eventdeliveries: get: description: This endpoint fetch event deliveries. @@ -2210,6 +2436,77 @@ paths: summary: Get delivery attempt tags: - DeliveryAttempts + /events/{eventID}/replay: + put: + description: This endpoint replays an app event + parameters: + - description: group id + in: query + name: groupId + required: true + schema: + type: string + - description: event id + in: path + name: eventID + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + allOf: + - $ref: '#/components/schemas/datastore.Event' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + type: object + description: OK + "400": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Bad Request + "401": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Unauthorized + "500": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Internal Server Error + security: + - ApiKeyAuth: [] + summary: Replay app event + tags: + - Events /groups: get: description: This endpoint fetches groups @@ -2517,6 +2814,245 @@ paths: summary: Update a group tags: - Group + /organisations: + get: + description: This endpoint fetches multiple organisations + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + allOf: + - $ref: '#/components/schemas/server.pagedResponse' + - properties: + content: + items: + $ref: '#/components/schemas/datastore.Organisation' + type: array + type: object + type: object + description: OK + "400": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Bad Request + "401": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Unauthorized + "500": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Internal Server Error + security: + - ApiKeyAuth: [] + summary: Get organisations + tags: + - Organisation + post: + description: This endpoint creates an organisation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/models.Organisation' + description: Organisation Details + required: true + x-originalParamName: application + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/datastore.Organisation' + type: object + description: OK + "400": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Bad Request + "401": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Unauthorized + "500": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Internal Server Error + security: + - ApiKeyAuth: [] + summary: Create an organisation + tags: + - Application + /organisations/{orgID}: + get: + description: This endpoint fetches an organisation by its id + parameters: + - description: organisation id + in: path + name: orgID + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/datastore.Organisation' + type: object + description: OK + "400": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Bad Request + "401": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Unauthorized + "500": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Internal Server Error + security: + - ApiKeyAuth: [] + summary: Get an organisation + tags: + - Organisation + put: + description: This endpoint deletes an organisation + parameters: + - description: organisation id + in: path + name: orgID + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: OK + "400": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Bad Request + "401": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Unauthorized + "500": + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/server.serverResponse' + - properties: + data: + $ref: '#/components/schemas/server.Stub' + type: object + description: Internal Server Error + security: + - ApiKeyAuth: [] + summary: Delete organisation + tags: + - Organisation /security/applications/{appID}/keys: post: description: This endpoint creates an api key that will be used by app portal diff --git a/go.mod b/go.mod index 6645926e97..5da7ed48a5 100644 --- a/go.mod +++ b/go.mod @@ -15,13 +15,15 @@ require ( github.com/getkin/kin-openapi v0.80.0 github.com/getsentry/sentry-go v0.11.0 github.com/ghodss/yaml v1.0.0 - github.com/go-chi/chi/v5 v5.0.3 + github.com/go-chi/chi/v5 v5.0.6 github.com/go-chi/httprate v0.5.2 github.com/go-chi/render v1.0.1 + github.com/go-co-op/gocron v1.13.0 github.com/go-redis/cache/v8 v8.4.3 github.com/go-redis/redis/v8 v8.11.4 github.com/go-redis/redis_rate/v9 v9.1.2 github.com/gobeam/mongo-go-pagination v0.0.7 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.7 // indirect github.com/google/uuid v1.3.0 @@ -52,6 +54,7 @@ require ( github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect go.mongodb.org/mongo-driver v1.8.4 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 + golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect golang.org/x/tools v0.1.7 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df diff --git a/go.sum b/go.sum index b77a148b85..99090594f7 100644 --- a/go.sum +++ b/go.sum @@ -382,12 +382,14 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/ github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= -github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4= -github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.6 h1:CHIMAkr36TRf/zYvOqNKklMDxEm9HuqdiK+syK+tYtw= +github.com/go-chi/chi/v5 v5.0.6/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/httprate v0.5.2 h1:pynJZu4jbSSHFRjpbT7EJJf8b9qt2CLZnqqKy0F+jH4= github.com/go-chi/httprate v0.5.2/go.mod h1:kYR4lorHX3It9tTh4eTdHhcF2bzrYnCrRNlv5+IBm2M= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/go-co-op/gocron v1.13.0 h1:BjkuNImPy5NuIPEifhWItFG7pYyr27cyjS6BN9w/D4c= +github.com/go-co-op/gocron v1.13.0/go.mod h1:GD5EIEly1YNW+LovFVx5dzbYVcIc8544K99D8UVRpGo= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -493,6 +495,8 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU= github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -967,6 +971,8 @@ github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3x github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1334,8 +1340,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/mocks/repository.go b/mocks/repository.go index 7bfa87314b..fdc7c05413 100644 --- a/mocks/repository.go +++ b/mocks/repository.go @@ -559,6 +559,102 @@ func (mr *MockGroupRepositoryMockRecorder) UpdateGroup(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGroup", reflect.TypeOf((*MockGroupRepository)(nil).UpdateGroup), arg0, arg1) } +// MockOrganisationRepository is a mock of OrganisationRepository interface. +type MockOrganisationRepository struct { + ctrl *gomock.Controller + recorder *MockOrganisationRepositoryMockRecorder +} + +// MockOrganisationRepositoryMockRecorder is the mock recorder for MockOrganisationRepository. +type MockOrganisationRepositoryMockRecorder struct { + mock *MockOrganisationRepository +} + +// NewMockOrganisationRepository creates a new mock instance. +func NewMockOrganisationRepository(ctrl *gomock.Controller) *MockOrganisationRepository { + mock := &MockOrganisationRepository{ctrl: ctrl} + mock.recorder = &MockOrganisationRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOrganisationRepository) EXPECT() *MockOrganisationRepositoryMockRecorder { + return m.recorder +} + +// CreateOrganisation mocks base method. +func (m *MockOrganisationRepository) CreateOrganisation(arg0 context.Context, arg1 *datastore.Organisation) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrganisation", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateOrganisation indicates an expected call of CreateOrganisation. +func (mr *MockOrganisationRepositoryMockRecorder) CreateOrganisation(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrganisation", reflect.TypeOf((*MockOrganisationRepository)(nil).CreateOrganisation), arg0, arg1) +} + +// DeleteOrganisation mocks base method. +func (m *MockOrganisationRepository) DeleteOrganisation(ctx context.Context, uid string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteOrganisation", ctx, uid) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteOrganisation indicates an expected call of DeleteOrganisation. +func (mr *MockOrganisationRepositoryMockRecorder) DeleteOrganisation(ctx, uid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganisation", reflect.TypeOf((*MockOrganisationRepository)(nil).DeleteOrganisation), ctx, uid) +} + +// FetchOrganisationByID mocks base method. +func (m *MockOrganisationRepository) FetchOrganisationByID(arg0 context.Context, arg1 string) (*datastore.Organisation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchOrganisationByID", arg0, arg1) + ret0, _ := ret[0].(*datastore.Organisation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchOrganisationByID indicates an expected call of FetchOrganisationByID. +func (mr *MockOrganisationRepositoryMockRecorder) FetchOrganisationByID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchOrganisationByID", reflect.TypeOf((*MockOrganisationRepository)(nil).FetchOrganisationByID), arg0, arg1) +} + +// LoadOrganisationsPaged mocks base method. +func (m *MockOrganisationRepository) LoadOrganisationsPaged(arg0 context.Context, arg1 datastore.Pageable) ([]datastore.Organisation, datastore.PaginationData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadOrganisationsPaged", arg0, arg1) + ret0, _ := ret[0].([]datastore.Organisation) + ret1, _ := ret[1].(datastore.PaginationData) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// LoadOrganisationsPaged indicates an expected call of LoadOrganisationsPaged. +func (mr *MockOrganisationRepositoryMockRecorder) LoadOrganisationsPaged(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOrganisationsPaged", reflect.TypeOf((*MockOrganisationRepository)(nil).LoadOrganisationsPaged), arg0, arg1) +} + +// UpdateOrganisation mocks base method. +func (m *MockOrganisationRepository) UpdateOrganisation(arg0 context.Context, arg1 *datastore.Organisation) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateOrganisation", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateOrganisation indicates an expected call of UpdateOrganisation. +func (mr *MockOrganisationRepositoryMockRecorder) UpdateOrganisation(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOrganisation", reflect.TypeOf((*MockOrganisationRepository)(nil).UpdateOrganisation), arg0, arg1) +} + // MockApplicationRepository is a mock of ApplicationRepository interface. type MockApplicationRepository struct { ctrl *gomock.Controller @@ -980,3 +1076,86 @@ func (mr *MockSourceRepositoryMockRecorder) UpdateSource(ctx, groupID, source in mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSource", reflect.TypeOf((*MockSourceRepository)(nil).UpdateSource), ctx, groupID, source) } + +// MockUserRepository is a mock of UserRepository interface. +type MockUserRepository struct { + ctrl *gomock.Controller + recorder *MockUserRepositoryMockRecorder +} + +// MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository. +type MockUserRepositoryMockRecorder struct { + mock *MockUserRepository +} + +// NewMockUserRepository creates a new mock instance. +func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository { + mock := &MockUserRepository{ctrl: ctrl} + mock.recorder = &MockUserRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder { + return m.recorder +} + +// CreateUser mocks base method. +func (m *MockUserRepository) CreateUser(arg0 context.Context, arg1 *datastore.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockUserRepositoryMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockUserRepository)(nil).CreateUser), arg0, arg1) +} + +// FindUserByEmail mocks base method. +func (m *MockUserRepository) FindUserByEmail(arg0 context.Context, arg1 string) (*datastore.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindUserByEmail", arg0, arg1) + ret0, _ := ret[0].(*datastore.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindUserByEmail indicates an expected call of FindUserByEmail. +func (mr *MockUserRepositoryMockRecorder) FindUserByEmail(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUserByEmail", reflect.TypeOf((*MockUserRepository)(nil).FindUserByEmail), arg0, arg1) +} + +// FindUserByID mocks base method. +func (m *MockUserRepository) FindUserByID(arg0 context.Context, arg1 string) (*datastore.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindUserByID", arg0, arg1) + ret0, _ := ret[0].(*datastore.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindUserByID indicates an expected call of FindUserByID. +func (mr *MockUserRepositoryMockRecorder) FindUserByID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUserByID", reflect.TypeOf((*MockUserRepository)(nil).FindUserByID), arg0, arg1) +} + +// LoadUsersPaged mocks base method. +func (m *MockUserRepository) LoadUsersPaged(arg0 context.Context, arg1 datastore.Pageable) ([]datastore.User, datastore.PaginationData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadUsersPaged", arg0, arg1) + ret0, _ := ret[0].([]datastore.User) + ret1, _ := ret[1].(datastore.PaginationData) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// LoadUsersPaged indicates an expected call of LoadUsersPaged. +func (mr *MockUserRepositoryMockRecorder) LoadUsersPaged(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUsersPaged", reflect.TypeOf((*MockUserRepository)(nil).LoadUsersPaged), arg0, arg1) +} diff --git a/server/application.go b/server/application.go index 17223a7759..0046aab554 100644 --- a/server/application.go +++ b/server/application.go @@ -22,25 +22,29 @@ import ( ) type applicationHandler struct { - subService *services.SubcriptionService - appService *services.AppService - eventService *services.EventService - groupService *services.GroupService - securityService *services.SecurityService - sourceService *services.SourceService - appRepo datastore.ApplicationRepository - eventRepo datastore.EventRepository - eventDeliveryRepo datastore.EventDeliveryRepository - subRepo datastore.SubscriptionRepository - groupRepo datastore.GroupRepository - apiKeyRepo datastore.APIKeyRepository - sourceRepo datastore.SourceRepository - eventQueue queue.Queuer - createEventQueue queue.Queuer - logger logger.Logger - tracer tracer.Tracer - cache cache.Cache - limiter limiter.RateLimiter + appService *services.AppService + eventService *services.EventService + groupService *services.GroupService + securityService *services.SecurityService + sourceService *services.SourceService + subService *services.SubcriptionService + organisationService *services.OrganisationService + orgRepo datastore.OrganisationRepository + appRepo datastore.ApplicationRepository + eventRepo datastore.EventRepository + subRepo datastore.SubscriptionRepository + eventDeliveryRepo datastore.EventDeliveryRepository + groupRepo datastore.GroupRepository + apiKeyRepo datastore.APIKeyRepository + sourceRepo datastore.SourceRepository + eventQueue queue.Queuer + createEventQueue queue.Queuer + logger logger.Logger + tracer tracer.Tracer + cache cache.Cache + limiter limiter.RateLimiter + userService *services.UserService + userRepo datastore.UserRepository } type pagedResponse struct { @@ -56,6 +60,8 @@ func newApplicationHandler( apiKeyRepo datastore.APIKeyRepository, subRepo datastore.SubscriptionRepository, sourceRepo datastore.SourceRepository, + orgRepo datastore.OrganisationRepository, + userRepo datastore.UserRepository, eventQueue queue.Queuer, createEventQueue queue.Queuer, logger logger.Logger, @@ -70,29 +76,34 @@ func newApplicationHandler( as := services.NewAppService(appRepo, eventRepo, eventDeliveryRepo, eventQueue, cache) ss := services.NewSecurityService(groupRepo, apiKeyRepo) rs := services.NewSubscriptionService(subRepo) + os := services.NewOrganisationService(orgRepo) sos := services.NewSourceService(sourceRepo) + us := services.NewUserService(userRepo, cache) return &applicationHandler{ - appService: as, - eventService: es, - groupService: gs, - securityService: ss, - subService: rs, - sourceService: sos, - eventRepo: eventRepo, - eventDeliveryRepo: eventDeliveryRepo, - apiKeyRepo: apiKeyRepo, - groupRepo: groupRepo, - appRepo: appRepo, - subRepo: subRepo, - - createEventQueue: createEventQueue, - eventQueue: eventQueue, - limiter: limiter, - logger: logger, - tracer: tracer, - cache: cache, - sourceRepo: sourceRepo, + appService: as, + eventService: es, + groupService: gs, + securityService: ss, + organisationService: os, + subService: rs, + sourceService: sos, + orgRepo: orgRepo, + eventRepo: eventRepo, + eventDeliveryRepo: eventDeliveryRepo, + apiKeyRepo: apiKeyRepo, + appRepo: appRepo, + groupRepo: groupRepo, + sourceRepo: sourceRepo, + subRepo: subRepo, + eventQueue: eventQueue, + createEventQueue: createEventQueue, + logger: logger, + tracer: tracer, + cache: cache, + limiter: limiter, + userService: us, + userRepo: userRepo, } } diff --git a/server/application_integration_test.go b/server/application_integration_test.go index f0c1a192b9..799d5a49de 100644 --- a/server/application_integration_test.go +++ b/server/application_integration_test.go @@ -47,7 +47,7 @@ func (s *ApplicationIntegrationTestSuite) SetupTest() { err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") require.NoError(s.T(), err) - initRealmChain(s.T(), s.DB.APIRepo()) + initRealmChain(s.T(), s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) } func (s *ApplicationIntegrationTestSuite) TearDownTest() { diff --git a/server/dashboard_integration_test.go b/server/dashboard_integration_test.go index 47459827e5..62098c88e4 100644 --- a/server/dashboard_integration_test.go +++ b/server/dashboard_integration_test.go @@ -48,7 +48,7 @@ func (s *DashboardIntegrationTestSuite) SetupTest() { err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") require.NoError(s.T(), err) - initRealmChain(s.T(), s.DB.APIRepo()) + initRealmChain(s.T(), s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) } func (s *DashboardIntegrationTestSuite) TearDownTest() { @@ -271,7 +271,7 @@ func (s *DashboardIntegrationTestSuite) TestGetDashboardSummary() { if err != nil { t.Errorf("Failed to load config file: %v", err) } - initRealmChain(t, s.DB.APIRepo()) + initRealmChain(t, s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) req := httptest.NewRequest(tc.method, fmt.Sprintf("/ui/dashboard/summary?startDate=%s&endDate=%s&type=%s&groupId=%s", tc.urlQuery.startDate, tc.urlQuery.endDate, tc.urlQuery.Type, tc.urlQuery.groupID), nil) w := httptest.NewRecorder() diff --git a/server/event.go b/server/event.go index cfb5347860..279aedfc5a 100644 --- a/server/event.go +++ b/server/event.go @@ -46,6 +46,31 @@ func (a *applicationHandler) CreateAppEvent(w http.ResponseWriter, r *http.Reque _ = render.Render(w, r, newServerResponse("App event created successfully", event, http.StatusCreated)) } +// ReplayAppEvent +// @Summary Replay app event +// @Description This endpoint replays an app event +// @Tags Events +// @Accept json +// @Produce json +// @Param groupId query string true "group id" +// @Param eventID path string true "event id" +// @Success 200 {object} serverResponse{data=datastore.Event{data=Stub}} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Security ApiKeyAuth +// @Router /events/{eventID}/replay [put] +func (a *applicationHandler) ReplayAppEvent(w http.ResponseWriter, r *http.Request) { + g := getGroupFromContext(r.Context()) + event := getEventFromContext(r.Context()) + + err := a.eventService.ReplayAppEvent(r.Context(), event, g) + if err != nil { + _ = render.Render(w, r, newServiceErrResponse(err)) + return + } + + _ = render.Render(w, r, newServerResponse("App event replayed successfully", event, http.StatusOK)) +} + // GetAppEvent // @Summary Get app event // @Description This endpoint fetches an app event diff --git a/server/event_integration_test.go b/server/event_integration_test.go index 7bf021d81f..6fef05df2a 100644 --- a/server/event_integration_test.go +++ b/server/event_integration_test.go @@ -43,7 +43,7 @@ func (s *EventIntegrationTestSuite) SetupTest() { err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") require.NoError(s.T(), err) - initRealmChain(s.T(), s.DB.APIRepo()) + initRealmChain(s.T(), s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) } func (s *EventIntegrationTestSuite) TearDownTest() { @@ -140,6 +140,25 @@ func (s *EventIntegrationTestSuite) Test_GetAppEvent_Valid_Event() { require.Equal(s.T(), event.UID, respEvent.UID) } +func (s *EventIntegrationTestSuite) Test_ReplayAppEvent_Valid_Event() { + eventID := uuid.NewString() + expectedStatusCode := http.StatusOK + + // Just Before. + app, _ := testdb.SeedApplication(s.DB, s.DefaultGroup, uuid.NewString(), "", false) + _, _ = testdb.SeedEvent(s.DB, app, s.DefaultGroup.UID, eventID, "*", []byte(`{}`)) + + url := fmt.Sprintf("/api/v1/events/%s/replay", eventID) + req := createRequest(http.MethodPut, url, nil) + w := httptest.NewRecorder() + + // Act. + s.Router.ServeHTTP(w, req) + + // Assert. + require.Equal(s.T(), expectedStatusCode, w.Code) +} + func (s *EventIntegrationTestSuite) Test_GetAppEvent_Event_not_found() { eventID := uuid.NewString() expectedStatusCode := http.StatusNotFound diff --git a/server/group_integration_test.go b/server/group_integration_test.go index ac23c31253..93d2d35cca 100644 --- a/server/group_integration_test.go +++ b/server/group_integration_test.go @@ -42,7 +42,7 @@ func (s *GroupIntegrationTestSuite) SetupTest() { err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") require.NoError(s.T(), err) - initRealmChain(s.T(), s.DB.APIRepo()) + initRealmChain(s.T(), s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) } func (s *GroupIntegrationTestSuite) TestGetGroup() { diff --git a/server/ingest_integration_test.go b/server/ingest_integration_test.go index 6a530069a6..44586b1d94 100644 --- a/server/ingest_integration_test.go +++ b/server/ingest_integration_test.go @@ -39,7 +39,7 @@ func (i *IngestIntegrationTestSuite) SetupTest() { err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") require.NoError(i.T(), err) - initRealmChain(i.T(), i.DB.APIRepo()) + initRealmChain(i.T(), i.DB.APIRepo(), i.DB.UserRepo(), i.ConvoyApp.cache) } func (i *IngestIntegrationTestSuite) TearDownTest() { diff --git a/server/middleware.go b/server/middleware.go index afae847423..9be009d275 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -39,6 +39,7 @@ type contextKey string const ( groupCtx contextKey = "group" appCtx contextKey = "app" + orgCtx contextKey = "organisation" endpointCtx contextKey = "endpoint" eventCtx contextKey = "event" eventDeliveryCtx contextKey = "eventDelivery" @@ -338,6 +339,26 @@ func requireEvent(eventRepo datastore.EventRepository) func(next http.Handler) h } } +func requireOrganisation(orgRepo datastore.OrganisationRepository) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + orgID := chi.URLParam(r, "orgID") + + org, err := orgRepo.FetchOrganisationByID(r.Context(), orgID) + if err != nil { + log.WithError(err).Error("failed to fetch organisation") + _ = render.Render(w, r, newErrorResponse("failed to fetch organisation", http.StatusBadRequest)) + return + } + + r = r.WithContext(setOrganisationInContext(r.Context(), org)) + next.ServeHTTP(w, r) + }) + } +} + func requireEventDelivery(eventRepo datastore.EventDeliveryRepository) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -455,6 +476,7 @@ func requireGroup(groupRepo datastore.GroupRepository, cache cache.Cache) func(n } } } else { + // TODO(all): maybe we should only use default-group if require_auth is false? groupCacheKey := convoy.GroupsCacheKey.Get("default-group").String() err = cache.Get(r.Context(), groupCacheKey, &group) if err != nil { @@ -610,14 +632,24 @@ func getAuthFromRequest(r *http.Request) (*auth.Credential, error) { Password: creds[1], }, nil case auth.CredentialTypeAPIKey: - if util.IsStringEmpty(authInfo[1]) { - return nil, errors.New("empty api key") + authToken := authInfo[1] + + if util.IsStringEmpty(authToken) { + return nil, errors.New("empty api key or token") + } + + prefix := fmt.Sprintf("%s%s", util.Prefix, util.Seperator) + if strings.HasPrefix(authToken, prefix) { + return &auth.Credential{ + Type: auth.CredentialTypeAPIKey, + APIKey: authToken, + }, nil } return &auth.Credential{ - Type: auth.CredentialTypeAPIKey, - APIKey: authInfo[1], - }, nil + Type: auth.CredentialTypeJWT, + Token: authToken}, nil + default: return nil, fmt.Errorf("unknown credential type: %s", credType.String()) } @@ -811,6 +843,18 @@ func fetchGroupApps(appRepo datastore.ApplicationRepository) func(next http.Hand } } +func shouldAuthRoute(r *http.Request) bool { + guestRoutes := []string{"/ui/auth/login", "/ui/auth/token/refresh"} + + for _, route := range guestRoutes { + if r.URL.Path == route { + return false + } + } + + return true +} + func ensurePeriod(start time.Time, end time.Time) error { if start.Unix() > end.Unix() { return errors.New("startDate cannot be greater than endDate") @@ -845,6 +889,15 @@ func getApplicationFromContext(ctx context.Context) *datastore.Application { return ctx.Value(appCtx).(*datastore.Application) } +func setOrganisationInContext(ctx context.Context, + org *datastore.Organisation) context.Context { + return context.WithValue(ctx, orgCtx, org) +} + +func getOrganisationFromContext(ctx context.Context) *datastore.Organisation { + return ctx.Value(orgCtx).(*datastore.Organisation) +} + func setEventInContext(ctx context.Context, event *datastore.Event) context.Context { return context.WithValue(ctx, eventCtx, event) diff --git a/server/middleware_test.go b/server/middleware_test.go index 9adf1b8e32..898844fcd0 100644 --- a/server/middleware_test.go +++ b/server/middleware_test.go @@ -59,7 +59,7 @@ func TestRequirePermission_Basic(t *testing.T) { if err != nil { t.Errorf("Failed to load config file: %v", err) } - initRealmChain(t, nil) + initRealmChain(t, nil, nil, nil) ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -126,7 +126,7 @@ func TestRequirePermission_Noop(t *testing.T) { if err != nil { t.Errorf("Failed to load config file: %v", err) } - initRealmChain(t, nil) + initRealmChain(t, nil, nil, nil) ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/server/models/models.go b/server/models/models.go index 1fb33b9ff0..845031a31b 100644 --- a/server/models/models.go +++ b/server/models/models.go @@ -19,6 +19,10 @@ type Group struct { Config datastore.GroupConfig } +type Organisation struct { + Name string `json:"name" bson:"name" valid:"required~please provide a valid name"` +} + type APIKey struct { Name string `json:"name"` Role auth.Role `json:"role"` @@ -68,6 +72,28 @@ type SourceResponse struct { DeletedAt primitive.DateTime `json:"deleted_at,omitempty"` } +type LoginUser struct { + Username string `json:"username" valid:"required~please provide your username"` + Password string `json:"password" valid:"required~please provide your password"` +} + +type LoginUserResponse struct { + UID string `json:"uid"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Role auth.Role `json:"role"` + Token Token `json:"token"` + + CreatedAt primitive.DateTime `json:"created_at,omitempty" bson:"created_at"` + UpdatedAt primitive.DateTime `json:"updated_at,omitempty" bson:"updated_at"` + DeletedAt primitive.DateTime `json:"deleted_at,omitempty" bson:"deleted_at"` +} + +type Token struct { + AccessToken string `json:"access_token" valid:"required~please provide an access token"` + RefreshToken string `json:"refresh_token" valid:"required~please provide a refresh token"` +} type Application struct { AppName string `json:"name" bson:"name" valid:"required~please provide your appName"` SupportEmail string `json:"support_email" bson:"support_email" valid:"email~please provide a valid email"` diff --git a/server/organisation.go b/server/organisation.go new file mode 100644 index 0000000000..a1876e7e7f --- /dev/null +++ b/server/organisation.go @@ -0,0 +1,130 @@ +package server + +import ( + "github.com/frain-dev/convoy/server/models" + "github.com/frain-dev/convoy/util" + "github.com/go-chi/render" + log "github.com/sirupsen/logrus" + "net/http" +) + +// GetOrganisation +// @Summary Get an organisation +// @Description This endpoint fetches an organisation by its id +// @Tags Organisation +// @Accept json +// @Produce json +// @Param orgID path string true "organisation id" +// @Success 200 {object} serverResponse{data=datastore.Organisation} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Security ApiKeyAuth +// @Router /organisations/{orgID} [get] +func (a *applicationHandler) GetOrganisation(w http.ResponseWriter, r *http.Request) { + + _ = render.Render(w, r, newServerResponse("Organisation fetched successfully", + getOrganisationFromContext(r.Context()), http.StatusOK)) +} + +// GetOrganisationsPaged +// @Summary Get organisations +// @Description This endpoint fetches multiple organisations +// @Tags Organisation +// @Accept json +// @Produce json +// @Success 200 {object} serverResponse{data=pagedResponse{content=[]datastore.Organisation}} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Security ApiKeyAuth +// @Router /organisations [get] +func (a *applicationHandler) GetOrganisationsPaged(w http.ResponseWriter, r *http.Request) { + pageable := getPageableFromContext(r.Context()) + + organisations, paginationData, err := a.organisationService.LoadOrganisationsPaged(r.Context(), pageable) + if err != nil { + log.WithError(err).Error("failed to load organisations") + _ = render.Render(w, r, newServiceErrResponse(err)) + return + } + + _ = render.Render(w, r, newServerResponse("Organisations fetched successfully", + pagedResponse{Content: &organisations, Pagination: &paginationData}, http.StatusOK)) +} + +// CreateOrganisation +// @Summary Create an organisation +// @Description This endpoint creates an organisation +// @Tags Application +// @Accept json +// @Produce json +// @Param application body models.Organisation true "Organisation Details" +// @Success 200 {object} serverResponse{data=datastore.Organisation} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Security ApiKeyAuth +// @Router /organisations [post] +func (a *applicationHandler) CreateOrganisation(w http.ResponseWriter, r *http.Request) { + var newOrg models.Organisation + err := util.ReadJSON(r, &newOrg) + if err != nil { + _ = render.Render(w, r, newErrorResponse(err.Error(), http.StatusBadRequest)) + return + } + + organisation, err := a.organisationService.CreateOrganisation(r.Context(), &newOrg) + if err != nil { + _ = render.Render(w, r, newServiceErrResponse(err)) + return + } + + _ = render.Render(w, r, newServerResponse("Organisation created successfully", organisation, http.StatusCreated)) +} + +// UpdateOrganisation +// @Summary Update an organisation +// @Description This endpoint updates an organisation +// @Tags Organisation +// @Accept json +// @Produce json +// @Param orgID path string true "organisation id" +// @Param application body models.Organisation true "Organisation Details" +// @Success 200 {object} serverResponse{data=datastore.Organisation} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Security ApiKeyAuth +// @Router /organisations/{orgID} [put] +func (a *applicationHandler) UpdateOrganisation(w http.ResponseWriter, r *http.Request) { + var orgUpdate models.Organisation + err := util.ReadJSON(r, &orgUpdate) + if err != nil { + _ = render.Render(w, r, newErrorResponse(err.Error(), http.StatusBadRequest)) + return + } + + org, err := a.organisationService.UpdateOrganisation(r.Context(), getOrganisationFromContext(r.Context()), &orgUpdate) + if err != nil { + _ = render.Render(w, r, newServiceErrResponse(err)) + return + } + + _ = render.Render(w, r, newServerResponse("App updated successfully", org, http.StatusAccepted)) +} + +// DeleteOrganisation +// @Summary Delete organisation +// @Description This endpoint deletes an organisation +// @Tags Organisation +// @Accept json +// @Produce json +// @Param orgID path string true "organisation id" +// @Success 200 {object} serverResponse{data=Stub} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Security ApiKeyAuth +// @Router /organisations/{orgID} [put] +func (a *applicationHandler) DeleteOrganisation(w http.ResponseWriter, r *http.Request) { + org := getOrganisationFromContext(r.Context()) + err := a.organisationService.DeleteOrganisation(r.Context(), org.UID) + if err != nil { + log.WithError(err).Error("failed to delete organisation") + _ = render.Render(w, r, newServiceErrResponse(err)) + return + } + + _ = render.Render(w, r, newServerResponse("Organisation deleted successfully", nil, http.StatusOK)) +} diff --git a/server/organisation_integration_test.go b/server/organisation_integration_test.go new file mode 100644 index 0000000000..ad515e6917 --- /dev/null +++ b/server/organisation_integration_test.go @@ -0,0 +1,218 @@ +//go:build integration +// +build integration + +package server + +import ( + "context" + "fmt" + "github.com/frain-dev/convoy/config" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/server/testdb" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +type OrganisationIntegrationTestSuite struct { + suite.Suite + DB datastore.DatabaseClient + Router http.Handler + ConvoyApp *applicationHandler + DefaultGroup *datastore.Group +} + +func (s *OrganisationIntegrationTestSuite) SetupSuite() { + s.DB = getDB() + s.ConvoyApp = buildApplication() + s.Router = buildRoutes(s.ConvoyApp) +} + +func (s *OrganisationIntegrationTestSuite) SetupTest() { + testdb.PurgeDB(s.DB) + + // Setup Default Group. + s.DefaultGroup, _ = testdb.SeedDefaultGroup(s.DB) + + // Setup Config. + err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") + require.NoError(s.T(), err) + + initRealmChain(s.T(), s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) +} + +func (s *OrganisationIntegrationTestSuite) TearDownTest() { + testdb.PurgeDB(s.DB) +} + +func (s *OrganisationIntegrationTestSuite) Test_CreateOrganisation() { + expectedStatusCode := http.StatusCreated + + body := strings.NewReader(`{"name":"new_org"}`) + // Arrange. + url := "/ui/organisations" + req := createRequest(http.MethodPost, url, body) + w := httptest.NewRecorder() + + // Act. + s.Router.ServeHTTP(w, req) + + // Assert. + require.Equal(s.T(), expectedStatusCode, w.Code) + + // Deep Assert. + var organisation datastore.Organisation + parseResponse(s.T(), w.Result(), &organisation) + + org, err := s.DB.OrganisationRepo().FetchOrganisationByID(context.Background(), organisation.UID) + require.NoError(s.T(), err) + require.Equal(s.T(), "new_org", org.Name) +} + +func (s *OrganisationIntegrationTestSuite) Test_CreateOrganisation_EmptyOrganisationName() { + expectedStatusCode := http.StatusBadRequest + + body := strings.NewReader(`{"name":""}`) + // Arrange. + url := "/ui/organisations" + req := createRequest(http.MethodPost, url, body) + w := httptest.NewRecorder() + + // Act. + s.Router.ServeHTTP(w, req) + + // Assert. + require.Equal(s.T(), expectedStatusCode, w.Code) +} + +func (s *OrganisationIntegrationTestSuite) Test_UpdateOrganisation_EmptyOrganisationName() { + expectedStatusCode := http.StatusBadRequest + + uid := uuid.NewString() + _, err := testdb.SeedOrganisation(s.DB, uid, "", "new_org") + require.NoError(s.T(), err) + + body := strings.NewReader(`{"name":""}`) + // Arrange. + url := fmt.Sprintf("/ui/organisations/%s", uid) + req := createRequest(http.MethodPut, url, body) + w := httptest.NewRecorder() + + // Act. + s.Router.ServeHTTP(w, req) + + // Assert. + require.Equal(s.T(), expectedStatusCode, w.Code) +} + +func (s *OrganisationIntegrationTestSuite) Test_UpdateOrganisation() { + expectedStatusCode := http.StatusAccepted + + uid := uuid.NewString() + _, err := testdb.SeedOrganisation(s.DB, uid, "", "new_org") + require.NoError(s.T(), err) + + body := strings.NewReader(`{"name":"update_org"}`) + + // Arrange. + url := fmt.Sprintf("/ui/organisations/%s", uid) + req := createRequest(http.MethodPut, url, body) + w := httptest.NewRecorder() + + // Act. + s.Router.ServeHTTP(w, req) + + // Assert. + require.Equal(s.T(), expectedStatusCode, w.Code) + + org, err := s.DB.OrganisationRepo().FetchOrganisationByID(context.Background(), uid) + require.NoError(s.T(), err) + require.Equal(s.T(), "update_org", org.Name) +} + +func (s *OrganisationIntegrationTestSuite) Test_GetOrganisation() { + expectedStatusCode := http.StatusOK + + uid := uuid.NewString() + _, err := testdb.SeedOrganisation(s.DB, uid, "", "new_org") + require.NoError(s.T(), err) + + // Arrange. + url := fmt.Sprintf("/ui/organisations/%s", uid) + req := createRequest(http.MethodGet, url, nil) + w := httptest.NewRecorder() + + // Act. + s.Router.ServeHTTP(w, req) + + // Assert. + require.Equal(s.T(), expectedStatusCode, w.Code) + + // Deep Assert. + var organisation datastore.Organisation + parseResponse(s.T(), w.Result(), &organisation) + + org, err := s.DB.OrganisationRepo().FetchOrganisationByID(context.Background(), uid) + require.NoError(s.T(), err) + require.Equal(s.T(), "new_org", org.Name) + require.Equal(s.T(), "new_org", organisation.Name) +} + +func (s *OrganisationIntegrationTestSuite) Test_GetOrganisations() { + expectedStatusCode := http.StatusOK + + _, err := testdb.SeedMultipleOrganisations(s.DB, "", 5) + require.NoError(s.T(), err) + + // Arrange. + url := "/ui/organisations?page=2&perPage=2" + req := createRequest(http.MethodGet, url, nil) + w := httptest.NewRecorder() + + // Act. + s.Router.ServeHTTP(w, req) + + // Assert. + require.Equal(s.T(), expectedStatusCode, w.Code) + + // Deep Assert. + var organisations []datastore.Organisation + pagedResp := pagedResponse{Content: &organisations} + parseResponse(s.T(), w.Result(), &pagedResp) + + require.Equal(s.T(), 2, len(organisations)) + require.Equal(s.T(), int64(5), pagedResp.Pagination.Total) +} + +func (s *OrganisationIntegrationTestSuite) Test_DeleteOrganisation() { + expectedStatusCode := http.StatusOK + + uid := uuid.NewString() + _, err := testdb.SeedOrganisation(s.DB, uid, "", "new_org") + require.NoError(s.T(), err) + + // Arrange. + url := fmt.Sprintf("/ui/organisations/%s", uid) + req := createRequest(http.MethodDelete, url, nil) + w := httptest.NewRecorder() + + // Act. + s.Router.ServeHTTP(w, req) + + // Assert. + require.Equal(s.T(), expectedStatusCode, w.Code) + + org, err := s.DB.OrganisationRepo().FetchOrganisationByID(context.Background(), uid) + require.NoError(s.T(), err) + + require.NotEmpty(s.T(), org.DeletedAt) + require.Equal(s.T(), datastore.DeletedDocumentStatus, org.DocumentStatus) +} + +func TestOrganisationIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(OrganisationIntegrationTestSuite)) +} diff --git a/server/route.go b/server/route.go index 60f7819b96..95989fdd73 100644 --- a/server/route.go +++ b/server/route.go @@ -132,6 +132,7 @@ func buildRoutes(app *applicationHandler) http.Handler { eventRouter.Route("/{eventID}", func(eventSubRouter chi.Router) { eventSubRouter.Use(requireEvent(app.eventRepo)) eventSubRouter.Get("/", app.GetAppEvent) + eventSubRouter.Put("/replay", app.ReplayAppEvent) }) }) @@ -209,7 +210,13 @@ func buildRoutes(app *applicationHandler) http.Handler { router.Route("/ui", func(uiRouter chi.Router) { uiRouter.Use(jsonResponse) uiRouter.Use(setupCORS) - uiRouter.Use(requireAuth()) + uiRouter.Use(middleware.Maybe(requireAuth(), shouldAuthRoute)) + + uiRouter.Route("/auth", func(authRouter chi.Router) { + authRouter.Post("/login", app.LoginUser) + authRouter.Post("/token/refresh", app.RefreshToken) + authRouter.Post("/logout", app.LogoutUser) + }) uiRouter.Route("/dashboard", func(dashboardRouter chi.Router) { dashboardRouter.Use(requireGroup(app.groupRepo, app.cache)) @@ -220,7 +227,6 @@ func buildRoutes(app *applicationHandler) http.Handler { }) uiRouter.Route("/groups", func(groupRouter chi.Router) { - groupRouter.Route("/", func(orgSubRouter chi.Router) { groupRouter.With(requirePermission(auth.RoleSuperUser)).Post("/", app.CreateGroup) groupRouter.Get("/", app.GetGroups) @@ -236,6 +242,21 @@ func buildRoutes(app *applicationHandler) http.Handler { }) }) + uiRouter.Route("/organisations", func(orgRouter chi.Router) { + orgRouter.Use(requirePermission(auth.RoleAdmin)) + + orgRouter.Post("/", app.CreateOrganisation) + orgRouter.With(pagination).Get("/", app.GetOrganisationsPaged) + + orgRouter.Route("/{orgID}", func(orgSubRouter chi.Router) { + orgSubRouter.Use(requireOrganisation(app.orgRepo)) + + orgSubRouter.Get("/", app.GetOrganisation) + orgSubRouter.Put("/", app.UpdateOrganisation) + orgSubRouter.Delete("/", app.DeleteOrganisation) + }) + }) + uiRouter.Route("/apps", func(appRouter chi.Router) { appRouter.Use(requireGroup(app.groupRepo, app.cache)) appRouter.Use(rateLimitByGroupID(app.limiter)) @@ -286,6 +307,7 @@ func buildRoutes(app *applicationHandler) http.Handler { eventRouter.Route("/{eventID}", func(eventSubRouter chi.Router) { eventSubRouter.Use(requireEvent(app.eventRepo)) eventSubRouter.Get("/", app.GetAppEvent) + eventSubRouter.Put("/replay", app.ReplayAppEvent) }) }) @@ -375,6 +397,7 @@ func buildRoutes(app *applicationHandler) http.Handler { eventRouter.Route("/{eventID}", func(eventSubRouter chi.Router) { eventSubRouter.Use(requireEvent(app.eventRepo)) eventSubRouter.Get("/", app.GetAppEvent) + eventSubRouter.Put("/replay", app.ReplayAppEvent) }) }) @@ -428,9 +451,11 @@ func New(cfg config.Configuration, eventDeliveryRepo datastore.EventDeliveryRepository, appRepo datastore.ApplicationRepository, apiKeyRepo datastore.APIKeyRepository, - orgRepo datastore.GroupRepository, subRepo datastore.SubscriptionRepository, + groupRepo datastore.GroupRepository, + orgRepo datastore.OrganisationRepository, sourceRepo datastore.SourceRepository, + userRepo datastore.UserRepository, eventQueue queue.Queuer, createEventQueue queue.Queuer, logger logger.Logger, @@ -444,10 +469,12 @@ func New(cfg config.Configuration, eventRepo, eventDeliveryRepo, appRepo, - orgRepo, + groupRepo, apiKeyRepo, subRepo, sourceRepo, + orgRepo, + userRepo, eventQueue, createEventQueue, logger, diff --git a/server/security_integration_test.go b/server/security_integration_test.go index 17dcec75d9..851c36bb46 100644 --- a/server/security_integration_test.go +++ b/server/security_integration_test.go @@ -44,7 +44,7 @@ func (s *SecurityIntegrationTestSuite) SetupTest() { err := config.LoadConfig("./testdata/Auth_Config/full-convoy-with-native-auth-realm.json") require.NoError(s.T(), err) - initRealmChain(s.T(), s.DB.APIRepo()) + initRealmChain(s.T(), s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) } func (s *SecurityIntegrationTestSuite) Test_CreateAPIKey() { diff --git a/server/server_suite_test.go b/server/server_suite_test.go index e75e460078..a67bd9756c 100644 --- a/server/server_suite_test.go +++ b/server/server_suite_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/frain-dev/convoy/auth/realm_chain" + "github.com/frain-dev/convoy/cache" ncache "github.com/frain-dev/convoy/cache/noop" "github.com/frain-dev/convoy/config" "github.com/frain-dev/convoy/datastore" @@ -28,13 +29,11 @@ import ( // TEST HELPERS. func getMongoDSN() string { - return "mongodb://localhost:27017/test_db" - // return os.Getenv("TEST_MONGO_DSN") + return os.Getenv("TEST_MONGO_DSN") } func getRedisDSN() string { - return "redis://localhost:6379" - // return os.Getenv("TEST_REDIS_DSN") + return os.Getenv("TEST_REDIS_DSN") } func getConfig() config.Configuration { @@ -90,6 +89,8 @@ func buildApplication() *applicationHandler { eventDeliveryRepo := db.EventDeliveryRepo() apiKeyRepo := db.APIRepo() sourceRepo := db.SourceRepo() + orgRepo := db.OrganisationRepo() + userRepo := db.UserRepo() eventQueue := redisqueue.NewQueue(qOpts) createEventQueue := redisqueue.NewQueue(cOpts) logger := logger.NewNoopLogger() @@ -101,18 +102,18 @@ func buildApplication() *applicationHandler { return newApplicationHandler( eventRepo, eventDeliveryRepo, appRepo, - groupRepo, apiKeyRepo, subRepo, sourceRepo, eventQueue, createEventQueue, + groupRepo, apiKeyRepo, subRepo, sourceRepo, orgRepo, userRepo, eventQueue, createEventQueue, logger, tracer, cache, limiter, searcher, ) } -func initRealmChain(t *testing.T, apiKeyRepo datastore.APIKeyRepository) { +func initRealmChain(t *testing.T, apiKeyRepo datastore.APIKeyRepository, userRepo datastore.UserRepository, cache cache.Cache) { cfg, err := config.Get() if err != nil { t.Errorf("failed to get config: %v", err) } - err = realm_chain.Init(&cfg.Auth, apiKeyRepo) + err = realm_chain.Init(&cfg.Auth, apiKeyRepo, userRepo, cache) if err != nil { t.Errorf("failed to initialize realm chain : %v", err) } diff --git a/server/source_integration_test.go b/server/source_integration_test.go index 28654332e9..ebc1940433 100644 --- a/server/source_integration_test.go +++ b/server/source_integration_test.go @@ -41,7 +41,7 @@ func (s *SourceIntegrationTestSuite) SetupTest() { err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") require.NoError(s.T(), err) - initRealmChain(s.T(), s.DB.APIRepo()) + initRealmChain(s.T(), s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) } func (s *SourceIntegrationTestSuite) TearDownTest() { diff --git a/server/subscription_integration_test.go b/server/subscription_integration_test.go index 38d62b81ee..d357ef272c 100644 --- a/server/subscription_integration_test.go +++ b/server/subscription_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + package server import ( @@ -41,7 +44,7 @@ func (s *SubscriptionIntegrationTestSuite) SetupTest() { err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") require.NoError(s.T(), err) - initRealmChain(s.T(), s.DB.APIRepo()) + initRealmChain(s.T(), s.DB.APIRepo(), s.DB.UserRepo(), s.ConvoyApp.cache) } func (s *SubscriptionIntegrationTestSuite) TearDownTest() { diff --git a/server/testdata/Auth_Config/jwt-convoy.json b/server/testdata/Auth_Config/jwt-convoy.json new file mode 100644 index 0000000000..bb0ee6c3f8 --- /dev/null +++ b/server/testdata/Auth_Config/jwt-convoy.json @@ -0,0 +1,33 @@ +{ + "multiple_tenants": false, + "queue": { + "type": "redis", + "redis": { + "dsn": "abc" + } + }, + "auth": { + "require_auth": true, + "jwt": { + "enabled": true + } + }, + "server": { + "http": { + "port": 80 + } + }, + "group": { + "strategy": { + "type": "default", + "default": { + "intervalSeconds": 125, + "retryLimit": 15 + } + }, + "signature": { + "header": "X-Company-Event-WebHook-Signature", + "hash": "SHA256" + } + } +} diff --git a/server/testdb/seed.go b/server/testdb/seed.go index 9be07e895a..14cd34a86a 100644 --- a/server/testdb/seed.go +++ b/server/testdb/seed.go @@ -248,6 +248,61 @@ func SeedEventDelivery(db datastore.DatabaseClient, app *datastore.Application, return eventDelivery, nil } +// SeedOrganisation is create random Organisation for integration tests. +func SeedOrganisation(db datastore.DatabaseClient, uid, ownerID, name string) (*datastore.Organisation, error) { + if util.IsStringEmpty(uid) { + uid = uuid.New().String() + } + + if util.IsStringEmpty(name) { + name = fmt.Sprintf("TestOrg-%s", uid) + } + + org := &datastore.Organisation{ + UID: uid, + OwnerID: ownerID, + Name: name, + DocumentStatus: datastore.ActiveDocumentStatus, + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + } + + // Seed Data. + err := db.OrganisationRepo().CreateOrganisation(context.TODO(), org) + if err != nil { + return &datastore.Organisation{}, err + } + + return org, nil +} + +// SeedMultipleOrganisations is creates random Organisations for integration tests. +func SeedMultipleOrganisations(db datastore.DatabaseClient, ownerID string, num int) ([]*datastore.Organisation, error) { + orgs := []*datastore.Organisation{} + + for i := 0; i < num; i++ { + uid := uuid.New().String() + + org := &datastore.Organisation{ + UID: uid, + OwnerID: ownerID, + Name: fmt.Sprintf("TestOrg-%s", uid), + DocumentStatus: datastore.ActiveDocumentStatus, + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + } + orgs = append(orgs, org) + + // Seed Data. + err := db.OrganisationRepo().CreateOrganisation(context.TODO(), org) + if err != nil { + return nil, err + } + } + + return orgs, nil +} + func SeedSource(db datastore.DatabaseClient, g *datastore.Group, uid string) (*datastore.Source, error) { if util.IsStringEmpty(uid) { uid = uuid.New().String() @@ -322,6 +377,31 @@ func SeedSubscription(db datastore.DatabaseClient, return subscription, nil } +func SeedUser(db datastore.DatabaseClient, password string) (*datastore.User, error) { + p := &datastore.Password{Plaintext: password} + err := p.GenerateHash() + if err != nil { + return nil, err + } + + user := &datastore.User{ + UID: uuid.NewString(), + FirstName: "test", + LastName: "test", + Password: string(p.Hash), + Email: "test@test.com", + DocumentStatus: datastore.ActiveDocumentStatus, + } + + //Seed Data + err = db.UserRepo().CreateUser(context.TODO(), user) + if err != nil { + return nil, err + } + + return user, nil +} + // PurgeDB is run after every test run and it's used to truncate the DB to have // a clean slate in the next run. func PurgeDB(db datastore.DatabaseClient) { diff --git a/server/user.go b/server/user.go new file mode 100644 index 0000000000..e3b2ae92ab --- /dev/null +++ b/server/user.go @@ -0,0 +1,99 @@ +package server + +import ( + "net/http" + + "github.com/frain-dev/convoy/server/models" + "github.com/frain-dev/convoy/util" + "github.com/go-chi/render" +) + +// LoginUser +// @Summary Login a user +// @Description This endpoint logs in a user +// @Tags User +// @Accept json +// @Produce json +// @Param user body models.LoginUser true "User Details" +// @Success 200 {object} serverResponse{data=models.LoginUserResponse} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Router /auth/login [post] +func (a *applicationHandler) LoginUser(w http.ResponseWriter, r *http.Request) { + var newUser models.LoginUser + if err := util.ReadJSON(r, &newUser); err != nil { + _ = render.Render(w, r, newErrorResponse(err.Error(), http.StatusBadRequest)) + return + } + + user, token, err := a.userService.LoginUser(r.Context(), &newUser) + if err != nil { + _ = render.Render(w, r, newServiceErrResponse(err)) + return + } + + u := &models.LoginUserResponse{ + UID: user.UID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + Role: user.Role, + Token: models.Token{AccessToken: token.AccessToken, RefreshToken: token.RefreshToken}, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + DeletedAt: user.DeletedAt, + } + + _ = render.Render(w, r, newServerResponse("Login successful", u, http.StatusOK)) +} + +// RefreshToken +// @Summary Refresh an access token +// @Description This endpoint refreshes an access token +// @Tags User +// @Accept json +// @Produce json +// @Param token body models.Token true "Token Details" +// @Success 200 {object} serverResponse{data=models.Token} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Router /auth/token/refresh [post] +func (a *applicationHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { + var refreshToken models.Token + if err := util.ReadJSON(r, &refreshToken); err != nil { + _ = render.Render(w, r, newErrorResponse(err.Error(), http.StatusBadRequest)) + return + } + + token, err := a.userService.RefreshToken(r.Context(), &refreshToken) + if err != nil { + _ = render.Render(w, r, newServiceErrResponse(err)) + return + } + + _ = render.Render(w, r, newServerResponse("Token refresh successful", token, http.StatusOK)) +} + +// LogoutUser +// @Summary Logs out a user +// @Description This endpoint logs out a user +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} serverResponse{data=Stub} +// @Failure 400,401,500 {object} serverResponse{data=Stub} +// @Security ApiKeyAuth +// @Router /auth/logout [post] +func (a *applicationHandler) LogoutUser(w http.ResponseWriter, r *http.Request) { + auth, err := getAuthFromRequest(r) + if err != nil { + _ = render.Render(w, r, newErrorResponse(err.Error(), http.StatusUnauthorized)) + return + } + + err = a.userService.LogoutUser(auth.Token) + if err != nil { + _ = render.Render(w, r, newServiceErrResponse(err)) + return + } + + _ = render.Render(w, r, newServerResponse("Logout successful", nil, http.StatusOK)) +} diff --git a/server/user_integration_test.go b/server/user_integration_test.go new file mode 100644 index 0000000000..03619b597c --- /dev/null +++ b/server/user_integration_test.go @@ -0,0 +1,247 @@ +package server + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/frain-dev/convoy/auth/realm/jwt" + "github.com/frain-dev/convoy/config" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/server/models" + "github.com/frain-dev/convoy/server/testdb" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type UserIntegrationTestSuite struct { + suite.Suite + DB datastore.DatabaseClient + Router http.Handler + ConvoyApp *applicationHandler + jwt *jwt.Jwt +} + +func (u *UserIntegrationTestSuite) SetupSuite() { + u.DB = getDB() + u.ConvoyApp = buildApplication() + u.Router = buildRoutes(u.ConvoyApp) +} + +func (u *UserIntegrationTestSuite) SetupTest() { + testdb.PurgeDB(u.DB) + + err := config.LoadConfig("./testdata/Auth_Config/jwt-convoy.json") + require.NoError(u.T(), err) + + config, err := config.Get() + require.NoError(u.T(), err) + + u.jwt = jwt.NewJwt(&config.Auth.Jwt, u.ConvoyApp.cache) + + initRealmChain(u.T(), u.DB.APIRepo(), u.DB.UserRepo(), u.ConvoyApp.cache) +} + +func (u *UserIntegrationTestSuite) TearDownTest() { + testdb.PurgeDB(u.DB) +} + +func (u *UserIntegrationTestSuite) Test_LoginUser() { + password := "123456" + user, _ := testdb.SeedUser(u.DB, password) + + //Arrange Request + url := "/ui/auth/login" + bodyStr := fmt.Sprintf(`{ + "username": "%s", + "password": "%s" + }`, user.Email, password) + + body := serialize(bodyStr) + req := createRequest(http.MethodPost, url, body) + w := httptest.NewRecorder() + + // Act + u.Router.ServeHTTP(w, req) + + //Assert + require.Equal(u.T(), http.StatusOK, w.Code) + + var response models.LoginUserResponse + parseResponse(u.T(), w.Result(), &response) + + require.NotEmpty(u.T(), response.UID) + require.NotEmpty(u.T(), response.Token.AccessToken) + require.NotEmpty(u.T(), response.Token.RefreshToken) + + require.Equal(u.T(), user.UID, response.UID) + require.Equal(u.T(), user.FirstName, response.FirstName) + require.Equal(u.T(), user.LastName, response.LastName) + require.Equal(u.T(), user.Email, response.Email) +} + +func (u *UserIntegrationTestSuite) Test_LoginUser_Invalid_Username() { + //Arrange Request + url := "/ui/auth/login" + bodyStr := fmt.Sprintf(`{ + "username": "%s", + "password": "%s" + }`, "random@test.com", "123456") + + body := serialize(bodyStr) + req := createRequest(http.MethodPost, url, body) + w := httptest.NewRecorder() + + // Act + u.Router.ServeHTTP(w, req) + + //Assert + require.Equal(u.T(), http.StatusUnauthorized, w.Code) +} + +func (u *UserIntegrationTestSuite) Test_LoginUser_Invalid_Password() { + password := "123456" + user, _ := testdb.SeedUser(u.DB, password) + + //Arrange Request + url := "/ui/auth/login" + bodyStr := fmt.Sprintf(`{ + "username": "%s", + "password": "%s" + }`, user.Email, "12345") + + body := serialize(bodyStr) + req := createRequest(http.MethodPost, url, body) + w := httptest.NewRecorder() + + // Act + u.Router.ServeHTTP(w, req) + + //Assert + require.Equal(u.T(), http.StatusUnauthorized, w.Code) +} + +func (u *UserIntegrationTestSuite) Test_RefreshToken() { + password := "123456" + user, _ := testdb.SeedUser(u.DB, password) + + token, err := u.jwt.GenerateToken(user) + require.NoError(u.T(), err) + + // Arrange Request + url := "/ui/auth/token/refresh" + bodyStr := fmt.Sprintf(`{ + "access_token": "%s", + "refresh_token": "%s" + }`, token.AccessToken, token.RefreshToken) + + body := serialize(bodyStr) + req := createRequest(http.MethodPost, url, body) + w := httptest.NewRecorder() + + // Act + u.Router.ServeHTTP(w, req) + + //Assert + require.Equal(u.T(), http.StatusOK, w.Code) + + var response jwt.Token + parseResponse(u.T(), w.Result(), &response) + + require.NotEmpty(u.T(), response.AccessToken) + require.NotEmpty(u.T(), response.RefreshToken) + +} + +func (u *UserIntegrationTestSuite) Test_RefreshToken_Invalid_Access_Token() { + password := "123456" + user, _ := testdb.SeedUser(u.DB, password) + + token, err := u.jwt.GenerateToken(user) + require.NoError(u.T(), err) + + // Arrange Request + url := "/ui/auth/token/refresh" + bodyStr := fmt.Sprintf(`{ + "access_token": "%s", + "refresh_token": "%s" + }`, uuid.NewString(), token.RefreshToken) + + body := serialize(bodyStr) + req := createRequest(http.MethodPost, url, body) + w := httptest.NewRecorder() + + // Act + u.Router.ServeHTTP(w, req) + + //Assert + require.Equal(u.T(), http.StatusUnauthorized, w.Code) +} + +func (u *UserIntegrationTestSuite) Test_RefreshToken_Invalid_Refresh_Token() { + password := "123456" + user, _ := testdb.SeedUser(u.DB, password) + + token, err := u.jwt.GenerateToken(user) + require.NoError(u.T(), err) + + // Arrange Request + url := "/ui/auth/token/refresh" + bodyStr := fmt.Sprintf(`{ + "access_token": "%s", + "refresh_token": "%s" + }`, token.AccessToken, uuid.NewString()) + + body := serialize(bodyStr) + req := createRequest(http.MethodPost, url, body) + w := httptest.NewRecorder() + + // Act + u.Router.ServeHTTP(w, req) + + //Assert + require.Equal(u.T(), http.StatusUnauthorized, w.Code) +} + +func (u *UserIntegrationTestSuite) Test_LogoutUser() { + password := "123456" + user, _ := testdb.SeedUser(u.DB, password) + + token, err := u.jwt.GenerateToken(user) + require.NoError(u.T(), err) + + // Arrange Request + url := "/ui/auth/logout" + req := httptest.NewRequest(http.MethodPost, url, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Act + u.Router.ServeHTTP(w, req) + + //Assert + require.Equal(u.T(), http.StatusOK, w.Code) + +} + +func (u *UserIntegrationTestSuite) Test_LogoutUser_Invalid_Access_Token() { + // Arrange Request + url := "/ui/auth/logout" + req := httptest.NewRequest(http.MethodPost, url, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", uuid.NewString())) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Act + u.Router.ServeHTTP(w, req) + + //Assert + require.Equal(u.T(), http.StatusUnauthorized, w.Code) +} + +func TestUserIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(UserIntegrationTestSuite)) +} diff --git a/services/app_service_test.go b/services/app_service_test.go index ad118fff84..14bbec5df5 100644 --- a/services/app_service_test.go +++ b/services/app_service_test.go @@ -783,6 +783,10 @@ func stripVariableFields(t *testing.T, obj string, v interface{}) { a := v.(*datastore.APIKey) a.UID, a.MaskID, a.Salt, a.Hash = "", "", "", "" a.CreatedAt, a.UpdatedAt = 0, 0 + case "organisation": + a := v.(*datastore.Organisation) + a.UID = "" + a.CreatedAt, a.UpdatedAt = 0, 0 default: t.Errorf("invalid data body - %v of type %T", obj, obj) t.FailNow() diff --git a/services/event_service.go b/services/event_service.go index 13a03ffc71..483e88877f 100644 --- a/services/event_service.go +++ b/services/event_service.go @@ -105,6 +105,22 @@ func (e *EventService) CreateAppEvent(ctx context.Context, newMessage *models.Ev return event, nil } +func (e *EventService) ReplayAppEvent(ctx context.Context, event *datastore.Event, g *datastore.Group) error { + taskName := convoy.CreateEventProcessor.SetPrefix(g.Name) + job := &queue.Job{ + ID: event.UID, + Event: event, + } + + err := e.createEventQueue.Publish(context.Background(), taskName, job, 0) + if err != nil { + log.WithError(err).Error("replay_event: failed to write event to the queue") + return NewServiceError(http.StatusBadRequest, errors.New("failed to write event to queue")) + } + + return nil +} + func (e *EventService) GetAppEvent(ctx context.Context, id string) (*datastore.Event, error) { event, err := e.eventRepo.FindEventByID(ctx, id) if err != nil { diff --git a/services/event_service_test.go b/services/event_service_test.go index 44c78e7348..1876cf60d9 100644 --- a/services/event_service_test.go +++ b/services/event_service_test.go @@ -452,6 +452,75 @@ func TestEventService_GetAppEvent(t *testing.T) { } } +func TestEventService_ReplayAppEvent(t *testing.T) { + ctx := context.Background() + type args struct { + ctx context.Context + event *datastore.Event + g *datastore.Group + } + tests := []struct { + name string + args args + dbFn func(es *EventService) + wantErr bool + wantErrCode int + wantErrMsg string + }{ + { + name: "should_replay_app_event", + args: args{ + ctx: ctx, + event: &datastore.Event{UID: "123"}, + g: &datastore.Group{UID: "123", Name: "test_group"}, + }, + dbFn: func(es *EventService) { + eq, _ := es.createEventQueue.(*mocks.MockQueuer) + eq.EXPECT().Publish(gomock.Any(), convoy.TaskName("test_group-CreateEventProcessor"), gomock.Any(), gomock.Any()). + Times(1).Return(nil) + }, + wantErr: false, + }, + { + name: "should_fail_to_replay_app_event", + args: args{ + ctx: ctx, + event: &datastore.Event{UID: "123"}, + g: &datastore.Group{UID: "123", Name: "test_group"}, + }, + dbFn: func(es *EventService) { + eq, _ := es.createEventQueue.(*mocks.MockQueuer) + eq.EXPECT().Publish(gomock.Any(), convoy.TaskName("test_group-CreateEventProcessor"), gomock.Any(), gomock.Any()). + Times(1).Return(errors.New("failed")) + }, + wantErr: true, + wantErrCode: http.StatusBadRequest, + wantErrMsg: "failed to write event to queue", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + es := provideEventService(ctrl) + + if tc.dbFn != nil { + tc.dbFn(es) + } + + err := es.ReplayAppEvent(tc.args.ctx, tc.args.event, tc.args.g) + if tc.wantErr { + require.NotNil(t, err) + require.Equal(t, tc.wantErrCode, err.(*ServiceError).ErrCode()) + require.Equal(t, tc.wantErrMsg, err.(*ServiceError).Error()) + return + } + + require.Nil(t, err) + }) + } +} + func TestEventService_GetEventDelivery(t *testing.T) { ctx := context.Background() diff --git a/services/organisation_service.go b/services/organisation_service.go new file mode 100644 index 0000000000..7c195b76d2 --- /dev/null +++ b/services/organisation_service.go @@ -0,0 +1,91 @@ +package services + +import ( + "context" + "errors" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/server/models" + "github.com/frain-dev/convoy/util" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + "go.mongodb.org/mongo-driver/bson/primitive" + "net/http" + "time" +) + +type OrganisationService struct { + orgRepo datastore.OrganisationRepository +} + +func NewOrganisationService(orgRepo datastore.OrganisationRepository) *OrganisationService { + return &OrganisationService{orgRepo: orgRepo} +} + +func (os *OrganisationService) CreateOrganisation(ctx context.Context, newOrg *models.Organisation) (*datastore.Organisation, error) { + err := util.Validate(newOrg) + if err != nil { + return nil, NewServiceError(http.StatusBadRequest, err) + } + + org := &datastore.Organisation{ + UID: uuid.NewString(), + OwnerID: "", // TODO(daniel): to be completed when the user auth is completed by @dotunj + Name: newOrg.Name, + DocumentStatus: datastore.ActiveDocumentStatus, + CreatedAt: primitive.NewDateTimeFromTime(time.Now()), + UpdatedAt: primitive.NewDateTimeFromTime(time.Now()), + } + + err = os.orgRepo.CreateOrganisation(ctx, org) + if err != nil { + log.WithError(err).Error("failed to create organisation") + return nil, NewServiceError(http.StatusBadRequest, errors.New("failed to create organisation")) + } + + return org, nil +} + +func (os *OrganisationService) UpdateOrganisation(ctx context.Context, org *datastore.Organisation, update *models.Organisation) (*datastore.Organisation, error) { + err := util.Validate(update) + if err != nil { + log.WithError(err).Error("failed to validate organisation update") + return nil, NewServiceError(http.StatusBadRequest, err) + } + + org.Name = update.Name + err = os.orgRepo.UpdateOrganisation(ctx, org) + if err != nil { + log.WithError(err).Error("failed to to update organisation") + return nil, NewServiceError(http.StatusBadRequest, errors.New("failed to update organisation")) + } + + return org, nil +} + +func (os *OrganisationService) FindOrganisationByID(ctx context.Context, id string) (*datastore.Organisation, error) { + org, err := os.orgRepo.FetchOrganisationByID(ctx, id) + if err != nil { + log.WithError(err).Error("failed to find organisation by id") + return nil, NewServiceError(http.StatusBadRequest, errors.New("failed to find organisation by id")) + } + return org, err +} + +func (os *OrganisationService) LoadOrganisationsPaged(ctx context.Context, pageable datastore.Pageable) ([]datastore.Organisation, datastore.PaginationData, error) { + orgs, paginationData, err := os.orgRepo.LoadOrganisationsPaged(ctx, pageable) + if err != nil { + log.WithError(err).Error("failed to fetch organisations") + return nil, datastore.PaginationData{}, NewServiceError(http.StatusBadRequest, errors.New("an error occurred while fetching organisations")) + } + + return orgs, paginationData, nil +} + +func (os *OrganisationService) DeleteOrganisation(ctx context.Context, id string) error { + err := os.orgRepo.DeleteOrganisation(ctx, id) + if err != nil { + log.WithError(err).Error("failed to delete organisation") + return NewServiceError(http.StatusBadRequest, errors.New("failed to delete organisation")) + } + return err +} diff --git a/services/organisation_service_test.go b/services/organisation_service_test.go new file mode 100644 index 0000000000..7500e224e9 --- /dev/null +++ b/services/organisation_service_test.go @@ -0,0 +1,445 @@ +package services + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/mocks" + "github.com/frain-dev/convoy/server/models" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +func provideOrganisationService(ctrl *gomock.Controller) *OrganisationService { + orgRepo := mocks.NewMockOrganisationRepository(ctrl) + return NewOrganisationService(orgRepo) +} + +func TestOrganisationService_CreateOrganisation(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + newOrg *models.Organisation + } + tests := []struct { + name string + args args + want *datastore.Organisation + dbFn func(os *OrganisationService) + wantErr bool + wantErrCode int + wantErrMsg string + }{ + { + name: "should_create_organisation", + args: args{ + ctx: ctx, + newOrg: &models.Organisation{Name: "new_org"}, + }, + want: &datastore.Organisation{Name: "new_org", DocumentStatus: datastore.ActiveDocumentStatus}, + dbFn: func(os *OrganisationService) { + a, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + a.EXPECT().CreateOrganisation(gomock.Any(), gomock.Any()). + Times(1).Return(nil) + }, + wantErr: false, + }, + { + name: "should_fail_to_validate_organisation", + args: args{ + ctx: ctx, + newOrg: &models.Organisation{Name: ""}, + }, + wantErr: true, + wantErrCode: http.StatusBadRequest, + wantErrMsg: "name:please provide a valid name", + }, + { + name: "should_fail_to_create_organisation", + args: args{ + ctx: ctx, + newOrg: &models.Organisation{Name: "new_org"}, + }, + dbFn: func(os *OrganisationService) { + a, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + a.EXPECT().CreateOrganisation(gomock.Any(), gomock.Any()). + Times(1).Return(errors.New("failed")) + }, + wantErr: true, + wantErrCode: http.StatusBadRequest, + wantErrMsg: "failed to create organisation", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + os := provideOrganisationService(ctrl) + + // Arrange Expectations + if tt.dbFn != nil { + tt.dbFn(os) + } + + org, err := os.CreateOrganisation(tt.args.ctx, tt.args.newOrg) + if tt.wantErr { + require.NotNil(t, err) + require.Equal(t, tt.wantErrCode, err.(*ServiceError).ErrCode()) + require.Equal(t, tt.wantErrMsg, err.(*ServiceError).Error()) + return + } + + require.Nil(t, err) + stripVariableFields(t, "organisation", org) + require.Equal(t, tt.want, org) + }) + } +} + +func TestOrganisationService_UpdateOrganisation(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + org *datastore.Organisation + update *models.Organisation + } + tests := []struct { + name string + args args + want *datastore.Organisation + dbFn func(os *OrganisationService) + wantErr bool + wantErrCode int + wantErrMsg string + }{ + { + name: "should_update_organisation", + args: args{ + ctx: ctx, + org: &datastore.Organisation{UID: "abc", Name: "test_org"}, + update: &models.Organisation{Name: "test_update_org"}, + }, + dbFn: func(os *OrganisationService) { + a, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + a.EXPECT().UpdateOrganisation(gomock.Any(), &datastore.Organisation{UID: "abc", Name: "test_update_org"}). + Times(1).Return(nil) + }, + want: &datastore.Organisation{UID: "abc", Name: "test_update_org"}, + wantErr: false, + }, + { + name: "should_fail_to_validate_organisation", + args: args{ + ctx: ctx, + update: &models.Organisation{Name: ""}, + }, + wantErr: true, + wantErrCode: http.StatusBadRequest, + wantErrMsg: "name:please provide a valid name", + }, + { + name: "should_fail_to_update_organisation", + args: args{ + ctx: ctx, + org: &datastore.Organisation{UID: "123"}, + update: &models.Organisation{Name: "test_update_org"}, + }, + dbFn: func(os *OrganisationService) { + a, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + a.EXPECT().UpdateOrganisation(gomock.Any(), gomock.Any()). + Times(1).Return(errors.New("failed")) + }, + wantErr: true, + wantErrCode: http.StatusBadRequest, + wantErrMsg: "failed to update organisation", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + os := provideOrganisationService(ctrl) + + // Arrange Expectations + if tt.dbFn != nil { + tt.dbFn(os) + } + + org, err := os.UpdateOrganisation(tt.args.ctx, tt.args.org, tt.args.update) + if tt.wantErr { + require.NotNil(t, err) + require.Equal(t, tt.wantErrCode, err.(*ServiceError).ErrCode()) + require.Equal(t, tt.wantErrMsg, err.(*ServiceError).Error()) + return + } + + require.Nil(t, err) + require.Equal(t, tt.want, org) + }) + } +} + +func TestOrganisationService_FindOrganisationByID(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + id string + } + tests := []struct { + name string + args args + want *datastore.Organisation + dbFn func(os *OrganisationService) + wantErr bool + wantErrCode int + wantErrMsg string + }{ + { + name: "should_find_organisation_by_id", + args: args{ + ctx: ctx, + id: "123", + }, + want: &datastore.Organisation{UID: "123"}, + dbFn: func(os *OrganisationService) { + a, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + a.EXPECT().FetchOrganisationByID(gomock.Any(), "123"). + Times(1).Return(&datastore.Organisation{UID: "123"}, nil) + }, + wantErr: false, + }, + { + name: "should_fail_to_find_organisation_by_id", + args: args{ + ctx: ctx, + id: "123", + }, + dbFn: func(os *OrganisationService) { + a, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + a.EXPECT().FetchOrganisationByID(gomock.Any(), "123"). + Times(1).Return(nil, errors.New("failed")) + }, + wantErr: true, + wantErrCode: http.StatusBadRequest, + wantErrMsg: "failed to find organisation by id", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + os := provideOrganisationService(ctrl) + + // Arrange Expectations + if tt.dbFn != nil { + tt.dbFn(os) + } + + org, err := os.FindOrganisationByID(tt.args.ctx, tt.args.id) + if tt.wantErr { + require.NotNil(t, err) + require.Equal(t, tt.wantErrCode, err.(*ServiceError).ErrCode()) + require.Equal(t, tt.wantErrMsg, err.(*ServiceError).Error()) + return + } + + require.Nil(t, err) + require.Equal(t, tt.want, org) + }) + } +} + +func TestOrganisationService_DeleteOrganisation(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + id string + } + tests := []struct { + name string + args args + dbFn func(os *OrganisationService) + wantErr bool + wantErrCode int + wantErrMsg string + }{ + { + name: "should_find_organisation_by_id", + args: args{ + ctx: ctx, + id: "123", + }, + dbFn: func(os *OrganisationService) { + a, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + a.EXPECT().DeleteOrganisation(gomock.Any(), "123"). + Times(1).Return(nil) + }, + wantErr: false, + }, + { + name: "should_fail_to_find_organisation_by_id", + args: args{ + ctx: ctx, + id: "123", + }, + dbFn: func(os *OrganisationService) { + a, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + a.EXPECT().DeleteOrganisation(gomock.Any(), "123"). + Times(1).Return(errors.New("failed")) + }, + wantErr: true, + wantErrCode: http.StatusBadRequest, + wantErrMsg: "failed to delete organisation", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + os := provideOrganisationService(ctrl) + + // Arrange Expectations + if tt.dbFn != nil { + tt.dbFn(os) + } + + err := os.DeleteOrganisation(tt.args.ctx, tt.args.id) + if tt.wantErr { + require.NotNil(t, err) + require.Equal(t, tt.wantErrCode, err.(*ServiceError).ErrCode()) + require.Equal(t, tt.wantErrMsg, err.(*ServiceError).Error()) + return + } + + require.Nil(t, err) + }) + } +} + +func TestOrganisationService_LoadOrganisationsPaged(t *testing.T) { + ctx := context.Background() + type args struct { + ctx context.Context + pageable datastore.Pageable + } + tests := []struct { + name string + dbFn func(os *OrganisationService) + args args + wantOrganisations []datastore.Organisation + wantPaginationData datastore.PaginationData + wantErr bool + wantErrCode int + wantErrMsg string + }{ + { + name: "should_load_organisations_paged", + args: args{ + ctx: ctx, + pageable: datastore.Pageable{ + Page: 1, + PerPage: 1, + Sort: 1, + }, + }, + wantOrganisations: []datastore.Organisation{ + {UID: "123"}, + {UID: "abc"}, + }, + wantPaginationData: datastore.PaginationData{ + Total: 1, + Page: 1, + PerPage: 1, + Prev: 1, + Next: 1, + TotalPage: 1, + }, + dbFn: func(os *OrganisationService) { + o, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + o.EXPECT().LoadOrganisationsPaged(gomock.Any(), datastore.Pageable{ + Page: 1, + PerPage: 1, + Sort: 1, + }).Times(1).Return( + []datastore.Organisation{ + {UID: "123"}, + {UID: "abc"}, + }, datastore.PaginationData{ + Total: 1, + Page: 1, + PerPage: 1, + Prev: 1, + Next: 1, + TotalPage: 1, + }, + nil) + }, + wantErr: false, + }, + { + name: "should_fail_to_load_organisations_paged", + args: args{ + ctx: ctx, + pageable: datastore.Pageable{ + Page: 1, + PerPage: 1, + Sort: 1, + }, + }, + wantOrganisations: []datastore.Organisation{ + {UID: "123"}, + {UID: "abc"}, + }, + wantPaginationData: datastore.PaginationData{ + Total: 1, + Page: 1, + PerPage: 1, + Prev: 1, + Next: 1, + TotalPage: 1, + }, + dbFn: func(os *OrganisationService) { + o, _ := os.orgRepo.(*mocks.MockOrganisationRepository) + o.EXPECT().LoadOrganisationsPaged(gomock.Any(), datastore.Pageable{ + Page: 1, + PerPage: 1, + Sort: 1, + }).Times(1).Return(nil, datastore.PaginationData{}, errors.New("failed")) + }, + wantErr: true, + wantErrCode: http.StatusBadRequest, + wantErrMsg: "an error occurred while fetching organisations", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + os := provideOrganisationService(ctrl) + + // Arrange Expectations + if tt.dbFn != nil { + tt.dbFn(os) + } + + orgs, paginationData, err := os.LoadOrganisationsPaged(tt.args.ctx, tt.args.pageable) + if tt.wantErr { + require.NotNil(t, err) + require.Equal(t, tt.wantErrCode, err.(*ServiceError).ErrCode()) + require.Equal(t, tt.wantErrMsg, err.(*ServiceError).Error()) + return + } + + require.Nil(t, err) + require.Equal(t, tt.wantOrganisations, orgs) + require.Equal(t, tt.wantPaginationData, paginationData) + }) + } +} diff --git a/services/user_service.go b/services/user_service.go new file mode 100644 index 0000000000..e466ff0b2d --- /dev/null +++ b/services/user_service.go @@ -0,0 +1,151 @@ +package services + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/frain-dev/convoy/auth/realm/jwt" + "github.com/frain-dev/convoy/cache" + "github.com/frain-dev/convoy/config" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/server/models" + "github.com/frain-dev/convoy/util" +) + +type UserService struct { + userRepo datastore.UserRepository + cache cache.Cache + jwt *jwt.Jwt +} + +func NewUserService(userRepo datastore.UserRepository, cache cache.Cache) *UserService { + return &UserService{userRepo: userRepo, cache: cache} +} + +func (u *UserService) LoginUser(ctx context.Context, data *models.LoginUser) (*datastore.User, *jwt.Token, error) { + if err := util.Validate(data); err != nil { + return nil, nil, NewServiceError(http.StatusBadRequest, err) + } + + user, err := u.userRepo.FindUserByEmail(ctx, data.Username) + if err != nil { + if err == datastore.ErrUserNotFound { + return nil, nil, NewServiceError(http.StatusUnauthorized, errors.New("invalid username or password")) + } + + return nil, nil, NewServiceError(http.StatusInternalServerError, err) + } + + p := datastore.Password{Plaintext: data.Password, Hash: []byte(user.Password)} + match, err := p.Matches() + + if err != nil { + return nil, nil, NewServiceError(http.StatusInternalServerError, err) + } + if !match { + return nil, nil, NewServiceError(http.StatusUnauthorized, errors.New("invalid username or password")) + } + + jwt, err := u.token() + if err != nil { + return nil, nil, NewServiceError(http.StatusInternalServerError, err) + } + + token, err := jwt.GenerateToken(user) + if err != nil { + return nil, nil, NewServiceError(http.StatusInternalServerError, err) + } + + return user, &token, nil + +} + +func (u *UserService) RefreshToken(ctx context.Context, data *models.Token) (*jwt.Token, error) { + if err := util.Validate(data); err != nil { + return nil, NewServiceError(http.StatusBadRequest, err) + } + + jw, err := u.token() + if err != nil { + return nil, NewServiceError(http.StatusInternalServerError, err) + } + isValid, err := jw.ValidateAccessToken(data.AccessToken) + if err != nil { + + if errors.Is(err, jwt.ErrTokenExpired) { + expiry := time.Unix(isValid.Expiry, 0) + gracePeriod := expiry.Add(time.Minute * 5) + currentTime := time.Now() + + // We allow a window period from the moment the access token has + // expired + if currentTime.After(gracePeriod) { + return nil, NewServiceError(http.StatusUnauthorized, err) + } + } else { + return nil, NewServiceError(http.StatusUnauthorized, err) + } + } + + verified, err := jw.ValidateRefreshToken(data.RefreshToken) + if err != nil { + return nil, NewServiceError(http.StatusUnauthorized, err) + } + + user, err := u.userRepo.FindUserByID(ctx, verified.UserID) + if err != nil { + if err == datastore.ErrUserNotFound { + return nil, NewServiceError(http.StatusUnauthorized, err) + } + + return nil, NewServiceError(http.StatusUnauthorized, err) + } + + token, err := jw.GenerateToken(user) + if err != nil { + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + err = jw.BlacklistToken(verified, data.RefreshToken) + if err != nil { + return nil, NewServiceError(http.StatusBadRequest, errors.New("failed to blacklist token")) + } + + return &token, nil + +} + +func (u *UserService) LogoutUser(token string) error { + jw, err := u.token() + if err != nil { + return NewServiceError(http.StatusInternalServerError, err) + } + + verified, err := jw.ValidateAccessToken(token) + if err != nil { + return NewServiceError(http.StatusUnauthorized, err) + } + + err = jw.BlacklistToken(verified, token) + if err != nil { + return NewServiceError(http.StatusBadRequest, errors.New("failed to blacklist token")) + } + + return nil +} + +func (u *UserService) token() (*jwt.Jwt, error) { + if u.jwt != nil { + return u.jwt, nil + } + + config, err := config.Get() + if err != nil { + return &jwt.Jwt{}, err + } + + u.jwt = jwt.NewJwt(&config.Auth.Jwt, u.cache) + return u.jwt, nil +} \ No newline at end of file diff --git a/services/user_service_test.go b/services/user_service_test.go new file mode 100644 index 0000000000..5d30e09618 --- /dev/null +++ b/services/user_service_test.go @@ -0,0 +1,367 @@ +package services + +import ( + "context" + "net/http" + "testing" + + "github.com/frain-dev/convoy/config" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/mocks" + "github.com/frain-dev/convoy/server/models" + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func provideUserService(ctrl *gomock.Controller, t *testing.T) *UserService { + userRepo := mocks.NewMockUserRepository(ctrl) + cache := mocks.NewMockCache(ctrl) + + err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") + require.Nil(t, err) + + userService := NewUserService(userRepo, cache) + return userService +} + +func TestUserService_LoginUser(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + user *models.LoginUser + } + + tests := []struct { + name string + args args + wantUser *datastore.User + dbFn func(u *UserService) + wantConfig bool + wantErr bool + wantErrCode int + wantErrMsg string + }{ + { + name: "should_login_user_with_valid_credentials", + args: args{ + ctx: ctx, + user: &models.LoginUser{Username: "test@test.com", Password: "123456"}, + }, + wantUser: &datastore.User{ + UID: "12345", + FirstName: "test", + LastName: "test", + Email: "test@test.com", + }, + dbFn: func(u *UserService) { + us, _ := u.userRepo.(*mocks.MockUserRepository) + p := &datastore.Password{Plaintext: "123456"} + err := p.GenerateHash() + + if err != nil { + t.Fatal(err) + } + + us.EXPECT().FindUserByEmail(gomock.Any(), gomock.Any()).Times(1).Return(&datastore.User{ + UID: "12345", + FirstName: "test", + LastName: "test", + Email: "test@test.com", + Password: string(p.Hash), + }, nil) + }, + wantConfig: true, + }, + + { + name: "should_not_login_with_invalid_username", + args: args{ + ctx: ctx, + user: &models.LoginUser{Username: "invalid@test.com", Password: "123456"}, + }, + dbFn: func(u *UserService) { + us, _ := u.userRepo.(*mocks.MockUserRepository) + us.EXPECT().FindUserByEmail(gomock.Any(), gomock.Any()).Times(1).Return(nil, datastore.ErrUserNotFound) + }, + wantErr: true, + wantErrCode: http.StatusUnauthorized, + wantErrMsg: "invalid username or password", + }, + + { + name: "should_not_login_with_invalid_password", + args: args{ + ctx: ctx, + user: &models.LoginUser{Username: "test@test.com", Password: "12345"}, + }, + dbFn: func(u *UserService) { + us, _ := u.userRepo.(*mocks.MockUserRepository) + p := &datastore.Password{Plaintext: "123456"} + err := p.GenerateHash() + + if err != nil { + t.Fatal(err) + } + + us.EXPECT().FindUserByEmail(gomock.Any(), gomock.Any()).Times(1).Return(&datastore.User{ + UID: "12345", + FirstName: "test", + LastName: "test", + Email: "test@test.com", + Password: string(p.Hash), + }, nil) + }, + wantErr: true, + wantErrCode: http.StatusUnauthorized, + wantErrMsg: "invalid username or password", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + u := provideUserService(ctrl, t) + + if tc.dbFn != nil { + tc.dbFn(u) + } + + if tc.wantConfig { + err := config.LoadConfig("./testdata/Auth_Config/full-convoy.json") + require.Nil(t, err) + } + + user, token, err := u.LoginUser(tc.args.ctx, tc.args.user) + if tc.wantErr { + require.NotNil(t, err) + require.Equal(t, tc.wantErrCode, err.(*ServiceError).ErrCode()) + require.Equal(t, tc.wantErrMsg, err.(*ServiceError).Error()) + return + } + + require.Nil(t, err) + require.NotEmpty(t, user.UID) + require.NotEmpty(t, user.FirstName) + + require.NotEmpty(t, token.AccessToken) + require.NotEmpty(t, token.RefreshToken) + + require.Equal(t, user.FirstName, tc.wantUser.FirstName) + require.Equal(t, user.LastName, tc.wantUser.LastName) + require.Equal(t, user.Email, tc.wantUser.Email) + }) + } +} + +func TestUserService_RefreshToken(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + user *datastore.User + token *models.Token + } + + type token struct { + generate bool + accessToken bool + refreshToken bool + } + + tests := []struct { + name string + args args + dbFn func(u *UserService) + wantConfig bool + wantToken token + wantErr bool + wantErrCode int + wantErrMsg string + }{ + { + name: "should_refresh_token", + args: args{ + ctx: ctx, + user: &datastore.User{UID: "123456"}, + token: &models.Token{}, + }, + dbFn: func(u *UserService) { + us, _ := u.userRepo.(*mocks.MockUserRepository) + ca, _ := u.cache.(*mocks.MockCache) + + us.EXPECT().FindUserByID(gomock.Any(), gomock.Any()).Times(1).Return(&datastore.User{UID: "123456"}, nil) + ca.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(nil) + ca.EXPECT().Set(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) + }, + wantConfig: true, + wantToken: token{generate: true, accessToken: true, refreshToken: true}, + }, + + { + name: "should_fail_to_refresh_for_invalid_access_token", + args: args{ + ctx: ctx, + token: &models.Token{AccessToken: uuid.NewString(), RefreshToken: uuid.NewString()}, + }, + dbFn: func(u *UserService) { + ca, _ := u.cache.(*mocks.MockCache) + ca.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) + }, + wantErr: true, + wantErrCode: http.StatusUnauthorized, + }, + + { + name: "should_fail_to_refresh_for_invalid_refresh_token", + args: args{ + ctx: ctx, + user: &datastore.User{UID: "123456"}, + token: &models.Token{RefreshToken: uuid.NewString()}, + }, + dbFn: func(u *UserService) { + ca, _ := u.cache.(*mocks.MockCache) + ca.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(nil) + }, + wantToken: token{generate: true, accessToken: true}, + wantErr: true, + wantErrCode: http.StatusUnauthorized, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + u := provideUserService(ctrl, t) + + if tc.dbFn != nil { + tc.dbFn(u) + } + + if tc.wantToken.generate { + jwt, err := u.token() + require.Nil(t, err) + + token, err := jwt.GenerateToken(tc.args.user) + require.Nil(t, err) + + if tc.wantToken.accessToken { + tc.args.token.AccessToken = token.AccessToken + } + + if tc.wantToken.refreshToken { + tc.args.token.RefreshToken = token.RefreshToken + } + } + + token, err := u.RefreshToken(tc.args.ctx, tc.args.token) + + if tc.wantErr { + require.NotNil(t, err) + require.Equal(t, tc.wantErrCode, err.(*ServiceError).ErrCode()) + return + } + + require.Nil(t, err) + require.NotEmpty(t, token.AccessToken) + require.NotEmpty(t, token.RefreshToken) + }) + } +} + +func TestUserService_LogoutUser(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + user *datastore.User + token *models.Token + } + + type token struct { + generate bool + accessToken bool + } + + tests := []struct { + name string + args args + dbFn func(u *UserService) + wantConfig bool + wantToken token + wantErr bool + wantErrCode int + wantErrMsg string + }{ + + { + name: "should_logout_user", + args: args{ + ctx: ctx, + user: &datastore.User{UID: "12345"}, + token: &models.Token{}, + }, + dbFn: func(u *UserService) { + ca, _ := u.cache.(*mocks.MockCache) + ca.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) + ca.EXPECT().Set(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) + }, + wantToken: token{generate: true, accessToken: true}, + }, + + { + name: "should_fail_to_logout_user_with_invalid_access_token", + args: args{ + ctx: ctx, + user: &datastore.User{UID: "12345"}, + token: &models.Token{AccessToken: uuid.NewString()}, + }, + dbFn: func(u *UserService) { + ca, _ := u.cache.(*mocks.MockCache) + ca.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) + }, + wantErr: true, + wantErrCode: http.StatusUnauthorized, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + u := provideUserService(ctrl, t) + + if tc.dbFn != nil { + tc.dbFn(u) + } + + if tc.wantToken.generate { + jwt, err := u.token() + require.Nil(t, err) + + token, err := jwt.GenerateToken(tc.args.user) + require.Nil(t, err) + + if tc.wantToken.accessToken { + tc.args.token.AccessToken = token.AccessToken + } + } + + err := u.LogoutUser(tc.args.token.AccessToken) + + if tc.wantErr { + require.NotNil(t, err) + require.Equal(t, tc.wantErrCode, err.(*ServiceError).ErrCode()) + return + } + + require.Nil(t, err) + }) + } +} diff --git a/type.go b/type.go index 04a9df9f63..0ea96a4872 100644 --- a/type.go +++ b/type.go @@ -70,6 +70,7 @@ const ( CreateEventProcessor TaskName = "CreateEventProcessor" ApplicationsCacheKey CacheKey = "applications" GroupsCacheKey CacheKey = "groups" + TokenCacheKey CacheKey = "tokens" ) const ( diff --git a/util/crypto.go b/util/crypto.go index e9569c6e80..57aaad0dbc 100644 --- a/util/crypto.go +++ b/util/crypto.go @@ -18,8 +18,8 @@ import ( ) const ( - prefix string = "CO" - seperator string = "." + Prefix string = "CO" + Seperator string = "." ) func ComputeJSONHmac(hash, data, secret string, order bool) (string, error) { @@ -84,10 +84,10 @@ func GenerateAPIKey() (string, string) { var api_key strings.Builder - api_key.WriteString(prefix) - api_key.WriteString(seperator) + api_key.WriteString(Prefix) + api_key.WriteString(Seperator) api_key.WriteString(mask) - api_key.WriteString(seperator) + api_key.WriteString(Seperator) api_key.WriteString(key) return mask, api_key.String() diff --git a/worker/scheduler.go b/worker/scheduler.go index 3a887587a8..6d92e597ae 100644 --- a/worker/scheduler.go +++ b/worker/scheduler.go @@ -3,7 +3,6 @@ package worker import ( "context" "fmt" - "sync" "time" "github.com/frain-dev/convoy" @@ -11,12 +10,35 @@ import ( "github.com/frain-dev/convoy/datastore" "github.com/frain-dev/convoy/limiter" "github.com/frain-dev/convoy/queue" - redisqueue "github.com/frain-dev/convoy/queue/redis" "github.com/frain-dev/convoy/worker/task" "github.com/frain-dev/disq" + "github.com/go-co-op/gocron" log "github.com/sirupsen/logrus" ) +type Scheduler struct { + inner *gocron.Scheduler + queue *queue.Queuer +} + +func NewScheduler(queue *queue.Queuer) *Scheduler { + return &Scheduler{ + inner: gocron.NewScheduler(time.UTC), + queue: queue, + } +} + +func (s *Scheduler) Start() { + s.inner.StartBlocking() +} + +func (s *Scheduler) AddTask(name string, secs int, task interface{}) { + _, err := s.inner.Every(secs).Seconds().Do(task) + if err != nil { + log.WithError(err).Fatalf("Failed to add %s scheduler task", name) + } +} + func RegisterNewGroupTask(applicationRepo datastore.ApplicationRepository, eventDeliveryRepo datastore.EventDeliveryRepository, groupRepo datastore.GroupRepository, rateLimiter limiter.RateLimiter, eventRepo datastore.EventRepository, cache cache.Cache, eventQueue queue.Queuer, subRepo datastore.SubscriptionRepository) { go func() { for { @@ -52,148 +74,3 @@ func RegisterNewGroupTask(applicationRepo datastore.ApplicationRepository, event } }() } - -func RequeueEventDeliveries(status string, timeInterval string, eventDeliveryRepo datastore.EventDeliveryRepository, groupRepo datastore.GroupRepository, eventQueue queue.Queuer) error { - d, err := time.ParseDuration(timeInterval) - if err != nil { - return fmt.Errorf("failed to parse time duration") - } - - s := datastore.EventDeliveryStatus(status) - if !s.IsValid() { - return fmt.Errorf("invalid event delivery status %s", s) - } - log.Infof("Requeuing for Status %v", status) - - now := time.Now() - then := now.Add(-d) - searchParams := datastore.SearchParams{ - CreatedAtStart: then.Unix(), - CreatedAtEnd: now.Unix(), - } - - pageable := datastore.Pageable{ - Page: 0, - PerPage: 1000, - Sort: -1, - } - - deliveryChan := make(chan []datastore.EventDelivery, 4) - count := 0 - - ctx := context.Background() - var q *redisqueue.RedisQueue - q, ok := eventQueue.(*redisqueue.RedisQueue) - if !ok { - return fmt.Errorf("invalid queue type for requeing event deliveries: %T", eventQueue) - } - - var wg sync.WaitGroup - - wg.Add(1) - go ProcessEventDeliveryBatches(ctx, s, eventDeliveryRepo, groupRepo, deliveryChan, q, &wg) - - counter, err := eventDeliveryRepo.CountDeliveriesByStatus(ctx, s, searchParams) - if err != nil { - return fmt.Errorf("failed to count event deliveries") - } - log.Infof("total number of event deliveries to requeue is %d", counter) - - for { - deliveries, _, err := eventDeliveryRepo.LoadEventDeliveriesPaged(ctx, "", "", "", []datastore.EventDeliveryStatus{s}, searchParams, pageable) - if err != nil { - log.WithError(err).Errorf("successfully fetched %d event deliveries, encountered error fetching page %d", count, pageable.Page) - close(deliveryChan) - log.Info("closed delivery channel") - break - } - - // stop when len(deliveries) is 0 - if len(deliveries) == 0 { - log.Info("no deliveries received from db, exiting") - close(deliveryChan) - log.Info("closed delivery channel") - break - } - - count += len(deliveries) - deliveryChan <- deliveries - pageable.Page++ - } - - log.Info("waiting for batch processor to finish") - wg.Wait() - return nil -} - -func ProcessEventDeliveryBatches(ctx context.Context, status datastore.EventDeliveryStatus, eventDeliveryRepo datastore.EventDeliveryRepository, groupRepo datastore.GroupRepository, deliveryChan <-chan []datastore.EventDelivery, q *redisqueue.RedisQueue, wg *sync.WaitGroup) { - defer wg.Done() - - // groups serves as a cache for already fetched groups - groups := map[string]*datastore.Group{} - - batchCount := 1 - for { - // ok will return false if the channel is closed and drained(empty), at which point - // we should return - batch, ok := <-deliveryChan - if !ok { - // the channel has been closed and there are no more deliveries coming in - log.Infof("batch processor exiting") - return - } - - batchIDs := make([]string, len(batch)) - for i := range batch { - batchIDs[i] = batch[i].UID - } - - if status == datastore.ProcessingEventStatus { - err := eventDeliveryRepo.UpdateStatusOfEventDeliveries(ctx, batchIDs, datastore.ScheduledEventStatus) - if err != nil { - log.WithError(err).Errorf("batch %d: failed to update event deliveries status", batchCount) - } - } - - // remove these event deliveries from the zset - err := q.DeleteEventDeliveriesFromZSET(ctx, batchIDs) - if err != nil { - log.WithError(err).WithField("ids", batchIDs).Errorf("batch %d: failed to delete event deliveries from zset", batchCount) - } - - // // remove these event deliveries from the stream - err = q.DeleteEventDeliveriesFromStream(ctx, batchIDs) - if err != nil { - log.WithError(err).WithField("ids", batchIDs).Errorf("batch %d: failed to delete event deliveries from stream", batchCount) - } - - var group *datastore.Group - for i := range batch { - delivery := &batch[i] - groupID := delivery.GroupID - - group, ok = groups[groupID] - if !ok { // never seen this group before, so fetch and cache - group, err = groupRepo.FetchGroupByID(ctx, delivery.GroupID) - if err != nil { - log.WithError(err).Errorf("batch %d: failed to fetch group %s for delivery %s", batchCount, delivery.GroupID, delivery.UID) - continue - } - groups[groupID] = group - } - - taskName := convoy.EventProcessor.SetPrefix(group.Name) - job := &queue.Job{ - ID: delivery.UID, - } - err = q.Publish(ctx, taskName, job, 1*time.Second) - if err != nil { - log.WithError(err).Errorf("batch %d: failed to send event delivery %s to the queue", batchCount, delivery.ID) - } - log.Infof("sucessfully requeued delivery with id: %s", delivery.UID) - } - - log.Infof("batch %d: sucessfully requeued %d deliveries", batchCount, len(batch)) - batchCount++ - } -} diff --git a/worker/task/process_event_delivery_test.go b/worker/task/process_event_delivery_test.go index d753b15a68..29e96cdcf5 100644 --- a/worker/task/process_event_delivery_test.go +++ b/worker/task/process_event_delivery_test.go @@ -710,6 +710,8 @@ func TestProcessEventDelivery(t *testing.T) { appRepo := mocks.NewMockApplicationRepository(ctrl) msgRepo := mocks.NewMockEventDeliveryRepository(ctrl) apiKeyRepo := mocks.NewMockAPIKeyRepository(ctrl) + userRepo := mocks.NewMockUserRepository(ctrl) + cache := mocks.NewMockCache(ctrl) rateLimiter := mocks.NewMockRateLimiter(ctrl) subRepo := mocks.NewMockSubscriptionRepository(ctrl) @@ -723,7 +725,7 @@ func TestProcessEventDelivery(t *testing.T) { t.Errorf("failed to get config: %v", err) } - err = realm_chain.Init(&cfg.Auth, apiKeyRepo) + err = realm_chain.Init(&cfg.Auth, apiKeyRepo, userRepo, cache) if err != nil { t.Errorf("failed to initialize realm chain : %v", err) } diff --git a/worker/task/retry_event_deliveries.go b/worker/task/retry_event_deliveries.go new file mode 100644 index 0000000000..96e3f021c8 --- /dev/null +++ b/worker/task/retry_event_deliveries.go @@ -0,0 +1,163 @@ +package task + +import ( + "context" + "sync" + "time" + + "github.com/frain-dev/convoy" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/queue" + redisqueue "github.com/frain-dev/convoy/queue/redis" + "github.com/frain-dev/convoy/util" + log "github.com/sirupsen/logrus" +) + +func RetryEventDeliveries(statuses []datastore.EventDeliveryStatus, lookBackDuration string, eventDeliveryRepo datastore.EventDeliveryRepository, groupRepo datastore.GroupRepository, eventQueue queue.Queuer) { + if statuses == nil { + statuses = []datastore.EventDeliveryStatus{"Retry", "Scheduled", "Processing"} + } + + if util.IsStringEmpty(lookBackDuration) { + // TODO(subomi): Setup configuration + lookBackDuration = "5h" + } + + d, err := time.ParseDuration(lookBackDuration) + if err != nil { + log.Error("Failed to parse time duration") + } + now := time.Now() + then := now.Add(-d) + + for _, status := range statuses { + searchParams := datastore.SearchParams{ + CreatedAtStart: then.Unix(), + CreatedAtEnd: now.Unix(), + } + + pageable := datastore.Pageable{ + Page: 0, + PerPage: 1000, + Sort: -1, + } + + deliveryChan := make(chan []datastore.EventDelivery, 4) + count := 0 + + ctx := context.Background() + var q *redisqueue.RedisQueue + q, ok := eventQueue.(*redisqueue.RedisQueue) + if !ok { + log.Errorf("Invalid queue type for requeing event deliveries: %T", eventQueue) + } + + var wg sync.WaitGroup + + wg.Add(1) + go processEventDeliveryBatch(ctx, status, eventDeliveryRepo, groupRepo, deliveryChan, q, &wg) + + counter, err := eventDeliveryRepo.CountDeliveriesByStatus(ctx, status, searchParams) + if err != nil { + log.Error("Failed to count event deliveries") + } + log.Infof("Total number of event deliveries to requeue is %d", counter) + + for { + deliveries, _, err := eventDeliveryRepo.LoadEventDeliveriesPaged(ctx, "", "", "", []datastore.EventDeliveryStatus{status}, searchParams, pageable) + if err != nil { + log.WithError(err).Errorf("successfully fetched %d event deliveries, encountered error fetching page %d", count, pageable.Page) + close(deliveryChan) + log.Info("closed delivery channel") + break + } + + // stop when len(deliveries) is 0 + if len(deliveries) == 0 { + log.Info("no deliveries received from db, exiting") + close(deliveryChan) + log.Info("closed delivery channel") + break + } + + count += len(deliveries) + deliveryChan <- deliveries + pageable.Page++ + } + + log.Info("waiting for batch processor to finish") + wg.Wait() + } +} + +func processEventDeliveryBatch(ctx context.Context, status datastore.EventDeliveryStatus, eventDeliveryRepo datastore.EventDeliveryRepository, groupRepo datastore.GroupRepository, deliveryChan <-chan []datastore.EventDelivery, q *redisqueue.RedisQueue, wg *sync.WaitGroup) { + defer wg.Done() + + // groups serves as a cache for already fetched groups + groups := map[string]*datastore.Group{} + + batchCount := 1 + for { + // ok will return false if the channel is closed and drained(empty), at which point + // we should return + batch, ok := <-deliveryChan + if !ok { + // the channel has been closed and there are no more deliveries coming in + log.Infof("batch processor exiting") + return + } + + batchIDs := make([]string, len(batch)) + for i := range batch { + batchIDs[i] = batch[i].UID + } + + if status == datastore.ProcessingEventStatus { + err := eventDeliveryRepo.UpdateStatusOfEventDeliveries(ctx, batchIDs, datastore.ScheduledEventStatus) + if err != nil { + log.WithError(err).Errorf("batch %d: failed to update event deliveries status", batchCount) + } + } + + // remove these event deliveries from the zset + err := q.DeleteEventDeliveriesFromZSET(ctx, batchIDs) + if err != nil { + log.WithError(err).WithField("ids", batchIDs).Errorf("batch %d: failed to delete event deliveries from zset", batchCount) + } + + // // remove these event deliveries from the stream + err = q.DeleteEventDeliveriesFromStream(ctx, batchIDs) + if err != nil { + log.WithError(err).WithField("ids", batchIDs).Errorf("batch %d: failed to delete event deliveries from stream", batchCount) + } + + var group *datastore.Group + for i := range batch { + delivery := &batch[i] + groupID := delivery.GroupID + + group, ok = groups[groupID] + if !ok { // never seen this group before, so fetch and cache + group, err = groupRepo.FetchGroupByID(ctx, delivery.GroupID) + if err != nil { + log.WithError(err).Errorf("batch %d: failed to fetch group %s for delivery %s", batchCount, delivery.GroupID, delivery.UID) + continue + } + groups[groupID] = group + } + + taskName := convoy.EventProcessor.SetPrefix(group.Name) + job := &queue.Job{ + ID: delivery.UID, + } + err = q.Publish(ctx, taskName, job, 1*time.Second) + if err != nil { + log.WithError(err).Errorf("batch %d: failed to send event delivery %s to the queue", batchCount, delivery.ID) + } + log.Infof("sucessfully requeued delivery with id: %s", delivery.UID) + } + + log.Infof("batch %d: sucessfully requeued %d deliveries", batchCount, len(batch)) + batchCount++ + } +}