* Updated Users Service

* user-old => test/users
This commit is contained in:
ben-toogood
2021-01-22 13:39:34 +00:00
committed by GitHub
parent e6495ff6d7
commit 055517ec14
22 changed files with 3359 additions and 1169 deletions

1
users/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
users

View File

@@ -1,3 +1,3 @@
FROM alpine:3.2
FROM alpine
ADD users /users
ENTRYPOINT [ "/users" ]

View File

@@ -7,13 +7,8 @@ init:
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/users.proto
.PHONY: docs
docs:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/users.proto
@redoc-cli bundle api-users.json
protoc --proto_path=. --micro_out=. --go_out=:. proto/users.proto
.PHONY: build
build:
go build -o users *.go

View File

@@ -1,76 +1,23 @@
A user service for storing accounts and simple auth.
# Users Service
The users service provides user management and authentication so you can easily add them to your own apps
without having to build the entire thing from scratch.
This is the Users service
## Getting started
Generated with
```
micro run github.com/micro/services/users
micro new users
```
## Usage
User server implements the following RPC Methods
Generate the proto code
Users
- Create
- Read
- Update
- Delete
- Search
- UpdatePassword
- Login
- Logout
- ReadSession
### Create
```shell
micro call users Users.Create '{"id": "ff3c06de-9e43-41c7-9bab-578f6b4ad32b", "username": "asim", "email": "asim@example.com", "password": "password1"}'
```
make proto
```
### Read
Run the service
```shell
micro call users Users.Read '{"id": "ff3c06de-9e43-41c7-9bab-578f6b4ad32b"}'
```
### Update
```shell
micro call users Users.Update '{"id": "ff3c06de-9e43-41c7-9bab-578f6b4ad32b", "username": "asim", "email": "asim+update@example.com"}'
```
### Update Password
```shell
micro call users Users.UpdatePassword '{"userId": "ff3c06de-9e43-41c7-9bab-578f6b4ad32b", "oldPassword": "password1", "newPassword": "newpassword1", "confirmPassword": "newpassword1" }'
```
### Delete
```shell
micro call users Users.Delete '{"id": "ff3c06de-9e43-41c7-9bab-578f6b4ad32b"}'
```
### Login
```shell
micro call users Users.Login '{"username": "asim", "password": "password1"}'
```
### Read Session
```shell
micro call users Users.ReadSession '{"sessionId": "sr7UEBmIMg5hYOgiljnhrd4XLsnalNewBV9KzpZ9aD8w37b3jRmEujGtKGcGlXPg1yYoSHR3RLy66ugglw0tofTNGm57NrNYUHsFxfwuGC6pvCn8BecB7aEF6UxTyVFq"}'
```
### Logout
```shell
micro call users Users.Logout '{"sessionId": "sr7UEBmIMg5hYOgiljnhrd4XLsnalNewBV9KzpZ9aD8w37b3jRmEujGtKGcGlXPg1yYoSHR3RLy66ugglw0tofTNGm57NrNYUHsFxfwuGC6pvCn8BecB7aEF6UxTyVFq"}'
```
micro run .
```

View File

@@ -1,149 +0,0 @@
package domain
import (
"errors"
"time"
"github.com/micro/dev/model"
"github.com/micro/micro/v3/service/store"
user "github.com/micro/services/users/proto"
)
type pw struct {
ID string `json:"id"`
Password string `json:"password"`
Salt string `json:"salt"`
}
type Domain struct {
users model.Model
sessions model.Model
passwords model.Model
nameIndex model.Index
emailIndex model.Index
idIndex model.Index
}
func New() *Domain {
nameIndex := model.ByEquality("username")
nameIndex.Unique = true
nameIndex.Order.Type = model.OrderTypeUnordered
emailIndex := model.ByEquality("email")
emailIndex.Unique = true
emailIndex.Order.Type = model.OrderTypeUnordered
// @todo there should be a better way to get the default index from model
// than recreating the options here
idIndex := model.ByEquality("id")
idIndex.Order.Type = model.OrderTypeUnordered
return &Domain{
users: model.New(store.DefaultStore, "users", model.Indexes(nameIndex, emailIndex), nil),
sessions: model.New(store.DefaultStore, "sessions", nil, nil),
passwords: model.New(store.DefaultStore, "passwords", nil, nil),
nameIndex: nameIndex,
emailIndex: emailIndex,
idIndex: idIndex,
}
}
func (domain *Domain) CreateSession(sess *user.Session) error {
if sess.Created == 0 {
sess.Created = time.Now().Unix()
}
if sess.Expires == 0 {
sess.Expires = time.Now().Add(time.Hour * 24 * 7).Unix()
}
return domain.sessions.Save(sess)
}
func (domain *Domain) DeleteSession(id string) error {
return domain.sessions.Delete(domain.idIndex.ToQuery(id))
}
func (domain *Domain) ReadSession(id string) (*user.Session, error) {
sess := &user.Session{}
// @todo there should be a Read in the model to get rid of this pattern
return sess, domain.sessions.Read(domain.idIndex.ToQuery(id), &sess)
}
func (domain *Domain) Create(user *user.User, salt string, password string) error {
user.Created = time.Now().Unix()
user.Updated = time.Now().Unix()
err := domain.users.Save(user)
if err != nil {
return err
}
return domain.passwords.Save(pw{
ID: user.Id,
Password: password,
Salt: salt,
})
}
func (domain *Domain) Delete(id string) error {
return domain.users.Delete(domain.idIndex.ToQuery(id))
}
func (domain *Domain) Update(user *user.User) error {
user.Updated = time.Now().Unix()
return domain.users.Save(user)
}
func (domain *Domain) Read(id string) (*user.User, error) {
user := &user.User{}
return user, domain.users.Read(domain.idIndex.ToQuery(id), user)
}
func (domain *Domain) Search(username, email string, limit, offset int64) ([]*user.User, error) {
var query model.Query
if len(username) > 0 {
query = domain.nameIndex.ToQuery(username)
} else if len(email) > 0 {
query = domain.emailIndex.ToQuery(email)
} else {
return nil, errors.New("username and email cannot be blank")
}
users := []*user.User{}
return users, domain.users.List(query, &users)
}
func (domain *Domain) UpdatePassword(id string, salt string, password string) error {
return domain.passwords.Save(pw{
ID: id,
Password: password,
Salt: salt,
})
}
func (domain *Domain) SaltAndPassword(username, email string) (string, string, error) {
var query model.Query
if len(username) > 0 {
query = domain.nameIndex.ToQuery(username)
} else if len(email) > 0 {
query = domain.emailIndex.ToQuery(email)
} else {
return "", "", errors.New("username and email cannot be blank")
}
user := &user.User{}
err := domain.users.Read(query, &user)
if err != nil {
return "", "", err
}
query = model.Equals("id", user.Id)
query.Order.Type = model.OrderTypeUnordered
password := &pw{}
err = domain.passwords.Read(query, password)
if err != nil {
return "", "", err
}
return password.Salt, password.Password, nil
}

View File

@@ -1,3 +1,2 @@
package main
//go:generate make proto

View File

@@ -1,174 +1,369 @@
package handler
import (
"crypto/rand"
"encoding/base64"
"context"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/services/users/domain"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
"golang.org/x/crypto/bcrypt"
"golang.org/x/net/context"
)
const (
x = "cruft123"
"gorm.io/gorm"
)
var (
alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
ErrMissingFirstName = errors.BadRequest("MISSING_FIRST_NAME", "Missing first name")
ErrMissingLastName = errors.BadRequest("MISSING_LAST_NAME", "Missing last name")
ErrMissingEmail = errors.BadRequest("MISSING_EMAIL", "Missing email")
ErrDuplicateEmail = errors.BadRequest("DUPLICATE_EMAIL", "A user with this email address already exists")
ErrInvalidEmail = errors.BadRequest("INVALID_EMAIL", "The email provided is invalid")
ErrInvalidPassword = errors.BadRequest("INVALID_PASSWORD", "Password must be at least 8 characters long")
ErrMissingIDs = errors.BadRequest("MISSING_IDS", "One or more ids are required")
ErrMissingID = errors.BadRequest("MISSING_ID", "Missing ID")
ErrMissingToken = errors.BadRequest("MISSING_TOKEN", "Missing token")
ErrIncorrectPassword = errors.BadRequest("INCORRECT_PASSWORD", "Incorrect password")
ErrTokenExpired = errors.BadRequest("TOKEN_EXPIRED", "Token has expired")
ErrInvalidToken = errors.BadRequest("INVALID_TOKEN", "Token is invalid")
ErrNotFound = errors.NotFound("NOT_FOUND", "User not found")
emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
tokenTTL = time.Hour * 7 * 24
)
func random(i int) string {
bytes := make([]byte, i)
for {
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
return string(bytes)
type User struct {
ID string
FirstName string
LastName string
Email string `gorm:"uniqueIndex"`
Password string
CreatedAt time.Time
Tokens []Token
}
func (u *User) Serialize() *pb.User {
return &pb.User{
Id: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
Email: u.Email,
}
return "ughwhy?!!!"
}
type Token struct {
Key string `gorm:"primaryKey"`
CreatedAt time.Time
ExpiresAt time.Time
UserID string
User User
}
type Users struct {
domain *domain.Domain
DB *gorm.DB
Time func() time.Time
}
func NewUsers() *Users {
return &Users{
domain: domain.New(),
// Create a user
func (u *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
// validate the request
if len(req.FirstName) == 0 {
return ErrMissingFirstName
}
if len(req.LastName) == 0 {
return ErrMissingLastName
}
if len(req.Email) == 0 {
return ErrMissingEmail
}
if !isEmailValid(req.Email) {
return ErrInvalidEmail
}
}
func (s *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
if len(req.Password) < 8 {
return errors.InternalServerError("users.Create.Check", "Password is less than 8 characters")
return ErrInvalidPassword
}
salt := random(16)
h, err := bcrypt.GenerateFromPassword([]byte(x+salt+req.Password), 10)
// hash and salt the password using bcrypt
phash, err := hashAndSalt(req.Password)
if err != nil {
return errors.InternalServerError("users.Create", err.Error())
logger.Errorf("Error hasing and salting password: %v", err)
return errors.InternalServerError("HASHING_ERROR", "Error hashing password")
}
pp := base64.StdEncoding.EncodeToString(h)
return s.domain.Create(&pb.User{
Id: req.Id,
Username: strings.ToLower(req.Username),
Email: strings.ToLower(req.Email),
}, salt, pp)
}
return u.DB.Transaction(func(tx *gorm.DB) error {
// write the user to the database
user := &User{
ID: uuid.New().String(),
FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email,
Password: phash,
}
err = u.DB.Create(user).Error
if err != nil && strings.Contains(err.Error(), "idx_users_email") {
return ErrDuplicateEmail
} else if err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
func (s *Users) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
user, err := s.domain.Read(req.Id)
if err != nil {
return err
}
rsp.User = user
return nil
}
// generate a token for the user
token := Token{
UserID: user.ID,
Key: uuid.New().String(),
ExpiresAt: u.Time().Add(time.Hour * 24 * 7),
}
if err := tx.Create(&token).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
func (s *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
return s.domain.Update(&pb.User{
Id: req.Id,
Username: strings.ToLower(req.Username),
Email: strings.ToLower(req.Email),
// serialize the response
rsp.User = user.Serialize()
rsp.Token = token.Key
return nil
})
}
func (s *Users) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
return s.domain.Delete(req.Id)
}
func (s *Users) Search(ctx context.Context, req *pb.SearchRequest, rsp *pb.SearchResponse) error {
users, err := s.domain.Search(req.Username, req.Email, req.Limit, req.Offset)
if err != nil {
return err
}
rsp.Users = users
return nil
}
func (s *Users) UpdatePassword(ctx context.Context, req *pb.UpdatePasswordRequest, rsp *pb.UpdatePasswordResponse) error {
usr, err := s.domain.Read(req.UserId)
if err != nil {
return errors.InternalServerError("users.updatepassword", err.Error())
}
if req.NewPassword != req.ConfirmPassword {
return errors.InternalServerError("users.updatepassword", "Passwords don't math")
// Read users using ID
func (u *Users) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
// validate the request
if len(req.Ids) == 0 {
return ErrMissingIDs
}
salt, hashed, err := s.domain.SaltAndPassword(usr.Username, usr.Email)
if err != nil {
return errors.InternalServerError("users.updatepassword", err.Error())
// query the database
var users []User
if err := u.DB.Model(&User{}).Where("id IN (?)", req.Ids).Find(&users).Error; err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
hh, err := base64.StdEncoding.DecodeString(hashed)
if err != nil {
return errors.InternalServerError("users.updatepassword", err.Error())
}
if err := bcrypt.CompareHashAndPassword(hh, []byte(x+salt+req.OldPassword)); err != nil {
return errors.Unauthorized("users.updatepassword", err.Error())
}
salt = random(16)
h, err := bcrypt.GenerateFromPassword([]byte(x+salt+req.NewPassword), 10)
if err != nil {
return errors.InternalServerError("users.updatepassword", err.Error())
}
pp := base64.StdEncoding.EncodeToString(h)
if err := s.domain.UpdatePassword(req.UserId, salt, pp); err != nil {
return errors.InternalServerError("users.updatepassword", err.Error())
// serialize the response
rsp.Users = make(map[string]*pb.User, len(users))
for _, u := range users {
rsp.Users[u.ID] = u.Serialize()
}
return nil
}
func (s *Users) Login(ctx context.Context, req *pb.LoginRequest, rsp *pb.LoginResponse) error {
username := strings.ToLower(req.Username)
email := strings.ToLower(req.Email)
salt, hashed, err := s.domain.SaltAndPassword(username, email)
if err != nil {
return err
// Update a user
func (u *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
if req.FirstName != nil && len(req.FirstName.Value) == 0 {
return ErrMissingFirstName
}
if req.LastName != nil && len(req.LastName.Value) == 0 {
return ErrMissingLastName
}
if req.Email != nil && len(req.Email.Value) == 0 {
return ErrMissingEmail
}
if req.Email != nil && !isEmailValid(req.Email.Value) {
return ErrInvalidEmail
}
hh, err := base64.StdEncoding.DecodeString(hashed)
if err != nil {
return errors.InternalServerError("users.Login", err.Error())
// lookup the user
var user User
if err := u.DB.Where(&User{ID: req.Id}).First(&user).Error; err == gorm.ErrRecordNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
if err := bcrypt.CompareHashAndPassword(hh, []byte(x+salt+req.Password)); err != nil {
return errors.Unauthorized("users.login", err.Error())
// assign the updated values
if req.FirstName != nil {
user.FirstName = req.FirstName.Value
}
// save session
sess := &pb.Session{
Id: random(128),
Username: username,
Email: email,
Created: time.Now().Unix(),
Expires: time.Now().Add(time.Hour * 24 * 7).Unix(),
if req.LastName != nil {
user.LastName = req.LastName.Value
}
if req.Email != nil {
user.Email = req.Email.Value
}
if err := s.domain.CreateSession(sess); err != nil {
return errors.InternalServerError("users.Login", err.Error())
// write the user to the database
err := u.DB.Save(user).Error
if err != nil && strings.Contains(err.Error(), "idx_users_email") {
return ErrDuplicateEmail
} else if err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
rsp.Session = sess
// serialize the user
rsp.User = user.Serialize()
return nil
}
func (s *Users) Logout(ctx context.Context, req *pb.LogoutRequest, rsp *pb.LogoutResponse) error {
return s.domain.DeleteSession(req.SessionId)
// Delete a user
func (u *Users) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
// delete the users tokens
return u.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Delete(&Token{}, &Token{UserID: req.Id}).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// delete from the database
if err := tx.Delete(&User{}, &User{ID: req.Id}).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
return nil
})
}
func (s *Users) ReadSession(ctx context.Context, req *pb.ReadSessionRequest, rsp *pb.ReadSessionResponse) error {
sess, err := s.domain.ReadSession(req.SessionId)
if err != nil {
return err
// List all users
func (u *Users) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
// query the database
var users []User
if err := u.DB.Model(&User{}).Find(&users).Error; err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.Users = make([]*pb.User, len(users))
for i, u := range users {
rsp.Users[i] = u.Serialize()
}
rsp.Session = sess
return nil
}
// Login using email and password returns the users profile and a token
func (u *Users) Login(ctx context.Context, req *pb.LoginRequest, rsp *pb.LoginResponse) error {
// validate the request
if len(req.Email) == 0 {
return ErrMissingEmail
}
if len(req.Password) == 0 {
return ErrInvalidPassword
}
return u.DB.Transaction(func(tx *gorm.DB) error {
// lookup the user
var user User
if err := tx.Where(&User{Email: req.Email}).First(&user).Error; err == gorm.ErrRecordNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// compare the passwords
if !passwordsMatch(user.Password, req.Password) {
return ErrIncorrectPassword
}
// generate a token for the user
token := Token{
UserID: user.ID,
Key: uuid.New().String(),
ExpiresAt: u.Time().Add(tokenTTL),
}
if err := tx.Create(&token).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.Token = token.Key
rsp.User = user.Serialize()
return nil
})
}
// Logout expires all tokens for the user
func (u *Users) Logout(ctx context.Context, req *pb.LogoutRequest, rsp *pb.LogoutResponse) error {
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
return u.DB.Transaction(func(tx *gorm.DB) error {
// lookup the user
var user User
if err := tx.Where(&User{ID: req.Id}).Preload("Tokens").First(&user).Error; err == gorm.ErrRecordNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// delete the tokens
if err := tx.Delete(user.Tokens).Error; err != nil {
logger.Errorf("Error deleting from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
return nil
})
}
// Validate a token, each time a token is validated it extends its lifetime for another week
func (u *Users) Validate(ctx context.Context, req *pb.ValidateRequest, rsp *pb.ValidateResponse) error {
// validate the request
if len(req.Token) == 0 {
return ErrMissingToken
}
return u.DB.Transaction(func(tx *gorm.DB) error {
// lookup the token
var token Token
if err := tx.Where(&Token{Key: req.Token}).Preload("User").First(&token).Error; err == gorm.ErrRecordNotFound {
return ErrInvalidToken
} else if err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// ensure the token is valid
if u.Time().After(token.ExpiresAt) {
return ErrTokenExpired
}
// extend the token for another lifetime
token.ExpiresAt = u.Time().Add(tokenTTL)
if err := tx.Save(&token).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.User = token.User.Serialize()
return nil
})
}
// isEmailValid checks if the email provided passes the required structure and length.
func isEmailValid(e string) bool {
if len(e) < 3 && len(e) > 254 {
return false
}
return emailRegex.MatchString(e)
}
func hashAndSalt(pwd string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
func passwordsMatch(hashed string, plain string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
return err == nil
}

View File

@@ -0,0 +1,668 @@
package handler_test
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func testHandler(t *testing.T) *handler.Users {
// connect to the database
db, err := gorm.Open(postgres.Open("postgresql://postgres@localhost:5432/users?sslmode=disable"), &gorm.Config{})
if err != nil {
t.Fatalf("Error connecting to database: %v", err)
}
// migrate the database
if err := db.AutoMigrate(&handler.User{}, &handler.Token{}); err != nil {
t.Fatalf("Error migrating database: %v", err)
}
// clean any data from a previous run
if err := db.Exec("TRUNCATE TABLE users, tokens CASCADE").Error; err != nil {
t.Fatalf("Error cleaning database: %v", err)
}
return &handler.Users{DB: db, Time: time.Now}
}
func TestCreate(t *testing.T) {
tt := []struct {
Name string
FirstName string
LastName string
Email string
Password string
Error error
}{
{
Name: "MissingFirstName",
LastName: "Doe",
Email: "john@doe.com",
Password: "password",
Error: handler.ErrMissingFirstName,
},
{
Name: "MissingLastName",
FirstName: "John",
Email: "john@doe.com",
Password: "password",
Error: handler.ErrMissingLastName,
},
{
Name: "MissingEmail",
FirstName: "John",
LastName: "Doe",
Password: "password",
Error: handler.ErrMissingEmail,
},
{
Name: "InvalidEmail",
FirstName: "John",
LastName: "Doe",
Password: "password",
Email: "foo.foo.foo",
Error: handler.ErrInvalidEmail,
},
{
Name: "InvalidPassword",
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "pwd",
Error: handler.ErrInvalidPassword,
},
}
// test the validations
h := testHandler(t)
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
err := h.Create(context.TODO(), &pb.CreateRequest{
FirstName: tc.FirstName,
LastName: tc.LastName,
Email: tc.Email,
Password: tc.Password,
}, &pb.CreateResponse{})
assert.Equal(t, tc.Error, err)
})
}
t.Run("Valid", func(t *testing.T) {
var rsp pb.CreateResponse
req := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &req, &rsp)
assert.NoError(t, err)
u := rsp.User
if u == nil {
t.Fatalf("No user returned")
}
assert.NotEmpty(t, u.Id)
assert.Equal(t, req.FirstName, u.FirstName)
assert.Equal(t, req.LastName, u.LastName)
assert.Equal(t, req.Email, u.Email)
assert.NotEmpty(t, rsp.Token)
})
t.Run("DuplicateEmail", func(t *testing.T) {
var rsp pb.CreateResponse
req := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &req, &rsp)
assert.Equal(t, handler.ErrDuplicateEmail, err)
assert.Nil(t, rsp.User)
})
t.Run("DifferentEmail", func(t *testing.T) {
var rsp pb.CreateResponse
req := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "johndoe@gmail.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &req, &rsp)
assert.NoError(t, err)
u := rsp.User
if u == nil {
t.Fatalf("No user returned")
}
assert.NotEmpty(t, u.Id)
assert.Equal(t, req.FirstName, u.FirstName)
assert.Equal(t, req.LastName, u.LastName)
assert.Equal(t, req.Email, u.Email)
})
}
func TestRead(t *testing.T) {
h := testHandler(t)
t.Run("MissingIDs", func(t *testing.T) {
var rsp pb.ReadResponse
err := h.Read(context.TODO(), &pb.ReadRequest{}, &rsp)
assert.Equal(t, handler.ErrMissingIDs, err)
assert.Nil(t, rsp.Users)
})
t.Run("NotFound", func(t *testing.T) {
var rsp pb.ReadResponse
err := h.Read(context.TODO(), &pb.ReadRequest{Ids: []string{"foo"}}, &rsp)
assert.Nil(t, err)
if rsp.Users == nil {
t.Fatal("Expected the users object to not be nil")
}
assert.Nil(t, rsp.Users["foo"])
})
// create some mock data
var rsp1 pb.CreateResponse
req1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &req1, &rsp1)
assert.NoError(t, err)
if rsp1.User == nil {
t.Fatal("No user returned")
return
}
var rsp2 pb.CreateResponse
req2 := pb.CreateRequest{
FirstName: "Apple",
LastName: "Tree",
Email: "apple@tree.com",
Password: "passwordabc",
}
err = h.Create(context.TODO(), &req2, &rsp2)
assert.NoError(t, err)
if rsp2.User == nil {
t.Fatal("No user returned")
return
}
// test the read
var rsp pb.ReadResponse
err = h.Read(context.TODO(), &pb.ReadRequest{
Ids: []string{rsp1.User.Id, rsp2.User.Id},
}, &rsp)
assert.NoError(t, err)
if rsp.Users == nil {
t.Fatal("Users not returned")
return
}
assert.NotNil(t, rsp.Users[rsp1.User.Id])
assert.NotNil(t, rsp.Users[rsp2.User.Id])
// check the users match
if u := rsp.Users[rsp1.User.Id]; u != nil {
assert.Equal(t, rsp1.User.Id, u.Id)
assert.Equal(t, rsp1.User.FirstName, u.FirstName)
assert.Equal(t, rsp1.User.LastName, u.LastName)
assert.Equal(t, rsp1.User.Email, u.Email)
}
if u := rsp.Users[rsp2.User.Id]; u != nil {
assert.Equal(t, rsp2.User.Id, u.Id)
assert.Equal(t, rsp2.User.FirstName, u.FirstName)
assert.Equal(t, rsp2.User.LastName, u.LastName)
assert.Equal(t, rsp2.User.Email, u.Email)
}
}
func TestUpdate(t *testing.T) {
h := testHandler(t)
t.Run("MissingID", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(context.TODO(), &pb.UpdateRequest{}, &rsp)
assert.Equal(t, handler.ErrMissingID, err)
assert.Nil(t, rsp.User)
})
t.Run("NotFound", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(context.TODO(), &pb.UpdateRequest{Id: "foo"}, &rsp)
assert.Equal(t, handler.ErrNotFound, err)
assert.Nil(t, rsp.User)
})
// create some mock data
var cRsp1 pb.CreateResponse
cReq1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &cReq1, &cRsp1)
assert.NoError(t, err)
if cRsp1.User == nil {
t.Fatal("No user returned")
return
}
var cRsp2 pb.CreateResponse
cReq2 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "johndoe@gmail.com",
Password: "passwordabc",
}
err = h.Create(context.TODO(), &cReq2, &cRsp2)
assert.NoError(t, err)
if cRsp2.User == nil {
t.Fatal("No user returned")
return
}
t.Run("BlankFirstName", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(context.TODO(), &pb.UpdateRequest{
Id: cRsp1.User.Id, FirstName: &wrapperspb.StringValue{},
}, &rsp)
assert.Equal(t, handler.ErrMissingFirstName, err)
assert.Nil(t, rsp.User)
})
t.Run("BlankLastName", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(context.TODO(), &pb.UpdateRequest{
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
}, &rsp)
assert.Equal(t, handler.ErrMissingLastName, err)
assert.Nil(t, rsp.User)
})
t.Run("BlankLastName", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(context.TODO(), &pb.UpdateRequest{
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
}, &rsp)
assert.Equal(t, handler.ErrMissingLastName, err)
assert.Nil(t, rsp.User)
})
t.Run("BlankEmail", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(context.TODO(), &pb.UpdateRequest{
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{},
}, &rsp)
assert.Equal(t, handler.ErrMissingEmail, err)
assert.Nil(t, rsp.User)
})
t.Run("InvalidEmail", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(context.TODO(), &pb.UpdateRequest{
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: "foo.bar"},
}, &rsp)
assert.Equal(t, handler.ErrInvalidEmail, err)
assert.Nil(t, rsp.User)
})
t.Run("EmailAlreadyExists", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(context.TODO(), &pb.UpdateRequest{
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: cRsp2.User.Email},
}, &rsp)
assert.Equal(t, handler.ErrDuplicateEmail, err)
assert.Nil(t, rsp.User)
})
t.Run("Valid", func(t *testing.T) {
uReq := pb.UpdateRequest{
Id: cRsp1.User.Id,
Email: &wrapperspb.StringValue{Value: "foobar@gmail.com"},
FirstName: &wrapperspb.StringValue{Value: "Foo"},
LastName: &wrapperspb.StringValue{Value: "Bar"},
}
var uRsp pb.UpdateResponse
err := h.Update(context.TODO(), &uReq, &uRsp)
assert.NoError(t, err)
if uRsp.User == nil {
t.Error("No user returned")
return
}
assert.Equal(t, cRsp1.User.Id, uRsp.User.Id)
assert.Equal(t, uReq.Email.Value, uRsp.User.Email)
assert.Equal(t, uReq.FirstName.Value, uRsp.User.FirstName)
assert.Equal(t, uReq.LastName.Value, uRsp.User.LastName)
})
}
func TestDelete(t *testing.T) {
h := testHandler(t)
t.Run("MissingID", func(t *testing.T) {
err := h.Delete(context.TODO(), &pb.DeleteRequest{}, &pb.DeleteResponse{})
assert.Equal(t, handler.ErrMissingID, err)
})
// create some mock data
var cRsp pb.CreateResponse
cReq := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &cReq, &cRsp)
assert.NoError(t, err)
if cRsp.User == nil {
t.Fatal("No user returned")
return
}
t.Run("Valid", func(t *testing.T) {
err := h.Delete(context.TODO(), &pb.DeleteRequest{
Id: cRsp.User.Id,
}, &pb.DeleteResponse{})
assert.NoError(t, err)
// check it was actually deleted
var rsp pb.ReadResponse
err = h.Read(context.TODO(), &pb.ReadRequest{
Ids: []string{cRsp.User.Id},
}, &rsp)
assert.NoError(t, err)
assert.Nil(t, rsp.Users[cRsp.User.Id])
})
t.Run("Retry", func(t *testing.T) {
err := h.Delete(context.TODO(), &pb.DeleteRequest{
Id: cRsp.User.Id,
}, &pb.DeleteResponse{})
assert.NoError(t, err)
})
}
func TestList(t *testing.T) {
h := testHandler(t)
// create some mock data
var cRsp1 pb.CreateResponse
cReq1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &cReq1, &cRsp1)
assert.NoError(t, err)
if cRsp1.User == nil {
t.Fatal("No user returned")
return
}
var cRsp2 pb.CreateResponse
cReq2 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "johndoe@gmail.com",
Password: "passwordabc",
}
err = h.Create(context.TODO(), &cReq2, &cRsp2)
assert.NoError(t, err)
if cRsp2.User == nil {
t.Fatal("No user returned")
return
}
var rsp pb.ListResponse
err = h.List(context.TODO(), &pb.ListRequest{}, &rsp)
assert.NoError(t, err)
if rsp.Users == nil {
t.Error("No users returned")
return
}
var u1Found, u2Found bool
for _, u := range rsp.Users {
switch u.Id {
case cRsp1.User.Id:
assertUsersMatch(t, cRsp1.User, u)
u1Found = true
case cRsp2.User.Id:
assertUsersMatch(t, cRsp2.User, u)
u2Found = true
default:
t.Fatal("Unexpected user returned")
return
}
}
assert.True(t, u1Found)
assert.True(t, u2Found)
}
func TestLogin(t *testing.T) {
h := testHandler(t)
// create some mock data
var cRsp pb.CreateResponse
cReq := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &cReq, &cRsp)
assert.NoError(t, err)
if cRsp.User == nil {
t.Fatal("No user returned")
return
}
tt := []struct {
Name string
Email string
Password string
Error error
User *pb.User
}{
{
Name: "MissingEmail",
Password: "passwordabc",
Error: handler.ErrMissingEmail,
},
{
Name: "MissingPassword",
Email: "john@doe.com",
Error: handler.ErrInvalidPassword,
},
{
Name: "UserNotFound",
Email: "foo@bar.com",
Password: "passwordabc",
Error: handler.ErrNotFound,
},
{
Name: "IncorrectPassword",
Email: "john@doe.com",
Password: "passwordabcdef",
Error: handler.ErrIncorrectPassword,
},
{
Name: "Valid",
Email: "john@doe.com",
Password: "passwordabc",
User: cRsp.User,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.LoginResponse
err := h.Login(context.TODO(), &pb.LoginRequest{
Email: tc.Email, Password: tc.Password,
}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.User != nil {
assertUsersMatch(t, tc.User, rsp.User)
assert.NotEmpty(t, rsp.Token)
} else {
assert.Nil(t, tc.User)
}
})
}
}
func TestLogout(t *testing.T) {
h := testHandler(t)
t.Run("MissingUserID", func(t *testing.T) {
err := h.Logout(context.TODO(), &pb.LogoutRequest{}, &pb.LogoutResponse{})
assert.Equal(t, handler.ErrMissingID, err)
})
t.Run("UserNotFound", func(t *testing.T) {
err := h.Logout(context.TODO(), &pb.LogoutRequest{Id: uuid.New().String()}, &pb.LogoutResponse{})
assert.Equal(t, handler.ErrNotFound, err)
})
t.Run("Valid", func(t *testing.T) {
// create some mock data
var cRsp pb.CreateResponse
cReq := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &cReq, &cRsp)
assert.NoError(t, err)
if cRsp.User == nil {
t.Fatal("No user returned")
return
}
err = h.Logout(context.TODO(), &pb.LogoutRequest{Id: cRsp.User.Id}, &pb.LogoutResponse{})
assert.NoError(t, err)
err = h.Validate(context.TODO(), &pb.ValidateRequest{Token: cRsp.Token}, &pb.ValidateResponse{})
assert.Error(t, err)
})
}
func TestValidate(t *testing.T) {
h := testHandler(t)
// create some mock data
var cRsp1 pb.CreateResponse
cReq1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(context.TODO(), &cReq1, &cRsp1)
assert.NoError(t, err)
if cRsp1.User == nil {
t.Fatal("No user returned")
return
}
var cRsp2 pb.CreateResponse
cReq2 := pb.CreateRequest{
FirstName: "Barry",
LastName: "Doe",
Email: "barry@doe.com",
Password: "passwordabc",
}
err = h.Create(context.TODO(), &cReq2, &cRsp2)
assert.NoError(t, err)
if cRsp2.User == nil {
t.Fatal("No user returned")
return
}
tt := []struct {
Name string
Token string
Time func() time.Time
Error error
User *pb.User
}{
{
Name: "MissingToken",
Error: handler.ErrMissingToken,
},
{
Name: "InvalidToken",
Error: handler.ErrInvalidToken,
Token: uuid.New().String(),
},
{
Name: "ExpiredToken",
Error: handler.ErrTokenExpired,
Token: cRsp1.Token,
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) },
},
{
Name: "ValidToken",
User: cRsp2.User,
Token: cRsp2.Token,
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 3) },
},
{
Name: "RefreshedToken",
User: cRsp2.User,
Token: cRsp2.Token,
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) },
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
if tc.Time == nil {
h.Time = time.Now
} else {
h.Time = tc.Time
}
var rsp pb.ValidateResponse
err := h.Validate(context.TODO(), &pb.ValidateRequest{Token: tc.Token}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.User != nil {
assertUsersMatch(t, tc.User, rsp.User)
} else {
assert.Nil(t, tc.User)
}
})
}
}
func assertUsersMatch(t *testing.T, exp, act *pb.User) {
if act == nil {
t.Error("No user returned")
return
}
assert.Equal(t, exp.Id, act.Id)
assert.Equal(t, exp.FirstName, act.FirstName)
assert.Equal(t, exp.LastName, act.LastName)
assert.Equal(t, exp.Email, act.Email)
}

View File

@@ -1,22 +1,43 @@
package main
import (
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
"time"
"github.com/micro/services/users/handler"
proto "github.com/micro/services/users/proto"
pb "github.com/micro/services/users/proto"
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/config"
"github.com/micro/micro/v3/service/logger"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var dbAddress = "postgresql://postgres@localhost:5432/users?sslmode=disable"
func main() {
service := service.New(
// Create service
srv := service.New(
service.Name("users"),
service.Version("latest"),
)
service.Init()
// Connect to the database
cfg, err := config.Get("users.database")
if err != nil {
logger.Fatalf("Error loading config: %v", err)
}
addr := cfg.String(dbAddress)
db, err := gorm.Open(postgres.Open(addr), &gorm.Config{})
if err != nil {
logger.Fatalf("Error connecting to database: %v", err)
}
proto.RegisterUsersHandler(service.Server(), handler.NewUsers())
// Register handler
pb.RegisterUsersHandler(srv.Server(), &handler.Users{DB: db, Time: time.Now})
if err := service.Run(); err != nil {
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ package users
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
_ "github.com/golang/protobuf/ptypes/wrappers"
math "math"
)
@@ -46,11 +47,13 @@ type UsersService interface {
Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error)
Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error)
Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error)
Search(ctx context.Context, in *SearchRequest, opts ...client.CallOption) (*SearchResponse, error)
UpdatePassword(ctx context.Context, in *UpdatePasswordRequest, opts ...client.CallOption) (*UpdatePasswordResponse, error)
List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error)
// Login using email and password returns the users profile and a token
Login(ctx context.Context, in *LoginRequest, opts ...client.CallOption) (*LoginResponse, error)
// Logout expires all tokens for the user
Logout(ctx context.Context, in *LogoutRequest, opts ...client.CallOption) (*LogoutResponse, error)
ReadSession(ctx context.Context, in *ReadSessionRequest, opts ...client.CallOption) (*ReadSessionResponse, error)
// Validate a token, each time a token is validated it extends its lifetime for another week
Validate(ctx context.Context, in *ValidateRequest, opts ...client.CallOption) (*ValidateResponse, error)
}
type usersService struct {
@@ -105,19 +108,9 @@ func (c *usersService) Delete(ctx context.Context, in *DeleteRequest, opts ...cl
return out, nil
}
func (c *usersService) Search(ctx context.Context, in *SearchRequest, opts ...client.CallOption) (*SearchResponse, error) {
req := c.c.NewRequest(c.name, "Users.Search", in)
out := new(SearchResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *usersService) UpdatePassword(ctx context.Context, in *UpdatePasswordRequest, opts ...client.CallOption) (*UpdatePasswordResponse, error) {
req := c.c.NewRequest(c.name, "Users.UpdatePassword", in)
out := new(UpdatePasswordResponse)
func (c *usersService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) {
req := c.c.NewRequest(c.name, "Users.List", in)
out := new(ListResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
@@ -145,9 +138,9 @@ func (c *usersService) Logout(ctx context.Context, in *LogoutRequest, opts ...cl
return out, nil
}
func (c *usersService) ReadSession(ctx context.Context, in *ReadSessionRequest, opts ...client.CallOption) (*ReadSessionResponse, error) {
req := c.c.NewRequest(c.name, "Users.ReadSession", in)
out := new(ReadSessionResponse)
func (c *usersService) Validate(ctx context.Context, in *ValidateRequest, opts ...client.CallOption) (*ValidateResponse, error) {
req := c.c.NewRequest(c.name, "Users.Validate", in)
out := new(ValidateResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
@@ -162,11 +155,13 @@ type UsersHandler interface {
Read(context.Context, *ReadRequest, *ReadResponse) error
Update(context.Context, *UpdateRequest, *UpdateResponse) error
Delete(context.Context, *DeleteRequest, *DeleteResponse) error
Search(context.Context, *SearchRequest, *SearchResponse) error
UpdatePassword(context.Context, *UpdatePasswordRequest, *UpdatePasswordResponse) error
List(context.Context, *ListRequest, *ListResponse) error
// Login using email and password returns the users profile and a token
Login(context.Context, *LoginRequest, *LoginResponse) error
// Logout expires all tokens for the user
Logout(context.Context, *LogoutRequest, *LogoutResponse) error
ReadSession(context.Context, *ReadSessionRequest, *ReadSessionResponse) error
// Validate a token, each time a token is validated it extends its lifetime for another week
Validate(context.Context, *ValidateRequest, *ValidateResponse) error
}
func RegisterUsersHandler(s server.Server, hdlr UsersHandler, opts ...server.HandlerOption) error {
@@ -175,11 +170,10 @@ func RegisterUsersHandler(s server.Server, hdlr UsersHandler, opts ...server.Han
Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error
Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error
Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error
Search(ctx context.Context, in *SearchRequest, out *SearchResponse) error
UpdatePassword(ctx context.Context, in *UpdatePasswordRequest, out *UpdatePasswordResponse) error
List(ctx context.Context, in *ListRequest, out *ListResponse) error
Login(ctx context.Context, in *LoginRequest, out *LoginResponse) error
Logout(ctx context.Context, in *LogoutRequest, out *LogoutResponse) error
ReadSession(ctx context.Context, in *ReadSessionRequest, out *ReadSessionResponse) error
Validate(ctx context.Context, in *ValidateRequest, out *ValidateResponse) error
}
type Users struct {
users
@@ -208,12 +202,8 @@ func (h *usersHandler) Delete(ctx context.Context, in *DeleteRequest, out *Delet
return h.UsersHandler.Delete(ctx, in, out)
}
func (h *usersHandler) Search(ctx context.Context, in *SearchRequest, out *SearchResponse) error {
return h.UsersHandler.Search(ctx, in, out)
}
func (h *usersHandler) UpdatePassword(ctx context.Context, in *UpdatePasswordRequest, out *UpdatePasswordResponse) error {
return h.UsersHandler.UpdatePassword(ctx, in, out)
func (h *usersHandler) List(ctx context.Context, in *ListRequest, out *ListResponse) error {
return h.UsersHandler.List(ctx, in, out)
}
func (h *usersHandler) Login(ctx context.Context, in *LoginRequest, out *LoginResponse) error {
@@ -224,6 +214,6 @@ func (h *usersHandler) Logout(ctx context.Context, in *LogoutRequest, out *Logou
return h.UsersHandler.Logout(ctx, in, out)
}
func (h *usersHandler) ReadSession(ctx context.Context, in *ReadSessionRequest, out *ReadSessionResponse) error {
return h.UsersHandler.ReadSession(ctx, in, out)
func (h *usersHandler) Validate(ctx context.Context, in *ValidateRequest, out *ValidateResponse) error {
return h.UsersHandler.Validate(ctx, in, out)
}

View File

@@ -2,112 +2,93 @@ syntax = "proto3";
package users;
option go_package = "proto;users";
import "google/protobuf/wrappers.proto";
service Users {
rpc Create(CreateRequest) returns (CreateResponse) {}
rpc Read(ReadRequest) returns (ReadResponse) {}
rpc Update(UpdateRequest) returns (UpdateResponse) {}
rpc Delete(DeleteRequest) returns (DeleteResponse) {}
rpc Search(SearchRequest) returns (SearchResponse) {}
rpc UpdatePassword(UpdatePasswordRequest) returns (UpdatePasswordResponse) {}
rpc List(ListRequest) returns (ListResponse) {}
// Login using email and password returns the users profile and a token
rpc Login(LoginRequest) returns (LoginResponse) {}
// Logout expires all tokens for the user
rpc Logout(LogoutRequest) returns (LogoutResponse) {}
rpc ReadSession(ReadSessionRequest) returns(ReadSessionResponse) {}
// Validate a token, each time a token is validated it extends its lifetime for another week
rpc Validate(ValidateRequest) returns (ValidateResponse) {}
}
message User {
string id = 1; // uuid
string username = 2; // alphanumeric user or org
string email = 3;
int64 created = 4; // unix
int64 updated = 5; // unix
}
message Session {
string id = 1;
string username = 2;
string email = 3;
int64 created = 4; // unix
int64 expires = 5; // unix
string id = 1;
string first_name = 2;
string last_name = 3;
string email = 4;
}
message CreateRequest {
string id = 1; // uuid
string username = 2; // alphanumeric user or org
string first_name = 1;
string last_name = 2;
string email = 3;
string password = 4;
string password = 4;
}
message CreateResponse {
User user = 1;
string token = 2;
}
message ReadRequest {
repeated string ids = 1;
}
message ReadResponse {
map<string,User> users = 1;
}
message UpdateRequest {
string id = 1;
google.protobuf.StringValue first_name = 2;
google.protobuf.StringValue last_name = 3;
google.protobuf.StringValue email = 4;
}
message UpdateResponse {
User user = 1;
}
message DeleteRequest {
string id = 1;
}
message DeleteResponse {
}
message DeleteResponse {}
message ReadRequest {
string id = 1;
}
message ListRequest {}
message ReadResponse {
User user = 1;
}
message UpdateRequest {
string id = 1; // uuid
string username = 2; // alphanumeric user or org
string email = 3;
}
message UpdateResponse {
}
message UpdatePasswordRequest {
string userId = 1;
string oldPassword = 2;
string newPassword = 3;
string confirm_password = 4;
}
message UpdatePasswordResponse {
}
message SearchRequest {
string username = 1;
string email = 2;
int64 limit = 3;
int64 offset = 4;
}
message SearchResponse {
message ListResponse {
repeated User users = 1;
}
message ReadSessionRequest {
string sessionId = 1;
}
message ReadSessionResponse {
Session session = 1;
}
message LoginRequest {
string username = 1;
string email = 2;
string password = 3;
string email = 1;
string password = 2;
}
message LoginResponse {
Session session = 1;
User user = 1;
string token = 2;
}
message LogoutRequest {
string sessionId = 1;
string id = 1;
}
message LogoutResponse {
message LogoutResponse {}
message ValidateRequest {
string token = 1;
}
message ValidateResponse {
User user = 1;
}