mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 != "" {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,6 +16,7 @@ func TestVersion(t *testing.T) {
|
||||
sdkConf := sdk.Config{
|
||||
BaseURL: ts.URL,
|
||||
UsersPrefix: "",
|
||||
GroupsPrefix: "",
|
||||
ThingsPrefix: "",
|
||||
HTTPAdapterPrefix: "",
|
||||
MsgContentType: contentType,
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, "")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
const (
|
||||
email = "user@example.com"
|
||||
password = "password"
|
||||
metadata = `{"role":"manager"}`
|
||||
|
||||
maxLocalLen = 64
|
||||
maxDomainLen = 255
|
||||
|
||||
Reference in New Issue
Block a user