feat: add OAuth2 authentication (Google/GitHub/Discord)
This commit is contained in:
parent
9e6bf25c83
commit
91a0e2a76f
@ -22,15 +22,7 @@ exports.serviceGenerator = {
|
|||||||
type: 'list',
|
type: 'list',
|
||||||
name: 'tag',
|
name: 'tag',
|
||||||
message: 'tag',
|
message: 'tag',
|
||||||
choices: [
|
choices: ['users', 'guilds', 'channels', 'messages', 'members', 'uploads']
|
||||||
'users',
|
|
||||||
'guilds',
|
|
||||||
'channels',
|
|
||||||
'invitations',
|
|
||||||
'messages',
|
|
||||||
'members',
|
|
||||||
'uploads'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
|
@ -33,24 +33,25 @@ export const getCurrentUser: FastifyPluginAsync = async (fastify) => {
|
|||||||
if (request.user == null) {
|
if (request.user == null) {
|
||||||
throw fastify.httpErrors.forbidden()
|
throw fastify.httpErrors.forbidden()
|
||||||
}
|
}
|
||||||
|
const { user } = request
|
||||||
const settings = await prisma.userSetting.findFirst({
|
const settings = await prisma.userSetting.findFirst({
|
||||||
where: { userId: request.user.current.id }
|
where: { userId: user.current.id }
|
||||||
})
|
})
|
||||||
const OAuths = await prisma.oAuth.findMany({
|
const OAuths = await prisma.oAuth.findMany({
|
||||||
where: { userId: request.user.current.id }
|
where: { userId: user.current.id }
|
||||||
})
|
})
|
||||||
const strategies = OAuths.map((oauth) => {
|
const strategies = OAuths.map((oauth) => {
|
||||||
return oauth.provider
|
return oauth.provider
|
||||||
})
|
})
|
||||||
if (request.user.current.password != null) {
|
if (user.current.password != null) {
|
||||||
strategies.push('local')
|
strategies.push('local')
|
||||||
}
|
}
|
||||||
reply.statusCode = 200
|
reply.statusCode = 200
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
...request.user.current,
|
...user.current,
|
||||||
settings,
|
settings,
|
||||||
currentStrategy: request.user.currentStrategy,
|
currentStrategy: user.currentStrategy,
|
||||||
strategies
|
strategies
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,10 @@ export const putCurrentUser: FastifyPluginAsync = async (fastify) => {
|
|||||||
const { redirectURI } = request.params
|
const { redirectURI } = request.params
|
||||||
const userValidation = await prisma.user.findFirst({
|
const userValidation = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ email }, { name }],
|
OR: [
|
||||||
|
...(email != null ? [{ email }] : [{}]),
|
||||||
|
...(name != null ? [{ name }] : [{}])
|
||||||
|
],
|
||||||
AND: [{ id: { not: request.user.current.id } }]
|
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 { putCurrentUserSettings } from './current/settings/put.js'
|
||||||
import { getUserById } from './[userId]/get.js'
|
import { getUserById } from './[userId]/get.js'
|
||||||
import { putCurrentUserLogo } from './current/logo/put.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) => {
|
export const usersService: FastifyPluginAsync = async (fastify) => {
|
||||||
await fastify.register(postSignupUser)
|
await fastify.register(postSignupUser)
|
||||||
@ -28,4 +35,15 @@ export const usersService: FastifyPluginAsync = async (fastify) => {
|
|||||||
await fastify.register(putCurrentUserSettings)
|
await fastify.register(putCurrentUserSettings)
|
||||||
await fastify.register(putCurrentUserLogo)
|
await fastify.register(putCurrentUserLogo)
|
||||||
await fastify.register(getUserById)
|
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
|
) as UserJWT
|
||||||
const accessToken = generateAccessToken({
|
const accessToken = generateAccessToken({
|
||||||
id: userJWT.id,
|
id: userJWT.id,
|
||||||
currentStrategy: 'local'
|
currentStrategy: userJWT.currentStrategy
|
||||||
})
|
})
|
||||||
reply.statusCode = 200
|
reply.statusCode = 200
|
||||||
return {
|
return {
|
||||||
|
@ -19,7 +19,8 @@ export const swaggerOptions: FastifyDynamicSwaggerOptions = {
|
|||||||
{ name: 'guilds' },
|
{ name: 'guilds' },
|
||||||
{ name: 'channels' },
|
{ name: 'channels' },
|
||||||
{ name: 'messages' },
|
{ name: 'messages' },
|
||||||
{ name: 'members' }
|
{ name: 'members' },
|
||||||
|
{ name: 'uploads' }
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
|
Reference in New Issue
Block a user