Signup registers a new user account (self-service). Creates a principal and assigns a workspace: - If the user's email was pre-invited to a workspace, joins that workspace. - Otherwise, creates a new workspace with the user as admin.
(ctx context.Context, req *connect.Request[v1pb.SignupRequest])
| 209 | // - If the user's email was pre-invited to a workspace, joins that workspace. |
| 210 | // - Otherwise, creates a new workspace with the user as admin. |
| 211 | func (s *AuthService) Signup(ctx context.Context, req *connect.Request[v1pb.SignupRequest]) (*connect.Response[v1pb.LoginResponse], error) { |
| 212 | request := req.Msg |
| 213 | if request.Email == "" { |
| 214 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("email must be set")) |
| 215 | } |
| 216 | if request.Title == "" { |
| 217 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("title must be set")) |
| 218 | } |
| 219 | if request.Password == "" { |
| 220 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("password must be set")) |
| 221 | } |
| 222 | if err := validateEndUserEmail(request.Email); err != nil { |
| 223 | return nil, err |
| 224 | } |
| 225 | |
| 226 | // Check if principal already exists. |
| 227 | existingUser, err := s.store.GetUserByEmail(ctx, request.Email) |
| 228 | if err != nil { |
| 229 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to find user by email")) |
| 230 | } |
| 231 | if existingUser != nil { |
| 232 | return nil, connect.NewError(connect.CodeAlreadyExists, errors.Errorf("email %s is already registered", request.Email)) |
| 233 | } |
| 234 | |
| 235 | // Resolve the target workspace (read-only) so we can check restrictions BEFORE |
| 236 | // any write — otherwise a rejected signup would leave an orphan user/workspace behind. |
| 237 | targetWorkspaceID, _, err := s.resolveWorkspaceIDByEmail(ctx, request.Email) |
| 238 | if err != nil { |
| 239 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to resolve target workspace")) |
| 240 | } |
| 241 | // Announce the workspace on every exit path so denied signups (DisallowSignup, |
| 242 | // password restriction) still produce audit entries. Uses targetWorkspaceID (resolved |
| 243 | // before any writes) rather than the provisioned workspaceID. |
| 244 | defer func() { common.SetAuditWorkspaceID(ctx, targetWorkspaceID) }() |
| 245 | |
| 246 | restriction, err := getAccountRestriction( |
| 247 | ctx, |
| 248 | s.store, |
| 249 | s.licenseService, |
| 250 | s.profile.SaaS, |
| 251 | targetWorkspaceID, |
| 252 | ) |
| 253 | if err != nil { |
| 254 | return nil, err |
| 255 | } |
| 256 | if restriction.DisallowSignup { |
| 257 | return nil, connect.NewError(connect.CodePermissionDenied, errors.Errorf("sign up is disallowed for this workspace %v", targetWorkspaceID)) |
| 258 | } |
| 259 | if err := validatePasswordWithRestriction(request.Password, convertToStorePasswordRestriction(restriction.PasswordRestriction)); err != nil { |
| 260 | return nil, err |
| 261 | } |
| 262 | |
| 263 | workspaceID, err := s.provisionWorkspaceForNewUser(ctx, request.Email) |
| 264 | if err != nil { |
| 265 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to provision workspace")) |
| 266 | } |
| 267 | |
| 268 | passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) |
nothing calls this directly
no test coverage detected