In the 'users.go' file, change the struct and constructor to include the cerberus client:
Copy 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:
Copy .
.
.
// 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):
Copy 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:
Copy .
.
.
// 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:
Copy 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):
Copy func (s *userService) GetAll(ctx context.Context) ([]cerberus.User, error) {
return s.cerberusClient.GetUsers(ctx)
}