NOISSUE - Implement Invitations Service Outline (#116)

* feat(invitations): Implement invitations outline

This commit adds the necessary data structures and methods for managing invitations to join a domain. It includes structs for invitations, pages of invitations, and pages of invitations with additional metadata. Additionally, error variables for missing and invalid relations are defined.
The commit also introduces methods for sending, viewing, listing, accepting, and deleting invitations. In addition, it includes functions for creating, retrieving, updating, and deleting invitations in a repository.
Furthermore, a function is added to check the validity of a relation, returning an error if it does not match specific values.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* docs(invitations): add godocs

* test: Add tests for MarshalJSON and CheckRelation

Add tests to ensure the MarshalJSON function of the InvitationPage struct correctly marshals the JSON representation. Also, add tests for the CheckRelation function to verify its behavior in different scenarios. The tests use a loop and the assert.Equal function to compare expected and actual errors.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* feat(invitations): add withToken parameter to RetrieveAll method

This commit adds a new parameter "withToken" to the RetrieveAll method in the invitations package. This parameter allows the caller to specify whether they want to include the token in the returned invitation list.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* docs(invitations): Add Base README

---------

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
This commit is contained in:
b1ackd0t
2023-12-07 16:15:16 +03:00
committed by GitHub
parent 71e600798d
commit 68f551f81d
5 changed files with 298 additions and 0 deletions
+13
View File
@@ -99,6 +99,13 @@ jobs:
internal:
- "internal/**"
invitations:
- "invitations/**"
- "cmd/invitations/**"
- "auth.proto"
- "auth.pb.go"
- "auth_grpc.pb.go"
lora:
- "lora/**"
- "cmd/lora/**"
@@ -204,6 +211,12 @@ jobs:
go test --race -v -count=1 -coverprofile=coverage/internal.out ./internal/...
go tool cover -html=coverage/internal.out -o coverage/internal.html
- name: Run invitations tests
if: steps.changes.outputs.invitations == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/invitations.out ./invitations/...
go tool cover -html=coverage/invitations.out -o coverage/invitations.html
- name: Run logger tests
if: steps.changes.outputs.logger == 'true'
run: |
+80
View File
@@ -0,0 +1,80 @@
# Invitation Service
Invitation service is responsible for sending invitations to users to join a domain.
## Configuration
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
| Variable | Description | Default |
| ------------------------------- | ------------------------------------------------ | ----------------------- |
| MG_INVITATION_LOG_LEVEL | Log level for the Invitation service | debug |
| MG_USERS_URL | Users service URL | <http://localhost:9002> |
| MG_DOMAINS_URL | Domains service URL | <http://localhost:8189> |
| MG_INVITATIONS_HTTP_HOST | Invitation service HTTP listening host | localhost |
| MG_INVITATIONS_HTTP_PORT | Invitation service HTTP listening port | 9020 |
| MG_INVITATIONS_HTTP_SERVER_CERT | Invitation service server certificate | "" |
| MG_INVITATIONS_HTTP_SERVER_KEY | Invitation service server key | "" |
| MG_AUTH_GRPC_URL | Auth service gRPC URL | localhost:8181 |
| MG_AUTH_GRPC_TIMEOUT | Auth service gRPC request timeout in seconds | 1s |
| MG_AUTH_GRPC_CLIENT_CERT | Path to client certificate in PEM format | "" |
| MG_AUTH_GRPC_CLIENT_KEY | Path to client key in PEM format | "" |
| MG_AUTH_GRPC_CLIENT_CA_CERTS | Path to trusted CAs in PEM format | "" |
| MG_INVITATIONS_DB_HOST | Invitation service database host | localhost |
| MG_INVITATIONS_DB_USER | Invitation service database user | magistrala |
| MG_INVITATIONS_DB_PASS | Invitation service database password | magistrala |
| MG_INVITATIONS_DB_PORT | Invitation service database port | 5432 |
| MG_INVITATIONS_DB_NAME | Invitation service database name | invitations |
| MG_INVITATIONS_DB_SSL_MODE | Invitation service database SSL mode | disable |
| MG_INVITATIONS_DB_SSL_CERT | Invitation service database SSL certificate | "" |
| MG_INVITATIONS_DB_SSL_KEY | Invitation service database SSL key | "" |
| MG_INVITATIONS_DB_SSL_ROOT_CERT | Invitation service database SSL root certificate | "" |
| MG_INVITATIONS_INSTANCE_ID | Invitation service instance ID | |
## Deployment
The service itself is distributed as Docker container. Check the [`invitation`](https://github.com/absmach/amdm/blob/master/docker/docker-compose.yml) service section in docker-compose to see how service is deployed.
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
git clone https://github.com/absmach/magistrala
cd magistrala
# compile the http
make invitation
# copy binary to bin
make install
# set the environment variables and run the service
MG_INVITATION_LOG_LEVEL=info \
MG_INVITATIONS_ENDPOINT=/invitations \
MG_USERS_URL="http://localhost:9002" \
MG_DOMAINS_URL="http://localhost:8189" \
MG_INVITATIONS_HTTP_HOST=localhost \
MG_INVITATIONS_HTTP_PORT=9020 \
MG_INVITATIONS_HTTP_SERVER_CERT="" \
MG_INVITATIONS_HTTP_SERVER_KEY="" \
MG_AUTH_GRPC_URL=localhost:8181 \
MG_AUTH_GRPC_TIMEOUT=1s \
MG_AUTH_GRPC_CLIENT_CERT="" \
MG_AUTH_GRPC_CLIENT_KEY="" \
MG_AUTH_GRPC_CLIENT_CA_CERTS="" \
MG_INVITATIONS_DB_HOST=localhost \
MG_INVITATIONS_DB_USER=magistrala \
MG_INVITATIONS_DB_PASS=magistrala \
MG_INVITATIONS_DB_PORT=5432 \
MG_INVITATIONS_DB_NAME=invitations \
MG_INVITATIONS_DB_SSL_MODE=disable \
MG_INVITATIONS_DB_SSL_CERT="" \
MG_INVITATIONS_DB_SSL_KEY="" \
MG_INVITATIONS_DB_SSL_ROOT_CERT="" \
$GOBIN/magistrala-invitation
```
## Usage
For more information about service capabilities and its usage, please check out the [API documentation](https://api.mainflux.io/?urls.primaryName=invitations.yml).
+7
View File
@@ -0,0 +1,7 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package invitations provides the API to manage invitations.
//
// An invitation is a request to join a domain.
package invitations
+119
View File
@@ -0,0 +1,119 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package invitations
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/absmach/magistrala/auth"
)
var (
errMissingRelation = errors.New("missing relation")
errInvalidRelation = errors.New("invalid relation")
)
// Invitation is an invitation to join a domain.
type Invitation struct {
InvitedBy string `json:"invited_by" db:"invited_by"`
UserID string `json:"user_id" db:"user_id"`
Domain string `json:"domain" db:"domain"`
Token string `json:"token,omitempty" db:"token"`
Relation string `json:"relation,omitempty" db:"relation"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at,omitempty"`
ConfirmedAt time.Time `json:"confirmed_at,omitempty" db:"confirmed_at,omitempty"`
}
// Page is a page of invitations.
type Page struct {
Offset uint64 `json:"offset" db:"offset"`
Limit uint64 `json:"limit" db:"limit"`
InvitedBy string `json:"invited_by,omitempty" db:"invited_by,omitempty"`
UserID string `json:"user_id,omitempty" db:"user_id,omitempty"`
Domain string `json:"domain,omitempty" db:"domain,omitempty"`
Relation string `json:"relation,omitempty" db:"relation,omitempty"`
}
// InvitationPage is a page of invitations.
type InvitationPage struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Invitations []Invitation `json:"invitations"`
}
func (page InvitationPage) MarshalJSON() ([]byte, error) {
type Alias InvitationPage
a := struct {
Alias
}{
Alias: Alias(page),
}
if a.Invitations == nil {
a.Invitations = make([]Invitation, 0)
}
return json.Marshal(a)
}
// Service is an interface that defines methods for managing invitations.
type Service interface {
// SendInvitation sends an invitation to the email address associated with the given user.
SendInvitation(ctx context.Context, token, host string, invitation Invitation) (err error)
// ViewInvitation returns an invitation.
ViewInvitation(ctx context.Context, token string, userID, domain string) (invitation Invitation, err error)
// ListInvitations returns a list of invitations.
ListInvitations(ctx context.Context, token string, page Page) (invitations InvitationPage, err error)
// AcceptInvitation accepts an invitation by adding the user to the domain.
AcceptInvitation(ctx context.Context, token string) (userID string, domains []string, err error)
// DeleteInvitation deletes an invitation.
DeleteInvitation(ctx context.Context, token string, userID, domain string) (err error)
}
type Repository interface {
// Create creates an invitation.
Create(ctx context.Context, invitation Invitation) (err error)
// RetrieveAll returns a list of invitations based on the given page.
RetrieveAll(ctx context.Context, withToken bool, page Page) (invitations InvitationPage, err error)
// UpdateToken updates an invitation by setting the token.
UpdateToken(ctx context.Context, invitation Invitation) (err error)
// UpdateConfirmation updates an invitation by setting the confirmation time.
UpdateConfirmation(ctx context.Context, invitation Invitation) (err error)
// Delete deletes an invitation.
Delete(ctx context.Context, userID, domain string) (err error)
}
// CheckRelation checks if the given relation is valid.
// It returns an error if the relation is empty or invalid.
func CheckRelation(relation string) error {
if relation == "" {
return errMissingRelation
}
if relation != auth.AdministratorRelation &&
relation != auth.EditorRelation &&
relation != auth.ViewerRelation &&
relation != auth.MemberRelation &&
relation != auth.DomainRelation &&
relation != auth.ParentGroupRelation &&
relation != auth.RoleGroupRelation &&
relation != auth.GroupRelation &&
relation != auth.PlatformRelation {
return errInvalidRelation
}
return nil
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package invitations_test
import (
"errors"
"fmt"
"testing"
"github.com/absmach/magistrala/invitations"
"github.com/stretchr/testify/assert"
)
var (
errMissingRelation = errors.New("missing relation")
errInvalidRelation = errors.New("invalid relation")
)
func TestInvitation_MarshalJSON(t *testing.T) {
cases := []struct {
desc string
page invitations.InvitationPage
res string
}{
{
desc: "empty page",
page: invitations.InvitationPage{
Invitations: []invitations.Invitation(nil),
},
res: `{"total":0,"offset":0,"limit":0,"invitations":[]}`,
},
{
desc: "page with invitations",
page: invitations.InvitationPage{
Total: 1,
Offset: 0,
Limit: 0,
Invitations: []invitations.Invitation{
{
InvitedBy: "John",
UserID: "123",
Domain: "123",
},
},
},
res: `{"total":1,"offset":0,"limit":0,"invitations":[{"invited_by":"John","user_id":"123","domain":"123","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","confirmed_at":"0001-01-01T00:00:00Z"}]}`,
},
}
for _, tc := range cases {
data, err := tc.page.MarshalJSON()
assert.NoError(t, err, "Unexpected error: %v", err)
assert.Equal(t, tc.res, string(data), fmt.Sprintf("%s: expected %s, got %s", tc.desc, tc.res, string(data)))
}
}
func TestCheckRelation(t *testing.T) {
cases := []struct {
relation string
err error
}{
{"", errMissingRelation},
{"admin", errInvalidRelation},
{"editor", nil},
{"viewer", nil},
{"member", nil},
{"domain", nil},
{"parent_group", nil},
{"role_group", nil},
{"group", nil},
{"platform", nil},
}
for _, tc := range cases {
err := invitations.CheckRelation(tc.relation)
assert.Equal(t, tc.err, err, "CheckRelation(%q) expected %v, got %v", tc.relation, tc.err, err)
}
}