CreateUser creates a user in the caller's workspace (admin action, self-hosted only). In SaaS mode, admins should add users via workspace IAM policy instead.
(ctx context.Context, request *connect.Request[v1pb.CreateUserRequest])
| 204 | // CreateUser creates a user in the caller's workspace (admin action, self-hosted only). |
| 205 | // In SaaS mode, admins should add users via workspace IAM policy instead. |
| 206 | func (s *UserService) CreateUser(ctx context.Context, request *connect.Request[v1pb.CreateUserRequest]) (*connect.Response[v1pb.User], error) { |
| 207 | if s.profile.SaaS { |
| 208 | return nil, connect.NewError(connect.CodeUnimplemented, errors.Errorf("CreateUser is not available in SaaS mode, add users via workspace IAM policy instead")) |
| 209 | } |
| 210 | |
| 211 | if request.Msg.User == nil { |
| 212 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("user must be set")) |
| 213 | } |
| 214 | if request.Msg.User.Email == "" { |
| 215 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("email must be set")) |
| 216 | } |
| 217 | if request.Msg.User.Title == "" { |
| 218 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("user title must be set")) |
| 219 | } |
| 220 | if request.Msg.User.Phone != "" { |
| 221 | if err := common.ValidatePhone(request.Msg.User.Phone); err != nil { |
| 222 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrapf(err, "invalid phone %q", request.Msg.User.Phone)) |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | workspaceID := common.GetWorkspaceIDFromContext(ctx) |
| 227 | email := request.Msg.User.Email |
| 228 | |
| 229 | if err := validateEmailWithDomains(ctx, s.licenseService, s.store, workspaceID, email, false); err != nil { |
| 230 | return nil, err |
| 231 | } |
| 232 | |
| 233 | existingUser, err := s.store.GetUserByEmail(ctx, email) |
| 234 | if err != nil { |
| 235 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to find user by email")) |
| 236 | } |
| 237 | if existingUser != nil { |
| 238 | return nil, connect.NewError(connect.CodeAlreadyExists, errors.Errorf("email %s exists", email)) |
| 239 | } |
| 240 | |
| 241 | // Validate password. |
| 242 | password := request.Msg.User.Password |
| 243 | if password != "" { |
| 244 | if err := s.validatePassword(ctx, workspaceID, password); err != nil { |
| 245 | return nil, err |
| 246 | } |
| 247 | } else { |
| 248 | pwd, err := common.RandomString(20) |
| 249 | if err != nil { |
| 250 | return nil, connect.NewError(connect.CodeInternal, errors.Errorf("failed to generate random password for user")) |
| 251 | } |
| 252 | password = pwd |
| 253 | } |
| 254 | passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) |
| 255 | if err != nil { |
| 256 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to generate password hash")) |
| 257 | } |
| 258 | |
| 259 | if err := s.preAddUserGuard(ctx, workspaceID); err != nil { |
| 260 | return nil, err |
| 261 | } |
| 262 | |
| 263 | user, err := s.store.CreateUser(ctx, &store.UserMessage{ |
no test coverage detected