Browse Source

feat: signin with google

pull/1/head
isra el 3 years ago
parent
commit
09baa28923
  1. 98
      api/package-lock.json
  2. 1
      api/package.json
  3. 7
      api/src/auth/auth.controller.ts
  4. 35
      api/src/auth/auth.service.ts
  5. 8
      api/src/users/schemas/user.schema.ts
  6. 2
      web/.env.example
  7. 6
      web/components/Navbar.tsx
  8. 16
      web/package-lock.json
  9. 1
      web/package.json
  10. 17
      web/pages/_app.tsx
  11. 18
      web/pages/index.tsx
  12. 17
      web/pages/login.tsx
  13. 17
      web/pages/register.tsx
  14. 8
      web/services/index.ts
  15. 5
      web/services/types.ts
  16. 30
      web/store/authReducer.ts
  17. 3043
      web/yarn.lock

98
api/package-lock.json

@ -16,6 +16,7 @@
"@nestjs/passport": "^8.2.1", "@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0", "@nestjs/platform-express": "^8.0.0",
"@nestjs/swagger": "^5.2.1", "@nestjs/swagger": "^5.2.1",
"axios": "^1.3.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"firebase-admin": "^10.0.2", "firebase-admin": "^10.0.2",
@ -1728,6 +1729,14 @@
} }
} }
}, },
"node_modules/@nestjs/common/node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.14.4"
}
},
"node_modules/@nestjs/core": { "node_modules/@nestjs/core": {
"version": "8.2.4", "version": "8.2.4",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-8.2.4.tgz", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-8.2.4.tgz",
@ -3228,8 +3237,7 @@
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
}, },
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
@ -3241,11 +3249,26 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": { "dependencies": {
"follow-redirects": "^1.14.4"
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
} }
}, },
"node_modules/babel-jest": { "node_modules/babel-jest": {
@ -3791,7 +3814,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
}, },
@ -4141,7 +4163,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
@ -5155,9 +5176,9 @@
"dev": true "dev": true
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -8255,6 +8276,11 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pseudomap": { "node_modules/pseudomap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@ -11506,6 +11532,16 @@
"iterare": "1.2.1", "iterare": "1.2.1",
"tslib": "2.3.1", "tslib": "2.3.1",
"uuid": "8.3.2" "uuid": "8.3.2"
},
"dependencies": {
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
}
} }
}, },
"@nestjs/core": { "@nestjs/core": {
@ -12686,8 +12722,7 @@
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
}, },
"at-least-node": { "at-least-node": {
"version": "1.0.0", "version": "1.0.0",
@ -12696,11 +12731,25 @@
"dev": true "dev": true
}, },
"axios": { "axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"requires": { "requires": {
"follow-redirects": "^1.14.4"
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
} }
}, },
"babel-jest": { "babel-jest": {
@ -13105,7 +13154,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": { "requires": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
@ -13408,8 +13456,7 @@
"delayed-stream": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
}, },
"denque": { "denque": {
"version": "2.0.1", "version": "2.0.1",
@ -14193,9 +14240,9 @@
"dev": true "dev": true
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
}, },
"fork-ts-checker-webpack-plugin": { "fork-ts-checker-webpack-plugin": {
"version": "6.5.0", "version": "6.5.0",
@ -16565,6 +16612,11 @@
"ipaddr.js": "1.9.1" "ipaddr.js": "1.9.1"
} }
}, },
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"pseudomap": { "pseudomap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",

1
api/package.json

@ -27,6 +27,7 @@
"@nestjs/passport": "^8.2.1", "@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0", "@nestjs/platform-express": "^8.0.0",
"@nestjs/swagger": "^5.2.1", "@nestjs/swagger": "^5.2.1",
"axios": "^1.3.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"firebase-admin": "^10.0.2", "firebase-admin": "^10.0.2",

7
api/src/auth/auth.controller.ts

@ -27,6 +27,13 @@ export class AuthController {
return { data } return { data }
} }
@ApiOperation({ summary: 'Login With Google' })
@Post('/google-login')
async googleLogin(@Body() input: any) {
const data = await this.authService.loginWithGoogle(input.idToken)
return { data }
}
@ApiOperation({ summary: 'Register' }) @ApiOperation({ summary: 'Register' })
@Post('/register') @Post('/register')
async register(@Body() input: RegisterInputDTO) { async register(@Body() input: RegisterInputDTO) {

35
api/src/auth/auth.service.ts

@ -7,6 +7,7 @@ import { InjectModel } from '@nestjs/mongoose'
import { ApiKey, ApiKeyDocument } from './schemas/api-key.schema' import { ApiKey, ApiKeyDocument } from './schemas/api-key.schema'
import { Model } from 'mongoose' import { Model } from 'mongoose'
import { User } from 'src/users/schemas/user.schema' import { User } from 'src/users/schemas/user.schema'
import axios from 'axios'
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( constructor(
@ -46,6 +47,40 @@ export class AuthService {
} }
} }
async loginWithGoogle(idToken: string) {
const response = await axios.get(
`https://oauth2.googleapis.com/tokeninfo?id_token=${idToken}`,
)
const { sub: googleId, name, email, picture } = response.data
let user = await this.usersService.findOne({ email })
if (!user) {
user = await this.usersService.create({
name,
email,
googleId,
avatar: picture,
})
} else {
user.googleId = googleId
if (!user.name) {
user.name = name
}
if (!user.avatar) {
user.avatar = picture
}
await user.save()
}
const payload = { email: user.email, sub: user._id }
return {
accessToken: this.jwtService.sign(payload),
user,
}
}
async register(userData: any) { async register(userData: any) {
const hashedPassword = await bcrypt.hash(userData.password, 10) const hashedPassword = await bcrypt.hash(userData.password, 10)
const user = await this.usersService.create({ const user = await this.usersService.create({

8
api/src/users/schemas/user.schema.ts

@ -14,10 +14,16 @@ export class User {
@Prop({ type: String, required: true, unique: true, lowercase: true }) @Prop({ type: String, required: true, unique: true, lowercase: true })
email: string email: string
@Prop({ type: String, unique: true })
googleId?: string
@Prop({ type: String })
avatar?: string
@Prop({ type: String, trim: true }) @Prop({ type: String, trim: true })
primaryPhone: string primaryPhone: string
@Prop({ type: String, required: true })
@Prop({ type: String })
password: string password: string
@Prop({ type: String, default: UserRole.REGULAR }) @Prop({ type: String, default: UserRole.REGULAR })

2
web/.env.example

@ -0,0 +1,2 @@
NEXT_PUBLIC_API_BASE_URL=http://localhost:3006/api/v1
NEXT_PUBLIC_GOOGLE_CLIENT_ID=

6
web/components/Navbar.tsx

@ -72,7 +72,10 @@ export default function Navbar() {
> >
<Avatar <Avatar
size={'sm'} size={'sm'}
src={'https://avatars.dicebear.com/api/male/username.svg'}
src={
user?.avatar ??
'https://avatars.dicebear.com/api/male/username.svg'
}
/> />
</MenuButton> </MenuButton>
<MenuList alignItems={'center'}> <MenuList alignItems={'center'}>
@ -81,6 +84,7 @@ export default function Navbar() {
<Avatar <Avatar
size={'xl'} size={'xl'}
src={ src={
user?.avatar ??
'https://avatars.dicebear.com/api/male/username.svg' 'https://avatars.dicebear.com/api/male/username.svg'
} }
/> />

16
web/package-lock.json

@ -12,6 +12,7 @@
"@chakra-ui/react": "^1.8.7", "@chakra-ui/react": "^1.8.7",
"@emotion/react": "^11.8.2", "@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@react-oauth/google": "^0.9.0",
"@reduxjs/toolkit": "^1.9.3", "@reduxjs/toolkit": "^1.9.3",
"axios": "^0.26.1", "axios": "^0.26.1",
"framer-motion": "^6.2.8", "framer-motion": "^6.2.8",
@ -1721,6 +1722,15 @@
"react-dom": "^16.8.0 || 17.x" "react-dom": "^16.8.0 || 17.x"
} }
}, },
"node_modules/@react-oauth/google": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.9.0.tgz",
"integrity": "sha512-iq9I6A4uwZezU/BixqLM6UET6an559ufC4Nh0lEIeIaKC3TJRvcPNWCjjHny56yAhgdT6ivUicLIvEoiSMjnmg==",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@reduxjs/toolkit": { "node_modules/@reduxjs/toolkit": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",
@ -6284,6 +6294,12 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"@react-oauth/google": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.9.0.tgz",
"integrity": "sha512-iq9I6A4uwZezU/BixqLM6UET6an559ufC4Nh0lEIeIaKC3TJRvcPNWCjjHny56yAhgdT6ivUicLIvEoiSMjnmg==",
"requires": {}
},
"@reduxjs/toolkit": { "@reduxjs/toolkit": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",

1
web/package.json

@ -13,6 +13,7 @@
"@chakra-ui/react": "^1.8.7", "@chakra-ui/react": "^1.8.7",
"@emotion/react": "^11.8.2", "@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@react-oauth/google": "^0.9.0",
"@reduxjs/toolkit": "^1.9.3", "@reduxjs/toolkit": "^1.9.3",
"axios": "^0.26.1", "axios": "^0.26.1",
"framer-motion": "^6.2.8", "framer-motion": "^6.2.8",

17
web/pages/_app.tsx

@ -5,16 +5,19 @@ import { store } from '../store/store'
import { ChakraProvider } from '@chakra-ui/react' import { ChakraProvider } from '@chakra-ui/react'
import Navbar from '../components/Navbar' import Navbar from '../components/Navbar'
import Meta from '../components/meta/Meta' import Meta from '../components/meta/Meta'
import { GoogleOAuthProvider } from '@react-oauth/google'
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
return ( return (
<Provider store={store}> <Provider store={store}>
<ChakraProvider>
<Meta />
<Navbar />
<Wrapper>
<Component {...pageProps} />
</Wrapper>
</ChakraProvider>
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID }>
<ChakraProvider>
<Meta />
<Navbar />
<Wrapper>
<Component {...pageProps} />
</Wrapper>
</ChakraProvider>
</GoogleOAuthProvider>
</Provider> </Provider>
) )
} }

18
web/pages/index.tsx

@ -1,11 +1,12 @@
import { Container } from '@chakra-ui/react' import { Container } from '@chakra-ui/react'
import { useGoogleOneTapLogin } from '@react-oauth/google'
import Router from 'next/router' import Router from 'next/router'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import FeaturesSection from '../components/home/FeaturesSection' import FeaturesSection from '../components/home/FeaturesSection'
import HowItWorksSection from '../components/home/HowItWorksSection' import HowItWorksSection from '../components/home/HowItWorksSection'
import IntroSection from '../components/home/IntroSection' import IntroSection from '../components/home/IntroSection'
import { selectAuth } from '../store/authReducer'
import { loginWithGoogle, selectAuth } from '../store/authReducer'
export default function HomePage() { export default function HomePage() {
const { accessToken, user } = useSelector(selectAuth) const { accessToken, user } = useSelector(selectAuth)
@ -15,6 +16,19 @@ export default function HomePage() {
} }
}, [accessToken, user]) }, [accessToken, user])
const dispatch = useDispatch()
useGoogleOneTapLogin({
onSuccess: ({ credential: idToken }) => {
dispatch(
loginWithGoogle({
idToken,
})
)
},
onError: () => {},
})
return ( return (
<Container maxW={'7xl'}> <Container maxW={'7xl'}>
<IntroSection /> <IntroSection />

17
web/pages/login.tsx

@ -17,9 +17,10 @@ import {
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { useState } from 'react'
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'
import { login, selectAuth } from '../store/authReducer'
import { login, loginWithGoogle, selectAuth } from '../store/authReducer'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { LoginRequestPayload } from '../services/types' import { LoginRequestPayload } from '../services/types'
import { GoogleLogin } from '@react-oauth/google'
export default function LoginPage() { export default function LoginPage() {
const [showPassword, setShowPassword] = useState<boolean>(false) const [showPassword, setShowPassword] = useState<boolean>(false)
@ -112,6 +113,20 @@ export default function LoginPage() {
{authState.loading ? 'Please Wait...' : 'Login'} {authState.loading ? 'Please Wait...' : 'Login'}
</Button> </Button>
</Stack> </Stack>
<GoogleLogin
onSuccess={({ credential: idToken }) => {
dispatch(loginWithGoogle({ idToken }))
}}
onError={() => {
toast({
title: 'Error',
description: 'Something went wrong',
status: 'error',
})
}}
useOneTap={true}
width='100%'
/>
<Stack pt={6}> <Stack pt={6}>
<Text align={'center'}> <Text align={'center'}>
Don&apos;t have an account?{' '} Don&apos;t have an account?{' '}

17
web/pages/register.tsx

@ -16,9 +16,10 @@ import {
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { useState } from 'react'
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'
import { register, selectAuth } from '../store/authReducer'
import { loginWithGoogle, register, selectAuth } from '../store/authReducer'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { RegisterRequestPayload } from '../services/types' import { RegisterRequestPayload } from '../services/types'
import { GoogleLogin } from '@react-oauth/google'
export default function RegisterPage() { export default function RegisterPage() {
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
@ -116,6 +117,20 @@ export default function RegisterPage() {
{authState.loading ? 'Please wait...' : 'Register'} {authState.loading ? 'Please wait...' : 'Register'}
</Button> </Button>
</Stack> </Stack>
<GoogleLogin
onSuccess={({ credential: idToken }) => {
dispatch(loginWithGoogle({ idToken }))
}}
onError={() => {
toast({
title: 'Error',
description: 'Something went wrong',
status: 'error',
})
}}
useOneTap={true}
width='100%'
/>
<Stack pt={6}> <Stack pt={6}>
<Text align={'center'}> <Text align={'center'}>
Already a user? <Link href='/login'>Login</Link> Already a user? <Link href='/login'>Login</Link>

8
web/services/index.ts

@ -1,6 +1,7 @@
import axios from 'axios' import axios from 'axios'
import { LOCAL_STORAGE_KEY } from '../shared/constants' import { LOCAL_STORAGE_KEY } from '../shared/constants'
import { import {
GoogleLoginRequestPayload,
LoginRequestPayload, LoginRequestPayload,
LoginResponse, LoginResponse,
RegisterRequestPayload, RegisterRequestPayload,
@ -21,6 +22,13 @@ export const loginRequest = async (
return res.data.data return res.data.data
} }
export const loginWithGoogleRequest = async (
payload: GoogleLoginRequestPayload
): Promise<LoginResponse> => {
const res = await axios.post(`${BASE_URL}/auth/google-login`, payload)
return res.data.data
}
export const registerRequest = async ( export const registerRequest = async (
payload: RegisterRequestPayload payload: RegisterRequestPayload
): Promise<RegisterResponse> => { ): Promise<RegisterResponse> => {

5
web/services/types.ts

@ -8,6 +8,7 @@ export interface UserEntity {
name: string name: string
email: string email: string
role: UserRole role: UserRole
avatar?: string
} }
export interface AuthState { export interface AuthState {
loading: boolean loading: boolean
@ -20,6 +21,10 @@ export interface LoginRequestPayload {
password: string password: string
} }
export interface GoogleLoginRequestPayload {
idToken: string
}
export interface RegisterRequestPayload extends LoginRequestPayload { export interface RegisterRequestPayload extends LoginRequestPayload {
name: string name: string
} }

30
web/store/authReducer.ts

@ -1,16 +1,22 @@
import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit' import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit'
import { loginRequest, registerRequest } from '../services'
import {
loginRequest,
loginWithGoogleRequest,
registerRequest,
} from '../services'
import { createStandaloneToast } from '@chakra-ui/react' import { createStandaloneToast } from '@chakra-ui/react'
import Router from 'next/router' import Router from 'next/router'
import { RootState } from './store' import { RootState } from './store'
import { import {
AuthState, AuthState,
GoogleLoginRequestPayload,
LoginRequestPayload, LoginRequestPayload,
RegisterRequestPayload, RegisterRequestPayload,
} from '../services/types' } from '../services/types'
import { removeUserAndToken, saveUserAndToken } from '../shared/utils' import { removeUserAndToken, saveUserAndToken } from '../shared/utils'
import { LOCAL_STORAGE_KEY } from '../shared/constants' import { LOCAL_STORAGE_KEY } from '../shared/constants'
import { googleLogout } from '@react-oauth/google'
const toast = createStandaloneToast() const toast = createStandaloneToast()
@ -45,6 +51,25 @@ export const login = createAsyncThunk(
} }
) )
export const loginWithGoogle = createAsyncThunk(
'auth/google-login',
async (payload: GoogleLoginRequestPayload, thunkAPI) => {
try {
const res = await loginWithGoogleRequest(payload)
const { accessToken, user } = res
saveUserAndToken(user, accessToken)
Router.push('/')
return res
} catch (e) {
toast({
title: e.response.data.error || 'Login failed',
status: 'error',
})
return thunkAPI.rejectWithValue(e.response.data)
}
}
)
export const register = createAsyncThunk( export const register = createAsyncThunk(
'auth/register', 'auth/register',
async (payload: RegisterRequestPayload, thunkAPI) => { async (payload: RegisterRequestPayload, thunkAPI) => {
@ -70,6 +95,7 @@ export const authSlice = createSlice({
reducers: { reducers: {
logout: (state) => { logout: (state) => {
removeUserAndToken() removeUserAndToken()
googleLogout()
state.accessToken = null state.accessToken = null
state.user = null state.user = null
Router.push('/login') Router.push('/login')
@ -78,7 +104,7 @@ export const authSlice = createSlice({
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
.addMatcher( .addMatcher(
isAnyOf(login.fulfilled, register.fulfilled),
isAnyOf(login.fulfilled, register.fulfilled, loginWithGoogle.fulfilled),
(state, action: PayloadAction<any>) => { (state, action: PayloadAction<any>) => {
state.loading = false state.loading = false
state.user = action.payload.user state.user = action.payload.user

3043
web/yarn.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save