-
Notifications
You must be signed in to change notification settings - Fork 540
feat: add a button to resend an invitation email to a user including a representative for tprm #2894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add a button to resend an invitation email to a user including a representative for tprm #2894
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -227,6 +227,26 @@ | |
| modalStore.trigger(modal); | ||
| } | ||
|
|
||
| function modalSendInvitation(id: string, email: string, action: string): void { | ||
| const modalComponent: ModalComponent = { | ||
| ref: ConfirmModal, | ||
| props: { | ||
| _form: { id: id, urlmodel: getModelInfo('representatives').urlModel, email: email }, | ||
| id: id, | ||
| debug: false, | ||
| URLModel: getModelInfo('representatives').urlModel, | ||
| formAction: action | ||
| } | ||
| }; | ||
| const modal: ModalSettings = { | ||
| type: 'component', | ||
| component: modalComponent, | ||
| title: m.confirmModalTitle(), | ||
| body: `Do you want to send the invitation to ${email}?` | ||
| }; | ||
| modalStore.trigger(modal); | ||
| } | ||
|
Comment on lines
+230
to
+248
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use i18n message instead of hard‑coded confirmation text The confirmation body text ( 🤖 Prompt for AI Agents |
||
|
|
||
| function getReverseForeignKeyEndpoint({ | ||
| parentModel, | ||
| targetUrlModel, | ||
|
|
@@ -632,6 +652,21 @@ | |
| {/if} | ||
|
|
||
| {#if displayEditButton()} | ||
| {#if data.urlModel === 'representatives' && data.data.user} | ||
| <button | ||
| class="btn preset-filled-ghost-500 mr-2" | ||
| onclick={() => | ||
| modalSendInvitation( | ||
| data.data.id, | ||
| data.data.email, | ||
| '/representatives/send-invitation' | ||
| )} | ||
| data-testid="send-invitation-button" | ||
| > | ||
| <i class="fa-solid fa-envelope mr-2"></i> | ||
| Send invitation | ||
| </button> | ||
|
Comment on lines
654
to
+668
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Localize the “Send invitation” button label The button label 🤖 Prompt for AI Agents |
||
| {/if} | ||
| {#if data.data.state === 'Created'} | ||
| <Tooltip | ||
| open={openStateRA && !data.data.approver} | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BASE_API_URL } from '$lib/utils/constants'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { RequestHandler } from './$types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const POST: RequestHandler = async (event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requestInitOptions: RequestInit = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'POST' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const form = await event.request.formData(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const raw = form.get('__superform_json') as string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let parsed = JSON.parse(raw); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let email: string | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Array.isArray(parsed)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mapping = parsed[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (mapping && typeof mapping === 'object' && typeof mapping.email === 'number') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const emailIndex = mapping.email; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email = parsed[emailIndex]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email = parsed.find((v: any) => typeof v === 'string' && v.includes('@')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (parsed && typeof parsed === 'object') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email = parsed.email ?? parsed['__email'] ?? undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const res = await fetch(`${BASE_API_URL}/iam/send-invitation/`, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { 'Content-Type': 'application/json' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ email: email }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Response(null, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate form payload, ensure email is present, and propagate backend response The handler is quite optimistic right now:
A more robust version could look like: export const POST: RequestHandler = async (event) => {
- const requestInitOptions: RequestInit = {
- method: 'POST'
- };
- const form = await event.request.formData();
- const raw = form.get('__superform_json') as string;
-
- let parsed = JSON.parse(raw);
-
- let email: string | undefined;
+ const form = await event.request.formData();
+ const raw = form.get('__superform_json');
+ if (typeof raw !== 'string') {
+ return new Response(JSON.stringify({ error: 'invalidFormPayload' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(raw);
+ } catch {
+ return new Response(JSON.stringify({ error: 'invalidJsonPayload' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ let email: string | undefined;
- if (Array.isArray(parsed)) {
+ if (Array.isArray(parsed)) {
const mapping = parsed[0];
if (mapping && typeof mapping === 'object' && typeof mapping.email === 'number') {
const emailIndex = mapping.email;
email = parsed[emailIndex];
} else {
email = parsed.find((v: any) => typeof v === 'string' && v.includes('@'));
}
- } else if (parsed && typeof parsed === 'object') {
+ } else if (parsed && typeof parsed === 'object') {
email = parsed.email ?? parsed['__email'] ?? undefined;
}
- const res = await fetch(`${BASE_API_URL}/iam/send-invitation/`, {
+ if (!email) {
+ return new Response(JSON.stringify({ error: 'emailRequired' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ const res = await fetch(`${BASE_API_URL}/iam/send-invitation/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email })
});
- return new Response(null, {
- headers: {
- 'Content-Type': 'application/json'
- }
- });
+
+ const text = await res.text();
+ return new Response(text || null, {
+ status: res.status,
+ headers: {
+ 'Content-Type': res.headers.get('Content-Type') ?? 'application/json'
+ }
+ });
};This keeps the email extraction logic but fails fast on bad form/JSON, guarantees an email before calling the API, and forwards the backend’s status and body so the UI can react appropriately. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden SendInvtationView: input validation, status codes, and info leakage
A few points to address here:
request.data["email"]will raise if the field is missing or malformed. Preferrequest.data.get("email")with a400response when it’s absent/invalid.HTTP_500_INTERNAL_SERVER_ERRORfor “No user associated with this email” is misleading; it’s a client issue. Also, this message + distinct status leaks whether an email exists, unlikePasswordResetView, which avoids that. Consider always returning202for non‑local/non‑existing users to avoid email enumeration.print(...)should be replaced bylogger.info/logger.errorfor consistency with the rest of the file.SendInvtationViewhas a typo; renaming it toSendInvitationViewwill avoid confusion (remember to update imports/URL mapping).An example of a more robust implementation:
Also consider tightening
permission_classes(e.g. requiring authentication/role) if you don’t intend this endpoint to be publicly callable.🤖 Prompt for AI Agents