Documentation
  • 👋Welcome to Cerberus
  • Overview
    • 💡What we do
    • ✨Our Features
  • Product Guides
    • 💡Core concepts
    • 🍎Creating an App
    • 📎Modeling your domain
    • 📄Creating policies
  • Tutorial
    • 🛠️Getting set up
      • 🌠Cloning project
      • 👷Setting up workspace
      • 🏃‍♂️Build and run app
    • 📏Creating static rules
      • Create an account
      • Add migrations
    • ✍️Implementation
      • Backend
        • Migrate existing data
        • Routes
        • Services
          • User
          • Project
          • Sprint
          • Story
      • Frontend
        • Settings
        • Projects
        • Sprints
        • Stories
  • APIs
    • 🎨REST API
    • 🖥️Websocket API
  • Migrations
    • 🐧Scripting language
    • 🏃‍♂️Running migrations
Powered by GitBook
On this page
  1. Tutorial
  2. Implementation
  3. Backend
  4. Services

User

In the 'users.go' file, change the struct and constructor to include the cerberus client:

type userService struct {
	txProvider     database.TxProvider
	userRepo       repositories.UserRepo
	accountRepo    repositories.AccountRepo
	jwtSecret      string
	saltRounds     int
	cerberusClient cerberus.CerberusClient
}

func NewUserService(
	txProvider database.TxProvider,
	userRepo repositories.UserRepo,
	accountRepo repositories.AccountRepo,
	jwtSecret string,
	saltRounds int,
	cerberusClient cerberus.CerberusClient) UserService {
	return &userService{
		txProvider:     txProvider,
		userRepo:       userRepo,
		accountRepo:    accountRepo,
		jwtSecret:      jwtSecret,
		saltRounds:     saltRounds,
		cerberusClient: cerberusClient,
	}
}

In the 'Register' function, include the following:

.
.
.
// CERBERUS create account resource, user and role
err = s.cerberusClient.Execute(account.Id, user.Id,
	s.cerberusClient.CreateAccountCmd(account.Id),
	s.cerberusClient.CreateResourceCmd(account.Id, "", common.Account_RT),
	s.cerberusClient.CreateUserCmd(user.Id, user.Email, user.Name),
	s.cerberusClient.CreateSuperRoleCmd(common.AccountAdministrator_R),
	s.cerberusClient.AssignRoleCmd(common.AccountAdministrator_R, user.Id),
	s.cerberusClient.CreateRolePermissionCmd(common.AccountAdministrator_R, account.Id, []string{common.CanManageAccount_P}))
if err != nil {
	if rbe := tx.Rollback(); rbe != nil {
		err = fmt.Errorf("rollback error (%v) after %w", rbe, err)
	}
	return repositories.User{}, err
}

subject := user.Id
token, err := jwtutils.Sign(subject, toClaims(user), s.jwtSecret)
if err != nil {
	if rbe := tx.Rollback(); rbe != nil {
		err = fmt.Errorf("rollback error (%v) after %w", rbe, err)
	}
	return repositories.User{}, err
}

return userWithTokens(user, token, cerberus.TokenPair{}), tx.Commit()

What's happening here?

Every time a new user registers, we create a new account for them too, which they can then add new users to themselves, as the admin of that account.

Here, we create corresponding cerberus artifacts for the user and the account, while also creating an administrator role, assigning the user to it, and adding permissions to that role.

It's important to wrap all of this within a local transaction, so that when anything fails, your local transaction can also be rolled back.

Doing it like this, local and remote cerberus changes are atomic.

We also need to update the 'userWithTokens' function, to include passing the cerberus tokenPair to the frontend (empty in this case, since it's only the register function and not Login):

func userWithTokens(user repositories.User, token string, cerberusTokenPair cerberus.TokenPair) repositories.User {
	return repositories.User{
		Token:             token,
		CerberusTokenPair: cerberusTokenPair,
		Id:                user.Id,
		AccountId:         user.AccountId,
		Email:             user.Email,
		Name:              user.Name,
	}
}

Now, in the 'Login' function change it to the following:

.
.
.
// get cerberus token
cerberusToken, err := s.cerberusClient.GetUserToken(user.AccountId, user.Id)
if err != nil {
	return repositories.User{}, err
}

subject := user.Id
token, err := jwtutils.Sign(subject, toClaims(user), s.jwtSecret)
if err != nil {
	return repositories.User{}, err
}

return userWithTokens(user, token, cerberusToken), nil

Every time a user logs in, we get a corresponding tokenpair from cerberus and pass it to the frontend. This will be useful for two reasons:

  • The frontend can communicate securely with cerberus API's

  • The frontend can pass the token back to the Acme app so we can act on the user's behalf.

Change the 'Add' function to this:

func (s *userService) Add(ctx context.Context, email, plainPassword, name, roleName string) (_ repositories.User, err error) {

	accountId := ctx.Value("accountId")
	if accountId == nil {
		return repositories.User{}, fmt.Errorf("no accountId")
	}

	tx, err := s.txProvider.GetTransaction()
	if err != nil {
		return repositories.User{}, err
	}

	user, err := s.userRepo.Save(accountId.(string), email, plainPassword, name, tx)
	if err != nil {
		if rbe := tx.Rollback(); rbe != nil {
			err = fmt.Errorf("rollback error (%v) after %w", rbe, err)
		}
		return repositories.User{}, err
	}

	err = s.cerberusClient.ExecuteWithCtx(ctx,
		s.cerberusClient.CreateUserCmd(user.Id, user.Email, user.Name),
		s.cerberusClient.AssignRoleCmd(roleName, user.Id))
	if err != nil {
		if rbe := tx.Rollback(); rbe != nil {
			err = fmt.Errorf("rollback error (%v) after %w", rbe, err)
		}
		return repositories.User{}, err
	}

	return user, tx.Commit()
}

We now accept a rolename when adding a user, and create the corresponding user and role in Cerberus too.

Since we're storing users in Cerberus, we can change the 'GetAll' function to this (don't forget to update the function signature in the top of the file):

func (s *userService) GetAll(ctx context.Context) ([]cerberus.User, error) {
	return s.cerberusClient.GetUsers(ctx)
}
PreviousServicesNextProject

Last updated 2 years ago

✍️