Skip to content

Commit 52caff7

Browse files
authored
Merge pull request #87 from openfoodfoundation/feature/improve-onboarding
Feature: Improved onboarding password reset experience
2 parents cae3fb1 + 5514b53 commit 52caff7

File tree

111 files changed

+3282
-1339
lines changed

Some content is hidden

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

111 files changed

+3282
-1339
lines changed
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Enums\ApiResponse;
6+
use App\Exceptions\DisallowedApiFieldException;
7+
use App\Http\Controllers\Api\HandlesAPIRequests;
8+
use App\Http\Controllers\Controller;
9+
use App\Models\User;
10+
use Exception;
11+
use Illuminate\Http\JsonResponse;
12+
use Illuminate\Support\Facades\Auth;
13+
use Illuminate\Support\Facades\Validator;
14+
use Illuminate\Validation\Rules\Password;
15+
use Knuckles\Scribe\Attributes\Authenticated;
16+
use Knuckles\Scribe\Attributes\BodyParam;
17+
use Knuckles\Scribe\Attributes\Endpoint;
18+
use Knuckles\Scribe\Attributes\Group;
19+
use Knuckles\Scribe\Attributes\QueryParam;
20+
use Knuckles\Scribe\Attributes\Response;
21+
use Knuckles\Scribe\Attributes\Subgroup;
22+
23+
#[Group('App Endpoints')]
24+
#[Subgroup('/my-profile', 'Manage your profile.')]
25+
class ApiMyProfileController extends Controller
26+
{
27+
use HandlesAPIRequests;
28+
29+
/**
30+
* Set the related data the GET request is allowed to ask for
31+
*/
32+
public array $availableRelations = [
33+
34+
];
35+
36+
public static array $searchableFields = [];
37+
38+
/**
39+
* GET /
40+
*
41+
* @return JsonResponse
42+
*
43+
* @throws DisallowedApiFieldException
44+
*/
45+
#[Endpoint(
46+
title : 'GET /',
47+
description : 'Retrieve your user profile.',
48+
authenticated: true
49+
)]
50+
#[Authenticated]
51+
#[QueryParam(
52+
name : 'cached',
53+
type : 'bool',
54+
description: 'Request the response to be cached. Default: `true`.',
55+
required : false,
56+
example : true
57+
)]
58+
#[QueryParam(
59+
name : 'fields',
60+
type : 'string',
61+
description: 'Comma-separated list of database fields to return within the object.',
62+
required : false,
63+
example : 'id,created_at'
64+
)]
65+
#[Response(
66+
content : '{
67+
"meta": {
68+
"responseCode": 200,
69+
"limit": 50,
70+
"offset": 0,
71+
"message": "",
72+
"cached": true,
73+
"cached_at": "2024-08-13 08:58:19",
74+
"availableRelations": []
75+
},
76+
"data": {"id": 1234, "name": "Your name", "email":"you@yourdomain.com", "created_at": "2024-01-01 00:00:00"}
77+
}',
78+
status : 200,
79+
description: ''
80+
)]
81+
public function index(): JsonResponse
82+
{
83+
$this->query = User::with($this->associatedData);
84+
$this->query = $this->updateReadQueryBasedOnUrl();
85+
$this->data = $this->query->find(Auth::id());
86+
87+
return $this->respond();
88+
}
89+
90+
/**
91+
* POST /
92+
*
93+
* @hideFromAPIDocumentation
94+
*
95+
* @return JsonResponse
96+
*/
97+
public function store(): JsonResponse
98+
{
99+
$this->responseCode = 403;
100+
$this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value;
101+
102+
return $this->respond();
103+
}
104+
105+
/**
106+
* GET /{id}
107+
*
108+
* @param string $id
109+
*
110+
* @return JsonResponse
111+
*
112+
* @throws DisallowedApiFieldException
113+
*/
114+
#[Endpoint(
115+
title : 'GET /{id}',
116+
description : 'Retrieve your profile. Alias of GET /',
117+
authenticated: true,
118+
)]
119+
#[Authenticated]
120+
#[QueryParam(
121+
name : 'cached',
122+
type : 'bool',
123+
description: 'Request the response to be cached. Default: `true`.',
124+
required : false,
125+
example : 1
126+
)]
127+
#[QueryParam(
128+
name : 'fields',
129+
type : 'string',
130+
description: 'Comma-separated list of database fields to return within the object.',
131+
required : false,
132+
example : 'id,created_at'
133+
)]
134+
#[Response(
135+
content : '{
136+
"meta": {
137+
"responseCode": 200,
138+
"limit": 50,
139+
"offset": 0,
140+
"message": "",
141+
"cached": true,
142+
"cached_at": "2024-08-13 08:58:19",
143+
"availableRelations": []
144+
},
145+
"data": {"id": 1234, "name": "Your name", "email":"you@yourdomain.com", "created_at": "2024-01-01 00:00:00"}
146+
}',
147+
status : 200,
148+
description: ''
149+
)]
150+
public function show(string $id)
151+
{
152+
$this->query = User::with($this->associatedData);
153+
$this->query = $this->updateReadQueryBasedOnUrl();
154+
$this->data = $this->query->find(Auth::id());
155+
156+
return $this->respond();
157+
}
158+
159+
/**
160+
* PUT/ {id}
161+
*
162+
* @param string $id
163+
*
164+
* @return JsonResponse
165+
*/
166+
#[Endpoint(
167+
title : 'PUT /{id}',
168+
description : 'Update your profile.',
169+
authenticated: true
170+
)]
171+
#[Authenticated]
172+
#[BodyParam(
173+
name : 'password',
174+
type : 'string',
175+
description: 'Your new password. Must conform to password validation requirements.',
176+
required : false
177+
)]
178+
#[Response(
179+
content : '{
180+
"meta": {
181+
"responseCode": 200,
182+
"limit": 50,
183+
"offset": 0,
184+
"message": "",
185+
"cached": true,
186+
"cached_at": "2024-08-13 08:58:19",
187+
"availableRelations": []
188+
},
189+
"data": {"id": 1234, "name": "Your name", "email":"you@yourdomain.com", "created_at": "2024-01-01 00:00:00"}
190+
}',
191+
status : 200,
192+
description: ''
193+
)]
194+
public function update(string $id)
195+
{
196+
$validationArray = [
197+
'password' => [
198+
'sometimes',
199+
Password::min(8)
200+
->letters()
201+
->mixedCase()
202+
->numbers()
203+
->symbols(),
204+
],
205+
206+
];
207+
208+
$validator = Validator::make($this->request->all(), $validationArray);
209+
210+
if ($validator->fails()) {
211+
212+
$this->responseCode = 400;
213+
$this->message = $validator->errors()
214+
->first();
215+
216+
}
217+
else {
218+
219+
try {
220+
221+
Auth::user()->password = $this->request->get('password');
222+
Auth::user()->requires_password_reset = 0;
223+
Auth::user()->save();
224+
225+
$this->data = Auth::user();
226+
}
227+
catch (Exception $e) {
228+
229+
$this->responseCode = 500;
230+
$this->message = ApiResponse::RESPONSE_ERROR->value . ': "' . $e->getMessage() . '".';
231+
232+
}
233+
}
234+
235+
return $this->respond();
236+
}
237+
238+
/**
239+
* DELETE / {id}
240+
*
241+
* @hideFromAPIDocumentation
242+
*
243+
* @param string $id
244+
*
245+
* @return JsonResponse
246+
*/
247+
public function destroy(string $id)
248+
{
249+
$this->responseCode = 403;
250+
$this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value;
251+
252+
return $this->respond();
253+
}
254+
}

app/Http/Controllers/ProfileController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ProfileController extends Controller
2020
*/
2121
public function edit(Request $request): Response
2222
{
23-
return Inertia::render('Profile/Edit', [
23+
return Inertia::render('App/Profile/Edit', [
2424
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
2525
'status' => session('status'),
2626
]);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/** @noinspection PhpUndefinedFieldInspection */
4+
5+
namespace App\Http\Middleware;
6+
7+
use Closure;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Support\Facades\Auth;
10+
use Illuminate\Support\Facades\Redirect;
11+
use Symfony\Component\HttpFoundation\Response;
12+
13+
class CheckIfPasswordUpdateRequired
14+
{
15+
protected array $except = [
16+
'profile.set-password',
17+
];
18+
19+
/**
20+
* Handle an incoming request.
21+
*
22+
* @param Request $request
23+
* @param Closure(Request): (Response) $next
24+
*
25+
* @return Response
26+
*/
27+
public function handle(Request $request, Closure $next): Response
28+
{
29+
if (Auth::user() && Auth::user()->requires_password_reset == 1 && ($request->route()->uri != 'profile/set-password')) {
30+
31+
return Redirect::to('/profile/set-password');
32+
}
33+
34+
return $next($request);
35+
36+
}
37+
}

app/Jobs/TeamUsers/SendTeamUserInvitationEmail.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public function handle(): void
2727
$userToNotify = User::find($this->teamUser->user_id);
2828

2929
if ($userToNotify) {
30-
$userToNotify->current_team_id = $this->teamUser->team_id;
30+
$userToNotify->requires_password_reset = 1;
31+
$userToNotify->current_team_id = $this->teamUser->team_id;
3132
$userToNotify->saveQuietly();
3233

3334
$userToNotify->notify(new SendTeamUserInvitationEmailNotification($this->teamUser));

app/Notifications/Mail/TeamUsers/SendTeamUserInvitationEmailNotification.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use App\Models\Team;
66
use App\Models\TeamUser;
7+
use App\Models\User;
8+
use App\Services\BounceService;
79
use Illuminate\Bus\Queueable;
810
use Illuminate\Contracts\Queue\ShouldQueue;
911
use Illuminate\Notifications\Messages\MailMessage;
@@ -41,13 +43,19 @@ public function toMail(object $notifiable): MailMessage
4143
{
4244
$team = Team::find($this->teamUser->team_id);
4345

46+
$urlToVisit = BounceService::generateSignedUrlForUser(
47+
user : User::find($notifiable->id),
48+
expiry : now()->addDays(2),
49+
redirectPath: '/dashboard'
50+
);
51+
4452
return (new MailMessage())
4553
->subject('You have been invited to join ' . $team->name . ' - ' . config('app.name'))
4654
->line('You have been invited to join team "' . $team->name . '" on ' . config('app.name') . '.')
47-
->line('An account has been created for you on the team, but if you have never logged in to use the system, you may need to reset your password in order to log in.')
48-
->line('Please follow the button below to reset your password and log in.')
49-
->action('Reset your password & log in', url('/forgot-password'))
50-
->line('Thank you for using our application!');
55+
->line('An account has been created for you on the team, but if you have never logged in to use the system, you will need to set your password in order to log in.')
56+
->line('This link will expire after 24 hours.')
57+
->line('Please follow the button below.')
58+
->action('Set your password & log in', $urlToVisit);
5159
}
5260

5361
/**

0 commit comments

Comments
 (0)