Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,19 @@ LONGITUDE_MAP = 115.046600

API_DATABASE_GABUNGAN_HOST=http://127.0.0.1:8001
SANCTUM_STATEFUL_DOMAINS=localhost

# OTP Configuration
OTP_TOKEN_EXPIRES_MINUTES=5
OTP_MAX_VERIFICATION_ATTEMPTS=3

# OTP Rate Limiter Configuration
OTP_SETUP_MAX_ATTEMPTS=3
OTP_SETUP_DECAY_SECONDS=300
OTP_VERIFY_MAX_ATTEMPTS=5
OTP_VERIFY_DECAY_SECONDS=300
OTP_RESEND_MAX_ATTEMPTS=2
OTP_RESEND_DECAY_SECONDS=30

# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
TELEGRAM_BOT_NAME=@your_bot_username_here
45 changes: 45 additions & 0 deletions app/Console/Commands/OtpCleanupCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Console\Commands;

use App\Services\OtpService;
use Illuminate\Console\Command;

class OtpCleanupCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'otp:cleanup';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Cleanup expired OTP tokens';

protected $otpService;

public function __construct(OtpService $otpService)
{
parent::__construct();
$this->otpService = $otpService;
}

/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting OTP cleanup...');

$deleted = $this->otpService->cleanupExpired();

$this->info("Cleanup completed. Deleted {$deleted} expired tokens.");

return 0;
}
}
8 changes: 8 additions & 0 deletions app/Console/Commands/updateAdminMenu.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,21 @@ private function collectPermissions()
$permissions = [];
foreach (Modul::Menu as $main_menu) {
foreach (Modul::permision as $permission) {
$menuPermission = $main_menu['permission'] ?? null;
if(empty($menuPermission)){
continue;
}
$permissionName = $main_menu['permission'].'-'.$permission;
Permission::findOrCreate($permissionName, 'web');
$permissions[] = $permissionName;
}
if (isset($main_menu['submenu'])) {
foreach ($main_menu['submenu'] as $sub_menu) {
foreach (Modul::permision as $permission) {
$subMenuPermission = $sub_menu['permission'] ?? null;
if(empty($subMenuPermission)){
continue;
}
$permissionName = $sub_menu['permission'].'-'.$permission;
Permission::findOrCreate($permissionName, 'web');
$permissions[] = $permissionName;
Expand Down
3 changes: 3 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();

// Cleanup expired OTP tokens every 30 minutes
$schedule->command('otp:cleanup')->everyThirtyMinutes();
}

/**
Expand Down
5 changes: 5 additions & 0 deletions app/Enums/Modul.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@ final class Modul extends Enum
'url' => 'pengaturan/settings',
'permission' => 'pengaturan-settings',
],
[
'text' => 'Aktivasi OTP',
'icon' => 'far fa-fw fa-circle',
'url' => 'pengaturan/otp',
]
],
],
];
Expand Down
169 changes: 169 additions & 0 deletions app/Http/Controllers/Auth/OtpLoginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\OtpLoginRequest;
use App\Http\Requests\OtpVerifyRequest;
use App\Models\User;
use App\Services\OtpService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;

class OtpLoginController extends Controller
{
protected $otpService;

public function __construct(OtpService $otpService)
{
$this->middleware('guest')->except('logout');
$this->otpService = $otpService;
}

/**
* Tampilkan form OTP login
*/
public function showLoginForm()
{
return view('auth.otp-login');
}

/**
* Kirim OTP untuk login
*/
public function sendOtp(OtpLoginRequest $request)
{
// Rate limiting
$key = 'otp-login:' . $request->ip();
$maxAttempts = env('OTP_VERIFY_MAX_ATTEMPTS', 5); // Default to 5 if not set in .env
$decaySeconds = env('OTP_VERIFY_DECAY_SECONDS', 300); // Default to 300 seconds if not set in .env

if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
return response()->json([
'success' => false,
'message' => 'Terlalu banyak percobaan. Coba lagi dalam ' . RateLimiter::availableIn($key) . ' detik.'
], 429);
}

RateLimiter::hit($key, $decaySeconds);

// Cari user berdasarkan identifier
$user = User::where('otp_enabled', true)
->where(function($query) use ($request) {
$query->where('otp_identifier', $request->identifier)
->orWhere('telegram_chat_id', $request->identifier);
})
->first();

if (!$user) {
return response()->json([
'success' => false,
'message' => 'User tidak ditemukan atau OTP tidak aktif'
], 404);
}

// Tentukan channel dan identifier
$channels = $user->getOtpChannels();
$channel = $channels[0] ?? 'email'; // Ambil channel pertama

$identifier = $request->identifier;

$result = $this->otpService->generateAndSend($user->id, $channel, $identifier);

if ($result['success']) {
// Simpan user ID di session untuk verifikasi
$request->session()->put('otp_login_user_id', $user->id);
$request->session()->put('otp_login_channel', $channel);
}

return response()->json($result, $result['success'] ? 200 : 400);
}

/**
* Verifikasi OTP dan login
*/
public function verifyOtp(OtpVerifyRequest $request)
{

$userId = $request->session()->get('otp_login_user_id');
if (!$userId) {
return response()->json([
'success' => false,
'message' => 'Sesi login tidak ditemukan. Silakan mulai dari awal.'
], 400);
}

// Rate limiting untuk verifikasi
$key = 'otp-verify-login:' . $request->ip();
$maxAttempts = env('OTP_VERIFY_MAX_ATTEMPTS', 5); // Default to 5 if not set in .env
$decaySeconds = env('OTP_VERIFY_DECAY_SECONDS', 300); // Default to 300 seconds if not set in .env

if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
return response()->json([
'success' => false,
'message' => 'Terlalu banyak percobaan verifikasi. Coba lagi dalam ' . RateLimiter::availableIn($key) . ' detik.'
], 429);
}

RateLimiter::hit($key, $decaySeconds);

$result = $this->otpService->verify($userId, $request->otp);

if ($result['success']) {
$user = User::find($userId);

// Login user
Auth::login($user, true);

// Clear session
$request->session()->forget(['otp_login_user_id', 'otp_login_channel']);
RateLimiter::clear($key);

return response()->json([
'success' => true,
'message' => 'Login berhasil',
'redirect' => \App\Providers\RouteServiceProvider::HOME
]);
}

return response()->json([
'success' => false,
'message' => $result['message']
], 400);
}

/**
* Kirim ulang OTP
*/
public function resendOtp(Request $request)
{
$userId = $request->session()->get('otp_login_user_id');
$channel = $request->session()->get('otp_login_channel');

if (!$userId || !$channel) {
return response()->json([
'success' => false,
'message' => 'Sesi login tidak ditemukan.'
], 400);
}

// Rate limiting untuk resend
$key = 'otp-resend-login:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 2)) {
return response()->json([
'success' => false,
'message' => 'Tunggu ' . RateLimiter::availableIn($key) . ' detik sebelum mengirim ulang.'
], 429);
}

RateLimiter::hit($key, 60);

$user = User::find($userId);
$identifier = $user->otp_identifier;

$result = $this->otpService->generateAndSend($userId, $channel, $identifier);

return response()->json($result, $result['success'] ? 200 : 400);
}
}
Loading
Loading