Supabase with NextJS 8편 - 패스워드 변경
안녕하세요?
Supabase 강좌가 벌써 8편이네요.
7편 링크를 남겨드릴 테니 지난 시간까지 뭐가 있었는지 참고 바랍니다.
https://cpro95.tistory.com/623
이번 시간에는 Supabase에 로그인 한 유저의 패스워드를 변경하는 방법에 대해 알아보겠습니다.
위 그림과 같이 Profile 페이지에서 패스워드 변경이 가능하게끔 만들 생각입니다.
위 그림과 같이 같은 페이지에서 위의 "Change Password" 버튼을 누르면 아래와 같이 패스워드 변경 UI가 나오도록 할 예정입니다.
UI 부분을 들여다보기 전에 먼저 supabase에서는 어떻게 패스워드를 변경할까요?
supabase에서 제공하는 API 도움말을 좀 볼까요?
위 그림과 같이 supabase에서는 auth 부분에서 update 함수를 제공해 줍니다.
그리고 설명에서 보듯이 update 안에 들어갈 객체에서 email, password, data는 옵셔널이라고 합니다.
즉, 필요한 것만 넣으면 된다는 얘기입니다.
그래서 우리는 password 항목만 넣을 예정인데요.
제가 만든 웹앱에서는 이메일은 유니크한 부분이라 변경하지 않는 걸 원칙으로 했기 때문입니다.
그럼 AuthProvider 부분을 고쳐서 updatePassword 함수를 auth Hook으로 만들어 볼까요?
/lib/auth/AuthContext.tsx 파일을 열어서 다음과 같이 코드를 추가합시다.
/lib/auth/AuthContext.tsx
...
...
...
import {
SupabaseAuthPayload,
SupabaseChangePasswordPayload,
} from "./auth.types";
export type AuthContextProps = {
user: User;
signUp: (payload: SupabaseAuthPayload) => void;
signIn: (payload: SupabaseAuthPayload) => void;
signInWithGithub: (evt: SyntheticEvent) => void;
signOut: () => void;
updatePassword: (payload: SupabaseChangePasswordPayload) => void;
loading: boolean;
loggedIn: boolean;
userLoading: boolean;
};
...
...
...
// change password with provided details
const updatePassword = async (payload: SupabaseChangePasswordPayload) => {
try {
setLoading(true);
const { error, user } = await supabase.auth.update(payload);
if (error) {
console.log(error);
handleMessage({ message: error.message, type: "error" });
} else {
handleMessage({
message: "Change Password successful.",
type: "success",
});
handleMessage({ message: `Welcome, ${user.email}`, type: "success" });
}
} catch (error) {
console.log(error);
handleMessage({
message: error.error_description || error,
type: "error",
});
} finally {
setLoading(false);
}
};
...
...
...
return (
<AuthContext.Provider
value={{
...
...
signOut,
updatePassword,
...
...
}}
>
{children}
</AuthContext.Provider>
);
전체적인 코드는 기존 강좌를 보시고 위와 같이 추가된 함수를 적당한 자리에 넣어주시면 됩니다.
그리고 auth.types인데요.
/lib/auth/auth.types.ts
export type SupabaseAuthPayload = {
email: string;
password: string;
};
export type SupabaseChangePasswordPayload = {
password: string;
}
changePassword 관련 type을 추가했습니다.
이제 Auth 훅을 추가했으니까 UI 부분에서 useAuth 훅을 이용해서 updatePassword 함수를 실행해 봅시다.
/pages/profile.tsx
import React from "react";
import { useState } from "react";
import { GetServerSideProps } from "next";
import classNames from "classnames";
import { useAuth } from "../lib/auth";
import { supabase } from "../lib/supabase";
import { User } from "@supabase/supabase-js";
import { useFormFields } from "../lib/utils";
import { useMessage } from "../lib/message";
type FormFieldProps = {
password: string;
password2: string;
};
const FORM_VALUES: FormFieldProps = {
password: "",
password2: "",
};
const ProfilePage = ({ user }) => {
const { signOut, updatePassword } = useAuth();
const [changePassword, setChangePassword] = useState<boolean>(false);
const [values, handleChange, resetFormFields] =
useFormFields<FormFieldProps>(FORM_VALUES);
const { messages, handleMessage } = useMessage();
// Form submit handler to call the above function
const handleSumbit = (event: React.FormEvent) => {
event.preventDefault();
if (values.password !== values.password2) {
resetFormFields();
handleMessage({
message: `Typed passwords are not identical`,
type: "error",
});
} else {
updatePassword({ password: values.password });
resetFormFields();
}
};
function alterPassword() {
setChangePassword(!changePassword);
}
return (
<div className="flex flex-col items-center justify-start py-36 min-h-screen">
<h2 className="text-4xl my-4">
Hello, {user && user.email ? user.email : "Supabase User!"}
</h2>
<h3 className="text-xl my-4">You are in Profile Page.</h3>
{user && (
<div className="flex flex-row items-center justify-center space-x-4 mb-4">
<button
className="border bg-gray-500 border-gray-600 text-white px-3 py-2 rounded text-center transition duration-150 shadow-lg"
onClick={signOut}
>
Log Out
</button>
<div>or</div>
<button
className="border bg-cyan-500 border-cyan-600 text-white px-3 py-2 rounded text-center transition duration-150 shadow-lg"
onClick={alterPassword}
>
Change Password
</button>
</div>
)}
{messages &&
messages.map((message, index) => (
<div
className={classNames(
"shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center",
message.type === "error"
? "bg-red-500 text-white"
: message.type === "success"
? "bg-green-300 text-gray-800"
: "bg-gray-100 text-gray-800"
)}
>
{message.message}
</div>
))}
{changePassword && (
<form
onSubmit={handleSumbit}
className="bg-white shadow-md rounded w-1/2 mt-4 px-8 pt-6 pb-8 mb-4"
>
<div className="mb-6">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="password"
>
New Password
</label>
<input
className="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
name="password"
type="password"
placeholder="New password"
required
value={values.password}
onChange={handleChange}
/>
</div>
<div className="mb-6">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="password"
>
Repeat New Password
</label>
<input
className="shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password2"
name="password2"
type="password"
placeholder="Repeat password"
required
value={values.password2}
onChange={handleChange}
/>
</div>
<div className="flex gap-2">
<button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit"
>
Update
</button>
</div>
</form>
)}
</div>
);
};
export default ProfilePage;
export type NextAppPageUserProps = {
props: {
user: User;
loggedIn: boolean;
};
};
export type NextAppPageRedirProps = {
redirect: {
destination: string;
permanent: boolean;
};
};
export type NextAppPageServerSideProps =
| NextAppPageUserProps
| NextAppPageRedirProps;
export const getServerSideProps: GetServerSideProps = async ({
req,
}): Promise<NextAppPageServerSideProps> => {
const { user } = await supabase.auth.api.getUserByCookie(req);
if (!user) {
return {
redirect: {
destination: "/auth",
permanent: false,
},
};
}
return {
props: {
user,
loggedIn: !!user,
},
};
};
참고로 Profile 페이지는 NextJS의 getServerSideProps를 이용해서 Profile 페이지는 로그인된 유저만 접근 가능토록 서버사이드단에서 처리했습니다.
그리고 기존 강좌에서 설명했던 Form Validation 방식을 그대로 차용했습니다.
기존 강좌를 보셨으면 상기 코드는 어렵지 않게 이해할 수 있을 겁니다.
참고로 useAuth 훅에서 updatePassword 함수를 사용할 때는 password 한 개만 전달해야 합니다.
handleSubmit 함수에서 입력된 두 개의 패스워드가 일치하지 않으면 에러코드를 나타내고요.
두 개가 일치하면 아래 처럼 password 한개만 들어 있는 객체를 전달해서 updatePassword함수를 실행시킵니다.
updatePassword({ password: values.password });
그리고 에러 처리 관련해서도 아래 그림을 보시면,
패스워드 두개가 일치하지 않으면 위와 같이 에러 코드도 나옵니다.
참고로 패스워드 변경에서 기존 패스워드를 확인하는 부분이 없는데요.
supabase는 그 방식을 지원하지 않습니다.
대신 이메일 링크를 통해서 패스워드 리커버리 하는 방식을 취하고 있죠.
supabase API를 보시면 위와 같이 resetPasswordForEmail 함수를 이용하면 됩니다.
이 부분은 여러분께서 직접 코드에 넣어보시기 바랍니다.
참고로 패스워드 변경은 Github 같은 Third-Party 로그인 방식으로 로그인했어도 패스워드를 새롭게 바꿀 수 있습니다.
당연히 바뀐 로그인은 supabase 앱 전용이 됩니다.
여기서 패스워드를 바꿨다고 github 패스워드가 바뀌는 건 아닙니다.
참고로 github repository 링크입니다.
https://github.com/cpro95/supa-auth