Update JWT Token

This commit is contained in:
ivan-sim
2026-02-24 07:51:20 +07:00
parent 8eb82a171c
commit f82c6177a3
11 changed files with 517 additions and 302 deletions

View File

@@ -16,280 +16,103 @@ use Illuminate\Support\Facades\Validator;
use Modules\Primaya\Helpers\ApiResponse;
use Illuminate\Support\Facades\DB;
use App\Helpers\Helper;
use App\Models\Corporate;
use Illuminate\Support\Facades\View;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
class AuthController extends Controller
{
public function login(Request $request)
public function loginJwt(Request $request)
{
$data = [
'email' => $request->email,
'password' => $request->password
];
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required'
], [
'email.required' => trans('Validation.required',['attribute' => 'Email']),
'email.email' => trans('Validation.email'),
'password.required' => trans('Validation.required',['attribute' => 'Password']),
]);
if ($validator->fails())
{
return ApiResponse::apiResponse('Bad Request', $data, $validator->errors(), 400);
}
else
{
$user = User::where('email', $request->email)->first();
if (!$user) {
return ApiResponse::apiResponse('Not Found', $data, trans('Message.not_found'), 404);
}
if (!Hash::check($request->password, $user->password)) {
return ApiResponse::apiResponse('Bad Request', $data, trans('Message.password'), 400);
}
$res_data = [
'user' => $user,
'token' => $user->createToken('app')->plainTextToken
];
return ApiResponse::apiResponse("Success", $res_data, trans('Message.success'), 200);
}
}
public function logout(Request $request)
{
$request->user()->tokens()->delete();
return ApiResponse::apiResponse('Success', [], trans('Message.logout'), 200);
}
public function resetPassword(Request $request)
{
$user = Auth::user();
$request->validate([
'old_password' => 'required',
'new_password' => 'required',
'confirm_new_password' => 'required'
]);
if (!Hash::check($request['old_password'], $user->password)) {
return response(['Message' => 'Password Salah'], 403);
if ($validator->fails()) {
return ApiResponse::apiResponse(
'Bad Request',
$data,
$validator->errors(),
400
);
}
if ($request["new_password"] != $request["confirm_new_password"]) {
return response([
'Message' => "Password Tidak Sama"
]);
// 🔥 1⃣ Ambil header
$apiKey = $request->header('X-API-KEY');
$apiSecret = $request->header('X-API-SECRET');
if (empty($apiKey) || empty($apiSecret)) {
return ApiResponse::apiResponse(
'Unauthorized',
null,
'API Key dan Secret wajib diisi',
401
);
}
$user->update([
'password' => Hash::make($request->confirm_new_password),
]);
return response()->json($user);
}
public function verifyEmail(Request $request)
{
$data = [
'email' => $request->email,
];
$validator = Validator::make($request->all(), [
'email' => 'required|email',
], [
'email.required' => trans('Validation.required',['attribute' => 'Email']),
'email.email' => trans('Validation.email'),
]);
if ($validator->fails())
{
return ApiResponse::apiResponse('Bad Request', $data, $validator->errors(), 400);
}
else
{
$user = User::where('email', $request->email)->first();
if (!$user) {
return ApiResponse::apiResponse('Not Found', $data, trans('Message.not_found'), 404);
}
//send email
// Insert data notifications
$emailTo = $request->email;
$dataNotif = [
'user_id' => $user->id,
'email' => $emailTo,
'title' => 'Forgot Password',
'description' => 'Request forgot password from Hospital Portal',
'type' => 1,
'isUnRead' => true,
'created_by' => auth()->check() ? auth()->user()->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
];
$sendNotif = Helper::insertNotification($dataNotif);
//Insert data password reset
$token = mt_rand(100000, 999999); // Menghasilkan angka acak antara 100000 dan 999999
$p_resets = DB::table('password_resets')
->insert([
'email' => $request->email,
'token' => $token,
'created_at' => date('Y-m-d H:i:s'),
]);
// Send Email after insert notifications
if($sendNotif && $p_resets)
{
//send to alarm
$nameTo = 'User';
$dataEmail = [
'email' => $emailTo,
'name' => $nameTo,
'subject' => 'Request Forgot Password from Hospital Portal Date '. date('Y-m-d H:i:s'),
'body' => View::make('email/forgot_password', ['token' => $token])->render(),
];
Helper::sendEmail($dataEmail);
$res = DB::table('password_resets')
->where('email', '=', $request->email)
->where('token', '=', $token)
->first();
return ApiResponse::apiResponse("Success", $res, trans('Message.success'), 200);
}
else
{
return ApiResponse::apiResponse("Internal Server Error", $data, trans('Message.server_error'), 500);
}
}
}
public function verifCode(Request $request)
{
$data = [
'email' => $request->email,
'token' => $request->token,
];
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'token' => 'required|numeric',
], [
'email.required' => trans('Validation.required',['attribute' => 'Email']),
'email.email' => trans('Validation.email'),
'token.required' => trans('Validation.required',['attribute' => 'Token']),
'token.numeric' => trans('Validation.required',['attribute' => 'Code Numeric']),
]);
if ($validator->fails())
{
return ApiResponse::apiResponse('Bad Request', $data, $validator->errors(), 400);
}
else
{
//Check Time
$check = DB::table('password_resets')
->where('email', '=', $request->email)
->where('token', '=', $request->token)
->select('created_at')
// 🔥 2⃣ Validasi corporate
$corporate = Corporate::where('api_key', $apiKey)
->where('api_secret', $apiSecret)
->first();
if($check)
{
$created_at = strtotime($check->created_at); // Konversi string waktu ke UNIX timestamp
$now = time(); // Waktu sekarang dalam UNIX timestamp
// Hitung selisih waktu dalam menit
$diffInMinutes = ($now - $created_at) / 60;
if ($diffInMinutes > 60) {
return ApiResponse::apiResponse('Not Found', $data, trans('Message.token_expired'), 404);
} else {
// Lanjutkan dengan proses pemulihan kata sandi
return ApiResponse::apiResponse("Success", $data, trans('Message.success'), 200);
}
}
else
{
return ApiResponse::apiResponse('Not Found', $data, trans('Message.not_found'), 404);
}
if (!$corporate) {
return ApiResponse::apiResponse(
'Unauthorized',
null,
'Invalid API Key',
401
);
}
}
public function forgetPassword(Request $request)
{
$data = [
'email' => $request->email,
'token' => $request->token,
'new_password' => $request->new_password
];
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'token' => 'required|numeric',
'new_password' => [
'required',
'min:8',
'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/'
]
], [
'email.required' => trans('Validation.required',['attribute' => 'Email']),
'email.email' => trans('Validation.email'),
'token.required' => trans('Validation.required',['attribute' => 'Token']),
'new_password.required' => trans('Validation.required',['attribute' => 'New Password']),
'new_password.min' => trans('Validation.min',['attribute' => 'New Password']),
'new_password.regex' => trans('Validation.regex',['attribute' => 'New Password']),
]);
if($request->new_password != $request->confirm_new_password)
{
return ApiResponse::apiResponse('Bad Request', $data, 'Confirm password is not the same', 400);
}
else if ($validator->fails())
{
return ApiResponse::apiResponse('Bad Request', $data, $validator->errors(), 400);
}
else
{
//Check Time
$check = DB::table('password_resets')
->where('email', '=', $request->email)
->where('token', '=', $request->token)
->select('created_at')
// 🔥 3⃣ Cari user sesuai corporate
$user = User::where('email', $request->email)
->where('corporate_id', $corporate->id)
->first();
if($check)
{
$created_at = strtotime($check->created_at); // Konversi string waktu ke UNIX timestamp
$now = time(); // Waktu sekarang dalam UNIX timestamp
// Hitung selisih waktu dalam menit
$diffInMinutes = ($now - $created_at) / 60;
if ($diffInMinutes > 60) {
return ApiResponse::apiResponse('Not Found', $data, trans('Message.token_expired'), 404);
} else {
// Lanjutkan dengan proses pemulihan kata sandi
$user = User::where('email', $request->email)->first();
if ($user)
{
$newPassword = Hash::make($request->new_password);
$user->password = $newPassword;
$user->save();
return ApiResponse::apiResponse("Success", $data, trans('Message.success'), 200);
}
else
{
return ApiResponse::apiResponse('Not Found', $data, trans('Message.token_expired'), 404);
}
}
}
else
{
return ApiResponse::apiResponse('Not Found', $data, trans('Message.not_found'), 404);
}
if (!$user || !Hash::check($request->password, $user->password)) {
return ApiResponse::apiResponse(
'Unauthorized',
$data,
'Email atau password salah',
401
);
}
try {
// 🔥 4⃣ Generate JWT dengan claim corporate_id
$token = auth('corporate-api')->claims([
'corporate_id' => $corporate->id
])->login($user);
} catch (JWTException $e) {
return ApiResponse::apiResponse(
'Error',
null,
'Gagal membuat token',
500
);
}
$res_data = [
'user' => $user,
'corporate_id' => $corporate->id,
'token' => $token,
'type' => 'Bearer',
'expires_in' => auth('corporate-api')->factory()->getTTL() * 60
];
return ApiResponse::apiResponse(
"Success",
$res_data,
'Login berhasil',
200
);
}
}

View File

@@ -22,50 +22,40 @@ class Authorization
$acceptHeader = $request->header('Accept');
$contentType = $request->header('Content-Type');
$locale = $request->header('Accept-Language');
$authorization = $request->header('Authorization');
// Add language
if(!$locale)
{
return ApiResponse::apiResponse('Unauthorized', null, trans('Validation.required', ['attribute' => 'Accept-Language']), 401);
if (!$locale) {
return ApiResponse::apiResponse(
'Unauthorized',
null,
trans('Validation.required', ['attribute' => 'Accept-Language']),
401
);
}
if($locale !== 'en-US' && $locale !== 'id-ID')
{
return ApiResponse::apiResponse('Bad Request', null, trans('Validation.invalid', ['attribute' => 'Accept-Language']), 400);
}
if ($locale === 'en-US')
{
if ($locale === 'en-US') {
App::setLocale('en');
} elseif ($locale === 'id-ID')
{
} elseif ($locale === 'id-ID') {
App::setLocale('id');
} else
{
App::setLocale('en');
}
// Validate authorization
if (empty($authorization) || strpos($authorization, 'Bearer ') !== 0) {
return ApiResponse::apiResponse('Unauthorized', null, trans('Validation.required', ['attribute' => 'Authorization']), 401);
if ($acceptHeader !== 'application/json') {
return ApiResponse::apiResponse(
'Bad Request',
null,
trans('Validation.invalid', ['attribute' => 'Accept']),
400
);
}
// Validate type accept & content type
if (!$acceptHeader)
{
return ApiResponse::apiResponse('Unauthorized', null, trans('Validation.required', ['attribute' => 'Accept']), 401);
}
if (!$contentType && $request->isMethod('post'))
{
return ApiResponse::apiResponse('Unauthorized', null, trans('Validation.required', ['attribute' => 'Content-Type']), 401);
}
if ($acceptHeader !== 'application/json')
{
return ApiResponse::apiResponse('Bad Request', null, trans('Validation.invalid', ['attribute' => 'Accept']), 400);
}
if($contentType !== 'application/json' && $request->isMethod('post'))
{
return ApiResponse::apiResponse('Bad Request', null, trans('Validation.invalid', ['attribute' => 'Content-Type']), 400);
if ($request->isMethod('post') && $contentType !== 'application/json') {
return ApiResponse::apiResponse(
'Bad Request',
null,
trans('Validation.invalid', ['attribute' => 'Content-Type']),
400
);
}
return $next($request);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Modules\Primaya\Http\Middleware;
use App\Models\Corporate;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckCorporateKey
{
public function handle(Request $request, Closure $next): Response
{
$apiKey = $request->header('X-API-KEY');
$apiSecret = $request->header('X-API-SECRET');
// 🔥 WAJIB: Cegah null atau kosong
if (empty($apiKey) || empty($apiSecret)) {
return response()->json([
'message' => 'API Key dan Secret wajib diisi'
], 401);
}
$corporate = Corporate::where('api_key', $apiKey)
->where('api_secret', $apiSecret)
->first();
if (!$corporate) {
return response()->json([
'message' => 'Invalid API Key'
], 401);
}
return $next($request);
}
}

View File

@@ -16,25 +16,24 @@ use Modules\Primaya\Http\Middleware\Authorization;
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::prefix('v1')->group(function() {
Route::prefix('v1')->group(function () {
Route::prefix('primaya')->group(function () {
Route::middleware(Authentication::class)->group(function () {
Route::controller(AuthController::class)->group(function () {
Route::post('login', 'login');
});
// LOGIN (pakai corporate key)
Route::middleware(['corporate.key'])->group(function () {
Route::post('login', [AuthController::class, 'loginJwt']);
});
Route::middleware('auth:sanctum')->group(function () {
// JWT Protected
Route::middleware(['auth:corporate-api'])->group(function () {
Route::middleware(Authorization::class)->group(function () {
//Search Member
Route::controller(MemberController::class)->group(function () {
Route::post('search-member', 'search');
});
Route::post('search-member', [MemberController::class, 'search']);
});
});
});
});

View File

@@ -67,11 +67,12 @@ class Kernel extends HttpKernel
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'linksehat.old.auth' => \App\Http\Middleware\LinksehatOldAuthMiddleware::class,
'corporate.key' => \Modules\Primaya\Http\Middleware\CheckCorporateKey::class,
// Role
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class,
];
}

View File

@@ -23,10 +23,19 @@ class Authenticate extends Middleware
public function handle($request, Closure $next, ...$guards)
{
if (Auth::guard('sanctum')->guest()) {
return response()->json(['error' => 'Bearer Authorization is required'], 401);
// Kalau tidak ada guard dikirim dari route
if (empty($guards)) {
$guards = [null];
}
return parent::handle($request, $next, ...$guards);
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return $next($request);
}
}
return response()->json([
'error' => 'Unauthorized'
], 401);
}
}

View File

@@ -1,7 +1,7 @@
<?php
namespace App\Models;
use Altek\Accountant\Contracts\Recordable;
use Altek\Accountant\Contracts\Recordable;
use App\Traits\Blameable;
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -23,6 +23,8 @@ class Corporate extends Model
'help_text',
'active',
'linking_rules',
'api_key',
'api_secret',
'automatic_linking',
'phone',
'phone_alarm_canter',

View File

@@ -11,9 +11,10 @@ use Spatie\Permission\Traits\HasRoles;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Illuminate\Support\Facades\DB;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable
class User extends Authenticatable implements JWTSubject
{
use HasApiTokens, HasFactory, Notifiable, HasRoles;
@@ -147,4 +148,14 @@ class User extends Authenticatable
{
return $this->notificationTokens()->orderBy('created_at', 'desc')->pluck('token')->toArray();
}
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}

View File

@@ -40,6 +40,10 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'corporate-api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
/*

301
config/jwt.php Normal file
View File

@@ -0,0 +1,301 @@
<?php
/*
* This file is part of jwt-auth.
*
* (c) Sean Tymon <tymon148@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
/*
|--------------------------------------------------------------------------
| JWT Authentication Secret
|--------------------------------------------------------------------------
|
| Don't forget to set this in your .env file, as it will be used to sign
| your tokens. A helper command is provided for this:
| `php artisan jwt:secret`
|
| Note: This will be used for Symmetric algorithms only (HMAC),
| since RSA and ECDSA use a private/public key combo (See below).
|
*/
'secret' => env('JWT_SECRET'),
/*
|--------------------------------------------------------------------------
| JWT Authentication Keys
|--------------------------------------------------------------------------
|
| The algorithm you are using, will determine whether your tokens are
| signed with a random string (defined in `JWT_SECRET`) or using the
| following public & private keys.
|
| Symmetric Algorithms:
| HS256, HS384 & HS512 will use `JWT_SECRET`.
|
| Asymmetric Algorithms:
| RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below.
|
*/
'keys' => [
/*
|--------------------------------------------------------------------------
| Public Key
|--------------------------------------------------------------------------
|
| A path or resource to your public key.
|
| E.g. 'file://path/to/public/key'
|
*/
'public' => env('JWT_PUBLIC_KEY'),
/*
|--------------------------------------------------------------------------
| Private Key
|--------------------------------------------------------------------------
|
| A path or resource to your private key.
|
| E.g. 'file://path/to/private/key'
|
*/
'private' => env('JWT_PRIVATE_KEY'),
/*
|--------------------------------------------------------------------------
| Passphrase
|--------------------------------------------------------------------------
|
| The passphrase for your private key. Can be null if none set.
|
*/
'passphrase' => env('JWT_PASSPHRASE'),
],
/*
|--------------------------------------------------------------------------
| JWT time to live
|--------------------------------------------------------------------------
|
| Specify the length of time (in minutes) that the token will be valid for.
| Defaults to 1 hour.
|
| You can also set this to null, to yield a never expiring token.
| Some people may want this behaviour for e.g. a mobile app.
| This is not particularly recommended, so make sure you have appropriate
| systems in place to revoke the token if necessary.
| Notice: If you set this to null you should remove 'exp' element from 'required_claims' list.
|
*/
'ttl' => env('JWT_TTL', 60),
/*
|--------------------------------------------------------------------------
| Refresh time to live
|--------------------------------------------------------------------------
|
| Specify the length of time (in minutes) that the token can be refreshed
| within. I.E. The user can refresh their token within a 2 week window of
| the original token being created until they must re-authenticate.
| Defaults to 2 weeks.
|
| You can also set this to null, to yield an infinite refresh time.
| Some may want this instead of never expiring tokens for e.g. a mobile app.
| This is not particularly recommended, so make sure you have appropriate
| systems in place to revoke the token if necessary.
|
*/
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
/*
|--------------------------------------------------------------------------
| JWT hashing algorithm
|--------------------------------------------------------------------------
|
| Specify the hashing algorithm that will be used to sign the token.
|
*/
'algo' => env('JWT_ALGO', Tymon\JWTAuth\Providers\JWT\Provider::ALGO_HS256),
/*
|--------------------------------------------------------------------------
| Required Claims
|--------------------------------------------------------------------------
|
| Specify the required claims that must exist in any token.
| A TokenInvalidException will be thrown if any of these claims are not
| present in the payload.
|
*/
'required_claims' => [
'iss',
'iat',
'exp',
'nbf',
'sub',
'jti',
],
/*
|--------------------------------------------------------------------------
| Persistent Claims
|--------------------------------------------------------------------------
|
| Specify the claim keys to be persisted when refreshing a token.
| `sub` and `iat` will automatically be persisted, in
| addition to the these claims.
|
| Note: If a claim does not exist then it will be ignored.
|
*/
'persistent_claims' => [
// 'foo',
// 'bar',
],
/*
|--------------------------------------------------------------------------
| Lock Subject
|--------------------------------------------------------------------------
|
| This will determine whether a `prv` claim is automatically added to
| the token. The purpose of this is to ensure that if you have multiple
| authentication models e.g. `App\User` & `App\OtherPerson`, then we
| should prevent one authentication request from impersonating another,
| if 2 tokens happen to have the same id across the 2 different models.
|
| Under specific circumstances, you may want to disable this behaviour
| e.g. if you only have one authentication model, then you would save
| a little on token size.
|
*/
'lock_subject' => true,
/*
|--------------------------------------------------------------------------
| Leeway
|--------------------------------------------------------------------------
|
| This property gives the jwt timestamp claims some "leeway".
| Meaning that if you have any unavoidable slight clock skew on
| any of your servers then this will afford you some level of cushioning.
|
| This applies to the claims `iat`, `nbf` and `exp`.
|
| Specify in seconds - only if you know you need it.
|
*/
'leeway' => env('JWT_LEEWAY', 0),
/*
|--------------------------------------------------------------------------
| Blacklist Enabled
|--------------------------------------------------------------------------
|
| In order to invalidate tokens, you must have the blacklist enabled.
| If you do not want or need this functionality, then set this to false.
|
*/
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
/*
| -------------------------------------------------------------------------
| Blacklist Grace Period
| -------------------------------------------------------------------------
|
| When multiple concurrent requests are made with the same JWT,
| it is possible that some of them fail, due to token regeneration
| on every request.
|
| Set grace period in seconds to prevent parallel request failure.
|
*/
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
/*
|--------------------------------------------------------------------------
| Cookies encryption
|--------------------------------------------------------------------------
|
| By default Laravel encrypt cookies for security reason.
| If you decide to not decrypt cookies, you will have to configure Laravel
| to not encrypt your cookie token by adding its name into the $except
| array available in the middleware "EncryptCookies" provided by Laravel.
| see https://laravel.com/docs/master/responses#cookies-and-encryption
| for details.
|
| Set it to true if you want to decrypt cookies.
|
*/
'decrypt_cookies' => false,
/*
|--------------------------------------------------------------------------
| Providers
|--------------------------------------------------------------------------
|
| Specify the various providers used throughout the package.
|
*/
'providers' => [
/*
|--------------------------------------------------------------------------
| JWT Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to create and decode the tokens.
|
*/
'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class,
/*
|--------------------------------------------------------------------------
| Authentication Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to authenticate users.
|
*/
'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
/*
|--------------------------------------------------------------------------
| Storage Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to store tokens in the blacklist.
|
*/
'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,
],
];

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('corporates', function (Blueprint $table) {
$table->string('api_key', 64)
->unique()
->nullable()
->after('linking_rules');
$table->string('api_secret', 128)
->nullable()
->after('api_key');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('corporates', function (Blueprint $table) {
$table->dropColumn(['api_key', 'api_secret']);
});
}
};