feat: add OAuth2 authentication (Google/GitHub/Discord)
This commit is contained in:
		| @@ -22,15 +22,7 @@ exports.serviceGenerator = { | ||||
|       type: 'list', | ||||
|       name: 'tag', | ||||
|       message: 'tag', | ||||
|       choices: [ | ||||
|         'users', | ||||
|         'guilds', | ||||
|         'channels', | ||||
|         'invitations', | ||||
|         'messages', | ||||
|         'members', | ||||
|         'uploads' | ||||
|       ] | ||||
|       choices: ['users', 'guilds', 'channels', 'messages', 'members', 'uploads'] | ||||
|     }, | ||||
|     { | ||||
|       type: 'confirm', | ||||
|   | ||||
| @@ -33,24 +33,25 @@ export const getCurrentUser: FastifyPluginAsync = async (fastify) => { | ||||
|       if (request.user == null) { | ||||
|         throw fastify.httpErrors.forbidden() | ||||
|       } | ||||
|       const { user } = request | ||||
|       const settings = await prisma.userSetting.findFirst({ | ||||
|         where: { userId: request.user.current.id } | ||||
|         where: { userId: user.current.id } | ||||
|       }) | ||||
|       const OAuths = await prisma.oAuth.findMany({ | ||||
|         where: { userId: request.user.current.id } | ||||
|         where: { userId: user.current.id } | ||||
|       }) | ||||
|       const strategies = OAuths.map((oauth) => { | ||||
|         return oauth.provider | ||||
|       }) | ||||
|       if (request.user.current.password != null) { | ||||
|       if (user.current.password != null) { | ||||
|         strategies.push('local') | ||||
|       } | ||||
|       reply.statusCode = 200 | ||||
|       return { | ||||
|         user: { | ||||
|           ...request.user.current, | ||||
|           ...user.current, | ||||
|           settings, | ||||
|           currentStrategy: request.user.currentStrategy, | ||||
|           currentStrategy: user.currentStrategy, | ||||
|           strategies | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -65,7 +65,10 @@ export const putCurrentUser: FastifyPluginAsync = async (fastify) => { | ||||
|       const { redirectURI } = request.params | ||||
|       const userValidation = await prisma.user.findFirst({ | ||||
|         where: { | ||||
|           OR: [{ email }, { name }], | ||||
|           OR: [ | ||||
|             ...(email != null ? [{ email }] : [{}]), | ||||
|             ...(name != null ? [{ name }] : [{}]) | ||||
|           ], | ||||
|           AND: [{ id: { not: request.user.current.id } }] | ||||
|         } | ||||
|       }) | ||||
|   | ||||
| @@ -13,6 +13,13 @@ import { putCurrentUser } from './current/put.js' | ||||
| import { putCurrentUserSettings } from './current/settings/put.js' | ||||
| import { getUserById } from './[userId]/get.js' | ||||
| import { putCurrentUserLogo } from './current/logo/put.js' | ||||
| import { getSigninDiscordOAuth2Service } from './oauth2/discord/signin/get.js' | ||||
| import { getCallbackDiscordOAuth2Service } from './oauth2/discord/callback/get.js' | ||||
| import { getSigninGoogleOAuth2Service } from './oauth2/google/signin/get.js' | ||||
| import { getCallbackGoogleOAuth2Service } from './oauth2/google/callback/get.js' | ||||
| import { getSigninGitHubOAuth2Service } from './oauth2/github/signin/get.js' | ||||
| import { getCallbackGitHubOAuth2Service } from './oauth2/github/callback/get.js' | ||||
| import { deleteProviderService } from './oauth2/[provider]/delete.js' | ||||
|  | ||||
| export const usersService: FastifyPluginAsync = async (fastify) => { | ||||
|   await fastify.register(postSignupUser) | ||||
| @@ -28,4 +35,15 @@ export const usersService: FastifyPluginAsync = async (fastify) => { | ||||
|   await fastify.register(putCurrentUserSettings) | ||||
|   await fastify.register(putCurrentUserLogo) | ||||
|   await fastify.register(getUserById) | ||||
|  | ||||
|   await fastify.register(getSigninDiscordOAuth2Service) | ||||
|   await fastify.register(getCallbackDiscordOAuth2Service) | ||||
|  | ||||
|   await fastify.register(getSigninGoogleOAuth2Service) | ||||
|   await fastify.register(getCallbackGoogleOAuth2Service) | ||||
|  | ||||
|   await fastify.register(getSigninGitHubOAuth2Service) | ||||
|   await fastify.register(getCallbackGitHubOAuth2Service) | ||||
|  | ||||
|   await fastify.register(deleteProviderService) | ||||
| } | ||||
|   | ||||
							
								
								
									
										75
									
								
								src/services/users/oauth2/[provider]/delete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/services/users/oauth2/[provider]/delete.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
| import { FastifyPluginAsync, FastifySchema } from 'fastify' | ||||
|  | ||||
| import prisma from '../../../../tools/database/prisma.js' | ||||
| import { fastifyErrors } from '../../../../models/utils.js' | ||||
| import authenticateUser from '../../../../tools/plugins/authenticateUser.js' | ||||
| import { oauthSchema } from '../../../../models/OAuth.js' | ||||
|  | ||||
| const parametersSchema = Type.Object({ | ||||
|   provider: oauthSchema.provider | ||||
| }) | ||||
|  | ||||
| type Parameters = Static<typeof parametersSchema> | ||||
|  | ||||
| const deleteServiceSchema: FastifySchema = { | ||||
|   description: 'DELETE a provider to authenticate with for a user.', | ||||
|   tags: ['users'] as string[], | ||||
|   security: [ | ||||
|     { | ||||
|       bearerAuth: [] | ||||
|     } | ||||
|   ] as Array<{ [key: string]: [] }>, | ||||
|   params: parametersSchema, | ||||
|   response: { | ||||
|     200: Type.Object(oauthSchema), | ||||
|     400: fastifyErrors[400], | ||||
|     401: fastifyErrors[401], | ||||
|     403: fastifyErrors[403], | ||||
|     404: fastifyErrors[404], | ||||
|     500: fastifyErrors[500] | ||||
|   } | ||||
| } as const | ||||
|  | ||||
| export const deleteProviderService: FastifyPluginAsync = async (fastify) => { | ||||
|   await fastify.register(authenticateUser) | ||||
|  | ||||
|   fastify.route<{ | ||||
|     Params: Parameters | ||||
|   }>({ | ||||
|     method: 'DELETE', | ||||
|     url: '/users/oauth2/:provider', | ||||
|     schema: deleteServiceSchema, | ||||
|     handler: async (request, reply) => { | ||||
|       if (request.user == null) { | ||||
|         throw fastify.httpErrors.forbidden() | ||||
|       } | ||||
|       const { user } = request | ||||
|       const { provider } = request.params | ||||
|       const OAuths = await prisma.oAuth.findMany({ | ||||
|         where: { userId: user.current.id } | ||||
|       }) | ||||
|       const strategies = OAuths.map((oauth) => { | ||||
|         return oauth.provider | ||||
|       }) | ||||
|       if (user.current.password != null) { | ||||
|         strategies.push('local') | ||||
|       } | ||||
|       const oauthProvider = OAuths.find((oauth) => oauth.provider === provider) | ||||
|       if (oauthProvider == null) { | ||||
|         throw fastify.httpErrors.notFound('You are not using this provider') | ||||
|       } | ||||
|       const hasOthersWayToAuthenticate = strategies.length >= 2 | ||||
|       if (!hasOthersWayToAuthenticate) { | ||||
|         throw fastify.httpErrors.badRequest( | ||||
|           "You can't delete your only way to authenticate" | ||||
|         ) | ||||
|       } | ||||
|       const oauthProviderDelete = await prisma.oAuth.delete({ | ||||
|         where: { id: oauthProvider.id } | ||||
|       }) | ||||
|       reply.statusCode = 200 | ||||
|       return oauthProviderDelete | ||||
|     } | ||||
|   }) | ||||
| } | ||||
							
								
								
									
										60
									
								
								src/services/users/oauth2/discord/__utils__/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/services/users/oauth2/discord/__utils__/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import querystring from 'node:querystring' | ||||
|  | ||||
| import axios from 'axios' | ||||
|  | ||||
| import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js' | ||||
|  | ||||
| export const DISCORD_PROVIDER = 'discord' | ||||
| export const DISCORD_BASE_URL = 'https://discord.com/api/v6' | ||||
| export const DISCORD_CLIENT_ID = | ||||
|   process.env.DISCORD_CLIENT_ID ?? 'DISCORD_CLIENT_ID' | ||||
| export const DISCORD_CLIENT_SECRET = | ||||
|   process.env.DISCORD_CLIENT_SECRET ?? 'DISCORD_CLIENT_SECRET' | ||||
| export const discordStrategy = new OAuthStrategy(DISCORD_PROVIDER) | ||||
|  | ||||
| export interface DiscordUser { | ||||
|   id: string | ||||
|   username: string | ||||
|   discriminator: string | ||||
|   avatar?: string | ||||
|   locale?: string | ||||
| } | ||||
|  | ||||
| export interface DiscordTokens { | ||||
|   access_token: string | ||||
|   token_type: string | ||||
|   expires_in: number | ||||
|   refresh_token: string | ||||
|   scope: string | ||||
| } | ||||
|  | ||||
| export const getDiscordUserData = async ( | ||||
|   code: string, | ||||
|   redirectURI: string | ||||
| ): Promise<DiscordUser> => { | ||||
|   const { data: tokens } = await axios.post<DiscordTokens>( | ||||
|     `${DISCORD_BASE_URL}/oauth2/token`, | ||||
|     querystring.stringify({ | ||||
|       client_id: DISCORD_CLIENT_ID, | ||||
|       client_secret: DISCORD_CLIENT_SECRET, | ||||
|       grant_type: 'authorization_code', | ||||
|       code, | ||||
|       redirect_uri: redirectURI, | ||||
|       scope: 'identify' | ||||
|     }), | ||||
|     { | ||||
|       headers: { | ||||
|         'Content-Type': 'application/x-www-form-urlencoded' | ||||
|       } | ||||
|     } | ||||
|   ) | ||||
|   const { data: discordUser } = await axios.get<DiscordUser>( | ||||
|     `${DISCORD_BASE_URL}/users/@me`, | ||||
|     { | ||||
|       headers: { | ||||
|         Authorization: `${tokens.token_type} ${tokens.access_token}` | ||||
|       } | ||||
|     } | ||||
|   ) | ||||
|   return discordUser | ||||
| } | ||||
							
								
								
									
										49
									
								
								src/services/users/oauth2/discord/callback/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/services/users/oauth2/discord/callback/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
| import { FastifyPluginAsync, FastifySchema } from 'fastify' | ||||
|  | ||||
| import { HOST, PORT } from '../../../../../tools/configurations/index.js' | ||||
| import { fastifyErrors } from '../../../../../models/utils.js' | ||||
| import { discordStrategy, getDiscordUserData } from '../__utils__/utils.js' | ||||
| import { buildQueryURL } from '../../../../../tools/utils/buildQueryURL.js' | ||||
|  | ||||
| const querySchema = Type.Object({ | ||||
|   code: Type.String(), | ||||
|   redirectURI: Type.String({ format: 'uri-reference' }) | ||||
| }) | ||||
|  | ||||
| type QuerySchemaType = Static<typeof querySchema> | ||||
|  | ||||
| const getServiceSchema: FastifySchema = { | ||||
|   description: 'Discord OAuth2 - callback', | ||||
|   tags: ['users'] as string[], | ||||
|   querystring: querySchema, | ||||
|   response: { | ||||
|     200: Type.String(), | ||||
|     400: fastifyErrors[400], | ||||
|     500: fastifyErrors[500] | ||||
|   } | ||||
| } as const | ||||
|  | ||||
| export const getCallbackDiscordOAuth2Service: FastifyPluginAsync = async ( | ||||
|   fastify | ||||
| ) => { | ||||
|   await fastify.route<{ | ||||
|     Querystring: QuerySchemaType | ||||
|   }>({ | ||||
|     method: 'GET', | ||||
|     url: '/users/oauth2/discord/callback', | ||||
|     schema: getServiceSchema, | ||||
|     handler: async (request, reply) => { | ||||
|       const { redirectURI, code } = request.query | ||||
|       const discordUser = await getDiscordUserData( | ||||
|         code, | ||||
|         `${request.protocol}://${HOST}:${PORT}/users/oauth2/discord/callback?redirectURI=${redirectURI}` | ||||
|       ) | ||||
|       const responseJWT = await discordStrategy.callbackSignin({ | ||||
|         name: discordUser.username, | ||||
|         id: discordUser.id | ||||
|       }) | ||||
|       return await reply.redirect(buildQueryURL(redirectURI, responseJWT)) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/services/users/oauth2/discord/signin/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/services/users/oauth2/discord/signin/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
| import { FastifyPluginAsync, FastifySchema } from 'fastify' | ||||
|  | ||||
| import { HOST, PORT } from '../../../../../tools/configurations/index.js' | ||||
| import { fastifyErrors } from '../../../../../models/utils.js' | ||||
| import { DISCORD_BASE_URL, DISCORD_CLIENT_ID } from '../__utils__/utils.js' | ||||
|  | ||||
| const querySchema = Type.Object({ | ||||
|   redirectURI: Type.String({ format: 'uri-reference' }) | ||||
| }) | ||||
|  | ||||
| type QuerySchemaType = Static<typeof querySchema> | ||||
|  | ||||
| const getServiceSchema: FastifySchema = { | ||||
|   description: 'Discord OAuth2 - signin', | ||||
|   tags: ['users'] as string[], | ||||
|   querystring: querySchema, | ||||
|   response: { | ||||
|     200: Type.String(), | ||||
|     400: fastifyErrors[400], | ||||
|     500: fastifyErrors[500] | ||||
|   } | ||||
| } as const | ||||
|  | ||||
| export const getSigninDiscordOAuth2Service: FastifyPluginAsync = async ( | ||||
|   fastify | ||||
| ) => { | ||||
|   await fastify.route<{ | ||||
|     Querystring: QuerySchemaType | ||||
|   }>({ | ||||
|     method: 'GET', | ||||
|     url: '/users/oauth2/discord/signin', | ||||
|     schema: getServiceSchema, | ||||
|     handler: async (request, reply) => { | ||||
|       const { redirectURI } = request.query | ||||
|       const redirectCallback = `${request.protocol}://${HOST}:${PORT}/users/oauth2/discord/callback?redirectURI=${redirectURI}` | ||||
|       const url = `${DISCORD_BASE_URL}/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&scope=identify&response_type=code&redirect_uri=${redirectCallback}` | ||||
|       reply.statusCode = 200 | ||||
|       return url | ||||
|     } | ||||
|   }) | ||||
| } | ||||
							
								
								
									
										57
									
								
								src/services/users/oauth2/github/__utils__/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/services/users/oauth2/github/__utils__/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import querystring from 'node:querystring' | ||||
|  | ||||
| import axios from 'axios' | ||||
|  | ||||
| import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js' | ||||
|  | ||||
| export const GITHUB_PROVIDER = 'github' | ||||
| export const GITHUB_BASE_URL = 'https://github.com' | ||||
| export const GITHUB_API_BASE_URL = 'https://api.github.com' | ||||
| export const GITHUB_CLIENT_ID = | ||||
|   process.env.GITHUB_CLIENT_ID ?? 'GITHUB_CLIENT_ID' | ||||
| export const GITHUB_CLIENT_SECRET = | ||||
|   process.env.GITHUB_CLIENT_SECRET ?? 'GITHUB_CLIENT_SECRET' | ||||
| export const githubStrategy = new OAuthStrategy(GITHUB_PROVIDER) | ||||
|  | ||||
| export interface GitHubUser { | ||||
|   login: string | ||||
|   id: number | ||||
|   name: string | ||||
|   avatar_url: string | ||||
| } | ||||
|  | ||||
| export interface GitHubTokens { | ||||
|   access_token: string | ||||
|   scope: string | ||||
|   token_type: string | ||||
| } | ||||
|  | ||||
| export const getGitHubUserData = async ( | ||||
|   code: string, | ||||
|   redirectURI: string | ||||
| ): Promise<GitHubUser> => { | ||||
|   const { data: token } = await axios.post<GitHubTokens>( | ||||
|     `${GITHUB_BASE_URL}/login/oauth/access_token`, | ||||
|     querystring.stringify({ | ||||
|       client_id: GITHUB_CLIENT_ID, | ||||
|       client_secret: GITHUB_CLIENT_SECRET, | ||||
|       code, | ||||
|       redirect_uri: redirectURI | ||||
|     }), | ||||
|     { | ||||
|       headers: { | ||||
|         'Content-Type': 'application/x-www-form-urlencoded', | ||||
|         Accept: 'application/json' | ||||
|       } | ||||
|     } | ||||
|   ) | ||||
|   const { data: githubUser } = await axios.get<GitHubUser>( | ||||
|     `${GITHUB_API_BASE_URL}/user`, | ||||
|     { | ||||
|       headers: { | ||||
|         Authorization: `token ${token.access_token}` | ||||
|       } | ||||
|     } | ||||
|   ) | ||||
|   return githubUser | ||||
| } | ||||
							
								
								
									
										49
									
								
								src/services/users/oauth2/github/callback/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/services/users/oauth2/github/callback/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
| import { FastifyPluginAsync, FastifySchema } from 'fastify' | ||||
|  | ||||
| import { HOST, PORT } from '../../../../../tools/configurations/index.js' | ||||
| import { fastifyErrors } from '../../../../../models/utils.js' | ||||
| import { githubStrategy, getGitHubUserData } from '../__utils__/utils.js' | ||||
| import { buildQueryURL } from '../../../../../tools/utils/buildQueryURL.js' | ||||
|  | ||||
| const querySchema = Type.Object({ | ||||
|   code: Type.String(), | ||||
|   redirectURI: Type.String({ format: 'uri-reference' }) | ||||
| }) | ||||
|  | ||||
| type QuerySchemaType = Static<typeof querySchema> | ||||
|  | ||||
| const getServiceSchema: FastifySchema = { | ||||
|   description: 'GitHub OAuth2 - callback', | ||||
|   tags: ['users'] as string[], | ||||
|   querystring: querySchema, | ||||
|   response: { | ||||
|     200: Type.String(), | ||||
|     400: fastifyErrors[400], | ||||
|     500: fastifyErrors[500] | ||||
|   } | ||||
| } as const | ||||
|  | ||||
| export const getCallbackGitHubOAuth2Service: FastifyPluginAsync = async ( | ||||
|   fastify | ||||
| ) => { | ||||
|   await fastify.route<{ | ||||
|     Querystring: QuerySchemaType | ||||
|   }>({ | ||||
|     method: 'GET', | ||||
|     url: '/users/oauth2/github/callback', | ||||
|     schema: getServiceSchema, | ||||
|     handler: async (request, reply) => { | ||||
|       const { redirectURI, code } = request.query | ||||
|       const githubUser = await getGitHubUserData( | ||||
|         code, | ||||
|         `${request.protocol}://${HOST}:${PORT}/users/oauth2/github/callback?redirectURI=${redirectURI}` | ||||
|       ) | ||||
|       const responseJWT = await githubStrategy.callbackSignin({ | ||||
|         name: githubUser.name, | ||||
|         id: githubUser.id | ||||
|       }) | ||||
|       return await reply.redirect(buildQueryURL(redirectURI, responseJWT)) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/services/users/oauth2/github/signin/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/services/users/oauth2/github/signin/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
| import { FastifyPluginAsync, FastifySchema } from 'fastify' | ||||
|  | ||||
| import { HOST, PORT } from '../../../../../tools/configurations/index.js' | ||||
| import { fastifyErrors } from '../../../../../models/utils.js' | ||||
| import { GITHUB_BASE_URL, GITHUB_CLIENT_ID } from '../__utils__/utils.js' | ||||
|  | ||||
| const querySchema = Type.Object({ | ||||
|   redirectURI: Type.String({ format: 'uri-reference' }) | ||||
| }) | ||||
|  | ||||
| type QuerySchemaType = Static<typeof querySchema> | ||||
|  | ||||
| const getServiceSchema: FastifySchema = { | ||||
|   description: 'GitHub OAuth2 - signin', | ||||
|   tags: ['users'] as string[], | ||||
|   querystring: querySchema, | ||||
|   response: { | ||||
|     200: Type.String(), | ||||
|     400: fastifyErrors[400], | ||||
|     500: fastifyErrors[500] | ||||
|   } | ||||
| } as const | ||||
|  | ||||
| export const getSigninGitHubOAuth2Service: FastifyPluginAsync = async ( | ||||
|   fastify | ||||
| ) => { | ||||
|   await fastify.route<{ | ||||
|     Querystring: QuerySchemaType | ||||
|   }>({ | ||||
|     method: 'GET', | ||||
|     url: '/users/oauth2/github/signin', | ||||
|     schema: getServiceSchema, | ||||
|     handler: async (request, reply) => { | ||||
|       const { redirectURI } = request.query | ||||
|       const redirectCallback = `${request.protocol}://${HOST}:${PORT}/users/oauth2/github/callback?redirectURI=${redirectURI}` | ||||
|       const url = `${GITHUB_BASE_URL}/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${redirectCallback}` | ||||
|       reply.statusCode = 200 | ||||
|       return url | ||||
|     } | ||||
|   }) | ||||
| } | ||||
							
								
								
									
										59
									
								
								src/services/users/oauth2/google/__utils__/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/services/users/oauth2/google/__utils__/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| import querystring from 'node:querystring' | ||||
|  | ||||
| import axios from 'axios' | ||||
|  | ||||
| import { OAuthStrategy } from '../../../../../tools/utils/OAuthStrategy.js' | ||||
|  | ||||
| export const GOOGLE_PROVIDER = 'google' | ||||
| export const GOOGLE_BASE_URL = 'https://accounts.google.com/o/oauth2/v2/auth' | ||||
| export const GOOGLE_OAUTH2_TOKEN = 'https://oauth2.googleapis.com/token' | ||||
| export const GOOGLE_USERINFO = | ||||
|   'https://www.googleapis.com/oauth2/v1/userinfo?alt=json' | ||||
| export const GOOGLE_CLIENT_ID = | ||||
|   process.env.GOOGLE_CLIENT_ID ?? 'GOOGLE_CLIENT_ID' | ||||
| export const GOOGLE_CLIENT_SECRET = | ||||
|   process.env.GOOGLE_CLIENT_SECRET ?? 'GOOGLE_CLIENT_SECRET' | ||||
| export const googleStrategy = new OAuthStrategy(GOOGLE_PROVIDER) | ||||
|  | ||||
| export interface GoogleUser { | ||||
|   id: string | ||||
|   name: string | ||||
|   given_name: string | ||||
|   link: string | ||||
|   picture: string | ||||
|   locale: string | ||||
| } | ||||
|  | ||||
| export interface GoogleTokens { | ||||
|   access_token: string | ||||
|   expires_in: number | ||||
|   token_type: string | ||||
|   scope: string | ||||
|   refresh_token?: string | ||||
| } | ||||
|  | ||||
| export const getGoogleUserData = async ( | ||||
|   code: string, | ||||
|   redirectURI: string | ||||
| ): Promise<GoogleUser> => { | ||||
|   const { data: token } = await axios.post<GoogleTokens>( | ||||
|     GOOGLE_OAUTH2_TOKEN, | ||||
|     querystring.stringify({ | ||||
|       client_id: GOOGLE_CLIENT_ID, | ||||
|       client_secret: GOOGLE_CLIENT_SECRET, | ||||
|       code, | ||||
|       redirect_uri: redirectURI, | ||||
|       grant_type: 'authorization_code' | ||||
|     }), | ||||
|     { | ||||
|       headers: { | ||||
|         'Content-Type': 'application/x-www-form-urlencoded', | ||||
|         Accept: 'application/json' | ||||
|       } | ||||
|     } | ||||
|   ) | ||||
|   const { data: googleUser } = await axios.get<GoogleUser>( | ||||
|     `${GOOGLE_USERINFO}&access_token=${token.access_token}` | ||||
|   ) | ||||
|   return googleUser | ||||
| } | ||||
							
								
								
									
										49
									
								
								src/services/users/oauth2/google/callback/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/services/users/oauth2/google/callback/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
| import { FastifyPluginAsync, FastifySchema } from 'fastify' | ||||
|  | ||||
| import { HOST, PORT } from '../../../../../tools/configurations/index.js' | ||||
| import { fastifyErrors } from '../../../../../models/utils.js' | ||||
| import { googleStrategy, getGoogleUserData } from '../__utils__/utils.js' | ||||
| import { buildQueryURL } from '../../../../../tools/utils/buildQueryURL.js' | ||||
|  | ||||
| const querySchema = Type.Object({ | ||||
|   code: Type.String(), | ||||
|   redirectURI: Type.String({ format: 'uri-reference' }) | ||||
| }) | ||||
|  | ||||
| type QuerySchemaType = Static<typeof querySchema> | ||||
|  | ||||
| const getServiceSchema: FastifySchema = { | ||||
|   description: 'Google OAuth2 - callback', | ||||
|   tags: ['users'] as string[], | ||||
|   querystring: querySchema, | ||||
|   response: { | ||||
|     200: Type.String(), | ||||
|     400: fastifyErrors[400], | ||||
|     500: fastifyErrors[500] | ||||
|   } | ||||
| } as const | ||||
|  | ||||
| export const getCallbackGoogleOAuth2Service: FastifyPluginAsync = async ( | ||||
|   fastify | ||||
| ) => { | ||||
|   await fastify.route<{ | ||||
|     Querystring: QuerySchemaType | ||||
|   }>({ | ||||
|     method: 'GET', | ||||
|     url: '/users/oauth2/google/callback', | ||||
|     schema: getServiceSchema, | ||||
|     handler: async (request, reply) => { | ||||
|       const { redirectURI, code } = request.query | ||||
|       const googleUser = await getGoogleUserData( | ||||
|         code, | ||||
|         `${request.protocol}://${HOST}:${PORT}/users/oauth2/google/callback?redirectURI=${redirectURI}` | ||||
|       ) | ||||
|       const responseJWT = await googleStrategy.callbackSignin({ | ||||
|         name: googleUser.name, | ||||
|         id: googleUser.id | ||||
|       }) | ||||
|       return await reply.redirect(buildQueryURL(redirectURI, responseJWT)) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/services/users/oauth2/google/signin/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/services/users/oauth2/google/signin/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
| import { FastifyPluginAsync, FastifySchema } from 'fastify' | ||||
|  | ||||
| import { HOST, PORT } from '../../../../../tools/configurations/index.js' | ||||
| import { fastifyErrors } from '../../../../../models/utils.js' | ||||
| import { GOOGLE_BASE_URL, GOOGLE_CLIENT_ID } from '../__utils__/utils.js' | ||||
|  | ||||
| const querySchema = Type.Object({ | ||||
|   redirectURI: Type.String({ format: 'uri-reference' }) | ||||
| }) | ||||
|  | ||||
| type QuerySchemaType = Static<typeof querySchema> | ||||
|  | ||||
| const getServiceSchema: FastifySchema = { | ||||
|   description: 'Google OAuth2 - signin', | ||||
|   tags: ['users'] as string[], | ||||
|   querystring: querySchema, | ||||
|   response: { | ||||
|     200: Type.String(), | ||||
|     400: fastifyErrors[400], | ||||
|     500: fastifyErrors[500] | ||||
|   } | ||||
| } as const | ||||
|  | ||||
| export const getSigninGoogleOAuth2Service: FastifyPluginAsync = async ( | ||||
|   fastify | ||||
| ) => { | ||||
|   await fastify.route<{ | ||||
|     Querystring: QuerySchemaType | ||||
|   }>({ | ||||
|     method: 'GET', | ||||
|     url: '/users/oauth2/google/signin', | ||||
|     schema: getServiceSchema, | ||||
|     handler: async (request, reply) => { | ||||
|       const { redirectURI } = request.query | ||||
|       const redirectCallback = `${request.protocol}://${HOST}:${PORT}/users/oauth2/google/callback?redirectURI=${redirectURI}` | ||||
|       const url = `${GOOGLE_BASE_URL}?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${redirectCallback}&response_type=code&scope=profile&access_type=online` | ||||
|       reply.statusCode = 200 | ||||
|       return url | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| @@ -56,7 +56,7 @@ export const postRefreshTokenUser: FastifyPluginAsync = async (fastify) => { | ||||
|         ) as UserJWT | ||||
|         const accessToken = generateAccessToken({ | ||||
|           id: userJWT.id, | ||||
|           currentStrategy: 'local' | ||||
|           currentStrategy: userJWT.currentStrategy | ||||
|         }) | ||||
|         reply.statusCode = 200 | ||||
|         return { | ||||
|   | ||||
| @@ -19,7 +19,8 @@ export const swaggerOptions: FastifyDynamicSwaggerOptions = { | ||||
|       { name: 'guilds' }, | ||||
|       { name: 'channels' }, | ||||
|       { name: 'messages' }, | ||||
|       { name: 'members' } | ||||
|       { name: 'members' }, | ||||
|       { name: 'uploads' } | ||||
|     ], | ||||
|     components: { | ||||
|       securitySchemes: { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user