Skip to content

Commit

Permalink
Added Guest Invitation APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Mar 31, 2024
1 parent e760b93 commit 1f9be87
Show file tree
Hide file tree
Showing 100 changed files with 3,618 additions and 379 deletions.
18 changes: 11 additions & 7 deletions docs/design-principles/0150-all-use-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ These are the main use cases of this product that are exposed via "public" APIs

### Users (End User)

1. Assign [platform] roles to the current user
1. Assign [platform] roles to an existing user
1. Unassign [platform] roles to an existing user (except `Standard` role)
1. Invite a guest to register on the platform (a referral)
1. Resend an invitation to a guest
1. Guest verifies an invitation is still valid

### Identities

Expand All @@ -78,15 +82,15 @@ These are the main use cases of this product that are exposed via "public" APIs

1. Register a new machine

#### Passwords
#### Password Credentials

1. Authenticate the current user (with a password)
2. Register a new person (with a password)
2. Register a new person (with a password, and optional invitation)
3. Confirm registration of a person (from email)

#### Single-Sign On

1. Authenticate and (auto-register) a person from another OAuth2 provider
1. Authenticate and (auto-register) a person from another OAuth2 provider (with an optional invitation)

### Images

Expand All @@ -96,17 +100,17 @@ TBD

1. Create a new organization for the current user
2. Inspect a specific organization
3.

### Subscriptions

TBD

### User Profiles

TBD
1. Change the names, phone, time zone of the profile,
2. Change the address of the profile

## BEFFE
## Backend for Frontend

These are the main use cases of this product that are exposed via "public" APIs in the Frontend, e.g., `WebsiteHost`.

Expand Down
86 changes: 86 additions & 0 deletions docs/design-principles/0160-user-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# User Lifecycle

## Design Principles

* We want to use the email address as a unique identifier for each person's identity (in the universe), and we only want one representation of that person's identity across all organizations (tenants). However, a person will likely have several identities (i.e., a specific company identity versus a personal identity).
* We want any registered person in the system to invite another person (via an email address) to register themselves to the system. We want to capture their email address early for referral purposes.
* Only the invited person can self-register after they were "warm" invited, or they approached the platform "cold".
* When a person self-registers, regardless of whether they were invited (via an email address or not) they can choose the email address they wish to register with.
* A person can be invited to the platform, or to a specific organization. In the later case, they join that organization by default.

## Implementation

### End Users

Users of the product are managed in the `EndUser` subdomain.

* Essentially an `EndUser` is a representation of an "identity".

* Users cannot be deleted. Once registered, they can be "disabled" (`Access=Suspended`) and cannot authenticate to use the product any further.

* Once registered, the email address they registered with becomes their username. Even though they may change that email address (in the future), no two users on the platform can use the same email address.

Users self-register in the `Identity` subdomain via either `PasswordCredentials`, `SSOUsers`, or other methods, each responsible for its own onboarding flow (e.g., passwords require email confirmations and a managed registration flow, whereas SSO can just automate it).

* New users can be invited by another party and can respond to that invitation (e.g., a "warm" guest invitation) or register without an invite (e.g., a "cold" direct registration).

![User Lifecycle](../images/EndUser-Lifecycle.png)

### Organizations

An `Organization` has a type of either `Personal` or `Shared`.

* `Shared` organizations are intended for use by companies/workgroups/organizations/teams/etc and for persons and machines.
* `Personal` organizations are for each `EndUser` (person or machine) to use on the platform and cannot be shared with others.
* A person/machine will have a membership to one `Personal` organization at all times, and can have a membership to one or more `Shared` organizations.

#### Roles and Responsibilities

* Any person can have the `Member` and/or `Owner` and/or `BillingAdmin` roles in an organization.
* Only `Owner` roles can assign/unassign roles to other members.
* Any person can have the `BillingAdmin` role of an organization, but they must also have the `Owner` role.
* Every organization (`Shared` or `Personal`) will always have a person who is the "billing subscriber". This person has a fiscal responsibility to pay for billing charges for that organization (tenant). The "billing subscriber" must always have the `Owner` and `BillingAdmin` roles at all times.
* When a person creates a new organization, they automatically become an `Owner` and `BillingAdmin` of it, as well as the "billing subscriber" for it.
* From that point, they can assign the `Owner` and `BillingAdmin` roles to one or more other members of the organization.
* The "billing subscriber" responsibility must be transferred via a (voluntary) payment method submission from another person with the `BillingAdmin` (role).

#### Personal Organizations

* Every `EndUser` (person or machine) has one `Personal` organization.

* It is automatically created for them when they register on the platform. It is named after that person/machine.
* That person/machine is the only member of that organization.

* They have the roles of `Owner` and `BillingAdmin`, and they are also the "billing subscriber" for it.

* This organization cannot be deleted, and that person cannot be removed from it, nor can their roles be changed.

> This organization is very important so that the product can be accessed at all times by them, regardless of whether the owner is a member of any `Shared` organizations or not.
#### Shared Organizations

* `Shared` organizations can be created at any time by any person on the platform (not machines).
* Any other person/machine can be invited into them. When they are, they are created a `Membership` to that `Organization`, and each `Membership` maintains its own roles.
* A person (or machine) can be a `Member` (role) of any number of `Shared` organizations into which they can be invited, removed, or they can leave themselves.
* A person (not a machine) can be assigned/unassigned any number of roles in those other organizations.
* A `Shared` organization must have at least one `Owner` (role) and one `BillingAdmin` (role) at all times, and they can be the same person or different persons. Like a `Personal` organization, a `Shared` will have one and only one "billing subscriber", who is ultimately responsible for any charges for the organization.
* A `Shared` organization can be deleted. However, they have to be deleted by the designated "billing subscriber" and only once all members are removed from it.

### Guest Invitations

Guest invitations are the mechanism to introduce and refer new users to the product.

#### To The Platform

* Any authenticated user can invite a guest to the platform (i.e., without any affiliation to any organization)
* A "guest invitation" requires only an email address and has an expiry (7 days, by default)
* The person is contacted at that email address and given a link to register with the platform (in the web app). The link contains an `InvitationToken`.
* In the web app, the `InvitationToken` is first verified to check if it is valid (and not expired), and if so, the guest is presented with a registration form, which accepts an email address and password, which is pre-populated with the email address and a "guessed" name (derived from their email address). Or they can sign up with an SSO provider.
* In either case, when signing up with password credentials or signing in with SSO, the registration includes the referral `InvitationToken`.
* In the server, the `InvitationToken` is used to "accept" the "guest invitation" before registering the user.
* The user is registered with their own `Personal` organization and has no other memberships with any other organizations.

#### To An Organization

TBD

10 changes: 6 additions & 4 deletions docs/design-principles/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# Design Principles

[All Use Cases](0150-all-use-cases.md) the main use cases that we have implemented across the product (so that you do not have to implement them yourselves)

* [REST API Design Guidelines](0010-rest-api.md) how REST API's should be designed
* [REST API Framework](0020-api-framework.md) how REST API's are implemented
* [Modularity](0025-modularity.md) how we build modules that can be scaled-out later as the product grows
* [Modularity](0025-modularity.md) is how we build modules that can be scaled-out later as the product grows
* [Recording/Logging/etc](0030-recording.md) how we do crash reporting, logging, auditing, and capture usage metrics
* [Configuration Management](0040-configuration.md) how we manage configuration in the source code at design-time and runtime
* [Domain Driven Design](0050-domain-driven-design.md) how to design your aggregates, and domains
* [Dependency Injection](0060-dependency-injection.md) how you implement DI
* [Persistence](0070-persistence.md) how you design your repository layer, and promote domain events
* [Ports and Adapters](0080-ports-and-adapters.md) how we keep infrastructure components at arms length, and testable, and how we integrate with any 3rd party system
* [Ports and Adapters](0080-ports-and-adapters.md) how we keep infrastructure components at arm's length, and testable, and how we integrate with any 3rd party system
* [Authentication and Authorization](0090-authentication-authorization.md) how we authenticate and authorize users
* [Email Delivery](0100-email-delivery.md) how we send emails and deliver them asynchronously and reliably
* [Backend for Frontend](0110-back-end-for-front-end.md) the BEFFE web server that is tailored for a web UI, and brokers secure access to the backend
* [Feature Flagging](0120-feature-flagging.md) how we enable and disable features at runtime
* [Multi-Tenancy](0130-multitenancy.md) how we support multiple tenants in our system (both logically and physical infrastructure)
* [Multi-Tenancy](0130-multitenancy.md) how we support multiple tenants in our system (both logical and physical infrastructure)
* [Developer Tooling](0140-developer-tooling.md) all the tooling that is included in this codebase to help developers use this codebase effectively, and consistently
* [All Use Cases](0150-all-use-cases.md) the main use cases that we have implemented across the product (so you dont have to)
* [User Lifecycle](0160-user-lifecycle.md) how are users managed on the platform, and the relationship to their organizations
Binary file modified docs/images/EndUser-Lifecycle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Sources.pptx
Binary file not shown.
2 changes: 1 addition & 1 deletion src/AncillaryInfrastructure/Persistence/AuditRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
using Application.Persistence.Common.Extensions;
using Application.Persistence.Interfaces;
using Common;
using Common.Extensions;
using Domain.Common.ValueObjects;
using Domain.Interfaces;
using Infrastructure.Persistence.Common;
using Infrastructure.Persistence.Interfaces;
using QueryAny;
using Tasks = Common.Extensions.Tasks;

namespace AncillaryInfrastructure.Persistence;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Infrastructure.Persistence.Common;
using Infrastructure.Persistence.Interfaces;
using QueryAny;
using Tasks = Common.Extensions.Tasks;

namespace AncillaryInfrastructure.Persistence;

Expand Down
9 changes: 9 additions & 0 deletions src/Application.Interfaces/Audits.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Application.Interfaces/Audits.resx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
<data name="EndUserApplication_PlatformRolesAssigned" xml:space="preserve">
<value>EndUser.PlatformRolesAssigned</value>
</data>
<data name="EndUserApplication_PlatformRolesUnassigned" xml:space="preserve">
<value>EndUser.PlatformRolesUnassigned</value>
</data>
<data name="SingleSignOnApplication_Authenticate_AccountOnboarded" xml:space="preserve">
<value>SingleSignOn.AutoRegistered</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/Application.Interfaces/UsageConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public static class UsageScenarios
public const string Audit = "Audited";
public const string BookingCancelled = "Booking Cancelled";
public const string BookingCreated = "Booking Created";
public const string GuestInvited = "User Guest Invited";
public const string MachineRegistered = "Machine Registered";
public const string Measurement = "Measured";
public const string PersonRegistrationConfirmed = "User Registered";
Expand Down
22 changes: 22 additions & 0 deletions src/Application.Persistence.Common/Extensions/Tasks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Common;

namespace Application.Persistence.Common.Extensions;

public static class Tasks
{
/// <summary>
/// Runs all the specified <see cref="tasks" /> and returns the first <see cref="Error" /> if any
/// </summary>
public static async Task<Result<bool, Error>> WhenAllAsync(params Task<Result<bool, Error>>[] tasks)
{
var results = await Task.WhenAll(tasks);

var hasError = results.Any(result => !result.IsSuccessful);
if (hasError)
{
return results.First(result => !result.IsSuccessful).Error;
}

return results.All(result => result.Value);
}
}
9 changes: 9 additions & 0 deletions src/Application.Resources.Shared/EndUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,13 @@ public class Membership : IIdentifiableResource
public List<string> Roles { get; set; } = new();

public required string Id { get; set; }
}

public class Invitation
{
public required string EmailAddress { get; set; }

public required string FirstName { get; set; }

public string? LastName { get; set; }
}
3 changes: 2 additions & 1 deletion src/Application.Services.Shared/IEndUsersService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Task<Result<RegisteredEndUser, Error>> RegisterMachinePrivateAsync(ICallerContex
string? timezone,
string? countryCode, CancellationToken cancellationToken);

Task<Result<RegisteredEndUser, Error>> RegisterPersonPrivateAsync(ICallerContext caller, string emailAddress,
Task<Result<RegisteredEndUser, Error>> RegisterPersonPrivateAsync(ICallerContext caller, string? invitationToken,
string emailAddress,
string firstName, string? lastName, string? timezone, string? countryCode, bool termsAndConditionsAccepted,
CancellationToken cancellationToken);
}
6 changes: 6 additions & 0 deletions src/Application.Services.Shared/INotificationsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ namespace Application.Services.Shared;
/// </summary>
public interface INotificationsService
{
/// <summary>
/// Notifies a user, via email, that they have been invited to register with the platform
/// </summary>
Task<Result<Error>> NotifyGuestInvitationToPlatformAsync(ICallerContext caller, string token,
string inviteeEmailAddress, string inviteeName, string inviterName, CancellationToken cancellationToken);

/// <summary>
/// Notifies a user, via email, to confirm their account registration
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/Application.Services.Shared/IUserProfilesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ Task<Result<UserProfile, Error>> CreatePersonProfilePrivateAsync(ICallerContext

Task<Result<Optional<UserProfile>, Error>> FindPersonByEmailAddressPrivateAsync(ICallerContext caller,
string emailAddress, CancellationToken cancellationToken);

Task<Result<UserProfile, Error>> GetProfilePrivateAsync(ICallerContext caller, string userId,
CancellationToken cancellationToken);
}
2 changes: 2 additions & 0 deletions src/Application.Services.Shared/IWebsiteUiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ namespace Application.Services.Shared;
public interface IWebsiteUiService
{
string ConstructPasswordRegistrationConfirmationPageUrl(string token);

string CreateRegistrationPageUrl(string token);
}
2 changes: 1 addition & 1 deletion src/CarsInfrastructure/Persistence/CarRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
using CarsApplication.Persistence.ReadModels;
using CarsDomain;
using Common;
using Common.Extensions;
using Domain.Common.ValueObjects;
using Domain.Interfaces;
using Infrastructure.Persistence.Common;
using Infrastructure.Persistence.Interfaces;
using QueryAny;
using Unavailability = CarsApplication.Persistence.ReadModels.Unavailability;
using Tasks = Common.Extensions.Tasks;

namespace CarsInfrastructure.Persistence;

Expand Down
2 changes: 2 additions & 0 deletions src/Domain.Services.Shared/DomainServices/ITokensService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public interface ITokensService
{
APIKeyToken CreateAPIKey();

string CreateGuestInvitationToken();

string CreateJWTRefreshToken();

string CreatePasswordResetToken();
Expand Down
Loading

0 comments on commit 1f9be87

Please sign in to comment.