asd
This commit is contained in:
commit
3e4a056b14
67 files changed
+13926
No files matched your search
@@ -0,0 +1,498 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
/*
|
||||
* POST handlers for the admin page. Each returns null on success (after a
|
||||
* flash + redirect) or an array ['errors' => [...], 'draft' => [...]] so the
|
||||
* form can be re-rendered with what the admin typed.
|
||||
*/
|
||||
|
||||
const MAX_TIER_ROWS = 30;
|
||||
const MAX_NOMINAL = 1e15; // Rp1.000 triliun; also keeps values inside DECIMAL(20,0)
|
||||
|
||||
/** Rejects oversized or malformed array input before any processing. */
|
||||
function guard_rows(array $rows, array &$errors, string $key, string $label): array
|
||||
{
|
||||
if (count($rows) > MAX_TIER_ROWS) {
|
||||
$errors[$key] = "$label: maksimal " . MAX_TIER_ROWS . ' baris.';
|
||||
return [];
|
||||
}
|
||||
return array_values(array_filter($rows, 'is_array'));
|
||||
}
|
||||
|
||||
function nominal_error(?float $v, string $label): ?string
|
||||
{
|
||||
return $v !== null && $v > MAX_NOMINAL ? "$label terlalu besar (maksimal Rp1.000 triliun)." : null;
|
||||
}
|
||||
|
||||
function pct_bounds_error($v, string $label, float $max = 1.0): ?string
|
||||
{
|
||||
if ($v === false || $v === null) {
|
||||
return "$label wajib diisi dengan angka persen, contoh 0,75.";
|
||||
}
|
||||
if ($v < 0 || $v > $max) {
|
||||
return "$label harus antara 0 dan " . fmt_pct($max) . '%.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Parses tier rows [['max' => '1.000.000', 'rate' => '0,75'], ...]; last row is "di atas". */
|
||||
function parse_tier_rows(array $rows, string $label, array &$errors, string $errKey): array
|
||||
{
|
||||
$rows = guard_rows($rows, $errors, $errKey, $label);
|
||||
if (isset($errors[$errKey])) {
|
||||
return [];
|
||||
}
|
||||
$rows = array_values(array_filter($rows, fn($r) => trim(input_str($r['max'] ?? '')) !== '' || trim(input_str($r['rate'] ?? '')) !== ''));
|
||||
if (!$rows) {
|
||||
$errors[$errKey] = "$label: minimal satu baris.";
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
$prev = -1;
|
||||
$last = count($rows) - 1;
|
||||
foreach ($rows as $i => $r) {
|
||||
$max = $i === $last ? null : parse_idr($r['max'] ?? '');
|
||||
$rate = parse_pct($r['rate'] ?? '');
|
||||
$n = $i + 1;
|
||||
if ($e = nominal_error($max, "$label baris $n")) {
|
||||
$errors[$errKey] = $e;
|
||||
} elseif ($i !== $last && $max === null) {
|
||||
$errors[$errKey] = "$label baris $n: isi batas nominal (hanya baris terakhir yang tanpa batas).";
|
||||
} elseif ($max !== null && $max <= $prev) {
|
||||
$errors[$errKey] = "$label baris $n: batas nominal harus lebih besar dari baris sebelumnya.";
|
||||
} elseif ($err = pct_bounds_error($rate, "$label baris $n")) {
|
||||
$errors[$errKey] = $err;
|
||||
}
|
||||
if ($max !== null) {
|
||||
$prev = $max;
|
||||
}
|
||||
$out[] = ['max' => $max, 'rate' => $rate === false ? null : $rate];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function changed_settings(array $old, array $new): array
|
||||
{
|
||||
$diff = [];
|
||||
foreach ($new as $k => $v) {
|
||||
if (abs((float)$old[$k]['value'] - $v) > 1e-9) {
|
||||
$diff[$k] = ['label' => $old[$k]['label'], 'unit' => $old[$k]['unit'], 'old' => (float)$old[$k]['value'], 'new' => $v];
|
||||
}
|
||||
}
|
||||
return $diff;
|
||||
}
|
||||
|
||||
function action_save_settings(): ?array
|
||||
{
|
||||
$rows = settings_rows();
|
||||
$input = (array)($_POST['s'] ?? []);
|
||||
$errors = [];
|
||||
$values = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$raw = input_str($input[$key] ?? '');
|
||||
if ($row['unit'] === 'pct') {
|
||||
$v = parse_pct($raw);
|
||||
if ($e = pct_bounds_error($v, $row['label'])) {
|
||||
$errors["s.$key"] = $e;
|
||||
continue;
|
||||
}
|
||||
} elseif ($row['unit'] === 'idr') {
|
||||
$v = parse_idr($raw);
|
||||
if ($v === null) {
|
||||
$errors["s.$key"] = $row['label'] . ' wajib diisi.';
|
||||
continue;
|
||||
}
|
||||
if ($e = nominal_error($v, $row['label'])) {
|
||||
$errors["s.$key"] = $e;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$v = is_numeric(str_replace(',', '.', $raw)) ? (float)str_replace(',', '.', $raw) : null;
|
||||
if ($v === null || $v <= 0 || $v > 1000) {
|
||||
$errors["s.$key"] = $row['label'] . ' harus angka lebih dari 0.';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$values[$key] = $v;
|
||||
}
|
||||
if (isset($values['hari_setahun']) && ($values['hari_setahun'] < 360 || $values['hari_setahun'] > 366)) {
|
||||
$errors['s.hari_setahun'] = 'Hari bunga setahun antara 360 dan 366.';
|
||||
}
|
||||
if (isset($values['hari_sebulan']) && ($values['hari_sebulan'] < 28 || $values['hari_sebulan'] > 31)) {
|
||||
$errors['s.hari_sebulan'] = 'Hari bunga sebulan antara 28 dan 31.';
|
||||
}
|
||||
if (isset($values['porsi_puab'], $values['porsi_kredit']) && abs($values['porsi_puab'] + $values['porsi_kredit'] - 1) > 1e-9) {
|
||||
$errors['s.porsi_kredit'] = 'Porsi PUAB + porsi kredit harus 100%.';
|
||||
}
|
||||
if (isset($values['gwm'], $values['kas']) && $values['gwm'] + $values['kas'] >= 1) {
|
||||
$errors['s.kas'] = 'GWM + kas harus kurang dari 100%.';
|
||||
}
|
||||
if ($errors) {
|
||||
return ['errors' => $errors, 'draft' => $input];
|
||||
}
|
||||
|
||||
$diff = changed_settings($rows, $values);
|
||||
if (!$diff) {
|
||||
flash('info', 'Tidak ada perubahan parameter.');
|
||||
redirect(base_url('admin/?tab=parameter'));
|
||||
}
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
$st = $pdo->prepare('UPDATE settings SET value = ? WHERE skey = ?');
|
||||
foreach ($diff as $k => $d) {
|
||||
$st->execute([$d['new'], $k]);
|
||||
}
|
||||
audit('param.settings', 'Mengubah ' . count($diff) . ' parameter: ' . implode(', ', array_column($diff, 'label')), $diff);
|
||||
$pdo->commit();
|
||||
flash('ok', count($diff) . ' parameter disimpan. Kalkulator langsung memakai nilai baru.');
|
||||
redirect(base_url('admin/?tab=parameter'));
|
||||
}
|
||||
|
||||
function action_save_products(): ?array
|
||||
{
|
||||
$input = (array)($_POST['tiers'] ?? []);
|
||||
$errors = [];
|
||||
$parsed = [];
|
||||
foreach (SERUBI_PRODUCTS as $key => $label) {
|
||||
$parsed[$key] = parse_tier_rows((array)($input[$key] ?? []), $label, $errors, "tiers.$key");
|
||||
}
|
||||
if ($errors) {
|
||||
return ['errors' => $errors, 'draft' => $input];
|
||||
}
|
||||
$before = load_params()['productRates'];
|
||||
$after = [];
|
||||
foreach ($parsed as $k => $rows) {
|
||||
$after[$k] = array_map(fn($r) => ['max' => $r['max'], 'rate' => $r['rate']], $rows);
|
||||
}
|
||||
$changed = array_keys(array_filter($after, fn($rows, $k) => json_encode($rows) !== json_encode($before[$k]), ARRAY_FILTER_USE_BOTH));
|
||||
if (!$changed) {
|
||||
flash('info', 'Tidak ada perubahan rate produk.');
|
||||
redirect(base_url('admin/?tab=produk'));
|
||||
}
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
$del = $pdo->prepare('DELETE FROM product_rates WHERE product = ?');
|
||||
$ins = $pdo->prepare('INSERT INTO product_rates (product, max_nominal, rate, sort) VALUES (?,?,?,?)');
|
||||
foreach ($changed as $k) {
|
||||
$del->execute([$k]);
|
||||
foreach ($after[$k] as $i => $r) {
|
||||
$ins->execute([$k, $r['max'], $r['rate'], $i]);
|
||||
}
|
||||
}
|
||||
$names = array_map(fn($k) => SERUBI_PRODUCTS[$k], $changed);
|
||||
audit('param.produk', 'Mengubah rate ' . implode(', ', $names),
|
||||
array_combine($changed, array_map(fn($k) => ['old' => $before[$k], 'new' => $after[$k]], $changed)));
|
||||
$pdo->commit();
|
||||
flash('ok', 'Rate ' . implode(', ', $names) . ' disimpan.');
|
||||
redirect(base_url('admin/?tab=produk'));
|
||||
}
|
||||
|
||||
function action_save_deposito(): ?array
|
||||
{
|
||||
$params = load_params();
|
||||
$tenors = $params['tenors'];
|
||||
$bandsIn = array_values((array)($_POST['bands'] ?? []));
|
||||
$ratesIn = array_values((array)($_POST['rates'] ?? []));
|
||||
$errors = [];
|
||||
if (count($bandsIn) > MAX_TIER_ROWS || count($ratesIn) > MAX_TIER_ROWS) {
|
||||
return ['errors' => ['deposito' => 'Maksimal ' . MAX_TIER_ROWS . ' kelompok nominal.'], 'draft' => null];
|
||||
}
|
||||
$bandsIn = array_map(fn($b) => is_array($b) ? '' : (string)$b, $bandsIn);
|
||||
$ratesIn = array_map(fn($r) => is_array($r) ? $r : [], $ratesIn);
|
||||
$count = count($bandsIn);
|
||||
if ($count === 0) {
|
||||
$errors['deposito'] = 'Minimal satu kelompok nominal.';
|
||||
}
|
||||
$bands = [];
|
||||
$matrix = [];
|
||||
$prev = -1;
|
||||
foreach ($bandsIn as $i => $raw) {
|
||||
$max = $i === $count - 1 ? null : parse_idr($raw);
|
||||
if ($e = nominal_error($max, 'Baris ' . ($i + 1))) {
|
||||
$errors['deposito'] = $e;
|
||||
} elseif ($i !== $count - 1 && $max === null) {
|
||||
$errors['deposito'] = 'Baris ' . ($i + 1) . ': isi batas nominal (hanya baris terakhir yang tanpa batas).';
|
||||
} elseif ($max !== null && $max <= $prev) {
|
||||
$errors['deposito'] = 'Baris ' . ($i + 1) . ': batas nominal harus lebih besar dari baris sebelumnya.';
|
||||
}
|
||||
if ($max !== null) {
|
||||
$prev = $max;
|
||||
}
|
||||
$bands[] = $max;
|
||||
foreach ($tenors as $t) {
|
||||
$v = parse_pct($ratesIn[$i][$t] ?? '');
|
||||
if ($e = pct_bounds_error($v, 'Rate ' . $t . ' bulan baris ' . ($i + 1))) {
|
||||
$errors['deposito'] = $e;
|
||||
}
|
||||
$matrix[$i][$t] = $v === false ? null : $v;
|
||||
}
|
||||
}
|
||||
if ($errors) {
|
||||
return ['errors' => $errors, 'draft' => ['bands' => $bandsIn, 'rates' => $ratesIn]];
|
||||
}
|
||||
$before = ['bands' => $params['depositoBands'], 'rates' => $params['depositoRates']];
|
||||
$afterRates = [];
|
||||
foreach ($tenors as $t) {
|
||||
foreach ($bands as $i => $max) {
|
||||
$afterRates[$t][] = ['max' => $max, 'rate' => $matrix[$i][$t]];
|
||||
}
|
||||
}
|
||||
if (json_encode($before['rates']) === json_encode($afterRates)) {
|
||||
flash('info', 'Tidak ada perubahan rate deposito.');
|
||||
redirect(base_url('admin/?tab=deposito'));
|
||||
}
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
$pdo->exec('DELETE FROM deposito_bands');
|
||||
$insB = $pdo->prepare('INSERT INTO deposito_bands (max_nominal, sort) VALUES (?,?)');
|
||||
$insR = $pdo->prepare('INSERT INTO deposito_rates (band_id, tenor, rate) VALUES (?,?,?)');
|
||||
foreach ($bands as $i => $max) {
|
||||
$insB->execute([$max, $i]);
|
||||
$id = (int)$pdo->lastInsertId();
|
||||
foreach ($tenors as $t) {
|
||||
$insR->execute([$id, $t, $matrix[$i][$t]]);
|
||||
}
|
||||
}
|
||||
audit('param.deposito', 'Mengubah tabel rate deposito', ['old' => $before['rates'], 'new' => $afterRates]);
|
||||
$pdo->commit();
|
||||
flash('ok', 'Rate deposito disimpan.');
|
||||
redirect(base_url('admin/?tab=deposito'));
|
||||
}
|
||||
|
||||
function action_save_cashback(): ?array
|
||||
{
|
||||
$params = load_params();
|
||||
$errors = [];
|
||||
$rowsIn = guard_rows((array)($_POST['cb'] ?? []), $errors, 'cb', 'Porsi cashback');
|
||||
if ($errors) {
|
||||
return ['errors' => $errors, 'draft' => null];
|
||||
}
|
||||
$tenorIn = array_map('intval', array_keys((array)($_POST['cbtenor'] ?? [])));
|
||||
$errors = [];
|
||||
$tiers = [];
|
||||
$seen = [];
|
||||
foreach ($rowsIn as $i => $r) {
|
||||
if (trim(input_str($r['min'] ?? '')) === '' && trim(input_str($r['share'] ?? '')) === '') {
|
||||
continue;
|
||||
}
|
||||
$min = parse_idr($r['min'] ?? '');
|
||||
$share = parse_pct($r['share'] ?? '');
|
||||
$n = $i + 1;
|
||||
if ($min === null || $min <= 0) {
|
||||
$errors['cb'] = "Baris $n: nominal minimal wajib diisi.";
|
||||
} elseif ($e = nominal_error($min, "Baris $n")) {
|
||||
$errors['cb'] = $e;
|
||||
} elseif (isset($seen[(string)$min])) {
|
||||
$errors['cb'] = "Baris $n: nominal minimal sama dengan baris lain.";
|
||||
} elseif ($e = pct_bounds_error($share, "Porsi nasabah baris $n")) {
|
||||
$errors['cb'] = $e;
|
||||
}
|
||||
$seen[(string)$min] = true;
|
||||
$tiers[] = ['min' => $min, 'nasabah' => $share === false ? null : $share];
|
||||
}
|
||||
if (!$tiers && !$errors) {
|
||||
$errors['cb'] = 'Minimal satu tingkat porsi cashback.';
|
||||
}
|
||||
$tenorIn = array_values(array_intersect($params['tenors'], $tenorIn));
|
||||
if ($errors) {
|
||||
return ['errors' => $errors, 'draft' => ['cb' => $rowsIn, 'cbtenor' => $tenorIn]];
|
||||
}
|
||||
usort($tiers, fn($a, $b) => $a['min'] <=> $b['min']);
|
||||
$before = ['tiers' => $params['cashbackTiers'], 'tenors' => $params['cashbackTenors']];
|
||||
$after = ['tiers' => $tiers, 'tenors' => $tenorIn];
|
||||
if (json_encode($before) === json_encode($after)) {
|
||||
flash('info', 'Tidak ada perubahan porsi cashback.');
|
||||
redirect(base_url('admin/?tab=cashback'));
|
||||
}
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
$pdo->exec('DELETE FROM cashback_tiers');
|
||||
$ins = $pdo->prepare('INSERT INTO cashback_tiers (min_nominal, share_nasabah) VALUES (?,?)');
|
||||
foreach ($tiers as $t) {
|
||||
$ins->execute([$t['min'], $t['nasabah']]);
|
||||
}
|
||||
$up = $pdo->prepare('UPDATE tenors SET cashback = ? WHERE months = ?');
|
||||
foreach ($params['tenors'] as $m) {
|
||||
$up->execute([in_array($m, $tenorIn, true) ? 1 : 0, $m]);
|
||||
}
|
||||
audit('param.cashback', 'Mengubah porsi cashback / tenor program', ['old' => $before, 'new' => $after]);
|
||||
$pdo->commit();
|
||||
flash('ok', 'Porsi cashback disimpan.');
|
||||
redirect(base_url('admin/?tab=cashback'));
|
||||
}
|
||||
|
||||
function action_reset_defaults(): ?array
|
||||
{
|
||||
if (($_POST['confirm'] ?? '') !== 'RESET') {
|
||||
return ['errors' => ['reset' => 'Ketik RESET untuk konfirmasi.'], 'draft' => []];
|
||||
}
|
||||
$before = load_params();
|
||||
db_seed(db(), true);
|
||||
audit('param.reset', 'Mengembalikan semua parameter ke nilai file Excel', ['old' => $before]);
|
||||
flash('ok', 'Semua parameter dikembalikan ke nilai file Excel 22092026.');
|
||||
redirect(base_url('admin/?tab=parameter'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Managing other admins is reserved for super admins and needs the acting
|
||||
* admin's own password (re-authentication). Returns an error response or null.
|
||||
*/
|
||||
function user_admin_guard(array $me, string $errKey, int $targetId = 0): ?array
|
||||
{
|
||||
if (!is_super_admin($me)) {
|
||||
http_response_code(403);
|
||||
return ['errors' => [$errKey => 'Hanya super admin yang dapat mengelola akun admin lain.', 'user_id' => $targetId], 'draft' => []];
|
||||
}
|
||||
if (!reauth_ok($me, input_str($_POST['confirm_password'] ?? ''))) {
|
||||
return ['errors' => [$errKey => 'Password Anda (konfirmasi) salah.', 'user_id' => $targetId], 'draft' => []];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function admin_target(int $id): ?array
|
||||
{
|
||||
$st = db()->prepare('SELECT * FROM admins WHERE id = ?');
|
||||
$st->execute([$id]);
|
||||
return $st->fetch() ?: null;
|
||||
}
|
||||
|
||||
function active_super_admins(): int
|
||||
{
|
||||
return (int)db()->query("SELECT COUNT(*) FROM admins WHERE role = 'super_admin' AND is_active = 1")->fetchColumn();
|
||||
}
|
||||
|
||||
function action_user_create(array $me): ?array
|
||||
{
|
||||
$u = strtolower(trim(input_str($_POST['username'] ?? '')));
|
||||
$n = trim(input_str($_POST['full_name'] ?? ''));
|
||||
if ($denied = user_admin_guard($me, 'user_new')) {
|
||||
$denied['draft'] = ['username' => $u, 'full_name' => $n];
|
||||
return $denied;
|
||||
}
|
||||
$p = input_str($_POST['password'] ?? '');
|
||||
$role = input_str($_POST['role'] ?? '') === 'super_admin' ? 'super_admin' : 'admin';
|
||||
$err = validate_new_admin($u, $n, $p, input_str($_POST['password_confirm'] ?? ''));
|
||||
if (!$err) {
|
||||
$st = db()->prepare('SELECT COUNT(*) FROM admins WHERE username = ?');
|
||||
$st->execute([$u]);
|
||||
if ((int)$st->fetchColumn() > 0) {
|
||||
$err = 'Username sudah dipakai.';
|
||||
}
|
||||
}
|
||||
if ($err) {
|
||||
return ['errors' => ['user_new' => $err], 'draft' => ['username' => $u, 'full_name' => $n]];
|
||||
}
|
||||
// The new admin replaces this initial password and sets up OTP at the first login.
|
||||
db()->prepare('INSERT INTO admins (username, full_name, role, password_hash, must_change_password) VALUES (?,?,?,?,1)')
|
||||
->execute([$u, $n, $role, password_hash($p, PASSWORD_DEFAULT)]);
|
||||
audit('user.create', "Menambah admin $u (" . ($role === 'super_admin' ? 'super admin' : 'admin') . ')');
|
||||
flash('ok', "Admin $u ditambahkan. Password awal wajib diganti dan OTP diaktifkan saat login pertama.");
|
||||
redirect(base_url('admin/?tab=pengguna'));
|
||||
}
|
||||
|
||||
function action_user_toggle(array $me): ?array
|
||||
{
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($denied = user_admin_guard($me, 'user', $id)) {
|
||||
return $denied;
|
||||
}
|
||||
$row = admin_target($id);
|
||||
if (!$row) {
|
||||
return ['errors' => ['user' => 'Admin tidak ditemukan.'], 'draft' => []];
|
||||
}
|
||||
if ($id === $me['id']) {
|
||||
return ['errors' => ['user' => 'Anda tidak dapat menonaktifkan akun sendiri.'], 'draft' => []];
|
||||
}
|
||||
$active = (int)$row['is_active'] ? 0 : 1;
|
||||
if (!$active && $row['role'] === 'super_admin' && active_super_admins() <= 1) {
|
||||
return ['errors' => ['user' => 'Super admin aktif terakhir tidak dapat dinonaktifkan.', 'user_id' => $id], 'draft' => []];
|
||||
}
|
||||
db()->prepare('UPDATE admins SET is_active = ? WHERE id = ?')->execute([$active, $id]);
|
||||
audit('user.toggle', ($active ? 'Mengaktifkan' : 'Menonaktifkan') . ' admin ' . $row['username']);
|
||||
flash('ok', 'Admin ' . $row['username'] . ($active ? ' diaktifkan.' : ' dinonaktifkan.'));
|
||||
redirect(base_url('admin/?tab=pengguna'));
|
||||
}
|
||||
|
||||
function action_user_password(array $me): ?array
|
||||
{
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($denied = user_admin_guard($me, 'user', $id)) {
|
||||
return $denied;
|
||||
}
|
||||
$p = input_str($_POST['password'] ?? '');
|
||||
$row = admin_target($id);
|
||||
$err = $row ? validate_password($p, input_str($_POST['password_confirm'] ?? ''), (string)$row['username']) : 'Admin tidak ditemukan.';
|
||||
if ($err) {
|
||||
return ['errors' => ['user' => $err, 'user_id' => $id], 'draft' => []];
|
||||
}
|
||||
// Temporary password: the owner must replace it at the next login (the new hash also ends their sessions).
|
||||
db()->prepare('UPDATE admins SET password_hash = ?, must_change_password = 1 WHERE id = ?')->execute([password_hash($p, PASSWORD_DEFAULT), $id]);
|
||||
audit('user.password', 'Me-reset password admin ' . $row['username']);
|
||||
flash('ok', 'Password ' . $row['username'] . ' di-reset. Pemilik akun wajib menggantinya saat login berikutnya.');
|
||||
redirect(base_url('admin/?tab=pengguna'));
|
||||
}
|
||||
|
||||
function action_user_role(array $me): ?array
|
||||
{
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($denied = user_admin_guard($me, 'user', $id)) {
|
||||
return $denied;
|
||||
}
|
||||
$row = admin_target($id);
|
||||
if (!$row || $id === $me['id']) {
|
||||
return ['errors' => ['user' => $row ? 'Anda tidak dapat mengubah peran akun sendiri.' : 'Admin tidak ditemukan.', 'user_id' => $id], 'draft' => []];
|
||||
}
|
||||
$role = $row['role'] === 'super_admin' ? 'admin' : 'super_admin';
|
||||
if ($role === 'admin' && (int)$row['is_active'] && active_super_admins() <= 1) {
|
||||
return ['errors' => ['user' => 'Harus ada minimal satu super admin aktif.', 'user_id' => $id], 'draft' => []];
|
||||
}
|
||||
db()->prepare('UPDATE admins SET role = ? WHERE id = ?')->execute([$role, $id]);
|
||||
$label = $role === 'super_admin' ? 'super admin' : 'admin';
|
||||
audit('user.role', 'Mengubah peran ' . $row['username'] . ' menjadi ' . $label);
|
||||
flash('ok', 'Peran ' . $row['username'] . ' diubah menjadi ' . $label . '.');
|
||||
redirect(base_url('admin/?tab=pengguna'));
|
||||
}
|
||||
|
||||
function action_user_reset_otp(array $me): ?array
|
||||
{
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($denied = user_admin_guard($me, 'user', $id)) {
|
||||
return $denied;
|
||||
}
|
||||
$row = admin_target($id);
|
||||
if (!$row || $id === $me['id']) {
|
||||
return ['errors' => ['user' => $row ? 'OTP akun sendiri tidak dapat di-reset dari sini.' : 'Admin tidak ditemukan.', 'user_id' => $id], 'draft' => []];
|
||||
}
|
||||
// Lost phone: the owner sets up a new authenticator at the next login.
|
||||
db()->prepare('UPDATE admins SET otp_secret = NULL, otp_enabled = 0, otp_last_step = NULL, recovery_codes = NULL WHERE id = ?')->execute([$id]);
|
||||
audit('user.otp', 'Me-reset OTP admin ' . $row['username']);
|
||||
flash('ok', 'OTP ' . $row['username'] . ' di-reset. Authenticator baru dipasang saat login berikutnya.');
|
||||
redirect(base_url('admin/?tab=pengguna'));
|
||||
}
|
||||
|
||||
function action_own_password(array $me): ?array
|
||||
{
|
||||
$st = db()->prepare('SELECT password_hash FROM admins WHERE id = ?');
|
||||
$st->execute([$me['id']]);
|
||||
$hash = (string)$st->fetchColumn();
|
||||
$err = null;
|
||||
if (!password_verify(input_str($_POST['current'] ?? ''), $hash)) {
|
||||
$err = 'Password saat ini salah.';
|
||||
} else {
|
||||
$err = validate_password(input_str($_POST['password'] ?? ''), input_str($_POST['password_confirm'] ?? ''), $me['username']);
|
||||
}
|
||||
if ($err) {
|
||||
return ['errors' => ['own' => $err], 'draft' => []];
|
||||
}
|
||||
$newHash = password_hash(input_str($_POST['password']), PASSWORD_DEFAULT);
|
||||
db()->prepare('UPDATE admins SET password_hash = ?, must_change_password = 0 WHERE id = ?')->execute([$newHash, $me['id']]);
|
||||
// keep this session, end every other session of this account
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['pwv'] = password_fingerprint($newHash);
|
||||
audit('user.password', 'Mengganti password sendiri');
|
||||
flash('ok', 'Password Anda diganti.');
|
||||
redirect(base_url('admin/?tab=pengguna'));
|
||||
}
|
||||
Reference in new issue
Block a user