FauziIsyrinApridal commited on
Commit
241884b
·
1 Parent(s): b7a53a7
app/(auth)/forgot-password/page.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ForgotPasswordForm from "@/components/ForgotPasswordForm";
2
+ import Image from "next/image";
3
+ import PnpLogo from "../../../public/logo-pnp.webp";
4
+ import Link from "next/link";
5
+
6
+ export default function ForgotPasswordPage() {
7
+ return (
8
+ <div className="flex min-h-screen flex-col items-center justify-center">
9
+ <div className="w-10/12 rounded-md border p-5 text-center shadow-md sm:w-8/12 md:w-6/12 lg:w-4/12 xl:w-3/12 2xl:w-3/12">
10
+ <div className="flex justify-center py-5">
11
+ <Image src={PnpLogo} width={120} height={120} alt="Logo Politeknik Negeri Padang" />
12
+ </div>
13
+ <h1 className="pb-3 text-xl font-semibold">Lupa Password</h1>
14
+ <p className="mb-5 text-sm">Masukkan email Anda untuk menerima tautan reset password.</p>
15
+ <ForgotPasswordForm />
16
+ <div className="mt-4 text-xs">
17
+ <Link href="/login" className="text-blue-600 hover:underline">
18
+ Kembali ke Login
19
+ </Link>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ );
24
+ }
app/(auth)/reset-password/page.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import Image from "next/image";
4
+ import PnpLogo from "../../../public/logo-pnp.webp";
5
+ import ResetPasswordForm from "@/components/ResetPasswordForm";
6
+ import { createClient } from "@/utils/supabase/client";
7
+
8
+ export default function ResetPasswordPage() {
9
+ const supabase = createClient();
10
+ const [ready, setReady] = useState(false);
11
+ const [errorMsg, setErrorMsg] = useState<string | null>(null);
12
+
13
+ useEffect(() => {
14
+ // When redirected from the email link, Supabase puts tokens in the hash.
15
+ // Exchange for a session so we can call updateUser.
16
+ const run = async () => {
17
+ try {
18
+ if (typeof window !== "undefined" && window.location.hash) {
19
+ const { error } = await supabase.auth.exchangeCodeForSession(window.location.hash);
20
+ if (error) {
21
+ setErrorMsg("Tautan tidak valid atau sudah kedaluwarsa.");
22
+ }
23
+ }
24
+ } catch (e) {
25
+ setErrorMsg("Gagal memproses tautan reset.");
26
+ } finally {
27
+ setReady(true);
28
+ }
29
+ };
30
+ run();
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ }, []);
33
+
34
+ return (
35
+ <div className="flex min-h-screen flex-col items-center justify-center">
36
+ <div className="w-10/12 rounded-md border p-5 text-center shadow-md sm:w-8/12 md:w-6/12 lg:w-4/12 xl:w-3/12 2xl:w-3/12">
37
+ <div className="flex justify-center py-5">
38
+ <Image src={PnpLogo} width={120} height={120} alt="Logo Politeknik Negeri Padang" />
39
+ </div>
40
+ <h1 className="pb-3 text-xl font-semibold">Reset Password</h1>
41
+ {!ready ? (
42
+ <p className="text-sm">Menyiapkan formulir...</p>
43
+ ) : errorMsg ? (
44
+ <p className="text-sm text-red-600">{errorMsg}</p>
45
+ ) : (
46
+ <>
47
+ <p className="mb-5 text-sm">Masukkan password baru Anda.</p>
48
+ <ResetPasswordForm />
49
+ </>
50
+ )}
51
+ </div>
52
+ </div>
53
+ );
54
+ }
components/ForgotPasswordForm.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { zodResolver } from "@hookform/resolvers/zod";
3
+ import { useForm, SubmitHandler } from "react-hook-form";
4
+ import { z } from "zod";
5
+ import { Input } from "./ui/input";
6
+ import { Button } from "./ui/button";
7
+ import { Label } from "./ui/label";
8
+ import { sendResetEmail } from "@/utils/forgotPassword";
9
+ import { useState } from "react";
10
+
11
+ const schema = z.object({
12
+ email: z.string().email("Email tidak valid"),
13
+ });
14
+
15
+ type FormFields = z.infer<typeof schema>;
16
+
17
+ export default function ForgotPasswordForm() {
18
+ const [successMsg, setSuccessMsg] = useState<string | null>(null);
19
+ const {
20
+ register,
21
+ handleSubmit,
22
+ setError,
23
+ formState: { errors, isSubmitting },
24
+ } = useForm<FormFields>({ resolver: zodResolver(schema) });
25
+
26
+ const onSubmit: SubmitHandler<FormFields> = async ({ email }) => {
27
+ setSuccessMsg(null);
28
+ const res = await sendResetEmail(email);
29
+ if (!res.ok) {
30
+ setError("root", { message: res.message });
31
+ return;
32
+ }
33
+ setSuccessMsg(res.message);
34
+ };
35
+
36
+ return (
37
+ <form className="flex flex-col gap-2" onSubmit={handleSubmit(onSubmit)}>
38
+ <Label htmlFor="email" className="text-left">
39
+ Email
40
+ </Label>
41
+ <Input id="email" type="email" placeholder="Email" {...register("email")} />
42
+ {errors.email && (
43
+ <div className="text-left text-xs text-red-500">{errors.email.message}</div>
44
+ )}
45
+ <Button disabled={isSubmitting} type="submit" className="mt-4 bg-orange-600 hover:bg-orange-800">
46
+ {isSubmitting ? "Mengirim..." : "Kirim tautan reset"}
47
+ </Button>
48
+ {errors.root && (
49
+ <div className="text-xs text-red-500">{errors.root.message}</div>
50
+ )}
51
+ {successMsg && (
52
+ <div className="text-xs text-green-600">{successMsg}</div>
53
+ )}
54
+ </form>
55
+ );
56
+ }
components/ResetPasswordForm.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { zodResolver } from "@hookform/resolvers/zod";
3
+ import { useForm, SubmitHandler } from "react-hook-form";
4
+ import { z } from "zod";
5
+ import { Input } from "./ui/input";
6
+ import { Button } from "./ui/button";
7
+ import { Label } from "./ui/label";
8
+ import { useState } from "react";
9
+ import { createClient } from "@/utils/supabase/client";
10
+
11
+ const schema = z
12
+ .object({
13
+ password: z.string().min(8, "Password minimal 8 karakter"),
14
+ confirm: z.string().min(8, "Konfirmasi minimal 8 karakter"),
15
+ })
16
+ .refine((data) => data.password === data.confirm, {
17
+ message: "Password dan konfirmasi tidak sama",
18
+ path: ["confirm"],
19
+ });
20
+
21
+ type FormFields = z.infer<typeof schema>;
22
+
23
+ export default function ResetPasswordForm() {
24
+ const supabase = createClient();
25
+ const [successMsg, setSuccessMsg] = useState<string | null>(null);
26
+ const {
27
+ register,
28
+ handleSubmit,
29
+ setError,
30
+ formState: { errors, isSubmitting },
31
+ } = useForm<FormFields>({ resolver: zodResolver(schema) });
32
+
33
+ const onSubmit: SubmitHandler<FormFields> = async ({ password }) => {
34
+ setSuccessMsg(null);
35
+ const { error } = await supabase.auth.updateUser({ password });
36
+ if (error) {
37
+ setError("root", { message: "Gagal mengubah password. Coba lagi." });
38
+ return;
39
+ }
40
+ setSuccessMsg("Password berhasil diubah. Silakan login kembali.");
41
+ };
42
+
43
+ return (
44
+ <form className="flex flex-col gap-2" onSubmit={handleSubmit(onSubmit)}>
45
+ <Label htmlFor="password" className="text-left">
46
+ Password Baru
47
+ </Label>
48
+ <Input id="password" type="password" placeholder="Password baru" {...register("password")} />
49
+ {errors.password && (
50
+ <div className="text-left text-xs text-red-500">{errors.password.message}</div>
51
+ )}
52
+
53
+ <Label htmlFor="confirm" className="text-left">
54
+ Konfirmasi Password
55
+ </Label>
56
+ <Input id="confirm" type="password" placeholder="Konfirmasi password" {...register("confirm")} />
57
+ {errors.confirm && (
58
+ <div className="text-left text-xs text-red-500">{errors.confirm.message}</div>
59
+ )}
60
+
61
+ <Button disabled={isSubmitting} type="submit" className="mt-4 bg-orange-600 hover:bg-orange-800">
62
+ {isSubmitting ? "Menyimpan..." : "Simpan Password"}
63
+ </Button>
64
+ {errors.root && (
65
+ <div className="text-xs text-red-500">{errors.root.message}</div>
66
+ )}
67
+ {successMsg && (
68
+ <div className="text-xs text-green-600">{successMsg}</div>
69
+ )}
70
+ </form>
71
+ );
72
+ }
components/SignInForm.tsx CHANGED
@@ -8,6 +8,7 @@ import { login } from "@/utils/signIn";
8
  import { Label } from "./ui/label";
9
  import { useState } from "react";
10
  import { Eye, EyeOff, Loader2 } from "lucide-react";
 
11
 
12
  const schema = z.object({
13
  email: z.string().email(),
@@ -31,11 +32,13 @@ const SignInForm = () => {
31
 
32
  const onSubmit: SubmitHandler<FormFields> = async (data) => {
33
  try {
34
- await login(data.email, data.password);
 
 
 
 
35
  } catch (error) {
36
- setError("root", {
37
- message: "Invalid email or password",
38
- });
39
  }
40
  };
41
 
@@ -99,6 +102,14 @@ const SignInForm = () => {
99
  {errors.root && (
100
  <div className="text-xs text-red-500">{errors.root.message}</div>
101
  )}
 
 
 
 
 
 
 
 
102
  </form>
103
  </>
104
  );
 
8
  import { Label } from "./ui/label";
9
  import { useState } from "react";
10
  import { Eye, EyeOff, Loader2 } from "lucide-react";
11
+ import Link from "next/link";
12
 
13
  const schema = z.object({
14
  email: z.string().email(),
 
32
 
33
  const onSubmit: SubmitHandler<FormFields> = async (data) => {
34
  try {
35
+ const res = await login(data.email, data.password);
36
+ if (res && "ok" in res && !res.ok) {
37
+ setError("root", { message: res.message || "Gagal login" });
38
+ }
39
+ // On success, server action will redirect to "/".
40
  } catch (error) {
41
+ setError("root", { message: "Terjadi kesalahan. Coba lagi." });
 
 
42
  }
43
  };
44
 
 
102
  {errors.root && (
103
  <div className="text-xs text-red-500">{errors.root.message}</div>
104
  )}
105
+ <div className="mt-3 text-center text-xs">
106
+ <Link
107
+ href="/forgot-password"
108
+ className="text-blue-600 hover:underline"
109
+ >
110
+ Lupa password?
111
+ </Link>
112
+ </div>
113
  </form>
114
  </>
115
  );
utils/forgotPassword.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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();
8
+
9
+ if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
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, {
19
+ redirectTo,
20
+ });
21
+
22
+ if (error) {
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
+ }
utils/resetPassword.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+ // Server action kept minimal; reset is better done on client using the recovery session token.
3
+ // Provided for completeness if needed by API routes in future.
4
+ import { createClient } from "@/utils/supabase/server";
5
+
6
+ export type ResetResult = { ok: true; message: string } | { ok: false; message: string };
7
+
8
+ export async function resetPasswordServer(password: string): Promise<ResetResult> {
9
+ if (!password || password.length < 8) {
10
+ return { ok: false, message: "Password minimal 8 karakter" };
11
+ }
12
+ const supabase = createClient();
13
+ const { error } = await supabase.auth.updateUser({ password });
14
+ if (error) {
15
+ return { ok: false, message: "Gagal memperbarui password." };
16
+ }
17
+ return { ok: true, message: "Password berhasil diubah." };
18
+ }
utils/signIn.ts CHANGED
@@ -4,21 +4,32 @@ import { redirect } from "next/navigation";
4
 
5
  import { createClient } from "@/utils/supabase/server";
6
 
7
- export async function login(email: string, password: string) {
 
 
8
  const supabase = createClient();
9
 
10
- //TODO validating input
11
- const data = {
12
- email: email,
13
- password: password,
14
- };
 
 
15
 
16
- const { error } = await supabase.auth.signInWithPassword(data);
17
 
18
  if (error) {
19
- return;
 
 
 
 
 
 
20
  }
21
 
 
22
  revalidatePath("/", "layout");
23
  redirect("/");
24
  }
 
4
 
5
  import { createClient } from "@/utils/supabase/server";
6
 
7
+ type LoginResult = { ok: true } | { ok: false; message: string };
8
+
9
+ export async function login(email: string, password: string): Promise<LoginResult> {
10
  const supabase = createClient();
11
 
12
+ // Basic validation (server-side safeguard)
13
+ if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
14
+ return { ok: false, message: "Email tidak valid" };
15
+ }
16
+ if (!password || password.length < 8) {
17
+ return { ok: false, message: "Password minimal 8 karakter" };
18
+ }
19
 
20
+ const { error } = await supabase.auth.signInWithPassword({ email, password });
21
 
22
  if (error) {
23
+ // Surface concise message
24
+ const msg =
25
+ error.message?.toLowerCase().includes("invalid") ||
26
+ error.status === 400
27
+ ? "Email atau password salah"
28
+ : "Gagal login. Coba lagi.";
29
+ return { ok: false, message: msg };
30
  }
31
 
32
+ // Success: revalidate and redirect
33
  revalidatePath("/", "layout");
34
  redirect("/");
35
  }