NextAuth đăng nhập với graphql, getClient và wodpress thư mục app sử dụng typescript (ok)
Last updated
Was this helpful?
Last updated
Was this helpful?
.env
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=jg3I65KxxWvmLWwbI8Zp9DbJsgyVJ+vHRAARmhF68+A=
WORDPRESS_GRAPHQL_ENDPOINT=http://wp1.com/graphql
types\next-auth.d.ts
// types/next-auth.d.ts
import NextAuth, { DefaultSession, DefaultUser } from "next-auth";
declare module "next-auth" {
interface Session {
authToken?: string;
refreshToken?: string;
user: {
id: string;
name?: string | null;
email?: string | null;
} & DefaultSession["user"];
}
interface User extends DefaultUser {
authToken?: string;
refreshToken?: string;
}
}
declare module "next-auth/jwt" {
interface JWT {
authToken?: string;
refreshToken?: string;
}
}
lib\apollo-client.ts
// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support";
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: process.env.WORDPRESS_GRAPHQL_ENDPOINT,
}),
});
});
app\layout.tsx
// app/layout.tsx
import AuthProvider from "./AuthProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}
app\AuthProvider.tsx
// app/AuthProvider.tsx
"use client";
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
interface AuthProviderProps {
children: ReactNode;
}
export default function AuthProvider({ children }: AuthProviderProps) {
return <SessionProvider>{children}</SessionProvider>;
}
app\login\page.tsx
// app/login/page.tsx
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await signIn("credentials", {
username,
password,
redirect: false,
});
if (res?.ok) {
router.push("/dashboard");
} else {
console.error("Login failed:", res?.error);
}
};
return (
<div>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
</div>
);
}
app\dashboard\page.tsx
// app/dashboard/page.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "../api/auth/[...nextauth]/route";
export default async function Dashboard() {
const session = await getServerSession(authOptions);
if (!session) {
return <p>Please log in.</p>;
}
return (
<div>
<h1>Welcome, {session.user.name}!</h1>
<p>Email: {session.user.email}</p>
<p>Auth Token: {session.authToken}</p>
</div>
);
}
app\api\auth[...nextauth]\route.ts
// app/api/auth/[...nextauth]/route.ts
import NextAuth, { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { getClient } from "@/lib/apollo-client";
import { gql } from "@apollo/client";
const LOGIN_MUTATION = gql`
mutation Login($username: String!, $password: String!) {
login(input: { username: $username, password: $password }) {
authToken
refreshToken
user {
id
name
email
}
}
}
`;
export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.username || !credentials?.password) {
return null;
}
const client = getClient();
try {
const { data } = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: {
id: string;
name: string;
email: string;
};
};
}>({
mutation: LOGIN_MUTATION,
variables: {
username: credentials.username,
password: credentials.password,
},
});
if (data?.login?.authToken) {
return {
id: data.login.user.id,
name: data.login.user.name,
email: data.login.user.email,
authToken: data.login.authToken,
refreshToken: data.login.refreshToken,
};
}
return null;
} catch (error) {
console.error("Login error:", error);
return null;
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.authToken = user.authToken;
token.refreshToken = user.refreshToken;
}
return token;
},
async session({ session, token }) {
session.authToken = token.authToken;
session.refreshToken = token.refreshToken;
return session;
},
},
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
app\api\auth[...nextauth]\route.ts
// app/api/auth/[...nextauth]/route.ts
import NextAuth, { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { getClient } from "@/lib/apollo-client";
import { gql } from "@apollo/client";
const LOGIN_MUTATION = gql`
mutation Login($username: String!, $password: String!) {
login(input: { username: $username, password: $password }) {
authToken
refreshToken
user {
id
name
email
}
}
}
`;
const REGISTER_MUTATION = gql`
mutation Register($username: String!, $email: String!, $password: String!) {
registerUser(input: { username: $username, email: $email, password: $password }) {
user {
id
name
email
}
}
}
`;
export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text" },
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
isRegister: { label: "Is Register", type: "hidden" }, // Trường ẩn để phân biệt đăng ký/đăng nhập
},
async authorize(credentials) {
if (!credentials?.username || !credentials?.password) {
return null;
}
const client = getClient();
const isRegister = credentials.isRegister === "true";
try {
if (isRegister) {
// Đăng ký người dùng
if (!credentials.email) {
throw new Error("Email is required for registration");
}
const { data } = await client.mutate<{
registerUser: {
user: {
id: string;
name: string;
email: string;
};
};
}>({
mutation: REGISTER_MUTATION,
variables: {
username: credentials.username,
email: credentials.email,
password: credentials.password,
},
});
if (data?.registerUser?.user) {
// Sau khi đăng ký thành công, tự động đăng nhập
const loginResponse = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: {
id: string;
name: string;
email: string;
};
};
}>({
mutation: LOGIN_MUTATION,
variables: {
username: credentials.username,
password: credentials.password,
},
});
if (loginResponse.data?.login?.authToken) {
return {
id: loginResponse.data.login.user.id,
name: loginResponse.data.login.user.name,
email: loginResponse.data.login.user.email,
authToken: loginResponse.data.login.authToken,
refreshToken: loginResponse.data.login.refreshToken,
};
}
}
} else {
// Đăng nhập thông thường
const { data } = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: {
id: string;
name: string;
email: string;
};
};
}>({
mutation: LOGIN_MUTATION,
variables: {
username: credentials.username,
password: credentials.password,
},
});
if (data?.login?.authToken) {
return {
id: data.login.user.id,
name: data.login.user.name,
email: data.login.user.email,
authToken: data.login.authToken,
refreshToken: data.login.refreshToken,
};
}
}
return null;
} catch (error) {
console.error(isRegister ? "Register error:" : "Login error:", error);
return null;
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.authToken = user.authToken;
token.refreshToken = user.refreshToken;
}
return token;
},
async session({ session, token }) {
session.authToken = token.authToken;
session.refreshToken = token.refreshToken;
return session;
},
},
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
app\login\page.tsx
// app/login/page.tsx
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [username, setUsername] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [isRegister, setIsRegister] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const res = await signIn("credentials", {
username,
email: isRegister ? email : undefined, // Chỉ gửi email nếu là đăng ký
password,
isRegister: isRegister.toString(),
redirect: false,
});
if (res?.ok) {
router.push("/dashboard");
} else {
setError(res?.error || "Something went wrong");
}
};
return (
<div>
<h1>{isRegister ? "Register" : "Login"}</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
{isRegister && (
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
)}
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">{isRegister ? "Register" : "Login"}</button>
</form>
<button onClick={() => setIsRegister(!isRegister)}>
{isRegister ? "Switch to Login" : "Switch to Register"}
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}
app\login\page.tsx
// app/login/page.tsx
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [identifier, setIdentifier] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [isRegister, setIsRegister] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const res = await signIn("credentials", {
identifier,
email: isRegister ? email : undefined,
password,
isRegister: isRegister.toString(),
redirect: false,
});
if (res?.ok) {
router.push("/dashboard");
} else {
setError(res?.error || "Something went wrong");
}
};
const handleGoogleSignIn = async () => {
setError(null);
const res = await signIn("google", { redirect: false });
if (res?.ok) {
router.push("/dashboard");
} else {
setError(res?.error || "Google sign-in failed");
}
};
return (
<div>
<h1>{isRegister ? "Register" : "Login"}</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Username or Email"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
required
/>
{isRegister && (
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
)}
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">{isRegister ? "Register" : "Login"}</button>
</form>
<button onClick={() => setIsRegister(!isRegister)}>
{isRegister ? "Switch to Login" : "Switch to Register"}
</button>
<button onClick={handleGoogleSignIn}>Login with Google</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}
app\api\auth[...nextauth]\route.ts
// app/api/auth/[...nextauth]/route.ts
import NextAuth, { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import { getClient } from "@/lib/apollo-client";
import { gql } from "@apollo/client";
const LOGIN_MUTATION = gql`
mutation Login($identifier: String!, $password: String!) {
login(input: { username: $identifier, password: $password }) {
authToken
refreshToken
user {
id
name
email
}
}
}
`;
const REGISTER_MUTATION = gql`
mutation Register($username: String!, $email: String!, $password: String!) {
registerUser(input: { username: $username, email: $email, password: $password }) {
user {
id
name
email
}
}
}
`;
const CHECK_USER_QUERY = gql`
query CheckUser($email: String!) {
users(where: { search: $email }) {
nodes {
id
name
email
}
}
}
`;
export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
identifier: { label: "Username or Email", type: "text" }, // Thay username bằng identifier
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
isRegister: { label: "Is Register", type: "hidden" },
},
async authorize(credentials) {
if (!credentials?.identifier || !credentials?.password) {
return null;
}
const client = getClient();
const isRegister = credentials.isRegister === "true";
try {
if (isRegister) {
if (!credentials.email) {
throw new Error("Email is required for registration");
}
const { data } = await client.mutate<{
registerUser: {
user: { id: string; name: string; email: string };
};
}>({
mutation: REGISTER_MUTATION,
variables: {
username: credentials.identifier,
email: credentials.email,
password: credentials.password,
},
});
if (data?.registerUser?.user) {
const loginResponse = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: { id: string; name: string; email: string };
};
}>({
mutation: LOGIN_MUTATION,
variables: {
identifier: credentials.identifier,
password: credentials.password,
},
});
if (loginResponse.data?.login?.authToken) {
return {
id: loginResponse.data.login.user.id,
name: loginResponse.data.login.user.name,
email: loginResponse.data.login.user.email,
authToken: loginResponse.data.login.authToken,
refreshToken: loginResponse.data.login.refreshToken,
};
}
}
} else {
const { data, error }:any = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: { id: string; name: string; email: string };
};
}>({
mutation: LOGIN_MUTATION,
variables: {
identifier: credentials.identifier, // Hỗ trợ username hoặc email
password: credentials.password,
},
});
if (error) {
throw new Error("Invalid credentials");
}
if (data?.login?.authToken) {
return {
id: data.login.user.id,
name: data.login.user.name,
email: data.login.user.email,
authToken: data.login.authToken,
refreshToken: data.login.refreshToken,
};
}
}
return null;
} catch (error) {
console.error(isRegister ? "Register error:" : "Login error:", error);
throw new Error("Unknown username or email. Check again.");
}
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.authToken = user.authToken;
token.refreshToken = user.refreshToken;
token.id = user.id;
}
return token;
},
async session({ session, token }) {
session.authToken = token.authToken;
session.refreshToken = token.refreshToken;
session.user.id = token.id as string;
return session;
},
async signIn({ user, account, profile }) {
if (account?.provider === "google" && profile?.email) {
const client = getClient();
// Kiểm tra xem người dùng đã tồn tại chưa
const { data } = await client.query<{
users: { nodes: { id: string; name: string; email: string }[] };
}>({
query: CHECK_USER_QUERY,
variables: { email: profile.email },
});
if (data?.users?.nodes?.length > 0) {
// Người dùng đã tồn tại, không cần đăng nhập lại qua WordPress
user.id = data.users.nodes[0].id;
user.name = data.users.nodes[0].name;
user.email = data.users.nodes[0].email;
return true;
} else {
// Đăng ký người dùng mới
const randomPassword = Math.random().toString(36).slice(-8);
const username = profile.name || profile.email.split("@")[0];
const registerResponse = await client.mutate<{
registerUser: {
user: { id: string; name: string; email: string };
};
}>({
mutation: REGISTER_MUTATION,
variables: {
username,
email: profile.email,
password: randomPassword,
},
});
if (registerResponse.data?.registerUser?.user) {
const loginResponse = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: { id: string; name: string; email: string };
};
}>({
mutation: LOGIN_MUTATION,
variables: {
identifier: username,
password: randomPassword,
},
});
if (loginResponse.data?.login?.authToken) {
user.id = loginResponse.data.login.user.id;
user.authToken = loginResponse.data.login.authToken;
user.refreshToken = loginResponse.data.login.refreshToken;
return true;
}
}
}
}
return true;
},
},
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
app\reset-password\request\page.tsx
// app/reset-password/request/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { getClient } from "@/lib/apollo-client";
import { gql } from "@apollo/client";
const SEND_RESET_EMAIL_MUTATION = gql`
mutation SendPasswordResetEmail($email: String!) {
sendPasswordResetEmail(input: { username: $email }) {
success
message
}
}
`;
export default function ResetPasswordRequestPage() {
const [email, setEmail] = useState<string>("");
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const client = getClient();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
setError(null);
try {
const { data } = await client.mutate<{
sendPasswordResetEmail: { success: boolean; message?: string };
}>({
mutation: SEND_RESET_EMAIL_MUTATION,
variables: { email },
});
if (data?.sendPasswordResetEmail?.success) {
setMessage("A password reset link has been sent to your email.");
} else {
setError(data?.sendPasswordResetEmail?.message || "Something went wrong.");
}
} catch (err) {
setError("Failed to send reset email. Please try again.");
console.error(err);
}
};
return (
<div>
<h1>Reset Password</h1>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit">Send Reset Link</button>
</form>
{message && <p style={{ color: "green" }}>{message}</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
<button onClick={() => router.push("/login")}>Back to Login</button>
</div>
);
}
app\reset-password[key]\page.tsx
// app/reset-password/[key]/page.tsx
"use client";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useMutation, gql } from "@apollo/client";
import React from "react";
const RESET_PASSWORD_MUTATION = gql`
mutation ResetUserPassword($key: String!, $login: String!, $password: String!) {
resetUserPassword(input: { key: $key, login: $login, password: $password }) {
success
}
}
`;
export default function ResetPasswordPage({ params }: { params: Promise<{ key: string }> }) {
const [login, setLogin] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const searchParams = useSearchParams();
const [resetPassword] = useMutation(RESET_PASSWORD_MUTATION);
const unwrappedParams = React.use(params);
const key = unwrappedParams.key;
useEffect(() => {
const loginFromUrl = searchParams.get("login");
if (loginFromUrl) setLogin(loginFromUrl);
}, [searchParams]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
setError(null);
console.log("Reset Password Variables:", { key, login, password });
try {
const { data } = await resetPassword({
variables: { key, login, password },
});
if (data?.resetUserPassword?.success) {
setMessage("Password reset successfully. Redirecting to login...");
setTimeout(() => router.push("/login"), 2000);
} else {
setError("Failed to reset password. Invalid key or login.");
}
} catch (err: any) {
const errorMessage = err.graphQLErrors?.[0]?.message || "An error occurred. Please try again.";
setError(errorMessage);
console.error("Mutation Error:", err);
}
};
return (
<div>
<h1>Set New Password</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Username or Email"
value={login}
onChange={(e) => setLogin(e.target.value)}
required
/>
<input
type="password"
placeholder="New Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Reset Password</button>
</form>
{message && <p style={{ color: "green" }}>{message}</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
<button onClick={() => router.push("/login")}>Back to Login</button>
</div>
);
}
lib\apollo-client.ts
// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
// import { registerApolloClient } from "@apollo/experimental-nextjs-app-support";
// export const { getClient } = registerApolloClient(() => {
// return new ApolloClient({
// cache: new InMemoryCache(),
// link: new HttpLink({
// uri: process.env.WORDPRESS_GRAPHQL_ENDPOINT,
// }),
// });
// });
// Tạo Apollo Client thủ công
let client: ApolloClient<any> | null = null;
export function getClient() {
if (!client) {
if (!process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_ENDPOINT) {
throw new Error("NEXT_PUBLIC_WORDPRESS_GRAPHQL_ENDPOINT is not defined in .env.local");
}
client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_ENDPOINT,
}),
});
}
return client;
}
lib\apollo-provider.tsx
// lib/apollo-provider.tsx
"use client";
import { ApolloClient, InMemoryCache, HttpLink, ApolloProvider } from "@apollo/client";
import { ReactNode } from "react";
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_ENDPOINT || "https://wp1.com/graphql", // Đảm bảo endpoint đúng
}),
});
export default function ApolloWrapper({ children }: { children: ReactNode }) {
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}
app\layout.tsx
// app/layout.tsx
import AuthProvider from "./AuthProvider";
import ApolloWrapper from "@/lib/apollo-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>
<ApolloWrapper>{children}</ApolloWrapper>
</AuthProvider>
</body>
</html>
);
}
wp-content\themes\astra-child\functions.php
<?php
add_filter('big_image_size_threshold', '__return_false');
add_filter('intermediate_image_sizes', 'remove_default_img_sizes', 10, 1);
function remove_default_img_sizes($sizes) {
$targets =
[
'thumbnail',
'medium',
'medium_large',
'large',
'1536x1536',
'2048x2048',
'woocommerce_thumbnail',
'woocommerce_single',
'woocommerce_gallery_thumbnail',
];
foreach($sizes as $size_index => $size) {
if (in_array($size, $targets)) {
unset($sizes[$size_index]);
}
}
return $sizes;
}
if (!function_exists('astra_register_menu_locations')) {
/**
* Register menus
*
* @since 1.0.0
*/
function astra_register_menu_locations() {
/**
* Primary Menus
*/
register_nav_menus(
array(
'primary' => esc_html__('Primary Menu', 'astra'),
)
);
/**
* Primary Menus
*/
register_nav_menus(
array(
'header' => esc_html__('Header T', 'astra'),
)
);
if (true === Astra_Builder_Helper::$is_header_footer_builder_active) {
/**
* Register the Secondary & Mobile menus.
*/
register_nav_menus(
array(
'secondary_menu' => esc_html__('Secondary Menu', 'astra'),
'mobile_menu' => esc_html__('Off-Canvas Menu', 'astra'),
)
);
$component_limit = defined('ASTRA_EXT_VER') ? Astra_Builder_Helper :: $component_limit: Astra_Builder_Helper:: $num_of_header_menu;
for ($index = 3; $index <= $component_limit; $index++) {
if (!is_customize_preview() && !Astra_Builder_Helper:: is_component_loaded('menu-'.$index)) {
continue;
}
register_nav_menus(
array(
'menu_'.$index => esc_html__('Menu ', 'astra').$index,
)
);
}
/**
* Register the Account menus.
*/
register_nav_menus(
array(
'loggedin_account_menu' => esc_html__('Logged In Account Menu', 'astra'),
)
);
}
/**
* Footer Menus
*/
register_nav_menus(
array(
'footer_menu' => esc_html__('Footer Menu', 'astra'),
)
);
}
}
/**
* Register a book post type.
*/
function wpdocs_codex_custom_init() {
$labels = array(
'name' => _x('Books', 'Post type general name', 'textdomain'),
'singular_name' => _x('Book', 'Post type singular name', 'textdomain'),
'menu_name' => _x('Books', 'Admin Menu text', 'textdomain'),
'name_admin_bar' => _x('Book', 'Add New on Toolbar', 'textdomain'),
'add_new' => __('Add New', 'textdomain'),
'add_new_item' => __('Add New Book', 'textdomain'),
'new_item' => __('New Book', 'textdomain'),
'edit_item' => __('Edit Book', 'textdomain'),
'view_item' => __('View Book', 'textdomain'),
'all_items' => __('All Books', 'textdomain'),
'search_items' => __('Search Books', 'textdomain'),
'parent_item_colon' => __('Parent Books:', 'textdomain'),
'not_found' => __('No books found.', 'textdomain'),
'not_found_in_trash' => __('No books found in Trash.', 'textdomain'),
'featured_image' => _x('Book Cover Image', 'Overrides the “Featured Image” phrase for this post type. Added in 4.3', 'textdomain'),
'set_featured_image' => _x('Set cover image', 'Overrides the “Set featured image” phrase for this post type. Added in 4.3', 'textdomain'),
'remove_featured_image' => _x('Remove cover image', 'Overrides the “Remove featured image” phrase for this post type. Added in 4.3', 'textdomain'),
'use_featured_image' => _x('Use as cover image', 'Overrides the “Use as featured image” phrase for this post type. Added in 4.3', 'textdomain'),
'archives' => _x('Book archives', 'The post type archive label used in nav menus. Default “Post Archives”. Added in 4.4', 'textdomain'),
'insert_into_item' => _x('Insert into book', 'Overrides the “Insert into post”/”Insert into page” phrase (used when inserting media into a post). Added in 4.4', 'textdomain'),
'uploaded_to_this_item' => _x('Uploaded to this book', 'Overrides the “Uploaded to this post”/”Uploaded to this page” phrase (used when viewing media attached to a post). Added in 4.4', 'textdomain'),
'filter_items_list' => _x('Filter books list', 'Screen reader text for the filter links heading on the post type listing screen. Default “Filter posts list”/”Filter pages list”. Added in 4.4', 'textdomain'),
'items_list_navigation' => _x('Books list navigation', 'Screen reader text for the pagination heading on the post type listing screen. Default “Posts list navigation”/”Pages list navigation”. Added in 4.4', 'textdomain'),
'items_list' => _x('Books list', 'Screen reader text for the items list heading on the post type listing screen. Default “Posts list”/”Pages list”. Added in 4.4', 'textdomain'),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'capability_type' => 'post',
'rewrite' => array('slug' => 'book'),
'label' => __('Books', 'textdomain'),
'has_archive' => true,
'hierarchical' => false,
'menu_position' => null,
'show_in_graphql' => true,
'graphql_single_name' => 'book',
'graphql_plural_name' => 'books',
'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments')
);
register_post_type('books', $args);
}
add_action('init', 'wpdocs_codex_custom_init');
add_filter('register_post_type_args', function ($args, $post_type) {
// Change this to the post type you are adding support for
if ('book' === $post_type) {
$args['show_in_graphql'] = true;
$args['graphql_single_name'] = 'book';
$args['graphql_plural_name'] = 'book'; # Don't set, and it will default to `all${graphql_single_name}`, i.e. `allDocument`.
}
return $args;
}, 10, 2);
/**
* !child_enqueue_styles
*/
add_post_type_support('page', 'excerpt');
function child_enqueue_styles() {
wp_enqueue_style('dev-custom-style', get_stylesheet_directory_uri(). '/assets/css/dev-custom.css', array('astra-theme-css'), '0.1.0', 'all');
wp_enqueue_script('dev-custom-script', get_stylesheet_directory_uri(). '/assets/js/dev-custom.js', array('jquery-core'), '1.0.1', true);
// astra-theme-css
wp_localize_script(
'dev-custom-script',
'frontend_ajax_object',
array(
'ajaxurl' => admin_url('admin-ajax.php'),
'ajaxnonce' => wp_create_nonce('ajaxnonce'),
)
);
}
add_action('wp_enqueue_scripts', 'child_enqueue_styles', 15);
/**
* child_enqueue_styles!
*/
add_filter('jwt_auth_default_username', function ($username, $email) {
if (is_email($email)) {
$user = get_user_by('email', $email);
if ($user) {
return $user -> user_login;
}
}
return $username;
}, 10, 2);
add_filter('retrieve_password_message', function ($message, $key, $user_login, $user_data) {
$reset_url = "http://localhost:3000/reset-password/{$key}?login=" . urlencode($user_login);
$message = "Click here to reset your password: {$reset_url}\n\nIf this was not requested by you, please ignore this email.";
return $message;
}, 10, 4);
define('SMTP_HOST', 'smtp.gmail.com');
define('SMTP_PORT', '587');
define('SMTP_USER', 'phamngoctuong1805@gmail.com');
define('SMTP_PASS', 'wrojrhklvdzkfnbh');
add_action('graphql_register_types', function () {
register_graphql_field('SendPasswordResetEmailPayload', 'message', [
'type' => 'String',
'description' => 'Message about the reset email status',
'resolve' => function ($payload) {
return $payload['success'] ? 'Email sent successfully' : 'Failed to send email';
},
]);
register_graphql_field('ResetUserPasswordPayload', 'success', [
'type' => 'String',
'description' => 'success about the reset password status',
'resolve' => function ($payload) {
return $payload['success'] ? 'Password reset successfully' : 'Invalid key or login';
},
]);
register_graphql_field('ResetUserPasswordPayload', 'message', [
'type' => 'String',
'description' => 'Message about the reset password status',
'resolve' => function ($payload) {
return $payload['success'] ? 'Password reset successfully' : 'Invalid key or login';
},
]);
});
.env
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=jg3I65KxxWvmLWwbI8Zp9DbJsgyVJ+vHRAARmhF68+A=
GOOGLE_CLIENT_ID=670516138495-as3qgiq1i1j54dpggghuain01ggj3dqf.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
NEXT_PUBLIC_WORDPRESS_GRAPHQL_ENDPOINT=http://wp1.com/graphql
wp-content\themes\astra-child\functions.php
add_filter('jwt_auth_default_username', function ($username, $email) {
if (is_email($email)) {
$user = get_user_by('email', $email);
if ($user) {
return $user -> user_login;
}
}
return $username;
}, 10, 2);
Điều kiện tiên quyết
WordPress Backend:
Đã cài đặt plugin WPGraphQL và WPGraphQL JWT Authentication.
Định nghĩa JWT_AUTH_SECRET_KEY trong wp-config.php.
Next.js Project:
Dự án đã được tạo với Next.js 13+ App Router và TypeScript (npx create-next-app@latest --typescript).
Cài đặt các thư viện: next-auth, @apollo/client, @apollo/experimental-nextjs-app-support.
Cấu hình môi trường:
File .env.local:
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-nextauth-secret
WORDPRESS_GRAPHQL_ENDPOINT=https://your-wordpress-site.com/graphql
Các bước thực hiện1. Cài đặt các thư việnChạy lệnh sau để cài đặt:bash
npm install next-auth @apollo/client @apollo/experimental-nextjs-app-support
npm install --save-dev @types/node
2. Cấu hình Apollo Client với TypeScriptTạo file lib/apollo-client.ts:typescript
// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: process.env.WORDPRESS_GRAPHQL_ENDPOINT,
}),
});
});
getClient: Trả về một instance của Apollo Client đã được đăng ký, hoạt động tốt với Server Components.
3. Định nghĩa Types cho NextAuth và GraphQLTạo file types/next-auth.d.ts để mở rộng type của NextAuth:typescript
// types/next-auth.d.ts
import NextAuth, { DefaultSession, DefaultUser } from "next-auth";
declare module "next-auth" {
interface Session {
authToken?: string;
refreshToken?: string;
user: {
id: string;
name?: string | null;
email?: string | null;
} & DefaultSession["user"];
}
interface User extends DefaultUser {
authToken?: string;
refreshToken?: string;
}
}
declare module "next-auth/jwt" {
interface JWT {
authToken?: string;
refreshToken?: string;
}
}
4. Cấu hình NextAuth với TypeScriptTạo file app/api/auth/[...nextauth]/route.ts:typescript
// app/api/auth/[...nextauth]/route.ts
import NextAuth, { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { getClient } from "@/lib/apollo-client";
import { gql } from "@apollo/client";
const LOGIN_MUTATION = gql`
mutation Login($username: String!, $password: String!) {
login(input: { username: $username, password: $password }) {
authToken
refreshToken
user {
id
name
email
}
}
}
`;
export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.username || !credentials?.password) {
return null;
}
const client = getClient();
try {
const { data } = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: {
id: string;
name: string;
email: string;
};
};
}>({
mutation: LOGIN_MUTATION,
variables: {
username: credentials.username,
password: credentials.password,
},
});
if (data?.login?.authToken) {
return {
id: data.login.user.id,
name: data.login.user.name,
email: data.login.user.email,
authToken: data.login.authToken,
refreshToken: data.login.refreshToken,
};
}
return null;
} catch (error) {
console.error("Login error:", error);
return null;
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.authToken = user.authToken;
token.refreshToken = user.refreshToken;
}
return token;
},
async session({ session, token }) {
session.authToken = token.authToken;
session.refreshToken = token.refreshToken;
return session;
},
},
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Type cho GraphQL: Định nghĩa type cho response của LOGIN_MUTATION để TypeScript hiểu cấu trúc dữ liệu.
authorize: Trả về null nếu đăng nhập thất bại, hoặc một object user nếu thành công.
5. Tạo SessionProvider với TypeScriptTạo file app/AuthProvider.tsx:typescript
// app/AuthProvider.tsx
"use client";
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
interface AuthProviderProps {
children: ReactNode;
}
export default function AuthProvider({ children }: AuthProviderProps) {
return <SessionProvider>{children}</SessionProvider>;
}
ReactNode: TypeScript yêu cầu định nghĩa type cho children.
6. Cập nhật Root LayoutTạo file app/layout.tsx:typescript
// app/layout.tsx
import AuthProvider from "./AuthProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}
7. Tạo trang đăng nhập với TypeScriptTạo file app/login/page.tsx:typescript
// app/login/page.tsx
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await signIn("credentials", {
username,
password,
redirect: false,
});
if (res?.ok) {
router.push("/dashboard");
} else {
console.error("Login failed:", res?.error);
}
};
return (
<div>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
</div>
);
}
useState<string>: Định nghĩa type cho state variables.
React.FormEvent: TypeScript yêu cầu type cho event handler.
8. Tạo trang Dashboard với TypeScriptTạo file app/dashboard/page.tsx:typescript
// app/dashboard/page.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "../api/auth/[...nextauth]/route";
export default async function Dashboard() {
const session = await getServerSession(authOptions);
if (!session) {
return <p>Please log in.</p>;
}
return (
<div>
<h1>Welcome, {session.user.name}!</h1>
<p>Email: {session.user.email}</p>
<p>Auth Token: {session.authToken}</p>
</div>
);
}
getServerSession: Lấy session trong Server Component, type được suy ra từ authOptions.
Cấu trúc thư mụcDự án của bạn sẽ trông như sau:
your-nextjs-app/
├── app/
│ ├── api/
│ │ └── auth/[...nextauth]/
│ │ └── route.ts
│ ├── login/
│ │ └── page.tsx
│ ├── dashboard/
│ │ └── page.tsx
│ ├── AuthProvider.tsx
│ └── layout.tsx
├── lib/
│ └── apollo-client.ts
├── types/
│ └── next-auth.d.ts
├── .env.local
├── package.json
└── tsconfig.json
Cách chạy
Chạy npm run dev để khởi động ứng dụng.
Truy cập http://localhost:3000/login để đăng nhập.
Sau khi đăng nhập thành công, bạn sẽ được chuyển hướng tới /dashboard.
Lưu ý
Type GraphQL: Nếu bạn muốn type chặt chẽ hơn cho truy vấn GraphQL, hãy sử dụng codegen (như @graphql-codegen/cli) để tạo type từ schema của WPGraphQL.
Bảo mật: Đảm bảo endpoint GraphQL của WordPress chỉ cho phép yêu cầu từ domain của bạn.
Error Handling: Bạn có thể thêm state để hiển thị lỗi đăng nhập trên UI.
Nếu bạn cần thêm chi tiết hoặc gặp lỗi cụ thể, hãy cho tôi biết để tôi hỗ trợ thêm!
Lỗi "Unknown username. Check again or try your email address." thường xuất hiện khi bạn cố gắng đăng nhập bằng thông tin không khớp với dữ liệu trong hệ thống WordPress thông qua NextAuth hoặc Google Provider. Dựa trên code hiện tại của bạn, lỗi này có thể xảy ra trong các trường hợp sau:
Credentials Provider: Username hoặc password không đúng khi gửi tới WordPress qua LOGIN_MUTATION.
Google Provider: Khi người dùng đăng nhập bằng Google, hệ thống cố gắng đăng nhập lại vào WordPress với thông tin không hợp lệ (ví dụ: username hoặc password không khớp).
Tôi sẽ phân tích nguyên nhân và đưa ra cách khắc phục, tập trung vào việc sửa lỗi trong cả hai luồng (Credentials và Google).
Nguyên nhân có thể gây lỗi1. Credentials Provider
Trong authorize của CredentialsProvider, mutation LOGIN_MUTATION yêu cầu username và password. Nếu username không tồn tại trong WordPress hoặc không khớp với bất kỳ người dùng nào, WPGraphQL sẽ trả về lỗi.
2. Google Provider
Trong callback signIn, sau khi đăng nhập bằng Google:
Nếu người dùng đã tồn tại trong WordPress (dựa trên email), hệ thống cố gắng gọi LOGIN_MUTATION với username lấy từ profile.name và một password giả ("google-auth-placeholder"). Điều này không khả thi vì WordPress yêu cầu mật khẩu thực tế của người dùng, dẫn đến lỗi "Unknown username" hoặc "Invalid credentials".
Nếu người dùng chưa tồn tại, hệ thống đăng ký thành công nhưng có thể gặp lỗi khi đăng nhập lại nếu username không được tạo đúng.
3. WordPress Backend
WordPress/WPGraphQL không cho phép đăng nhập bằng email thay vì username, hoặc cấu hình JWT Authentication không hỗ trợ đăng nhập linh hoạt.
Cách khắc phụcĐể giải quyết lỗi này, chúng ta cần:
Cho phép đăng nhập bằng email trong Credentials Provider.
Sửa Google Provider: Không dựa vào LOGIN_MUTATION cho người dùng đã tồn tại, thay vào đó sử dụng thông tin từ Google trực tiếp hoặc tạo cơ chế đăng nhập đặc biệt cho Google.
Cải thiện error handling để thông báo lỗi rõ ràng hơn.
Dưới đây là cách sửa code.1. Cập nhật app/api/auth/[...nextauth]/route.tsSửa lại để hỗ trợ đăng nhập bằng email và xử lý Google Provider tốt hơn:typescript
// app/api/auth/[...nextauth]/route.ts
import NextAuth, { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import { getClient } from "@/lib/apollo-client";
import { gql } from "@apollo/client";
const LOGIN_MUTATION = gql`
mutation Login($identifier: String!, $password: String!) {
login(input: { username: $identifier, password: $password }) {
authToken
refreshToken
user {
id
name
email
}
}
}
`;
const REGISTER_MUTATION = gql`
mutation Register($username: String!, $email: String!, $password: String!) {
registerUser(input: { username: $username, email: $email, password: $password }) {
user {
id
name
email
}
}
}
`;
const CHECK_USER_QUERY = gql`
query CheckUser($email: String!) {
users(where: { search: $email }) {
nodes {
id
name
email
}
}
}
`;
export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
identifier: { label: "Username or Email", type: "text" }, // Thay username bằng identifier
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
isRegister: { label: "Is Register", type: "hidden" },
},
async authorize(credentials) {
if (!credentials?.identifier || !credentials?.password) {
return null;
}
const client = getClient();
const isRegister = credentials.isRegister === "true";
try {
if (isRegister) {
if (!credentials.email) {
throw new Error("Email is required for registration");
}
const { data } = await client.mutate<{
registerUser: {
user: { id: string; name: string; email: string };
};
}>({
mutation: REGISTER_MUTATION,
variables: {
username: credentials.identifier,
email: credentials.email,
password: credentials.password,
},
});
if (data?.registerUser?.user) {
const loginResponse = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: { id: string; name: string; email: string };
};
}>({
mutation: LOGIN_MUTATION,
variables: {
identifier: credentials.identifier,
password: credentials.password,
},
});
if (loginResponse.data?.login?.authToken) {
return {
id: loginResponse.data.login.user.id,
name: loginResponse.data.login.user.name,
email: loginResponse.data.login.user.email,
authToken: loginResponse.data.login.authToken,
refreshToken: loginResponse.data.login.refreshToken,
};
}
}
} else {
const { data, error } = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: { id: string; name: string; email: string };
};
}>({
mutation: LOGIN_MUTATION,
variables: {
identifier: credentials.identifier, // Hỗ trợ username hoặc email
password: credentials.password,
},
});
if (error) {
throw new Error("Invalid credentials");
}
if (data?.login?.authToken) {
return {
id: data.login.user.id,
name: data.login.user.name,
email: data.login.user.email,
authToken: data.login.authToken,
refreshToken: data.login.refreshToken,
};
}
}
return null;
} catch (error) {
console.error(isRegister ? "Register error:" : "Login error:", error);
throw new Error("Unknown username or email. Check again.");
}
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.authToken = user.authToken;
token.refreshToken = user.refreshToken;
token.id = user.id;
}
return token;
},
async session({ session, token }) {
session.authToken = token.authToken;
session.refreshToken = token.refreshToken;
session.user.id = token.id as string;
return session;
},
async signIn({ user, account, profile }) {
if (account?.provider === "google" && profile?.email) {
const client = getClient();
// Kiểm tra xem người dùng đã tồn tại chưa
const { data } = await client.query<{
users: { nodes: { id: string; name: string; email: string }[] };
}>({
query: CHECK_USER_QUERY,
variables: { email: profile.email },
});
if (data?.users?.nodes?.length > 0) {
// Người dùng đã tồn tại, không cần đăng nhập lại qua WordPress
user.id = data.users.nodes[0].id;
user.name = data.users.nodes[0].name;
user.email = data.users.nodes[0].email;
return true;
} else {
// Đăng ký người dùng mới
const randomPassword = Math.random().toString(36).slice(-8);
const username = profile.name || profile.email.split("@")[0];
const registerResponse = await client.mutate<{
registerUser: {
user: { id: string; name: string; email: string };
};
}>({
mutation: REGISTER_MUTATION,
variables: {
username,
email: profile.email,
password: randomPassword,
},
});
if (registerResponse.data?.registerUser?.user) {
const loginResponse = await client.mutate<{
login: {
authToken: string;
refreshToken: string;
user: { id: string; name: string; email: string };
};
}>({
mutation: LOGIN_MUTATION,
variables: {
identifier: username,
password: randomPassword,
},
});
if (loginResponse.data?.login?.authToken) {
user.id = loginResponse.data.login.user.id;
user.authToken = loginResponse.data.login.authToken;
user.refreshToken = loginResponse.data.login.refreshToken;
return true;
}
}
}
}
return true;
},
},
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Thay đổi chính:
Credentials Provider:
Đổi username thành identifier để hỗ trợ cả username và email. Tuy nhiên, lưu ý rằng WPGraphQL JWT Authentication mặc định chỉ dùng username trong login. Nếu muốn dùng email, bạn cần sửa mutation hoặc backend WordPress (xem phần dưới).
Ném lỗi rõ ràng hơn khi đăng nhập thất bại.
Google Provider:
Nếu người dùng đã tồn tại, không gọi LOGIN_MUTATION nữa (vì không có mật khẩu thực tế). Thay vào đó, dùng thông tin từ WordPress trực tiếp.
Nếu đăng ký mới, vẫn tạo người dùng và đăng nhập như trước.
2. Cập nhật app/login/page.tsxSửa form để phản ánh việc dùng identifier thay vì username:typescript
// app/login/page.tsx
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [identifier, setIdentifier] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [isRegister, setIsRegister] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const res = await signIn("credentials", {
identifier,
email: isRegister ? email : undefined,
password,
isRegister: isRegister.toString(),
redirect: false,
});
if (res?.ok) {
router.push("/dashboard");
} else {
setError(res?.error || "Something went wrong");
}
};
const handleGoogleSignIn = async () => {
setError(null);
const res = await signIn("google", { redirect: false });
if (res?.ok) {
router.push("/dashboard");
} else {
setError(res?.error || "Google sign-in failed");
}
};
return (
<div>
<h1>{isRegister ? "Register" : "Login"}</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Username or Email"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
required
/>
{isRegister && (
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
)}
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">{isRegister ? "Register" : "Login"}</button>
</form>
<button onClick={() => setIsRegister(!isRegister)}>
{isRegister ? "Switch to Login" : "Switch to Register"}
</button>
<button onClick={handleGoogleSignIn}>Login with Google</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}
3. Sửa WordPress Backend (nếu cần)Nếu bạn muốn hỗ trợ đăng nhập bằng email thay vì chỉ username:
WPGraphQL JWT Authentication: Mặc định, login mutation chỉ chấp nhận username. Để dùng email, bạn cần tùy chỉnh plugin hoặc thêm logic trong WordPress:
Cài plugin như "JWT Authentication for WP REST API" hoặc sửa WPGraphQL JWT Authentication.
Thêm filter trong functions.php của theme WordPress để cho phép email:php
add_filter('jwt_auth_default_username', function ($username, $email) {
if (is_email($email)) {
$user = get_user_by('email', $email);
if ($user) {
return $user->user_login;
}
}
return $username;
}, 10, 2);
Cập nhật LOGIN_MUTATION nếu cần:graphql
mutation Login($identifier: String!, $password: String!) {
login(input: { username: $identifier, password: $password }) {
authToken
refreshToken
user {
id
name
email
}
}
}
Kiểm tra và chạy
Đăng nhập bằng Credentials:
Thử với username hoặc email đã đăng ký trong WordPress cùng mật khẩu đúng.
Đăng nhập bằng Google:
Nhấn "Login with Google". Nếu email đã tồn tại, hệ thống sẽ dùng thông tin từ WordPress. Nếu chưa, sẽ đăng ký mới.
Kiểm tra lỗi: Xem console và UI để đảm bảo lỗi được hiển thị rõ ràng.
Lưu ý
Google Provider: Hiện tại, nếu người dùng đã tồn tại trong WordPress, hệ thống không yêu cầu mật khẩu (bỏ qua LOGIN_MUTATION). Nếu bạn cần token từ WordPress, hãy cấu hình backend để hỗ trợ đăng nhập không mật khẩu cho Google users (ví dụ: dùng custom token).
Debug: Nếu lỗi vẫn xảy ra, kiểm tra response từ WordPress trong console hoặc thêm console.log vào authorize và signIn callback.
Nếu bạn vẫn gặp lỗi hoặc cần thêm chi tiết, hãy gửi thông tin cụ thể (log, cấu hình WordPress), tôi sẽ hỗ trợ thêm!