NOISSUE - Add Invitation service (#126)

* feat(service): Add new "invitations" service

This commit adds a new service called "invitations" to the existing file. The service includes the necessary imports and initializes components for its functionality. It also includes configuration settings and a Docker Compose file. Additionally, instructions for deploying and using the service are provided, along with a function to create an HTTP handler.

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

* docs(api): invitation api

The commit adds documentation for an API that allows users to manage invitations. It includes information about the endpoints, parameters, data types, and components used in the API. The documentation also outlines the properties and specifications of the Invitation object. This commit provides a comprehensive overview of the API's functionality and structure.

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

* fix: accept invitation to take in domain

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

* refactor(invitations): rename domain to domainID

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

* Authorize on id(domain+user) rather than user

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

---------

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
This commit is contained in:
b1ackd0t
2023-12-11 20:24:37 +03:00
committed by GitHub
parent 0ecf5aa746
commit a07aabe783
14 changed files with 1128 additions and 48 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ MG_DOCKER_IMAGE_NAME_PREFIX ?= magistrala
BUILD_DIR = build
SERVICES = auth users things http coap ws lora influxdb-writer influxdb-reader mongodb-writer \
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader timescale-writer timescale-reader cli \
bootstrap opcua twins mqtt provision certs smtp-notifier smpp-notifier
bootstrap opcua twins mqtt provision certs smtp-notifier smpp-notifier invitations
DOCKERS = $(addprefix docker_,$(SERVICES))
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
CGO_ENABLED ?= 0
@@ -244,7 +244,7 @@ ifeq ($(MG_ES_TYPE), redis)
else
sed -i "s,MG_ES_TYPE=.*,MG_ES_TYPE=$$\{MG_MESSAGE_BROKER_TYPE}," docker/.env
sed -i "s,MG_ES_URL=.*,MG_ES_URL=$$\{MG_$(shell echo ${MG_MESSAGE_BROKER_TYPE} | tr 'a-z' 'A-Z')_URL\}," docker/.env
docker-compose -f docker/docker-compose.yml --profile $(DOCKER_PROFILE) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
docker-compose -f docker/docker-compose.yml --env-file docker/.env --profile $(DOCKER_PROFILE) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
endif
run_addons: check_certs
+472
View File
@@ -0,0 +1,472 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.3
info:
title: Magistrala Invitations Service
description: |
This is the Invitations Server based on the OpenAPI 3.0 specification. It is the HTTP API for managing platform invitations. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@mainflux.com
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/master/LICENSE
version: 0.14.0
servers:
- url: http://localhost:9020
- url: https://localhost:9020
tags:
- name: Invitations
description: Everything about your Invitations
externalDocs:
description: Find out more about Invitations
url: http://docs.mainflux.io/
paths:
/invitations:
post:
tags:
- Invitations
summary: Send invitation
description: |
Send invitation to user to join domain.
requestBody:
$ref: "#/components/requestBodies/SendInvitationReq"
security:
- bearerAuth: []
responses:
"201":
description: Invitation sent.
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"409":
description: Failed due to using an existing identity.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
get:
tags:
- Invitations
summary: List invitations
description: |
Retrieves a list of invitations. Due to performance concerns, data
is retrieved in subsets. The API must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
parameters:
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/UserID"
- $ref: "#/components/parameters/InvitedBy"
- $ref: "#/components/parameters/DomainID"
- $ref: "#/components/parameters/Relation"
security:
- bearerAuth: []
responses:
"200":
$ref: "#/components/responses/InvitationPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: |
Missing or invalid access token provided.
This endpoint is available only for administrators.
"404":
description: A non-existent entity request.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/invitations/accept:
post:
summary: Accept invitation
description: |
Current logged in user accepts invitation to join domain.
tags:
- Invitations
security:
- bearerAuth: []
requestBody:
$ref: "#/components/requestBodies/AcceptInvitationReq"
responses:
"200":
description: Invitation accepted.
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
/invitations/{user_id}/{domain_id}:
get:
summary: Retrieves a specific invitation
description: |
Retrieves a specific invitation that is identifier by the user ID and domain ID.
tags:
- Invitations
parameters:
- $ref: "#/components/parameters/user_id"
- $ref: "#/components/parameters/domain_id"
security:
- bearerAuth: []
responses:
"200":
$ref: "#/components/responses/InvitationRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
summary: Deletes a specific invitation
description: |
Deletes a specific invitation that is identifier by the user ID and domain ID.
tags:
- Invitations
parameters:
- $ref: "#/components/parameters/user_id"
- $ref: "#/components/parameters/domain_id"
security:
- bearerAuth: []
responses:
"204":
description: Invitation deleted.
"400":
description: Failed due to malformed JSON.
"404":
description: Failed due to non existing user.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
SendInvitationReqObj:
type: object
properties:
user_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: User unique identifier.
domain_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Domain unique identifier.
relation:
type: string
enum:
- administrator
- editor
- viewer
- member
- domain
- parent_group
- role_group
- group
- platform
example: editor
description: Relation between user and domain.
resend:
type: boolean
example: true
description: Resend invitation.
required:
- user_id
- domain_id
- relation
Invitation:
type: object
properties:
invited_by:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: User unique identifier.
user_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: User unique identifier.
domain_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Domain unique identifier.
relation:
type: string
enum:
- administrator
- editor
- viewer
- member
- domain
- parent_group
- role_group
- group
- platform
example: editor
description: Relation between user and domain.
created_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the group was created.
updated_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the group was created.
confirmed_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the group was created.
xml:
name: invitation
InvitationPage:
type: object
properties:
invitations:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/components/schemas/Invitation"
total:
type: integer
example: 1
description: Total number of items.
offset:
type: integer
description: Number of items to skip during retrieval.
limit:
type: integer
example: 10
description: Maximum number of items to return in one page.
required:
- invitations
- total
- offset
Error:
type: object
properties:
error:
type: string
description: Error message
example: { "error": "malformed entity specification" }
HealthRes:
type: object
properties:
status:
type: string
description: Service status.
enum:
- pass
version:
type: string
description: Service version.
example: 0.14.0
commit:
type: string
description: Service commit hash.
example: 7d6f4dc4f7f0c1fa3dc24eddfb18bb5073ff4f62
description:
type: string
description: Service description.
example: <service_name> service
build_time:
type: string
description: Service build time.
example: 1970-01-01_00:00:00
parameters:
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
example: "0"
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 10
minimum: 1
required: false
example: "10"
UserID:
name: user_id
description: Unique user identifier.
in: query
schema:
type: string
format: uuid
required: true
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
user_id:
name: user_id
description: Unique user identifier.
in: path
schema:
type: string
format: uuid
required: true
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
DomainID:
name: domain_id
description: Unique identifier for a domain.
in: query
schema:
type: string
format: uuid
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
domain_id:
name: domain_id
description: Unique identifier for a domain.
in: path
schema:
type: string
format: uuid
required: true
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
InvitedBy:
name: invited_by
description: Unique identifier for a user that invited the user.
in: query
schema:
type: string
format: uuid
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
Relation:
name: relation
description: Relation between user and domain.
in: query
schema:
type: string
enum:
- administrator
- editor
- viewer
- member
- domain
- parent_group
- role_group
- group
- platform
required: false
example: editor
requestBodies:
SendInvitationReq:
description: JSON-formatted document describing request for sending invitation
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SendInvitationReqObj"
AcceptInvitationReq:
description: JSON-formatted document describing request for accepting invitation
required: true
content:
application/json:
schema:
type: object
properties:
domain_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Domain unique identifier.
required:
- domain_id
responses:
InvitationRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Invitation"
InvitationPageRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/InvitationPage"
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "#/components/schemas/HealthRes"
ServiceError:
description: Unexpected server-side error occurred.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* User access: "Authorization: Bearer <user_access_token>"
security:
- bearerAuth: []
+128
View File
@@ -0,0 +1,128 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
mgxsdk "github.com/absmach/magistrala/pkg/sdk/go"
"github.com/spf13/cobra"
)
var cmdInvitations = []cobra.Command{
{
Use: "send <user_id> <domain_id> <relation> <user_auth_token>",
Short: "Send invitation",
Long: "Send invitation to user\n" +
"For example:\n" +
"\tmagistrala-cli invitations send 39f97daf-d6b6-40f4-b229-2697be8006ef 4ef09eff-d500-4d56-b04f-d23a512d6f2a administrator $USER_AUTH_TOKEN\n",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 4 {
logUsage(cmd.Use)
return
}
inv := mgxsdk.Invitation{
UserID: args[0],
DomainID: args[1],
Relation: args[2],
}
if err := sdk.SendInvitation(inv, args[3]); err != nil {
logError(err)
return
}
logOK()
},
},
{
Use: "get [all | <user_id> <domain_id> ] <user_auth_token>",
Short: "Get invitations",
Long: "Get invitations\n" +
"Usage:\n" +
"\tmagistrala-cli invitations get all <user_auth_token> - lists all invitations\n" +
"\tmagistrala-cli invitations get all <user_auth_token> --offset <offset> --limit <limit> - lists all invitations with provided offset and limit\n" +
"\tmagistrala-cli invitations get <user_id> <domain_id> <user_auth_token> - shows invitation by user id and domain id\n",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 && len(args) != 3 {
logUsage(cmd.Use)
return
}
pageMetadata := mgxsdk.PageMetadata{
Identity: Identity,
Offset: Offset,
Limit: Limit,
}
if args[0] == all {
l, err := sdk.Invitations(pageMetadata, args[1])
if err != nil {
logError(err)
return
}
logJSON(l)
return
}
u, err := sdk.Invitation(args[0], args[1], args[2])
if err != nil {
logError(err)
return
}
logJSON(u)
},
},
{
Use: "accept <domain_id> <user_auth_token>",
Short: "Accept invitation",
Long: "Accept invitation to domain\n" +
"Usage:\n" +
"\tmagistrala-cli invitations accept 39f97daf-d6b6-40f4-b229-2697be8006ef $USERTOKEN\n",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Use)
return
}
if err := sdk.AcceptInvitation(args[0], args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
{
Use: "delete <user_id> <domain_id> <user_auth_token>",
Short: "Delete invitation",
Long: "Delete invitation\n" +
"Usage:\n" +
"\tmagistrala-cli invitations delete 39f97daf-d6b6-40f4-b229-2697be8006ef 4ef09eff-d500-4d56-b04f-d23a512d6f2a $USERTOKEN\n",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Use)
return
}
if err := sdk.DeleteInvitation(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
}
// NewInvitationsCmd returns invitations command.
func NewInvitationsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "invitations [send | get | accept | delete]",
Short: "Invitations management",
Long: `Invitations management to send, get, accept and delete invitations`,
}
for i := range cmdInvitations {
cmd.AddCommand(&cmdInvitations[i])
}
return &cmd
}
+18 -6
View File
@@ -14,12 +14,13 @@ import (
)
const (
defURL string = "http://localhost"
defUsersURL string = defURL + ":9002"
defThingsURL string = defURL + ":9000"
defBootstrapURL string = defURL + ":9013"
defDomainsURL string = defURL + ":8189"
defCertsURL string = defURL + ":9019"
defURL string = "http://localhost"
defUsersURL string = defURL + ":9002"
defThingsURL string = defURL + ":9000"
defBootstrapURL string = defURL + ":9013"
defDomainsURL string = defURL + ":8189"
defCertsURL string = defURL + ":9019"
defInvitationsURL string = defURL + ":9020"
)
func main() {
@@ -32,6 +33,7 @@ func main() {
BootstrapURL: defBootstrapURL,
CertsURL: defCertsURL,
DomainsURL: defDomainsURL,
InvitationsURL: defInvitationsURL,
MsgContentType: sdk.ContentType(msgContentType),
TLSVerification: false,
HostURL: defURL,
@@ -65,6 +67,7 @@ func main() {
certsCmd := cli.NewCertsCmd()
subscriptionsCmd := cli.NewSubscriptionCmd()
configCmd := cli.NewConfigCmd()
invitationsCmd := cli.NewInvitationsCmd()
// Root Commands
rootCmd.AddCommand(healthCmd)
@@ -79,6 +82,7 @@ func main() {
rootCmd.AddCommand(certsCmd)
rootCmd.AddCommand(subscriptionsCmd)
rootCmd.AddCommand(configCmd)
rootCmd.AddCommand(invitationsCmd)
// Root Flags
rootCmd.PersistentFlags().StringVarP(
@@ -137,6 +141,14 @@ func main() {
"Reader URL",
)
rootCmd.PersistentFlags().StringVarP(
&sdkConf.InvitationsURL,
"invitations-url",
"v",
sdkConf.InvitationsURL,
"Inivitations URL",
)
rootCmd.PersistentFlags().StringVarP(
&sdkConf.HostURL,
"host-url",
+172
View File
@@ -0,0 +1,172 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package main contains invitations main function to start the invitations service.
package main
import (
"context"
"fmt"
"log"
"net/url"
"os"
"github.com/absmach/magistrala"
"github.com/absmach/magistrala/internal"
"github.com/absmach/magistrala/internal/clients/jaeger"
clientspg "github.com/absmach/magistrala/internal/clients/postgres"
"github.com/absmach/magistrala/internal/postgres"
"github.com/absmach/magistrala/internal/server"
"github.com/absmach/magistrala/internal/server/http"
"github.com/absmach/magistrala/invitations"
"github.com/absmach/magistrala/invitations/api"
"github.com/absmach/magistrala/invitations/middleware"
invitationspg "github.com/absmach/magistrala/invitations/postgres"
mglog "github.com/absmach/magistrala/logger"
"github.com/absmach/magistrala/pkg/auth"
mgsdk "github.com/absmach/magistrala/pkg/sdk/go"
"github.com/absmach/magistrala/pkg/uuid"
"github.com/caarlos0/env/v10"
"github.com/jmoiron/sqlx"
chclient "github.com/mainflux/callhome/pkg/client"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
)
const (
svcName = "invitations"
envPrefixDB = "MG_INVITATIONS_DB_"
envPrefixHTTP = "MG_INVITATIONS_HTTP_"
envPrefixAuth = "MG_AUTH_GRPC_"
defDB = "invitations"
defSvcHTTPPort = "9020"
)
type config struct {
LogLevel string `env:"MG_INVITATIONS_LOG_LEVEL" envDefault:"info"`
UsersURL string `env:"MG_USERS_URL" envDefault:"http://localhost:9002"`
DomainsURL string `env:"MG_DOMAINS_URL" envDefault:"http://localhost:8189"`
InstanceID string `env:"MG_INVITATIONS_INSTANCE_ID" envDefault:""`
JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:14268/api/traces"`
TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"`
SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"`
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
g, ctx := errgroup.WithContext(ctx)
cfg := config{}
if err := env.Parse(&cfg); err != nil {
log.Fatalf("failed to load %s configuration : %s", svcName, err)
}
logger, err := mglog.New(os.Stdout, cfg.LogLevel)
if err != nil {
log.Fatalf("failed to init logger: %s", err)
}
var exitCode int
defer mglog.ExitWithError(&exitCode)
if cfg.InstanceID == "" {
if cfg.InstanceID, err = uuid.New().ID(); err != nil {
logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err))
exitCode = 1
return
}
}
dbConfig := clientspg.Config{Name: defDB}
if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil {
logger.Error(fmt.Sprintf("failed to load %s database configuration : %s", svcName, err))
exitCode = 1
return
}
db, err := clientspg.Setup(dbConfig, *invitationspg.Migration())
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
defer db.Close()
authConfig := auth.Config{}
if err := env.ParseWithOptions(&authConfig, env.Options{Prefix: envPrefixAuth}); err != nil {
logger.Error(fmt.Sprintf("failed to load auth configuration : %s", err.Error()))
exitCode = 1
return
}
authClient, authHandler, err := auth.Setup(authConfig)
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
defer authHandler.Close()
logger.Info("Successfully connected to auth grpc server " + authHandler.Secure())
tp, err := jaeger.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio)
if err != nil {
logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err))
exitCode = 1
return
}
defer func() {
if err := tp.Shutdown(ctx); err != nil {
logger.Error(fmt.Sprintf("error shutting down tracer provider: %v", err))
}
}()
tracer := tp.Tracer(svcName)
svc, err := newService(db, dbConfig, authClient, tracer, cfg, logger)
if err != nil {
logger.Error(fmt.Sprintf("failed to create %s service: %s", svcName, err))
exitCode = 1
return
}
httpServerConfig := server.Config{Port: defSvcHTTPPort}
if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil {
logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err))
exitCode = 1
return
}
httpSvr := http.New(ctx, cancel, svcName, httpServerConfig, api.MakeHandler(svc, logger, cfg.InstanceID), logger)
if cfg.SendTelemetry {
chc := chclient.New(svcName, magistrala.Version, logger, cancel)
go chc.CallHome(ctx)
}
g.Go(func() error {
return httpSvr.Start()
})
g.Go(func() error {
return server.StopSignalHandler(ctx, cancel, logger, svcName, httpSvr)
})
if err := g.Wait(); err != nil {
logger.Error(fmt.Sprintf("%s service terminated: %s", svcName, err))
}
}
func newService(db *sqlx.DB, dbConfig clientspg.Config, authClient magistrala.AuthServiceClient, tracer trace.Tracer, conf config, logger mglog.Logger) (invitations.Service, error) {
database := postgres.NewDatabase(db, dbConfig, tracer)
repo := invitationspg.NewRepository(database)
config := mgsdk.Config{
UsersURL: conf.UsersURL,
DomainsURL: conf.DomainsURL,
}
sdk := mgsdk.NewSDK(config)
svc := invitations.NewService(repo, authClient, sdk)
svc = middleware.Tracing(svc, tracer)
svc = middleware.Logging(logger, svc)
counter, latency := internal.MakeMetrics(svcName, "api")
svc = middleware.Metrics(counter, latency, svc)
return svc, nil
}
+21
View File
@@ -105,6 +105,9 @@ MG_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}
MG_AUTH_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key}
MG_AUTH_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
#### Domains Client Config
MG_DOMAINS_URL=http://auth:8189
### SpiceDB Datastore config
MG_SPICEDB_DB_USER=magistrala
MG_SPICEDB_DB_PASS=magistrala
@@ -118,6 +121,24 @@ MG_SPICEDB_HOST=magistrala-spicedb
MG_SPICEDB_PORT=50051
MG_SPICEDB_DATASTORE_ENGINE=postgres
### Invitations
MG_INVITATIONS_LOG_LEVEL=info
MG_INVITATIONS_HTTP_HOST=invitations
MG_INVITATIONS_HTTP_PORT=9020
MG_INVITATIONS_HTTP_SERVER_CERT=
MG_INVITATIONS_HTTP_SERVER_KEY=
MG_INVITATIONS_DB_HOST=invitations-db
MG_INVITATIONS_DB_PORT=5432
MG_INVITATIONS_DB_USER=magistrala
MG_INVITATIONS_DB_PASS=magistrala
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=
MG_INVITATIONS_INSTANCE_ID=
### Users
MG_USERS_LOG_LEVEL=debug
MG_USERS_SECRET_KEY=HyE2D4RUt9nnKG6v8zKEqAp6g6ka8hhZsqUpzgKvnwpXrNVQSH
+73
View File
@@ -17,6 +17,7 @@ volumes:
magistrala-es-volume:
magistrala-spicedb-db-volume:
magistrala-auth-db-volume:
magistrala-invitations-db-volume:
include:
- path: brokers/docker-compose.yml
@@ -158,6 +159,78 @@ services:
target: /auth-grpc-client-ca${MG_AUTH_GRPC_CLIENT_CA_CERTS:+.crt}
bind:
create_host_path: true
invitations-db:
image: postgres:15.3-alpine
container_name: magistrala-invitations-db
restart: on-failure
command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}"
environment:
POSTGRES_USER: ${MG_INVITATIONS_DB_USER}
POSTGRES_PASSWORD: ${MG_INVITATIONS_DB_PASS}
POSTGRES_DB: ${MG_INVITATIONS_DB_NAME}
MG_POSTGRES_MAX_CONNECTIONS: ${MG_POSTGRES_MAX_CONNECTIONS}
ports:
- 6021:5432
networks:
- magistrala-base-net
volumes:
- magistrala-invitations-db-volume:/var/lib/postgresql/data
invitations:
image: magistrala/invitations:${MG_RELEASE_TAG}
container_name: magistrala-invitations
restart: on-failure
depends_on:
- invitations-db
environment:
MG_INVITATIONS_LOG_LEVEL: ${MG_INVITATIONS_LOG_LEVEL}
MG_USERS_URL: ${MG_USERS_URL}
MG_DOMAINS_URL: ${MG_DOMAINS_URL}
MG_INVITATIONS_HTTP_HOST: ${MG_INVITATIONS_HTTP_HOST}
MG_INVITATIONS_HTTP_PORT: ${MG_INVITATIONS_HTTP_PORT}
MG_INVITATIONS_HTTP_SERVER_CERT: ${MG_INVITATIONS_HTTP_SERVER_CERT}
MG_INVITATIONS_HTTP_SERVER_KEY: ${MG_INVITATIONS_HTTP_SERVER_KEY}
MG_INVITATIONS_DB_HOST: ${MG_INVITATIONS_DB_HOST}
MG_INVITATIONS_DB_USER: ${MG_INVITATIONS_DB_USER}
MG_INVITATIONS_DB_PASS: ${MG_INVITATIONS_DB_PASS}
MG_INVITATIONS_DB_PORT: ${MG_INVITATIONS_DB_PORT}
MG_INVITATIONS_DB_NAME: ${MG_INVITATIONS_DB_NAME}
MG_INVITATIONS_DB_SSL_MODE: ${MG_INVITATIONS_DB_SSL_MODE}
MG_INVITATIONS_DB_SSL_CERT: ${MG_INVITATIONS_DB_SSL_CERT}
MG_INVITATIONS_DB_SSL_KEY: ${MG_INVITATIONS_DB_SSL_KEY}
MG_INVITATIONS_DB_SSL_ROOT_CERT: ${MG_INVITATIONS_DB_SSL_ROOT_CERT}
MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL}
MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT}
MG_AUTH_GRPC_CLIENT_CERT: ${MG_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt}
MG_AUTH_GRPC_CLIENT_KEY: ${MG_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key}
MG_AUTH_GRPC_SERVER_CA_CERTS: ${MG_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt}
MG_JAEGER_URL: ${MG_JAEGER_URL}
MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO}
MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY}
MG_INVITATIONS_INSTANCE_ID: ${MG_INVITATIONS_INSTANCE_ID}
ports:
- ${MG_INVITATIONS_HTTP_PORT}:${MG_INVITATIONS_HTTP_PORT}
networks:
- magistrala-base-net
volumes:
# Auth gRPC client certificates
- type: bind
source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt}
bind:
create_host_path: true
- type: bind
source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key}
bind:
create_host_path: true
- type: bind
source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
bind:
create_host_path: true
nginx:
image: nginx:1.23.3-alpine
container_name: magistrala-nginx
+1
View File
@@ -19,6 +19,7 @@ envsubst '
${MG_HTTP_ADAPTER_PORT}
${MG_NGINX_MQTT_PORT}
${MG_NGINX_MQTTS_PORT}
${MG_INVITATIONS_HTTP_PORT}
${MG_WS_ADAPTER_HTTP_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
exec nginx -g "daemon off;"
+7
View File
@@ -139,6 +139,13 @@ http {
proxy_pass http://things:${MG_THINGS_HTTP_PORT}/policies;
}
# Proxy pass to invitations service
location ~ ^/(invitations) {
include snippets/proxy-headers.conf;
add_header Access-Control-Expose-Headers Location;
proxy_pass http://invitations:${MG_INVITATIONS_HTTP_PORT};
}
location /health {
include snippets/proxy-headers.conf;
proxy_pass http://things:${MG_THINGS_HTTP_PORT};
+7
View File
@@ -84,6 +84,13 @@ http {
proxy_pass http://things:${MG_THINGS_HTTP_PORT}/policies;
}
# Proxy pass to invitations service
location ~ ^/(invitations) {
include snippets/proxy-headers.conf;
add_header Access-Control-Expose-Headers Location;
proxy_pass http://invitations:${MG_INVITATIONS_HTTP_PORT};
}
location /health {
include snippets/proxy-headers.conf;
proxy_pass http://things:${MG_THINGS_HTTP_PORT};
+30 -28
View File
@@ -33,17 +33,17 @@ func (svc *service) SendInvitation(ctx context.Context, token string, invitation
return err
}
userID, err := svc.identify(ctx, token)
user, err := svc.identify(ctx, token)
if err != nil {
return err
}
invitation.InvitedBy = userID
invitation.InvitedBy = user.GetUserId()
if err := svc.checkAdmin(ctx, userID, invitation.DomainID); err != nil {
if err := svc.checkAdmin(ctx, user.GetId(), invitation.DomainID); err != nil {
return err
}
joinToken, err := svc.auth.Issue(ctx, &magistrala.IssueReq{UserId: userID, DomainId: &invitation.DomainID, Type: uint32(auth.InvitationKey)})
joinToken, err := svc.auth.Issue(ctx, &magistrala.IssueReq{UserId: user.GetUserId(), DomainId: &invitation.DomainID, Type: uint32(auth.InvitationKey)})
if err != nil {
return err
}
@@ -61,7 +61,7 @@ func (svc *service) SendInvitation(ctx context.Context, token string, invitation
}
func (svc *service) ViewInvitation(ctx context.Context, token, userID, domainID string) (invitation Invitation, err error) {
tokenUserID, err := svc.identify(ctx, token)
user, err := svc.identify(ctx, token)
if err != nil {
return Invitation{}, err
}
@@ -71,15 +71,15 @@ func (svc *service) ViewInvitation(ctx context.Context, token, userID, domainID
}
inv.Token = ""
if tokenUserID == userID {
if user.GetUserId() == userID {
return inv, nil
}
if inv.InvitedBy == tokenUserID {
if inv.InvitedBy == user.GetUserId() {
return inv, nil
}
if err := svc.checkAdmin(ctx, tokenUserID, domainID); err != nil {
if err := svc.checkAdmin(ctx, user.GetId(), domainID); err != nil {
return Invitation{}, err
}
@@ -87,49 +87,50 @@ func (svc *service) ViewInvitation(ctx context.Context, token, userID, domainID
}
func (svc *service) ListInvitations(ctx context.Context, token string, page Page) (invitations InvitationPage, err error) {
userID, err := svc.identify(ctx, token)
user, err := svc.identify(ctx, token)
if err != nil {
return InvitationPage{}, err
}
if err := svc.authorize(ctx, auth.UserType, auth.UsersKind, userID, auth.AdminPermission, auth.PlatformType, auth.MagistralaObject); err == nil {
if err := svc.authorize(ctx, user.GetId(), auth.AdminPermission, auth.PlatformType, auth.MagistralaObject); err == nil {
return svc.repo.RetrieveAll(ctx, page)
}
if page.DomainID != "" {
if err := svc.checkAdmin(ctx, userID, page.DomainID); err != nil {
if err := svc.checkAdmin(ctx, user.GetId(), page.DomainID); err != nil {
return InvitationPage{}, err
}
return svc.repo.RetrieveAll(ctx, page)
}
page.InvitedByOrUserID = userID
page.InvitedByOrUserID = user.GetUserId()
return svc.repo.RetrieveAll(ctx, page)
}
func (svc *service) AcceptInvitation(ctx context.Context, token, domainID string) error {
userID, err := svc.identify(ctx, token)
user, err := svc.identify(ctx, token)
if err != nil {
return err
}
inv, err := svc.repo.Retrieve(ctx, userID, domainID)
inv, err := svc.repo.Retrieve(ctx, user.GetUserId(), domainID)
if err != nil {
return err
}
if inv.UserID == userID && inv.ConfirmedAt.IsZero() {
if inv.UserID == user.GetUserId() && inv.ConfirmedAt.IsZero() {
req := mgsdk.UsersRelationRequest{
Relation: inv.Relation,
UserIDs: []string{userID},
UserIDs: []string{user.GetUserId()},
}
if sdkerr := svc.sdk.AddUserToDomain(inv.DomainID, req, inv.Token); sdkerr != nil {
return sdkerr
}
inv.ConfirmedAt = time.Now()
inv.UpdatedAt = time.Now()
if err := svc.repo.UpdateConfirmation(ctx, inv); err != nil {
return err
}
@@ -139,11 +140,11 @@ func (svc *service) AcceptInvitation(ctx context.Context, token, domainID string
}
func (svc *service) DeleteInvitation(ctx context.Context, token, userID, domainID string) error {
tokenUserID, err := svc.identify(ctx, token)
user, err := svc.identify(ctx, token)
if err != nil {
return err
}
if tokenUserID == userID {
if user.GetUserId() == userID {
return svc.repo.Delete(ctx, userID, domainID)
}
@@ -152,30 +153,30 @@ func (svc *service) DeleteInvitation(ctx context.Context, token, userID, domainI
return err
}
if inv.InvitedBy == tokenUserID {
if inv.InvitedBy == user.GetUserId() {
return svc.repo.Delete(ctx, userID, domainID)
}
if err := svc.checkAdmin(ctx, tokenUserID, domainID); err != nil {
if err := svc.checkAdmin(ctx, user.GetId(), domainID); err != nil {
return err
}
return svc.repo.Delete(ctx, userID, domainID)
}
func (svc *service) identify(ctx context.Context, token string) (string, error) {
func (svc *service) identify(ctx context.Context, token string) (*magistrala.IdentityRes, error) {
user, err := svc.auth.Identify(ctx, &magistrala.IdentityReq{Token: token})
if err != nil {
return "", err
return &magistrala.IdentityRes{}, err
}
return user.GetUserId(), nil
return user, nil
}
func (svc *service) authorize(ctx context.Context, subjType, subjKind, subj, perm, objType, obj string) error {
func (svc *service) authorize(ctx context.Context, subj, perm, objType, obj string) error {
req := &magistrala.AuthorizeReq{
SubjectType: subjType,
SubjectKind: subjKind,
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: subj,
Permission: perm,
ObjectType: objType,
@@ -195,10 +196,11 @@ func (svc *service) authorize(ctx context.Context, subjType, subjKind, subj, per
// checkAdmin checks if the given user is a domain or platform administrator.
func (svc *service) checkAdmin(ctx context.Context, userID, domainID string) error {
if err := svc.authorize(ctx, auth.UserType, auth.UsersKind, userID, auth.AdminPermission, auth.DomainType, domainID); err == nil {
if err := svc.authorize(ctx, userID, auth.AdminPermission, auth.DomainType, domainID); err == nil {
return nil
}
if err := svc.authorize(ctx, auth.UserType, auth.UsersKind, userID, auth.AdminPermission, auth.PlatformType, auth.MagistralaObject); err == nil {
if err := svc.authorize(ctx, userID, auth.AdminPermission, auth.PlatformType, auth.MagistralaObject); err == nil {
return nil
}
+28 -12
View File
@@ -147,11 +147,15 @@ func TestSendInvitation(t *testing.T) {
}
for _, tc := range cases {
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(&magistrala.IdentityRes{UserId: tc.tokenUserID}, tc.authNErr)
idRes := &magistrala.IdentityRes{
UserId: tc.tokenUserID,
Id: testsutil.GenerateUUID(t) + "_" + tc.tokenUserID,
}
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(idRes, tc.authNErr)
domainReq := magistrala.AuthorizeReq{
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: tc.tokenUserID,
Subject: idRes.GetId(),
Permission: auth.AdminPermission,
ObjectType: auth.DomainType,
Object: tc.req.DomainID,
@@ -160,7 +164,7 @@ func TestSendInvitation(t *testing.T) {
platformReq := magistrala.AuthorizeReq{
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: tc.tokenUserID,
Subject: idRes.GetId(),
Permission: auth.AdminPermission,
ObjectType: auth.PlatformType,
Object: auth.MagistralaObject,
@@ -324,11 +328,15 @@ func TestViewInvitation(t *testing.T) {
}
for _, tc := range cases {
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(&magistrala.IdentityRes{UserId: tc.tokenUserID}, tc.authNErr)
idRes := &magistrala.IdentityRes{
UserId: tc.tokenUserID,
Id: testsutil.GenerateUUID(t) + "_" + tc.tokenUserID,
}
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(idRes, tc.authNErr)
domainReq := magistrala.AuthorizeReq{
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: tc.tokenUserID,
Subject: idRes.GetId(),
Permission: auth.AdminPermission,
ObjectType: auth.DomainType,
Object: tc.domainID,
@@ -337,7 +345,7 @@ func TestViewInvitation(t *testing.T) {
platformReq := magistrala.AuthorizeReq{
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: tc.tokenUserID,
Subject: idRes.GetId(),
Permission: auth.AdminPermission,
ObjectType: auth.PlatformType,
Object: auth.MagistralaObject,
@@ -498,11 +506,15 @@ func TestListInvitations(t *testing.T) {
}
for _, tc := range cases {
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(&magistrala.IdentityRes{UserId: tc.tokenUserID}, tc.authNErr)
idRes := &magistrala.IdentityRes{
UserId: tc.tokenUserID,
Id: testsutil.GenerateUUID(t) + "_" + tc.tokenUserID,
}
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(idRes, tc.authNErr)
domainReq := magistrala.AuthorizeReq{
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: tc.tokenUserID,
Subject: idRes.GetId(),
Permission: auth.AdminPermission,
ObjectType: auth.DomainType,
Object: tc.page.DomainID,
@@ -511,7 +523,7 @@ func TestListInvitations(t *testing.T) {
platformReq := magistrala.AuthorizeReq{
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: tc.tokenUserID,
Subject: idRes.GetId(),
Permission: auth.AdminPermission,
ObjectType: auth.PlatformType,
Object: auth.MagistralaObject,
@@ -720,11 +732,15 @@ func TestDeleteInvitation(t *testing.T) {
}
for _, tc := range cases {
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(&magistrala.IdentityRes{UserId: tc.tokenUserID}, tc.authNErr)
idRes := &magistrala.IdentityRes{
UserId: tc.tokenUserID,
Id: tc.domainID + "_" + tc.userID,
}
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(idRes, tc.authNErr)
domainReq := magistrala.AuthorizeReq{
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: tc.tokenUserID,
Subject: idRes.GetId(),
Permission: auth.AdminPermission,
ObjectType: auth.DomainType,
Object: tc.domainID,
@@ -733,7 +749,7 @@ func TestDeleteInvitation(t *testing.T) {
platformReq := magistrala.AuthorizeReq{
SubjectType: auth.UserType,
SubjectKind: auth.UsersKind,
Subject: tc.tokenUserID,
Subject: idRes.GetId(),
Permission: auth.AdminPermission,
ObjectType: auth.PlatformType,
Object: auth.MagistralaObject,
+109
View File
@@ -0,0 +1,109 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package sdk
import (
"encoding/json"
"net/http"
"time"
"github.com/absmach/magistrala/pkg/errors"
)
const (
invitationsEndpoint = "invitations"
acceptEndpoint = "accept"
)
type Invitation struct {
InvitedBy string `json:"invited_by"`
UserID string `json:"user_id"`
DomainID string `json:"domain_id"`
Token string `json:"token,omitempty"`
Relation string `json:"relation,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
ConfirmedAt time.Time `json:"confirmed_at,omitempty"`
Resend bool `json:"resend,omitempty"`
}
type InvitationPage struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Invitations []Invitation `json:"invitations"`
}
func (sdk mgSDK) SendInvitation(invitation Invitation, token string) (err error) {
data, err := json.Marshal(invitation)
if err != nil {
return errors.NewSDKError(err)
}
url := sdk.invitationsURL + "/" + invitationsEndpoint
_, _, sdkerr := sdk.processRequest(http.MethodPost, url, token, data, nil, http.StatusCreated)
return sdkerr
}
func (sdk mgSDK) Invitation(userID, domainID, token string) (invitation Invitation, err error) {
url := sdk.invitationsURL + "/" + invitationsEndpoint + "/" + userID + "/" + domainID
_, body, sdkerr := sdk.processRequest(http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return Invitation{}, sdkerr
}
if err := json.Unmarshal(body, &invitation); err != nil {
return Invitation{}, errors.NewSDKError(err)
}
return invitation, nil
}
func (sdk mgSDK) Invitations(pm PageMetadata, token string) (invitations InvitationPage, err error) {
url, err := sdk.withQueryParams(sdk.invitationsURL, invitationsEndpoint, pm)
if err != nil {
return InvitationPage{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return InvitationPage{}, sdkerr
}
var invPage InvitationPage
if err := json.Unmarshal(body, &invPage); err != nil {
return InvitationPage{}, errors.NewSDKError(err)
}
return invPage, nil
}
func (sdk mgSDK) AcceptInvitation(domainID, token string) (err error) {
req := struct {
DomainID string `json:"domain_id"`
}{
DomainID: domainID,
}
data, err := json.Marshal(req)
if err != nil {
return errors.NewSDKError(err)
}
url := sdk.invitationsURL + "/" + invitationsEndpoint + "/" + acceptEndpoint
_, _, sdkerr := sdk.processRequest(http.MethodPost, url, token, data, nil, http.StatusOK)
return sdkerr
}
func (sdk mgSDK) DeleteInvitation(userID, domainID, token string) (err error) {
url := sdk.invitationsURL + "/" + invitationsEndpoint + "/" + userID + "/" + domainID
_, _, sdkerr := sdk.processRequest(http.MethodDelete, url, token, nil, nil, http.StatusNoContent)
return sdkerr
}
+60
View File
@@ -93,6 +93,10 @@ type PageMetadata struct {
State string `json:"state,omitempty"`
Order string `json:"order,omitempty"`
ListPermissions string `json:"list_perms,omitempty"`
InvitedBy string `json:"invited_by,omitempty"`
UserID string `json:"user_id,omitempty"`
DomainID string `json:"domain_id,omitempty"`
Relation string `json:"relation,omitempty"`
}
// Credentials represent client credentials: it contains
@@ -1086,6 +1090,46 @@ type SDK interface {
// err := sdk.RemoveUserFromDomain("domainID", req, "token")
// fmt.Println(err)
RemoveUserFromDomain(domainID string, req UsersRelationRequest, token string) errors.SDKError
// SendInvitation sends an invitation to the email address associated with the given user.
//
// For example:
// invitation := sdk.Invitation{
// DomainID: "domainID",
// UserID: "userID",
// Relation: "viewer", // available options: "owner", "admin", "editor", "viewer"
// }
// err := sdk.SendInvitation(invitation, "token")
// fmt.Println(err)
SendInvitation(invitation Invitation, token string) (err error)
// Invitation returns an invitation.
//
// For example:
// invitation, _ := sdk.Invitation("userID", "domainID", "token")
// fmt.Println(invitation)
Invitation(userID, domainID, token string) (invitation Invitation, err error)
// Invitations returns a list of invitations.
//
// For example:
// invitations, _ := sdk.Invitations(PageMetadata{Offset: 0, Limit: 10, Domain: "domainID"}, "token")
// fmt.Println(invitations)
Invitations(pm PageMetadata, token string) (invitations InvitationPage, err error)
// AcceptInvitation accepts an invitation by adding the user to the domain that they were invited to.
//
// For example:
// err := sdk.AcceptInvitation("domainID", "token")
// fmt.Println(err)
AcceptInvitation(domainID, token string) (err error)
// DeleteInvitation deletes an invitation.
//
// For example:
// err := sdk.DeleteInvitation("userID", "domainID", "token")
// fmt.Println(err)
DeleteInvitation(userID, domainID, token string) (err error)
}
type mgSDK struct {
@@ -1096,6 +1140,7 @@ type mgSDK struct {
thingsURL string
usersURL string
domainsURL string
invitationsURL string
HostURL string
msgContentType ContentType
@@ -1111,6 +1156,7 @@ type Config struct {
ThingsURL string
UsersURL string
DomainsURL string
InvitationsURL string
HostURL string
MsgContentType ContentType
@@ -1127,6 +1173,7 @@ func NewSDK(conf Config) SDK {
thingsURL: conf.ThingsURL,
usersURL: conf.UsersURL,
domainsURL: conf.DomainsURL,
invitationsURL: conf.InvitationsURL,
HostURL: conf.HostURL,
msgContentType: conf.MsgContentType,
@@ -1260,5 +1307,18 @@ func (pm PageMetadata) query() (string, error) {
if pm.ListPermissions != "" {
q.Add("list_perms", pm.ListPermissions)
}
if pm.InvitedBy != "" {
q.Add("invited_by", pm.InvitedBy)
}
if pm.UserID != "" {
q.Add("user_id", pm.UserID)
}
if pm.DomainID != "" {
q.Add("domain_id", pm.DomainID)
}
if pm.Relation != "" {
q.Add("relation", pm.Relation)
}
return q.Encode(), nil
}