Skip to content

Commit eae8b4c

Browse files
Merge pull request #3 from cloudshopt/dev
Dev
2 parents 52edf81 + b6b6804 commit eae8b4c

10 files changed

Lines changed: 261 additions & 29 deletions

File tree

.github/workflows/deploy.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,7 @@ jobs:
5454

5555
DB_PASSWORD_PROD: ${{ secrets.PROD_DB_PASSWORD }}
5656
REDIS_PASSWORD_PROD: ${{ secrets.PROD_REDIS_PASSWORD }}
57+
JWT_SECRET_PROD: ${{ secrets.PROD_JWT_SECRET }}
5758
DB_PASSWORD_DEV: ${{ secrets.DEV_DB_PASSWORD }}
58-
REDIS_PASSWORD_DEV: ${{ secrets.DEV_REDIS_PASSWORD }}
59+
REDIS_PASSWORD_DEV: ${{ secrets.DEV_REDIS_PASSWORD }}
60+
JWT_SECRET_DEV: ${{ secrets.DEV_JWT_SECRET }}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Models\User;
6+
use App\Services\JwtService;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\Hash;
9+
use Illuminate\Validation\ValidationException;
10+
11+
class AuthController extends Controller
12+
{
13+
public function register(Request $request, JwtService $jwt)
14+
{
15+
$data = $request->validate([
16+
'name' => ['required','string','max:120'],
17+
'email' => ['required','email','max:190','unique:users,email'],
18+
'password' => ['required','string','min:8','confirmed'],
19+
]);
20+
21+
$user = User::query()->create([
22+
'name' => $data['name'],
23+
'email' => $data['email'],
24+
'password' => Hash::make($data['password']),
25+
]);
26+
27+
$token = $jwt->issueToken($user);
28+
29+
return response()->json([
30+
'token' => $token,
31+
'token_type' => 'Bearer',
32+
'expires_in' => (int) config('jwt.ttl'),
33+
'user' => [
34+
'id' => $user->id,
35+
'name' => $user->name,
36+
'email' => $user->email,
37+
],
38+
], 201);
39+
}
40+
41+
public function login(Request $request, JwtService $jwt)
42+
{
43+
$data = $request->validate([
44+
'email' => ['required','email'],
45+
'password' => ['required','string'],
46+
]);
47+
48+
$user = User::query()->where('email', $data['email'])->first();
49+
50+
if (!$user || !Hash::check($data['password'], $user->password)) {
51+
throw ValidationException::withMessages([
52+
'email' => ['Invalid credentials.'],
53+
]);
54+
}
55+
56+
$token = $jwt->issueToken($user);
57+
58+
return response()->json([
59+
'token' => $token,
60+
'token_type' => 'Bearer',
61+
'expires_in' => (int) config('jwt.ttl'),
62+
'user' => [
63+
'id' => $user->id,
64+
'name' => $user->name,
65+
'email' => $user->email,
66+
],
67+
]);
68+
}
69+
70+
public function me(Request $request)
71+
{
72+
$user = $request->user();
73+
74+
return response()->json([
75+
'user' => [
76+
'id' => $user?->id,
77+
'name' => $user?->name,
78+
'email' => $user?->email,
79+
],
80+
]);
81+
}
82+
}

app/Http/Middleware/JwtAuth.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use App\Models\User;
6+
use App\Services\JwtService;
7+
use Closure;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Support\Facades\Auth;
10+
11+
class JwtAuth
12+
{
13+
public function handle(Request $request, Closure $next)
14+
{
15+
$authHeader = (string) $request->header('Authorization', '');
16+
17+
if (!str_starts_with($authHeader, 'Bearer ')) {
18+
return response()->json(['message' => 'Missing Bearer token'], 401);
19+
}
20+
21+
$token = trim(substr($authHeader, 7));
22+
23+
try {
24+
$claims = app(JwtService::class)->decode($token);
25+
26+
$userId = $claims['sub'] ?? null;
27+
if (!$userId) {
28+
return response()->json(['message' => 'Invalid token'], 401);
29+
}
30+
31+
$user = User::query()->find($userId);
32+
if (!$user) {
33+
return response()->json(['message' => 'User not found'], 401);
34+
}
35+
36+
Auth::setUser($user);
37+
$request->attributes->set('jwt_claims', $claims);
38+
39+
return $next($request);
40+
} catch (\Throwable $e) {
41+
return response()->json(['message' => 'Invalid or expired token'], 401);
42+
}
43+
}
44+
}

app/Models/User.php

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,12 @@
99

1010
class User extends Authenticatable
1111
{
12-
/** @use HasFactory<\Database\Factories\UserFactory> */
1312
use HasFactory, Notifiable;
1413

15-
/**
16-
* The attributes that are mass assignable.
17-
*
18-
* @var list<string>
19-
*/
20-
protected $fillable = [
21-
'name',
22-
'email',
23-
'password',
24-
];
14+
protected $fillable = ['name', 'email', 'password'];
2515

26-
/**
27-
* The attributes that should be hidden for serialization.
28-
*
29-
* @var list<string>
30-
*/
31-
protected $hidden = [
32-
'password',
33-
'remember_token',
34-
];
16+
protected $hidden = ['password', 'remember_token'];
3517

36-
/**
37-
* Get the attributes that should be cast.
38-
*
39-
* @return array<string, string>
40-
*/
4118
protected function casts(): array
4219
{
4320
return [

app/Services/JwtService.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use App\Models\User;
6+
use Firebase\JWT\JWT;
7+
use Firebase\JWT\Key;
8+
use Illuminate\Support\Carbon;
9+
10+
class JwtService
11+
{
12+
public function issueToken(User $user): string
13+
{
14+
$now = Carbon::now()->timestamp;
15+
$ttl = (int) config('jwt.ttl');
16+
17+
$payload = [
18+
'iss' => config('jwt.issuer'),
19+
'aud' => config('jwt.audience'),
20+
'iat' => $now,
21+
'nbf' => $now,
22+
'exp' => $now + $ttl,
23+
'sub' => (string) $user->id,
24+
'email' => $user->email,
25+
];
26+
27+
return JWT::encode($payload, $this->secret(), 'HS256');
28+
}
29+
30+
public function decode(string $token): array
31+
{
32+
$decoded = JWT::decode($token, new Key($this->secret(), 'HS256'));
33+
return (array) $decoded;
34+
}
35+
36+
private function secret(): string
37+
{
38+
$secret = (string) config('jwt.secret');
39+
if ($secret === '') {
40+
throw new \RuntimeException('JWT secret is not configured (JWT_SECRET).');
41+
}
42+
return $secret;
43+
}
44+
}

bootstrap/app.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Http\Middleware\JwtAuth;
34
use Illuminate\Foundation\Application;
45
use Illuminate\Foundation\Configuration\Exceptions;
56
use Illuminate\Foundation\Configuration\Middleware;
@@ -12,7 +13,9 @@
1213
health: '/up',
1314
)
1415
->withMiddleware(function (Middleware $middleware): void {
15-
//
16+
$middleware->alias([
17+
'jwt' => JwtAuth::class,
18+
]);
1619
})
1720
->withExceptions(function (Exceptions $exceptions): void {
1821
//

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"license": "MIT",
88
"require": {
99
"php": "^8.2",
10+
"firebase/php-jwt": "^7.0",
1011
"laravel/framework": "^12.0",
1112
"laravel/tinker": "^2.10.1"
1213
},

composer.lock

Lines changed: 64 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/jwt.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
return [
4+
'secret' => env('JWT_SECRET'),
5+
'ttl' => (int) env('JWT_TTL_SECONDS', 3600),
6+
'issuer' => env('JWT_ISSUER', 'cloudshopt-user-service'),
7+
'audience' => env('JWT_AUDIENCE', 'cloudshopt'),
8+
];

routes/api.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<?php
22

3+
use App\Http\Controllers\AuthController;
34
use Illuminate\Support\Facades\DB;
45
use Illuminate\Support\Facades\Route;
56

67
Route::get('/info', function () {
78
return response()->json([
8-
'ok11' => true,
9+
'ok' => true,
910
'service' => config('app.name'),
1011
'sha' => env('IMAGE_SHA', null),
1112
'time' => now()->toISOString(),
@@ -35,3 +36,10 @@
3536
], 500);
3637
}
3738
});
39+
40+
Route::prefix('auth')->group(function () {
41+
Route::post('/register', [AuthController::class, 'register']);
42+
Route::post('/login', [AuthController::class, 'login']);
43+
});
44+
45+
Route::middleware('jwt')->get('/me', [AuthController::class, 'me']);

0 commit comments

Comments
 (0)