Skip to content

Commit 72604a1

Browse files
authored
Merge pull request #52 from joselfonseca/feature/37-email-verification
Feature: #37 email verification
2 parents e17263b + c6e116f commit 72604a1

File tree

16 files changed

+575
-26
lines changed

16 files changed

+575
-26
lines changed

config/config.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,16 @@
5151
|
5252
*/
5353
'migrations' => true,
54+
/*
55+
|--------------------------------------------------------------------------
56+
| Settings for email verification
57+
|--------------------------------------------------------------------------
58+
|
59+
| Update this values for your use case
60+
|
61+
*/
62+
'verify_email' => [
63+
'base_url' => env('FRONT_URL').'/email-verify',
64+
],
5465

5566
];

graphql/auth.graphql

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ type User {
1414
}
1515

1616
type AuthPayload {
17-
access_token: String!
18-
refresh_token: String!
19-
expires_in: Int!
20-
token_type: String!
21-
user: User!
17+
access_token: String
18+
refresh_token: String
19+
expires_in: Int
20+
token_type: String
21+
user: User
2222
}
2323

2424
type RefreshTokenPayload {
@@ -38,6 +38,16 @@ type ForgotPasswordResponse {
3838
message: String
3939
}
4040

41+
type RegisterResponse {
42+
tokens: AuthPayload
43+
status: RegisterStatuses!
44+
}
45+
46+
enum RegisterStatuses {
47+
MUST_VERIFY_EMAIL
48+
SUCCESS
49+
}
50+
4151
input ForgotPasswordInput {
4252
email: String! @rules(apply: ["required", "email"])
4353
}
@@ -51,7 +61,7 @@ input NewPasswordWithCodeInput {
5161

5262
input RegisterInput {
5363
name: String! @rules(apply: ["required", "string"])
54-
email: String! @rules(apply: ["required", "email"])
64+
email: String! @rules(apply: ["required", "email", "unique:users,email"])
5565
password: String! @rules(apply: ["required", "confirmed", "min:8"])
5666
password_confirmation: String!
5767
}
@@ -61,12 +71,17 @@ input SocialLoginInput {
6171
token: String! @rules(apply: ["required"])
6272
}
6373

74+
input VerifyEmailInput {
75+
token: String!
76+
}
77+
6478
extend type Mutation {
6579
login(input: LoginInput @spread): AuthPayload! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\Login@resolve")
6680
refreshToken(input: RefreshTokenInput @spread): RefreshTokenPayload! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\RefreshToken@resolve")
6781
logout: LogoutResponse! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\Logout@resolve")
6882
forgotPassword(input: ForgotPasswordInput! @spread): ForgotPasswordResponse! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\ForgotPassword@resolve")
6983
updateForgottenPassword(input: NewPasswordWithCodeInput @spread): ForgotPasswordResponse! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\ResetPassword@resolve")
70-
register(input: RegisterInput @spread): AuthPayload! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\Register@resolve")
84+
register(input: RegisterInput @spread): RegisterResponse! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\Register@resolve")
7185
socialLogin(input: SocialLoginInput! @spread): AuthPayload! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\SocialLogin@resolve")
86+
verifyEmail(input: VerifyEmailInput! @spread): AuthPayload! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\VerifyEmail@resolve")
7287
}

readme.md

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ type User {
5656
}
5757

5858
type AuthPayload {
59-
access_token: String!
60-
refresh_token: String!
61-
expires_in: Int!
62-
token_type: String!
63-
user: User!
59+
access_token: String
60+
refresh_token: String
61+
expires_in: Int
62+
token_type: String
63+
user: User
6464
}
6565

6666
type RefreshTokenPayload {
@@ -80,6 +80,16 @@ type ForgotPasswordResponse {
8080
message: String
8181
}
8282

83+
type RegisterResponse {
84+
tokens: AuthPayload
85+
status: RegisterStatuses!
86+
}
87+
88+
enum RegisterStatuses {
89+
MUST_VERIFY_EMAIL
90+
SUCCESS
91+
}
92+
8393
input ForgotPasswordInput {
8494
email: String! @rules(apply: ["required", "email"])
8595
}
@@ -97,11 +107,15 @@ input RegisterInput {
97107
password: String! @rules(apply: ["required", "confirmed", "min:8"])
98108
password_confirmation: String!
99109
}
110+
100111
input SocialLoginInput {
101112
provider: String! @rules(apply: ["required"])
102113
token: String! @rules(apply: ["required"])
103114
}
104115

116+
input VerifyEmailInput {
117+
token: String!
118+
}
105119

106120
extend type Mutation {
107121
login(input: LoginInput @spread): AuthPayload! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\Login@resolve")
@@ -111,6 +125,7 @@ extend type Mutation {
111125
updateForgottenPassword(input: NewPasswordWithCodeInput @spread): ForgotPasswordResponse! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\ResetPassword@resolve")
112126
register(input: RegisterInput @spread): AuthPayload! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\Register@resolve")
113127
socialLogin(input: SocialLoginInput! @spread): AuthPayload @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\SocialLogin@resolve")
128+
verifyEmail(input: VerifyEmailInput! @spread): AuthPayload! @field(resolver: "Joselfonseca\\LighthouseGraphQLPassport\\GraphQL\\Mutations\\VerifyEmail@resolve")
114129
}
115130
```
116131

@@ -133,7 +148,7 @@ This will allow you to change the schema and resolvers if needed.
133148

134149
## Usage
135150

136-
This will add 7 mutations to your GraphQL API
151+
This will add 8 mutations to your GraphQL API
137152

138153
```js
139154
extend type Mutation {
@@ -144,6 +159,7 @@ extend type Mutation {
144159
updateForgottenPassword(input: NewPasswordWithCodeInput): ForgotPasswordResponse!
145160
register(input: RegisterInput @spread): AuthPayload!
146161
socialLogin(input: SocialLoginInput! @spread): AuthPayload!
162+
verifyEmail(input: VerifyEmailInput! @spread): AuthPayload!
147163
}
148164
```
149165

@@ -154,6 +170,46 @@ extend type Mutation {
154170
- **updateForgottenPassword:** Will allow your clients to update the forgotten password from the email received.
155171
- **register:** Will allow your clients to register a new user using the default Laravel registration fields
156172
- **socialLogin:** Will allow your clients to log in using access token from social providers using socialite
173+
- **verifyEmail:** Will allow your clients to verify the email after they receive a token in the email
174+
175+
### Using the email verification
176+
177+
If you want to use the email verification feature that comes with laravel, please follow the instruction in the laravel documentation to configure the model in [https://laravel.com/docs/6.x/verification](https://laravel.com/docs/6.x/verification), once that is done add the following traits
178+
179+
```php
180+
use Illuminate\Contracts\Auth\MustVerifyEmail;
181+
use Joselfonseca\LighthouseGraphQLPassport\HasLoggedInTokens;
182+
use Joselfonseca\LighthouseGraphQLPassport\MustVerifyEmailGraphQL;
183+
184+
class User extends Authenticatable implements MustVerifyEmail
185+
{
186+
use Notifiable;
187+
use HasApiTokens;
188+
use HasSocialLogin;
189+
use MustVerifyEmailGraphQL;
190+
use HasLoggedInTokens;
191+
}
192+
```
193+
This will add some methods for the email notification to be sent with a token. Use the token in the following mutation.
194+
195+
```js
196+
{
197+
mutation {
198+
verifyEmail(input: {
199+
"token": "HERE_THE_TOKEN"
200+
}) {
201+
access_token
202+
refresh_token
203+
user {
204+
id
205+
name
206+
email
207+
}
208+
}
209+
}
210+
}
211+
```
212+
If the token is valid the tokens will be issued.
157213

158214
### Using socialite for social login
159215

src/GraphQL/Mutations/Register.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use GraphQL\Type\Definition\ResolveInfo;
66
use Illuminate\Auth\Events\Registered;
7+
use Illuminate\Contracts\Auth\MustVerifyEmail;
78
use Illuminate\Support\Facades\Hash;
89
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
910

@@ -26,6 +27,14 @@ public function resolve($rootValue, array $args, GraphQLContext $context = null,
2627
$input['password'] = Hash::make($input['password']);
2728
$model->fill($input);
2829
$model->save();
30+
if ($model instanceof MustVerifyEmail) {
31+
$model->sendEmailVerificationNotification();
32+
33+
return [
34+
'tokens' => [],
35+
'status' => 'MUST_VERIFY_EMAIL',
36+
];
37+
}
2938
$credentials = $this->buildCredentials([
3039
'username' => $args[config('lighthouse-graphql-passport.username')],
3140
'password' => $args['password'],
@@ -35,6 +44,9 @@ public function resolve($rootValue, array $args, GraphQLContext $context = null,
3544
$response['user'] = $user;
3645
event(new Registered($user));
3746

38-
return $response;
47+
return [
48+
'tokens' => $response,
49+
'status' => 'SUCCESS',
50+
];
3951
}
4052
}

src/GraphQL/Mutations/VerifyEmail.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Joselfonseca\LighthouseGraphQLPassport\GraphQL\Mutations;
4+
5+
use GraphQL\Type\Definition\ResolveInfo;
6+
use Illuminate\Auth\Events\Verified;
7+
use Illuminate\Database\Eloquent\ModelNotFoundException;
8+
use Illuminate\Support\Carbon;
9+
use Illuminate\Support\Facades\Auth;
10+
use Joselfonseca\LighthouseGraphQLPassport\Exceptions\ValidationException;
11+
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
12+
13+
class VerifyEmail
14+
{
15+
/**
16+
* @param $rootValue
17+
* @param array $args
18+
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext|null $context
19+
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo
20+
*
21+
* @throws \Exception
22+
*
23+
* @return array
24+
*/
25+
public function resolve($rootValue, array $args, GraphQLContext $context = null, ResolveInfo $resolveInfo)
26+
{
27+
$decodedToken = json_decode(base64_decode($args['token']));
28+
$expiration = decrypt($decodedToken->expiration);
29+
$email = decrypt($decodedToken->hash);
30+
if (Carbon::parse($expiration) < now()) {
31+
throw new ValidationException([
32+
'token' => 'The token is invalid',
33+
], 'Validation Error');
34+
}
35+
$model = app(config('auth.providers.users.model'));
36+
37+
try {
38+
$user = $model->where('email', $email)->firstOrFail();
39+
$user->markEmailAsVerified();
40+
event(new Verified($user));
41+
Auth::onceUsingId($user->id);
42+
$tokens = $user->getTokens();
43+
$tokens['user'] = $user;
44+
45+
return $tokens;
46+
} catch (ModelNotFoundException $e) {
47+
throw new ValidationException([
48+
'token' => 'The token is invalid',
49+
], 'Validation Error');
50+
}
51+
}
52+
}

src/HasLoggedInTokens.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Joselfonseca\LighthouseGraphQLPassport;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Facades\Auth;
7+
8+
trait HasLoggedInTokens
9+
{
10+
/**
11+
* @throws \Exception
12+
*
13+
* @return mixed
14+
*/
15+
public function getTokens()
16+
{
17+
$request = Request::create('oauth/token', 'POST', [
18+
'grant_type' => 'logged_in_grant',
19+
'client_id' => config('lighthouse-graphql-passport.client_id'),
20+
'client_secret' => config('lighthouse-graphql-passport.client_secret'),
21+
], [], [], [
22+
'HTTP_Accept' => 'application/json',
23+
]);
24+
$response = app()->handle($request);
25+
26+
return json_decode($response->getContent(), true);
27+
}
28+
29+
/**
30+
* @param $request
31+
*
32+
* @return mixed
33+
*/
34+
public function byLoggedInUser($request)
35+
{
36+
return Auth::user();
37+
}
38+
}

src/MustVerifyEmailGraphQL.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Joselfonseca\LighthouseGraphQLPassport;
4+
5+
use Joselfonseca\LighthouseGraphQLPassport\Notifications\VerifyEmail;
6+
7+
trait MustVerifyEmailGraphQL
8+
{
9+
/**
10+
* Send the email verification notification.
11+
*
12+
* @return void
13+
*/
14+
public function sendEmailVerificationNotification()
15+
{
16+
$this->notify(new VerifyEmail());
17+
}
18+
}

src/Notifications/VerifyEmail.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Joselfonseca\LighthouseGraphQLPassport\Notifications;
4+
5+
use Illuminate\Support\Carbon;
6+
7+
class VerifyEmail extends \Illuminate\Auth\Notifications\VerifyEmail
8+
{
9+
/**
10+
* Get the verification URL for the given notifiable.
11+
*
12+
* @param mixed $notifiable
13+
*
14+
* @return string
15+
*/
16+
protected function verificationUrl($notifiable)
17+
{
18+
$payload = base64_encode(json_encode([
19+
'id' => $notifiable->getKey(),
20+
'hash' => encrypt($notifiable->getEmailForVerification()),
21+
'expiration' => encrypt(Carbon::now()->addMinutes(10)->toIso8601String()),
22+
]));
23+
24+
return config('lighthouse-graphql-passport.verify_email.base_url').'?token='.$payload;
25+
}
26+
}

0 commit comments

Comments
 (0)