Invitations
An invitation brings people onto the portal on purpose: staff choose who receives it and the roles it grants, and each recipient is emailed a link of their own. Following it creates their login on the contact that already exists for them — with a password or an external sign-in — or, if they already have one, adds the roles to it. It is how a portal that doesn't let strangers register brings its users on board.
How an invitation travels
An invitation is written as a draft, and sending it changes nothing but its status. The portal's own timed process — running every five minutes in each portal instance — notices it, works out the recipients, and emails each a link with a code of its own. Only the code's hash is stored, so reading the table gives nobody a working link.
- Draft — choose who receives it, the roles it grants, its expiry and the email it goes out in.
- Ready to send — Send was pressed; the portal picks it up within a few minutes.
- Sent — the recipients exist and their links work until it expires (30 days after sending, unless you set a date).
- Expired or Canceled — the links stop working.
Who receives it
An invitation goes to a contact, to an account — every active contact it has — or to whoever criteria written with the FetchXML query builder return. Recipients are worked out when the invitation is sent, with the access of the person who sent it, and sending it again later reaches whoever matches by then and hasn't received it, without emailing anybody twice. Contacts without an email address are skipped, and so, unless the invitation says otherwise, are contacts who already have a portal login.
Who receives it is fixed once sent
Once an invitation leaves Draft, who receives it and the roles it grants can't change — the roles are read when somebody accepts, so changing them would change what people already holding a link receive. Its expiry date can still move. Each recipient can be resent a new link or revoked on their own.
The email
Left to itself, the portal sends its own email, from its localization (app.email-templates.invitation). To write your own, create a contact or global email template in Dataverse, put {{invitation-link}} where the link should go, and choose it on the invitation. A template without the placeholder fails the recipient rather than sending an email nobody can act on.
Accepting
The link opens /Account/Invitation. Someone without a login creates one there — the invited contact's own email address, with a password or any configured external sign-in — even when sign-up is turned off. Someone with a login signs in and accepts; someone signed in as another person is asked to sign out. Following a link mailed to the address counts as confirming it, so no confirmation email follows. A code that is unknown, used, revoked, expired or for another portal all say the same thing.
The public address
The timed process works outside any request, so it reads the address the portal is reached at from PowerPortalsPro:PublicUrl — required, for every portal. Its links point there, and it is the address the portal's license is checked for when deciding which invitations are its own. Set it per environment so UAT's links point at UAT; in Azure App Service it is the PowerPortalsPro__PublicUrl application setting, marked as a deployment slot setting. A portal running on localhost sends no invitations unless PowerPortalsPro:Invitations:DevelopmentWebsiteId names the website record to act as.
{
"PowerPortalsPro": {
"PublicUrl": "https://portal.contoso.com"
}
}
Wiring it up
InvitationAdmin lists the invitations, with Send and Cancel; InvitationForm edits one, with its recipients beneath. Gate the page on the portal administrator check, like the other admin pages, and give the form a page of its own: criteria are written with the FetchXML query builder, which wants the room. Every change is made as the signed-in member of staff, so Dataverse security applies here as it does in the model-driven app.
// Opens each invitation on a page of its own
<InvitationAdmin
invitationUrl={(id) => `/InvitationAdmin/Invitation/${id ?? 'new'}`}
onNavigate={(url) => navigate(url)}
/>@attribute [Route("InvitationAdmin")]
@attribute [AuthorizeSystemAdmin]
@using PowerPortalsPro.Web.Blazor.FluentUI.Components
<SystemAdminView>
<Authorized>
<InvitationAdmin InvitationUrl="InvitationAdmin/Invitation?invitationId={0}" />
</Authorized>
</SystemAdminView>// One invitation, in a RecordContext with a Save button
const { id } = useParams();
const isCreateMode = id === 'new';
<RecordContext table="ppp_invitation" {...(isCreateMode ? {} : { id })}>
<SaveContextButton appearance="primary" />
<InvitationForm />
</RecordContext>
InvitationAdmin
InvitationAdmin Class
Parameters
Name | Type | Default | Description |
|---|---|---|---|
InvitationUrl | string? | Where an invitation is edited, as a URL whose |
InvitationUrlInvitationForm
InvitationForm Class
Properties
Name | Type | Default | Description |
|---|---|---|---|
Record | TableRecord? | The invitation being edited. |
Record