Skip to content

Commit 6ce8e72

Browse files
authored
docs: add pre-request with magic link (#759)
1 parent e15839f commit 6ce8e72

File tree

47 files changed

+312
-78
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+312
-78
lines changed

client/src/app/request-feedback/request-feedback.component.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { APP_BASE_HREF } from '@angular/common';
12
import { Component, DOCUMENT, ViewEncapsulation, inject } from '@angular/core';
23
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
34
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@@ -11,6 +12,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
1112
import { MatTooltipModule } from '@angular/material/tooltip';
1213
import { ActivatedRoute, Router } from '@angular/router';
1314
import { concatMap, from, toArray } from 'rxjs';
15+
import { environment } from '../../environments/environment';
1416
import { AuthService } from '../shared/auth';
1517
import { MultiAutocompleteEmailComponent } from '../shared/autocomplete-email';
1618
import { ConfirmBeforeSubmitDirective } from '../shared/confirm-before-submit';
@@ -57,6 +59,8 @@ import {
5759
export class RequestFeedbackComponent {
5860
private document = inject(DOCUMENT);
5961

62+
private appBaseHref = inject(APP_BASE_HREF);
63+
6064
private router = inject(Router);
6165

6266
private activatedRoute = inject(ActivatedRoute);
@@ -187,11 +191,21 @@ export class RequestFeedbackComponent {
187191
this.feedbackService.preRequestToken({ message, shared }).subscribe(({ token }) => {
188192
this.navigateToSuccess({
189193
method: 'generate',
190-
magicLink: `${this.document.location.origin}/pre-request/token/${token}`,
194+
magicLink: this.buildMagicLink(token),
191195
});
192196
});
193197
}
194198

199+
private buildMagicLink(token: string) {
200+
// IMPORTANT NOTE:
201+
// The magic link does not contain the locale.
202+
// The redirection to `/fr` or `/en` occurs when the colleague visits the magic link page (see `src/404.html` for details).
203+
// However, since this redirection is not functional in the `dev-local` environment, the locale is explicitly added only in this case.
204+
const locale = environment.alias === 'dev-local' ? this.appBaseHref : '/';
205+
206+
return `${this.document.location.origin}${locale}pre-request/token/${token}`;
207+
}
208+
195209
private navigateToSuccess(state: RequestFeedbackSuccess) {
196210
this.router.navigate(['success'], { relativeTo: this.activatedRoute, state });
197211
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Pre-request feedback with magic link
2+
3+
## User story
4+
5+
As a Zenika employee, I would like to request feedback from colleagues when I don't know their email addresses upfront. Instead of specifying individual emails, I can generate a shareable "magic link" that colleagues can use to provide their email address and trigger a feedback request.
6+
7+
This is particularly useful when:
8+
9+
- I want to share the request in a Slack channel or Teams group
10+
- My colleagues' company blocks emails sent via Mailgun. However, they can still give me feedback using their personal email addresses, which I don't know in advance.
11+
12+
Once a colleague uses the magic link and provides their email, they receive a feedback request email just like in the standard feedback request workflow.
13+
14+
## Technical specifications
15+
16+
Be sure to read [Request feedback](./request-feedback) first, as this feature is an enhancement of that workflow.
17+
18+
### Magic link generation workflow
19+
20+
1. The requester (authenticated user) navigates to `/request`
21+
2. Selects **"With a magic link"** method
22+
3. Fills and Submits the form (the recipients field is disabled for this method)
23+
4. Client calls `POST /feedback/pre-request/token`
24+
5. Server creates a document in the `feedbackPreRequestToken` collection and returns `{ token: string }`
25+
6. Client navigates to the success page displaying the magic link
26+
27+
The requester can copy this link and share it through any channel (Slack, Teams, email, etc.).
28+
29+
### Firestore schema
30+
31+
A document is added in the `feedbackPreRequestToken` collection to store magic link tokens:
32+
33+
```ts
34+
const preRequestToken: FeedbackPreRequestToken = {
35+
receiverEmail: 'pinocchio@zenika.com', // Who will receive the feedback
36+
message: 'Hi team, please share your feedback...', // Encrypted
37+
shared: true, // Whether to share with manager
38+
expiresAt: 1711662999463, // Timestamp
39+
usedBy: ['gimini@gmail.com', 'gepetto@gmail.com'], // Emails that used this token
40+
};
41+
```
42+
43+
- The document ID is the token itself (auto-generated by Firestore).
44+
- The `usedBy` array tracks which emails have already used this token
45+
- Each use of the token triggers a separate feedback request
46+
47+
### Magic link format
48+
49+
The magic link format is: `{origin}/pre-request/token/{token}`
50+
51+
**Example:**
52+
53+
```txt
54+
https://feedzback.znk.io/pre-request/token/abc123xyz
55+
```
56+
57+
The magic link does not contain the locale.
58+
The redirection to `/fr` or `/en` occurs when the colleague visits the magic link page (see `src/404.html` for details).
59+
60+
However, since this redirection is not functional in the `dev-local` environment, the locale is explicitly added only in this case.
61+
62+
**Example in `dev-local` environment:**
63+
64+
```txt
65+
https://feedzback.znk.io/fr/pre-request/token/abc123xyz
66+
```
67+
68+
### Magic link usage workflow
69+
70+
When a colleague accesses the magic link:
71+
72+
1. The colleague (not authenticated) visits `/pre-request/token/{token}`
73+
2. The colleague's browser redirects to the appropriate locale (example: `/fr/pre-request/token/{token}`)
74+
3. Client calls `GET /feedback/check-pre-request/{token}` to validate the token
75+
4. If valid, the page displays the details of the pre-request and a form field allowing the colleague to enter their email address
76+
5. The colleague enters their email and submits
77+
6. Client calls `POST /feedback/pre-request/email` with `{ token, recipient }`
78+
7. Server validates the token and email, then:
79+
- Adds the email to the `usedBy` array in `feedbackPreRequestToken` document
80+
- Triggers the standard feedback request workflow:
81+
- Creates `feedback` and `feedbackRequestToken` documents
82+
- Sends a feedback request email to the colleague
83+
8. Client navigates to a success page confirming the email was sent
84+
85+
From this point forward, the flow is identical to the standard [Reply to feedback request](./reply-to-feedback-request) workflow.
86+
87+
### Token constraints
88+
89+
- **Expiration** configured via `FEEDBACK_PRE_REQUEST_EXPIRATION_IN_DAYS` constant (3 days)
90+
- **Maximum uses** configured via `FEEDBACK_PRE_REQUEST_MAX_USES` constant (10 uses per token)
91+
92+
When a colleague attempts to use a magic link, the following checks are performed:
93+
94+
| Validation | Error Type | Description |
95+
| ---------------- | ------------------------ | ----------------------------------------------- |
96+
| Token exists | `token_invalid` | The token doesn't exist in the database |
97+
| Not expired | `token_expired` | The current time exceeds `expiresAt` |
98+
| Under max uses | `token_max_uses_reached` | The `usedBy` array length has reached the limit |
99+
| Email not used | `recipient_already_used` | The email is already in the `usedBy` array |
100+
| Not self-request | `recipient_forbidden` | The email matches the `receiverEmail` |
101+
102+
If any validation fails, a `BadRequestException` or `ForbiddenException` is thrown with the corresponding error type.
103+
104+
## Links
105+
106+
- **Client**
107+
- [`RequestFeedbackComponent`](https://github.com/Zenika/feedzback/blob/main/client/src/app/request-feedback/request-feedback.component.ts)
108+
- [`PreRequestFeedbackComponent`](https://github.com/Zenika/feedzback/blob/main/client/src/app/pre-request-feedback/pre-request-feedback.component.ts)
109+
- **Server**
110+
- [`FeedbackController`](https://github.com/Zenika/feedzback/blob/main/server/src/feedback/feedback.controller.ts)
111+
- `preRequestToken` - Creates pre-request token
112+
- `checkPreRequest` - Validates token and returns details
113+
- `preRequestEmail` - Processes email submission and triggers feedback request

docs-source/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const sidebars: SidebarsConfig = {
1010
label: 'Business cases',
1111
items: [
1212
'business-cases/request-feedback',
13+
'business-cases/pre-request-feedback',
1314
'business-cases/reply-to-feedback-request',
1415
'business-cases/give-spontaneous-feedback',
1516
'business-cases/feedback-draft',

docs/404.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<meta name="generator" content="Docusaurus v3.9.2">
66
<title data-rh="true">Page Not Found | FeedZback</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://feedzback.znk.io/feedzback/404.html"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docusaurus_tag" content="default"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="docsearch:docusaurus_tag" content="default"><meta data-rh="true" property="og:title" content="Page Not Found | FeedZback"><link data-rh="true" rel="icon" href="/feedzback/img/favicon.svg"><link data-rh="true" rel="canonical" href="https://feedzback.znk.io/feedzback/404.html"><link data-rh="true" rel="alternate" href="https://feedzback.znk.io/feedzback/404.html" hreflang="en"><link data-rh="true" rel="alternate" href="https://feedzback.znk.io/feedzback/404.html" hreflang="x-default"><link rel="alternate" type="application/rss+xml" href="/feedzback/blog/rss.xml" title="FeedZback RSS Feed">
77
<link rel="alternate" type="application/atom+xml" href="/feedzback/blog/atom.xml" title="FeedZback Atom Feed"><link rel="stylesheet" href="/feedzback/assets/css/styles.acc3dc83.css">
8-
<script src="/feedzback/assets/js/runtime~main.acc19b4f.js" defer="defer"></script>
9-
<script src="/feedzback/assets/js/main.62bfbbe3.js" defer="defer"></script>
8+
<script src="/feedzback/assets/js/runtime~main.c7d965f2.js" defer="defer"></script>
9+
<script src="/feedzback/assets/js/main.1fecb4e7.js" defer="defer"></script>
1010
</head>
1111
<body class="navigation-with-keyboard">
1212
<svg style="display: none;"><defs>

docs/assets/js/184f8cc3.53a1590c.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)