1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-26 13:48:53 +08:00

MF-858 Users metadata (#861)

* add users metadata

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

* add users metadata

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

* add metadata to users

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

* add metadata to users

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

* run.sh

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

* add metadata to users

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

* add default value for metadata

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

* add default value for metadata

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

* when metadata is not set dont save 'null' string

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

* when metadata is not set dont save 'null' string

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

* change metadata type, add error handling

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

* add pause

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

* remove extra char

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

* retype from string to []byte

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

* add wait logic for gnatsd

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

* few small fixes

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

* fix identityRes

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

* add users metadata

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

* add users metadata

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

* revert run.sh for now as gnats availability check is solved in other PR

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

* revert changes

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

* change metadata database/sql handling

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

* fix commit issues

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

* small change to errors handling

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

* minor comment change

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
This commit is contained in:
Mirko Teodorovic 2019-09-28 11:15:41 +00:00 committed by Drasko DRASKOVIC
parent 873ef4c96f
commit 92a640f6fc
12 changed files with 166 additions and 9 deletions

View File

@ -7,9 +7,7 @@
package grpc
import (
"github.com/mainflux/mainflux/users"
)
import "github.com/mainflux/mainflux/users"
type identityReq struct {
token string

View File

@ -27,6 +27,23 @@ func registrationEndpoint(svc users.Service) endpoint.Endpoint {
}
}
func userInfoEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(viewUserInfoReq)
if err := req.validate(); err != nil {
return nil, err
}
u, err := svc.UserInfo(ctx, req.token)
if err != nil {
return nil, err
}
return identityRes{u.Email, u.Metadata}, nil
}
}
func loginEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(userReq)

View File

@ -20,3 +20,14 @@ type userReq struct {
func (req userReq) validate() error {
return req.user.Validate()
}
type viewUserInfoReq struct {
token string
}
func (req viewUserInfoReq) validate() error {
if req.token == "" {
return users.ErrUnauthorizedAccess
}
return nil
}

View File

@ -13,7 +13,10 @@ import (
"github.com/mainflux/mainflux"
)
var _ mainflux.Response = (*tokenRes)(nil)
var (
_ mainflux.Response = (*tokenRes)(nil)
_ mainflux.Response = (*identityRes)(nil)
)
type tokenRes struct {
Token string `json:"token,omitempty"`
@ -30,3 +33,20 @@ func (res tokenRes) Headers() map[string]string {
func (res tokenRes) Empty() bool {
return res.Token == ""
}
type identityRes struct {
Email string `json:"email"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (res identityRes) Code() int {
return http.StatusOK
}
func (res identityRes) Headers() map[string]string {
return map[string]string{}
}
func (res identityRes) Empty() bool {
return false
}

View File

@ -50,6 +50,13 @@ func MakeHandler(svc users.Service, tracer opentracing.Tracer, l log.Logger) htt
opts...,
))
mux.Get("/users", kithttp.NewServer(
kitot.TraceServer(tracer, "register")(userInfoEndpoint(svc)),
decodeViewInfo,
encodeResponse,
opts...,
))
mux.Post("/tokens", kithttp.NewServer(
kitot.TraceServer(tracer, "login")(loginEndpoint(svc)),
decodeCredentials,
@ -63,6 +70,13 @@ func MakeHandler(svc users.Service, tracer opentracing.Tracer, l log.Logger) htt
return mux
}
func decodeViewInfo(_ context.Context, r *http.Request) (interface{}, error) {
req := viewUserInfoReq{
token: r.Header.Get("Authorization"),
}
return req, nil
}
func decodeCredentials(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
logger.Warn("Invalid or missing content type.")

View File

@ -57,7 +57,7 @@ func (lm *loggingMiddleware) Login(ctx context.Context, user users.User) (token
func (lm *loggingMiddleware) Identify(key string) (id string, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method identity for client %s took %s to complete", id, time.Since(begin))
message := fmt.Sprintf("Method identity 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
@ -67,3 +67,16 @@ func (lm *loggingMiddleware) Identify(key string) (id string, err error) {
return lm.svc.Identify(key)
}
func (lm *loggingMiddleware) UserInfo(ctx context.Context, key string) (u users.User, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method user_info for user %s took %s to complete", u.Email, 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.UserInfo(ctx, key)
}

View File

@ -59,3 +59,12 @@ func (ms *metricsMiddleware) Identify(key string) (string, error) {
return ms.svc.Identify(key)
}
func (ms *metricsMiddleware) UserInfo(ctx context.Context, key string) (users.User, error) {
defer func(begin time.Time) {
ms.counter.With("method", "user_info").Add(1)
ms.latency.With("method", "user_info").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UserInfo(ctx, key)
}

View File

@ -59,6 +59,12 @@ func migrateDB(db *sqlx.DB) error {
},
Down: []string{"DROP TABLE users"},
},
{
Id: "users_2",
Up: []string{
`ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS metadata JSONB`,
},
},
},
}

View File

@ -10,6 +10,8 @@ package postgres
import (
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"github.com/jmoiron/sqlx"
@ -32,9 +34,10 @@ func New(db *sqlx.DB) users.UserRepository {
}
func (ur userRepository) Save(_ context.Context, user users.User) error {
q := `INSERT INTO users (email, password) VALUES (:email, :password)`
q := `INSERT INTO users (email, password, metadata) VALUES (:email, :password, :metadata)`
dbu := toDBUser(user)
if _, err := ur.db.NamedExec(q, dbu); err != nil {
if pqErr, ok := err.(*pq.Error); ok && errDuplicate == pqErr.Code.Name() {
return users.ErrConflict
@ -46,7 +49,7 @@ func (ur userRepository) Save(_ context.Context, user users.User) error {
}
func (ur userRepository) RetrieveByID(_ context.Context, email string) (users.User, error) {
q := `SELECT password FROM users WHERE email = $1`
q := `SELECT password, metadata FROM users WHERE email = $1`
dbu := dbUser{
Email: email,
@ -63,15 +66,54 @@ func (ur userRepository) RetrieveByID(_ context.Context, email string) (users.Us
return user, 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
}
if err := json.Unmarshal(b, m); err != nil {
m = &dbMetadata{}
return err
}
return nil
}
// Value Implements valuer
func (m dbMetadata) Value() (driver.Value, error) {
if len(m) == 0 {
return nil, nil
}
b, err := json.Marshal(m)
if err != nil {
return nil, err
}
return b, err
}
type dbUser struct {
Email string `json:"email"`
Password string `json:"password"`
Email string `db:"email"`
Password string `db:"password"`
Metadata dbMetadata `db:"metadata"`
}
func toDBUser(u users.User) dbUser {
return dbUser{
Email: u.Email,
Password: u.Password,
Metadata: u.Metadata,
}
}
@ -79,5 +121,6 @@ func toUser(dbu dbUser) users.User {
return users.User{
Email: dbu.Email,
Password: dbu.Password,
Metadata: dbu.Metadata,
}
}

View File

@ -27,6 +27,9 @@ var (
// ErrNotFound indicates a non-existent entity request.
ErrNotFound = errors.New("non-existent entity")
// ErrScanMetadata indicates problem with metadata in db
ErrScanMetadata = errors.New("Failed to scan metadata")
)
// Service specifies an API that must be fullfiled by the domain service
@ -45,6 +48,9 @@ type Service interface {
// is returned. If token is invalid, or invocation failed for some
// other reason, non-nil error values are returned in response.
Identify(string) (string, error)
// Get authenticated user info for the given token.
UserInfo(ctx context.Context, token string) (User, error)
}
var _ Service = (*usersService)(nil)
@ -90,3 +96,21 @@ func (svc usersService) Identify(token string) (string, error) {
}
return id, nil
}
func (svc usersService) UserInfo(ctx context.Context, token string) (User, error) {
id, err := svc.idp.Identity(token)
if err != nil {
return User{}, ErrUnauthorizedAccess
}
dbUser, err := svc.users.RetrieveByID(ctx, id)
if err != nil {
return User{}, ErrUnauthorizedAccess
}
return User{
Email: id,
Password: "",
Metadata: dbUser.Metadata,
}, nil
}

View File

@ -24,6 +24,7 @@ var (
type User struct {
Email string
Password string
Metadata map[string]interface{}
}
// Validate returns an error if user representation is invalid.

View File

@ -18,6 +18,7 @@ import (
const (
email = "user@example.com"
password = "password"
metadata = `{"role":"manager"}`
)
func TestValidate(t *testing.T) {