Merge branch 'feature/standardize-ui-display' into 'main'
Feature/standardize ui display See merge request erahn/voting!19
This commit is contained in:
+5
-1
@@ -54,4 +54,8 @@ ONEWAY_SMS_LANG=1
|
||||
PHYSICAL_ATTENDANCE_GATE_ENABLED=false
|
||||
PHYSICAL_ATTENDANCE_GATE_SECRET=
|
||||
PHYSICAL_ATTENDANCE_GATE_PERIOD=60
|
||||
PHYSICAL_ATTENDANCE_GATE_DISPLAY_KEY=
|
||||
PHYSICAL_ATTENDANCE_GATE_DISPLAY_KEY=
|
||||
|
||||
# API token idle lifetime in minutes (enforced server-side for Passport tokens)
|
||||
VOTER_TOKEN_LIFETIME=120
|
||||
ADMIN_TOKEN_LIFETIME=480
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateVoterLatestYuranSahamFromJson extends Command
|
||||
{
|
||||
protected $signature = 'voter:update-latest-yuransaham-from-json
|
||||
{--file=database/seeds/inject/latest_yuransaham.json : Relative path from project root}
|
||||
{--election_id= : Override election_id (default: latest in voter table)}
|
||||
{--dry-run : Show what would change without updating}';
|
||||
|
||||
protected $description = 'Update voter saham_terkini/yuran_terkini from JSON for latest election only';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$fileOpt = (string) $this->option('file');
|
||||
$filePath = $this->resolvePath($fileOpt);
|
||||
|
||||
if (!is_file($filePath)) {
|
||||
$this->error("❌ JSON file not found: {$filePath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$electionId = $this->option('election_id');
|
||||
$electionId = $electionId !== null && $electionId !== '' ? (int) $electionId : (int) DB::table('voter')->max('election_id');
|
||||
|
||||
if ($electionId <= 0) {
|
||||
$this->error("❌ Cannot determine latest election_id (got {$electionId}).");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$json = file_get_contents($filePath);
|
||||
$rows = json_decode($json, true);
|
||||
|
||||
if (!is_array($rows)) {
|
||||
$this->error("❌ Invalid JSON: expected array of rows.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$noAnggota = isset($row['no_anggota']) ? trim((string) $row['no_anggota']) : '';
|
||||
if ($noAnggota === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$noAnggota] = [
|
||||
'saham_terkini' => $this->cleanNumber($row['saham_terkini'] ?? 0),
|
||||
'yuran_terkini' => $this->cleanNumber($row['yuran_terkini'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
if (count($map) === 0) {
|
||||
$this->error("❌ No usable rows found in JSON (missing no_anggota?).");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
$this->info("Target election_id: {$electionId}");
|
||||
$this->info("JSON members loaded: " . count($map));
|
||||
$this->info($dryRun ? "Mode: DRY RUN (no DB updates)" : "Mode: UPDATE");
|
||||
|
||||
$totalInElection = (int) DB::table('voter')->where('election_id', $electionId)->count();
|
||||
$this->info("Voters in election: {$totalInElection}");
|
||||
|
||||
$updated = 0;
|
||||
$matched = 0;
|
||||
$missingInJson = 0;
|
||||
$unchanged = 0;
|
||||
|
||||
$bar = $this->output->createProgressBar(max(1, $totalInElection));
|
||||
$bar->start();
|
||||
|
||||
DB::table('voter')
|
||||
->select(['id', 'no_anggota', 'saham_terkini', 'yuran_terkini'])
|
||||
->where('election_id', $electionId)
|
||||
->orderBy('id')
|
||||
->chunkById(500, function ($chunk) use ($map, $dryRun, &$updated, &$matched, &$missingInJson, &$unchanged, $bar) {
|
||||
foreach ($chunk as $voter) {
|
||||
$noAnggota = $voter->no_anggota !== null ? trim((string) $voter->no_anggota) : '';
|
||||
|
||||
if ($noAnggota === '' || !array_key_exists($noAnggota, $map)) {
|
||||
$missingInJson++;
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$matched++;
|
||||
$jsonRow = $map[$noAnggota];
|
||||
|
||||
$oldSahamTerkini = $voter->saham_terkini !== null ? (float) $voter->saham_terkini : 0.0;
|
||||
$oldYuranTerkini = $voter->yuran_terkini !== null ? (float) $voter->yuran_terkini : 0.0;
|
||||
|
||||
if (
|
||||
abs($oldSahamTerkini - $jsonRow['saham_terkini']) < 0.00001
|
||||
&& abs($oldYuranTerkini - $jsonRow['yuran_terkini']) < 0.00001
|
||||
) {
|
||||
$unchanged++;
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
DB::table('voter')
|
||||
->where('id', $voter->id)
|
||||
->update([
|
||||
'saham_terkini' => $jsonRow['saham_terkini'],
|
||||
'yuran_terkini' => $jsonRow['yuran_terkini'],
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$updated++;
|
||||
$bar->advance();
|
||||
}
|
||||
}, 'id');
|
||||
|
||||
$bar->finish();
|
||||
$this->line('');
|
||||
|
||||
$this->info("✅ Matched in JSON: {$matched}");
|
||||
$this->info("✅ Updated: {$updated}" . ($dryRun ? " (would update)" : ""));
|
||||
$this->info("✅ Unchanged: {$unchanged}");
|
||||
$this->info("⚠️ Missing in JSON: {$missingInJson}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function resolvePath(string $path): string
|
||||
{
|
||||
if (strpos($path, DIRECTORY_SEPARATOR) === 0) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return base_path($path);
|
||||
}
|
||||
|
||||
private function cleanNumber($value): float
|
||||
{
|
||||
if ($value === null) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$value = (string) $value;
|
||||
$value = str_replace(['RM', ',', ' '], '', $value);
|
||||
|
||||
if (preg_match('/^\((.*)\)$/', $value, $matches)) {
|
||||
$value = $matches[1];
|
||||
}
|
||||
|
||||
$value = preg_replace('/[^0-9\.\-]/', '', $value);
|
||||
|
||||
return is_numeric($value) ? (float) $value : 0.0;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class Kernel extends ConsoleKernel
|
||||
Commands\ImportCSV::class,
|
||||
Commands\UpdateVoterPelaburanFromJson::class,
|
||||
Commands\UpdateVoterDividenFromJson::class,
|
||||
Commands\UpdateVoterLatestYuranSahamFromJson::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\API\v1\Admin\Impersonate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\ApiTokenLifetime;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -72,12 +73,12 @@ class LeaveController extends Controller
|
||||
])
|
||||
->log("admin leave impersonation: {$impersonator->email}");
|
||||
|
||||
return response()->json([
|
||||
return response()->json(array_merge([
|
||||
'status' => 'success',
|
||||
'message' => 'Berjaya kembali ke akaun asal.',
|
||||
'user' => $impersonator,
|
||||
'token' => $accessToken,
|
||||
]);
|
||||
], ApiTokenLifetime::loginMeta('admin')));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\API\v1\Admin\Impersonate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\ApiTokenLifetime;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -70,13 +71,13 @@ class StartController extends Controller
|
||||
])
|
||||
->log("admin impersonate: {$impersonator->email} -> {$target->email}");
|
||||
|
||||
return response()->json([
|
||||
return response()->json(array_merge([
|
||||
'status' => 'success',
|
||||
'message' => 'Impersonate berjaya.',
|
||||
'user' => $target,
|
||||
'impersonator' => $impersonator,
|
||||
'token' => $accessToken,
|
||||
]);
|
||||
], ApiTokenLifetime::loginMeta('admin')));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Support\ApiTokenLifetime;
|
||||
use App\User;
|
||||
|
||||
class LoginController extends Controller
|
||||
@@ -21,11 +22,13 @@ class LoginController extends Controller
|
||||
|
||||
if ($this->checkLogin($request)) {
|
||||
$user = Auth::user();
|
||||
ApiTokenLifetime::revokeActiveTokens($user);
|
||||
$result['status'] = 'success';
|
||||
$result['message'] = 'Login Successfully';
|
||||
$result['user'] = $user;
|
||||
$result['election_status'] = Util::getElectionStatus();
|
||||
$result['token'] = $user->createToken('My app', ['admin'])->accessToken;
|
||||
$result = array_merge($result, ApiTokenLifetime::loginMeta('admin'));
|
||||
|
||||
activity()
|
||||
->performedOn($user)
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\API\v1\Voter;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Support\ApiTokenLifetime;
|
||||
use App\UserOTP;
|
||||
use App\Voter;
|
||||
use Carbon\Carbon;
|
||||
@@ -219,6 +220,7 @@ class LoginController extends Controller
|
||||
|
||||
if (Auth::guard('voter')->attempt(['no_kp' => $request->no_kp, 'password' => 'admin', 'election_id' => Util::getCurrentElection()])) {
|
||||
$user = Auth::guard('voter')->user();
|
||||
ApiTokenLifetime::revokeActiveTokens($user);
|
||||
activity()
|
||||
->performedOn($user)
|
||||
->withProperties([
|
||||
@@ -231,13 +233,13 @@ class LoginController extends Controller
|
||||
])
|
||||
->log("voter login: {$user->name}");
|
||||
|
||||
return response()->json([
|
||||
return response()->json(array_merge([
|
||||
'status' => 'success',
|
||||
'message' => 'Login successfully.',
|
||||
'user' => Auth::guard('voter')->user(),
|
||||
'election_status' => Util::getElectionStatus(),
|
||||
'token' => Auth::guard('voter')->user()->createToken('My Token', ['vote'])->accessToken,
|
||||
]);
|
||||
], ApiTokenLifetime::loginMeta('voter')));
|
||||
}
|
||||
|
||||
activity()
|
||||
|
||||
@@ -67,5 +67,6 @@ class Kernel extends HttpKernel
|
||||
'has_voted' => \App\Http\Middleware\HasVotedMiddleware::class,
|
||||
'physical_gate_display' => \App\Http\Middleware\PhysicalGateDisplayKeyMiddleware::class,
|
||||
'fizikal_registration_verified' => \App\Http\Middleware\FizikalRegistrationVerifiedMiddleware::class,
|
||||
'api_token_lifetime' => \App\Http\Middleware\EnforceApiTokenLifetimeMiddleware::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Support\ApiTokenLifetime;
|
||||
use Closure;
|
||||
|
||||
class EnforceApiTokenLifetimeMiddleware
|
||||
{
|
||||
/**
|
||||
* Enforce application session lifetime for Passport personal access tokens.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param string $portal voter|admin
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next, $portal = 'voter')
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user || ! ApiTokenLifetime::isExpired($user, $portal)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
try {
|
||||
if (method_exists($user, 'token') && $user->token()) {
|
||||
$user->token()->revoke();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Continue with 401 even if revoke fails.
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Sesi tamat. Sila log masuk semula.',
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class ApiTokenLifetime
|
||||
{
|
||||
public static function minutesForPortal(string $portal): int
|
||||
{
|
||||
$key = $portal . '_lifetime';
|
||||
|
||||
return (int) config('api_tokens.' . $key, config('session.lifetime', 120));
|
||||
}
|
||||
|
||||
public static function expiresAtForPortal(string $portal): Carbon
|
||||
{
|
||||
return Carbon::now()->addMinutes(self::minutesForPortal($portal));
|
||||
}
|
||||
|
||||
public static function loginMeta(string $portal): array
|
||||
{
|
||||
$expiresAt = self::expiresAtForPortal($portal);
|
||||
|
||||
return [
|
||||
'token_expires_at' => $expiresAt->toIso8601String(),
|
||||
'token_lifetime_minutes' => self::minutesForPortal($portal),
|
||||
];
|
||||
}
|
||||
|
||||
public static function isExpired(Authenticatable $user, string $portal): bool
|
||||
{
|
||||
if (! method_exists($user, 'token')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = $user->token();
|
||||
if (! $token || ! $token->created_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Carbon::now()->greaterThanOrEqualTo(
|
||||
Carbon::parse($token->created_at)->addMinutes(self::minutesForPortal($portal))
|
||||
);
|
||||
}
|
||||
|
||||
public static function revokeActiveTokens(Authenticatable $user): void
|
||||
{
|
||||
if (! method_exists($user, 'tokens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$user->tokens()->where('revoked', false)->update(['revoked' => true]);
|
||||
} catch (\Throwable $e) {
|
||||
// Login should not fail if revocation fails.
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ class Voter extends Authenticatable
|
||||
|
||||
protected $table = 'voter';
|
||||
|
||||
protected $fillable = ['name', 'no_kp', 'no_anggota','unit', 'election_id', 'alamat', 'telefon', 'saham', 'yuran', 'dividen_saham', 'dividen_yuran', 'kehadiran','status_penyata','persetujuan','tarikh_sah','cadangan','pelaburan', 'accumulated_amount', 'tergempar','barangan','peribadi','berjamin_yuran','roadtax','pelbagai'];
|
||||
protected $fillable = ['name', 'no_kp', 'no_anggota','unit', 'election_id', 'alamat', 'telefon', 'saham', 'yuran', 'dividen_saham', 'dividen_yuran', 'saham_terkini', 'yuran_terkini', 'kehadiran','status_penyata','persetujuan','tarikh_sah','cadangan','pelaburan', 'accumulated_amount', 'tergempar','barangan','peribadi','berjamin_yuran','roadtax','pelbagai'];
|
||||
|
||||
protected $casts = [
|
||||
'fizikal_registration_verified_at' => 'datetime',
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API token idle lifetimes (minutes)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Passport 4 personal access tokens are issued with a one-year JWT TTL.
|
||||
| These values are enforced in middleware based on oauth_access_tokens.created_at.
|
||||
|
|
||||
*/
|
||||
|
||||
'voter_lifetime' => (int) env('VOTER_TOKEN_LIFETIME', env('SESSION_LIFETIME', 120)),
|
||||
|
||||
'admin_lifetime' => (int) env('ADMIN_TOKEN_LIFETIME', 480),
|
||||
|
||||
];
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSahamTerkiniAndYuranTerkiniToVoterTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('voter', function (Blueprint $table) {
|
||||
$table->decimal('saham_terkini', 12, 2)->default(0)->after('dividen_yuran');
|
||||
$table->decimal('yuran_terkini', 12, 2)->default(0)->after('saham_terkini');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('voter', function (Blueprint $table) {
|
||||
$table->dropColumn(['saham_terkini', 'yuran_terkini']);
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+30
-3
@@ -19,16 +19,34 @@ const router = new VueRouter({
|
||||
routes
|
||||
});
|
||||
|
||||
var publicVoterRouteNames = {
|
||||
'Voter Login': true,
|
||||
'Voter Verify': true,
|
||||
'Physical Gate Display': true,
|
||||
'Roulette Wheel Demo': true,
|
||||
'penyata': true
|
||||
};
|
||||
|
||||
router.beforeEach(function (to, from, next) {
|
||||
if (to.name === 'Voter Login' && utilMethods.isAdminPortalSession()) {
|
||||
var dest = utilMethods.getAdminEntryRoute();
|
||||
return next(Object.assign({}, dest, { replace: true }));
|
||||
}
|
||||
if (!to.path.startsWith('/admin') || to.name === 'Admin Login') {
|
||||
|
||||
var isAdminRoute = to.path.indexOf('/admin') === 0;
|
||||
var isPublicVoterRoute = !!publicVoterRouteNames[to.name]
|
||||
|| to.path.indexOf('/display/') === 0
|
||||
|| to.path.indexOf('/demo/') === 0;
|
||||
|
||||
if (!isAdminRoute && !isPublicVoterRoute && !utilMethods.isLogin()) {
|
||||
return next({ name: 'Voter Login', replace: true });
|
||||
}
|
||||
|
||||
if (!isAdminRoute || to.name === 'Admin Login') {
|
||||
return next();
|
||||
}
|
||||
if (!localStorage.getItem('Access Token')) {
|
||||
return next();
|
||||
if (!utilMethods.isLogin()) {
|
||||
return next({ name: 'Admin Login', replace: true });
|
||||
}
|
||||
var role = localStorage.getItem('admin_role');
|
||||
if (role === '3' && to.name !== 'Kehadiran Calon') {
|
||||
@@ -37,6 +55,15 @@ router.beforeEach(function (to, from, next) {
|
||||
next();
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(function (response) {
|
||||
return response;
|
||||
}, function (error) {
|
||||
if (error.response && error.response.status === 401) {
|
||||
utilMethods.handleUnauthorized(router);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
const app = new Vue({
|
||||
router
|
||||
}).$mount('#app');
|
||||
|
||||
@@ -139,15 +139,14 @@ export default {
|
||||
// Swap admin token + user in local state.
|
||||
var token = response.data.token;
|
||||
if (token) {
|
||||
localStorage['Access Token'] = 'Bearer ' + token;
|
||||
vm.util.persistLoginSession(token, response.data.token_expires_at, {
|
||||
adminSession: true,
|
||||
adminRole: response.data.user && response.data.user.role
|
||||
});
|
||||
localStorage.setItem('is_impersonating', '1');
|
||||
if (vm.util && vm.util.setAuthorization) vm.util.setAuthorization();
|
||||
}
|
||||
if (response.data.user) {
|
||||
vm.data.user = response.data.user;
|
||||
if (response.data.user.role !== undefined && response.data.user.role !== null) {
|
||||
localStorage.setItem('admin_role', String(response.data.user.role));
|
||||
}
|
||||
}
|
||||
|
||||
vm.util.notify('Menyamar berjaya', 'success');
|
||||
|
||||
@@ -146,9 +146,14 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
$.notifyClose();
|
||||
if (this.util.showResult(error) == 401) {
|
||||
vm.logout();
|
||||
var status = error.response ? error.response.status : 500;
|
||||
if (status === 401) {
|
||||
vm.util.clearSession();
|
||||
vm.$router.replace({ name: 'Admin Login' });
|
||||
return;
|
||||
}
|
||||
vm.util.showResult(error, 'error');
|
||||
vm.loading = false;
|
||||
})
|
||||
} else {
|
||||
this.$router.push({ name: 'Admin Login' });
|
||||
@@ -186,15 +191,14 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (response.data.token) {
|
||||
localStorage['Access Token'] = 'Bearer ' + response.data.token;
|
||||
if (vm.util && vm.util.setAuthorization) vm.util.setAuthorization();
|
||||
vm.util.persistLoginSession(response.data.token, response.data.token_expires_at, {
|
||||
adminSession: true,
|
||||
adminRole: response.data.user && response.data.user.role
|
||||
});
|
||||
}
|
||||
localStorage.removeItem('is_impersonating');
|
||||
if (response.data.user) {
|
||||
vm.data.user = response.data.user;
|
||||
if (response.data.user.role !== undefined && response.data.user.role !== null) {
|
||||
localStorage.setItem('admin_role', String(response.data.user.role));
|
||||
}
|
||||
}
|
||||
vm.util.notify('Berjaya kembali', 'success');
|
||||
if (vm.$router) vm.$router.go(0);
|
||||
@@ -206,9 +210,7 @@ export default {
|
||||
},
|
||||
|
||||
logout: function () {
|
||||
localStorage.removeItem('admin_role');
|
||||
localStorage.removeItem('is_impersonating');
|
||||
localStorage.clear();
|
||||
this.util.clearSession();
|
||||
this.$router.push({ name: 'Admin Login' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,12 +84,10 @@ export default {
|
||||
.then(response => {
|
||||
vm.stopLoading();
|
||||
if (this.util.showResult(response, 'success')) {
|
||||
localStorage['Access Token'] = `Bearer ${response.data.token}`;
|
||||
localStorage.setItem('admin_session', '1');
|
||||
if (response.data.user && response.data.user.role !== undefined && response.data.user.role !== null) {
|
||||
localStorage.setItem('admin_role', String(response.data.user.role));
|
||||
}
|
||||
this.util.setAuthorization();
|
||||
this.util.persistLoginSession(response.data.token, response.data.token_expires_at, {
|
||||
adminSession: true,
|
||||
adminRole: response.data.user && response.data.user.role
|
||||
});
|
||||
if (Number(response.data.user && response.data.user.role) === 3) {
|
||||
vm.$router.push({ name: 'Kehadiran Calon' });
|
||||
} else {
|
||||
|
||||
@@ -53,13 +53,21 @@
|
||||
</div>
|
||||
|
||||
<div class="member-info-card home-member-card home-member-card--in-section">
|
||||
<div v-for="row in memberRows" :key="row.label" class="member-field">
|
||||
<div v-for="row in memberRows" :key="row.key || row.label" class="member-field">
|
||||
<span class="member-field-icon" aria-hidden="true"><i class="fa" :class="row.icon"></i></span>
|
||||
<div class="member-field-body">
|
||||
<span class="member-field-label">{{ row.label }}</span>
|
||||
<div class="member-field-value-wrap">
|
||||
<span class="member-field-value">{{ row.value }}</span>
|
||||
<span v-if="row.subvalue" class="member-field-subvalue">{{ row.subvalue }}</span>
|
||||
<template v-if="row.entries">
|
||||
<div v-for="(entry, entryIdx) in row.entries" :key="entryIdx" class="member-field-entry">
|
||||
<span class="member-field-value">{{ entry.period }}: {{ entry.value }}</span>
|
||||
<span v-if="entry.subvalue" class="member-field-subvalue">{{ entry.subvalue }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="member-field-value">{{ row.value }}</span>
|
||||
<span v-if="row.subvalue" class="member-field-subvalue">{{ row.subvalue }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,19 +407,43 @@ export default {
|
||||
{ icon: "fa-barcode", label: "No. Anggota", value: u.no_anggota || "—" },
|
||||
{ icon: "fa-building", label: "Unit", value: u.unit || "—" },
|
||||
{
|
||||
key: "saham",
|
||||
icon: "fa-line-chart",
|
||||
label: "Saham",
|
||||
value: "RM " + fmt(u.saham),
|
||||
subvalue: dividendSub(u.saham, u.dividen_saham, 40)
|
||||
entries: [
|
||||
{
|
||||
period: "Sehingga Jun 2025",
|
||||
value: "RM " + fmt(u.saham),
|
||||
subvalue: dividendSub(u.saham, u.dividen_saham, 40)
|
||||
},
|
||||
{
|
||||
period: "Terkumpul Sehingga Mei 2026",
|
||||
value: "RM " + fmt(u.saham_terkini)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "yuran",
|
||||
icon: "fa-money",
|
||||
label: "Yuran",
|
||||
value: "RM " + fmt(u.yuran),
|
||||
subvalue: dividendSub(u.yuran, u.dividen_yuran, 8)
|
||||
entries: [
|
||||
{
|
||||
period: "Sehingga Jun 2025",
|
||||
value: "RM " + fmt(u.yuran),
|
||||
subvalue: dividendSub(u.yuran, u.dividen_yuran, 8)
|
||||
},
|
||||
{
|
||||
period: "Terkumpul Sehingga Mei 2026",
|
||||
value: "RM " + fmt(u.yuran_terkini)
|
||||
}
|
||||
]
|
||||
},
|
||||
{ icon: "fa-bank", label: "Simpanan Khas (Tabung 1) - (Sehingga Dis 2025)", value: "RM " + fmt(u.accumulated_amount) },
|
||||
{ icon: "fa-percent", label: "Dividen Simpanan Khas (Tabung 1) Tahun 2025", value: "RM " + fmt(u.pelaburan) }
|
||||
{
|
||||
icon: "fa-bank",
|
||||
label: "Simpanan Khas (Tabung 1) - (Sehingga Dis 2025)",
|
||||
value: "RM " + fmt(u.accumulated_amount),
|
||||
subvalue: dividendSub(u.accumulated_amount, u.pelaburan, 12)
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
@@ -1043,6 +1075,20 @@ export default {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.member-field-entry {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.member-field-entry+.member-field-entry {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.member-field-value {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
|
||||
@@ -47,6 +47,10 @@ export default {
|
||||
}),
|
||||
|
||||
created: function () {
|
||||
if (!this.util.isLogin()) {
|
||||
this.$router.replace({ name: 'Voter Login' });
|
||||
return;
|
||||
}
|
||||
this.refreshInfo();
|
||||
},
|
||||
|
||||
@@ -81,7 +85,7 @@ export default {
|
||||
axios.post(config.API + 'voter/logout')
|
||||
.catch(function () { })
|
||||
.finally(function () {
|
||||
localStorage.clear();
|
||||
vm.util.clearSession();
|
||||
vm.$router.push({ name: 'Voter Login' });
|
||||
});
|
||||
},
|
||||
@@ -105,10 +109,14 @@ export default {
|
||||
vm.data.result = response.data.result;
|
||||
})
|
||||
.catch(error => {
|
||||
vm.loading = false;
|
||||
if (vm.util.showResult(error, 'error') == 401) {
|
||||
vm.$router.push({ name: 'Voter Login' });
|
||||
var status = error.response ? error.response.status : 500;
|
||||
if (status === 401) {
|
||||
vm.util.clearSession();
|
||||
vm.$router.replace({ name: 'Voter Login' });
|
||||
return;
|
||||
}
|
||||
vm.loading = false;
|
||||
vm.util.showResult(error, 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,10 +157,9 @@ export default {
|
||||
.then(response => {
|
||||
vm.stopLoading();
|
||||
if (this.util.showResult(response, 'success')) {
|
||||
localStorage.removeItem('admin_role');
|
||||
localStorage.removeItem('admin_session');
|
||||
localStorage['Access Token'] = `Bearer ${response.data.token}`;
|
||||
this.util.setAuthorization();
|
||||
this.util.persistLoginSession(response.data.token, response.data.token_expires_at, {
|
||||
clearAdminSession: true
|
||||
});
|
||||
vm.$router.push({ name: 'Voter Home' });
|
||||
}
|
||||
})
|
||||
|
||||
Vendored
+72
-1
@@ -14,7 +14,78 @@ const methods = {
|
||||
* @return {Boolean} isLogin
|
||||
*/
|
||||
isLogin: function () {
|
||||
return localStorage['Access Token'] ? true : false;
|
||||
if (!localStorage['Access Token']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isSessionExpired()) {
|
||||
this.clearSession();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
isSessionExpired: function () {
|
||||
var expiresAt = localStorage.getItem('token_expires_at');
|
||||
if (!expiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Date.now() >= new Date(expiresAt).getTime();
|
||||
},
|
||||
|
||||
clearSession: function () {
|
||||
localStorage.removeItem('Access Token');
|
||||
localStorage.removeItem('token_expires_at');
|
||||
localStorage.removeItem('admin_role');
|
||||
localStorage.removeItem('admin_session');
|
||||
localStorage.removeItem('is_impersonating');
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
},
|
||||
|
||||
persistLoginSession: function (token, expiresAt, options) {
|
||||
options = options || {};
|
||||
var bearer = token.indexOf('Bearer ') === 0 ? token : 'Bearer ' + token;
|
||||
localStorage['Access Token'] = bearer;
|
||||
|
||||
if (expiresAt) {
|
||||
localStorage.setItem('token_expires_at', expiresAt);
|
||||
} else {
|
||||
localStorage.removeItem('token_expires_at');
|
||||
}
|
||||
|
||||
if (options.adminRole !== undefined && options.adminRole !== null) {
|
||||
localStorage.setItem('admin_role', String(options.adminRole));
|
||||
}
|
||||
|
||||
if (options.adminSession) {
|
||||
localStorage.setItem('admin_session', '1');
|
||||
}
|
||||
|
||||
if (options.clearAdminSession) {
|
||||
localStorage.removeItem('admin_role');
|
||||
localStorage.removeItem('admin_session');
|
||||
}
|
||||
|
||||
this.setAuthorization();
|
||||
},
|
||||
|
||||
handleUnauthorized: function (router) {
|
||||
var isAdminPath = window.location.pathname.indexOf('/admin') === 0;
|
||||
this.clearSession();
|
||||
|
||||
if (!router || !router.currentRoute) {
|
||||
window.location.href = isAdminPath ? '/admin/login' : '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
var routeName = router.currentRoute.name;
|
||||
if (routeName === 'Voter Login' || routeName === 'Admin Login') {
|
||||
return;
|
||||
}
|
||||
|
||||
router.replace({ name: isAdminPath ? 'Admin Login' : 'Voter Login' });
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ Route::prefix('v1')->group(function () { //Version 1 of my Rest 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::middleware(['auth:voterAPI', 'api_token_lifetime:voter', 'voter'])->group(function () {
|
||||
Route::get('election/information', 'API\v1\Election\InformationController');
|
||||
Route::post('election/vote', 'API\v1\Election\VoteController')->middleware(['fizikal_registration_verified', 'isvoted']);
|
||||
Route::get('election/result', 'API\v1\Election\ResultController')->middleware('has_voted');
|
||||
@@ -52,7 +52,7 @@ Route::prefix('v1')->group(function () { //Version 1 of my Rest API
|
||||
//Admins API
|
||||
Route::post('admin/login', 'API\v1\Admin\LoginController'); //Excluding Login for auth middleware
|
||||
|
||||
Route::middleware(['auth:api', 'admin', 'election', 'attendance_committee_role'])->group(function () {
|
||||
Route::middleware(['auth:api', 'api_token_lifetime:admin', 'admin', 'election', 'attendance_committee_role'])->group(function () {
|
||||
|
||||
Route::prefix('admin')->group(function () { //Route /api/v1/admin
|
||||
Route::get('information', 'API\v1\Admin\InformationController');
|
||||
|
||||
Reference in New Issue
Block a user