Skip to content

Commit

Permalink
google login done
Browse files Browse the repository at this point in the history
  • Loading branch information
fachrihawari committed Oct 2, 2024
1 parent ab0fa37 commit 784a398
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 23 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ NEXT_PUBLIC_URL=http://localhost:3000
JWT_SECRET=secret
MIDTRANS_SERVER_KEY=server_key
MIDTRANS_CLIENT_KEY=client_key
NEXT_PUBLIC_MIDTRANS_CLIENT_KEY=client_key
NEXT_PUBLIC_MIDTRANS_CLIENT_KEY=client_key
NEXT_PUBLIC_GOOGLE_CLIENT_ID=google-client-id
GOOGLE_CLIENT_SECRET=google-secret-id
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ declare global {
MIDTRANS_SERVER_KEY: string;
MIDTRANS_CLIENT_KEY: string;
NEXT_PUBLIC_MIDTRANS_CLIENT_KEY: string;
NEXT_PUBLIC_GOOGLE_CLIENT_ID: string
GOOGLE_CLIENT_SECRET: string
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
},
"dependencies": {
"@faker-js/faker": "^9.0.0",
"@react-oauth/google": "^0.12.1",
"bcryptjs": "^2.4.3",
"google-auth-library": "^9.14.1",
"jose": "^5.9.2",
"midtrans-client": "^1.3.1",
"mongodb": "^6.8.1",
Expand Down
13 changes: 13 additions & 0 deletions src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { GoogleOAuthProvider } from "@react-oauth/google";

export default function AuthLayout({ children }: Readonly<Props>) {
return (
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}>
{children}
</GoogleOAuthProvider>
);
}

type Props = {
children: React.ReactNode
}
15 changes: 4 additions & 11 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import TokoPakEdiLogo from '@/components/TokoPakEdiLogo';
import { ResolvingMetadata } from 'next';
import Link from 'next/link';
import { FaEnvelope, FaLock, FaFacebook } from 'react-icons/fa';
import { FcGoogle } from 'react-icons/fc';
import { FaEnvelope, FaLock } from 'react-icons/fa';
import { login } from '@/lib/actions/users';
import GoogleLogin from '@/components/GoogleLogin';

export const generateMetadata = async (_: {}, parentPromise: ResolvingMetadata) => {
const parent = await parentPromise;
Expand Down Expand Up @@ -64,15 +64,8 @@ export default function LoginPage() {
<span className="mx-4 text-sm text-gray-500">or login with</span>
<div className="flex-grow border-t border-gray-300"></div>
</div>
<div className="mt-6 flex space-x-4">
<button className="flex-1 flex items-center justify-center py-2 px-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition duration-300">
<FcGoogle className="mr-2" size={20} />
<span className="text-sm font-medium">Google</span>
</button>
<button className="flex-1 flex items-center justify-center py-2 px-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition duration-300">
<FaFacebook className="mr-2 text-blue-600" size={20} />
<span className="text-sm font-medium">Facebook</span>
</button>
<div className="mt-6">
<GoogleLogin />
</div>
</div>
<div className="mt-8 text-center">
Expand Down
15 changes: 4 additions & 11 deletions src/app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import GoogleLogin from '@/components/GoogleLogin';
import TokoPakEdiLogo from '@/components/TokoPakEdiLogo';
import { register } from '@/lib/actions/users';
import { ResolvingMetadata } from 'next';
import Link from 'next/link';
import { FaEnvelope, FaLock, FaUser, FaFacebook } from 'react-icons/fa';
import { FcGoogle } from 'react-icons/fc';
import { FaEnvelope, FaLock, FaUser } from 'react-icons/fa';

export const generateMetadata = async (_: {}, parentPromise: ResolvingMetadata) => {
const parent = await parentPromise;
Expand Down Expand Up @@ -81,15 +81,8 @@ export default function RegisterPage() {
<span className="mx-4 text-sm text-gray-500">or register with</span>
<div className="flex-grow border-t border-gray-300"></div>
</div>
<div className="mt-6 flex space-x-4">
<button className="flex-1 flex items-center justify-center py-2 px-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition duration-300">
<FcGoogle className="mr-2" size={20} />
<span className="text-sm font-medium">Google</span>
</button>
<button className="flex-1 flex items-center justify-center py-2 px-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition duration-300">
<FaFacebook className="mr-2 text-blue-600" size={20} />
<span className="text-sm font-medium">Facebook</span>
</button>
<div className="mt-6">
<GoogleLogin />
</div>
</div>
<div className="mt-8 text-center">
Expand Down
73 changes: 73 additions & 0 deletions src/app/api/auth/google-login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from "next/server";
import { formatErrors } from "@/lib/utils/validator";
import { z } from "zod";
import { auth, OAuth2Client } from 'google-auth-library'
import { usersCollection } from "@/lib/db/user_collection";
import { cartsCollection } from "@/lib/db/cart_collection";
import { signToken } from "@/lib/utils/jwt";

const GoogleLoginSchema = z.object({
authCode: z.string({ message: "Google Token is required" })
})

export async function POST(request: NextRequest) {
try {
const body = await request.json();

const { success, data, error } = GoogleLoginSchema.safeParse(body);

if (!success) {
return NextResponse.json({ errors: formatErrors(error) }, { status: 400 });
}

const client = new OAuth2Client(
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
'postmessage'
);

const { tokens } = await client.getToken(data.authCode)

if (!tokens.id_token) {
return NextResponse.json({ errors: "Invalid google token" }, { status: 400 });
}

const ticket = await client.verifyIdToken({ idToken: tokens.id_token });
const payload = ticket.getPayload();

if (!payload) {
return NextResponse.json({ errors: "Invalid google token" }, { status: 400 });
}

let user = await usersCollection.findOne({ email: payload.email });
if (!user) {
const newUser = {
name: payload.name as string,
email: payload.email as string,
password: "", // just empty password, because using google login
googleId: payload.sub as string,
createdAt: new Date(),
}
const result = await usersCollection.insertOne(newUser);

await cartsCollection.insertOne({
userId: result.insertedId,
items: [],
});

user = {
_id: result.insertedId,
...newUser
}
}

// Generate JWT token
const token = await signToken({ sub: user._id.toString() });

return NextResponse.json({ token }, { status: 200 });
} catch (error) {
console.log(error);

return NextResponse.json({ errors: ["An unexpected error occurred"] }, { status: 500 });
}
}
23 changes: 23 additions & 0 deletions src/components/GoogleLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'
import { googleLogin } from "@/lib/actions/users";
import { GoogleOAuthProvider, useGoogleLogin } from "@react-oauth/google";
import { FcGoogle } from "react-icons/fc";

export default function GoogleLogin() {
const login = useGoogleLogin({
flow: 'auth-code',
onSuccess: tokenResponse => {
console.log(JSON.stringify(tokenResponse, null, 2))
googleLogin(tokenResponse.code)
},
});

return (
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}>
<button onClick={() => login()} className="w-full flex items-center justify-center py-3 px-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition duration-300">
<FcGoogle className="mr-2" size={20} />
<span className="text-sm font-medium">Google</span>
</button>
</GoogleOAuthProvider>
)
}
30 changes: 30 additions & 0 deletions src/lib/actions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,36 @@ export const login = async (formData: FormData) => {
redirect('/?' + successQuery);
};

export const googleLogin = async (authCode: string) => {
const res = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/auth/google-login`, {
method: 'POST',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ authCode }),
});

const data = await res.json();

if (!res.ok) {
const errorQuery = setQueryParams({ type: 'error', title: "Login with Google Failed", message: "Please try again" });
redirect('/login?' + errorQuery);
}

// Set the token in a cookie
cookies().set('token', data.token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 1 week
path: '/',
});

const successQuery = setQueryParams({ type: 'success', title: "Login Successful", message: "You have successfully logged in." });
redirect('/?' + successQuery);
};

export const logout = async () => {
cookies().delete('token');
revalidateTag('cart')
Expand Down
1 change: 1 addition & 0 deletions src/lib/db/user_collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const UserSchema = z.object({
name: z.string().min(3, { message: "Name must be at least 3 characters long" }),
email: z.string().email({ message: "Invalid email address" }),
password: z.string().min(6, { message: "Password must be at least 6 characters long" }),
googleId: z.string().optional(),
createdAt: z.date().default(new Date()).optional(),
});

Expand Down

0 comments on commit 784a398

Please sign in to comment.