From c657eafc709e184be6a6bf9ee86cd8c441998fdf Mon Sep 17 00:00:00 2001 From: Hafifie PKB Date: Mon, 25 Mar 2024 21:40:03 +0800 Subject: [PATCH] added feature for OTP-SMS Koperasi --- .env.example | 5 + .../API/v1/Voter/LoginController.php | 184 +++++++++++++++++- app/UserOTP.php | 13 ++ composer.json | 2 +- config/onewaysms.php | 8 + .../2024_03_23_171932_user_login_otp.php | 36 ++++ package.json | 5 +- resources/assets/js/app.js | 3 + .../assets/js/components/demo/voter/login.vue | 32 ++- .../js/components/demo/voter/verification.vue | 154 +++++++++++++++ .../js/components/mycomponents/countdown.vue | 48 +++++ resources/assets/js/routesIndex.js | 8 + routes/api.php | 1 + 13 files changed, 475 insertions(+), 24 deletions(-) create mode 100644 app/UserOTP.php create mode 100644 config/onewaysms.php create mode 100644 database/migrations/2024_03_23_171932_user_login_otp.php create mode 100644 resources/assets/js/components/demo/voter/verification.vue create mode 100644 resources/assets/js/components/mycomponents/countdown.vue diff --git a/.env.example b/.env.example index ec702ba..2bece89 100644 --- a/.env.example +++ b/.env.example @@ -43,3 +43,8 @@ MAIL_ENCRYPTION=null PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= + +ONEWAY_SMS_USERNAME=APIFKZQX6MN6N +ONEWAY_SMS_PASSWORD=APIFKZQX6MN6N9YHXP +ONEWAY_SMS_SENDERID=INFO +ONEWAY_SMS_LANG=1 diff --git a/app/Http/Controllers/API/v1/Voter/LoginController.php b/app/Http/Controllers/API/v1/Voter/LoginController.php index aa56082..5e66643 100644 --- a/app/Http/Controllers/API/v1/Voter/LoginController.php +++ b/app/Http/Controllers/API/v1/Voter/LoginController.php @@ -5,14 +5,102 @@ namespace App\Http\Controllers\API\v1\Voter; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Controllers\Util; +use App\UserOTP; +use App\Voter; +use Carbon\Carbon; +use Exception; use Illuminate\Support\Facades\Auth; +use GuzzleHttp\Client; class LoginController extends Controller { public function __invoke(Request $request) { //return response()->json(Auth::guard('user')); - if (Auth::guard('voter')->attempt(['no_kp' => $request->no_kp, 'password' => 'admin', 'election_id' => Util::getCurrentElection()])) { + + /* Request OTP */ + try{ + $currentDateTime = Carbon::now(); + + $expired_at = Carbon::now()->addMinutes(config('onewaysms.minutes')); + $created_at = $currentDateTime->toDateTimeString(); + + $voter = Voter::where('no_kp',$request->no_kp)->firstOrFail(); + + if($voter){ + $expiration = $this->checkExpirationTAC($request->no_kp); + + if($expiration['notexpired'] == true){ + throw new \Exception('Expired TAC : ' . $expiration['remaining']); + } + + $sms_token = $this->getTokenSMS($voter->telefon,$currentDateTime); + + if($sms_token == 'error'){ + throw new \Exception('error in getting the data from SMSToken'); + } + + $user_otp = new UserOTP(); + $user_otp->nokp = $voter->no_kp; + $user_otp->telefon = $voter->telefon; + $user_otp->token = $sms_token; + $user_otp->created_at = $created_at; + $user_otp->expired_at = $expired_at; + $user_otp->save(); + + return response()->json([ + 'status' => 'success', + 'message' => 'generated SMS Token', + 'notel' => $voter->telefon, + 'nokp' => $voter->no_kp, + + ]); + + }else{ + return response()->json([ + 'status' => 'failed', + 'message' => 'Does not found NO KP.' + ]); + } + }catch(Exception $e){ + return response()->json([ + 'status' => 'failed', + 'message' => 'Error: ' . $e->getMessage() + ]); + } + } + + public function checkExpirationTAC($nokp) : Array{ + $currentDateTime = Carbon::now(); + + $expiretac = UserOTP::where('nokp',$nokp) + ->whereDate('created_at','=',$currentDateTime) + ->whereDate('expired_at','=',$currentDateTime) + ->whereTime('expired_at','>=',$currentDateTime->toTimeString()) + ->whereTime('created_at','<=',$currentDateTime->toTimeString()) + ->latest() + ->first(); + + if($expiretac){ + $remaining = $this->getRemainingTime($expiretac->expired_at); + return ['notexpired' => true,'remaining' => $remaining]; + }else{ + return ['notexpired' => false]; + } + } + + public function verifyTAC(Request $request){ + + $authenticateOTP = $this->getTACModel($request->no_kp,$request->token); + + if(!$authenticateOTP['condition']){ + return response()->json([ + 'status' => 'failed', + 'message' => $authenticateOTP['message'] + ]); + } + + if (Auth::guard('voter')->attempt(['no_kp' => $request->no_kp, 'password' => 'admin', 'election_id' => Util::getCurrentElection()])) { return response()->json([ 'status' => 'success', 'message' => 'Login successfully.', @@ -26,5 +114,99 @@ class LoginController extends Controller 'message' => 'Invalid ID.' ]); } + + return response()->json([ + 'status' => 'success', + 'message' => 'Login successfully.', + ]); + } + + private function getTACModel($no_kp,$token){ + $currentDateTime = Carbon::now(); + + $result = UserOTP::where('nokp',$no_kp) + ->whereDate('created_at','=',$currentDateTime) + ->whereDate('expired_at','=',$currentDateTime) + ->whereTime('expired_at','>=',$currentDateTime->toTimeString()) + ->whereTime('created_at','<=',$currentDateTime->toTimeString()) + ->latest() + ->first(); + + if(!$result){ + return ['condition' => false, 'message' => 'Token Expired']; + } + + if($result['token'] == $token){ + return ['condition' => true]; + }else{ + return ['condition' => false, 'message' => 'Invalid Token']; + } + } + + private function getTokenSMS($tele,$date){ + $tac_token = $this->randum_number(6,2,false); + if(env("APP_ENV") == 'production'){ + $mobileno = '6' . $tele; + $messages = sprintf(config('onewaysms.message'),$tac_token,$date); + + var_dump($mobileno,$messages); + $parameter = [ + 'apiusername' => env("ONEWAY_SMS_USERNAME"), + 'apipassword' => env("ONEWAY_SMS_PASSWORD"), + 'senderid' => env("ONEWAY_SMS_SENDERID",'INFO'), + 'mobileno' => $mobileno, + 'message'=> $messages, + 'languagetype'=> env("ONEWAY_SMS_LANG",1) + ]; + + $client = new Client(['verify' => false]); + $status = $client->get('http://gateway.onewaysms.com.my:10001/api.aspx',[ + 'query' => $parameter + ]); + + if($status){ + return $tac_token; + }else{ + return 'error'; + } + + }else{ + return $tac_token; + } + } + + private function randum_number($len = 6,$dup = 1, $sort = false){ + if($dup < 1) + throw new \InvalidArgumentException('Second argument is < 1'); + + $num = range(0,9); + shuffle($num); + + $num = array_slice($num, 0, ($len-$dup) + 1); + + if($dup > 0){ + $k = array_rand($num, 1); + for($i=0;$i<($dup-1);$i++) + { + $num[] = $num[$k]; + } + } + + if($sort){ + sort($num); + } + + return implode('',$num); } + + private function getRemainingTime($expired_at){ + $currentDateTime = Carbon::now(); + $expiredAt = Carbon::parse($expired_at); + + $remainingSeconds = $currentDateTime->diffInSeconds($expiredAt); + + $remainingFormatted = gmdate('H:i:s', $remainingSeconds); + + return $remainingFormatted; + } } diff --git a/app/UserOTP.php b/app/UserOTP.php new file mode 100644 index 0000000..ad22a70 --- /dev/null +++ b/app/UserOTP.php @@ -0,0 +1,13 @@ +=5.6.4", "cloudinary/cloudinary_php": "^2.3", "doctrine/dbal": "^2.5", - "guzzlehttp/guzzle": "^6.3", + "guzzlehttp/guzzle": "^6.5", "laravel/framework": "5.8.*", "laravel/passport": "^4.0", "laravel/tinker": "~1.0", diff --git a/config/onewaysms.php b/config/onewaysms.php new file mode 100644 index 0000000..b8ad041 --- /dev/null +++ b/config/onewaysms.php @@ -0,0 +1,8 @@ + 'KOPERASI : TAC: %s - %s NOMBOR TAC', + 'minutes' => 5 +]; + +?> \ No newline at end of file diff --git a/database/migrations/2024_03_23_171932_user_login_otp.php b/database/migrations/2024_03_23_171932_user_login_otp.php new file mode 100644 index 0000000..8c7ee94 --- /dev/null +++ b/database/migrations/2024_03_23_171932_user_login_otp.php @@ -0,0 +1,36 @@ +increments('id'); + $table->string('nokp', 20); + $table->string('telefon'); + $table->string('token'); + $table->timestamp('created_at'); + $table->timestamp('expired_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/package.json b/package.json index 7e841ef..54616db 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "sass": "^1.50.1", "sass-loader": "^12.1.0", "vue-loader": "^15.9.7", - "vue-template-compiler": "^2.6.14" + "vue-template-compiler": "^2.7.16" + }, + "dependencies": { + "@bachdgvn/vue-otp-input": "^1.0.8" } } diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 3182c20..9f3fe8b 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -3,10 +3,13 @@ require('./bootstrap.js'); //import VueRouter from 'vue-router'; import Util from './util.js'; import routes from './routesIndex.js'; +import OtpInput from "@bachdgvn/vue-otp-input"; +Vue.component("v-otp-input", OtpInput); Vue.use(VueRouter); Vue.mixin(Util); +Vue.use(OtpInput); const router = new VueRouter({ mode: 'history', diff --git a/resources/assets/js/components/demo/voter/login.vue b/resources/assets/js/components/demo/voter/login.vue index c9e07ed..23348df 100644 --- a/resources/assets/js/components/demo/voter/login.vue +++ b/resources/assets/js/components/demo/voter/login.vue @@ -43,29 +43,19 @@ export default{ }, methods: { - login: function () { - if (this.loading) return; - - let vm = this; - - this.startLoading(); - + login: function(){ axios.post(config.API+'voter/login', $('#login_form').serialize()) - .then(response => { - vm.stopLoading(); - if (this.util.showResult(response, 'success')) { - localStorage['Access Token'] = `Bearer ${response.data.token}`; - this.util.setAuthorization(); - vm.$router.push({name: 'Voter Home'}); - } - }) - .catch(error => { - vm.stopLoading(); - this.util.showResult(error, 'error'); - }) - + .then(response => { + this.stopLoading(); + if (this.util.showResult(response, 'success')) { + this.$router.push({ name: 'Voter Verify',params: {'nokp' : response.data.nokp,'notel' : response.data.notel} }) + } + }) + .catch(error => { + vm.stopLoading(); + this.util.showResult(error, 'error'); + }) }, - startLoading: function () { this.util.notify('Logging in', 'loading'); this.loading = true; diff --git a/resources/assets/js/components/demo/voter/verification.vue b/resources/assets/js/components/demo/voter/verification.vue new file mode 100644 index 0000000..e47310e --- /dev/null +++ b/resources/assets/js/components/demo/voter/verification.vue @@ -0,0 +1,154 @@ + + + diff --git a/resources/assets/js/components/mycomponents/countdown.vue b/resources/assets/js/components/mycomponents/countdown.vue new file mode 100644 index 0000000..d140ec2 --- /dev/null +++ b/resources/assets/js/components/mycomponents/countdown.vue @@ -0,0 +1,48 @@ + + + diff --git a/resources/assets/js/routesIndex.js b/resources/assets/js/routesIndex.js index d87cf50..6b15e27 100644 --- a/resources/assets/js/routesIndex.js +++ b/resources/assets/js/routesIndex.js @@ -1,4 +1,6 @@ import VoterLogin from './components/demo/voter/login.vue'; +import VoterVerification from './components/demo/voter/verification.vue'; + import VoterHome from './components/demo/voter/index.vue'; import Home from './components/demo/voter/home/index.vue'; @@ -91,6 +93,12 @@ export default [ name: 'Voter Login' }, + { + path: '/verification', + component: VoterVerification, + name: 'Voter Verify' + }, + { path: '/admin/login', component: AdminLogin, diff --git a/routes/api.php b/routes/api.php index 067804f..59fe6f1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,6 +23,7 @@ Route::get('/user', 'Auth\UserController@index')->middleware('auth:api'); Route::prefix('v1')->group(function(){ //Version 1 of my Rest API //Voters API Route::post('voter/login', 'API\v1\Voter\LoginController'); + Route::post('voter/verify','API\v1\Voter\LoginController@verifyTAC'); Route::middleware(['auth:voterAPI', 'voter'])->group(function() { Route::get('election/information', 'API\v1\Election\InformationController');