Skip to content

Commit

Permalink
build(root): ✨ added google oauth
Browse files Browse the repository at this point in the history
  • Loading branch information
sahrohit committed Sep 5, 2023
1 parent 4a0b352 commit c88cda8
Show file tree
Hide file tree
Showing 16 changed files with 346 additions and 8 deletions.
5 changes: 4 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ KHALTI_SECRET_KEY=
RESEND_HOST=
RESENT_PORT=
RESEND_AUTH_USER=
RESEND_AUTH_PASS=
RESEND_AUTH_PASS=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
API_URL=
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@types/ioredis": "^4.28.10",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^17.0.45",
"@types/nodemailer": "^6.4.9",
"@types/uuid": "^8.3.4",
Expand Down Expand Up @@ -54,6 +55,7 @@
"helmet": "^7.0.0",
"ioredis": "^4.28.5",
"joi": "^17.9.2",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.4",
"pg": "^8.11.2",
"reflect-metadata": "^0.1.13",
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const __prod__ = process.env.NODE_ENV === "production";
export const COOKIE_NAME = "qid";
export const FORGOT_PASSWORD_PREFIX = "forgot-password:";
export const VERIFY_EMAIL_PREFIX = "verify-email:";
export const USER_VALIDATION_PREFIX = "validate-user:";
export const COMPANY = {
name: "Hamropasal",
logo: `https://hamropasal.vercel.app/logo.png`,
Expand All @@ -10,3 +11,4 @@ export const COMPANY = {
city: "Kathmandu",
country: "Nepal",
};
export const GOOGLE_OAUTH_REDIRECT_URL = `${process.env.API_URL}/auth/google/callback`;
2 changes: 2 additions & 0 deletions apps/api/src/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ProductVariant } from "./entities/ProductVariant";
import { Promo } from "./entities/Promo";
import { Favourite } from "./entities/Favourite";
import { ProductReview } from "./entities/ProductReview";
import { Account } from "./entities/Account";

export const AppDataSource = new DataSource({
type: "postgres",
Expand All @@ -46,6 +47,7 @@ export const AppDataSource = new DataSource({
Promo,
Favourite,
ProductReview,
Account,
],
migrations: ["dist/migration/**/*.js"],
subscribers: [],
Expand Down
77 changes: 77 additions & 0 deletions apps/api/src/entities/Account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Field, Int, ObjectType } from "type-graphql";
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
BaseEntity,
ManyToOne,
} from "typeorm";
import { User } from "./User";

@ObjectType()
@Entity()
export class Account extends BaseEntity {
@Field(() => Int)
@PrimaryGeneratedColumn()
id!: number;

@Field()
@Column()
userId!: number;

@ManyToOne(() => User, (user) => user.addresses)
user!: User;

@Field(() => String)
@Column({
type: "enum",
enum: ["OAUTH", "PASSWORD"],
})
type!: "OAUTH" | "PASSWORD";

@Field()
@Column()
provider!: string;

@Field()
@Column()
providerAccountId!: string;

@Field({ nullable: true })
@Column({ nullable: true })
refresh_token!: string;

@Field({ nullable: true })
@Column({ nullable: true })
access_token!: string;

@Field({ nullable: true })
@Column({ nullable: true })
expires_at!: number;

@Field({ nullable: true })
@Column({ nullable: true })
token_type!: string;

@Field({ nullable: true })
@Column({ nullable: true })
scope!: string;

@Field({ nullable: true })
@Column({ nullable: true })
id_token!: string;

@Field({ nullable: true })
@Column({ nullable: true })
session_state!: string;

@Field(() => String)
@CreateDateColumn()
created_at = new Date();

@Field(() => String)
@UpdateDateColumn()
updated_at = new Date();
}
3 changes: 3 additions & 0 deletions apps/api/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ declare global {
RESENT_PORT: string;
RESEND_AUTH_USER: string;
RESEND_AUTH_PASS: string;
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
API_URL: string;
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ReviewResolver } from "./resolvers/review";
import Redis from "ioredis";
import { Product } from "./entities/Product";
import { InvoiceResolver } from "./resolvers/invoice";
import authRouter from "./routers/auth";

const Server = async () => {
AppDataSource.initialize()
Expand Down Expand Up @@ -79,6 +80,8 @@ const Server = async () => {
res.json({ status: "ok" });
});

app.use("/auth", authRouter);

app.get("/products", async (_req, res) => {
res.json(
await Product.find({
Expand Down
108 changes: 108 additions & 0 deletions apps/api/src/routers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Router } from "express";
import jwt from "jsonwebtoken";
import { Account } from "../entities/Account";
import { User } from "../entities/User";
import { MyContext } from "../types";
import { getGoogleOAuthToken } from "../utils/oauth";

const router: Router = Router();

interface GoogleUserResponse {
iss: string;
azp: string;
aud: string;
sub: string;
email: string;
email_verified: boolean;
at_hash: string;
name: string;
picture: string;
given_name: string;
family_name: string;
locale: string;
iat: number;
exp: number;
}

router.get(
"/google/callback",
async (req: MyContext["req"], res: MyContext["res"]) => {
// 1. Get the code from query string

const code = req.query.code as string;

// 2. Get the id and access token

const { id_token, access_token, refresh_token, expires_in } =
await getGoogleOAuthToken({ code });

// 3. Get user with token

const profile = jwt.decode(id_token) as GoogleUserResponse;

// 4. Upsert the User

const exisitingUser = await Account.findOne({
where: {
providerAccountId: profile.sub,
},
relations: {
user: true,
},
});

if (exisitingUser) {
req.session.userId = exisitingUser.userId;
res.redirect(`${process.env.CLIENT_URL}`);
return;
}

const existingUserByEmail = await User.findOne({
where: {
email: profile.email,
},
});

if (existingUserByEmail) {
await Account.create({
userId: existingUserByEmail.id,
type: "OAUTH",
provider: "google",
providerAccountId: profile.sub,
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_in,
}).save();

req.session.userId = existingUserByEmail.id;
res.redirect(`${process.env.CLIENT_URL}`);
return;
}

const userRes = await User.create({
password: "unset",
first_name: profile.given_name,
last_name: profile.family_name,
email: profile.email,
email_verified: profile.email_verified,
imageUrl:
profile.picture ??
`https://api.dicebear.com/6.x/micah/svg?size=256&seed=${profile.name}`,
}).save();

await Account.create({
userId: userRes.id,
type: "OAUTH",
provider: "google",
providerAccountId: profile.sub,
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_in,
}).save();

req.session.userId = userRes.id;
res.redirect(`${process.env.CLIENT_URL}`);
}
);

export default router;
38 changes: 38 additions & 0 deletions apps/api/src/utils/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { GOOGLE_OAUTH_REDIRECT_URL } from "../constants";

interface GoogleTokenResult {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
id_token: string;
}

export const getGoogleOAuthToken = async ({
code,
}: {
code: string;
}): Promise<GoogleTokenResult> => {
const url = "https://oauth2.googleapis.com/token";
const values = new URLSearchParams({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: GOOGLE_OAUTH_REDIRECT_URL,
grant_type: "authorization_code",
});
try {
const res = await fetch(url, {
method: "POST",
body: values,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});

return res.json();
} catch (error: any) {
console.error(error);
throw new Error(error.message);
}
};
1 change: 1 addition & 0 deletions apps/storefront/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_GOOGLE_CLIENT_ID=
KHALTI_SECRET_KEY=
2 changes: 2 additions & 0 deletions apps/storefront/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export const COLOR = "Ecommerce Storefront";
export const APP_URL = !PROD
? "http://localhost:3000"
: "https://www.rudejellyfish.live";

export const GOOGLE_OAUTH_REDIRECT_URL = `${process.env.NEXT_PUBLIC_API_URL}/auth/google/callback`;
1 change: 1 addition & 0 deletions apps/storefront/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_URL: string;
NEXT_PUBLIC_GOOGLE_CLIENT_ID: string;
KHALTI_SECRET_KEY: string;
}
}
Expand Down
21 changes: 20 additions & 1 deletion apps/storefront/src/components/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { Stack, Button, Box } from "@chakra-ui/react";
import { FcGoogle } from "react-icons/fc";
import { GOOGLE_OAUTH_REDIRECT_URL } from "../../../constants";

const SignInWithGoogle = () => (
<Stack spacing="4">
<Stack spacing="4" as="a" href={getGoogleOAuthUrl()}>
<Button leftIcon={<Box as={FcGoogle} color="red.500" />}>
Sign up with Google
</Button>
</Stack>
);

export default SignInWithGoogle;

export const getGoogleOAuthUrl = () => {
const rootUrl = "https://accounts.google.com/o/oauth2/v2/auth";

const qs = new URLSearchParams({
redirect_uri: GOOGLE_OAUTH_REDIRECT_URL,
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
access_type: "offline",
response_type: "code",
prompt: "consent",
scope: [
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",
].join(" "),
});

return `${rootUrl}?${qs.toString()}`;
};
2 changes: 1 addition & 1 deletion apps/storefront/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const App = ({ Component, pageProps }: AppProps) => {
const router = useRouter();

const client = new ApolloClient({
uri: process.env.NEXT_PUBLIC_API_URL,
uri: `${process.env.NEXT_PUBLIC_API_URL}/graphql`,
cache: new InMemoryCache({
// ! Pagination is working, but filtering stops working after this
// typePolicies: {
Expand Down
Loading

2 comments on commit c88cda8

@vercel
Copy link

@vercel vercel bot commented on c88cda8 Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ecommerce-admin-client – ./apps/admin

ecommerce-admin-client.vercel.app
ecommerce-admin-client-git-main-sahrohit.vercel.app
ecommerce-admin-client-sahrohit.vercel.app

@vercel
Copy link

@vercel vercel bot commented on c88cda8 Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.