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:
parent
873ef4c96f
commit
92a640f6fc
@ -7,9 +7,7 @@
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/users"
|
||||
)
|
||||
import "github.com/mainflux/mainflux/users"
|
||||
|
||||
type identityReq struct {
|
||||
token string
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
const (
|
||||
email = "user@example.com"
|
||||
password = "password"
|
||||
metadata = `{"role":"manager"}`
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user