admin roadmap filter & search, user management, reopen rejected, system health
This commit is contained in:
@@ -78,6 +78,38 @@ export const organizerService = {
|
||||
return v?.status === "APPROVED";
|
||||
},
|
||||
|
||||
/**
|
||||
* Buka kembali verifikasi yang REJECTED ke PENDING. Dipakai admin saat
|
||||
* organizer kirim foto/data baru via email/WA dan ingin di-review ulang
|
||||
* tanpa drop & recreate row.
|
||||
*
|
||||
* - `rejectionReason` lama disimpan di field — kalau di-reject lagi nanti,
|
||||
* field-nya di-overwrite (audit trail via auditLog Phase 4 di roadmap).
|
||||
* - Reset `reviewedById`, `reviewedAt` supaya muncul lagi di tab PENDING.
|
||||
*/
|
||||
async reopenVerification(input: {
|
||||
verificationId: string;
|
||||
adminId: string;
|
||||
note: string;
|
||||
}) {
|
||||
const verification = await organizerRepo.findById(input.verificationId);
|
||||
if (!verification) {
|
||||
throw new Error("Pengajuan tidak ditemukan");
|
||||
}
|
||||
if (verification.status !== "REJECTED") {
|
||||
throw new Error("Hanya pengajuan REJECTED yang bisa dibuka kembali");
|
||||
}
|
||||
const trimmedNote = input.note.trim();
|
||||
if (trimmedNote.length < 10) {
|
||||
throw new Error("Catatan reopen wajib min 10 karakter untuk audit");
|
||||
}
|
||||
if (trimmedNote.length > 500) {
|
||||
throw new Error("Catatan reopen maksimal 500 karakter");
|
||||
}
|
||||
|
||||
return organizerRepo.reopen(input.verificationId, trimmedNote);
|
||||
},
|
||||
|
||||
/** Reveal NIK plaintext. Caller must enforce authorization (owner or admin). */
|
||||
decryptNik(nikEncrypted: string): string {
|
||||
return decryptString(nikEncrypted);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { userRepo } from "@/server/repositories/user.repo";
|
||||
|
||||
export const userService = {
|
||||
/**
|
||||
* Suspend user oleh admin. Idempotent — kalau sudah suspended, tolak supaya
|
||||
* admin tahu (cegah race condition multiple admin suspend sekaligus).
|
||||
* Wajib reason untuk audit (min 10 char).
|
||||
*/
|
||||
async suspendUser(input: {
|
||||
userId: string;
|
||||
adminId: string;
|
||||
reason: string;
|
||||
}) {
|
||||
if (input.userId === input.adminId) {
|
||||
throw new Error("Tidak bisa suspend akun sendiri");
|
||||
}
|
||||
const trimmedReason = input.reason.trim();
|
||||
if (trimmedReason.length < 10) {
|
||||
throw new Error("Alasan suspend wajib min 10 karakter untuk audit");
|
||||
}
|
||||
if (trimmedReason.length > 500) {
|
||||
throw new Error("Alasan suspend maksimal 500 karakter");
|
||||
}
|
||||
|
||||
const target = await userRepo.findById(input.userId);
|
||||
if (!target) {
|
||||
throw new Error("User tidak ditemukan");
|
||||
}
|
||||
if (target.suspended) {
|
||||
throw new Error("User sudah dalam status suspended");
|
||||
}
|
||||
|
||||
return userRepo.setSuspension(input.userId, {
|
||||
suspended: true,
|
||||
suspendedById: input.adminId,
|
||||
suspendedReason: trimmedReason,
|
||||
});
|
||||
},
|
||||
|
||||
async unsuspendUser(input: { userId: string; adminId: string }) {
|
||||
if (input.userId === input.adminId) {
|
||||
throw new Error("Tidak bisa modifikasi akun sendiri");
|
||||
}
|
||||
const target = await userRepo.findById(input.userId);
|
||||
if (!target) {
|
||||
throw new Error("User tidak ditemukan");
|
||||
}
|
||||
if (!target.suspended) {
|
||||
throw new Error("User tidak dalam status suspended");
|
||||
}
|
||||
|
||||
return userRepo.setSuspension(input.userId, { suspended: false });
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user