NOISSUE - Add user groups (#1228)

* adding group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding user group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add retrieve methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add default admin user

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add default admin user

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding endpoints

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding endpoints

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* changes signature for AssignUser

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding tests

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* bug fixing retrieving groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove unused code

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* bug fixing retrieving groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* retrieve groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change environment for admin

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change environment for admin

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* retrieve groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove adding default group

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* expose port for debugging purposes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix tests, and linter errors

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add prefix Users for groups endpoint

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix linter problems

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix endpoint prefix url

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix endpoint test

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add group features in cli

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove println

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* when user is created return id in response

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* when user is created return id in response

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* adding default admin env

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* proper alignment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* proper alignment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename  method

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* return user id when created

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* return user id when created

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove unused variable

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix to retrieve whole tree starting from parent

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add endpoint to list groups for user

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add readme for groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fixing bugs

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fixing bugs

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add group commands for add and remove user

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* replace default email, use example.com

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix capital letters beginning of sentence

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove warning for deprecated api, mistakenly copied

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* simplify repo methods, rely on db driver rather than the check before operation

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* check if group is valid

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* openapi spec 3.0

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove check for existing users in groups before delete

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change func signature

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change func signature

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix bugs, resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix bugs, resolve comments

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix alignment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add missing command

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* reorganize envs

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix doc

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix compile

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* reorganize cli commands

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* minor corrections

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename methods

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix naming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* renaming

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* resolve comments, minor changes

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
This commit is contained in:
Mirko Teodorovic
2020-09-23 23:18:53 +02:00
committed by GitHub
parent 043d1e0aac
commit 8ea26c5ab7
45 changed files with 3826 additions and 356 deletions
+2
View File
@@ -43,6 +43,8 @@ MF_USERS_DB_PORT=5432
MF_USERS_DB_USER=mainflux
MF_USERS_DB_PASS=mainflux
MF_USERS_DB=users
MF_USERS_ADMIN_EMAIL=admin@example.com
MF_USERS_ADMIN_PASSWORD=12345678
### Email utility
MF_EMAIL_DRIVER=smtp
+38
View File
@@ -202,3 +202,41 @@ mainflux-cli bootstrap remove <thing_id> <user_auth_token>
```bash
mainflux-cli bootstrap bootstrap <external_id> <external_key>
```
### Groups
#### Create new group
```bash
mainflux-cli groups create '{"name":"<group_name>","parent_id":"<parent_group_id>","description":"<description>","metadata":{"key":"value",...}}' <user_auth_token>
```
#### Delete group
```bash
mainflux-cli groups delete <group_id> <user_auth_token>
```
#### Get group with id
```bash
mainflux-cli groups get <group_id> <user_auth_token>
```
#### List all groups
```bash
mainflux-cli groups get all <user_auth_token>
```
#### List children groups for some group
```bash
mainflux-cli groups get children <parent_group_id> <user_auth_token>
```
#### Assign user to a group
```bash
mainflux-cli groups assign <user_id> <group_id> <user_auth_token>
```
#### Unassign user from group
```bash
mainflux-cli groups unassign <user_id> <group_id> <user_auth_token>
```
#### List users for a group
```bash
mainflux-cli groups members <group_id> <user_auth_token>
```
#### List groups that user belongs to
```bash
mainflux-cli groups membership <user_id> <user_auth_token>
```
+182
View File
@@ -0,0 +1,182 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
"github.com/spf13/cobra"
)
var cmdGroups = []cobra.Command{
cobra.Command{
Use: "create",
Short: "create <JSON_group> <user_auth_token>",
Long: `Creates new group
JSON_group:
{
"Name":<group_name>,
"Description":<description>,
"ParentID":<parent_id>,
"Metadata":<metadata>,
}
Name - is unique group name
ParentID - ID of a group that is a parent to the creating group
Metadata - JSON structured string`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var group mfxsdk.Group
if err := json.Unmarshal([]byte(args[0]), &group); err != nil {
logError(err)
return
}
id, err := sdk.CreateGroup(group, args[1])
if err != nil {
logError(err)
return
}
logCreated(id)
},
},
cobra.Command{
Use: "get",
Short: "get [all | children <group_id> | group_id] <user_auth_token>",
Long: `Get all users groups, group children or group by id.
all - lists all groups
children <group_id> - lists all children groups of <group_id>
<group_id> - shows group with provided group ID`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
logUsage(cmd.Short)
return
}
if args[0] == "all" {
l, err := sdk.Groups(args[1], uint64(Offset), uint64(Limit), "")
if err != nil {
logError(err)
return
}
logJSON(l)
return
}
if args[0] == "children" {
l, err := sdk.Groups(args[2], uint64(Offset), uint64(Limit), args[1])
if err != nil {
logError(err)
return
}
logJSON(l)
return
}
t, err := sdk.Group(args[0], args[1])
if err != nil {
logError(err)
return
}
logJSON(t)
},
},
cobra.Command{
Use: "assign",
Short: "assign <user_id> <group_id> <user_auth_token>",
Long: `Assign user to a group.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.Assign(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "unassign",
Short: "unassign <user_id> <group_id> <user_auth_token>",
Long: `Unassign user from a group.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.Unassign(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "delete",
Short: "delete <group_id> <user_auth_token>",
Long: `Delete users group.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if err := sdk.DeleteGroup(args[0], args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "members",
Short: "members <group_id> <user_auth_token>",
Long: `Lists all user members of a group.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
up, err := sdk.Members(args[0], args[1], uint64(Offset), uint64(Limit))
if err != nil {
logError(err)
return
}
logJSON(up)
},
},
cobra.Command{
Use: "membership",
Short: "membership <user_id> <user_auth_token>",
Long: `List user groups membership`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
up, err := sdk.Memberships(args[0], args[1], uint64(Offset), uint64(Limit))
if err != nil {
logError(err)
return
}
logJSON(up)
},
},
}
// NewGroupsCmd returns users command.
func NewGroupsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "groups",
Short: "Groups management",
Long: `Groups management: create groups and assigns user to groups"`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("Usage: Groups [create | get | delete | assign | unassign | members | membership]")
},
}
for i := range cmdGroups {
cmd.AddCommand(&cmdGroups[i])
}
return &cmd
}
+1 -1
View File
@@ -124,7 +124,7 @@ var cmdProvision = []cobra.Command{
Email: un,
Password: "12345678",
}
if err := sdk.CreateUser(user); err != nil {
if _, err := sdk.CreateUser(user); err != nil {
logError(err)
return
}
+3 -2
View File
@@ -25,12 +25,13 @@ var cmdUsers = []cobra.Command{
Email: args[0],
Password: args[1],
}
if err := sdk.CreateUser(user); err != nil {
id, err := sdk.CreateUser(user)
if err != nil {
logError(err)
return
}
logOK()
logCreated(id)
},
},
cobra.Command{
+11
View File
@@ -20,6 +20,7 @@ func main() {
CertsURL: "http://localhost:8204",
ReaderPrefix: "",
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "http",
BootstrapPrefix: "things",
@@ -41,6 +42,7 @@ func main() {
versionCmd := cli.NewVersionCmd()
usersCmd := cli.NewUsersCmd()
thingsCmd := cli.NewThingsCmd()
groupsCmd := cli.NewGroupsCmd()
channelsCmd := cli.NewChannelsCmd()
messagesCmd := cli.NewMessagesCmd()
provisionCmd := cli.NewProvisionCmd()
@@ -50,6 +52,7 @@ func main() {
// Root Commands
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(usersCmd)
rootCmd.AddCommand(groupsCmd)
rootCmd.AddCommand(thingsCmd)
rootCmd.AddCommand(channelsCmd)
rootCmd.AddCommand(messagesCmd)
@@ -82,6 +85,14 @@ func main() {
"Mainflux things service prefix",
)
rootCmd.PersistentFlags().StringVarP(
&sdkConf.GroupsPrefix,
"groups-prefix",
"g",
sdkConf.GroupsPrefix,
"Mainflux groups service prefix",
)
rootCmd.PersistentFlags().StringVarP(
&sdkConf.HTTPAdapterPrefix,
"http-prefix",
+1 -1
View File
@@ -91,7 +91,7 @@ func main() {
// Merge environment variables and file settings.
mergeConfigs(&cfgFromFile, &cfg)
cfg = cfgFromFile
logger.Info("Continue with settings from file:" + cfg.File)
logger.Info("Continue with settings from file: " + cfg.File)
}
SDKCfg := mfSDK.Config{
+62 -32
View File
@@ -4,6 +4,7 @@
package main
import (
"context"
"fmt"
"io"
"io/ioutil"
@@ -17,6 +18,7 @@ import (
"github.com/mainflux/mainflux/internal/email"
"github.com/mainflux/mainflux/users"
"github.com/mainflux/mainflux/users/bcrypt"
"github.com/mainflux/mainflux/users/emailer"
"github.com/mainflux/mainflux/users/tracing"
"google.golang.org/grpc"
@@ -28,7 +30,6 @@ import (
authapi "github.com/mainflux/mainflux/authn/api/grpc"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/users/api"
"github.com/mainflux/mainflux/users/bcrypt"
"github.com/mainflux/mainflux/users/postgres"
opentracing "github.com/opentracing/opentracing-go"
stdprometheus "github.com/prometheus/client_golang/prometheus"
@@ -51,7 +52,6 @@ const (
defServerKey = ""
defJaegerURL = ""
defEmailLogLevel = "debug"
defEmailDriver = "smtp"
defEmailHost = "localhost"
defEmailPort = "25"
@@ -60,6 +60,9 @@ const (
defEmailFromAddress = ""
defEmailFromName = ""
defEmailTemplate = "email.tmpl"
defAdminEmail = ""
defAdminPassword = ""
defAdminGroup = "mainflux"
defTokenResetEndpoint = "/reset-request" // URL where user lands after click on the reset link from email
@@ -83,6 +86,9 @@ const (
envServerKey = "MF_USERS_SERVER_KEY"
envJaegerURL = "MF_JAEGER_URL"
envAdminEmail = "MF_USERS_ADMIN_EMAIL"
envAdminPassword = "MF_USERS_ADMIN_PASSWORD"
envEmailDriver = "MF_EMAIL_DRIVER"
envEmailHost = "MF_EMAIL_HOST"
envEmailPort = "MF_EMAIL_PORT"
@@ -102,18 +108,20 @@ const (
)
type config struct {
logLevel string
dbConfig postgres.Config
emailConf email.Config
httpPort string
serverCert string
serverKey string
jaegerURL string
resetURL string
authnTLS bool
authnCACerts string
authnURL string
authnTimeout time.Duration
logLevel string
dbConfig postgres.Config
emailConf email.Config
httpPort string
serverCert string
serverKey string
jaegerURL string
resetURL string
authnTLS bool
authnCACerts string
authnURL string
authnTimeout time.Duration
adminEmail string
adminPassword string
}
func main() {
@@ -123,7 +131,6 @@ func main() {
if err != nil {
log.Fatalf(err.Error())
}
db := connectToDB(cfg.dbConfig, logger)
defer db.Close()
@@ -191,18 +198,20 @@ func loadConfig() config {
}
return config{
logLevel: mainflux.Env(envLogLevel, defLogLevel),
dbConfig: dbConfig,
emailConf: emailConf,
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
serverCert: mainflux.Env(envServerCert, defServerCert),
serverKey: mainflux.Env(envServerKey, defServerKey),
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
resetURL: mainflux.Env(envTokenResetEndpoint, defTokenResetEndpoint),
authnTLS: tls,
authnCACerts: mainflux.Env(envAuthnCACerts, defAuthnCACerts),
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
authnTimeout: authnTimeout,
logLevel: mainflux.Env(envLogLevel, defLogLevel),
dbConfig: dbConfig,
emailConf: emailConf,
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
serverCert: mainflux.Env(envServerCert, defServerCert),
serverKey: mainflux.Env(envServerKey, defServerKey),
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
resetURL: mainflux.Env(envTokenResetEndpoint, defTokenResetEndpoint),
authnTLS: tls,
authnCACerts: mainflux.Env(envAuthnCACerts, defAuthnCACerts),
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
authnTimeout: authnTimeout,
adminEmail: mainflux.Env(envAdminEmail, defAdminEmail),
adminPassword: mainflux.Env(envAdminPassword, defAdminPassword),
}
}
@@ -230,14 +239,12 @@ func initJaeger(svcName, url string, logger logger.Logger) (opentracing.Tracer,
return tracer, closer
}
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sqlx.DB {
db, err := postgres.Connect(dbConfig)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
os.Exit(1)
}
return db
}
@@ -268,14 +275,16 @@ func connectToAuthn(cfg config, tracer opentracing.Tracer, logger logger.Logger)
func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthNServiceClient, c config, logger logger.Logger) users.Service {
database := postgres.NewDatabase(db)
repo := tracing.UserRepositoryMiddleware(postgres.New(database), tracer)
hasher := bcrypt.New()
userRepo := tracing.UserRepositoryMiddleware(postgres.NewUserRepo(database), tracer)
groupRepo := tracing.GroupRepositoryMiddleware(postgres.NewGroupRepo(database), tracer)
emailer, err := emailer.New(c.resetURL, &c.emailConf)
if err != nil {
logger.Error(fmt.Sprintf("Failed to configure e-mailing util: %s", err.Error()))
}
svc := users.New(repo, hasher, auth, emailer)
svc := users.New(userRepo, groupRepo, hasher, auth, emailer)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
@@ -292,10 +301,31 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthNServi
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
if err := createAdmin(svc, userRepo, groupRepo, c); err != nil {
logger.Error("failed to create admin user: " + err.Error())
os.Exit(1)
}
return svc
}
func createAdmin(svc users.Service, userRepo users.UserRepository, groupRepo users.GroupRepository, c config) error {
user := users.User{
Email: c.adminEmail,
Password: c.adminPassword,
}
if _, err := userRepo.RetrieveByEmail(context.Background(), user.Email); err == nil {
// Exiting if user already exists
return nil
}
if _, err := svc.Register(context.Background(), user); err != nil {
return err
}
return nil
}
func startHTTPServer(tracer opentracing.Tracer, svc users.Service, port string, certFile string, keyFile string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
if certFile != "" || keyFile != "" {
+2
View File
@@ -135,6 +135,8 @@ services:
MF_TOKEN_RESET_ENDPOINT: ${MF_TOKEN_RESET_ENDPOINT}
MF_AUTHN_GRPC_URL: ${MF_AUTHN_GRPC_URL}
MF_AUTHN_GRPC_TIMEOUT: ${MF_AUTHN_GRPC_TIMEOUT}
MF_USERS_ADMIN_EMAIL: ${MF_USERS_ADMIN_EMAIL}
MF_USERS_ADMIN_PASSWORD: ${MF_USERS_ADMIN_PASSWORD}
ports:
- ${MF_USERS_HTTP_PORT}:${MF_USERS_HTTP_PORT}
networks:
+7
View File
@@ -28,6 +28,7 @@ func TestCreateChannel(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -88,6 +89,7 @@ func TestCreateChannels(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -154,6 +156,7 @@ func TestChannel(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -209,6 +212,7 @@ func TestChannels(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -302,6 +306,7 @@ func TestChannelsByThing(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -445,6 +450,7 @@ func TestUpdateChannel(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -506,6 +512,7 @@ func TestDeleteChannel(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
+230
View File
@@ -0,0 +1,230 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package sdk
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/mainflux/mainflux/pkg/errors"
)
const groupsEndpoint = "groups"
func (sdk mfSDK) CreateGroup(g Group, token string) (string, error) {
data, err := json.Marshal(g)
if err != nil {
return "", err
}
url := createURL(sdk.baseURL, sdk.groupsPrefix, groupsEndpoint)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
if err != nil {
return "", err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", errors.Wrap(ErrFailedCreation, errors.New(resp.Status))
}
id := strings.TrimPrefix(resp.Header.Get("Location"), fmt.Sprintf("/%s/", groupsEndpoint))
return id, nil
}
func (sdk mfSDK) DeleteGroup(id, token string) error {
endpoint := fmt.Sprintf("%s/%s", groupsEndpoint, id)
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrap(ErrFailedRemoval, errors.New(resp.Status))
}
return nil
}
func (sdk mfSDK) Assign(userID, groupID, token string) error {
endpoint := fmt.Sprintf("%s/%s/users/%s", groupsEndpoint, groupID, userID)
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader([]byte{}))
if err != nil {
return err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrap(ErrFailedUserAdd, errors.New(resp.Status))
}
return nil
}
func (sdk mfSDK) Unassign(userID, groupID, token string) error {
endpoint := fmt.Sprintf("%s/%s/users/%s", groupsEndpoint, groupID, userID)
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)
req, err := http.NewRequest(http.MethodDelete, url, bytes.NewReader([]byte{}))
if err != nil {
return err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrap(ErrFailedRemoval, errors.New(resp.Status))
}
return nil
}
func (sdk mfSDK) Members(groupID, token string, offset, limit uint64) (UsersPage, error) {
endpoint := fmt.Sprintf("%s/%s/users?offset=%d&limit=%d&", groupsEndpoint, groupID, offset, limit)
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return UsersPage{}, err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return UsersPage{}, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return UsersPage{}, err
}
if resp.StatusCode != http.StatusOK {
return UsersPage{}, errors.Wrap(ErrFailedFetch, errors.New(resp.Status))
}
var tp UsersPage
if err := json.Unmarshal(body, &tp); err != nil {
return UsersPage{}, err
}
return tp, nil
}
func (sdk mfSDK) Groups(token string, offset, limit uint64, id string) (GroupsPage, error) {
endpoint := fmt.Sprintf("%s?offset=%d&limit=%d", groupsEndpoint, offset, limit)
if id != "" {
endpoint = fmt.Sprintf("%s/%s/groups?offset=%d&limit=%d", groupsEndpoint, id, offset, limit)
}
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return GroupsPage{}, err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return GroupsPage{}, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return GroupsPage{}, err
}
if resp.StatusCode != http.StatusOK {
return GroupsPage{}, errors.Wrap(ErrFailedFetch, errors.New(resp.Status))
}
var tp GroupsPage
if err := json.Unmarshal(body, &tp); err != nil {
return GroupsPage{}, err
}
return tp, nil
}
func (sdk mfSDK) Group(id, token string) (Group, error) {
endpoint := fmt.Sprintf("%s/%s", groupsEndpoint, id)
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return Group{}, err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return Group{}, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return Group{}, err
}
if resp.StatusCode != http.StatusOK {
return Group{}, errors.Wrap(ErrFailedFetch, errors.New(resp.Status))
}
var t Group
if err := json.Unmarshal(body, &t); err != nil {
return Group{}, err
}
return t, nil
}
func (sdk mfSDK) UpdateGroup(t Group, token string) error {
data, err := json.Marshal(t)
if err != nil {
return err
}
endpoint := fmt.Sprintf("%s/%s", groupsEndpoint, t.ID)
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
if err != nil {
return err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.Wrap(ErrFailedUpdate, errors.New(resp.Status))
}
return nil
}
+2
View File
@@ -40,6 +40,7 @@ func TestSendMessage(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -109,6 +110,7 @@ func TestSetContentType(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
+10
View File
@@ -40,3 +40,13 @@ type MessagesPage struct {
Messages []senml.Message `json:"messages,omitempty"`
pageRes
}
type GroupsPage struct {
Groups []Group `json:"groups"`
pageRes
}
type UsersPage struct {
Users []User `json:"users"`
pageRes
}
+44 -1
View File
@@ -69,6 +69,9 @@ var (
// ErrFailedCertUpdate failed to update certs in bootstrap config
ErrFailedCertUpdate = errors.New("failed to update certs in bootstrap config")
// ErrFailedUserAdd failed to add user to a group.
ErrFailedUserAdd = errors.New("failed to add user to group")
)
// ContentType represents all possible content types.
@@ -80,10 +83,20 @@ var _ SDK = (*mfSDK)(nil)
type User struct {
ID string `json:"id,omitempty"`
Email string `json:"email,omitempty"`
Groups []string `json:"groups,omitempty"`
Password string `json:"password,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// Group represents mainflux users group.
type Group struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// Thing represents mainflux thing.
type Thing struct {
ID string `json:"id,omitempty"`
@@ -102,7 +115,7 @@ type Channel struct {
// SDK contains Mainflux API.
type SDK interface {
// CreateUser registers mainflux user.
CreateUser(user User) error
CreateUser(user User) (string, error)
// User returns user object.
User(token string) (User, error)
@@ -138,6 +151,33 @@ type SDK interface {
// DeleteThing removes existing thing.
DeleteThing(id, token string) error
// CreateGroup creates new group and returns its id.
CreateGroup(group Group, token string) (string, error)
// DeleteGroup deletes users group.
DeleteGroup(id, token string) error
// Groups returns page of users groups.
Groups(token string, offset, limit uint64, name string) (GroupsPage, error)
// Group returns users group object by id.
Group(id, token string) (Group, error)
// Assign assigns user to a group.
Assign(userID, groupID, token string) error
// Unassign removes user from a group.
Unassign(userID, groupID, token string) error
// Members lists member users of a group.
Members(groupID, token string, offset, limit uint64) (UsersPage, error)
// Memberships lists groups for user.
Memberships(userID, token string, offset, limit uint64) (GroupsPage, error)
// UpdateGroup updates existing group.
UpdateGroup(group Group, token string) error
// Connect bulk connects things to channels specified by id.
Connect(conns ConnectionIDs, token string) error
@@ -216,6 +256,7 @@ type mfSDK struct {
certsURL string
readerPrefix string
usersPrefix string
groupsPrefix string
thingsPrefix string
certsPrefix string
channelsPrefix string
@@ -233,6 +274,7 @@ type Config struct {
CertsURL string
ReaderPrefix string
UsersPrefix string
GroupsPrefix string
ThingsPrefix string
HTTPAdapterPrefix string
BootstrapPrefix string
@@ -249,6 +291,7 @@ func NewSDK(conf Config) SDK {
certsURL: conf.CertsURL,
readerPrefix: conf.ReaderPrefix,
usersPrefix: conf.UsersPrefix,
groupsPrefix: conf.GroupsPrefix,
thingsPrefix: conf.ThingsPrefix,
httpAdapterPrefix: conf.HTTPAdapterPrefix,
bootstrapPrefix: conf.BootstrapPrefix,
+10
View File
@@ -65,6 +65,7 @@ func TestCreateThing(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -125,6 +126,7 @@ func TestCreateThings(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -192,6 +194,7 @@ func TestThing(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -248,6 +251,7 @@ func TestThings(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -344,6 +348,7 @@ func TestThingsByChannel(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -490,6 +495,7 @@ func TestUpdateThing(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -572,6 +578,7 @@ func TestDeleteThing(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -643,6 +650,7 @@ func TestConnectThing(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -747,6 +755,7 @@ func TestConnect(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -852,6 +861,7 @@ func TestDisconnectThing(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
+49 -10
View File
@@ -6,34 +6,43 @@ package sdk
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/mainflux/mainflux/pkg/errors"
)
func (sdk mfSDK) CreateUser(u User) error {
const (
usersEndpoint = "users"
tokensEndpoint = "tokens"
passwordEndpoint = "password"
)
func (sdk mfSDK) CreateUser(u User) (string, error) {
data, err := json.Marshal(u)
if err != nil {
return err
return "", err
}
url := createURL(sdk.baseURL, sdk.usersPrefix, "users")
url := createURL(sdk.baseURL, sdk.usersPrefix, usersEndpoint)
resp, err := sdk.client.Post(url, string(CTJSON), bytes.NewReader(data))
if err != nil {
return err
return "", err
}
if resp.StatusCode != http.StatusCreated {
return errors.Wrap(ErrFailedCreation, errors.New(resp.Status))
return "", errors.Wrap(ErrFailedCreation, errors.New(resp.Status))
}
return nil
id := strings.TrimPrefix(resp.Header.Get("Location"), fmt.Sprintf("/%s/", usersEndpoint))
return id, nil
}
func (sdk mfSDK) User(token string) (User, error) {
url := createURL(sdk.baseURL, sdk.usersPrefix, "users")
url := createURL(sdk.baseURL, sdk.usersPrefix, usersEndpoint)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
@@ -69,7 +78,7 @@ func (sdk mfSDK) CreateToken(user User) (string, error) {
return "", err
}
url := createURL(sdk.baseURL, sdk.usersPrefix, "tokens")
url := createURL(sdk.baseURL, sdk.usersPrefix, tokensEndpoint)
resp, err := sdk.client.Post(url, string(CTJSON), bytes.NewReader(data))
if err != nil {
@@ -100,7 +109,7 @@ func (sdk mfSDK) UpdateUser(u User, token string) error {
return err
}
url := createURL(sdk.baseURL, sdk.usersPrefix, "users")
url := createURL(sdk.baseURL, sdk.usersPrefix, usersEndpoint)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
if err != nil {
@@ -129,7 +138,7 @@ func (sdk mfSDK) UpdatePassword(oldPass, newPass, token string) error {
return err
}
url := createURL(sdk.baseURL, sdk.usersPrefix, "password")
url := createURL(sdk.baseURL, sdk.usersPrefix, passwordEndpoint)
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewReader(data))
if err != nil {
@@ -147,3 +156,33 @@ func (sdk mfSDK) UpdatePassword(oldPass, newPass, token string) error {
return nil
}
func (sdk mfSDK) Memberships(userID, token string, offset, limit uint64) (GroupsPage, error) {
endpoint := fmt.Sprintf("%s/%s/groups?offset=%d&limit=%d&", usersEndpoint, userID, offset, limit)
url := createURL(sdk.baseURL, sdk.groupsPrefix, endpoint)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return GroupsPage{}, err
}
resp, err := sdk.sendRequest(req, token, string(CTJSON))
if err != nil {
return GroupsPage{}, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return GroupsPage{}, err
}
if resp.StatusCode != http.StatusOK {
return GroupsPage{}, errors.Wrap(ErrFailedFetch, errors.New(resp.Status))
}
var tp GroupsPage
if err := json.Unmarshal(body, &tp); err != nil {
return GroupsPage{}, err
}
return tp, nil
}
+6 -4
View File
@@ -26,13 +26,13 @@ const (
)
func newUserService() users.Service {
repo := mocks.NewUserRepository()
usersRepo := mocks.NewUserRepository()
groupsRepo := mocks.NewGroupRepository()
hasher := mocks.NewHasher()
auth := mocks.NewAuthService(map[string]string{"user@example.com": "user@example.com"})
emailer := mocks.NewEmailer()
return users.New(repo, hasher, auth, emailer)
return users.New(usersRepo, groupsRepo, hasher, auth, emailer)
}
func newUserServer(svc users.Service) *httptest.Server {
@@ -48,6 +48,7 @@ func TestCreateUser(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
@@ -99,7 +100,7 @@ func TestCreateUser(t *testing.T) {
}
for _, tc := range cases {
err := mainfluxSDK.CreateUser(tc.user)
_, err := mainfluxSDK.CreateUser(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
}
}
@@ -111,6 +112,7 @@ func TestCreateToken(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
+1
View File
@@ -16,6 +16,7 @@ func TestVersion(t *testing.T) {
sdkConf := sdk.Config{
BaseURL: ts.URL,
UsersPrefix: "",
GroupsPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "",
MsgContentType: contentType,
-6
View File
@@ -5,12 +5,6 @@ package http
import "github.com/mainflux/mainflux/things"
var _ apiReq = (*identifyReq)(nil)
type apiReq interface {
validate() error
}
type identifyReq struct {
Token string `json:"token"`
}
+1 -1
View File
@@ -79,7 +79,7 @@ func Provision(conf Config) {
}
// Create new user
if err := s.CreateUser(user); err != nil {
if _, err := s.CreateUser(user); err != nil {
log.Fatalf("Unable to create new user: %s", err.Error())
return
+2
View File
@@ -31,6 +31,8 @@ default values.
| MF_USERS_HTTP_PORT | Users service HTTP port | 8180 |
| MF_USERS_SERVER_CERT | Path to server certificate in pem format | |
| MF_USERS_SERVER_KEY | Path to server key in pem format | |
| MF_USERS_ADMIN_EMAIL | Default user, created on startup | |
| MF_USERS_ADMIN_PASSWORD | Default user password, created on startup | |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
| MF_EMAIL_DRIVER | Mail server driver, mail server for sending reset password token | smtp |
| MF_EMAIL_HOST | Mail server host | localhost |
+208 -20
View File
@@ -13,15 +13,19 @@ import (
func registrationEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(userReq)
if err := req.validate(); err != nil {
return nil, err
return createUserRes{}, err
}
if err := svc.Register(ctx, req.user); err != nil {
return tokenRes{}, err
uid, err := svc.Register(ctx, req.user)
if err != nil {
return createUserRes{}, err
}
return tokenRes{}, nil
ucr := createUserRes{
ID: uid,
created: true,
}
logger.Info("User successfully registered")
return ucr, nil
}
}
@@ -41,14 +45,13 @@ func passwordResetRequestEndpoint(svc users.Service) endpoint.Endpoint {
if err := req.validate(); err != nil {
return nil, err
}
res := passwChangeRes{}
email := req.Email
if err := svc.GenerateResetToken(ctx, email, req.Host); err != nil {
return nil, err
}
res.Msg = MailSent
logger.Info("User made a password reset request")
return res, nil
}
}
@@ -63,7 +66,6 @@ func passwordResetEndpoint(svc users.Service) endpoint.Endpoint {
return nil, err
}
res := passwChangeRes{}
if err := svc.ResetPassword(ctx, req.Token, req.Password); err != nil {
return nil, err
}
@@ -75,16 +77,14 @@ func passwordResetEndpoint(svc users.Service) endpoint.Endpoint {
func viewUserEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(viewUserReq)
if err := req.validate(); err != nil {
return nil, err
}
u, err := svc.ViewUser(ctx, req.token)
u, err := svc.User(ctx, req.token)
if err != nil {
return nil, err
}
return viewUserRes{
ID: u.ID,
Email: u.Email,
@@ -96,11 +96,9 @@ func viewUserEndpoint(svc users.Service) endpoint.Endpoint {
func updateUserEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(updateUserReq)
if err := req.validate(); err != nil {
return nil, err
}
user := users.User{
Metadata: req.Metadata,
}
@@ -108,7 +106,6 @@ func updateUserEndpoint(svc users.Service) endpoint.Endpoint {
if err != nil {
return nil, err
}
return updateUserRes{}, nil
}
}
@@ -120,11 +117,9 @@ func passwordChangeEndpoint(svc users.Service) endpoint.Endpoint {
return nil, err
}
res := passwChangeRes{}
if err := svc.ChangePassword(ctx, req.Token, req.Password, req.OldPassword); err != nil {
return nil, err
}
return res, nil
}
}
@@ -132,16 +127,209 @@ func passwordChangeEndpoint(svc users.Service) endpoint.Endpoint {
func loginEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(userReq)
if err := req.validate(); err != nil {
return nil, err
}
token, err := svc.Login(ctx, req.user)
if err != nil {
return nil, err
}
logger.Info("User logged in")
return tokenRes{token}, nil
}
}
func createGroupEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(createGroupReq)
if err := req.validate(); err != nil {
return nil, err
}
group := users.Group{
Name: req.Name,
ParentID: req.ParentID,
Description: req.Description,
Metadata: req.Metadata,
}
saved, err := svc.CreateGroup(ctx, req.token, group)
if err != nil {
return nil, err
}
res := createGroupRes{
ID: saved.ID,
Name: saved.Name,
Description: saved.Description,
Metadata: saved.Metadata,
ParentID: saved.ParentID,
created: true,
}
logger.Info("Group: " + res.Name + " is created")
return res, nil
}
}
func assignUserToGroup(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(userGroupReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.Assign(ctx, req.token, req.userID, req.groupID); err != nil {
return nil, err
}
return assignUserToGroupRes{}, nil
}
}
func removeUserFromGroup(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(userGroupReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.Unassign(ctx, req.token, req.userID, req.groupID); err != nil {
return nil, err
}
return removeUserFromGroupRes{}, nil
}
}
func listUsersForGroupEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listUserGroupReq)
if err := req.validate(); err != nil {
return users.UserPage{}, err
}
up, err := svc.Members(ctx, req.token, req.groupID, req.offset, req.limit, req.metadata)
if err != nil {
return users.UserPage{}, err
}
return buildUsersResponse(up), nil
}
}
func listUserGroupsEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listUserGroupReq)
if err := req.validate(); err != nil {
return users.UserPage{}, err
}
gp, err := svc.Memberships(ctx, req.token, req.userID, req.offset, req.limit, req.metadata)
if err != nil {
return groupPageRes{}, err
}
return buildGroupsResponse(gp), nil
}
}
func updateGroupEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(updateGroupReq)
if err := req.validate(); err != nil {
return createGroupRes{}, err
}
group := users.Group{
Name: req.Name,
Description: req.Description,
Metadata: req.Metadata,
}
if err := svc.UpdateGroup(ctx, req.token, group); err != nil {
return createGroupRes{}, err
}
res := createGroupRes{
Name: group.Name,
Description: group.Description,
Metadata: group.Metadata,
created: false,
}
return res, nil
}
}
func viewGroupEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(groupReq)
if err := req.validate(); err != nil {
return viewGroupRes{}, err
}
group, err := svc.Group(ctx, req.token, req.groupID)
if err != nil {
return viewGroupRes{}, err
}
res := viewGroupRes{
Name: group.Name,
Description: group.Description,
Metadata: group.Metadata,
}
return res, nil
}
}
func listGroupsEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listUserGroupReq)
if err := req.validate(); err != nil {
return groupPageRes{}, err
}
gp, err := svc.Groups(ctx, req.token, req.groupID, req.offset, req.limit, req.metadata)
if err != nil {
return groupPageRes{}, err
}
return buildGroupsResponse(gp), nil
}
}
func deleteGroupEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(groupReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.RemoveGroup(ctx, req.token, req.groupID); err != nil {
return nil, err
}
return groupDeleteRes{}, nil
}
}
func buildGroupsResponse(gp users.GroupPage) groupPageRes {
res := groupPageRes{
pageRes: pageRes{
Total: gp.Total,
Offset: gp.Offset,
Limit: gp.Limit,
},
Groups: []viewGroupRes{},
}
for _, group := range gp.Groups {
view := viewGroupRes{
ID: group.ID,
ParentID: group.ParentID,
Name: group.Name,
Description: group.Description,
Metadata: group.Metadata,
}
res.Groups = append(res.Groups, view)
}
return res
}
func buildUsersResponse(up users.UserPage) userPageRes {
res := userPageRes{
pageRes: pageRes{
Total: up.Total,
Offset: up.Offset,
Limit: up.Limit,
},
Users: []viewUserRes{},
}
for _, user := range up.Users {
view := viewUserRes{
ID: user.ID,
Email: user.Email,
Metadata: user.Metadata,
}
res.Users = append(res.Users, view)
}
return res
}
+88 -12
View File
@@ -19,16 +19,16 @@ import (
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/users"
"github.com/mainflux/mainflux/users/api"
"github.com/mainflux/mainflux/users/bcrypt"
"github.com/mainflux/mainflux/users/mocks"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
contentType = "application/json"
invalidEmail = "userexample.com"
wrongID = "123e4567-e89b-12d3-a456-000000000042"
id = "123e4567-e89b-12d3-a456-000000000001"
)
var (
@@ -38,6 +38,7 @@ var (
malformedRes = toJSON(errorRes{users.ErrMalformedEntity.Error()})
unsupportedRes = toJSON(errorRes{api.ErrUnsupportedContentType.Error()})
failDecodeRes = toJSON(errorRes{api.ErrFailedDecode.Error()})
groupExists = toJSON(errorRes{users.ErrGroupConflict.Error()})
)
type testRequest struct {
@@ -66,12 +67,13 @@ func (tr testRequest) make() (*http.Response, error) {
}
func newService() users.Service {
repo := mocks.NewUserRepository()
hasher := mocks.NewHasher()
usersRepo := mocks.NewUserRepository()
groupRepo := mocks.NewGroupRepository()
hasher := bcrypt.New()
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
email := mocks.NewEmailer()
return users.New(repo, hasher, auth, email)
return users.New(usersRepo, groupRepo, hasher, auth, email)
}
func newServer(svc users.Service) *httptest.Server {
@@ -147,7 +149,8 @@ func TestLogin(t *testing.T) {
Email: "non-existentuser@example.com",
Password: "password",
})
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("register user got unexpected error: %s", err))
cases := []struct {
desc string
@@ -185,12 +188,13 @@ func TestLogin(t *testing.T) {
}
}
func TestViewUser(t *testing.T) {
func TestUser(t *testing.T) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("register user got unexpected error: %s", err))
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
tkn, _ := auth.Issue(context.Background(), &mainflux.IssueReq{Issuer: user.Email, Type: 0})
@@ -241,7 +245,8 @@ func TestPasswordResetRequest(t *testing.T) {
api.MailSent,
})
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("register user got unexpected error: %s", err))
cases := []struct {
desc string
@@ -297,9 +302,14 @@ func TestPasswordReset(t *testing.T) {
resData.Msg = users.ErrUserNotFound.Error()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("register user got unexpected error: %s", err))
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
tkn, _ := auth.Issue(context.Background(), &mainflux.IssueReq{Issuer: user.Email, Type: 0})
tkn, err := auth.Issue(context.Background(), &mainflux.IssueReq{Issuer: user.Email, Type: 0})
require.Nil(t, err, fmt.Sprintf("issue user token error: %s", err))
token := tkn.GetValue()
reqData.Password = user.Password
@@ -376,7 +386,8 @@ func TestPasswordChange(t *testing.T) {
}{}
resData.Msg = users.ErrUnauthorizedAccess.Error()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("register user got unexpected error: %s", err))
reqData.Password = user.Password
reqData.OldPassw = user.Password
@@ -427,6 +438,71 @@ func TestPasswordChange(t *testing.T) {
}
}
func TestGroupCreate(t *testing.T) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
tkn, _ := auth.Issue(context.Background(), &mainflux.IssueReq{Issuer: user.Email, Type: 0})
token := tkn.GetValue()
expectedSuccess := ""
groupData := struct {
Token string `json:"token,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
}{}
groupData.Token = token
groupData.Name = "Mainflux"
createValidTokenRequest := toJSON(groupData)
groupData.Token = "invalid"
createInvalidTokenRequest := toJSON(groupData)
cases := []struct {
desc string
req string
contentType string
status int
res string
tok string
}{
{"group create with valid token", createValidTokenRequest, contentType, http.StatusCreated, expectedSuccess, token},
{"group create with existing name", createValidTokenRequest, contentType, http.StatusConflict, groupExists, token},
{"group create with invalid token", createInvalidTokenRequest, contentType, http.StatusForbidden, unauthRes, ""},
{"group create with empty JSON request", "{}", contentType, http.StatusBadRequest, malformedRes, token},
{"group create empty request", "", contentType, http.StatusBadRequest, failDecodeRes, token},
{"group create missing content type", createValidTokenRequest, "", http.StatusUnsupportedMediaType, unsupportedRes, token},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPost,
url: fmt.Sprintf("%s/groups", ts.URL),
contentType: tc.contentType,
body: strings.NewReader(tc.req),
token: tc.tok,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
body, err := ioutil.ReadAll(res.Body)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
token := strings.Trim(string(body), "\n")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.res, token, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, token))
}
}
type errorRes struct {
Err string `json:"error"`
}
+120 -3
View File
@@ -24,7 +24,7 @@ func LoggingMiddleware(svc users.Service, logger log.Logger) users.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Register(ctx context.Context, user users.User) (err error) {
func (lm *loggingMiddleware) Register(ctx context.Context, user users.User) (uid string, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method register for user %s took %s to complete", user.Email, time.Since(begin))
if err != nil {
@@ -51,7 +51,7 @@ func (lm *loggingMiddleware) Login(ctx context.Context, user users.User) (token
return lm.svc.Login(ctx, user)
}
func (lm *loggingMiddleware) ViewUser(ctx context.Context, token string) (u users.User, err error) {
func (lm *loggingMiddleware) User(ctx context.Context, token string) (u users.User, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method view_user for user %s took %s to complete", u.Email, time.Since(begin))
if err != nil {
@@ -61,7 +61,7 @@ func (lm *loggingMiddleware) ViewUser(ctx context.Context, token string) (u user
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ViewUser(ctx, token)
return lm.svc.User(ctx, token)
}
func (lm *loggingMiddleware) UpdateUser(ctx context.Context, token string, u users.User) (err error) {
@@ -128,3 +128,120 @@ func (lm *loggingMiddleware) SendPasswordReset(ctx context.Context, host, email,
return lm.svc.SendPasswordReset(ctx, host, email, token)
}
func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, group users.Group) (u users.Group, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method create_group with name %s took %s to complete", group.Name, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.CreateGroup(ctx, token, group)
}
func (lm *loggingMiddleware) Groups(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (e users.GroupPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method groups for parent %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Groups(ctx, token, id, offset, limit, meta)
}
func (lm *loggingMiddleware) Members(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (e users.UserPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method members for parent %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Members(ctx, token, id, offset, limit, meta)
}
func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove_group with id %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.RemoveGroup(ctx, token, id)
}
func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, group users.Group) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_group %s took %s to complete", group.Name, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.UpdateGroup(ctx, token, group)
}
func (lm *loggingMiddleware) Group(ctx context.Context, token, id string) (u users.Group, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method group with id %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Group(ctx, token, id)
}
func (lm *loggingMiddleware) Assign(ctx context.Context, token, userID, groupID string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method assign user %s, group %s took %s to complete", userID, groupID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Assign(ctx, token, userID, groupID)
}
func (lm *loggingMiddleware) Unassign(ctx context.Context, token, userID, groupID string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method unassign for user %s, group %s took %s to complete", userID, groupID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Unassign(ctx, token, userID, groupID)
}
func (lm *loggingMiddleware) Memberships(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (e users.GroupPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method memberships for user %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Memberships(ctx, token, id, offset, limit, meta)
}
+85 -3
View File
@@ -29,7 +29,7 @@ func MetricsMiddleware(svc users.Service, counter metrics.Counter, latency metri
}
}
func (ms *metricsMiddleware) Register(ctx context.Context, user users.User) error {
func (ms *metricsMiddleware) Register(ctx context.Context, user users.User) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "register").Add(1)
ms.latency.With("method", "register").Observe(time.Since(begin).Seconds())
@@ -47,13 +47,13 @@ func (ms *metricsMiddleware) Login(ctx context.Context, user users.User) (string
return ms.svc.Login(ctx, user)
}
func (ms *metricsMiddleware) ViewUser(ctx context.Context, token string) (users.User, error) {
func (ms *metricsMiddleware) User(ctx context.Context, token string) (users.User, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_user").Add(1)
ms.latency.With("method", "view_user").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewUser(ctx, token)
return ms.svc.User(ctx, token)
}
func (ms *metricsMiddleware) UpdateUser(ctx context.Context, token string, u users.User) (err error) {
@@ -100,3 +100,85 @@ func (ms *metricsMiddleware) SendPasswordReset(ctx context.Context, host, email,
return ms.svc.SendPasswordReset(ctx, host, email, token)
}
func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, group users.Group) (users.Group, error) {
defer func(begin time.Time) {
ms.counter.With("method", "create_group").Add(1)
ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CreateGroup(ctx, token, group)
}
func (ms *metricsMiddleware) Groups(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (users.GroupPage, error) {
defer func(begin time.Time) {
ms.counter.With("method", "groups").Add(1)
ms.latency.With("method", "groups").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Groups(ctx, token, id, offset, limit, meta)
}
func (ms *metricsMiddleware) Members(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (users.UserPage, error) {
defer func(begin time.Time) {
ms.counter.With("method", "members").Add(1)
ms.latency.With("method", "members").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Members(ctx, token, id, offset, limit, meta)
}
func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_group").Add(1)
ms.latency.With("method", "remove_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RemoveGroup(ctx, token, id)
}
func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, group users.Group) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_group").Add(1)
ms.latency.With("method", "update_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateGroup(ctx, token, group)
}
func (ms *metricsMiddleware) Group(ctx context.Context, token, name string) (users.Group, error) {
defer func(begin time.Time) {
ms.counter.With("method", "group").Add(1)
ms.latency.With("method", "group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Group(ctx, token, name)
}
func (ms *metricsMiddleware) Assign(ctx context.Context, token, userID, groupID string) error {
defer func(begin time.Time) {
ms.counter.With("method", "assign").Add(1)
ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Assign(ctx, token, userID, groupID)
}
func (ms *metricsMiddleware) Unassign(ctx context.Context, token, userID, groupID string) error {
defer func(begin time.Time) {
ms.counter.With("method", "unassign").Add(1)
ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Unassign(ctx, token, userID, groupID)
}
func (ms *metricsMiddleware) Memberships(ctx context.Context, token, id string, offset, limit uint64, meta users.Metadata) (users.GroupPage, error) {
defer func(begin time.Time) {
ms.counter.With("method", "memberships").Add(1)
ms.latency.With("method", "memberships").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Memberships(ctx, token, id, offset, limit, meta)
}
+94 -5
View File
@@ -7,11 +7,10 @@ import (
"github.com/mainflux/mainflux/users"
)
const minPassLen = 8
type apiReq interface {
validate() error
}
const (
minPassLen = 8
maxNameSize = 1024
)
type userReq struct {
user users.User
@@ -93,3 +92,93 @@ func (req passwChangeReq) validate() error {
}
return nil
}
type createGroupReq struct {
token string
Name string `json:"name,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Description string `json:"description,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (req createGroupReq) validate() error {
if req.token == "" {
return users.ErrUnauthorizedAccess
}
if len(req.Name) > maxNameSize || req.Name == "" {
return users.ErrMalformedEntity
}
return nil
}
type updateGroupReq struct {
token string
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (req updateGroupReq) validate() error {
if req.token == "" {
return users.ErrUnauthorizedAccess
}
if req.Name == "" {
return users.ErrMalformedEntity
}
if len(req.Name) > maxNameSize {
return users.ErrMalformedEntity
}
return nil
}
type listUserGroupReq struct {
token string
offset uint64
limit uint64
metadata users.Metadata
name string
groupID string
userID string
}
func (req listUserGroupReq) validate() error {
if req.token == "" {
return users.ErrUnauthorizedAccess
}
return nil
}
type userGroupReq struct {
token string
groupID string
userID string
}
func (req userGroupReq) validate() error {
if req.token == "" {
return users.ErrUnauthorizedAccess
}
if req.groupID == "" {
return users.ErrMalformedEntity
}
if req.userID == "" {
return users.ErrMalformedEntity
}
return nil
}
type groupReq struct {
token string
groupID string
name string
}
func (req groupReq) validate() error {
if req.token == "" {
return users.ErrUnauthorizedAccess
}
if req.groupID == "" && req.name == "" {
return users.ErrMalformedEntity
}
return nil
}
+184
View File
@@ -4,7 +4,9 @@
package api
import (
"fmt"
"net/http"
"os/user"
"github.com/mainflux/mainflux"
)
@@ -13,11 +15,51 @@ var (
_ mainflux.Response = (*tokenRes)(nil)
_ mainflux.Response = (*viewUserRes)(nil)
_ mainflux.Response = (*passwChangeRes)(nil)
_ mainflux.Response = (*updateGroupRes)(nil)
_ mainflux.Response = (*viewGroupRes)(nil)
_ mainflux.Response = (*createGroupRes)(nil)
_ mainflux.Response = (*createUserRes)(nil)
_ mainflux.Response = (*groupDeleteRes)(nil)
_ mainflux.Response = (*assignUserToGroupRes)(nil)
_ mainflux.Response = (*removeUserFromGroupRes)(nil)
)
// MailSent message response when link is sent
const MailSent = "Email with reset link is sent"
type pageRes struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
}
type createUserRes struct {
ID string
created bool
}
func (res createUserRes) Code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res createUserRes) Headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprintf("/users/%s", res.ID),
}
}
return map[string]string{}
}
func (res createUserRes) Empty() bool {
return true
}
type tokenRes struct {
Token string `json:"token,omitempty"`
}
@@ -51,6 +93,7 @@ func (res updateUserRes) Empty() bool {
type viewUserRes struct {
ID string `json:"id"`
Email string `json:"email"`
Groups []user.Group `json:"groups"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
@@ -66,6 +109,88 @@ func (res viewUserRes) Empty() bool {
return false
}
type userPageRes struct {
pageRes
Users []viewUserRes
}
func (res userPageRes) Code() int {
return http.StatusOK
}
func (res userPageRes) Headers() map[string]string {
return map[string]string{}
}
func (res userPageRes) Empty() bool {
return false
}
type createGroupRes struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
ParentID string `json:"parent_id"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
created bool
}
func (res createGroupRes) Code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res createGroupRes) Headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprintf("/groups/%s", res.ID),
}
}
return map[string]string{}
}
func (res createGroupRes) Empty() bool {
return true
}
type updateGroupRes struct{}
func (res updateGroupRes) Code() int {
return http.StatusOK
}
func (res updateGroupRes) Headers() map[string]string {
return map[string]string{}
}
func (res updateGroupRes) Empty() bool {
return true
}
type viewGroupRes struct {
ID string `json:"id"`
Name string `json:"name"`
ParentID string `json:"parent_id"`
OwnerID string `json:"owner_id"`
Description string `json:"description"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (res viewGroupRes) Code() int {
return http.StatusOK
}
func (res viewGroupRes) Headers() map[string]string {
return map[string]string{}
}
func (res viewGroupRes) Empty() bool {
return false
}
type errorRes struct {
Err string `json:"error"`
}
@@ -85,3 +210,62 @@ func (res passwChangeRes) Headers() map[string]string {
func (res passwChangeRes) Empty() bool {
return false
}
type groupPageRes struct {
pageRes
Groups []viewGroupRes `json:"groups"`
}
func (res groupPageRes) Code() int {
return http.StatusOK
}
func (res groupPageRes) Headers() map[string]string {
return map[string]string{}
}
func (res groupPageRes) Empty() bool {
return false
}
type groupDeleteRes struct{}
func (res groupDeleteRes) Code() int {
return http.StatusNoContent
}
func (res groupDeleteRes) Headers() map[string]string {
return map[string]string{}
}
func (res groupDeleteRes) Empty() bool {
return true
}
type assignUserToGroupRes struct{}
func (res assignUserToGroupRes) Code() int {
return http.StatusNoContent
}
func (res assignUserToGroupRes) Headers() map[string]string {
return map[string]string{}
}
func (res assignUserToGroupRes) Empty() bool {
return true
}
type removeUserFromGroupRes struct{}
func (res removeUserFromGroupRes) Code() int {
return http.StatusNoContent
}
func (res removeUserFromGroupRes) Headers() map[string]string {
return map[string]string{}
}
func (res removeUserFromGroupRes) Empty() bool {
return true
}
+215 -12
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"github.com/mainflux/mainflux/pkg/errors"
@@ -22,14 +23,24 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const contentType = "application/json"
const (
contentType = "application/json"
offsetKey = "offset"
limitKey = "limit"
nameKey = "name"
metadataKey = "metadata"
defOffset = 0
defLimit = 10
)
var (
errInvalidQueryParams = errors.New("invalid query params")
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type
ErrUnsupportedContentType = errors.New("unsupported content type")
errMissingRefererHeader = errors.New("missing referer header")
errInvalidToken = errors.New("invalid token")
errNoTokenSupplied = errors.New("no token supplied")
// ErrFailedDecode indicates failed to decode request body
ErrFailedDecode = errors.New("failed to decode request body")
logger log.Logger
@@ -66,6 +77,13 @@ func MakeHandler(svc users.Service, tracer opentracing.Tracer, l log.Logger) htt
opts...,
))
mux.Get("/users/:userID/groups", kithttp.NewServer(
kitot.TraceServer(tracer, "memberships")(listUserGroupsEndpoint(svc)),
decodeListUserGroupRequest,
encodeResponse,
opts...,
))
mux.Post("/password/reset-request", kithttp.NewServer(
kitot.TraceServer(tracer, "res-req")(passwordResetRequestEndpoint(svc)),
decodePasswordResetRequest,
@@ -87,6 +105,69 @@ func MakeHandler(svc users.Service, tracer opentracing.Tracer, l log.Logger) htt
opts...,
))
mux.Post("/groups", kithttp.NewServer(
kitot.TraceServer(tracer, "add_group")(createGroupEndpoint(svc)),
decodeGroupCreate,
encodeResponse,
opts...,
))
mux.Get("/groups", kithttp.NewServer(
kitot.TraceServer(tracer, "groups")(listGroupsEndpoint(svc)),
decodeListUserGroupRequest,
encodeResponse,
opts...,
))
mux.Delete("/groups/:groupID", kithttp.NewServer(
kitot.TraceServer(tracer, "delete_group")(deleteGroupEndpoint(svc)),
decodeGroupRequest,
encodeResponse,
opts...,
))
mux.Put("/groups/:groupID/users/:userID", kithttp.NewServer(
kitot.TraceServer(tracer, "assign_user_to_group")(assignUserToGroup(svc)),
decodeUserGroupRequest,
encodeResponse,
opts...,
))
mux.Delete("/groups/:groupID/users/:userID", kithttp.NewServer(
kitot.TraceServer(tracer, "remove_user_from_group")(removeUserFromGroup(svc)),
decodeUserGroupRequest,
encodeResponse,
opts...,
))
mux.Get("/groups/:groupID/users", kithttp.NewServer(
kitot.TraceServer(tracer, "members")(listUsersForGroupEndpoint(svc)),
decodeListUserGroupRequest,
encodeResponse,
opts...,
))
mux.Patch("/groups/:groupID", kithttp.NewServer(
kitot.TraceServer(tracer, "update_group")(updateGroupEndpoint(svc)),
decodeGroupCreate,
encodeResponse,
opts...,
))
mux.Get("/groups/:groupID/groups", kithttp.NewServer(
kitot.TraceServer(tracer, "list_children_groups")(listGroupsEndpoint(svc)),
decodeListUserGroupRequest,
encodeResponse,
opts...,
))
mux.Get("/groups/:groupID", kithttp.NewServer(
kitot.TraceServer(tracer, "group")(viewGroupEndpoint(svc)),
decodeGroupRequest,
encodeResponse,
opts...,
))
mux.Post("/tokens", kithttp.NewServer(
kitot.TraceServer(tracer, "login")(loginEndpoint(svc)),
decodeCredentials,
@@ -173,19 +254,88 @@ func decodePasswordChange(_ context.Context, r *http.Request) (interface{}, erro
return req, nil
}
func decodeToken(_ context.Context, r *http.Request) (interface{}, error) {
vals := bone.GetQuery(r, "token")
if len(vals) > 1 {
return "", errInvalidToken
// Group related methods
func decodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, ErrUnsupportedContentType
}
if len(vals) == 0 {
return "", errNoTokenSupplied
var req createGroupReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(ErrFailedDecode, err)
}
t := vals[0]
return t, nil
req.token = r.Header.Get("Authorization")
return req, nil
}
func decodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, ErrUnsupportedContentType
}
req := groupReq{
token: r.Header.Get("Authorization"),
groupID: bone.GetValue(r, "groupID"),
name: bone.GetValue(r, "name"),
}
return req, nil
}
func decodeListUserGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, ErrUnsupportedContentType
}
o, err := readUintQuery(r, offsetKey, defOffset)
if err != nil {
return nil, err
}
l, err := readUintQuery(r, limitKey, defLimit)
if err != nil {
return nil, err
}
n, err := readStringQuery(r, nameKey)
if err != nil {
return nil, err
}
m, err := readMetadataQuery(r, metadataKey)
if err != nil {
return nil, err
}
groupID := bone.GetValue(r, "groupID")
userID := bone.GetValue(r, "userID")
req := listUserGroupReq{
token: r.Header.Get("Authorization"),
groupID: groupID,
userID: userID,
offset: o,
limit: l,
name: n,
metadata: m,
}
return req, nil
}
func decodeUserGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, ErrUnsupportedContentType
}
req := userGroupReq{
token: r.Header.Get("Authorization"),
groupID: bone.GetValue(r, "groupID"),
userID: bone.GetValue(r, "userID"),
}
return req, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
if ar, ok := response.(mainflux.Response); ok {
for k, v := range ar.Headers() {
@@ -213,6 +363,8 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.WriteHeader(http.StatusForbidden)
case errors.Contains(errorVal, users.ErrConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(errorVal, users.ErrGroupConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(errorVal, ErrUnsupportedContentType):
w.WriteHeader(http.StatusUnsupportedMediaType)
case errors.Contains(errorVal, ErrFailedDecode):
@@ -237,3 +389,54 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.WriteHeader(http.StatusInternalServerError)
}
}
func readUintQuery(r *http.Request, key string, def uint64) (uint64, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return 0, errInvalidQueryParams
}
if len(vals) == 0 {
return def, nil
}
strval := vals[0]
val, err := strconv.ParseUint(strval, 10, 64)
if err != nil {
return 0, errInvalidQueryParams
}
return val, nil
}
func readStringQuery(r *http.Request, key string) (string, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return "", errInvalidQueryParams
}
if len(vals) == 0 {
return "", nil
}
return vals[0], nil
}
func readMetadataQuery(r *http.Request, key string) (map[string]interface{}, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return nil, errInvalidQueryParams
}
if len(vals) == 0 {
return nil, nil
}
m := make(map[string]interface{})
err := json.Unmarshal([]byte(vals[0]), &m)
if err != nil {
return nil, errors.Wrap(errInvalidQueryParams, err)
}
return m, nil
}
+1 -2
View File
@@ -24,6 +24,5 @@ func New(url string, c *email.Config) (users.Emailer, error) {
func (e *emailer) SendPasswordReset(To []string, host string, token string) error {
url := fmt.Sprintf("%s%s?token=%s", host, e.resetURL, token)
content := fmt.Sprintf("%s", url)
return e.agent.Send(To, "", "Password reset", "", content, "")
return e.agent.Send(To, "", "Password reset", "", url, "")
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package users
import (
"context"
)
// Group of users
type Group struct {
ID string
Name string
OwnerID string
ParentID string
Description string
Metadata map[string]interface{}
}
// GroupRepository specifies an group persistence API.
type GroupRepository interface {
// Save persists the group.
Save(ctx context.Context, g Group) (Group, error)
// Update updates the group data.
Update(ctx context.Context, g Group) error
// Delete deletes group for given id.
Delete(ctx context.Context, id string) error
// RetrieveByID retrieves group by its unique identifier.
RetrieveByID(ctx context.Context, id string) (Group, error)
// RetrieveByName retrieves group by name
RetrieveByName(ctx context.Context, name string) (Group, error)
// RetrieveAllWithAncestors retrieves all groups if groupID == "", if groupID is specified returns children groups
RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, gm Metadata) (GroupPage, error)
// Memberships retrieves all groups that user belongs to
Memberships(ctx context.Context, userID string, offset, limit uint64, gm Metadata) (GroupPage, error)
// Assign adds user to group.
Assign(ctx context.Context, userID, groupID string) error
// Unassign removes user from group
Unassign(ctx context.Context, userID, groupID string) error
}
+205
View File
@@ -0,0 +1,205 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"context"
"sync"
"github.com/mainflux/mainflux/users"
)
var _ users.GroupRepository = (*groupRepositoryMock)(nil)
type groupRepositoryMock struct {
mu sync.Mutex
groups map[string]users.Group
// Map of "Maps of users assigned to a group" where group is a key
users map[string]map[string]users.User
groupsByUser map[string]map[string]users.Group
groupsByName map[string]users.Group
childrenByGroups map[string]map[string]users.Group
}
// NewGroupRepository creates in-memory user repository
func NewGroupRepository() users.GroupRepository {
return &groupRepositoryMock{
groups: make(map[string]users.Group),
groupsByName: make(map[string]users.Group),
users: make(map[string]map[string]users.User),
groupsByUser: make(map[string]map[string]users.Group),
childrenByGroups: make(map[string]map[string]users.Group),
}
}
func (grm *groupRepositoryMock) Save(ctx context.Context, g users.Group) (users.Group, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[g.ID]; ok {
return users.Group{}, users.ErrGroupConflict
}
if _, ok := grm.groupsByName[g.Name]; ok {
return users.Group{}, users.ErrGroupConflict
}
if g.ParentID != "" {
if _, ok := grm.groups[g.ParentID]; !ok {
return users.Group{}, users.ErrCreateGroup
}
if _, ok := grm.childrenByGroups[g.ParentID]; !ok {
grm.childrenByGroups[g.ParentID] = make(map[string]users.Group)
}
grm.childrenByGroups[g.ParentID][g.ID] = g
}
grm.groups[g.ID] = g
grm.groupsByName[g.Name] = g
if _, ok := grm.groupsByUser[g.OwnerID]; !ok {
grm.groupsByUser[g.OwnerID] = make(map[string]users.Group)
}
grm.groupsByUser[g.OwnerID][g.ID] = g
return g, nil
}
func (grm *groupRepositoryMock) Delete(ctx context.Context, id string) error {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[id]; !ok {
return users.ErrNotFound
}
delete(grm.groups, id)
return nil
}
func (grm *groupRepositoryMock) Unassign(ctx context.Context, userID, groupID string) error {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[groupID]; !ok {
return users.ErrNotFound
}
delete(grm.users[groupID], userID)
return nil
}
func (grm *groupRepositoryMock) Update(ctx context.Context, g users.Group) error {
grm.mu.Lock()
defer grm.mu.Unlock()
var group users.Group
group, ok := grm.groups[g.ID]
if !ok {
return users.ErrNotFound
}
group.Description = g.Description
group.Metadata = g.Metadata
group.ParentID = g.ParentID
group.Name = g.Name
group.OwnerID = g.OwnerID
grm.groups[g.ID] = group
grm.groupsByName[g.ID] = group
grm.groupsByUser[g.OwnerID][g.ID] = group
return nil
}
func (grm *groupRepositoryMock) Remove(ctx context.Context, g users.Group) error {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[g.ID]; !ok {
return users.ErrDeleteGroupMissing
}
if _, ok := grm.groups[g.ID]; !ok {
return users.ErrDeleteGroupMissing
}
delete(grm.users, g.ID)
delete(grm.groups, g.ID)
delete(grm.childrenByGroups, g.ID)
delete(grm.groupsByName, g.Name)
return nil
}
func (grm *groupRepositoryMock) RetrieveByID(ctx context.Context, id string) (users.Group, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
val, ok := grm.groups[id]
if !ok {
return users.Group{}, users.ErrNotFound
}
return val, nil
}
func (grm *groupRepositoryMock) RetrieveByName(ctx context.Context, name string) (users.Group, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
var val users.Group
err := users.ErrNotFound
for _, g := range grm.groups {
if g.Name == name {
val = g
err = nil
break
}
}
return val, err
}
func (grm *groupRepositoryMock) Assign(ctx context.Context, userID, groupID string) error {
grm.mu.Lock()
defer grm.mu.Unlock()
if _, ok := grm.groups[groupID]; !ok {
return users.ErrNotFound
}
if _, ok := grm.users[groupID]; !ok {
grm.users[groupID] = make(map[string]users.User)
}
if _, ok := grm.groupsByUser[userID]; !ok {
grm.groupsByUser[userID] = make(map[string]users.Group)
}
grm.users[groupID][userID] = users.User{ID: userID}
grm.groupsByUser[userID][groupID] = users.Group{ID: groupID}
return nil
}
func (grm *groupRepositoryMock) Memberships(ctx context.Context, userID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
var items []users.Group
groups, ok := grm.groupsByUser[userID]
if !ok {
return users.GroupPage{}, users.ErrNotFound
}
for _, g := range groups {
items = append(items, g)
}
return users.GroupPage{
Groups: items,
PageMetadata: users.PageMetadata{
Limit: limit,
Offset: offset,
Total: uint64(len(items)),
},
}, nil
}
func (grm *groupRepositoryMock) RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
grm.mu.Lock()
defer grm.mu.Unlock()
var items []users.Group
for _, g := range grm.groups {
items = append(items, g)
}
return users.GroupPage{
Groups: items,
PageMetadata: users.PageMetadata{
Limit: limit,
Offset: offset,
Total: uint64(len(items)),
},
}, nil
}
+35 -6
View File
@@ -13,27 +13,32 @@ import (
var _ users.UserRepository = (*userRepositoryMock)(nil)
type userRepositoryMock struct {
mu sync.Mutex
users map[string]users.User
mu sync.Mutex
users map[string]users.User
usersByID map[string]users.User
usersByGroupID map[string]users.User
}
// NewUserRepository creates in-memory user repository
func NewUserRepository() users.UserRepository {
return &userRepositoryMock{
users: make(map[string]users.User),
users: make(map[string]users.User),
usersByID: make(map[string]users.User),
usersByGroupID: make(map[string]users.User),
}
}
func (urm *userRepositoryMock) Save(ctx context.Context, user users.User) error {
func (urm *userRepositoryMock) Save(ctx context.Context, user users.User) (string, error) {
urm.mu.Lock()
defer urm.mu.Unlock()
if _, ok := urm.users[user.Email]; ok {
return users.ErrConflict
return "", users.ErrConflict
}
urm.users[user.Email] = user
return nil
urm.usersByID[user.ID] = user
return user.ID, nil
}
func (urm *userRepositoryMock) Update(ctx context.Context, user users.User) error {
@@ -72,6 +77,30 @@ func (urm *userRepositoryMock) RetrieveByEmail(ctx context.Context, email string
return val, nil
}
func (urm *userRepositoryMock) RetrieveByID(ctx context.Context, id string) (users.User, error) {
urm.mu.Lock()
defer urm.mu.Unlock()
val, ok := urm.usersByID[id]
if !ok {
return users.User{}, users.ErrNotFound
}
return val, nil
}
func (urm *userRepositoryMock) Members(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.UserPage, error) {
urm.mu.Lock()
defer urm.mu.Unlock()
_, ok := urm.usersByGroupID[groupID]
if !ok {
return users.UserPage{}, users.ErrNotFound
}
return users.UserPage{}, nil
}
func (urm *userRepositoryMock) UpdatePassword(_ context.Context, token, password string) error {
urm.mu.Lock()
defer urm.mu.Unlock()
+451
View File
@@ -0,0 +1,451 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/gofrs/uuid"
"github.com/lib/pq"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/users"
)
var (
errDeleteGroupDB = errors.New("delete group failed")
errSelectDb = errors.New("select group from db error")
errFK = "foreign_key_violation"
errInvalid = "invalid_text_representation"
errTruncation = "string_data_right_truncation"
)
var _ users.GroupRepository = (*groupRepository)(nil)
type groupRepository struct {
db Database
}
// NewGroupRepo instantiates a PostgreSQL implementation of group
// repository.
func NewGroupRepo(db Database) users.GroupRepository {
return &groupRepository{
db: db,
}
}
func (gr groupRepository) Save(ctx context.Context, group users.Group) (users.Group, error) {
var id string
q := `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata) VALUES (:name, :description, :id, :owner_id, :parent_id, :metadata) RETURNING id`
if group.ParentID == "" {
q = `INSERT INTO groups (name, description, id, owner_id, metadata) VALUES (:name, :description, :id, :owner_id, :metadata) RETURNING id`
}
dbu, err := toDBGroup(group)
if err != nil {
return users.Group{}, err
}
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return users.Group{}, errors.Wrap(users.ErrMalformedEntity, err)
case errDuplicate:
return users.Group{}, errors.Wrap(users.ErrGroupConflict, err)
}
}
return users.Group{}, errors.Wrap(users.ErrCreateGroup, err)
}
defer row.Close()
row.Next()
if err := row.Scan(&id); err != nil {
return users.Group{}, err
}
group.ID = id
return group, nil
}
func (gr groupRepository) Update(ctx context.Context, group users.Group) error {
q := `UPDATE groups SET(name, description, metadata) VALUES (:name, :description, :metadata) WHERE id = :id`
dbu, err := toDBGroup(group)
if err != nil {
return errors.Wrap(errUpdateDB, err)
}
if _, err := gr.db.NamedExecContext(ctx, q, dbu); err != nil {
return errors.Wrap(errUpdateDB, err)
}
return nil
}
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
qd := `DELETE FROM groups WHERE id = :id`
dbg, err := toDBGroup(users.Group{ID: groupID})
if err != nil {
return errors.Wrap(errUpdateDB, err)
}
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
if err != nil {
return errors.Wrap(errDeleteGroupDB, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(errDeleteGroupDB, err)
}
if cnt != 1 {
return errors.Wrap(users.ErrDeleteGroupMissing, err)
}
return nil
}
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (users.Group, error) {
q := `SELECT id, name, owner_id, parent_id, description, metadata FROM groups WHERE id = $1`
dbu := dbGroup{
ID: id,
}
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return users.Group{}, errors.Wrap(users.ErrNotFound, err)
}
return users.Group{}, errors.Wrap(errRetrieveDB, err)
}
return toGroup(dbu), nil
}
func (gr groupRepository) RetrieveByName(ctx context.Context, name string) (users.Group, error) {
q := `SELECT id, name, description, metadata FROM groups WHERE name = $1`
dbu := dbGroup{
Name: name,
}
if err := gr.db.QueryRowxContext(ctx, q, name).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return users.Group{}, errors.Wrap(users.ErrNotFound, err)
}
return users.Group{}, errors.Wrap(errRetrieveDB, err)
}
group := toGroup(dbu)
return group, nil
}
func (gr groupRepository) RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
_, mq, err := getGroupsMetadataQuery(gm)
if err != nil {
return users.GroupPage{}, errors.Wrap(errRetrieveDB, err)
}
q := fmt.Sprintf(`WITH RECURSIVE subordinates AS (
SELECT id, owner_id, parent_id, name, description, metadata
FROM groups
WHERE id = :id
UNION
SELECT groups.id, groups.owner_id, groups.parent_id, groups.name, groups.description, groups.metadata
FROM groups
INNER JOIN subordinates s ON s.id = groups.parent_id %s
) SELECT * FROM subordinates ORDER BY id LIMIT :limit OFFSET :offset`, mq)
dbPage, err := toDBGroupPage("", groupID, offset, limit, gm)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
var items []users.Group
for rows.Next() {
dbgr := dbGroup{}
if err := rows.StructScan(&dbgr); err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
gr := toGroup(dbgr)
if err != nil {
return users.GroupPage{}, err
}
items = append(items, gr)
}
cq := fmt.Sprintf(`WITH RECURSIVE subordinates AS (
SELECT id, owner_id, parent_id, name, description, metadata
FROM groups
WHERE id = :id
UNION
SELECT groups.id, groups.owner_id, groups.parent_id, groups.name, groups.description, groups.metadata
FROM groups
INNER JOIN subordinates s ON s.id = groups.parent_id %s
) SELECT COUNT(*) FROM subordinates`, mq)
total, err := total(ctx, gr.db, cq, dbPage)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
page := users.GroupPage{
Groups: items,
PageMetadata: users.PageMetadata{
Total: total,
Offset: offset,
Limit: limit,
},
}
return page, nil
}
func (gr groupRepository) Memberships(ctx context.Context, userID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
m, mq, err := getGroupsMetadataQuery(gm)
if err != nil {
return users.GroupPage{}, errors.Wrap(errRetrieveDB, err)
}
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
FROM group_relations gr, groups g
WHERE gr.group_id = g.id and gr.user_id = :userID
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
params := map[string]interface{}{
"userID": userID,
"limit": limit,
"offset": offset,
"metadata": m,
}
rows, err := gr.db.NamedQueryContext(ctx, q, params)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
var items []users.Group
for rows.Next() {
dbgr := dbGroup{}
if err := rows.StructScan(&dbgr); err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
gr := toGroup(dbgr)
if err != nil {
return users.GroupPage{}, err
}
items = append(items, gr)
}
cq := fmt.Sprintf(`SELECT COUNT(*)
FROM group_relations gr, groups g
WHERE gr.group_id = g.id and gr.user_id = :userID %s;`, mq)
total, err := total(ctx, gr.db, cq, params)
if err != nil {
return users.GroupPage{}, errors.Wrap(errSelectDb, err)
}
page := users.GroupPage{
Groups: items,
PageMetadata: users.PageMetadata{
Total: total,
Offset: offset,
Limit: limit,
},
}
return page, nil
}
func (gr groupRepository) Assign(ctx context.Context, userID, groupID string) error {
dbr, err := toDBGroupRelation(userID, groupID)
if err != nil {
return errors.Wrap(users.ErrAssignUserToGroup, err)
}
qIns := `INSERT INTO group_relations (group_id, user_id) VALUES (:group_id, :user_id)`
_, err = gr.db.NamedQueryContext(ctx, qIns, dbr)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(users.ErrMalformedEntity, err)
case errDuplicate:
return errors.Wrap(users.ErrGroupConflict, err)
case errFK:
return errors.Wrap(users.ErrNotFound, err)
}
}
return errors.Wrap(users.ErrAssignUserToGroup, err)
}
return nil
}
func (gr groupRepository) Unassign(ctx context.Context, userID, groupID string) error {
q := `DELETE FROM group_relations WHERE user_id = :user_id AND group_id = :group_id`
dbr, err := toDBGroupRelation(userID, groupID)
if err != nil {
return errors.Wrap(users.ErrNotFound, err)
}
if _, err := gr.db.NamedExecContext(ctx, q, dbr); err != nil {
return errors.Wrap(users.ErrConflict, err)
}
return nil
}
type dbGroup struct {
ID string `db:"id"`
Name string `db:"name"`
OwnerID uuid.NullUUID `db:"owner_id"`
ParentID uuid.NullUUID `db:"parent_id"`
Description string `db:"description"`
Metadata dbMetadata `db:"metadata"`
}
type dbGroupPage struct {
ID uuid.NullUUID `db:"id"`
OwnerID uuid.NullUUID `db:"owner_id"`
ParentID uuid.NullUUID `db:"parent_id"`
Metadata dbMetadata `db:"metadata"`
Limit uint64
Offset uint64
Size uint64
}
func toUUID(id string) (uuid.NullUUID, error) {
var parentID uuid.NullUUID
if err := parentID.Scan(id); err != nil {
if id != "" {
return parentID, err
}
if err := parentID.Scan(nil); err != nil {
return parentID, err
}
}
return parentID, nil
}
func toDBGroup(g users.Group) (dbGroup, error) {
parentID := ""
if g.ParentID != "" {
parentID = g.ParentID
}
parent, err := toUUID(parentID)
if err != nil {
return dbGroup{}, err
}
owner, err := toUUID(g.OwnerID)
if err != nil {
return dbGroup{}, err
}
return dbGroup{
ID: g.ID,
Name: g.Name,
ParentID: parent,
OwnerID: owner,
Description: g.Description,
Metadata: g.Metadata,
}, nil
}
func toDBGroupPage(ownerID, groupID string, offset, limit uint64, metadata users.Metadata) (dbGroupPage, error) {
owner, err := toUUID(ownerID)
if err != nil {
return dbGroupPage{}, err
}
group, err := toUUID(groupID)
if err != nil {
return dbGroupPage{}, err
}
if err != nil {
return dbGroupPage{}, err
}
return dbGroupPage{
ID: group,
Metadata: dbMetadata(metadata),
OwnerID: owner,
Offset: offset,
Limit: limit,
}, nil
}
func toGroup(dbu dbGroup) users.Group {
return users.Group{
ID: dbu.ID,
Name: dbu.Name,
ParentID: dbu.ParentID.UUID.String(),
OwnerID: dbu.OwnerID.UUID.String(),
Description: dbu.Description,
Metadata: dbu.Metadata,
}
}
type dbGroupRelation struct {
Group uuid.UUID `db:"group_id"`
User uuid.UUID `db:"user_id"`
}
func toDBGroupRelation(userID, groupID string) (dbGroupRelation, error) {
group, err := uuid.FromString(groupID)
if err != nil {
return dbGroupRelation{}, err
}
user, err := uuid.FromString(userID)
if err != nil {
return dbGroupRelation{}, err
}
return dbGroupRelation{
Group: group,
User: user,
}, nil
}
func getGroupsMetadataQuery(m users.Metadata) ([]byte, string, error) {
mq := ""
mb := []byte("{}")
if len(m) > 0 {
mq = ` AND groups.metadata @> :metadata`
b, err := json.Marshal(m)
if err != nil {
return nil, "", err
}
mb = b
}
return mb, mq, nil
}
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
rows, err := db.NamedQueryContext(ctx, query, params)
if err != nil {
return 0, err
}
defer rows.Close()
total := uint64(0)
if rows.Next() {
if err := rows.Scan(&total); err != nil {
return 0, err
}
}
return total, nil
}
+348
View File
@@ -0,0 +1,348 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres_test
import (
"context"
"fmt"
"testing"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/pkg/uuid"
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
"github.com/mainflux/mainflux/users"
"github.com/mainflux/mainflux/users/postgres"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
groupName = "Mainflux"
password = "12345678"
)
func TestGroupSave(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.NewGroupRepo(dbMiddleware)
userRepo := postgres.NewUserRepo(dbMiddleware)
uid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("user id unexpected error: %s", err))
user := users.User{
ID: uid,
Email: "TestGroupSave@mainflux.com",
Password: password,
}
_, err = userRepo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
user, err = userRepo.RetrieveByEmail(context.Background(), user.Email)
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
uid, err = uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group := users.Group{
ID: uid,
Name: "TestGroupSave",
OwnerID: user.ID,
}
cases := []struct {
desc string
group users.Group
err error
}{
{
desc: "create new group",
group: group,
err: nil,
},
{
desc: "create group that already exist",
group: group,
err: users.ErrGroupConflict,
},
{
desc: "create thing with invalid name",
group: users.Group{
Name: "x^%",
},
err: users.ErrMalformedEntity,
},
}
for _, tc := range cases {
_, err := repo.Save(context.Background(), tc.group)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestGroupRetrieveByID(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.NewGroupRepo(dbMiddleware)
userRepo := postgres.NewUserRepo(dbMiddleware)
uid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
user := users.User{
ID: uid,
Email: "TestGroupRetrieveByID@mainflux.com",
Password: password,
}
_, err = userRepo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
user, err = userRepo.RetrieveByEmail(context.Background(), user.Email)
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
gid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group1 := users.Group{
ID: gid,
Name: groupName + "TestGroupRetrieveByID1",
OwnerID: user.ID,
}
gid, err = uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group2 := users.Group{
ID: gid,
Name: groupName + "TestGroupRetrieveByID2",
OwnerID: user.ID,
}
g1, err := repo.Save(context.Background(), group1)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
g2, err := repo.Save(context.Background(), group2)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
g2.ID, err = uuidProvider.New().ID()
require.Nil(t, err, fmt.Sprintf("failed to generate id error: %s", err))
cases := []struct {
desc string
group users.Group
err error
}{
{
desc: "retrieve group for valid id",
group: g1,
err: nil,
},
{
desc: "retrieve group for invalid id",
group: g2,
err: users.ErrNotFound,
},
}
for _, tc := range cases {
_, err := repo.RetrieveByID(context.Background(), tc.group.ID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestGroupDelete(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.NewGroupRepo(dbMiddleware)
userRepo := postgres.NewUserRepo(dbMiddleware)
uid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
user := users.User{
ID: uid,
Email: "TestGroupDelete@mainflux.com",
Password: password,
}
_, err = userRepo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
user, err = userRepo.RetrieveByEmail(context.Background(), user.Email)
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
gid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group1 := users.Group{
ID: gid,
Name: groupName + "TestGroupDelete1",
OwnerID: user.ID,
}
g1, err := repo.Save(context.Background(), group1)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
err = repo.Assign(context.Background(), user.ID, g1.ID)
require.Nil(t, err, fmt.Sprintf("failed to assign user to a group: %s", err))
gid, err = uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group2 := users.Group{
ID: gid,
Name: groupName + "TestGroupDelete2",
OwnerID: user.ID,
}
g2, err := repo.Save(context.Background(), group2)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
cases := []struct {
desc string
group users.Group
err error
}{
{
desc: "delete group for existing id",
group: g2,
err: nil,
},
{
desc: "delete group for non-existing id",
group: g2,
err: users.ErrDeleteGroupMissing,
},
}
for _, tc := range cases {
err := repo.Delete(context.Background(), tc.group.ID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestAssignUser(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.NewGroupRepo(dbMiddleware)
userRepo := postgres.NewUserRepo(dbMiddleware)
uid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
user := users.User{
ID: uid,
Email: "TestAssignUser@mainflux.com",
Password: password,
}
_, err = userRepo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
user, err = userRepo.RetrieveByEmail(context.Background(), user.Email)
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
gid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group1 := users.Group{
ID: gid,
Name: groupName + "TestAssignUser1",
OwnerID: user.ID,
}
g1, err := repo.Save(context.Background(), group1)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
gid, err = uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group2 := users.Group{
ID: gid,
Name: groupName + "TestAssignUser2",
OwnerID: user.ID,
}
g2, err := repo.Save(context.Background(), group2)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
gid, err = uuidProvider.New().ID()
require.Nil(t, err, fmt.Sprintf("group id generating error: %s", err))
g3 := users.Group{
ID: gid,
}
cases := []struct {
desc string
group users.Group
err error
}{
{
desc: "assign user to existing group",
group: g1,
err: nil,
},
{
desc: "assign user to another existing group",
group: g2,
err: nil,
},
{
desc: "assign user to non existing group",
group: g3,
err: users.ErrNotFound,
},
}
for _, tc := range cases {
err := repo.Assign(context.Background(), user.ID, tc.group.ID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUnassignUser(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.NewGroupRepo(dbMiddleware)
userRepo := postgres.NewUserRepo(dbMiddleware)
uid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
user := users.User{
ID: uid,
Email: "UnassignUser1@mainflux.com",
Password: password,
}
_, err = userRepo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
user1, err := userRepo.RetrieveByEmail(context.Background(), user.Email)
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
uid, err = uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
user = users.User{
ID: uid,
Email: "UnassignUser2@mainflux.com",
Password: password,
}
_, err = userRepo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("save got unexpected error: %s", err))
user2, err := userRepo.RetrieveByEmail(context.Background(), user.Email)
require.Nil(t, err, fmt.Sprintf("retrieve got unexpected error: %s", err))
gid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("group id unexpected error: %s", err))
group1 := users.Group{
ID: gid,
Name: groupName + "UnassignUser1",
OwnerID: user.ID,
}
g1, err := repo.Save(context.Background(), group1)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
err = repo.Assign(context.Background(), user1.ID, group1.ID)
require.Nil(t, err, fmt.Sprintf("failed to assign user: %s", err))
cases := []struct {
desc string
group users.Group
user users.User
err error
}{
{desc: "remove user from a group", group: g1, user: user1, err: nil},
{desc: "remove already removed user from a group", group: g1, user: user1, err: nil},
{desc: "remove non existing user from a group", group: g1, user: user2, err: nil},
}
for _, tc := range cases {
err := repo.Unassign(context.Background(), tc.user.ID, tc.group.ID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
+34 -4
View File
@@ -43,14 +43,16 @@ func Connect(cfg Config) (*sqlx.DB, error) {
}
func migrateDB(db *sqlx.DB) error {
migrations := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "users_1",
Up: []string{
`CREATE TABLE IF NOT EXISTS users (
email VARCHAR(254) PRIMARY KEY,
password CHAR(60) NOT NULL)`,
email VARCHAR(254) PRIMARY KEY,
password CHAR(60) NOT NULL
)`,
},
Down: []string{"DROP TABLE users"},
},
@@ -64,8 +66,36 @@ func migrateDB(db *sqlx.DB) error {
Id: "users_3",
Up: []string{
`CREATE EXTENSION IF NOT EXISTS "pgcrypto";
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS
id UUID NOT NULL DEFAULT gen_random_uuid()`,
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS
id UUID NOT NULL DEFAULT gen_random_uuid()`,
},
},
{
Id: "users_4",
Up: []string{
`ALTER TABLE IF EXISTS users DROP CONSTRAINT users_pkey`,
`ALTER TABLE IF EXISTS users ADD CONSTRAINT users_email_key UNIQUE (email)`,
`ALTER TABLE IF EXISTS users ADD PRIMARY KEY (id)`,
`CREATE TABLE IF NOT EXISTS groups (
id UUID NOT NULL,
parent_id UUID,
owner_id UUID,
name VARCHAR(254) UNIQUE NOT NULL,
description VARCHAR(1024),
metadata JSONB,
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS group_relations (
user_id UUID NOT NULL,
group_id UUID NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (user_id, group_id)
)`,
`ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS owner_id UUID`,
`ALTER TABLE IF EXISTS users ADD FOREIGN KEY (owner_id) REFERENCES groups(id)`,
},
},
},
-2
View File
@@ -17,8 +17,6 @@ import (
dockertest "github.com/ory/dockertest/v3"
)
const wrong string = "wrong-value"
var db *sqlx.DB
func TestMain(m *testing.M) {
+164 -24
View File
@@ -8,7 +8,9 @@ import (
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"github.com/lib/pq"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/users"
)
@@ -19,6 +21,8 @@ var (
errUpdateUserDB = errors.New("Update user metadata to DB failed")
errRetrieveDB = errors.New("Retreiving from DB failed")
errUpdatePasswordDB = errors.New("Update password to DB failed")
errMarshal = errors.New("Failed to marshal metadata")
errUnmarshal = errors.New("Failed to unmarshal metadata")
)
var _ users.UserRepository = (*userRepository)(nil)
@@ -31,27 +35,54 @@ type userRepository struct {
// New instantiates a PostgreSQL implementation of user
// repository.
func New(db Database) users.UserRepository {
func NewUserRepo(db Database) users.UserRepository {
return &userRepository{
db: db,
}
}
func (ur userRepository) Save(ctx context.Context, user users.User) error {
q := `INSERT INTO users (id, email, password, metadata) VALUES (:id, :email, :password, :metadata)`
dbu := toDBUser(user)
if _, err := ur.db.NamedExecContext(ctx, q, dbu); err != nil {
return errors.Wrap(errSaveUserDB, err)
func (ur userRepository) Save(ctx context.Context, user users.User) (string, error) {
q := `INSERT INTO users (email, password, id, metadata) VALUES (:email, :password, :id, :metadata) RETURNING id`
if user.ID == "" || user.Email == "" {
return "", users.ErrMalformedEntity
}
return nil
dbu, err := toDBUser(user)
if err != nil {
return "", errors.Wrap(errSaveUserDB, err)
}
row, err := ur.db.NamedQueryContext(ctx, q, dbu)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return "", errors.Wrap(users.ErrMalformedEntity, err)
case errDuplicate:
return "", errors.Wrap(users.ErrConflict, err)
}
}
return "", errors.Wrap(errSaveUserDB, err)
}
defer row.Close()
row.Next()
var id string
if err := row.Scan(&id); err != nil {
return "", err
}
return id, nil
}
func (ur userRepository) Update(ctx context.Context, user users.User) error {
q := `UPDATE users SET(email, password, metadata) VALUES (:email, :password, :metadata) WHERE email = :email`
dbu := toDBUser(user)
dbu, err := toDBUser(user)
if err != nil {
return errors.Wrap(errUpdateDB, err)
}
if _, err := ur.db.NamedExecContext(ctx, q, dbu); err != nil {
return errors.Wrap(errUpdateDB, err)
}
@@ -62,7 +93,11 @@ func (ur userRepository) Update(ctx context.Context, user users.User) error {
func (ur userRepository) UpdateUser(ctx context.Context, user users.User) error {
q := `UPDATE users SET metadata = :metadata WHERE email = :email`
dbu := toDBUser(user)
dbu, err := toDBUser(user)
if err != nil {
return errors.Wrap(errUpdateUserDB, err)
}
if _, err := ur.db.NamedExecContext(ctx, q, dbu); err != nil {
return errors.Wrap(errUpdateUserDB, err)
}
@@ -76,6 +111,7 @@ func (ur userRepository) RetrieveByEmail(ctx context.Context, email string) (use
dbu := dbUser{
Email: email,
}
if err := ur.db.QueryRowxContext(ctx, q, email).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return users.User{}, errors.Wrap(users.ErrNotFound, err)
@@ -84,9 +120,25 @@ func (ur userRepository) RetrieveByEmail(ctx context.Context, email string) (use
return users.User{}, errors.Wrap(errRetrieveDB, err)
}
user := toUser(dbu)
return toUser(dbu)
}
return user, nil
func (ur userRepository) RetrieveByID(ctx context.Context, id string) (users.User, error) {
q := `SELECT id, password, metadata FROM users WHERE id = $1`
dbu := dbUser{
ID: id,
}
if err := ur.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return users.User{}, errors.Wrap(users.ErrNotFound, err)
}
return users.User{}, errors.Wrap(errRetrieveDB, err)
}
return toUser(dbu)
}
func (ur userRepository) UpdatePassword(ctx context.Context, email, password string) error {
@@ -104,19 +156,75 @@ func (ur userRepository) UpdatePassword(ctx context.Context, email, password str
return nil
}
func (ur userRepository) Members(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.UserPage, error) {
m, mq, err := getUsersMetadataQuery(gm)
if err != nil {
return users.UserPage{}, errors.Wrap(errRetrieveDB, err)
}
q := fmt.Sprintf(`SELECT u.id, u.email, u.metadata FROM users u, group_relations g
WHERE u.id = g.user_id AND g.group_id = :group
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
params := map[string]interface{}{
"group": groupID,
"limit": limit,
"offset": offset,
"metadata": m,
}
rows, err := ur.db.NamedQueryContext(ctx, q, params)
if err != nil {
return users.UserPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
var items []users.User
for rows.Next() {
dbusr := dbUser{}
if err := rows.StructScan(&dbusr); err != nil {
return users.UserPage{}, errors.Wrap(errSelectDb, err)
}
user, err := toUser(dbusr)
if err != nil {
return users.UserPage{}, err
}
items = append(items, user)
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM users u, group_relations g
WHERE u.id = g.user_id AND g.group_id = :group %s;`, mq)
total, err := total(ctx, ur.db, cq, params)
if err != nil {
return users.UserPage{}, errors.Wrap(errSelectDb, err)
}
page := users.UserPage{
Users: items,
PageMetadata: users.PageMetadata{
Total: total,
Offset: offset,
Limit: limit,
},
}
return page, nil
}
// dbMetadata type for handling metadata properly in database/sql
type dbMetadata map[string]interface{}
// Scan - Implement the database/sql scanner interface
func (m *dbMetadata) Scan(value interface{}) error {
if value == nil {
m = nil
return nil
}
b, ok := value.([]byte)
if !ok {
m = &dbMetadata{}
return users.ErrScanMetadata
}
@@ -141,26 +249,58 @@ func (m dbMetadata) Value() (driver.Value, error) {
}
type dbUser struct {
ID string `db:"id"`
Email string `db:"email"`
Password string `db:"password"`
Metadata dbMetadata `db:"metadata"`
ID string `db:"id"`
Owner string `db:"owner"`
Email string `db:"email"`
Password string `db:"password"`
Metadata []byte `db:"metadata"`
}
func toDBUser(u users.User) dbUser {
func toDBUser(u users.User) (dbUser, error) {
data := []byte("{}")
if len(u.Metadata) > 0 {
b, err := json.Marshal(u.Metadata)
if err != nil {
return dbUser{}, errors.Wrap(errMarshal, err)
}
data = b
}
return dbUser{
ID: u.ID,
Email: u.Email,
Password: u.Password,
Metadata: u.Metadata,
}
Metadata: data,
}, nil
}
func toUser(dbu dbUser) users.User {
func toUser(dbu dbUser) (users.User, error) {
var metadata map[string]interface{}
if dbu.Metadata != nil {
if err := json.Unmarshal([]byte(dbu.Metadata), &metadata); err != nil {
return users.User{}, errors.Wrap(errUnmarshal, err)
}
}
return users.User{
ID: dbu.ID,
Email: dbu.Email,
Password: dbu.Password,
Metadata: dbu.Metadata,
}
Metadata: metadata,
}, nil
}
func getUsersMetadataQuery(m users.Metadata) ([]byte, string, error) {
mq := ""
mb := []byte("{}")
if len(m) > 0 {
mq = ` AND users.metadata @> :metadata`
b, err := json.Marshal(m)
if err != nil {
return nil, "", err
}
mb = b
}
return mb, mq, nil
}
+65 -4
View File
@@ -48,17 +48,17 @@ func TestUserSave(t *testing.T) {
}
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
repo := postgres.NewUserRepo(dbMiddleware)
for _, tc := range cases {
err := repo.Save(context.Background(), tc.user)
_, err := repo.Save(context.Background(), tc.user)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestSingleUserRetrieval(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
repo := postgres.New(dbMiddleware)
repo := postgres.NewUserRepo(dbMiddleware)
email := "user-retrieval@example.com"
@@ -71,7 +71,7 @@ func TestSingleUserRetrieval(t *testing.T) {
Password: "pass",
}
err = repo.Save(context.Background(), user)
_, err = repo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
cases := map[string]struct {
@@ -87,3 +87,64 @@ func TestSingleUserRetrieval(t *testing.T) {
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestMembers(t *testing.T) {
dbMiddleware := postgres.NewDatabase(db)
groupRepo := postgres.NewGroupRepo(dbMiddleware)
userRepo := postgres.NewUserRepo(dbMiddleware)
var nUsers = uint64(10)
var usrs []users.User
for i := uint64(0); i < nUsers; i++ {
uid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
email := fmt.Sprintf("retrieve-all-for-group%d@example.com", i)
user := users.User{
ID: uid,
Email: email,
Password: "pass",
}
_, err = userRepo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("saving user error: %s", err))
u, _ := userRepo.RetrieveByEmail(context.Background(), user.Email)
usrs = append(usrs, u)
}
uid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("user uuid error: %s", err))
group := users.Group{
ID: uid,
Name: "TestMembers",
}
g, err := groupRepo.Save(context.Background(), group)
require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err))
for _, u := range usrs {
err := groupRepo.Assign(context.Background(), u.ID, g.ID)
require.Nil(t, err, fmt.Sprintf("group user assign got unexpected error: %s", err))
}
cases := map[string]struct {
group string
offset uint64
limit uint64
size uint64
total uint64
metadata users.Metadata
}{
"retrieve all users for existing group": {
group: g.ID,
offset: 0,
limit: nUsers,
size: nUsers,
total: nUsers,
},
}
for desc, tc := range cases {
page, err := userRepo.Members(context.Background(), tc.group, tc.offset, tc.limit, tc.metadata)
size := uint64(len(usrs))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.total, page.Total, fmt.Sprintf("%s: expected total %d got %d\n", desc, tc.total, page.Total))
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
}
}
+173 -24
View File
@@ -5,6 +5,7 @@ package users
import (
"context"
"regexp"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/authn"
@@ -13,10 +14,15 @@ import (
)
var (
groupRegexp = regexp.MustCompile("^[a-zA-Z0-9]+$")
// ErrConflict indicates usage of the existing email during account
// registration.
ErrConflict = errors.New("email already taken")
// ErrGroupConflict indicates group name already taken.
ErrGroupConflict = errors.New("group already exists")
// ErrMalformedEntity indicates malformed entity specification
// (e.g. invalid username or password).
ErrMalformedEntity = errors.New("malformed entity specification")
@@ -47,8 +53,17 @@ var (
// ErrGetToken indicates error in getting signed token.
ErrGetToken = errors.New("failed to fetch signed token")
// ErrCreateUser indicates error in creating User
// ErrCreateUser indicates error in creating user.
ErrCreateUser = errors.New("failed to create user")
// ErrCreateGroup indicates error in creating group.
ErrCreateGroup = errors.New("failed to create group")
// ErrDeleteGroupMissing indicates in delete operation that group doesnt exist.
ErrDeleteGroupMissing = errors.New("group is not existing, already deleted")
// ErrAssignUserToGroup indicates an error in assigning user to a group.
ErrAssignUserToGroup = errors.New("failed assigning user to a group")
)
// Service specifies an API that must be fullfiled by the domain service
@@ -56,15 +71,15 @@ var (
type Service interface {
// Register creates new user account. In case of the failed registration, a
// non-nil error value is returned.
Register(ctx context.Context, user User) error
Register(ctx context.Context, user User) (string, error)
// Login authenticates the user given its credentials. Successful
// authentication generates new access token. Failed invocations are
// identified by the non-nil error values in the response.
Login(ctx context.Context, user User) (string, error)
// ViewUser authenticated user info for the given token.
ViewUser(ctx context.Context, token string) (User, error)
// User authenticated user info for the given token.
User(ctx context.Context, token string) (User, error)
// UpdateUser updates the user metadata.
UpdateUser(ctx context.Context, token string, user User) error
@@ -82,42 +97,94 @@ type Service interface {
//SendPasswordReset sends reset password link to email.
SendPasswordReset(ctx context.Context, host, email, token string) error
// CreateGroup creates new user group.
CreateGroup(ctx context.Context, token string, group Group) (Group, error)
// UpdateGroup updates the group identified by the provided ID.
UpdateGroup(ctx context.Context, token string, group Group) error
// Group retrieves data about the group identified by ID.
Group(ctx context.Context, token, id string) (Group, error)
// ListGroups retrieves groups that are children to group identified by parenID
// if parentID is empty all groups are listed.
Groups(ctx context.Context, token, parentID string, offset, limit uint64, meta Metadata) (GroupPage, error)
// Members retrieves users that are assigned to a group identified by groupID.
Members(ctx context.Context, token, groupID string, offset, limit uint64, meta Metadata) (UserPage, error)
// Memberships retrieves groups that user identified with userID belongs to.
Memberships(ctx context.Context, token, groupID string, offset, limit uint64, meta Metadata) (GroupPage, error)
// RemoveGroup removes the group identified with the provided ID.
RemoveGroup(ctx context.Context, token, id string) error
// Assign adds user with userID into the group identified by groupID.
Assign(ctx context.Context, token, userID, groupID string) error
// Unassign removes user with userID from group identified by groupID.
Unassign(ctx context.Context, token, userID, groupID string) error
}
// PageMetadata contains page metadata that helps navigation.
type PageMetadata struct {
Total uint64
Offset uint64
Limit uint64
Name string
}
type GroupPage struct {
PageMetadata
Groups []Group
}
type UserPage struct {
PageMetadata
Users []User
}
var _ Service = (*usersService)(nil)
type usersService struct {
users UserRepository
groups GroupRepository
hasher Hasher
email Emailer
auth mainflux.AuthNServiceClient
}
// New instantiates the users service implementation
func New(users UserRepository, hasher Hasher, auth mainflux.AuthNServiceClient, m Emailer) Service {
func New(users UserRepository, groups GroupRepository, hasher Hasher, auth mainflux.AuthNServiceClient, m Emailer) Service {
return &usersService{
users: users,
groups: groups,
hasher: hasher,
auth: auth,
email: m,
}
}
func (svc usersService) Register(ctx context.Context, user User) error {
func (svc usersService) Register(ctx context.Context, user User) (string, error) {
if err := user.Validate(); err != nil {
return "", err
}
hash, err := svc.hasher.Hash(user.Password)
if err != nil {
return errors.Wrap(ErrMalformedEntity, err)
return "", errors.Wrap(ErrMalformedEntity, err)
}
user.Password = hash
uid, err := uuidProvider.New().ID()
if err != nil {
return errors.Wrap(ErrCreateUser, err)
return "", errors.Wrap(ErrCreateUser, err)
}
user.ID = uid
return svc.users.Save(ctx, user)
uid, err = svc.users.Save(ctx, user)
if err != nil {
return "", err
}
return uid, nil
}
func (svc usersService) Login(ctx context.Context, user User) (string, error) {
@@ -125,25 +192,21 @@ func (svc usersService) Login(ctx context.Context, user User) (string, error) {
if err != nil {
return "", errors.Wrap(ErrUnauthorizedAccess, err)
}
if err := svc.hasher.Compare(user.Password, dbUser.Password); err != nil {
return "", errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.issue(ctx, dbUser.Email, authn.UserKey)
}
func (svc usersService) ViewUser(ctx context.Context, token string) (User, error) {
func (svc usersService) User(ctx context.Context, token string) (User, error) {
email, err := svc.identify(ctx, token)
if err != nil {
return User{}, err
}
dbUser, err := svc.users.RetrieveByEmail(ctx, email)
if err != nil {
return User{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
return User{
ID: dbUser.ID,
Email: email,
@@ -152,17 +215,23 @@ func (svc usersService) ViewUser(ctx context.Context, token string) (User, error
}, nil
}
func (svc usersService) ListUsers(ctx context.Context, token string, groupID string, offset, limit uint64, um Metadata) (UserPage, error) {
_, err := svc.identify(ctx, token)
if err != nil {
return UserPage{}, err
}
return svc.users.Members(ctx, groupID, offset, limit, um)
}
func (svc usersService) UpdateUser(ctx context.Context, token string, u User) error {
email, err := svc.identify(ctx, token)
if err != nil {
return errors.Wrap(ErrUnauthorizedAccess, err)
}
user := User{
Email: email,
Metadata: u.Metadata,
}
return svc.users.UpdateUser(ctx, user)
}
@@ -171,7 +240,6 @@ func (svc usersService) GenerateResetToken(ctx context.Context, email, host stri
if err != nil || user.Email == "" {
return ErrUserNotFound
}
t, err := svc.issue(ctx, email, authn.RecoveryKey)
if err != nil {
return errors.Wrap(ErrRecoveryToken, err)
@@ -184,12 +252,10 @@ func (svc usersService) ResetPassword(ctx context.Context, resetToken, password
if err != nil {
return errors.Wrap(ErrUnauthorizedAccess, err)
}
u, err := svc.users.RetrieveByEmail(ctx, email)
if err != nil || u.Email == "" {
return ErrUserNotFound
}
password, err = svc.hasher.Hash(password)
if err != nil {
return err
@@ -202,7 +268,6 @@ func (svc usersService) ChangePassword(ctx context.Context, authToken, password,
if err != nil {
return errors.Wrap(ErrUnauthorizedAccess, err)
}
u := User{
Email: email,
Password: oldPassword,
@@ -210,7 +275,6 @@ func (svc usersService) ChangePassword(ctx context.Context, authToken, password,
if _, err := svc.Login(ctx, u); err != nil {
return ErrUnauthorizedAccess
}
u, err = svc.users.RetrieveByEmail(ctx, email)
if err != nil || u.Email == "" {
return ErrUserNotFound
@@ -237,6 +301,83 @@ func (svc usersService) identify(ctx context.Context, token string) (string, err
return email.GetValue(), nil
}
func (svc usersService) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
if group.Name == "" || !groupRegexp.MatchString(group.Name) {
return Group{}, ErrMalformedEntity
}
userID, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
user, err := svc.users.RetrieveByEmail(ctx, userID.Value)
if err != nil {
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
uid, err := uuidProvider.New().ID()
if err != nil {
return Group{}, errors.Wrap(ErrCreateUser, err)
}
group.ID = uid
group.OwnerID = user.ID
return svc.groups.Save(ctx, group)
}
func (svc usersService) Groups(ctx context.Context, token string, parentID string, offset, limit uint64, meta Metadata) (GroupPage, error) {
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.groups.RetrieveAllWithAncestors(ctx, parentID, offset, limit, meta)
}
func (svc usersService) Members(ctx context.Context, token, groupID string, offset, limit uint64, meta Metadata) (UserPage, error) {
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return UserPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.users.Members(ctx, groupID, offset, limit, meta)
}
func (svc usersService) RemoveGroup(ctx context.Context, token, id string) error {
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.groups.Delete(ctx, id)
}
func (svc usersService) Unassign(ctx context.Context, token, userID, groupID string) error {
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.groups.Unassign(ctx, userID, groupID)
}
func (svc usersService) UpdateGroup(ctx context.Context, token string, group Group) error {
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.groups.Update(ctx, group)
}
func (svc usersService) Group(ctx context.Context, token, id string) (Group, error) {
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.groups.RetrieveByID(ctx, id)
}
func (svc usersService) Assign(ctx context.Context, token, userID, groupID string) error {
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.groups.Assign(ctx, userID, groupID)
}
func (svc usersService) issue(ctx context.Context, email string, keyType uint32) (string, error) {
key, err := svc.auth.Issue(ctx, &mainflux.IssueReq{Issuer: email, Type: keyType})
if err != nil {
@@ -244,3 +385,11 @@ func (svc usersService) issue(ctx context.Context, email string, keyType uint32)
}
return key.GetValue(), nil
}
func (svc usersService) Memberships(ctx context.Context, token, userID string, offset, limit uint64, meta Metadata) (GroupPage, error) {
_, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
}
return svc.groups.Memberships(ctx, userID, offset, limit, meta)
}
+154 -15
View File
@@ -10,6 +10,7 @@ import (
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/pkg/errors"
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
"github.com/mainflux/mainflux/users"
"github.com/mainflux/mainflux/users/mocks"
@@ -23,15 +24,17 @@ var (
user = users.User{Email: "user@example.com", Password: "password", Metadata: map[string]interface{}{"role": "user"}}
nonExistingUser = users.User{Email: "non-ex-user@example.com", Password: "password", Metadata: map[string]interface{}{"role": "user"}}
host = "example.com"
groupName = "Mainflux"
)
func newService() users.Service {
repo := mocks.NewUserRepository()
userRepo := mocks.NewUserRepository()
groupRepo := mocks.NewGroupRepository()
hasher := mocks.NewHasher()
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
e := mocks.NewEmailer()
return users.New(repo, hasher, auth, e)
return users.New(userRepo, groupRepo, hasher, auth, e)
}
func TestRegister(t *testing.T) {
@@ -63,20 +66,20 @@ func TestRegister(t *testing.T) {
}
for _, tc := range cases {
err := svc.Register(context.Background(), tc.user)
_, err := svc.Register(context.Background(), tc.user)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestLogin(t *testing.T) {
svc := newService()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
noAuthUser := users.User{
Email: "email@test.com",
Password: "pwd",
Password: "12345678",
}
svc.Register(context.Background(), user)
svc.Register(context.Background(), noAuthUser)
cases := map[string]struct {
user users.User
@@ -112,9 +115,10 @@ func TestLogin(t *testing.T) {
}
}
func TestViewUser(t *testing.T) {
func TestUser(t *testing.T) {
svc := newService()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
token, err := svc.Login(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
@@ -140,14 +144,17 @@ func TestViewUser(t *testing.T) {
}
for desc, tc := range cases {
_, err := svc.ViewUser(context.Background(), tc.token)
_, err := svc.User(context.Background(), tc.token)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateUser(t *testing.T) {
svc := newService()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
token, err := svc.Login(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
@@ -178,7 +185,8 @@ func TestUpdateUser(t *testing.T) {
func TestGenerateResetToken(t *testing.T) {
svc := newService()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
cases := map[string]struct {
email string
@@ -196,7 +204,8 @@ func TestGenerateResetToken(t *testing.T) {
func TestChangePassword(t *testing.T) {
svc := newService()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("register user error: %s", err))
token, _ := svc.Login(context.Background(), user)
cases := map[string]struct {
@@ -219,7 +228,8 @@ func TestChangePassword(t *testing.T) {
func TestResetPassword(t *testing.T) {
svc := newService()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
auth := mocks.NewAuthService(map[string]string{user.Email: user.Email})
resetToken, err := auth.Issue(context.Background(), &mainflux.IssueReq{Issuer: user.Email, Type: 2})
assert.Nil(t, err, fmt.Sprintf("Generating reset token expected to succeed: %s", err))
@@ -240,7 +250,8 @@ func TestResetPassword(t *testing.T) {
func TestSendPasswordReset(t *testing.T) {
svc := newService()
svc.Register(context.Background(), user)
_, err := svc.Register(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("register user error: %s", err))
token, _ := svc.Login(context.Background(), user)
cases := map[string]struct {
@@ -257,3 +268,131 @@ func TestSendPasswordReset(t *testing.T) {
}
}
func TestCreateGroup(t *testing.T) {
svc := newService()
_, err := svc.Register(context.Background(), user)
assert.Nil(t, err, fmt.Sprintf("registering user expected to succeed: %s", err))
token, err := svc.Login(context.Background(), user)
assert.Nil(t, err, fmt.Sprintf("authenticating user expected to succeed: %s", err))
uuid, err := uuidProvider.New().ID()
assert.Nil(t, err, fmt.Sprintf("generating uuid expected to succeed: %s", err))
group := users.Group{
ID: uuid,
Name: groupName,
}
cases := []struct {
desc string
group users.Group
err error
}{
{
desc: "create new group",
group: group,
err: nil,
},
{
desc: "create group with existing name",
group: group,
err: users.ErrGroupConflict,
},
}
for _, tc := range cases {
_, err := svc.CreateGroup(context.Background(), token, tc.group)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateGroup(t *testing.T) {
svc := newService()
_, err := svc.Register(context.Background(), user)
assert.Nil(t, err, fmt.Sprintf("registering user expected to succeed: %s", err))
token, err := svc.Login(context.Background(), user)
assert.Nil(t, err, fmt.Sprintf("authenticating user expected to succeed: %s", err))
group := users.Group{
Name: groupName,
}
saved, err := svc.CreateGroup(context.Background(), token, group)
assert.Nil(t, err, fmt.Sprintf("generating uuid expected to succeed: %s", err))
group.Description = "test description"
group.Name = "NewName"
group.ID = saved.ID
group.OwnerID = saved.OwnerID
cases := []struct {
desc string
group users.Group
err error
}{
{
desc: "update group",
group: group,
err: nil,
},
}
for _, tc := range cases {
err := svc.UpdateGroup(context.Background(), token, tc.group)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
g, err := svc.Group(context.Background(), token, saved.ID)
assert.Nil(t, err, fmt.Sprintf("retrieve group failed: %s", err))
assert.Equal(t, tc.group.Description, g.Description, tc.desc, tc.err)
assert.Equal(t, tc.group.Name, g.Name, tc.desc, tc.err)
assert.Equal(t, tc.group.ID, g.ID, tc.desc, tc.err)
assert.Equal(t, tc.group.OwnerID, g.OwnerID, tc.desc, tc.err)
}
}
func TestRemoveGroup(t *testing.T) {
svc := newService()
_, err := svc.Register(context.Background(), user)
assert.Nil(t, err, fmt.Sprintf("registering user expected to succeed: %s", err))
token, err := svc.Login(context.Background(), user)
assert.Nil(t, err, fmt.Sprintf("authenticating user expected to succeed: %s", err))
group := users.Group{
Name: groupName,
}
saved, err := svc.CreateGroup(context.Background(), token, group)
assert.Nil(t, err, fmt.Sprintf("generating uuid expected to succeed: %s", err))
group.Description = "test description"
group.Name = "NewName"
group.ID = saved.ID
group.OwnerID = saved.OwnerID
cases := []struct {
desc string
group users.Group
err error
}{
{
desc: "remove existing group",
group: group,
err: nil,
},
{
desc: "remove non existing group",
group: group,
err: users.ErrNotFound,
},
}
for _, tc := range cases {
err := svc.RemoveGroup(context.Background(), token, tc.group.ID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
+344 -153
View File
@@ -1,12 +1,9 @@
swagger: "2.0"
openapi: 3.0.1
info:
title: Mainflux users service
description: HTTP API for managing platform users.
version: "1.0.0"
consumes:
- "application/json"
produces:
- "application/json"
paths:
/users:
post:
@@ -16,16 +13,19 @@ paths:
be uniquely identified by its email address.
tags:
- users
parameters:
- name: user
description: JSON-formatted document describing the new user.
in: body
schema:
$ref: "#/definitions/User"
required: true
requestBody:
$ref: "#/components/requestBodies/CreateUserReq"
responses:
201:
description: Registered new user.
headers:
Location:
content:
text/plain:
schema:
type: string
description: Registred user relative URL
example: /users/{userId}
400:
description: Failed due to malformed JSON.
409:
@@ -33,7 +33,7 @@ paths:
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
$ref: "#/components/responses/ServiceError"
get:
summary: Gets info on currently logged in user.
description: |
@@ -41,19 +41,21 @@ paths:
authorization token
tags:
- users
parameters:
- $ref: "#/parameters/Authorization"
security:
- Authorization: []
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/UsersPage"
content:
application/json:
schema:
$ref: "#/components/schemas/UserRes"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
$ref: "#/components/responses/ServiceError"
put:
summary: Updates info on currently logged in user.
description: |
@@ -61,14 +63,10 @@ paths:
authorization token and the new received info.
tags:
- users
parameters:
- $ref: "#/parameters/Authorization"
- name: metadata
description: JSON-formatted document containing user info.
in: body
schema:
$ref: "#/definitions/updateUserReq"
required: true
security:
- Authorization: []
requestBody:
$ref: "#/components/requestBodies/UpdateUserReq"
responses:
200:
description: User updated.
@@ -77,7 +75,72 @@ paths:
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
$ref: "#/components/responses/ServiceError"
/users/{userId}/groups:
get:
summary: Get groups that user belongs to
description: |
Retrieves a list of groups that user belongs to.
tags:
- users
security:
- Authorization: []
parameters:
- $ref: "#/parameters/UserID"
- $ref: "#/parameters/Offset"
- $ref: "#/parameters/Limit"
responses:
200:
description: Data retrieved.
content:
application/json:
schema:
$ref: '#/components/schemas/GroupsPage'
403:
description: Missing or invalid access token provided.
500:
$ref: '#/components/responses/ServiceError'
/groups:
post:
summary: Create users group
description: |
Create users group.
tags:
- groups
security:
- Authorization: []
requestBody:
$ref: '#/components/requestBodies/CreateGroupReq'
responses:
200:
description: Group created.
content:
application/json:
schema:
$ref: '#/components/schemas/GroupsRes'
403:
description: Missing or invalid access token provided.
500:
$ref: '#/components/responses/ServiceError'
get:
summary: Get users groups
description: |
Get all users groups
tags:
- groups
security:
- Authorization: []
responses:
200:
description: Groups retrieved.
content:
application/json:
schema:
$ref: '#/components/schemas/GroupsPage'
403:
description: Missing or invalid access token provided.
500:
$ref: '#/components/responses/ServiceError'
/tokens:
post:
summary: User authentication
@@ -85,34 +148,37 @@ paths:
Generates an access token when provided with proper credentials.
tags:
- users
parameters:
- name: credentials
description: JSON-formatted document containing user credentials.
in: body
schema:
$ref: "#/definitions/User"
required: true
security:
- Authorization: []
responses:
201:
description: User authenticated.
schema:
$ref: "#/definitions/Token"
content:
application/json:
schema:
$ref: '#/components/schemas/Token'
400:
description: |
Failed due to malformed JSON.
schema:
$ref: "#/definitions/Error"
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
403:
description: |
Failed due to using invalid credentials.
schema:
$ref: "#/definitions/Error"
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
415:
description: Missing or invalid content type.
schema:
$ref: "#/definitions/Error"
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
500:
$ref: "#/responses/ServiceError"
$ref: '#/components/responses/ServiceError'
/password/reset-request:
post:
summary: User password reset request
@@ -122,37 +188,27 @@ paths:
- users
parameters:
- $ref: "#/parameters/Referer"
- $ref: "#/parameters/Authorization"
- name: email
description: JSON-formatted document containing user email.
in: body
schema:
$ref: "#/definitions/PasswordResetRequest"
required: true
requestBody:
$ref: '#/components/requestBodies/RequestPasswordReset'
responses:
201:
description: User link .
description: Users link for reseting password.
400:
description: |
Failed due to malformed JSON.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
$ref: '#/components/responses/ServiceError'
/password/reset:
put:
summary: User password reset endpoint
description: |
When user gets reset token posting a new password along to this endpoint will change password.
When user gets reset token, after he submited email to `/password/reset-request`, posting a new password along to this endpoint will change password.
tags:
- users
parameters:
- name: password
description: JSON-formatted document containing user email, token and new password.
in: body
schema:
$ref: "#/definitions/PasswordReset"
required: true
requestBody:
$ref: '#/components/requestBodies/PasswordReset'
responses:
201:
description: User link .
@@ -162,7 +218,7 @@ paths:
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
$ref: '#/components/responses/ServiceError'
/password:
patch:
summary: User password change endpoint
@@ -170,14 +226,10 @@ paths:
When authenticated user wants to change password.
tags:
- users
parameters:
- $ref: "#/parameters/Authorization"
- name: password
description: JSON-formatted document containing user email, token and new password.
in: body
schema:
$ref: "#/definitions/PasswordChange"
required: true
security:
- Authorization: []
requestBody:
$ref: '#/components/requestBodies/PasswordChange'
responses:
201:
description: User link .
@@ -187,92 +239,204 @@ paths:
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
responses:
ServiceError:
description: Unexpected server-side error occurred.
definitions:
Token:
type: object
properties:
token:
type: string
description: Generated access token.
required:
- token
User:
type: object
properties:
email:
type: string
format: email
example: "test@example.com"
description: User's email address will be used as its unique identifier
password:
type: string
format: password
minimum: 8
description: Free-form account password used for acquiring auth token(s).
required:
- email
- password
UsersPage:
type: object
properties:
email:
type: string
description: ID of the user
metadata:
type: object
description: Custom metadata related to User
updateUserReq:
type: object
description: Arbitrary, object-encoded user's data.
PasswordResetRequest:
type: object
properties:
email:
type: string
description: Email of the user
PasswordReset:
type: object
properties:
password:
type: string
description: New password
minimum: 8
confirm_password:
type: string
description: New password confirmed
minimum: 8
token:
type: string
description: Reset token generated and sent in email
PasswordChange:
type: object
properties:
password:
type: string
description: New password
old_password:
type: string
description: Confirm password
Error:
type: object
properties:
error:
type: string
description: Error message
$ref: "#/components/responses/ServiceError"
components:
securitySchemes:
Authorization:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Token:
type: object
properties:
token:
type: string
description: Generated access token.
required:
- token
User:
type: object
properties:
email:
type: string
format: email
example: "test@example.com"
description: User's email address will be used as its unique identifier
password:
type: string
format: password
minimum: 8
description: Free-form account password used for acquiring auth token(s).
required:
- email
- password
Group:
type: object
properties:
name:
type: string
description: Unique name of the group. Group name matching `"^[a-zA-Z0-9]+$"` regexp.
parent_id:
type: string
format: uuid
description: Id of parent group
description:
type: string
description: Description of group
metadata:
type: object
description: Arbitrary, object-encoded thing's data.
required:
- name
UserRes:
type: object
properties:
id:
type: string
format: uuid
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
description: UUID id of the user.
email:
type: string
format: email
example: "test@example.com"
description: User's email address will be used as its unique identifier
metadata:
type: string
format: JSON
description: Users metadata
GroupsRes:
type: object
properties:
id:
type: string
format: uuid
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
description: UUID id of the group.
name:
type: string
example: "MainflxGroup"
description: Group name matching `"^[a-zA-Z0-9]+$"` regexp.
description:
type: string
description: Description free form text describing a group
metadata:
type: object
description: Groups metadata
UsersPage:
type: object
properties:
email:
type: string
description: ID of the user
metadata:
type: object
description: Custom metadata related to User
UserMetadata:
type: object
properties:
metadata:
type: string
description: Users metadata
GroupsPage:
type: object
properties:
groups:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/components/schemas/GroupsRes"
total:
type: integer
description: Total number of items.
offset:
type: integer
description: Number of items to skip during retrieval.
limit:
type: integer
description: Maximum number of items to return in one page.
Error:
type: object
properties:
error:
type: string
description: Error message
requestBodies:
CreateUserReq:
description: JSON-formatted document describing the new user to be registered
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
UpdateUserReq:
description: JSON-formated document describing the metadata of user to be update
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserMetadata"
CreateGroupReq:
description: JSON-formated document describing the new group to be created.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Group'
RequestPasswordReset:
description: Initiate password request procedure.
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
description: Email of the user
PasswordReset:
description: Password reset request data, new password and token that is appended on password reset link received in email.
content:
application/json:
schema:
type: object
properties:
password:
type: string
description: New password
minimum: 8
confirm_password:
type: string
description: New password confirmed
minimum: 8
token:
type: string
description: Reset token generated and sent in email
PasswordChange:
description: Password change data. User can change its password.
required: true
content:
application/json:
schema:
type: object
properties:
password:
type: string
format: pass
description: New password
old_password:
type: string
description: Confirm password
responses:
ServiceError:
description: Unexpected server-side error occurred.
parameters:
Authorization:
name: Authorization
description: User's access token.
in: header
type: string
required: true
Referer:
name: Referer
description: Host being sent by browser.
@@ -286,3 +450,30 @@ parameters:
type: string
minimum: 0
required: false
UserID:
name: userId
description: Unique user identifier.
in: path
schema:
type: string
format: UUID
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
+110
View File
@@ -0,0 +1,110 @@
// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
// Package tracing contains middlewares that will add spans
// to existing traces.
package tracing
import (
"context"
"github.com/mainflux/mainflux/users"
opentracing "github.com/opentracing/opentracing-go"
)
const (
assignUser = "assign_user"
saveGroup = "save_group"
deleteGroup = "delete_group"
updateGroup = "update_group"
retrieveGroupByID = "retrieve_group_by_id"
retrieveAll = "retrieve_all_groups"
retrieveByName = "retrieve_by_name"
memberships = "memberships"
unassignUser = "unassign_user"
)
var _ users.GroupRepository = (*groupRepositoryMiddleware)(nil)
type groupRepositoryMiddleware struct {
tracer opentracing.Tracer
repo users.GroupRepository
}
// GroupRepositoryMiddleware tracks request and their latency, and adds spans to context.
func GroupRepositoryMiddleware(repo users.GroupRepository, tracer opentracing.Tracer) users.GroupRepository {
return groupRepositoryMiddleware{
tracer: tracer,
repo: repo,
}
}
func (grm groupRepositoryMiddleware) Save(ctx context.Context, group users.Group) (users.Group, error) {
span := createSpan(ctx, grm.tracer, saveGroup)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Save(ctx, group)
}
func (grm groupRepositoryMiddleware) Update(ctx context.Context, group users.Group) error {
span := createSpan(ctx, grm.tracer, updateGroup)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Update(ctx, group)
}
func (grm groupRepositoryMiddleware) Delete(ctx context.Context, groupID string) error {
span := createSpan(ctx, grm.tracer, deleteGroup)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Delete(ctx, groupID)
}
func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (users.Group, error) {
span := createSpan(ctx, grm.tracer, retrieveGroupByID)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.RetrieveByID(ctx, id)
}
func (grm groupRepositoryMiddleware) RetrieveByName(ctx context.Context, name string) (users.Group, error) {
span := createSpan(ctx, grm.tracer, retrieveByName)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.RetrieveByName(ctx, name)
}
func (grm groupRepositoryMiddleware) RetrieveAllWithAncestors(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
span := createSpan(ctx, grm.tracer, retrieveAll)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.RetrieveAllWithAncestors(ctx, groupID, offset, limit, gm)
}
func (grm groupRepositoryMiddleware) Memberships(ctx context.Context, userID string, offset, limit uint64, gm users.Metadata) (users.GroupPage, error) {
span := createSpan(ctx, grm.tracer, memberships)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Memberships(ctx, userID, offset, limit, gm)
}
func (grm groupRepositoryMiddleware) Unassign(ctx context.Context, userID, groupID string) error {
span := createSpan(ctx, grm.tracer, unassignUser)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Unassign(ctx, userID, groupID)
}
func (grm groupRepositoryMiddleware) Assign(ctx context.Context, userID, groupID string) error {
span := createSpan(ctx, grm.tracer, assignUser)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return grm.repo.Assign(ctx, userID, groupID)
}
+21 -6
View File
@@ -13,11 +13,10 @@ import (
)
const (
saveOp = "save_op"
retrieveByEmailOp = "retrieve_by_email"
generateResetToken = "generate_reset_token"
updatePassword = "update_password"
sendPasswordReset = "send_reset_password"
saveOp = "save_op"
retrieveByEmailOp = "retrieve_by_email"
updatePassword = "update_password"
members = "members"
)
var _ users.UserRepository = (*userRepositoryMiddleware)(nil)
@@ -36,7 +35,7 @@ func UserRepositoryMiddleware(repo users.UserRepository, tracer opentracing.Trac
}
}
func (urm userRepositoryMiddleware) Save(ctx context.Context, user users.User) error {
func (urm userRepositoryMiddleware) Save(ctx context.Context, user users.User) (string, error) {
span := createSpan(ctx, urm.tracer, saveOp)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
@@ -60,6 +59,14 @@ func (urm userRepositoryMiddleware) RetrieveByEmail(ctx context.Context, email s
return urm.repo.RetrieveByEmail(ctx, email)
}
func (urm userRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (users.User, error) {
span := createSpan(ctx, urm.tracer, retrieveByEmailOp)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return urm.repo.RetrieveByID(ctx, id)
}
func (urm userRepositoryMiddleware) UpdatePassword(ctx context.Context, email, password string) error {
span := createSpan(ctx, urm.tracer, updatePassword)
defer span.Finish()
@@ -68,6 +75,14 @@ func (urm userRepositoryMiddleware) UpdatePassword(ctx context.Context, email, p
return urm.repo.UpdatePassword(ctx, email, password)
}
func (urm userRepositoryMiddleware) Members(ctx context.Context, groupID string, offset, limit uint64, gm users.Metadata) (users.UserPage, error) {
span := createSpan(ctx, urm.tracer, members)
defer span.Finish()
ctx = opentracing.ContextWithSpan(ctx, span)
return urm.repo.Members(ctx, groupID, offset, limit, gm)
}
func createSpan(ctx context.Context, tracer opentracing.Tracer, opName string) opentracing.Span {
if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
return tracer.StartSpan(
+15 -2
View File
@@ -28,13 +28,20 @@ var (
userDotRegexp = regexp.MustCompile("(^[.]{1})|([.]{1}$)|([.]{2,})")
)
// Metadata to be used for mainflux thing or channel for customized
// describing of particular thing or channel.
type Metadata map[string]interface{}
// User represents a Mainflux user account. Each user is identified given its
// email and password.
type User struct {
ID string
Email string
Password string
Metadata map[string]interface{}
OwnerID string
Owner *User
Groups []Group
Metadata Metadata
}
// Validate returns an error if user representation is invalid.
@@ -54,7 +61,7 @@ func (u User) Validate() error {
type UserRepository interface {
// Save persists the user account. A non-nil error is returned to indicate
// operation failure.
Save(ctx context.Context, u User) error
Save(ctx context.Context, u User) (string, error)
// Update updates the user metadata.
UpdateUser(ctx context.Context, u User) error
@@ -62,8 +69,14 @@ type UserRepository interface {
// RetrieveByEmail retrieves user by its unique identifier (i.e. email).
RetrieveByEmail(ctx context.Context, email string) (User, error)
// RetrieveByID retrieves user by its unique identifier ID.
RetrieveByID(ctx context.Context, id string) (User, error)
// UpdatePassword updates password for user with given email
UpdatePassword(ctx context.Context, email, password string) error
// Members retrieves all users that belong to a group
Members(ctx context.Context, groupID string, offset, limit uint64, um Metadata) (UserPage, error)
}
func isEmail(email string) bool {
-1
View File
@@ -16,7 +16,6 @@ import (
const (
email = "user@example.com"
password = "password"
metadata = `{"role":"manager"}`
maxLocalLen = 64
maxDomainLen = 255