FauziIsyrinApridal commited on
Commit
43be92e
·
1 Parent(s): 7ca96c1
Files changed (2) hide show
  1. middleware.ts +20 -5
  2. utils/forgotPassword.ts +52 -4
middleware.ts CHANGED
@@ -23,22 +23,36 @@ export async function middleware(request: NextRequest) {
23
  },
24
  set(name: string, value: string, options: CookieOptions) {
25
  request.cookies.set({ name, value, ...options });
26
- response = NextResponse.next({ request: { headers: request.headers } });
 
 
27
  response.cookies.set({ name, value, ...options });
28
  },
29
  remove(name: string, options: CookieOptions) {
30
  request.cookies.set({ name, value: "", ...options });
31
- response = NextResponse.next({ request: { headers: request.headers } });
 
 
32
  response.cookies.set({ name, value: "", ...options });
33
  },
34
  },
35
- }
36
  );
37
 
38
  const { data } = await supabase.auth.getUser();
39
- const role = data?.user?.user_metadata?.role;
 
40
 
41
- if (!data?.user || role !== "admin") {
 
 
 
 
 
 
 
 
 
42
  const url = request.nextUrl.clone();
43
  url.pathname = "/login";
44
  url.searchParams.set("error", "not_admin");
@@ -53,3 +67,4 @@ export const config = {
53
  "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
54
  ],
55
  };
 
 
23
  },
24
  set(name: string, value: string, options: CookieOptions) {
25
  request.cookies.set({ name, value, ...options });
26
+ response = NextResponse.next({
27
+ request: { headers: request.headers },
28
+ });
29
  response.cookies.set({ name, value, ...options });
30
  },
31
  remove(name: string, options: CookieOptions) {
32
  request.cookies.set({ name, value: "", ...options });
33
+ response = NextResponse.next({
34
+ request: { headers: request.headers },
35
+ });
36
  response.cookies.set({ name, value: "", ...options });
37
  },
38
  },
39
+ },
40
  );
41
 
42
  const { data } = await supabase.auth.getUser();
43
+ const user = data?.user;
44
+ const role = user?.user_metadata?.role;
45
 
46
+ // If not authenticated, send to login without error flag
47
+ if (!user) {
48
+ const url = request.nextUrl.clone();
49
+ url.pathname = "/login";
50
+ url.searchParams.delete("error");
51
+ return NextResponse.redirect(url);
52
+ }
53
+
54
+ // If authenticated but not admin, show not_admin
55
+ if (role !== "admin") {
56
  const url = request.nextUrl.clone();
57
  url.pathname = "/login";
58
  url.searchParams.set("error", "not_admin");
 
67
  "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
68
  ],
69
  };
70
+
utils/forgotPassword.ts CHANGED
@@ -1,7 +1,10 @@
1
  "use server";
2
  import { createClient } from "@/utils/supabase/server";
 
3
 
4
- export type ForgotResult = { ok: true; message: string } | { ok: false; message: string };
 
 
5
 
6
  export async function sendResetEmail(email: string): Promise<ForgotResult> {
7
  const supabase = createClient();
@@ -10,9 +13,51 @@ export async function sendResetEmail(email: string): Promise<ForgotResult> {
10
  return { ok: false, message: "Email tidak valid" };
11
  }
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  // Determine redirect URL for the password reset flow
14
- const siteUrl = process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ||
15
- (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000");
 
 
 
16
  const redirectTo = `${siteUrl}/reset-password`;
17
 
18
  const { error } = await supabase.auth.resetPasswordForEmail(email, {
@@ -23,5 +68,8 @@ export async function sendResetEmail(email: string): Promise<ForgotResult> {
23
  return { ok: false, message: "Gagal mengirim email reset. Coba lagi." };
24
  }
25
 
26
- return { ok: true, message: "Email reset telah dikirim. Periksa kotak masuk Anda." };
 
 
 
27
  }
 
1
  "use server";
2
  import { createClient } from "@/utils/supabase/server";
3
+ import { createClient as createSupabaseClient } from "@supabase/supabase-js";
4
 
5
+ export type ForgotResult =
6
+ | { ok: true; message: string }
7
+ | { ok: false; message: string };
8
 
9
  export async function sendResetEmail(email: string): Promise<ForgotResult> {
10
  const supabase = createClient();
 
13
  return { ok: false, message: "Email tidak valid" };
14
  }
15
 
16
+ // Admin-only: verify the email belongs to an admin user before sending reset link
17
+ try {
18
+ const adminClient = createSupabaseClient(
19
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
20
+ process.env.SUPABASE_SERVICE_KEY!,
21
+ );
22
+ // list users and find by email (no direct getUserByEmail in v2)
23
+ let page = 1;
24
+ const perPage = 200;
25
+ let foundRole: unknown = null;
26
+ while (true) {
27
+ const { data, error } = await adminClient.auth.admin.listUsers({ page, perPage });
28
+ if (error) throw error;
29
+ const users = data?.users || [];
30
+ const match = users.find((u) => u.email?.toLowerCase() === email.toLowerCase());
31
+ if (match) {
32
+ foundRole = match.user_metadata?.role;
33
+ break;
34
+ }
35
+ if (!data || users.length < perPage) break; // no more pages
36
+ page += 1;
37
+ // safety cap to avoid excessive loops
38
+ if (page > 50) break;
39
+ }
40
+ if (foundRole !== "admin") {
41
+ // Do not reveal whether the email exists; return a generic message
42
+ return {
43
+ ok: false,
44
+ message: "Akun tidak diizinkan melakukan reset di aplikasi admin.",
45
+ };
46
+ }
47
+ } catch {
48
+ // Fail closed: if role check fails, do not proceed
49
+ return {
50
+ ok: false,
51
+ message: "Gagal memverifikasi akun. Coba lagi.",
52
+ };
53
+ }
54
+
55
  // Determine redirect URL for the password reset flow
56
+ const siteUrl =
57
+ process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ||
58
+ (process.env.VERCEL_URL
59
+ ? `https://${process.env.VERCEL_URL}`
60
+ : "http://localhost:3000");
61
  const redirectTo = `${siteUrl}/reset-password`;
62
 
63
  const { error } = await supabase.auth.resetPasswordForEmail(email, {
 
68
  return { ok: false, message: "Gagal mengirim email reset. Coba lagi." };
69
  }
70
 
71
+ return {
72
+ ok: true,
73
+ message: "Email reset telah dikirim. Periksa kotak masuk Anda.",
74
+ };
75
  }