From 67228628b10dd950064e68d59ea77706e8c94dcd Mon Sep 17 00:00:00 2001 From: ISMAIL MASSERAN Date: Mon, 5 Oct 2026 11:31:18 +0800 Subject: [PATCH] DONE: improve auth security, add email verification when register, add mailpit to docker service --- .env.example.local => .env.development | 4 +- .env.docker | 6 +- .env.example.prod | 323 ------------------ .../Controllers/AuthMobile/AuthController.php | 81 +++-- app/Mail/VerifyEmail.php | 31 ++ config/sanctum.php | 2 +- ...ill_email_verified_at_for_active_users.php | 24 ++ docker-compose.yml | 14 +- .../views/template/partials/footer.blade.php | 17 + .../views/template/partials/header.blade.php | 52 +++ .../views/template/verify-email.blade.php | 21 ++ routes/api.php | 1 + 12 files changed, 219 insertions(+), 357 deletions(-) rename .env.example.local => .env.development (99%) delete mode 100644 .env.example.prod create mode 100644 app/Mail/VerifyEmail.php create mode 100644 database/migrations/2026_10_05_000001_backfill_email_verified_at_for_active_users.php create mode 100644 resources/views/template/partials/footer.blade.php create mode 100644 resources/views/template/partials/header.blade.php create mode 100644 resources/views/template/verify-email.blade.php diff --git a/.env.example.local b/.env.development similarity index 99% rename from .env.example.local rename to .env.development index 35675b1d..a3633bdc 100644 --- a/.env.example.local +++ b/.env.development @@ -278,12 +278,12 @@ REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_MAILER=smtp -MAIL_HOST=mailhog +MAIL_HOST=mailpit MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS=null +MAIL_FROM_ADDRESS=noreply@lelong.local MAIL_FROM_NAME="${APP_NAME}" AWS_ACCESS_KEY_ID= diff --git a/.env.docker b/.env.docker index 51fe9042..ca644663 100644 --- a/.env.docker +++ b/.env.docker @@ -3,7 +3,7 @@ APP_ENV=local APP_KEY=base64:ezY+zzzO7+TOh5WtUG544ffDFNCqNQr4c9/uxFCVywo= APP_DEBUG=true APP_URL=http://localhost:8082 -APP_PEMBIDA=http://localhost:8080/ +APP_PEMBIDA=http://localhost:8083/ API_URL=http://host.docker.internal:8080/ API_URL_KAUNTER=http://host.docker.internal:8081/ API_VERIFY=false @@ -43,12 +43,12 @@ REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_MAILER=smtp -MAIL_HOST=mailhog +MAIL_HOST=mailpit MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS=null +MAIL_FROM_ADDRESS=noreply@lelong.local MAIL_FROM_NAME="${APP_NAME}" AWS_ACCESS_KEY_ID= diff --git a/.env.example.prod b/.env.example.prod deleted file mode 100644 index 0b2eb3aa..00000000 --- a/.env.example.prod +++ /dev/null @@ -1,323 +0,0 @@ -APP_NAME=Laravel -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL=http://localhost -APP_PEMBIDA=http://localhost:8080/ -API_URL=https://api_arrahn.test/lelong/ -API_URL_KAUNTER=http://kaunter.test/ -API_VERIFY = false - -LOG_CHANNEL=stack -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=lelong -DB_USERNAME=root -DB_PASSWORD=G0dz!ll4@2020 - -DB_CONNECTION_MAIN=mysql -DB_HOST_MAIN=192.168.0.101 -DB_PORT_MAIN=3306 -DB_DATABASE_MAIN=online_arrahn -DB_USERNAME_MAIN=spider -DB_PASSWORD_MAIN=G0dz!ll42020 - -DB_CONNECTION_KB2=mysql -DB_HOST_KB2=192.168.0.101 -DB_PORT_KB2=3306 -DB_DATABASE_KB2=arrahn_kb2 -DB_USERNAME_KB2=spider -DB_PASSWORD_KB2=G0dz!ll42020 - -DB_CONNECTION_WCY=mysql -DB_HOST_WCY=192.168.0.101 -DB_PORT_WCY=3306 -DB_DATABASE_WCY=arrahn_wcy -DB_USERNAME_WCY=spider -DB_PASSWORD_WCY=G0dz!ll42020 - -DB_CONNECTION_1STOP=mysql -DB_HOST_1STOP=192.168.0.101 -DB_PORT_1STOP=3306 -DB_DATABASE_1STOP=arrahn_1stop -DB_USERNAME_1STOP=spider -DB_PASSWORD_1STOP=G0dz!ll42020 - -DB_CONNECTION_020301=mysql -DB_HOST_020301=192.168.0.101 -DB_PORT_020301=3306 -DB_DATABASE_020301=arrahn_jlwt -DB_USERNAME_020301=spider -DB_PASSWORD_020301=G0dz!ll42020 - -DB_CONNECTION_WB=mysql -DB_HOST_WB=192.168.0.101 -DB_PORT_WB=3306 -DB_DATABASE_WB=arrahn_wb -DB_USERNAME_WB=spider -DB_PASSWORD_WB=G0dz!ll42020 - -DB_CONNECTION_ALS=mysql -DB_HOST_ALS=192.168.0.101 -DB_PORT_ALS=3306 -DB_DATABASE_ALS=arrahn_kls -DB_USERNAME_ALS=spider -DB_PASSWORD_ALS=G0dz!ll42020 - -DB_CONNECTION_020306=mysql -DB_HOST_020306=192.168.0.101 -DB_PORT_020306=3306 -DB_DATABASE_020306=arrahn_pp -DB_USERNAME_020306=spider -DB_PASSWORD_020306=G0dz!ll42020 - -DB_CONNECTION_ARM=mysql -DB_HOST_ARM=192.168.0.101 -DB_PORT_ARM=3306 -DB_DATABASE_ARM=arrahn_mace -DB_USERNAME_ARM=spider -DB_PASSWORD_ARM=G0dz!ll42020 - -DB_CONNECTION_KB1=mysql -DB_HOST_KB1=192.168.0.101 -DB_PORT_KB1=3306 -DB_DATABASE_KB1=arrahn_kb1 -DB_USERNAME_KB1=spider -DB_PASSWORD_KB1=G0dz!ll42020 - -DB_CONNECTION_APM=mysql -DB_HOST_APM=192.168.0.101 -DB_PORT_APM=3306 -DB_DATABASE_APM=arrahn_pm -DB_USERNAME_APM=spider -DB_PASSWORD_APM=G0dz!ll42020 - -DB_CONNECTION_010311=mysql -DB_HOST_010311=192.168.0.101 -DB_PORT_010311=3306 -DB_DATABASE_010311=arrahn_gm -DB_USERNAME_010311=spider -DB_PASSWORD_010311=G0dz!ll42020 - -DB_CONNECTION_GC=mysql -DB_HOST_GC=192.168.0.101 -DB_PORT_GC=3306 -DB_DATABASE_GC=arrahn_gcl -DB_USERNAME_GC=spider -DB_PASSWORD_GC=G0dz!ll42020 - -DB_CONNECTION_JLI=mysql -DB_HOST_JLI=192.168.0.101 -DB_PORT_JLI=3306 -DB_DATABASE_JLI=arrahn_jli -DB_USERNAME_JLI=spider -DB_PASSWORD_JLI=G0dz!ll42020 - -DB_CONNECTION_ARP=mysql -DB_HOST_ARP=192.168.0.101 -DB_PORT_ARP=3306 -DB_DATABASE_ARP=arrahn_rp -DB_USERNAME_ARP=spider -DB_PASSWORD_ARP=G0dz!ll42020 - -DB_CONNECTION_010621=mysql -DB_HOST_010621=192.168.0.101 -DB_PORT_010621=3306 -DB_DATABASE_010621=arrahn_ktn -DB_USERNAME_010621=spider -DB_PASSWORD_010621=G0dz!ll42020 - -DB_CONNECTION_010136=mysql -DB_HOST_010136=192.168.0.101 -DB_PORT_010136=3306 -DB_DATABASE_010136=arrahn_pg -DB_USERNAME_010136=spider -DB_PASSWORD_010136=G0dz!ll42020 - -DB_CONNECTION_011235=mysql -DB_HOST_011235=192.168.0.101 -DB_PORT_011235=3306 -DB_DATABASE_011235=arrahn_skudai -DB_USERNAME_011235=spider -DB_PASSWORD_011235=G0dz!ll42020 - -DB_CONNECTION_011227=mysql -DB_HOST_011227=192.168.0.101 -DB_PORT_011227=3306 -DB_DATABASE_011227=arrahn_pttn -DB_USERNAME_011227=spider -DB_PASSWORD_011227=G0dz!ll42020 - -DB_CONNECTION_010620=mysql -DB_HOST_010620=192.168.0.101 -DB_PORT_010620=3306 -DB_DATABASE_010620=arrahn_btg -DB_USERNAME_010620=spider -DB_PASSWORD_010620=G0dz!ll42020 - -DB_CONNECTION_010617=mysql -DB_HOST_010617=192.168.0.101 -DB_PORT_010617=3306 -DB_DATABASE_010617=arrahn_raub -DB_USERNAME_010617=spider -DB_PASSWORD_010617=G0dz!ll42020 - -DB_CONNECTION_010915=mysql -DB_HOST_010915=192.168.0.101 -DB_PORT_010915=3306 -DB_DATABASE_010915=arrahn_kgr -DB_USERNAME_010915=spider -DB_PASSWORD_010915=G0dz!ll42020 - -DB_CONNECTION_011228=mysql -DB_HOST_011228=192.168.0.101 -DB_PORT_011228=3306 -DB_DATABASE_011228=arrahn_sdk -DB_USERNAME_011228=spider -DB_PASSWORD_011228=G0dz!ll42020 - -DB_CONNECTION_010219=mysql -DB_HOST_010219=192.168.0.101 -DB_PORT_010219=3306 -DB_DATABASE_010219=arrahn_sp -DB_USERNAME_010219=spider -DB_PASSWORD_010219=G0dz!ll42020 - -DB_CONNECTION_011229=mysql -DB_HOST_011229=192.168.0.101 -DB_PORT_011229=3306 -DB_DATABASE_011229=arrahn_twu -DB_USERNAME_011229=spider -DB_PASSWORD_011229=G0dz!ll42020 - -DB_CONNECTION_011230=mysql -DB_HOST_011230=192.168.0.101 -DB_PORT_011230=3306 -DB_DATABASE_011230=arrahn_api2 -DB_USERNAME_011230=spider -DB_PASSWORD_011230=G0dz!ll42020 - -DB_CONNECTION_011013=mysql -DB_HOST_011013=192.168.0.101 -DB_PORT_011013=3306 -DB_DATABASE_011013=arrahn_kjg -DB_USERNAME_011013=spider -DB_PASSWORD_011013=G0dz!ll42020 - -DB_CONNECTION_011412=mysql -DB_HOST_011412=192.168.0.101 -DB_PORT_011412=3306 -DB_DATABASE_011412=arrahn_rl -DB_USERNAME_011412=spider -DB_PASSWORD_011412=G0dz!ll42020 - -DB_CONNECTION_011231=mysql -DB_HOST_011231=192.168.0.101 -DB_PORT_011231=3306 -DB_DATABASE_011231=arrahn_bfort -DB_USERNAME_011231=spider -DB_PASSWORD_011231=G0dz!ll42020 - -DB_CONNECTION_011232=mysql -DB_HOST_011232=192.168.0.101 -DB_PORT_011232=3306 -DB_DATABASE_011232=arrahn_kbelud -DB_USERNAME_011232=spider -DB_PASSWORD_011232=G0dz!ll42020 - -DB_CONNECTION_011234=mysql -DB_HOST_011234=192.168.0.101 -DB_PORT_011234=3306 -DB_DATABASE_011234=arrahn_tuaran -DB_USERNAME_011234=spider -DB_PASSWORD_011234=G0dz!ll42020 - -DB_CONNECTION_011233=mysql -DB_HOST_011233=192.168.0.101 -DB_PORT_011233=3306 -DB_DATABASE_011233=arrahn_kngau -DB_USERNAME_011233=spider -DB_PASSWORD_011233=G0dz!ll42020 - -DB_CONNECTION_020303=mysql -DB_HOST_020303=192.168.0.101 -DB_PORT_020303=3306 -DB_DATABASE_020303=arrahn_tm -DB_USERNAME_020303=spider -DB_PASSWORD_020303=G0dz!ll42020 - -DB_CONNECTION_010618=mysql -DB_HOST_010618=192.168.0.101 -DB_PORT_010618=3306 -DB_DATABASE_010618=arrahn_tmlh -DB_USERNAME_010618=spider -DB_PASSWORD_010618=G0dz!ll42020 - -# DB_CONNECTION_011237=mysql -# DB_HOST_011237=192.168.0.101 -# DB_PORT_011237=3306 -# DB_DATABASE_011237=arrahn_semporna -# DB_USERNAME_011237=spider -# DB_PASSWORD_011237=G0dz!ll42020 - -BROADCAST_DRIVER=log -CACHE_DRIVER=file -FILESYSTEM_DRIVER=local -QUEUE_CONNECTION=sync -SESSION_DRIVER=file -SESSION_LIFETIME=120 - -MEMCACHED_HOST=127.0.0.1 - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=smtp -MAIL_HOST=mailhog -MAIL_PORT=1025 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS=null -MAIL_FROM_NAME="${APP_NAME}" - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false - -PUSHER_APP_ID= -PUSHER_APP_KEY= -PUSHER_APP_SECRET= -PUSHER_APP_CLUSTER=mt1 - -MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" -MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" - -MARHUN_CAMERAS="Doccamera" - -WABOT_API=https://md.wabot.my/api/send.php -WABOT_INSTANCES_ID=63C26B7AAD21A -WABOT_TOKEN_KEYS=cbee7e95cf3ffbfa0531d5fddfd44f87 - -# wabot use for blast message to pembida -WABOT_API_SECOND=https://md.wabot.my/api/send.php -WABOT_INSTANCES_ID_SECOND=64892FD7440A6 -WABOT_TOKEN_KEYS_SECOND=cbee7e95cf3ffbfa0531d5fddfd44f87 - -# for config STORAGE_DIRECTORY, set value without slash at the end -# STORAGE_DIRECTORY=/Users/UTM/Sites/lelong/storage - -ADMIN_EMAIL="admin_1@example.com,admin_2@example.com" - -# fill in phone number for local testing -ADMIN_PHONE_NUMBER=601234566789 - -ONE_WAY_SMS_USERNAME=apiusername -ONE_WAY_SMS_PASSWORD=apipassword \ No newline at end of file diff --git a/app/Http/Controllers/AuthMobile/AuthController.php b/app/Http/Controllers/AuthMobile/AuthController.php index c02c018a..1e44bd52 100644 --- a/app/Http/Controllers/AuthMobile/AuthController.php +++ b/app/Http/Controllers/AuthMobile/AuthController.php @@ -14,10 +14,12 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Support\Facades\Mail; use App\Mail\ResetPassword; +use App\Mail\VerifyEmail; class AuthController extends Controller { - public function register(Request $request) { + public function register(Request $request) + { // dd($request->all); $validator = Validator::make($request->all(), [ 'name' => 'required|string', @@ -56,6 +58,7 @@ class AuthController extends Controller $currentdate = Carbon::now(); $currentdate = $currentdate->toDateTimeString(); + $verificationToken = Str::random(60); $user = new User; $user->name = $fields['name']; @@ -69,34 +72,57 @@ class AuthController extends Controller $user->status = 'active'; $user->user_type = 2; //type 2 = pembida $user->alamat = $fields['alamat']; + $user->token = $verificationToken; + $user->is_verified = 0; + $user->email_verified_at = null; $user->save(); - // if($user->save()) - // { - // $images = $request->images; - // foreach($images as $img) - // { - // $file = new File; - // $file->user_id = $user->id; - // $file->file_type = $img['file_type']; - // $file->filename = $img['filename']; - // $file->save(); + $verifyUrl = url('/api/verifyEmail') . '?' . http_build_query([ + 'email' => $user->email, + 'token' => $verificationToken, + ]); - // } + Mail::to($user->email)->send(new VerifyEmail($user->name, $verifyUrl)); - - // } - - $token = $user->createToken('myapptoken')->plainTextToken; $response = [ 'success' => true, + 'message' => 'Pendaftaran berjaya. Sila semak emel anda untuk pautan pengesahan.', 'user' => $user, - 'token' => $token ]; return response($response, 201); } + public function verifyEmail(Request $request) + { + $redirectBase = rtrim(config('app.app_pembida'), '/'); + + $validator = Validator::make($request->all(), [ + 'email' => 'required|email', + 'token' => 'required|string', + ]); + + if ($validator->fails()) { + return redirect()->away($redirectBase . '?email_verified=0&message=' . urlencode('Pautan pengesahan tidak sah.')); + } + + $user = User::where('email', $request->email) + ->where('token', $request->token) + ->where('is_verified', 0) + ->first(); + + if (!$user) { + return redirect()->away($redirectBase . '?email_verified=0&message=' . urlencode('Pautan pengesahan tidak sah atau emel telah disahkan.')); + } + + $user->is_verified = 1; + $user->email_verified_at = Carbon::now(); + $user->token = null; + $user->save(); + + return redirect()->away($redirectBase . '?email_verified=1&message=' . urlencode('Emel berjaya disahkan.')); + } + public function loginUser(Request $request) { $fields = $request->validate([ @@ -120,7 +146,13 @@ class AuthController extends Controller 'message2' => "Sila login menggunakan url " . env('APP_URL'), ]); } else { - if ($user->status == 'active') { + if (is_null($user->email_verified_at)) { + return response([ + 'statusAkaun' => 7, + 'message' => 'Sila sahkan emel anda sebelum log masuk. Semak peti masuk anda untuk pautan pengesahan.', + ]); + } else if ($user->status == 'active') { + $user->tokens()->delete(); $token = $user->createToken('myapptoken')->plainTextToken; return response([ 'statusAkaun' => 1, @@ -158,13 +190,14 @@ class AuthController extends Controller } } - public function logoutUser(Request $request){ - auth()->user()->currentAccessToken()->delete(); - - return [ - 'message' => 'Logged Out' - ]; + public function logoutUser(Request $request) + { + $token = $request->user()?->currentAccessToken(); + if ($token) { + $token->delete(); + } + return response()->json(['message' => 'Logged Out']); } public function forgotPassword(Request $request) diff --git a/app/Mail/VerifyEmail.php b/app/Mail/VerifyEmail.php new file mode 100644 index 00000000..7fe5876d --- /dev/null +++ b/app/Mail/VerifyEmail.php @@ -0,0 +1,31 @@ +name = $name; + $this->verifyUrl = $verifyUrl; + } + + public function build() + { + $user['name'] = $this->name; + $user['verify_url'] = $this->verifyUrl; + + return $this->from(config('mail.from.address'), config('mail.from.name')) + ->subject('ArrahnBid - Pengesahan Emel') + ->view('template.verify-email', ['user' => $user]); + } +} diff --git a/config/sanctum.php b/config/sanctum.php index 442726a7..827f3cf0 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -30,7 +30,7 @@ return [ | */ - 'expiration' => null, + 'expiration' => 60 * 2, // 2 hours /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2026_10_05_000001_backfill_email_verified_at_for_active_users.php b/database/migrations/2026_10_05_000001_backfill_email_verified_at_for_active_users.php new file mode 100644 index 00000000..e0a4db92 --- /dev/null +++ b/database/migrations/2026_10_05_000001_backfill_email_verified_at_for_active_users.php @@ -0,0 +1,24 @@ +where('status', 'active') + ->whereNull('email_verified_at') + ->update([ + 'email_verified_at' => DB::raw('COALESCE(t_daftar, NOW())'), + 'is_verified' => 1, + ]); + } + + public function down() + { + // Irreversible data backfill. + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 76c27058..20030d5f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,16 +9,22 @@ services: - "8082:8000" volumes: - ./:/var/www/html:cached - environment: - APP_ENV: local - APP_DEBUG: "true" - # Lets PHP resolve host.docker.internal to the host machine (Linux Docker; Desktop already provides this). extra_hosts: - "host.docker.internal:host-gateway" networks: - default - api_arrahn_default + mailpit: + image: axllent/mailpit + container_name: lelong-mailpit + restart: unless-stopped + ports: + - "8025:8025" # Web UI + - "1025:1025" # SMTP + networks: + - default + networks: api_arrahn_default: external: true diff --git a/resources/views/template/partials/footer.blade.php b/resources/views/template/partials/footer.blade.php new file mode 100644 index 00000000..4ca93315 --- /dev/null +++ b/resources/views/template/partials/footer.blade.php @@ -0,0 +1,17 @@ +

+ Terima Kasih, +

+

+ ArrahnBid +

+ + + + + + + + + + + diff --git a/resources/views/template/partials/header.blade.php b/resources/views/template/partials/header.blade.php new file mode 100644 index 00000000..90430c17 --- /dev/null +++ b/resources/views/template/partials/header.blade.php @@ -0,0 +1,52 @@ + + + + + + + + + + + + + +
+ + + + + +
+ ArrahnBid +
+ + +
diff --git a/resources/views/template/verify-email.blade.php b/resources/views/template/verify-email.blade.php new file mode 100644 index 00000000..f1bc7bbe --- /dev/null +++ b/resources/views/template/verify-email.blade.php @@ -0,0 +1,21 @@ +@include('template.partials.header') + +

+ Hello {{ $user['name'] ?? '' }}, +

+

+ Terima kasih kerana mendaftar di ArrahnBid. Sila sahkan alamat emel anda dengan mengklik butang di bawah: +

+ +

+ Sahkan Emel Anda +

+ +

+ Jika butang tidak berfungsi, salin dan tampal pautan ini ke pelayar anda: +

+

+ {{ $user['verify_url'] }} +

+ +@include('template.partials.footer') diff --git a/routes/api.php b/routes/api.php index 542f8f9c..bdb1bff4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,6 +23,7 @@ use App\Http\Controllers\ApiController; //Public routes Route::post('/register', [AuthController::class, 'register']); +Route::get('/verifyEmail', [AuthController::class, 'verifyEmail']); Route::post('/user/login', [AuthController::class, 'loginUser']); Route::post('/forgotPassword', [AuthController::class, 'forgotPassword']);