Skip to content

Commit 8a5bf84

Browse files
committed
feat: Create API Documentation with simple authentication routes and global error handling on top of global api responder
1 parent 1c39e1e commit 8a5bf84

32 files changed

Lines changed: 2651 additions & 951 deletions

app/Exceptions/NotAccept.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace App\Exceptions;
4+
5+
use App\Traits\ApiResponder;
6+
use Exception;
7+
use Illuminate\Http\Request;
8+
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
9+
10+
class NotAccept extends Exception
11+
{
12+
use ApiResponder;
13+
14+
public function __invoke(AccessDeniedHttpException $e, Request $request)
15+
{
16+
return $this->error(null, 'Unauthorized', 403);
17+
}
18+
}

app/Exceptions/NotFound.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace App\Exceptions;
4+
5+
use App\Traits\ApiResponder;
6+
use Exception;
7+
use Illuminate\Http\Request;
8+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
9+
10+
class NotFound extends Exception
11+
{
12+
use ApiResponder;
13+
14+
public function __invoke(NotFoundHttpException $e, Request $request)
15+
{
16+
return $this->error(null, 'Item not found', 404);
17+
}
18+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Exceptions\NotAccept;
6+
use App\Http\Requests\LoginRequest;
7+
use App\Http\Requests\RegisterRequest;
8+
use App\Http\Resources\UserResource;
9+
use App\Models\User;
10+
use App\Traits\ApiResponder;
11+
use Dedoc\Scramble\Attributes\Group;
12+
use Illuminate\Http\Request;
13+
use Illuminate\Support\Facades\Auth;
14+
15+
class AuthController extends Controller
16+
{
17+
use ApiResponder;
18+
19+
/**
20+
* Register
21+
*
22+
* Register new user
23+
*/
24+
#[Group('Authentication')]
25+
public function register(RegisterRequest $request)
26+
{
27+
$user = User::create($request->all());
28+
return $this->created(new UserResource($user), 'Success to register');
29+
}
30+
31+
/**
32+
* Login
33+
*
34+
* Login using existing user credential
35+
*/
36+
#[Group('Authentication')]
37+
public function login(LoginRequest $request)
38+
{
39+
$credentials = $request->all();
40+
$token = Auth::attempt($credentials);
41+
if (!$token) throw new NotAccept;
42+
return $this->success([
43+
'token' => $token,
44+
'expiresIn' => now()
45+
->addMinutes(Auth::factory()->getTTL())
46+
->setTimezone(config('app.timezone')),
47+
]);
48+
}
49+
50+
/**
51+
* User Profile
52+
*
53+
* Get logged in user profile
54+
*/
55+
#[Group('User')]
56+
public function profile()
57+
{
58+
return $this->success(new UserResource(Auth::user()));
59+
}
60+
}

app/Http/Requests/LoginRequest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Http\Requests;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
7+
class LoginRequest extends FormRequest
8+
{
9+
/**
10+
* Determine if the user is authorized to make this request.
11+
*/
12+
public function authorize(): bool
13+
{
14+
return true;
15+
}
16+
17+
/**
18+
* Get the validation rules that apply to the request.
19+
*
20+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
21+
*/
22+
public function rules(): array
23+
{
24+
return [
25+
'email' => 'string|max:50|min:8|email|exists:users,email',
26+
'password' => 'string|min:8'
27+
];
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Http\Requests;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
7+
class RegisterRequest extends FormRequest
8+
{
9+
/**
10+
* Determine if the user is authorized to make this request.
11+
*/
12+
public function authorize(): bool
13+
{
14+
return true;
15+
}
16+
17+
/**
18+
* Get the validation rules that apply to the request.
19+
*
20+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
21+
*/
22+
public function rules(): array
23+
{
24+
return [
25+
'email' => 'string|max:50|min:8|email|unique:users,email',
26+
'password' => 'string|min:8'
27+
];
28+
}
29+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Http\Resources;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Http\Resources\Json\JsonResource;
7+
8+
class UserResource extends JsonResource
9+
{
10+
/**
11+
* Transform the resource into an array.
12+
*
13+
* @return array<string, mixed>
14+
*/
15+
public function toArray(Request $request): array
16+
{
17+
return parent::toArray($request);
18+
}
19+
}

app/Models/User.php

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,29 @@
22

33
namespace App\Models;
44

5-
// use Illuminate\Contracts\Auth\MustVerifyEmail;
6-
use Illuminate\Database\Eloquent\Factories\HasFactory;
5+
use App\Observers\UserObserver;
6+
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
77
use Illuminate\Foundation\Auth\User as Authenticatable;
8-
use Illuminate\Notifications\Notifiable;
8+
use Tymon\JWTAuth\Contracts\JWTSubject;
99

10-
class User extends Authenticatable
10+
#[ObservedBy([UserObserver::class])]
11+
class User extends Authenticatable implements JWTSubject
1112
{
12-
/** @use HasFactory<\Database\Factories\UserFactory> */
13-
use HasFactory, Notifiable;
13+
protected $guarded = [];
1414

15-
/**
16-
* The attributes that are mass assignable.
17-
*
18-
* @var list<string>
19-
*/
20-
protected $fillable = [
21-
'name',
22-
'email',
23-
'password',
24-
];
25-
26-
/**
27-
* The attributes that should be hidden for serialization.
28-
*
29-
* @var list<string>
30-
*/
3115
protected $hidden = [
3216
'password',
33-
'remember_token',
3417
];
18+
protected $casts = [
19+
'password' => 'hashed'
20+
];
21+
public function getJWTIdentifier()
22+
{
23+
return $this->getKey();
24+
}
3525

36-
/**
37-
* Get the attributes that should be cast.
38-
*
39-
* @return array<string, string>
40-
*/
41-
protected function casts(): array
26+
public function getJWTCustomClaims()
4227
{
43-
return [
44-
'email_verified_at' => 'datetime',
45-
'password' => 'hashed',
46-
];
28+
return [];
4729
}
4830
}

app/Observers/UserObserver.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace App\Observers;
4+
5+
use App\Models\User;
6+
7+
class UserObserver
8+
{
9+
public function creating(User $user)
10+
{
11+
$user->username = strstr($user->email, '@', true);
12+
}
13+
}

app/Providers/AppServiceProvider.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace App\Providers;
44

5+
use Dedoc\Scramble\Scramble;
6+
use Dedoc\Scramble\Support\Generator\OpenApi;
7+
use Dedoc\Scramble\Support\Generator\SecurityScheme;
58
use Illuminate\Support\ServiceProvider;
69

710
class AppServiceProvider extends ServiceProvider
@@ -11,14 +14,19 @@ class AppServiceProvider extends ServiceProvider
1114
*/
1215
public function register(): void
1316
{
14-
//
17+
Scramble::ignoreDefaultRoutes();
1518
}
1619

1720
/**
1821
* Bootstrap any application services.
1922
*/
2023
public function boot(): void
2124
{
22-
//
25+
Scramble::configure()
26+
->withDocumentTransformers(function (OpenApi $openApi) {
27+
$openApi->secure(
28+
SecurityScheme::http('bearer')
29+
);
30+
});
2331
}
2432
}

app/Traits/ApiResponder.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Traits;
4+
5+
trait ApiResponder
6+
{
7+
protected function success($data, $message = 'Success', $status = 200)
8+
{
9+
return response()->json([
10+
'success' => true,
11+
'message' => $message,
12+
'data' => $data,
13+
], $status);
14+
}
15+
16+
protected function delete($data = null, $message = 'Deleted', $status = 200)
17+
{
18+
return response()->json([
19+
'success' => true,
20+
'message' => $message,
21+
'data' => $data,
22+
], $status);
23+
}
24+
25+
protected function created($data, $message = 'Success')
26+
{
27+
return response()->json([
28+
'success' => true,
29+
'message' => $message,
30+
'data' => $data,
31+
], 201);
32+
}
33+
34+
protected function error($errors = [], $message, $status = 400)
35+
{
36+
return response()->json([
37+
'success' => false,
38+
'message' => $message,
39+
'data' => $errors,
40+
], $status);
41+
}
42+
}

0 commit comments

Comments
 (0)