mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:40:19 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;"
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user