admin roadmap filter & search, user management, reopen rejected, system health
This commit is contained in:
@@ -10,3 +10,8 @@ export function isAdminEmail(email: string | null | undefined): boolean {
|
||||
if (!email) return false;
|
||||
return adminEmails().includes(email.toLowerCase());
|
||||
}
|
||||
|
||||
/** Daftar email admin yang aman di-expose ke UI admin (dropdown filter). */
|
||||
export function listAdminEmails(): string[] {
|
||||
return adminEmails();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { userRepo } from "@/server/repositories/user.repo";
|
||||
|
||||
/**
|
||||
* Pastikan user session aktif (tidak suspended) sebelum lanjut aksi mutatif.
|
||||
* Dipanggil di awal server action yang menulis ke DB (joinTrip, createTrip,
|
||||
* createReview, dll). Lookup fresh dari DB — jangan trust JWT karena suspend
|
||||
* bisa di-trigger sejak token dibuat.
|
||||
*
|
||||
* Throw Error dengan pesan user-friendly kalau suspended.
|
||||
*/
|
||||
export async function requireActiveUser(userId: string): Promise<void> {
|
||||
const suspended = await userRepo.isSuspended(userId);
|
||||
if (suspended) {
|
||||
throw new Error(
|
||||
"Akun kamu sedang ditangguhkan. Hubungi support@setrip.id untuk klarifikasi."
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
@@ -63,6 +63,22 @@ export const authOptions: AuthOptions = {
|
||||
strategy: "jwt",
|
||||
},
|
||||
callbacks: {
|
||||
async signIn({ user }) {
|
||||
// Block suspended user dari sign-in (Credentials + OAuth).
|
||||
// Email-based lookup karena `user.id` belum tentu ada untuk first-time
|
||||
// OAuth sign-in sebelum adapter persist.
|
||||
const email = user.email;
|
||||
if (!email) return true;
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
select: { suspended: true },
|
||||
});
|
||||
if (existing?.suspended) {
|
||||
// NextAuth menerjemahkan return false jadi error "AccessDenied".
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async jwt({ token, user, trigger }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
/**
|
||||
* Wrapper untuk cron route handler — otomatis log start/finish/error ke
|
||||
* `CronRun`. Idempotent terhadap kegagalan: kalau row gagal dibuat (DB down),
|
||||
* fn tetap jalan dan kegagalannya hanya hilang log.
|
||||
*
|
||||
* Pemakaian:
|
||||
* ```ts
|
||||
* return runCron("auto-complete-trips", async () => {
|
||||
* const result = await tripService.autoCompletePastTrips();
|
||||
* return { completed: result.count, ids: result.ids };
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Caller bertanggung jawab untuk mengembalikan NextResponse — `runCron`
|
||||
* cuma menjalankan fn dan log; return value fn dipassthrough sebagai `payload`.
|
||||
*/
|
||||
export async function runCron<T>(
|
||||
jobName: string,
|
||||
fn: () => Promise<T>
|
||||
): Promise<{ ok: true; payload: T } | { ok: false; error: string }> {
|
||||
let runId: string | null = null;
|
||||
try {
|
||||
const row = await prisma.cronRun.create({
|
||||
data: { jobName, status: "RUNNING" },
|
||||
select: { id: true },
|
||||
});
|
||||
runId = row.id;
|
||||
} catch (err) {
|
||||
console.error(`[cron-runner] gagal create row ${jobName}`, err);
|
||||
// Lanjut tanpa log — jangan blok cron karena DB log gagal.
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await fn();
|
||||
if (runId) {
|
||||
await prisma.cronRun
|
||||
.update({
|
||||
where: { id: runId },
|
||||
data: {
|
||||
status: "SUCCESS",
|
||||
finishedAt: new Date(),
|
||||
payload: payload as unknown as object,
|
||||
},
|
||||
})
|
||||
.catch((e) => console.error(`[cron-runner] gagal update SUCCESS`, e));
|
||||
}
|
||||
return { ok: true, payload };
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Unknown cron failure";
|
||||
if (runId) {
|
||||
await prisma.cronRun
|
||||
.update({
|
||||
where: { id: runId },
|
||||
data: {
|
||||
status: "FAILED",
|
||||
finishedAt: new Date(),
|
||||
errorMessage: message,
|
||||
},
|
||||
})
|
||||
.catch((e) => console.error(`[cron-runner] gagal update FAILED`, e));
|
||||
}
|
||||
return { ok: false, error: message };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user