Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/27843-invite-link-only-outcome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/plugin-auth': patch
'@objectstack/platform-objects': patch
'@objectstack/spec': patch
---

feat(plugin-auth): an invite says whether its mail was delivered, and a pending invitation's link can be copied from Setup → Invitations

Clause-②: yes (widening)
Metadata-protocol: none

On a deployment that does not deliver mail (the stock `log` transport, or no email service at all), inviting someone used to toast "Invitation sent to …" while nothing left the server, and nothing offered the accept link, so the invitee never arrived.

- `POST /api/v1/auth/organization/invite-member` (invite and resend) now answers a top-level `outcome` beside the invitation row: `sent` when the deployment delivers mail, `link_only` when it does not. It is decided at call time by the same rule sign-up uses, the email service's own `isConfigured()` answer, so a wired `log` transport is not delivery. The row's columns are unchanged.
- The four `invite_user` actions (Users, Members, Invitations, the organization page) and `resend_invitation` declare `outcomeMessages`: the `link_only` message says the invitation was created and no email went out, and points to Copy Invite Link. `successMessage` stays the fallback.
- New action `sys_invitation.copy_invitation_link` (Copy Invite Link, on the invitation row and record header): answers `{ acceptUrl }`, the same link the invitation mail carries, in a copyable result dialog. The link admits whoever opens it, so the server answers it only to an owner, admin or delegated admin of the invitation's organization, only for a pending invitation, and never while the environment is cloud-managed (`409 RESOURCE_CONFLICT`); it is never logged.
- `PUBLIC_AUTH_FEATURES` books the new action under the `organization` and `cloudManaged` gates.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
label: "Invite User",
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
successMessage: "Invitation sent to {{ result.email }}",
outcomeMessages: {
sent: "Invitation sent to ${result.email}",
link_only: "Invitation created for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
},
params: {
role: {
options: {
Expand Down Expand Up @@ -577,6 +581,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
label: "Invite User",
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
successMessage: "Invitation sent to {{ result.email }}",
outcomeMessages: {
sent: "Invitation sent to ${result.email}",
link_only: "Invitation created for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
},
params: {
role: {
options: {
Expand Down Expand Up @@ -664,6 +672,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
label: "Invite User",
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
successMessage: "Invitation sent to {{ result.email }}",
outcomeMessages: {
sent: "Invitation sent to ${result.email}",
link_only: "Invitation created for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
},
params: {
role: {
options: {
Expand Down Expand Up @@ -794,6 +806,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
label: "Invite User",
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
successMessage: "Invitation sent to {{ result.email }}",
outcomeMessages: {
sent: "Invitation sent to ${result.email}",
link_only: "Invitation created for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
},
params: {
role: {
options: {
Expand All @@ -813,7 +829,22 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
resend_invitation: {
label: "Resend Invitation",
successMessage: "Invitation resent"
successMessage: "Invitation resent",
outcomeMessages: {
sent: "Invitation resent to ${result.email}",
link_only: "Invitation renewed for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
}
},
copy_invitation_link: {
label: "Copy Invite Link",
resultDialog: {
title: "Invite link",
description: "Anyone who opens this link can accept the invitation, so share it only with the person you invited. It stops working once the invitation is accepted, declined, canceled or expires.",
acknowledge: "Done",
fields: {
acceptUrl: "Invite link"
}
}
},
accept_invitation: {
label: "Accept Invitation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
label: "Invitar usuario",
description: "Invita a alguien por su dirección de correo electrónico. Se une a esta organización con el rol elegido cuando acepta la invitación.",
successMessage: "Invitación enviada a {{ result.email }}",
outcomeMessages: {
sent: "Invitación enviada a ${result.email}",
link_only: "Invitación creada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
},
params: {
role: {
options: {
Expand Down Expand Up @@ -577,6 +581,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
label: "Invitar usuario",
description: "Invita a alguien por su dirección de correo electrónico. Se une a esta organización con el rol elegido cuando acepta la invitación.",
successMessage: "Invitación enviada a {{ result.email }}",
outcomeMessages: {
sent: "Invitación enviada a ${result.email}",
link_only: "Invitación creada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
},
params: {
role: {
options: {
Expand Down Expand Up @@ -664,6 +672,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
label: "Invitar usuario",
description: "Invita a alguien por su dirección de correo electrónico. Se une a esta organización con el rol elegido cuando acepta la invitación.",
successMessage: "Invitación enviada a {{ result.email }}",
outcomeMessages: {
sent: "Invitación enviada a ${result.email}",
link_only: "Invitación creada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
},
params: {
role: {
options: {
Expand Down Expand Up @@ -794,6 +806,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
label: "Invitar usuario",
description: "Invita a alguien por su dirección de correo electrónico. Se une a esta organización con el rol elegido cuando acepta la invitación.",
successMessage: "Invitación enviada a {{ result.email }}",
outcomeMessages: {
sent: "Invitación enviada a ${result.email}",
link_only: "Invitación creada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
},
params: {
role: {
options: {
Expand All @@ -813,7 +829,22 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
resend_invitation: {
label: "Reenviar invitación",
successMessage: "Invitación reenviada"
successMessage: "Invitación reenviada",
outcomeMessages: {
sent: "Invitación reenviada a ${result.email}",
link_only: "Invitación renovada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
}
},
copy_invitation_link: {
label: "Copiar enlace de invitación",
resultDialog: {
title: "Enlace de invitación",
description: "Cualquiera que abra este enlace puede aceptar la invitación, así que compártelo solo con la persona que invitaste. Deja de funcionar cuando la invitación se acepta, se rechaza, se cancela o caduca.",
acknowledge: "Hecho",
fields: {
acceptUrl: "Enlace de invitación"
}
}
},
accept_invitation: {
label: "Aceptar invitación",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
label: "ユーザーを招待",
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
successMessage: "{{ result.email }} に招待を送信しました",
outcomeMessages: {
sent: "${result.email} に招待を送信しました",
link_only: "${result.email} の招待を作成しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
},
params: {
role: {
options: {
Expand Down Expand Up @@ -577,6 +581,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
label: "ユーザーを招待",
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
successMessage: "{{ result.email }} に招待を送信しました",
outcomeMessages: {
sent: "${result.email} に招待を送信しました",
link_only: "${result.email} の招待を作成しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
},
params: {
role: {
options: {
Expand Down Expand Up @@ -664,6 +672,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
label: "ユーザーを招待",
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
successMessage: "{{ result.email }} に招待を送信しました",
outcomeMessages: {
sent: "${result.email} に招待を送信しました",
link_only: "${result.email} の招待を作成しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
},
params: {
role: {
options: {
Expand Down Expand Up @@ -794,6 +806,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
label: "ユーザーを招待",
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
successMessage: "{{ result.email }} に招待を送信しました",
outcomeMessages: {
sent: "${result.email} に招待を送信しました",
link_only: "${result.email} の招待を作成しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
},
params: {
role: {
options: {
Expand All @@ -813,7 +829,22 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
},
resend_invitation: {
label: "招待を再送",
successMessage: "招待を再送しました"
successMessage: "招待を再送しました",
outcomeMessages: {
sent: "${result.email} に招待を再送しました",
link_only: "${result.email} の招待を更新しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
}
},
copy_invitation_link: {
label: "招待リンクをコピー",
resultDialog: {
title: "招待リンク",
description: "このリンクを開いた人は誰でも招待を承諾できるため、招待した本人にのみ共有してください。招待が承諾、辞退、キャンセルされるか、期限が切れると、リンクは使えなくなります。",
acknowledge: "完了",
fields: {
acceptUrl: "招待リンク"
}
}
},
accept_invitation: {
label: "招待を受諾",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
label: "邀请用户",
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
successMessage: "已向 {{ result.email }} 发送邀请",
outcomeMessages: {
sent: "已向 ${result.email} 发送邀请",
link_only: "已为 ${result.email} 创建邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
},
params: {
role: {
options: {
Expand Down Expand Up @@ -577,6 +581,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
label: "邀请用户",
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
successMessage: "已向 {{ result.email }} 发送邀请",
outcomeMessages: {
sent: "已向 ${result.email} 发送邀请",
link_only: "已为 ${result.email} 创建邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
},
params: {
role: {
options: {
Expand Down Expand Up @@ -664,6 +672,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
label: "邀请用户",
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
successMessage: "已向 {{ result.email }} 发送邀请",
outcomeMessages: {
sent: "已向 ${result.email} 发送邀请",
link_only: "已为 ${result.email} 创建邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
},
params: {
role: {
options: {
Expand Down Expand Up @@ -794,6 +806,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
label: "邀请用户",
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
successMessage: "已向 {{ result.email }} 发送邀请",
outcomeMessages: {
sent: "已向 ${result.email} 发送邀请",
link_only: "已为 ${result.email} 创建邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
},
params: {
role: {
options: {
Expand All @@ -813,7 +829,22 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
},
resend_invitation: {
label: "重新发送邀请",
successMessage: "邀请已重新发送"
successMessage: "邀请已重新发送",
outcomeMessages: {
sent: "已向 ${result.email} 重新发送邀请",
link_only: "已为 ${result.email} 续期邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
}
},
copy_invitation_link: {
label: "复制邀请链接",
resultDialog: {
title: "邀请链接",
description: "任何打开此链接的人都可以接受该邀请,请只分享给您邀请的人。邀请被接受、拒绝、取消或过期后,链接即失效。",
acknowledge: "完成",
fields: {
acceptUrl: "邀请链接"
}
}
},
accept_invitation: {
label: "接受邀请",
Expand Down
Loading
Loading