feat: migrate from express to fastify
This commit is contained in:
@ -1,18 +0,0 @@
|
||||
/users/{userId}:
|
||||
get:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'GET the user information with its id'
|
||||
parameters:
|
||||
- name: 'userId'
|
||||
in: 'path'
|
||||
required: true
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/NotFoundError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/User'
|
@ -1,41 +1,20 @@
|
||||
import request from 'supertest'
|
||||
import { application } from '../../../../application.js'
|
||||
import { userExample } from '../../../../models/User.js'
|
||||
import { userSettingsExample } from '../../../../models/UserSettings.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import { formatErrors } from '../../../../__test__/utils/formatErrors'
|
||||
import application from '../../../../application'
|
||||
|
||||
describe('GET /users/:userId', () => {
|
||||
it('should returns the user without the email', async () => {
|
||||
const { userId } = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.get(`/users/${userId}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
expect(response.body.user.email).toBeUndefined()
|
||||
expect(response.body.user.id).toEqual(userId)
|
||||
})
|
||||
|
||||
it('should returns the user with the email', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
await request(application)
|
||||
.put('/users/current/settings')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ isPublicEmail: true })
|
||||
.expect(200)
|
||||
const response = await request(application)
|
||||
.get(`/users/${userToken.userId}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
expect(response.body.user.email).not.toBeNull()
|
||||
expect(response.body.user.id).toEqual(userToken.userId)
|
||||
})
|
||||
|
||||
it("should returns 404 error if the user doesn't exist", async () => {
|
||||
const response = await request(application).get('/users/1').send().expect(404)
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors.length).toEqual(1)
|
||||
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
|
||||
describe('GET /users/[userId]', () => {
|
||||
it('succeeds', async () => {
|
||||
prismaMock.guild.findMany.mockResolvedValue([])
|
||||
prismaMock.user.findUnique.mockResolvedValue(userExample)
|
||||
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
|
||||
const response = await application.inject({
|
||||
method: 'GET',
|
||||
url: `/users/${userExample.id}`
|
||||
})
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(200)
|
||||
expect(responseJson.user.id).toEqual(userExample.id)
|
||||
expect(responseJson.user.name).toEqual(userExample.name)
|
||||
})
|
||||
})
|
||||
|
@ -1,30 +1,84 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
|
||||
import User from '../../../models/User'
|
||||
import UserSetting from '../../../models/UserSetting'
|
||||
import { NotFoundError } from '../../../tools/errors/NotFoundError'
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import { userPublicSchema } from '../../../models/User.js'
|
||||
import { guildSchema } from '../../../models/Guild.js'
|
||||
|
||||
export const getUsersRouter = Router()
|
||||
const parametersGetUserSchema = Type.Object({
|
||||
userId: userPublicSchema.id
|
||||
})
|
||||
|
||||
getUsersRouter.get(
|
||||
'/users/:userId',
|
||||
[],
|
||||
async (req: Request, res: Response) => {
|
||||
const { userId } = req.params as { userId: string }
|
||||
const user = await User.findOne({ where: { id: userId } })
|
||||
if (user == null) {
|
||||
throw new NotFoundError()
|
||||
}
|
||||
const userSettings = await UserSetting.findOne({
|
||||
where: { userId: user.id }
|
||||
})
|
||||
if (userSettings == null) {
|
||||
throw new NotFoundError()
|
||||
}
|
||||
const result = Object.assign({}, user.toJSON())
|
||||
if (!userSettings.isPublicEmail) {
|
||||
delete result.email
|
||||
}
|
||||
return res.status(200).json({ user: result })
|
||||
export type ParametersGetUser = Static<typeof parametersGetUserSchema>
|
||||
|
||||
const getServiceSchema: FastifySchema = {
|
||||
description: 'GET the public user informations with its id',
|
||||
tags: ['users'] as string[],
|
||||
params: parametersGetUserSchema,
|
||||
response: {
|
||||
200: Type.Object({
|
||||
user: Type.Object(userPublicSchema),
|
||||
guilds: Type.Union([Type.Array(Type.Object(guildSchema)), Type.Null()])
|
||||
}),
|
||||
400: fastifyErrors[400],
|
||||
404: fastifyErrors[404],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
)
|
||||
} as const
|
||||
|
||||
export const getUserById: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.route<{
|
||||
Params: ParametersGetUser
|
||||
}>({
|
||||
method: 'GET',
|
||||
url: '/users/:userId',
|
||||
schema: getServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { userId } = request.params
|
||||
const settings = await prisma.userSetting.findFirst({
|
||||
where: { userId }
|
||||
})
|
||||
if (settings == null) {
|
||||
throw fastify.httpErrors.notFound('User not found')
|
||||
}
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: settings.isPublicEmail,
|
||||
logo: true,
|
||||
status: true,
|
||||
biography: true,
|
||||
website: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
}
|
||||
})
|
||||
if (user == null) {
|
||||
throw fastify.httpErrors.notFound('User not found')
|
||||
}
|
||||
reply.statusCode = 200
|
||||
return {
|
||||
user: {
|
||||
...user,
|
||||
settings
|
||||
},
|
||||
guilds: !settings.isPublicGuilds
|
||||
? null
|
||||
: await prisma.guild.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { getUsersRouter } from './get'
|
||||
|
||||
export const usersGetByIdRouter = Router()
|
||||
|
||||
usersGetByIdRouter.use('/', getUsersRouter)
|
@ -1,99 +0,0 @@
|
||||
definitions:
|
||||
User:
|
||||
type: 'object'
|
||||
properties:
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: 'integer'
|
||||
description: 'Unique id'
|
||||
name:
|
||||
type: 'string'
|
||||
description: 'Unique name'
|
||||
email:
|
||||
type: 'string'
|
||||
description: 'Unique email address'
|
||||
status:
|
||||
type: 'string'
|
||||
biography:
|
||||
type: 'string'
|
||||
logo:
|
||||
type: 'string'
|
||||
isConfirmed:
|
||||
type: 'boolean'
|
||||
createdAt:
|
||||
type: 'string'
|
||||
format: 'date-time'
|
||||
updatedAt:
|
||||
type: 'string'
|
||||
format: 'date-time'
|
||||
|
||||
Language:
|
||||
type: 'string'
|
||||
enum: ['en', 'fr']
|
||||
default: 'en'
|
||||
|
||||
Theme:
|
||||
type: 'string'
|
||||
enum: ['dark', 'light']
|
||||
default: 'dark'
|
||||
|
||||
UserSettings:
|
||||
type: 'object'
|
||||
properties:
|
||||
language:
|
||||
allOf:
|
||||
- $ref: '#/definitions/Language'
|
||||
theme:
|
||||
allOf:
|
||||
- $ref: '#/definitions/Theme'
|
||||
isPublicEmail:
|
||||
type: 'boolean'
|
||||
|
||||
UserSettingsObject:
|
||||
type: 'object'
|
||||
properties:
|
||||
settings:
|
||||
allOf:
|
||||
- $ref: '#/definitions/UserSettings'
|
||||
|
||||
AuthenticationStrategy:
|
||||
type: 'string'
|
||||
enum: ['local', 'google', 'github', 'discord']
|
||||
|
||||
UserStrategies:
|
||||
type: 'object'
|
||||
properties:
|
||||
strategies:
|
||||
type: 'array'
|
||||
items:
|
||||
allOf:
|
||||
- $ref: '#/definitions/AuthenticationStrategy'
|
||||
|
||||
UserCurrentStrategy:
|
||||
type: 'object'
|
||||
properties:
|
||||
currentStrategy:
|
||||
allOf:
|
||||
- $ref: '#/definitions/AuthenticationStrategy'
|
||||
|
||||
AccessTokenResponse:
|
||||
type: 'object'
|
||||
properties:
|
||||
accessToken:
|
||||
type: 'string'
|
||||
expiresIn:
|
||||
type: 'number'
|
||||
description: 'expiresIn is how long, in milliseconds, until the returned accessToken expires'
|
||||
type:
|
||||
type: 'string'
|
||||
enum: ['Bearer']
|
||||
|
||||
RefreshTokenResponse:
|
||||
allOf:
|
||||
- $ref: '#/definitions/AccessTokenResponse'
|
||||
- type: 'object'
|
||||
properties:
|
||||
refreshToken:
|
||||
type: 'string'
|
@ -1,89 +0,0 @@
|
||||
import OAuth, { ProviderOAuth } from '../../../models/OAuth'
|
||||
import User, { UserRequest } from '../../../models/User'
|
||||
import UserSetting from '../../../models/UserSetting'
|
||||
import {
|
||||
expiresIn,
|
||||
generateAccessToken,
|
||||
generateRefreshToken,
|
||||
ResponseJWT
|
||||
} from '../../../tools/configurations/jwtToken'
|
||||
|
||||
interface ProviderData {
|
||||
name: string
|
||||
id: number | string
|
||||
}
|
||||
|
||||
type ResponseCallbackAddStrategy =
|
||||
| 'success'
|
||||
| 'This account is already used by someone else'
|
||||
| 'You are already using this account'
|
||||
|
||||
export class OAuthStrategy {
|
||||
constructor (public provider: ProviderOAuth) {}
|
||||
|
||||
async callbackAddStrategy (
|
||||
providerData: ProviderData,
|
||||
userRequest: UserRequest
|
||||
): Promise<ResponseCallbackAddStrategy> {
|
||||
const OAuthUser = await OAuth.findOne({
|
||||
where: { providerId: providerData.id, provider: this.provider }
|
||||
})
|
||||
let message: ResponseCallbackAddStrategy = 'success'
|
||||
|
||||
if (OAuthUser == null) {
|
||||
await OAuth.create({
|
||||
provider: this.provider,
|
||||
providerId: providerData.id,
|
||||
userId: userRequest.current.id
|
||||
})
|
||||
} else if (OAuthUser.userId !== userRequest.current.id) {
|
||||
message = 'This account is already used by someone else'
|
||||
} else {
|
||||
message = 'You are already using this account'
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
async callbackSignin (providerData: ProviderData): Promise<ResponseJWT> {
|
||||
const OAuthUser = await OAuth.findOne({
|
||||
where: { providerId: providerData.id, provider: this.provider }
|
||||
})
|
||||
let userId: number = OAuthUser?.userId ?? 0
|
||||
if (OAuthUser == null) {
|
||||
let name = providerData.name
|
||||
let isAlreadyUsedName = true
|
||||
let countId: string | number = providerData.id
|
||||
while (isAlreadyUsedName) {
|
||||
const foundUsers = await User.count({ where: { name } })
|
||||
isAlreadyUsedName = foundUsers > 0
|
||||
if (isAlreadyUsedName) {
|
||||
name = `${name}-${countId}`
|
||||
countId = Math.random() * Date.now()
|
||||
}
|
||||
}
|
||||
const user = await User.create({ name })
|
||||
await UserSetting.create({ userId: user.id })
|
||||
userId = user.id
|
||||
await OAuth.create({
|
||||
provider: this.provider,
|
||||
providerId: providerData.id,
|
||||
userId: user.id
|
||||
})
|
||||
}
|
||||
const accessToken = generateAccessToken({
|
||||
currentStrategy: this.provider,
|
||||
id: userId
|
||||
})
|
||||
const refreshToken = await generateRefreshToken({
|
||||
currentStrategy: this.provider,
|
||||
id: userId
|
||||
})
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
type: 'Bearer'
|
||||
}
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
import { OAuthStrategy } from '../OAuthStrategy'
|
||||
import OAuth from '../../../../models/OAuth'
|
||||
import User from '../../../../models/User'
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import RefreshToken from '../../../../models/RefreshToken'
|
||||
|
||||
const oauthStrategy = new OAuthStrategy('discord')
|
||||
|
||||
describe('/users/utils/OAuthStrategy - callbackSignin', () => {
|
||||
it('should signup the user', async () => {
|
||||
let users = await User.findAll()
|
||||
let oauths = await OAuth.findAll()
|
||||
expect(users.length).toEqual(0)
|
||||
expect(oauths.length).toEqual(0)
|
||||
const name = 'Martin'
|
||||
const id = '12345'
|
||||
await oauthStrategy.callbackSignin({ id, name })
|
||||
users = await User.findAll()
|
||||
oauths = await OAuth.findAll()
|
||||
expect(users.length).toEqual(1)
|
||||
expect(oauths.length).toEqual(1)
|
||||
expect(users[0].name).toEqual(name)
|
||||
expect(oauths[0].providerId).toEqual(id)
|
||||
expect(oauths[0].provider).toEqual(oauthStrategy.provider)
|
||||
})
|
||||
|
||||
it('should signup the user and generate a new name when already used', async () => {
|
||||
const oauths = await OAuth.findAll()
|
||||
expect(oauths.length).toEqual(0)
|
||||
const name = 'Martin'
|
||||
const id = '1234'
|
||||
await authenticateUserTest({
|
||||
name,
|
||||
shouldBeConfirmed: true,
|
||||
email: 'martin@example.com',
|
||||
password: 'password'
|
||||
})
|
||||
await oauthStrategy.callbackSignin({ id, name })
|
||||
const oauth = await OAuth.findOne({ where: { providerId: id } })
|
||||
expect(oauth?.provider).toEqual(oauthStrategy.provider)
|
||||
expect(oauth?.providerId).toEqual(id)
|
||||
expect(oauth?.userId).toEqual(2)
|
||||
const user = await User.findByPk(oauth?.userId)
|
||||
expect(user?.name.startsWith(name)).toBeTruthy()
|
||||
expect(user?.name).not.toEqual(name)
|
||||
})
|
||||
|
||||
it('should signin the user if already connected with the provider', async () => {
|
||||
const name = 'Martin'
|
||||
const id = '1234'
|
||||
await oauthStrategy.callbackSignin({ id, name })
|
||||
let oauths = await OAuth.findAll()
|
||||
expect(oauths.length).toEqual(1)
|
||||
await oauthStrategy.callbackSignin({ id, name })
|
||||
oauths = await OAuth.findAll()
|
||||
expect(oauths.length).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('/users/utils/OAuthStrategy - callbackAddStrategy', () => {
|
||||
it('should add the strategy', async () => {
|
||||
const userTokens = await authenticateUserTest()
|
||||
const user = await User.findOne({ where: { id: userTokens.userId } })
|
||||
expect(user).not.toBeNull()
|
||||
if (user != null) {
|
||||
const result = await oauthStrategy.callbackAddStrategy(
|
||||
{ name: user.name, id: '1234' },
|
||||
{
|
||||
current: user,
|
||||
accessToken: userTokens.accessToken,
|
||||
currentStrategy: 'local'
|
||||
}
|
||||
)
|
||||
expect(result).toEqual('success')
|
||||
}
|
||||
})
|
||||
|
||||
it('should not add the strategy if the account of the provider is already used', async () => {
|
||||
const userTokens = await authenticateUserTest()
|
||||
const user = await User.findOne({ where: { id: userTokens.userId } })
|
||||
const name = 'Martin'
|
||||
const id = '1234'
|
||||
await oauthStrategy.callbackSignin({ id, name })
|
||||
expect(user).not.toBeNull()
|
||||
if (user != null) {
|
||||
const result = await oauthStrategy.callbackAddStrategy(
|
||||
{ name: user.name, id: '1234' },
|
||||
{
|
||||
current: user,
|
||||
accessToken: userTokens.accessToken,
|
||||
currentStrategy: 'local'
|
||||
}
|
||||
)
|
||||
expect(result).toEqual('This account is already used by someone else')
|
||||
}
|
||||
})
|
||||
|
||||
it('should not add the strategy if the user is already connected with it', async () => {
|
||||
const name = 'Martin'
|
||||
const id = '1234'
|
||||
const userTokens = await oauthStrategy.callbackSignin({ id, name })
|
||||
const refreshToken = await RefreshToken.findOne({
|
||||
where: { token: userTokens.refreshToken as string },
|
||||
include: [{ model: User }]
|
||||
})
|
||||
expect(refreshToken).not.toBeNull()
|
||||
if (refreshToken != null) {
|
||||
const result = await oauthStrategy.callbackAddStrategy(
|
||||
{ name: refreshToken.user.name, id: '1234' },
|
||||
{
|
||||
current: refreshToken.user,
|
||||
accessToken: userTokens.accessToken,
|
||||
currentStrategy: oauthStrategy.provider
|
||||
}
|
||||
)
|
||||
expect(result).toEqual('You are already using this account')
|
||||
}
|
||||
})
|
||||
})
|
@ -1,20 +0,0 @@
|
||||
import { buildQueryURL } from '../buildQueryURL'
|
||||
|
||||
test('controllers/users/utils/buildQueryUrl', () => {
|
||||
expect(
|
||||
buildQueryURL('http://localhost:8080', {
|
||||
test: 'query'
|
||||
})
|
||||
).toEqual('http://localhost:8080/?test=query')
|
||||
expect(
|
||||
buildQueryURL('http://localhost:8080/', {
|
||||
test: 'query'
|
||||
})
|
||||
).toEqual('http://localhost:8080/?test=query')
|
||||
expect(
|
||||
buildQueryURL('http://localhost:3000', {
|
||||
test: 'query',
|
||||
code: 'abc'
|
||||
})
|
||||
).toEqual('http://localhost:3000/?test=query&code=abc')
|
||||
})
|
@ -1,12 +0,0 @@
|
||||
import { ObjectAny } from '../../../typings/utils'
|
||||
|
||||
export const buildQueryURL = (
|
||||
baseURL: string,
|
||||
queryObject: ObjectAny
|
||||
): string => {
|
||||
const url = new URL(baseURL)
|
||||
Object.entries(queryObject).forEach(([query, value]) => {
|
||||
url.searchParams.append(query, value)
|
||||
})
|
||||
return url.href
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import RefreshToken from '../../../models/RefreshToken'
|
||||
|
||||
export const deleteEveryRefreshTokens = async (
|
||||
userId: number
|
||||
): Promise<void> => {
|
||||
const refreshTokens = await RefreshToken.findAll({
|
||||
where: { userId }
|
||||
})
|
||||
for (const refreshToken of refreshTokens) {
|
||||
await refreshToken.destroy()
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/users/addLocalStrategy:
|
||||
post:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Allows a user to add the local strategy.'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
email:
|
||||
type: 'string'
|
||||
format: 'email'
|
||||
password:
|
||||
type: 'string'
|
||||
format: 'password'
|
||||
example: 'password'
|
||||
required:
|
||||
- 'email'
|
||||
- 'password'
|
||||
parameters:
|
||||
- name: 'redirectURI'
|
||||
description: 'The redirect URI to redirect the user when he successfuly confirm his email (could be a signin page), if not provided it will redirect the user to a simple page with a message to tell the user he can now signin.'
|
||||
in: 'query'
|
||||
required: false
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- $ref: '#/definitions/UnauthorizedError'
|
||||
- $ref: '#/definitions/ForbiddenError'
|
||||
- '201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
@ -1,76 +0,0 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import application from '../../../../application'
|
||||
import User from '../../../../models/User'
|
||||
import { generateAccessToken } from '../../../../tools/configurations/jwtToken'
|
||||
|
||||
describe('POST /users/addLocalStrategy', () => {
|
||||
it('succeeds and add local strategy', async () => {
|
||||
const user = await User.create({ name: 'John' })
|
||||
const accessToken = generateAccessToken({
|
||||
currentStrategy: 'github',
|
||||
id: user.id
|
||||
})
|
||||
const email = 'johndoe@example.com'
|
||||
const response = await request(application)
|
||||
.post('/users/addLocalStrategy')
|
||||
.send({
|
||||
email,
|
||||
password: 'password'
|
||||
})
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(201)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
expect(response.body.user.id).toEqual(user.id)
|
||||
expect(response.body.user.email).toEqual(email)
|
||||
})
|
||||
|
||||
it('fails if the user is already using local strategy', async () => {
|
||||
const user = await User.create({ name: 'John' })
|
||||
const accessToken = generateAccessToken({
|
||||
currentStrategy: 'local',
|
||||
id: user.id
|
||||
})
|
||||
const email = 'johndoe@example.com'
|
||||
const response = await request(application)
|
||||
.post('/users/addLocalStrategy')
|
||||
.send({
|
||||
email,
|
||||
password: 'password'
|
||||
})
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('fails with invalid email', async () => {
|
||||
const user = await User.create({ name: 'John' })
|
||||
const accessToken = generateAccessToken({
|
||||
currentStrategy: 'local',
|
||||
id: user.id
|
||||
})
|
||||
const email = 'johndoecom'
|
||||
const response = await request(application)
|
||||
.post('/users/addLocalStrategy')
|
||||
.send({
|
||||
email,
|
||||
password: 'password'
|
||||
})
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('fails if the user is not connected', async () => {
|
||||
const email = 'johndoecom'
|
||||
const response = await request(application)
|
||||
.post('/users/addLocalStrategy')
|
||||
.send({
|
||||
email,
|
||||
password: 'password'
|
||||
})
|
||||
.set('Authorization', 'Bearer token')
|
||||
.expect(403)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
})
|
||||
})
|
@ -1,79 +0,0 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
|
||||
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import UserSetting from '../../../models/UserSetting'
|
||||
import { sendEmail } from '../../../tools/email/sendEmail'
|
||||
import { BadRequestError } from '../../../tools/errors/BadRequestError'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { alreadyUsedValidation } from '../../../tools/validations/alreadyUsedValidation'
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
alreadyConnected: 'You are already connected with this email address'
|
||||
}
|
||||
}
|
||||
|
||||
export const addLocalStrategyRouter = Router()
|
||||
|
||||
addLocalStrategyRouter.post(
|
||||
'/users/addLocalStrategy',
|
||||
authenticateUser,
|
||||
[
|
||||
body('email')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.isEmail()
|
||||
.withMessage(errorsMessages.email.mustBeValid)
|
||||
.custom(async (email: string, meta) => {
|
||||
if (email === meta.req.user?.current.email) {
|
||||
return await Promise.reject(
|
||||
new Error(errorsMessages.email.alreadyConnected)
|
||||
)
|
||||
}
|
||||
return await alreadyUsedValidation(User, 'email', email)
|
||||
}),
|
||||
body('password').trim().notEmpty().isString(),
|
||||
query('redirectURI').optional({ nullable: true }).trim()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
}
|
||||
const user = req.user.current
|
||||
const { email, password } = req.body as {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
const { redirectURI } = req.query as { redirectURI?: string }
|
||||
if (req.user.currentStrategy === 'local' || user.password != null) {
|
||||
throw new BadRequestError('You are already using local strategy')
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 12)
|
||||
const tempToken = uuidv4()
|
||||
user.email = email
|
||||
user.password = hashedPassword
|
||||
user.tempToken = tempToken
|
||||
user.isConfirmed = false
|
||||
await user.save()
|
||||
const userSettings = await UserSetting.findOne({
|
||||
where: { userId: user.id }
|
||||
})
|
||||
const redirectQuery =
|
||||
redirectURI != null ? `&redirectURI=${redirectURI}` : ''
|
||||
await sendEmail({
|
||||
type: 'confirm-email',
|
||||
email,
|
||||
url: `${process.env.API_BASE_URL}/users/confirmEmail?tempToken=${tempToken}${redirectQuery}`,
|
||||
language: userSettings?.language,
|
||||
theme: userSettings?.theme
|
||||
})
|
||||
return res.status(201).json({ user })
|
||||
}
|
||||
)
|
39
src/services/users/confirm-email/__test__/get.test.ts
Normal file
39
src/services/users/confirm-email/__test__/get.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { application } from '../../../../application.js'
|
||||
import { userExample } from '../../../../models/User.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
|
||||
describe('GET /users/confirm-email', () => {
|
||||
it('should succeeds', async () => {
|
||||
prismaMock.user.findFirst.mockResolvedValue(userExample)
|
||||
prismaMock.user.update.mockResolvedValue({
|
||||
...userExample,
|
||||
isConfirmed: true,
|
||||
temporaryToken: null
|
||||
})
|
||||
const response = await application.inject({
|
||||
method: 'GET',
|
||||
url: '/users/confirm-email',
|
||||
query: {
|
||||
temporaryToken: userExample.temporaryToken ?? ''
|
||||
}
|
||||
})
|
||||
expect(response.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('should fails with invalid `temporaryToken`', async () => {
|
||||
prismaMock.user.findFirst.mockResolvedValue(null)
|
||||
prismaMock.user.update.mockResolvedValue({
|
||||
...userExample,
|
||||
isConfirmed: true,
|
||||
temporaryToken: null
|
||||
})
|
||||
const response = await application.inject({
|
||||
method: 'GET',
|
||||
url: '/users/confirm-email',
|
||||
query: {
|
||||
temporaryToken: userExample.temporaryToken ?? ''
|
||||
}
|
||||
})
|
||||
expect(response.statusCode).toEqual(403)
|
||||
})
|
||||
})
|
59
src/services/users/confirm-email/get.ts
Normal file
59
src/services/users/confirm-email/get.ts
Normal file
@ -0,0 +1,59 @@
|
||||
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 { userSchema } from '../../../models/User.js'
|
||||
|
||||
const queryGetConfirmEmailSchema = Type.Object({
|
||||
redirectURI: Type.Optional(Type.String({ format: 'uri-reference' })),
|
||||
temporaryToken: userSchema.temporaryToken
|
||||
})
|
||||
|
||||
type QueryGetConfirmEmailSchemaType = Static<typeof queryGetConfirmEmailSchema>
|
||||
|
||||
const getConfirmEmailSchema: FastifySchema = {
|
||||
description: 'Confirm the account of the user.',
|
||||
tags: ['users'] as string[],
|
||||
querystring: queryGetConfirmEmailSchema,
|
||||
response: {
|
||||
200: Type.String(),
|
||||
400: fastifyErrors[400],
|
||||
403: fastifyErrors[403],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const getConfirmEmail: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.route<{
|
||||
Querystring: QueryGetConfirmEmailSchemaType
|
||||
}>({
|
||||
method: 'GET',
|
||||
url: '/users/confirm-email',
|
||||
schema: getConfirmEmailSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { redirectURI, temporaryToken } = request.query
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
temporaryToken,
|
||||
isConfirmed: false
|
||||
}
|
||||
})
|
||||
if (user == null) {
|
||||
throw fastify.httpErrors.forbidden()
|
||||
}
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
temporaryToken: null,
|
||||
isConfirmed: true
|
||||
}
|
||||
})
|
||||
if (redirectURI == null) {
|
||||
reply.statusCode = 200
|
||||
return 'Success, your email has been confirmed, you can now signin!'
|
||||
}
|
||||
await reply.redirect(redirectURI)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/users/confirmEmail:
|
||||
get:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Confirm the account of the user'
|
||||
parameters:
|
||||
- name: 'tempToken'
|
||||
in: 'query'
|
||||
required: true
|
||||
- name: 'redirectURI'
|
||||
description: 'The redirect URI to redirect the user when he successfuly confirm his email.'
|
||||
in: 'query'
|
||||
required: false
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- $ref: '#/definitions/ForbiddenError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/definitions/AccessTokenResponse'
|
@ -1,43 +0,0 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import User from '../../../../models/User'
|
||||
|
||||
describe('GET /users/confirmEmail', () => {
|
||||
it('succeeds and confirm the user', async () => {
|
||||
const name = 'John'
|
||||
await authenticateUserTest({
|
||||
name,
|
||||
email: 'contact@john.com',
|
||||
shouldBeConfirmed: false
|
||||
})
|
||||
|
||||
const user = await User.findOne({ where: { name } })
|
||||
expect(user).not.toBeNull()
|
||||
expect(user?.isConfirmed).toBe(false)
|
||||
await request(application)
|
||||
.get(`/users/confirmEmail?tempToken=${user?.tempToken as string}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
const foundUser = await User.findOne({ where: { name } })
|
||||
expect(foundUser).not.toBeNull()
|
||||
expect(foundUser?.isConfirmed).toBe(true)
|
||||
expect(foundUser?.tempToken).toBe(null)
|
||||
})
|
||||
|
||||
it('fails with invalid tempToken', async () => {
|
||||
await request(application)
|
||||
.get('/users/confirmEmail?tempToken=mybadtoken')
|
||||
.send()
|
||||
.expect(403)
|
||||
})
|
||||
|
||||
it('fails with empty tempToken', async () => {
|
||||
await request(application)
|
||||
.get('/users/confirmEmail')
|
||||
.send()
|
||||
.expect(400)
|
||||
})
|
||||
})
|
@ -1,48 +0,0 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { query } from 'express-validator'
|
||||
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { isValidRedirectURIValidation } from '../../../tools/validations/isValidRedirectURIValidation'
|
||||
|
||||
export const confirmEmailRouter = Router()
|
||||
|
||||
confirmEmailRouter.get(
|
||||
'/users/confirmEmail',
|
||||
[
|
||||
query('tempToken')
|
||||
.trim()
|
||||
.notEmpty(),
|
||||
query('redirectURI')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { tempToken, redirectURI } = req.query as {
|
||||
tempToken: string
|
||||
redirectURI?: string
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { tempToken, isConfirmed: false }
|
||||
})
|
||||
if (user == null) {
|
||||
throw new ForbiddenError()
|
||||
}
|
||||
|
||||
user.tempToken = null
|
||||
user.isConfirmed = true
|
||||
await user.save()
|
||||
|
||||
if (redirectURI == null) {
|
||||
return res
|
||||
.status(200)
|
||||
.json('Success, your email has been confirmed, you can now signin!')
|
||||
}
|
||||
|
||||
return res.redirect(redirectURI)
|
||||
}
|
||||
)
|
@ -1,21 +0,0 @@
|
||||
/users/current:
|
||||
get:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'GET the current connected user'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- $ref: '#/definitions/UnauthorizedError'
|
||||
- $ref: '#/definitions/ForbiddenError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/User'
|
||||
- $ref: '#/definitions/UserSettingsObject'
|
||||
- $ref: '#/definitions/UserCurrentStrategy'
|
||||
- $ref: '#/definitions/UserStrategies'
|
@ -1,45 +0,0 @@
|
||||
/users/current:
|
||||
put:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Edit the current connected user info'
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
email:
|
||||
type: 'string'
|
||||
format: 'email'
|
||||
name:
|
||||
type: 'string'
|
||||
minLength: 3
|
||||
maxLength: 30
|
||||
example: 'user'
|
||||
biography:
|
||||
type: 'string'
|
||||
maxLength: 160
|
||||
example: 'biography'
|
||||
status:
|
||||
type: 'string'
|
||||
maxLength: 100
|
||||
example: '👀 Working on secrets projects...'
|
||||
logo:
|
||||
type: 'string'
|
||||
format: 'binary'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- $ref: '#/definitions/UnauthorizedError'
|
||||
- $ref: '#/definitions/ForbiddenError'
|
||||
- $ref: '#/definitions/PayloadTooLargeError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/User'
|
||||
- $ref: '#/definitions/UserCurrentStrategy'
|
@ -1,46 +1,29 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import { application } from '../../../../application.js'
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
|
||||
|
||||
describe('GET /users/current', () => {
|
||||
it('succeeds with valid Bearer accessToken', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.get('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
expect(response.body.settings).not.toBeNull()
|
||||
expect(response.body.currentStrategy).toEqual('local')
|
||||
expect(Array.isArray(response.body.strategies)).toBeTruthy()
|
||||
expect(response.body.strategies.includes('local')).toBeTruthy()
|
||||
it('succeeds', async () => {
|
||||
const { accessToken, user } = await authenticateUserTest()
|
||||
const response = await application.inject({
|
||||
method: 'GET',
|
||||
url: '/users/current',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(200)
|
||||
expect(responseJson.user.name).toEqual(user.name)
|
||||
expect(responseJson.user.strategies).toEqual(
|
||||
expect.arrayContaining(['local'])
|
||||
)
|
||||
})
|
||||
|
||||
it('fails with unconfirmed account', async () => {
|
||||
const userToken = await authenticateUserTest({ shouldBeConfirmed: false })
|
||||
const response = await request(application)
|
||||
.get('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(401)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('fails ForbiddenError with invalid Bearer accessToken', async () => {
|
||||
await request(application)
|
||||
.get('/users/current')
|
||||
.set('Authorization', 'Bearer invalidtoken')
|
||||
.send()
|
||||
.expect(403)
|
||||
})
|
||||
|
||||
it('fails NotAuthorizedError with invalid accessToken', async () => {
|
||||
await request(application)
|
||||
.get('/users/current')
|
||||
.set('Authorization', 'invalidtoken')
|
||||
.send()
|
||||
.expect(401)
|
||||
it('fails with unauthenticated user', async () => {
|
||||
const response = await application.inject({
|
||||
method: 'GET',
|
||||
url: '/users/current'
|
||||
})
|
||||
expect(response.statusCode).toEqual(401)
|
||||
})
|
||||
})
|
||||
|
@ -1,228 +1,107 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import { formatErrors } from '../../../../__test__/utils/formatErrors'
|
||||
import application from '../../../../application'
|
||||
import User from '../../../../models/User'
|
||||
import { commonErrorsMessages } from '../../../../tools/configurations/constants'
|
||||
import { randomString } from '../../../../tools/utils/random'
|
||||
import { errorsMessages } from '../index'
|
||||
import { application } from '../../../../application.js'
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
import { userExample } from '../../../../models/User.js'
|
||||
|
||||
describe('PUT /users/current', () => {
|
||||
it('succeeds with valid accessToken, valid email and valid name', async () => {
|
||||
const name = 'test2'
|
||||
const email = 'test2@test2.com'
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ name, email })
|
||||
.expect(200)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
expect(response.body.user.name).toBe(name)
|
||||
expect(response.body.user.email).toBe(email)
|
||||
expect(response.body.currentStrategy).not.toBeNull()
|
||||
})
|
||||
|
||||
it('succeeds and only change the email', async () => {
|
||||
const name = 'John'
|
||||
const email = 'contact@test.com'
|
||||
const userToken = await authenticateUserTest({
|
||||
name,
|
||||
email
|
||||
it('succeeds with valid accessToken and valid name', async () => {
|
||||
const newName = 'John Doe'
|
||||
const { accessToken, user } = await authenticateUserTest()
|
||||
prismaMock.user.update.mockResolvedValue({
|
||||
...user,
|
||||
name: newName
|
||||
})
|
||||
let user = (await User.findAll())[0]
|
||||
expect(user.email).toEqual(email)
|
||||
expect(user.name).toEqual(name)
|
||||
|
||||
const email2 = 'test2@test2.com'
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ email: email2 })
|
||||
.expect(200)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
expect(response.body.user.email).toEqual(email2)
|
||||
expect(response.body.user.name).toEqual(name)
|
||||
|
||||
user = (await User.findAll())[0]
|
||||
expect(user.email).toEqual(email2)
|
||||
expect(user.name).toEqual(name)
|
||||
expect(user.isConfirmed).toBe(false)
|
||||
})
|
||||
|
||||
it('succeeds and only change the name', async () => {
|
||||
const name = 'John'
|
||||
const email = 'contact@test.com'
|
||||
const userToken = await authenticateUserTest({
|
||||
name,
|
||||
email
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/current',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
payload: {
|
||||
name: newName
|
||||
}
|
||||
})
|
||||
let user = (await User.findAll())[0]
|
||||
expect(user.email).toEqual(email)
|
||||
expect(user.name).toEqual(name)
|
||||
|
||||
const name2 = 'test2'
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ name: name2 })
|
||||
.expect(200)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
|
||||
user = (await User.findAll())[0]
|
||||
expect(user.email).toEqual(email)
|
||||
expect(user.name).toEqual(name2)
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(200)
|
||||
expect(responseJson.user.name).toEqual(newName)
|
||||
})
|
||||
|
||||
it('succeeds and only change the status', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const status = '👀 Working on secret projects...'
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ status })
|
||||
.expect(200)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
expect(response.body.user.status).toBe(status)
|
||||
})
|
||||
|
||||
it('succeeds and only change the biography', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const biography = 'My awesome biography'
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ biography })
|
||||
.expect(200)
|
||||
expect(response.body.user).not.toBeNull()
|
||||
expect(response.body.user.biography).toBe(biography)
|
||||
})
|
||||
|
||||
it('fails with unconfirmed account', async () => {
|
||||
const userToken = await authenticateUserTest({
|
||||
name: 'John',
|
||||
email: 'contact@john.com',
|
||||
shouldBeConfirmed: false
|
||||
it('succeeds and only update the status', async () => {
|
||||
const newStatus = '👀 Working on secret projects...'
|
||||
const { accessToken, user } = await authenticateUserTest()
|
||||
prismaMock.user.update.mockResolvedValue({
|
||||
...user,
|
||||
status: newStatus
|
||||
})
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(401)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('fails with invalid status', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const status = randomString(110)
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ status })
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toEqual(
|
||||
commonErrorsMessages.charactersLength('status', { max: 100 })
|
||||
)
|
||||
})
|
||||
|
||||
it('fails with invalid biography', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const biography = randomString(170)
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ biography })
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toEqual(
|
||||
commonErrorsMessages.charactersLength('biography', { max: 160 })
|
||||
)
|
||||
})
|
||||
|
||||
it('fails with invalid name and invalid email', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({
|
||||
name: 'jo',
|
||||
email: 'test2@test2'
|
||||
})
|
||||
.expect(400)
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors.length).toEqual(2)
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
errorsMessages.email.mustBeValid,
|
||||
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('fails with name and email already used', async () => {
|
||||
const firstUserName = 'Test'
|
||||
const firstUserEmail = 'test@test.com'
|
||||
await authenticateUserTest({
|
||||
name: firstUserName,
|
||||
email: firstUserEmail,
|
||||
shouldBeConfirmed: true
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/current',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
payload: {
|
||||
status: newStatus
|
||||
}
|
||||
})
|
||||
const secondUserToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set(
|
||||
'Authorization',
|
||||
`${secondUserToken.type} ${secondUserToken.accessToken}`
|
||||
)
|
||||
.send({
|
||||
name: firstUserName,
|
||||
email: firstUserEmail
|
||||
})
|
||||
.expect(400)
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors.length).toEqual(2)
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining(['Name already used', 'Email already used'])
|
||||
)
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(200)
|
||||
expect(responseJson.user.name).toEqual(user.name)
|
||||
expect(responseJson.user.status).toEqual(newStatus)
|
||||
})
|
||||
|
||||
it('fails with name identical to the current user name', async () => {
|
||||
const name = 'Test'
|
||||
const email = 'test@test.com'
|
||||
const userToken = await authenticateUserTest({
|
||||
name,
|
||||
email
|
||||
it('fails with name already used', async () => {
|
||||
const newName = 'John Doe'
|
||||
prismaMock.user.findFirst.mockResolvedValue(userExample)
|
||||
const { accessToken } = await authenticateUserTest()
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/current',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
payload: {
|
||||
name: newName
|
||||
}
|
||||
})
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ name })
|
||||
.expect(400)
|
||||
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([errorsMessages.name.alreadyConnected])
|
||||
)
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('fails with email identical to the current user email', async () => {
|
||||
const name = 'Test'
|
||||
const email = 'test@test.com'
|
||||
const userToken = await authenticateUserTest({
|
||||
name,
|
||||
email
|
||||
it('fails with invalid website url', async () => {
|
||||
const newWebsite = 'invalid website url'
|
||||
const { accessToken } = await authenticateUserTest()
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/current',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
payload: {
|
||||
website: newWebsite
|
||||
}
|
||||
})
|
||||
const response = await request(application)
|
||||
.put('/users/current')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ email })
|
||||
.expect(400)
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([errorsMessages.email.alreadyConnected])
|
||||
)
|
||||
it('suceeds with valid website url', async () => {
|
||||
const newWebsite = 'https://somerandomwebsite.com'
|
||||
const { accessToken, user } = await authenticateUserTest()
|
||||
prismaMock.user.update.mockResolvedValue({
|
||||
...user,
|
||||
website: newWebsite
|
||||
})
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/current',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
payload: {
|
||||
website: newWebsite
|
||||
}
|
||||
})
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(200)
|
||||
expect(responseJson.user.name).toEqual(user.name)
|
||||
expect(responseJson.user.website).toEqual(newWebsite)
|
||||
})
|
||||
})
|
||||
|
@ -1,36 +1,59 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
|
||||
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
|
||||
import OAuth, { AuthenticationStrategy } from '../../../models/OAuth'
|
||||
import UserSetting from '../../../models/UserSetting'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import authenticateUser from '../../../tools/plugins/authenticateUser.js'
|
||||
import { userCurrentSchema } from '../../../models/User.js'
|
||||
|
||||
export const getCurrentRouter = Router()
|
||||
|
||||
getCurrentRouter.get(
|
||||
'/users/current',
|
||||
authenticateUser,
|
||||
async (req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
const getCurrentUserSchema: FastifySchema = {
|
||||
description: 'GET the current connected user',
|
||||
tags: ['users'] as string[],
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
const settings = await UserSetting.findOne({
|
||||
where: { userId: req.user.current.id }
|
||||
})
|
||||
const OAuths = await OAuth.findAll({
|
||||
where: { userId: req.user.current.id }
|
||||
})
|
||||
const strategies: AuthenticationStrategy[] = OAuths.map((oauth) => {
|
||||
return oauth.provider
|
||||
})
|
||||
if (req.user.current.password != null) {
|
||||
strategies.push('local')
|
||||
}
|
||||
return res.status(200).json({
|
||||
user: req.user.current,
|
||||
settings,
|
||||
currentStrategy: req.user.currentStrategy,
|
||||
strategies
|
||||
})
|
||||
] as Array<{ [key: string]: [] }>,
|
||||
response: {
|
||||
200: userCurrentSchema,
|
||||
400: fastifyErrors[400],
|
||||
401: fastifyErrors[401],
|
||||
403: fastifyErrors[403],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
)
|
||||
} as const
|
||||
|
||||
export const getCurrentUser: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.register(authenticateUser)
|
||||
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: '/users/current',
|
||||
schema: getCurrentUserSchema,
|
||||
handler: async (request, reply) => {
|
||||
if (request.user == null) {
|
||||
throw fastify.httpErrors.forbidden()
|
||||
}
|
||||
const settings = await prisma.userSetting.findFirst({
|
||||
where: { userId: request.user.current.id }
|
||||
})
|
||||
const OAuths = await prisma.oAuth.findMany({
|
||||
where: { userId: request.user.current.id }
|
||||
})
|
||||
const strategies = OAuths.map((oauth) => {
|
||||
return oauth.provider
|
||||
})
|
||||
if (request.user.current.password != null) {
|
||||
strategies.push('local')
|
||||
}
|
||||
reply.statusCode = 200
|
||||
return {
|
||||
user: {
|
||||
...request.user.current,
|
||||
settings,
|
||||
currentStrategy: request.user.currentStrategy,
|
||||
strategies
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { getCurrentRouter } from './get'
|
||||
import { putCurrentRouter } from './put'
|
||||
import { currentSettingsRouter } from './settings'
|
||||
|
||||
export const currentRouter = Router()
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
alreadyConnected: 'You are already connected with this email address'
|
||||
},
|
||||
name: {
|
||||
alreadyConnected: 'You are already connected with this name'
|
||||
}
|
||||
}
|
||||
|
||||
currentRouter.use('/', getCurrentRouter)
|
||||
currentRouter.use('/', putCurrentRouter)
|
||||
currentRouter.use('/', currentSettingsRouter)
|
97
src/services/users/current/logo/put.ts
Normal file
97
src/services/users/current/logo/put.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import fs from 'node:fs'
|
||||
import { URL } from 'node:url'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
|
||||
import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
|
||||
import { fastifyErrors } from '../../../../models/utils.js'
|
||||
import fastifyMultipart, { Multipart } from 'fastify-multipart'
|
||||
import {
|
||||
maximumImageSize,
|
||||
supportedImageMimetype,
|
||||
ROOT_URL
|
||||
} from '../../../../tools/configurations'
|
||||
import prisma from '../../../../tools/database/prisma.js'
|
||||
|
||||
const putServiceSchema: FastifySchema = {
|
||||
description: 'Edit the current connected user logo',
|
||||
tags: ['users'] as string[],
|
||||
consumes: ['multipart/form-data'] as string[],
|
||||
produces: ['application/json'] as string[],
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
] as Array<{ [key: string]: [] }>,
|
||||
response: {
|
||||
200: Type.Object({
|
||||
user: Type.Object({
|
||||
logo: Type.String()
|
||||
})
|
||||
}),
|
||||
400: fastifyErrors[400],
|
||||
401: fastifyErrors[401],
|
||||
403: fastifyErrors[403],
|
||||
431: fastifyErrors[431],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const putCurrentUserLogo: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.register(authenticateUser)
|
||||
|
||||
await fastify.register(fastifyMultipart)
|
||||
|
||||
fastify.route({
|
||||
method: 'PUT',
|
||||
url: '/users/current/logo',
|
||||
schema: putServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
if (request.user == null) {
|
||||
throw fastify.httpErrors.forbidden()
|
||||
}
|
||||
let files: Multipart[] = []
|
||||
try {
|
||||
files = await request.saveRequestFiles({
|
||||
limits: {
|
||||
files: 1,
|
||||
fileSize: maximumImageSize * 1024 * 1024
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
throw fastify.httpErrors.requestHeaderFieldsTooLarge(
|
||||
`body.logo should be less than ${maximumImageSize}mb.`
|
||||
)
|
||||
}
|
||||
if (files.length !== 1) {
|
||||
throw fastify.httpErrors.badRequest('You must upload at most one file.')
|
||||
}
|
||||
const image = files[0]
|
||||
if (!supportedImageMimetype.includes(image.mimetype)) {
|
||||
throw fastify.httpErrors.badRequest(
|
||||
`The file must have a valid type (${supportedImageMimetype.join(
|
||||
', '
|
||||
)}).`
|
||||
)
|
||||
}
|
||||
const splitedMimetype = image.mimetype.split('/')
|
||||
const imageExtension = splitedMimetype[1]
|
||||
const logoPath = `uploads/users/${randomUUID()}.${imageExtension}`
|
||||
const logoURL = new URL(logoPath, ROOT_URL)
|
||||
const logo = `/${logoPath}`
|
||||
await fs.promises.copyFile(image.filepath, logoURL)
|
||||
await prisma.user.update({
|
||||
where: { id: request.user.current.id },
|
||||
data: { logo }
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return {
|
||||
user: {
|
||||
logo
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -1,141 +1,136 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import fileUpload from 'express-fileupload'
|
||||
import { body, query } from 'express-validator'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import {
|
||||
commonErrorsMessages,
|
||||
imageFileUploadOptions,
|
||||
usersLogoPath
|
||||
} from '../../../tools/configurations/constants'
|
||||
import { alreadyUsedValidation } from '../../../tools/validations/alreadyUsedValidation'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { uploadImage } from '../../../tools/utils/uploadImage'
|
||||
import { deleteEveryRefreshTokens } from '../__utils__/deleteEveryRefreshTokens'
|
||||
import UserSetting from '../../../models/UserSetting'
|
||||
import { sendEmail } from '../../../tools/email/sendEmail'
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
alreadyConnected: 'You are already connected with this email address'
|
||||
},
|
||||
name: {
|
||||
alreadyConnected: 'You are already connected with this name'
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import authenticateUser from '../../../tools/plugins/authenticateUser.js'
|
||||
import { userCurrentSchema, userSchema } from '../../../models/User.js'
|
||||
import { sendEmail } from '../../../tools/email/sendEmail.js'
|
||||
import { HOST, PORT } from '../../../tools/configurations/index.js'
|
||||
import { Language, Theme } from '../../../models/UserSettings.js'
|
||||
|
||||
const bodyPutServiceSchema = Type.Object({
|
||||
name: Type.Optional(userSchema.name),
|
||||
email: Type.Optional(userSchema.email),
|
||||
status: Type.Optional(userSchema.status),
|
||||
biography: Type.Optional(userSchema.biography),
|
||||
website: Type.Optional(userSchema.website)
|
||||
})
|
||||
|
||||
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
|
||||
|
||||
const queryPutCurrentUserSchema = Type.Object({
|
||||
redirectURI: Type.Optional(Type.String({ format: 'uri-reference' }))
|
||||
})
|
||||
|
||||
type QueryPutCurrentUserSchemaType = Static<typeof queryPutCurrentUserSchema>
|
||||
|
||||
const putServiceSchema: FastifySchema = {
|
||||
description: 'Edit the current connected user information',
|
||||
tags: ['users'] as string[],
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
] as Array<{ [key: string]: [] }>,
|
||||
body: bodyPutServiceSchema,
|
||||
querystring: queryPutCurrentUserSchema,
|
||||
response: {
|
||||
200: userCurrentSchema,
|
||||
400: fastifyErrors[400],
|
||||
401: fastifyErrors[401],
|
||||
403: fastifyErrors[403],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
export const putCurrentRouter = Router()
|
||||
export const putCurrentUser: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.register(authenticateUser)
|
||||
|
||||
putCurrentRouter.put(
|
||||
'/users/current',
|
||||
authenticateUser,
|
||||
fileUpload(imageFileUploadOptions),
|
||||
[
|
||||
body('email')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.isEmail()
|
||||
.withMessage(errorsMessages.email.mustBeValid)
|
||||
.custom(async (email: string, meta) => {
|
||||
if (email === meta.req.user?.current.email) {
|
||||
return await Promise.reject(
|
||||
new Error(errorsMessages.email.alreadyConnected)
|
||||
)
|
||||
}
|
||||
return await alreadyUsedValidation(User, 'email', email)
|
||||
}),
|
||||
body('name')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: 30, min: 3 })
|
||||
.withMessage(
|
||||
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
|
||||
)
|
||||
.custom(async (name: string, meta) => {
|
||||
if (name === meta.req.user?.current.name) {
|
||||
return await Promise.reject(
|
||||
new Error(errorsMessages.name.alreadyConnected)
|
||||
)
|
||||
}
|
||||
return await alreadyUsedValidation(User, 'name', name)
|
||||
}),
|
||||
body('biography')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: 160 })
|
||||
.withMessage(
|
||||
commonErrorsMessages.charactersLength('biography', { max: 160 })
|
||||
),
|
||||
body('status')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage(
|
||||
commonErrorsMessages.charactersLength('status', { max: 100 })
|
||||
),
|
||||
query('redirectURI').optional({ nullable: true }).trim()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
}
|
||||
const { name, email, status, biography } = req.body as {
|
||||
name?: string
|
||||
email?: string
|
||||
status?: string
|
||||
biography?: string
|
||||
}
|
||||
const logo = req.files?.logo
|
||||
const { redirectURI } = req.query as { redirectURI?: string }
|
||||
const user = req.user.current
|
||||
|
||||
user.name = name ?? user.name
|
||||
user.status = status ?? user.status
|
||||
user.biography = biography ?? user.biography
|
||||
|
||||
const resultUpload = await uploadImage({
|
||||
image: logo,
|
||||
propertyName: 'logo',
|
||||
oldImage: user.logo,
|
||||
imagesPath: usersLogoPath.filePath
|
||||
})
|
||||
if (resultUpload != null) {
|
||||
user.logo = `${usersLogoPath.name}/${resultUpload}`
|
||||
}
|
||||
|
||||
if (email != null) {
|
||||
user.email = email
|
||||
if (req.user.currentStrategy === 'local') {
|
||||
await deleteEveryRefreshTokens(user.id)
|
||||
fastify.route<{
|
||||
Body: BodyPutServiceSchemaType
|
||||
Params: QueryPutCurrentUserSchemaType
|
||||
}>({
|
||||
method: 'PUT',
|
||||
url: '/users/current',
|
||||
schema: putServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
if (request.user == null) {
|
||||
throw fastify.httpErrors.forbidden()
|
||||
}
|
||||
const tempToken = uuidv4()
|
||||
user.tempToken = tempToken
|
||||
user.isConfirmed = false
|
||||
const userSettings = await UserSetting.findOne({
|
||||
where: { userId: user.id }
|
||||
const { name, email, status, biography, website } = request.body
|
||||
const { redirectURI } = request.params
|
||||
const userValidation = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email }, { name }],
|
||||
AND: [{ id: { not: request.user.current.id } }]
|
||||
}
|
||||
})
|
||||
const redirectQuery =
|
||||
redirectURI != null ? `&redirectURI=${redirectURI}` : ''
|
||||
await sendEmail({
|
||||
type: 'confirm-email',
|
||||
email,
|
||||
url: `${process.env.API_BASE_URL}/users/confirmEmail?tempToken=${tempToken}${redirectQuery}`,
|
||||
language: userSettings?.language,
|
||||
theme: userSettings?.theme
|
||||
if (userValidation != null) {
|
||||
throw fastify.httpErrors.badRequest(
|
||||
'body.email or body.name already taken.'
|
||||
)
|
||||
}
|
||||
const settings = await prisma.userSetting.findFirst({
|
||||
where: { userId: request.user.current.id }
|
||||
})
|
||||
if (settings == null) {
|
||||
throw fastify.httpErrors.internalServerError()
|
||||
}
|
||||
const OAuths = await prisma.oAuth.findMany({
|
||||
where: { userId: request.user.current.id }
|
||||
})
|
||||
const strategies = OAuths.map((oauth) => {
|
||||
return oauth.provider
|
||||
})
|
||||
if (request.user.current.password != null) {
|
||||
strategies.push('local')
|
||||
}
|
||||
if (email != null) {
|
||||
await prisma.refreshToken.deleteMany({
|
||||
where: {
|
||||
userId: request.user.current.id
|
||||
}
|
||||
})
|
||||
const temporaryToken = randomUUID()
|
||||
const redirectQuery =
|
||||
redirectURI != null ? `&redirectURI=${redirectURI}` : ''
|
||||
await sendEmail({
|
||||
type: 'confirm-email',
|
||||
email,
|
||||
url: `${request.protocol}://${HOST}:${PORT}/users/confirm-email?temporaryToken=${temporaryToken}${redirectQuery}`,
|
||||
language: settings.language as Language,
|
||||
theme: settings.theme as Theme
|
||||
})
|
||||
await prisma.user.update({
|
||||
where: { id: request.user.current.id },
|
||||
data: {
|
||||
email,
|
||||
temporaryToken,
|
||||
isConfirmed: false
|
||||
}
|
||||
})
|
||||
}
|
||||
const user = await prisma.user.update({
|
||||
where: { id: request.user.current.id },
|
||||
data: {
|
||||
name: name ?? request.user.current.name,
|
||||
status: status ?? request.user.current.status,
|
||||
biography: biography ?? request.user.current.biography,
|
||||
website: website ?? request.user.current.website
|
||||
}
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return {
|
||||
user: {
|
||||
...user,
|
||||
settings,
|
||||
currentStrategy: request.user.currentStrategy,
|
||||
strategies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userSaved = await user.save()
|
||||
return res
|
||||
.status(200)
|
||||
.json({ user: userSaved, strategy: req.user.currentStrategy })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
/users/current/settings:
|
||||
put:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Edit the current connected user settings'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/UserSettings'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- $ref: '#/definitions/UnauthorizedError'
|
||||
- $ref: '#/definitions/ForbiddenError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/UserSettingsObject'
|
@ -1,66 +1,56 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../../application'
|
||||
import { application } from '../../../../../application.js'
|
||||
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
|
||||
import { prismaMock } from '../../../../../__test__/setup.js'
|
||||
import { userSettingsExample } from '../../../../../models/UserSettings.js'
|
||||
|
||||
describe('PUT /users/current/settings', () => {
|
||||
it('should succeeds and edit theme, language and isPublicEmail', async () => {
|
||||
const isPublicEmail = true
|
||||
const theme = 'light'
|
||||
const language = 'fr'
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.put('/users/current/settings')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ isPublicEmail, theme, language })
|
||||
.expect(200)
|
||||
expect(response.body.settings).not.toBeNull()
|
||||
expect(response.body.settings.theme).toEqual(theme)
|
||||
expect(response.body.settings.language).toEqual(language)
|
||||
expect(response.body.settings.isPublicEmail).toEqual(isPublicEmail)
|
||||
})
|
||||
|
||||
it('fails with unconfirmed account', async () => {
|
||||
const userToken = await authenticateUserTest({
|
||||
name: 'John',
|
||||
email: 'contact@john.com',
|
||||
shouldBeConfirmed: false
|
||||
it('succeeds and edit the theme, language, isPublicEmail and isPublicGuilds', async () => {
|
||||
const newSettings = {
|
||||
theme: 'light',
|
||||
language: 'fr',
|
||||
isPublicEmail: true,
|
||||
isPublicGuilds: true
|
||||
}
|
||||
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
|
||||
prismaMock.userSetting.update.mockResolvedValue({
|
||||
...userSettingsExample,
|
||||
...newSettings
|
||||
})
|
||||
const response = await request(application)
|
||||
.put('/users/current/settings')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(401)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('fails with invalid theme', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.put('/users/current/settings')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ theme: 'random theme value' })
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
const { accessToken } = await authenticateUserTest()
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/current/settings',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
payload: newSettings
|
||||
})
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(200)
|
||||
expect(responseJson.settings.theme).toEqual(newSettings.theme)
|
||||
expect(responseJson.settings.language).toEqual(newSettings.language)
|
||||
expect(responseJson.settings.isPublicEmail).toEqual(
|
||||
newSettings.isPublicEmail
|
||||
)
|
||||
expect(responseJson.settings.isPublicGuilds).toEqual(
|
||||
newSettings.isPublicGuilds
|
||||
)
|
||||
})
|
||||
|
||||
it('fails with invalid language', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.put('/users/current/settings')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ language: 'random language value' })
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('fails with invalid isPublicEmail', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.put('/users/current/settings')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send({ isPublicEmail: 'not a boolean value' })
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
const newSettings = {
|
||||
language: 'somerandomlanguage'
|
||||
}
|
||||
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
|
||||
const { accessToken } = await authenticateUserTest()
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/current/settings',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
payload: newSettings
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
})
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { putCurrentSettingsRouter } from './put'
|
||||
|
||||
export const currentSettingsRouter = Router()
|
||||
|
||||
currentSettingsRouter.use('/', putCurrentSettingsRouter)
|
@ -1,63 +1,71 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
|
||||
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
|
||||
import { validateRequest } from '../../../../tools/middlewares/validateRequest'
|
||||
import UserSetting, {
|
||||
themes,
|
||||
Theme,
|
||||
languages,
|
||||
Language
|
||||
} from '../../../../models/UserSetting'
|
||||
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
|
||||
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
|
||||
import { onlyPossibleValuesValidation } from '../../../../tools/validations/onlyPossibleValuesValidation'
|
||||
import prisma from '../../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../../models/utils.js'
|
||||
import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
|
||||
import { userSettingsSchema } from '../../../../models/UserSettings.js'
|
||||
|
||||
export const putCurrentSettingsRouter = Router()
|
||||
const bodyPutServiceSchema = Type.Object({
|
||||
theme: Type.Optional(userSettingsSchema.theme),
|
||||
language: Type.Optional(userSettingsSchema.language),
|
||||
isPublicEmail: Type.Optional(userSettingsSchema.isPublicEmail),
|
||||
isPublicGuilds: Type.Optional(userSettingsSchema.isPublicGuilds)
|
||||
})
|
||||
|
||||
putCurrentSettingsRouter.put(
|
||||
'/users/current/settings',
|
||||
authenticateUser,
|
||||
[
|
||||
body('isPublicEmail').optional({ nullable: true }).isBoolean(),
|
||||
body('theme')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.isString()
|
||||
.custom(async (theme: Theme) => {
|
||||
return await onlyPossibleValuesValidation([...themes], 'theme', theme)
|
||||
}),
|
||||
body('language')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.isString()
|
||||
.custom(async (language: Language) => {
|
||||
return await onlyPossibleValuesValidation(
|
||||
languages,
|
||||
'language',
|
||||
language
|
||||
)
|
||||
})
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
|
||||
|
||||
const putServiceSchema: FastifySchema = {
|
||||
description: 'Edit the current connected user settings',
|
||||
tags: ['users'] as string[],
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
const { isPublicEmail, theme, language } = req.body as {
|
||||
isPublicEmail?: boolean
|
||||
theme?: Theme
|
||||
language?: Language
|
||||
}
|
||||
const user = req.user.current
|
||||
const settings = await UserSetting.findOne({ where: { id: user.id } })
|
||||
if (settings == null) {
|
||||
throw new NotFoundError()
|
||||
}
|
||||
settings.isPublicEmail = isPublicEmail ?? settings.isPublicEmail
|
||||
settings.theme = theme ?? settings.theme
|
||||
settings.language = language ?? settings.language
|
||||
await settings.save()
|
||||
return res.status(200).json({ settings })
|
||||
] as Array<{ [key: string]: [] }>,
|
||||
body: bodyPutServiceSchema,
|
||||
response: {
|
||||
200: Type.Object({
|
||||
settings: Type.Object(userSettingsSchema)
|
||||
}),
|
||||
400: fastifyErrors[400],
|
||||
401: fastifyErrors[401],
|
||||
403: fastifyErrors[403],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
)
|
||||
} as const
|
||||
|
||||
export const putCurrentUserSettings: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.register(authenticateUser)
|
||||
|
||||
fastify.route<{
|
||||
Body: BodyPutServiceSchemaType
|
||||
}>({
|
||||
method: 'PUT',
|
||||
url: '/users/current/settings',
|
||||
schema: putServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
if (request.user == null) {
|
||||
throw fastify.httpErrors.forbidden()
|
||||
}
|
||||
const { theme, language, isPublicEmail, isPublicGuilds } = request.body
|
||||
const settings = await prisma.userSetting.findFirst({
|
||||
where: { userId: request.user.current.id }
|
||||
})
|
||||
if (settings == null) {
|
||||
throw fastify.httpErrors.internalServerError()
|
||||
}
|
||||
const newSettings = await prisma.userSetting.update({
|
||||
where: { id: request.user.current.id },
|
||||
data: {
|
||||
theme: theme ?? settings.theme,
|
||||
language: language ?? settings.language,
|
||||
isPublicEmail: isPublicEmail ?? settings.isPublicEmail,
|
||||
isPublicGuilds: isPublicGuilds ?? settings.isPublicGuilds
|
||||
}
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return { settings: newSettings }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,26 +1,31 @@
|
||||
import { Router } from 'express'
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import { confirmEmailRouter } from './confirmEmail/get'
|
||||
import { currentRouter } from './current'
|
||||
import { OAuth2Router } from './oauth2'
|
||||
import { refreshTokenRouter } from './refreshToken/post'
|
||||
import { resetPasswordRouter } from './resetPassword'
|
||||
import { signinRouter } from './signin/post'
|
||||
import { signoutRouter } from './signout'
|
||||
import { signupRouter } from './signup/post'
|
||||
import { usersGetByIdRouter } from './[userId]'
|
||||
import { addLocalStrategyRouter } from './addLocalStrategy/post'
|
||||
import { postSignupUser } from './signup/post.js'
|
||||
import { getConfirmEmail } from './confirm-email/get.js'
|
||||
import { postSigninUser } from './signin/post.js'
|
||||
import { postSignoutUser } from './signout/post.js'
|
||||
import { deleteSignoutUser } from './signout/delete.js'
|
||||
import { postRefreshTokenUser } from './refresh-token/post.js'
|
||||
import { putResetPasswordUser } from './reset-password/put.js'
|
||||
import { postResetPasswordUser } from './reset-password/post.js'
|
||||
import { getCurrentUser } from './current/get.js'
|
||||
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'
|
||||
|
||||
export const usersRouter = Router()
|
||||
|
||||
usersRouter.use('/', confirmEmailRouter)
|
||||
usersRouter.use('/', currentRouter)
|
||||
usersRouter.use('/', refreshTokenRouter)
|
||||
usersRouter.use('/', resetPasswordRouter)
|
||||
usersRouter.use('/', signinRouter)
|
||||
usersRouter.use('/', signoutRouter)
|
||||
usersRouter.use('/', signupRouter)
|
||||
usersRouter.use('/', OAuth2Router)
|
||||
usersRouter.use('/', usersGetByIdRouter)
|
||||
usersRouter.use('/', signoutRouter)
|
||||
usersRouter.use('/', addLocalStrategyRouter)
|
||||
export const usersService: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.register(postSignupUser)
|
||||
await fastify.register(getConfirmEmail)
|
||||
await fastify.register(postSigninUser)
|
||||
await fastify.register(postSignoutUser)
|
||||
await fastify.register(deleteSignoutUser)
|
||||
await fastify.register(postRefreshTokenUser)
|
||||
await fastify.register(putResetPasswordUser)
|
||||
await fastify.register(postResetPasswordUser)
|
||||
await fastify.register(getCurrentUser)
|
||||
await fastify.register(putCurrentUser)
|
||||
await fastify.register(putCurrentUserSettings)
|
||||
await fastify.register(putCurrentUserLogo)
|
||||
await fastify.register(getUserById)
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
/users/oauth2/{provider}:
|
||||
delete:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Allows a user to delete a strategy of authentication (except local).'
|
||||
parameters:
|
||||
- name: 'provider'
|
||||
in: 'path'
|
||||
required: true
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- $ref: '#/definitions/UnauthorizedError'
|
||||
- $ref: '#/definitions/ForbiddenError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
message:
|
||||
type: 'string'
|
@ -1,55 +0,0 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import OAuth from '../../../../models/OAuth'
|
||||
import { formatErrors } from '../../../../__test__/utils/formatErrors'
|
||||
import { errorsMessages } from '../delete'
|
||||
import { GOOGLE_PROVIDER, googleStrategy } from '../google'
|
||||
|
||||
describe('DELETE /users/oauth2/:provider', () => {
|
||||
it('succeeds with valid provider', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const oauth = await OAuth.create({
|
||||
provider: 'google',
|
||||
providerId: 'randomid',
|
||||
userId: userToken.userId
|
||||
})
|
||||
await request(application)
|
||||
.delete(`/users/oauth2/${oauth.provider}`)
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
})
|
||||
|
||||
it('fails with invalid provider', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.delete('/users/oauth2/google')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(400)
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors.length).toEqual(1)
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([errorsMessages.provider.notUsed])
|
||||
)
|
||||
})
|
||||
|
||||
it('fails with the only way to authenticate', async () => {
|
||||
const userToken = await googleStrategy.callbackSignin({
|
||||
id: 'randomproviderid',
|
||||
name: 'john'
|
||||
})
|
||||
const response = await request(application)
|
||||
.delete(`/users/oauth2/${GOOGLE_PROVIDER}`)
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(400)
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors.length).toEqual(1)
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([errorsMessages.provider.onlyWayToAuthenticate])
|
||||
)
|
||||
})
|
||||
})
|
@ -1,81 +0,0 @@
|
||||
import request from 'supertest'
|
||||
import axios from 'axios'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import { authorizedRedirectDomains } from '../../../../tools/configurations/constants'
|
||||
import { DISCORD_PROVIDER } from '../discord'
|
||||
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe(`/users/oauth2/${DISCORD_PROVIDER}`, () => {
|
||||
test(`GET /users/oauth2/${DISCORD_PROVIDER}/add-strategy`, async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${DISCORD_PROVIDER}/add-strategy?redirectURI=${authorizedRedirectDomains[0]}`
|
||||
)
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(typeof response.body).toEqual('string')
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${DISCORD_PROVIDER}/callback-add-strategy`, async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
data: {
|
||||
id: 'randomid',
|
||||
username: 'John',
|
||||
discriminator: '1234'
|
||||
}
|
||||
})
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: {
|
||||
token_type: 'Bearer',
|
||||
access_token: 'randomtoken'
|
||||
}
|
||||
})
|
||||
const userToken = await authenticateUserTest()
|
||||
await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${DISCORD_PROVIDER}/callback-add-strategy?redirectURI=${authorizedRedirectDomains[0]}&code=randomtokencode&state=${userToken.accessToken}`
|
||||
)
|
||||
.send()
|
||||
.expect(302)
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${DISCORD_PROVIDER}/signin`, async () => {
|
||||
const response = await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${DISCORD_PROVIDER}/signin?redirectURI=${authorizedRedirectDomains[0]}`
|
||||
)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(typeof response.body).toEqual('string')
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${DISCORD_PROVIDER}/callback`, async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
data: {
|
||||
id: 'randomid',
|
||||
username: 'John',
|
||||
discriminator: '1234'
|
||||
}
|
||||
})
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: {
|
||||
token_type: 'Bearer',
|
||||
access_token: 'randomtoken'
|
||||
}
|
||||
})
|
||||
await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${DISCORD_PROVIDER}/callback?redirectURI=${authorizedRedirectDomains[0]}&code=randomtokencode`
|
||||
)
|
||||
.send()
|
||||
.expect(302)
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
})
|
@ -1,77 +0,0 @@
|
||||
import request from 'supertest'
|
||||
import axios from 'axios'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import { authorizedRedirectDomains } from '../../../../tools/configurations/constants'
|
||||
import { GITHUB_PROVIDER } from '../github'
|
||||
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe(`/users/oauth2/${GITHUB_PROVIDER}`, () => {
|
||||
test(`GET /users/oauth2/${GITHUB_PROVIDER}/add-strategy`, async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${GITHUB_PROVIDER}/add-strategy?redirectURI=${authorizedRedirectDomains[0]}`
|
||||
)
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(typeof response.body).toEqual('string')
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${GITHUB_PROVIDER}/callback-add-strategy`, async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
data: {
|
||||
id: 12,
|
||||
name: 'John'
|
||||
}
|
||||
})
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: {
|
||||
access_token: 'randomtoken'
|
||||
}
|
||||
})
|
||||
const userToken = await authenticateUserTest()
|
||||
await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${GITHUB_PROVIDER}/callback-add-strategy?redirectURI=${authorizedRedirectDomains[0]}&code=randomtokencode&state=${userToken.accessToken}`
|
||||
)
|
||||
.send()
|
||||
.expect(302)
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${GITHUB_PROVIDER}/signin`, async () => {
|
||||
const response = await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${GITHUB_PROVIDER}/signin?redirectURI=${authorizedRedirectDomains[0]}`
|
||||
)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(typeof response.body).toEqual('string')
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${GITHUB_PROVIDER}/callback`, async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
data: {
|
||||
id: 12,
|
||||
name: 'John'
|
||||
}
|
||||
})
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: {
|
||||
access_token: 'randomtoken'
|
||||
}
|
||||
})
|
||||
await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${GITHUB_PROVIDER}/callback?redirectURI=${authorizedRedirectDomains[0]}&code=randomtokencode`
|
||||
)
|
||||
.send()
|
||||
.expect(302)
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
})
|
@ -1,79 +0,0 @@
|
||||
import request from 'supertest'
|
||||
import axios from 'axios'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import { authorizedRedirectDomains } from '../../../../tools/configurations/constants'
|
||||
import { GOOGLE_PROVIDER } from '../google'
|
||||
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe(`/users/oauth2/${GOOGLE_PROVIDER}`, () => {
|
||||
test(`GET /users/oauth2/${GOOGLE_PROVIDER}/add-strategy`, async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${GOOGLE_PROVIDER}/add-strategy?redirectURI=${authorizedRedirectDomains[0]}`
|
||||
)
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(typeof response.body).toEqual('string')
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${GOOGLE_PROVIDER}/callback-add-strategy`, async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
data: {
|
||||
id: 'randomid',
|
||||
name: 'John'
|
||||
}
|
||||
})
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: {
|
||||
token_type: 'Bearer',
|
||||
access_token: 'randomtoken'
|
||||
}
|
||||
})
|
||||
const userToken = await authenticateUserTest()
|
||||
await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${GOOGLE_PROVIDER}/callback-add-strategy?redirectURI=${authorizedRedirectDomains[0]}&code=randomtokencode&state=${userToken.accessToken}`
|
||||
)
|
||||
.send()
|
||||
.expect(302)
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${GOOGLE_PROVIDER}/signin`, async () => {
|
||||
const response = await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${GOOGLE_PROVIDER}/signin?redirectURI=${authorizedRedirectDomains[0]}`
|
||||
)
|
||||
.send()
|
||||
.expect(200)
|
||||
expect(typeof response.body).toEqual('string')
|
||||
})
|
||||
|
||||
test(`GET /users/oauth2/${GOOGLE_PROVIDER}/callback`, async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
data: {
|
||||
id: 'randomid',
|
||||
name: 'John'
|
||||
}
|
||||
})
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: {
|
||||
token_type: 'Bearer',
|
||||
access_token: 'randomtoken'
|
||||
}
|
||||
})
|
||||
await request(application)
|
||||
.get(
|
||||
`/users/oauth2/${GOOGLE_PROVIDER}/callback?redirectURI=${authorizedRedirectDomains[0]}&code=randomtokencode`
|
||||
)
|
||||
.send()
|
||||
.expect(302)
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
})
|
@ -1,68 +0,0 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { param } from 'express-validator'
|
||||
|
||||
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import OAuth, {
|
||||
AuthenticationStrategy,
|
||||
ProviderOAuth,
|
||||
providers
|
||||
} from '../../../models/OAuth'
|
||||
import { BadRequestError } from '../../../tools/errors/BadRequestError'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { onlyPossibleValuesValidation } from '../../../tools/validations/onlyPossibleValuesValidation'
|
||||
|
||||
export const errorsMessages = {
|
||||
provider: {
|
||||
notUsed: 'You are not using this provider',
|
||||
onlyWayToAuthenticate: "You can't delete your only way to authenticate"
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteOAuthStrategy = Router()
|
||||
|
||||
deleteOAuthStrategy.delete(
|
||||
'/users/oauth2/:provider',
|
||||
authenticateUser,
|
||||
[
|
||||
param('provider')
|
||||
.trim()
|
||||
.isString()
|
||||
.custom(async (provider: ProviderOAuth) => {
|
||||
return await onlyPossibleValuesValidation(
|
||||
providers,
|
||||
'provider',
|
||||
provider
|
||||
)
|
||||
})
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
}
|
||||
const user = req.user.current
|
||||
const { provider } = req.params as { provider: ProviderOAuth }
|
||||
const OAuths = await OAuth.findAll({
|
||||
where: { userId: user.id }
|
||||
})
|
||||
const strategies: AuthenticationStrategy[] = OAuths.map((oauth) => {
|
||||
return oauth.provider
|
||||
})
|
||||
if (req.user.current.password != null) {
|
||||
strategies.push('local')
|
||||
}
|
||||
const oauthProvider = OAuths.find((oauth) => oauth.provider === provider)
|
||||
if (oauthProvider == null) {
|
||||
throw new BadRequestError(errorsMessages.provider.notUsed)
|
||||
}
|
||||
const hasOthersWayToAuthenticate = strategies.length >= 2
|
||||
if (!hasOthersWayToAuthenticate) {
|
||||
throw new BadRequestError(errorsMessages.provider.onlyWayToAuthenticate)
|
||||
}
|
||||
await oauthProvider.destroy()
|
||||
return res.status(200).json({
|
||||
message: `Success, you will not be able to login with ${oauthProvider.provider} anymore.`
|
||||
})
|
||||
}
|
||||
)
|
@ -1,164 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import querystring from 'querystring'
|
||||
|
||||
import {
|
||||
authenticateUser,
|
||||
getUserWithBearerToken
|
||||
} from '../../../tools/middlewares/authenticateUser'
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { buildQueryURL } from '../__utils__/buildQueryURL'
|
||||
import { isValidRedirectURIValidation } from '../../../tools/validations/isValidRedirectURIValidation'
|
||||
import { OAuthStrategy } from '../__utils__/OAuthStrategy'
|
||||
|
||||
export const DISCORD_PROVIDER = 'discord'
|
||||
export const DISCORD_BASE_URL = 'https://discordapp.com/api/v6'
|
||||
export const discordStrategy = new OAuthStrategy(DISCORD_PROVIDER)
|
||||
|
||||
interface DiscordUser {
|
||||
id: string
|
||||
username: string
|
||||
discriminator: string
|
||||
avatar?: string
|
||||
locale?: string
|
||||
}
|
||||
|
||||
interface DiscordTokens {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
refresh_token: string
|
||||
scope: string
|
||||
}
|
||||
|
||||
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: process.env.DISCORD_CLIENT_ID,
|
||||
client_secret: process.env.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
|
||||
}
|
||||
|
||||
export const discordRouter = Router()
|
||||
|
||||
discordRouter.get(
|
||||
`/users/oauth2/${DISCORD_PROVIDER}/add-strategy`,
|
||||
authenticateUser,
|
||||
[
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
(req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
}
|
||||
const { redirectURI } = req.query as { redirectURI: string }
|
||||
const redirectCallback = `${process.env.API_BASE_URL}/users/oauth2/${DISCORD_PROVIDER}/callback-add-strategy?redirectURI=${redirectURI}`
|
||||
const url = `${DISCORD_BASE_URL}/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&scope=identify&response_type=code&state=${req.user.accessToken}&redirect_uri=${redirectCallback}`
|
||||
return res.json(url)
|
||||
}
|
||||
)
|
||||
|
||||
discordRouter.get(
|
||||
`/users/oauth2/${DISCORD_PROVIDER}/callback-add-strategy`,
|
||||
[
|
||||
query('code').notEmpty(),
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation),
|
||||
query('state')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { code, redirectURI, state: accessToken } = req.query as {
|
||||
code: string
|
||||
redirectURI: string
|
||||
state: string
|
||||
}
|
||||
const userRequest = await getUserWithBearerToken(`Bearer ${accessToken}`)
|
||||
const discordUser = await getDiscordUserData(
|
||||
code,
|
||||
`${process.env.API_BASE_URL}/users/oauth2/${DISCORD_PROVIDER}/callback-add-strategy?redirectURI=${redirectURI}`
|
||||
)
|
||||
const message = await discordStrategy.callbackAddStrategy(
|
||||
{ name: discordUser.username, id: discordUser.id },
|
||||
userRequest
|
||||
)
|
||||
return res.redirect(buildQueryURL(redirectURI, { message }))
|
||||
}
|
||||
)
|
||||
|
||||
discordRouter.get(
|
||||
`/users/oauth2/${DISCORD_PROVIDER}/signin`,
|
||||
[
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
(req: Request, res: Response) => {
|
||||
const { redirectURI } = req.query as { redirectURI: string }
|
||||
const redirectCallback = `${process.env.API_BASE_URL}/users/oauth2/${DISCORD_PROVIDER}/callback?redirectURI=${redirectURI}`
|
||||
const url = `${DISCORD_BASE_URL}/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&scope=identify&response_type=code&redirect_uri=${redirectCallback}`
|
||||
return res.json(url)
|
||||
}
|
||||
)
|
||||
|
||||
discordRouter.get(
|
||||
`/users/oauth2/${DISCORD_PROVIDER}/callback`,
|
||||
[
|
||||
query('code').notEmpty(),
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { code, redirectURI } = req.query as {
|
||||
code: string
|
||||
redirectURI: string
|
||||
}
|
||||
const discordUser = await getDiscordUserData(
|
||||
code,
|
||||
`${process.env.API_BASE_URL}/users/oauth2/${DISCORD_PROVIDER}/callback?redirectURI=${redirectURI}`
|
||||
)
|
||||
const responseJWT = await discordStrategy.callbackSignin({
|
||||
name: discordUser.username,
|
||||
id: discordUser.id
|
||||
})
|
||||
return res.redirect(buildQueryURL(redirectURI, responseJWT))
|
||||
}
|
||||
)
|
@ -1,161 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import querystring from 'querystring'
|
||||
|
||||
import {
|
||||
authenticateUser,
|
||||
getUserWithBearerToken
|
||||
} from '../../../tools/middlewares/authenticateUser'
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { buildQueryURL } from '../__utils__/buildQueryURL'
|
||||
import { isValidRedirectURIValidation } from '../../../tools/validations/isValidRedirectURIValidation'
|
||||
import { OAuthStrategy } from '../__utils__/OAuthStrategy'
|
||||
|
||||
interface GitHubUser {
|
||||
login: string
|
||||
id: number
|
||||
name: string
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
interface GitHubTokens {
|
||||
access_token: string
|
||||
scope: string
|
||||
token_type: string
|
||||
}
|
||||
|
||||
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 githubStrategy = new OAuthStrategy(GITHUB_PROVIDER)
|
||||
|
||||
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: process.env.GITHUB_CLIENT_ID,
|
||||
client_secret: process.env.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
|
||||
}
|
||||
|
||||
export const githubRouter = Router()
|
||||
|
||||
githubRouter.get(
|
||||
`/users/oauth2/${GITHUB_PROVIDER}/add-strategy`,
|
||||
authenticateUser,
|
||||
[
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
(req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
}
|
||||
const { redirectURI } = req.query as { redirectURI: string }
|
||||
const redirectCallback = `${process.env.API_BASE_URL}/users/oauth2/${GITHUB_PROVIDER}/callback-add-strategy?redirectURI=${redirectURI}`
|
||||
const url = `${GITHUB_BASE_URL}/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}&state=${req.user.accessToken}&redirect_uri=${redirectCallback}`
|
||||
return res.json(url)
|
||||
}
|
||||
)
|
||||
|
||||
githubRouter.get(
|
||||
`/users/oauth2/${GITHUB_PROVIDER}/callback-add-strategy`,
|
||||
[
|
||||
query('code').notEmpty(),
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation),
|
||||
query('state')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { code, redirectURI, state: accessToken } = req.query as {
|
||||
code: string
|
||||
redirectURI: string
|
||||
state: string
|
||||
}
|
||||
const userRequest = await getUserWithBearerToken(`Bearer ${accessToken}`)
|
||||
const githubUser = await getGitHubUserData(
|
||||
code,
|
||||
`${process.env.API_BASE_URL}/users/oauth2/${GITHUB_PROVIDER}/callback-add-strategy?redirectURI=${redirectURI}`
|
||||
)
|
||||
const message = await githubStrategy.callbackAddStrategy(
|
||||
{ name: githubUser.name, id: githubUser.id },
|
||||
userRequest
|
||||
)
|
||||
return res.redirect(buildQueryURL(redirectURI, { message }))
|
||||
}
|
||||
)
|
||||
|
||||
githubRouter.get(
|
||||
`/users/oauth2/${GITHUB_PROVIDER}/signin`,
|
||||
[
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
(req: Request, res: Response) => {
|
||||
const { redirectURI } = req.query as { redirectURI: string }
|
||||
const redirectCallback = `${process.env.API_BASE_URL}/users/oauth2/${GITHUB_PROVIDER}/callback?redirectURI=${redirectURI}`
|
||||
const url = `${GITHUB_BASE_URL}/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}&redirect_uri=${redirectCallback}`
|
||||
return res.json(url)
|
||||
}
|
||||
)
|
||||
|
||||
githubRouter.get(
|
||||
`/users/oauth2/${GITHUB_PROVIDER}/callback`,
|
||||
[
|
||||
query('code').notEmpty(),
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { code, redirectURI } = req.query as {
|
||||
code: string
|
||||
redirectURI: string
|
||||
}
|
||||
const githubUser = await getGitHubUserData(
|
||||
code,
|
||||
`${process.env.API_BASE_URL}/users/oauth2/${GITHUB_PROVIDER}/callback?redirectURI=${redirectURI}`
|
||||
)
|
||||
const responseJWT = await githubStrategy.callbackSignin({
|
||||
name: githubUser.name,
|
||||
id: githubUser.id
|
||||
})
|
||||
return res.redirect(buildQueryURL(redirectURI, responseJWT))
|
||||
}
|
||||
)
|
@ -1,162 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import querystring from 'querystring'
|
||||
|
||||
import {
|
||||
authenticateUser,
|
||||
getUserWithBearerToken
|
||||
} from '../../../tools/middlewares/authenticateUser'
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { buildQueryURL } from '../__utils__/buildQueryURL'
|
||||
import { isValidRedirectURIValidation } from '../../../tools/validations/isValidRedirectURIValidation'
|
||||
import { OAuthStrategy } from '../__utils__/OAuthStrategy'
|
||||
|
||||
interface GoogleUser {
|
||||
id: string
|
||||
name: string
|
||||
given_name: string
|
||||
link: string
|
||||
picture: string
|
||||
locale: string
|
||||
}
|
||||
|
||||
interface GoogleTokens {
|
||||
access_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
scope: string
|
||||
refresh_token?: string
|
||||
}
|
||||
|
||||
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 googleStrategy = new OAuthStrategy(GOOGLE_PROVIDER)
|
||||
|
||||
const getGoogleUserData = async (
|
||||
code: string,
|
||||
redirectURI: string
|
||||
): Promise<GoogleUser> => {
|
||||
const { data: token } = await axios.post<GoogleTokens>(
|
||||
GOOGLE_OAUTH2_TOKEN,
|
||||
querystring.stringify({
|
||||
client_id: process.env.GOOGLE_CLIENT_ID,
|
||||
client_secret: process.env.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
|
||||
}
|
||||
|
||||
export const googleRouter = Router()
|
||||
|
||||
googleRouter.get(
|
||||
`/users/oauth2/${GOOGLE_PROVIDER}/add-strategy`,
|
||||
authenticateUser,
|
||||
[
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
(req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
}
|
||||
const { redirectURI } = req.query as { redirectURI: string }
|
||||
const redirectCallback = `${process.env.API_BASE_URL}/users/oauth2/${GOOGLE_PROVIDER}/callback-add-strategy?redirectURI=${redirectURI}`
|
||||
const url = `${GOOGLE_BASE_URL}?client_id=${process.env.GOOGLE_CLIENT_ID}&redirect_uri=${redirectCallback}&response_type=code&scope=profile&access_type=online&state=${req.user.accessToken}`
|
||||
return res.json(url)
|
||||
}
|
||||
)
|
||||
|
||||
googleRouter.get(
|
||||
`/users/oauth2/${GOOGLE_PROVIDER}/callback-add-strategy`,
|
||||
[
|
||||
query('code').notEmpty(),
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation),
|
||||
query('state')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { code, redirectURI, state: accessToken } = req.query as {
|
||||
code: string
|
||||
redirectURI: string
|
||||
state: string
|
||||
}
|
||||
const userRequest = await getUserWithBearerToken(`Bearer ${accessToken}`)
|
||||
const googleUser = await getGoogleUserData(
|
||||
code,
|
||||
`${process.env.API_BASE_URL}/users/oauth2/${GOOGLE_PROVIDER}/callback-add-strategy?redirectURI=${redirectURI}`
|
||||
)
|
||||
const message = await googleStrategy.callbackAddStrategy(
|
||||
{ name: googleUser.name, id: googleUser.id },
|
||||
userRequest
|
||||
)
|
||||
return res.redirect(buildQueryURL(redirectURI, { message }))
|
||||
}
|
||||
)
|
||||
|
||||
googleRouter.get(
|
||||
`/users/oauth2/${GOOGLE_PROVIDER}/signin`,
|
||||
[
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
(req: Request, res: Response) => {
|
||||
const { redirectURI } = req.query as { redirectURI: string }
|
||||
const redirectCallback = `${process.env.API_BASE_URL}/users/oauth2/${GOOGLE_PROVIDER}/callback?redirectURI=${redirectURI}`
|
||||
const url = `${GOOGLE_BASE_URL}?client_id=${process.env.GOOGLE_CLIENT_ID}&redirect_uri=${redirectCallback}&response_type=code&scope=profile&access_type=online`
|
||||
return res.json(url)
|
||||
}
|
||||
)
|
||||
|
||||
googleRouter.get(
|
||||
`/users/oauth2/${GOOGLE_PROVIDER}/callback`,
|
||||
[
|
||||
query('code').notEmpty(),
|
||||
query('redirectURI')
|
||||
.notEmpty()
|
||||
.trim()
|
||||
.custom(isValidRedirectURIValidation)
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { code, redirectURI } = req.query as {
|
||||
code: string
|
||||
redirectURI: string
|
||||
}
|
||||
const googleUser = await getGoogleUserData(
|
||||
code,
|
||||
`${process.env.API_BASE_URL}/users/oauth2/${GOOGLE_PROVIDER}/callback?redirectURI=${redirectURI}`
|
||||
)
|
||||
const responseJWT = await googleStrategy.callbackSignin({
|
||||
name: googleUser.name,
|
||||
id: googleUser.id
|
||||
})
|
||||
return res.redirect(buildQueryURL(redirectURI, responseJWT))
|
||||
}
|
||||
)
|
@ -1,15 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { deleteOAuthStrategy } from './delete'
|
||||
import { discordRouter } from './discord'
|
||||
import { githubRouter } from './github'
|
||||
import { googleRouter } from './google'
|
||||
|
||||
const OAuth2Router = Router()
|
||||
|
||||
OAuth2Router.use('/', discordRouter)
|
||||
OAuth2Router.use('/', githubRouter)
|
||||
OAuth2Router.use('/', googleRouter)
|
||||
OAuth2Router.use('/', deleteOAuthStrategy)
|
||||
|
||||
export { OAuth2Router }
|
46
src/services/users/refresh-token/__test__/post.test.ts
Normal file
46
src/services/users/refresh-token/__test__/post.test.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { application } from '../../../../application.js'
|
||||
import { refreshTokenExample } from '../../../../models/RefreshToken.js'
|
||||
import { expiresIn } from '../../../../tools/utils/jwtToken.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
|
||||
|
||||
describe('POST /users/refresh-token', () => {
|
||||
it('succeeds', async () => {
|
||||
const { refreshToken } = await authenticateUserTest()
|
||||
prismaMock.refreshToken.findFirst.mockResolvedValue({
|
||||
...refreshTokenExample,
|
||||
id: 1,
|
||||
token: refreshToken
|
||||
})
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/refresh-token',
|
||||
payload: { refreshToken }
|
||||
})
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(200)
|
||||
expect(responseJson.type).toEqual('Bearer')
|
||||
expect(responseJson.expiresIn).toEqual(expiresIn)
|
||||
expect(typeof responseJson.accessToken).toEqual('string')
|
||||
})
|
||||
|
||||
it('fails with refreshToken noty saved in database', async () => {
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/refresh-token',
|
||||
payload: { refreshToken: 'somerandomtoken' }
|
||||
})
|
||||
expect(response.statusCode).toEqual(403)
|
||||
})
|
||||
|
||||
it('fails with invalid jwt refreshToken', async () => {
|
||||
const { refreshToken } = await authenticateUserTest()
|
||||
prismaMock.refreshToken.findFirst.mockResolvedValue(refreshTokenExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/refresh-token',
|
||||
payload: { refreshToken }
|
||||
})
|
||||
expect(response.statusCode).toEqual(403)
|
||||
})
|
||||
})
|
72
src/services/users/refresh-token/post.ts
Normal file
72
src/services/users/refresh-token/post.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import {
|
||||
generateAccessToken,
|
||||
jwtSchema,
|
||||
expiresIn
|
||||
} from '../../../tools/utils/jwtToken.js'
|
||||
import { UserJWT } from '../../../models/User.js'
|
||||
import { JWT_REFRESH_SECRET } from '../../../tools/configurations/index.js'
|
||||
|
||||
const bodyPostRefreshTokenSchema = Type.Object({
|
||||
refreshToken: jwtSchema.refreshToken
|
||||
})
|
||||
|
||||
type BodyPostRefreshTokenSchemaType = Static<typeof bodyPostRefreshTokenSchema>
|
||||
|
||||
const postRefreshTokenSchema: FastifySchema = {
|
||||
description: 'Refresh the accessToken of the user',
|
||||
tags: ['users'] as string[],
|
||||
body: bodyPostRefreshTokenSchema,
|
||||
response: {
|
||||
200: Type.Object({
|
||||
accessToken: jwtSchema.accessToken,
|
||||
expiresIn: jwtSchema.expiresIn,
|
||||
type: jwtSchema.type
|
||||
}),
|
||||
400: fastifyErrors[400],
|
||||
403: fastifyErrors[403],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const postRefreshTokenUser: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.route<{
|
||||
Body: BodyPostRefreshTokenSchemaType
|
||||
}>({
|
||||
method: 'POST',
|
||||
url: '/users/refresh-token',
|
||||
schema: postRefreshTokenSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { refreshToken } = request.body
|
||||
const foundRefreshToken = await prisma.refreshToken.findFirst({
|
||||
where: { token: refreshToken }
|
||||
})
|
||||
if (foundRefreshToken == null) {
|
||||
throw fastify.httpErrors.forbidden()
|
||||
}
|
||||
try {
|
||||
const userJWT = jwt.verify(
|
||||
foundRefreshToken.token,
|
||||
JWT_REFRESH_SECRET
|
||||
) as UserJWT
|
||||
const accessToken = generateAccessToken({
|
||||
id: userJWT.id,
|
||||
currentStrategy: 'local'
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return {
|
||||
accessToken,
|
||||
expiresIn,
|
||||
type: 'Bearer'
|
||||
}
|
||||
} catch {
|
||||
throw fastify.httpErrors.forbidden()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/users/refreshToken:
|
||||
post:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Refresh the accessToken of the user'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
refreshToken:
|
||||
type: 'string'
|
||||
required:
|
||||
- 'refreshToken'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- $ref: '#/definitions/UnauthorizedError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/definitions/AccessTokenResponse'
|
@ -1,26 +0,0 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
|
||||
describe('POST /users/refreshToken', () => {
|
||||
it('succeeds and generate a new accessToken with a valid refreshToken', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
const response = await request(application)
|
||||
.post('/users/refreshToken')
|
||||
.send({
|
||||
refreshToken: userToken.refreshToken
|
||||
})
|
||||
.expect(200)
|
||||
expect(response.body.accessToken).not.toBeNull()
|
||||
})
|
||||
|
||||
it('fails with invalid refreshToken', async () => {
|
||||
await request(application)
|
||||
.post('/users/refreshToken')
|
||||
.send({
|
||||
refreshToken: 'invalidtoken'
|
||||
})
|
||||
.expect(401)
|
||||
})
|
||||
})
|
@ -1,55 +0,0 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import RefreshToken from '../../../models/RefreshToken'
|
||||
import { UserJWT } from '../../../models/User'
|
||||
import {
|
||||
expiresIn,
|
||||
generateAccessToken,
|
||||
ResponseJWT
|
||||
} from '../../../tools/configurations/jwtToken'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { UnauthorizedError } from '../../../tools/errors/UnauthorizedError'
|
||||
|
||||
export const refreshTokenRouter = Router()
|
||||
|
||||
refreshTokenRouter.post(
|
||||
'/users/refreshToken',
|
||||
[
|
||||
body('refreshToken')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { refreshToken } = req.body as { refreshToken: string }
|
||||
const foundRefreshToken = await RefreshToken.findOne({
|
||||
where: { token: refreshToken }
|
||||
})
|
||||
if (foundRefreshToken == null) {
|
||||
throw new UnauthorizedError()
|
||||
}
|
||||
jwt.verify(
|
||||
foundRefreshToken.token,
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
(error, user) => {
|
||||
if (error != null) {
|
||||
throw new ForbiddenError()
|
||||
}
|
||||
const userJWT = user as UserJWT
|
||||
const accessToken = generateAccessToken({
|
||||
id: userJWT.id,
|
||||
currentStrategy: userJWT.currentStrategy
|
||||
})
|
||||
const responseJWT: ResponseJWT = {
|
||||
accessToken,
|
||||
expiresIn,
|
||||
type: 'Bearer'
|
||||
}
|
||||
return res.status(200).json(responseJWT)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
66
src/services/users/reset-password/__test__/post.test.ts
Normal file
66
src/services/users/reset-password/__test__/post.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import ms from 'ms'
|
||||
|
||||
import { application } from '../../../../application.js'
|
||||
import { userExample } from '../../../../models/User.js'
|
||||
import { userSettingsExample } from '../../../../models/UserSettings.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
|
||||
describe('POST /users/reset-password', () => {
|
||||
it('succeeds', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue(userExample)
|
||||
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/reset-password?redirectURI=https://redirecturi.com',
|
||||
payload: { email: userExample.email }
|
||||
})
|
||||
expect(response.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it("fails with email that doesn't exist", async () => {
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/reset-password?redirectURI=https://redirecturi.com',
|
||||
payload: { email: userExample.email }
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('fails with unconfirmed account', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue({
|
||||
...userExample,
|
||||
isConfirmed: false
|
||||
})
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/reset-password?redirectURI=https://redirecturi.com',
|
||||
payload: { email: userExample.email }
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it("fails if userSettings doenst' exist", async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue(userExample)
|
||||
prismaMock.userSetting.findFirst.mockResolvedValue(null)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/reset-password?redirectURI=https://redirecturi.com',
|
||||
payload: { email: userExample.email }
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('fails with a request already in progress', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue({
|
||||
...userExample,
|
||||
temporaryExpirationToken: new Date(Date.now() + ms('1 hour'))
|
||||
})
|
||||
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/reset-password?redirectURI=https://redirecturi.com',
|
||||
payload: { email: userExample.email }
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
})
|
36
src/services/users/reset-password/__test__/put.test.ts
Normal file
36
src/services/users/reset-password/__test__/put.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import ms from 'ms'
|
||||
|
||||
import { application } from '../../../../application.js'
|
||||
import { userExample } from '../../../../models/User.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
|
||||
describe('PUT /users/reset-password', () => {
|
||||
it('succeeds', async () => {
|
||||
prismaMock.user.findFirst.mockResolvedValue({
|
||||
...userExample,
|
||||
temporaryExpirationToken: new Date(Date.now() + ms('1 hour'))
|
||||
})
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/reset-password',
|
||||
payload: {
|
||||
password: 'new password',
|
||||
temporaryToken: userExample.temporaryToken
|
||||
}
|
||||
})
|
||||
expect(response.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('fails with expired temporaryToken', async () => {
|
||||
prismaMock.user.findFirst.mockResolvedValue(userExample)
|
||||
const response = await application.inject({
|
||||
method: 'PUT',
|
||||
url: '/users/reset-password',
|
||||
payload: {
|
||||
password: 'new password',
|
||||
temporaryToken: userExample.temporaryToken
|
||||
}
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
})
|
100
src/services/users/reset-password/post.ts
Normal file
100
src/services/users/reset-password/post.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import ms from 'ms'
|
||||
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import { userSchema } from '../../../models/User.js'
|
||||
import { sendEmail } from '../../../tools/email/sendEmail.js'
|
||||
import { Language, Theme } from '../../../models/UserSettings.js'
|
||||
|
||||
const queryPostResetPasswordSchema = Type.Object({
|
||||
redirectURI: Type.String({ format: 'uri-reference' })
|
||||
})
|
||||
|
||||
type QueryPostResetPasswordSchemaType = Static<
|
||||
typeof queryPostResetPasswordSchema
|
||||
>
|
||||
|
||||
const bodyPostResetPasswordSchema = Type.Object({
|
||||
email: userSchema.email
|
||||
})
|
||||
|
||||
type BodyPostResetPasswordSchemaType = Static<
|
||||
typeof bodyPostResetPasswordSchema
|
||||
>
|
||||
|
||||
const postResetPasswordSchema: FastifySchema = {
|
||||
description: 'Request a password-reset change',
|
||||
tags: ['users'] as string[],
|
||||
body: bodyPostResetPasswordSchema,
|
||||
querystring: queryPostResetPasswordSchema,
|
||||
response: {
|
||||
200: Type.String(),
|
||||
400: fastifyErrors[400],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const postResetPasswordUser: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.route<{
|
||||
Body: BodyPostResetPasswordSchemaType
|
||||
Querystring: QueryPostResetPasswordSchemaType
|
||||
}>({
|
||||
method: 'POST',
|
||||
url: '/users/reset-password',
|
||||
schema: postResetPasswordSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { email } = request.body
|
||||
const { redirectURI } = request.query
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email
|
||||
}
|
||||
})
|
||||
if (user == null) {
|
||||
throw fastify.httpErrors.badRequest("Email address doesn't exist")
|
||||
}
|
||||
if (!user.isConfirmed) {
|
||||
throw fastify.httpErrors.badRequest(
|
||||
'You should have a confirmed account, please check your email and follow the instructions to verify your account'
|
||||
)
|
||||
}
|
||||
const isValidTemporaryToken =
|
||||
user.temporaryExpirationToken != null &&
|
||||
user.temporaryExpirationToken.getTime() > Date.now()
|
||||
if (user.temporaryToken != null && isValidTemporaryToken) {
|
||||
throw fastify.httpErrors.badRequest(
|
||||
'A request to reset-password is already in progress'
|
||||
)
|
||||
}
|
||||
const temporaryToken = randomUUID()
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id
|
||||
},
|
||||
data: {
|
||||
temporaryExpirationToken: new Date(Date.now() + ms('1 hour')),
|
||||
temporaryToken
|
||||
}
|
||||
})
|
||||
const userSettings = await prisma.userSetting.findFirst({
|
||||
where: { userId: user.id }
|
||||
})
|
||||
if (userSettings == null) {
|
||||
throw fastify.httpErrors.badRequest()
|
||||
}
|
||||
await sendEmail({
|
||||
type: 'reset-password',
|
||||
email,
|
||||
url: `${redirectURI}?temporaryToken=${temporaryToken}`,
|
||||
language: userSettings.language as Language,
|
||||
theme: userSettings.theme as Theme
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return 'Password-reset request successful, please check your emails!'
|
||||
}
|
||||
})
|
||||
}
|
59
src/services/users/reset-password/put.ts
Normal file
59
src/services/users/reset-password/put.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import { userSchema } from '../../../models/User.js'
|
||||
|
||||
const bodyPutResetPasswordSchema = Type.Object({
|
||||
password: userSchema.password,
|
||||
temporaryToken: userSchema.temporaryToken
|
||||
})
|
||||
|
||||
type BodyPutResetPasswordSchemaType = Static<typeof bodyPutResetPasswordSchema>
|
||||
|
||||
const putResetPasswordSchema: FastifySchema = {
|
||||
description:
|
||||
'Change the user password if the provided temporaryToken (sent in the email of POST /users/reset-password) is correct.',
|
||||
tags: ['users'] as string[],
|
||||
body: bodyPutResetPasswordSchema,
|
||||
response: {
|
||||
200: Type.String(),
|
||||
400: fastifyErrors[400],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const putResetPasswordUser: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.route<{
|
||||
Body: BodyPutResetPasswordSchemaType
|
||||
}>({
|
||||
method: 'PUT',
|
||||
url: '/users/reset-password',
|
||||
schema: putResetPasswordSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { password, temporaryToken } = request.body
|
||||
const user = await prisma.user.findFirst({ where: { temporaryToken } })
|
||||
const isValidTemporaryToken =
|
||||
user?.temporaryExpirationToken != null &&
|
||||
user.temporaryExpirationToken.getTime() > Date.now()
|
||||
if (user == null || !isValidTemporaryToken) {
|
||||
throw fastify.httpErrors.badRequest('"tempToken" is invalid')
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 12)
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
temporaryToken: null,
|
||||
temporaryExpirationToken: null
|
||||
}
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return 'The new password has been saved!'
|
||||
}
|
||||
})
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/users/resetPassword:
|
||||
post:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Request a password-reset change'
|
||||
description: 'Allows a user to reset his password, if he forgets thanks to his email address.'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
email:
|
||||
type: 'string'
|
||||
format: 'email'
|
||||
required:
|
||||
- 'email'
|
||||
parameters:
|
||||
- name: 'redirectURI'
|
||||
description: 'The redirect URI to redirect the user when he clicks on the button of the email, so he can change his password with a form on the frontend.'
|
||||
in: 'query'
|
||||
required: true
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
message:
|
||||
type: 'string'
|
||||
enum:
|
||||
[
|
||||
'Password-reset request successful, please check your emails!'
|
||||
]
|
@ -1,33 +0,0 @@
|
||||
/users/resetPassword:
|
||||
put:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Change the user password'
|
||||
description: 'Change the user password if the provided tempToken (sent in the email of POST /users/resetPassword) is correct.'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
password:
|
||||
type: 'string'
|
||||
format: 'password'
|
||||
example: 'password'
|
||||
tempToken:
|
||||
type: 'string'
|
||||
required:
|
||||
- 'password'
|
||||
- 'tempToken'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
message:
|
||||
type: 'string'
|
||||
enum: ['The new password has been saved!']
|
@ -1,97 +0,0 @@
|
||||
import ms from 'ms'
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import { errorsMessages as errorsConfirmed } from '../../../../tools/middlewares/authenticateUser'
|
||||
import User from '../../../../models/User'
|
||||
import { errorsMessages } from '..'
|
||||
|
||||
describe('POST /users/resetPassword', () => {
|
||||
it('succeeds with valid email and generate a tempToken', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
await authenticateUserTest({ email, name, shouldBeConfirmed: true })
|
||||
const userBefore = await User.findOne({ where: { name } })
|
||||
expect(userBefore).not.toBeNull()
|
||||
expect(userBefore?.tempToken).toBe(null)
|
||||
expect(userBefore?.tempExpirationToken).toBe(null)
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const userAfter = await User.findOne({ where: { name } })
|
||||
expect(userAfter?.tempToken).not.toBeNull()
|
||||
expect(userAfter?.tempExpirationToken).not.toBeNull()
|
||||
})
|
||||
|
||||
it('succeeds even if there is already a password-reset request in progress (but outdated)', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
await authenticateUserTest({ email, name, shouldBeConfirmed: true })
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const user = await User.findOne({ where: { name } })
|
||||
expect(user).not.toBeNull()
|
||||
if (user != null) {
|
||||
user.tempExpirationToken = Date.now() - ms('2 hour')
|
||||
await user.save()
|
||||
}
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
})
|
||||
|
||||
it("fails with email address that doesn't exist", async () => {
|
||||
const response = await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email: 'contact@test.com' })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(errorsMessages.email.notExist)
|
||||
})
|
||||
|
||||
it('fails with unconfirmed account', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
await authenticateUserTest({ email, name, shouldBeConfirmed: false })
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(errorsConfirmed.invalidAccount)
|
||||
})
|
||||
|
||||
it('fails if there is already a password-reset request in progress', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
await authenticateUserTest({ email, name, shouldBeConfirmed: true })
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
errorsMessages.password.alreadyInProgress
|
||||
)
|
||||
})
|
||||
})
|
@ -1,95 +0,0 @@
|
||||
import ms from 'ms'
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import User from '../../../../models/User'
|
||||
import { errorsMessages } from '..'
|
||||
|
||||
describe('PUT /users/resetPassword', () => {
|
||||
it('succeeds and change the password so we can signin again', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
const password = 'test'
|
||||
await authenticateUserTest({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
shouldBeConfirmed: true
|
||||
})
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const user = await User.findOne({ where: { name } })
|
||||
expect(user).not.toBeNull()
|
||||
|
||||
const newPassword = 'newpassword'
|
||||
await request(application)
|
||||
.put('/users/resetPassword')
|
||||
.send({ password: newPassword, tempToken: user?.tempToken })
|
||||
.expect(200)
|
||||
|
||||
await request(application)
|
||||
.post('/users/signin')
|
||||
.send({ email, password: newPassword })
|
||||
.expect(200)
|
||||
})
|
||||
|
||||
it('fails with an invalid "tempToken"', async () => {
|
||||
const response = await request(application)
|
||||
.put('/users/resetPassword')
|
||||
.send({ password: 'newpassword', tempToken: 'sometemptoken' })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
errorsMessages.tempToken.invalid
|
||||
)
|
||||
})
|
||||
|
||||
it('fails if there is no password and tempToken provided', async () => {
|
||||
const response = await request(application)
|
||||
.put('/users/resetPassword')
|
||||
.send()
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(2)
|
||||
})
|
||||
|
||||
it('fails if the tempToken is outdated', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
const password = 'test'
|
||||
await authenticateUserTest({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
shouldBeConfirmed: true
|
||||
})
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const user = await User.findOne({ where: { name } })
|
||||
expect(user).not.toBeNull()
|
||||
if (user != null) {
|
||||
user.tempExpirationToken = Date.now() - ms('2 hour')
|
||||
await user.save()
|
||||
}
|
||||
|
||||
const newPassword = 'newpassword'
|
||||
const response = await request(application)
|
||||
.put('/users/resetPassword')
|
||||
.send({ password: newPassword, tempToken: user?.tempToken })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
errorsMessages.tempToken.invalid
|
||||
)
|
||||
})
|
||||
})
|
@ -1,22 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { postResetPasswordRouter } from './post'
|
||||
import { putResetPasswordRouter } from './put'
|
||||
|
||||
export const resetPasswordRouter = Router()
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
notExist: "Email address doesn't exist"
|
||||
},
|
||||
password: {
|
||||
alreadyInProgress: 'A request to reset-password is already in progress'
|
||||
},
|
||||
tempToken: {
|
||||
invalid: '"tempToken" is invalid'
|
||||
}
|
||||
}
|
||||
|
||||
resetPasswordRouter.use('/', putResetPasswordRouter)
|
||||
resetPasswordRouter.use('/', postResetPasswordRouter)
|
@ -1,73 +0,0 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import ms from 'ms'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { errorsMessages as errorsConfirmed } from '../../../tools/middlewares/authenticateUser'
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import UserSetting from '../../../models/UserSetting'
|
||||
import { sendEmail } from '../../../tools/email/sendEmail'
|
||||
import { BadRequestError } from '../../../tools/errors/BadRequestError'
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
notExist: "Email address doesn't exist"
|
||||
},
|
||||
password: {
|
||||
alreadyInProgress: 'A request to reset-password is already in progress'
|
||||
},
|
||||
tempToken: {
|
||||
invalid: '"tempToken" is invalid'
|
||||
}
|
||||
}
|
||||
|
||||
export const postResetPasswordRouter = Router()
|
||||
|
||||
postResetPasswordRouter.post(
|
||||
'/users/resetPassword',
|
||||
[
|
||||
body('email')
|
||||
.trim()
|
||||
.isEmail()
|
||||
.withMessage(errorsMessages.email.mustBeValid),
|
||||
query('redirectURI').notEmpty().trim()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { email } = req.body as { email: string }
|
||||
const { redirectURI } = req.query as { redirectURI: string }
|
||||
|
||||
const user = await User.findOne({ where: { email } })
|
||||
if (user == null) {
|
||||
throw new BadRequestError(errorsMessages.email.notExist)
|
||||
}
|
||||
if (!user.isConfirmed) {
|
||||
throw new BadRequestError(errorsConfirmed.invalidAccount)
|
||||
}
|
||||
const isValidTempToken =
|
||||
user.tempExpirationToken != null && user.tempExpirationToken > Date.now()
|
||||
if (user.tempToken != null && isValidTempToken) {
|
||||
throw new BadRequestError(errorsMessages.password.alreadyInProgress)
|
||||
}
|
||||
|
||||
const tempToken = uuidv4()
|
||||
user.tempToken = tempToken
|
||||
user.tempExpirationToken = Date.now() + ms('1 hour')
|
||||
await user.save()
|
||||
const userSettings = await UserSetting.findOne({
|
||||
where: { userId: user.id }
|
||||
})
|
||||
await sendEmail({
|
||||
type: 'reset-password',
|
||||
email,
|
||||
url: `${redirectURI}?tempToken=${tempToken}`,
|
||||
language: userSettings?.language,
|
||||
theme: userSettings?.theme
|
||||
})
|
||||
return res.status(200).json({
|
||||
message: 'Password-reset request successful, please check your emails!'
|
||||
})
|
||||
}
|
||||
)
|
@ -1,53 +0,0 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body } from 'express-validator'
|
||||
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import { BadRequestError } from '../../../tools/errors/BadRequestError'
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
notExist: "Email address doesn't exist"
|
||||
},
|
||||
password: {
|
||||
alreadyInProgress: 'A request to reset-password is already in progress'
|
||||
},
|
||||
tempToken: {
|
||||
invalid: '"tempToken" is invalid'
|
||||
}
|
||||
}
|
||||
|
||||
export const putResetPasswordRouter = Router()
|
||||
|
||||
putResetPasswordRouter.put(
|
||||
'/users/resetPassword',
|
||||
[
|
||||
body('password')
|
||||
.trim()
|
||||
.notEmpty(),
|
||||
body('tempToken')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { password, tempToken } = req.body as {
|
||||
password: string
|
||||
tempToken: string
|
||||
}
|
||||
const user = await User.findOne({ where: { tempToken } })
|
||||
const isValidTempToken =
|
||||
user?.tempExpirationToken != null && user.tempExpirationToken > Date.now()
|
||||
if (user == null || !isValidTempToken) {
|
||||
throw new BadRequestError(errorsMessages.tempToken.invalid)
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 12)
|
||||
user.password = hashedPassword
|
||||
user.tempToken = null
|
||||
user.tempExpirationToken = null
|
||||
await user.save()
|
||||
return res.status(200).json({ message: 'The new password has been saved!' })
|
||||
}
|
||||
)
|
@ -1,29 +0,0 @@
|
||||
/users/signin:
|
||||
post:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Signin the user'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
email:
|
||||
type: 'string'
|
||||
format: 'email'
|
||||
password:
|
||||
type: 'string'
|
||||
format: 'password'
|
||||
example: 'password'
|
||||
required:
|
||||
- 'email'
|
||||
- 'password'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/definitions/RefreshTokenResponse'
|
@ -1,65 +1,80 @@
|
||||
import request from 'supertest'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
import application from '../../../../application'
|
||||
import User from '../../../../models/User'
|
||||
import { errorsMessages } from '../post'
|
||||
import { application } from '../../../../application.js'
|
||||
import { refreshTokenExample } from '../../../../models/RefreshToken.js'
|
||||
import { userExample } from '../../../../models/User.js'
|
||||
import { expiresIn } from '../../../../tools/utils/jwtToken.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
|
||||
const payload = {
|
||||
email: userExample.email,
|
||||
password: userExample.password
|
||||
}
|
||||
|
||||
describe('POST /users/signin', () => {
|
||||
it('succeeds with valid credentials', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
const password = 'test'
|
||||
const response = await request(application)
|
||||
.post('/users/signup')
|
||||
.send({ name, email, password })
|
||||
.expect(201)
|
||||
|
||||
const user = await User.findOne({ where: { id: response.body.user.id } })
|
||||
if (user != null) {
|
||||
await request(application)
|
||||
.get(`/users/confirmEmail?tempToken=${user.tempToken as string}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
await request(application)
|
||||
.post('/users/signin')
|
||||
.send({ email, password })
|
||||
.expect(200)
|
||||
it('succeeds', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue({
|
||||
...userExample,
|
||||
password: await bcrypt.hash(userExample.password as string, 12)
|
||||
})
|
||||
prismaMock.refreshToken.create.mockResolvedValue(refreshTokenExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signin',
|
||||
payload
|
||||
})
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(200)
|
||||
expect(responseJson.type).toEqual('Bearer')
|
||||
expect(responseJson.expiresIn).toEqual(expiresIn)
|
||||
})
|
||||
|
||||
it('fails with unconfirmed account and valid credentials', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
const password = 'test'
|
||||
await request(application)
|
||||
.post('/users/signup')
|
||||
.send({ name, email, password })
|
||||
.expect(201)
|
||||
|
||||
await request(application)
|
||||
.post('/users/signin')
|
||||
.send({ email, password })
|
||||
.expect(400)
|
||||
it('fails with invalid user', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue(null)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signin',
|
||||
payload
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('fails with invalid credentials', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
const password = 'test'
|
||||
await request(application)
|
||||
.post('/users/signup')
|
||||
.send({ name, email, password })
|
||||
.expect(201)
|
||||
it('fails with invalid email', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue(null)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signin',
|
||||
payload: {
|
||||
...payload,
|
||||
email: 'incorrect-email'
|
||||
}
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/signin')
|
||||
.send({ email, password: 'some random password' })
|
||||
.expect(400)
|
||||
it("fails if user hasn't got a password", async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue({
|
||||
...userExample,
|
||||
password: null
|
||||
})
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signin',
|
||||
payload: payload
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
errorsMessages.invalidCredentials
|
||||
)
|
||||
it('fails with incorrect password', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue(userExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signin',
|
||||
payload: {
|
||||
...payload,
|
||||
password: userExample.password
|
||||
}
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
})
|
||||
|
@ -1,72 +1,72 @@
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body } from 'express-validator'
|
||||
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import { userSchema } from '../../../models/User.js'
|
||||
import {
|
||||
expiresIn,
|
||||
generateAccessToken,
|
||||
generateRefreshToken,
|
||||
ResponseJWT
|
||||
} from '../../../tools/configurations/jwtToken'
|
||||
import { BadRequestError } from '../../../tools/errors/BadRequestError'
|
||||
jwtSchema,
|
||||
expiresIn
|
||||
} from '../../../tools/utils/jwtToken.js'
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid'
|
||||
},
|
||||
password: {
|
||||
required: 'Password is required'
|
||||
},
|
||||
invalidCredentials: 'Invalid credentials'
|
||||
}
|
||||
const bodyPostSigninSchema = Type.Object({
|
||||
email: userSchema.email,
|
||||
password: userSchema.password
|
||||
})
|
||||
|
||||
export const signinRouter = Router()
|
||||
type BodyPostSigninSchemaType = Static<typeof bodyPostSigninSchema>
|
||||
|
||||
signinRouter.post(
|
||||
'/users/signin',
|
||||
[
|
||||
body('email')
|
||||
.trim()
|
||||
.isEmail()
|
||||
.withMessage(errorsMessages.email.mustBeValid),
|
||||
body('password')
|
||||
.notEmpty()
|
||||
.withMessage(errorsMessages.password.required)
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { email, password } = req.body as {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
const user = await User.findOne({ where: { email, isConfirmed: true } })
|
||||
if (user == null) {
|
||||
throw new BadRequestError(errorsMessages.invalidCredentials)
|
||||
}
|
||||
|
||||
if (user.password == null) {
|
||||
throw new BadRequestError(errorsMessages.invalidCredentials)
|
||||
}
|
||||
const isCorrectPassword = await bcrypt.compare(password, user.password)
|
||||
if (!isCorrectPassword) {
|
||||
throw new BadRequestError(errorsMessages.invalidCredentials)
|
||||
}
|
||||
const accessToken = generateAccessToken({
|
||||
currentStrategy: 'local',
|
||||
id: user.id
|
||||
})
|
||||
const refreshToken = await generateRefreshToken({
|
||||
currentStrategy: 'local',
|
||||
id: user.id
|
||||
})
|
||||
const responseJWT: ResponseJWT = {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
type: 'Bearer'
|
||||
}
|
||||
return res.status(200).json(responseJWT)
|
||||
const postSigninSchema: FastifySchema = {
|
||||
description: 'Signin the user',
|
||||
tags: ['users'] as string[],
|
||||
body: bodyPostSigninSchema,
|
||||
response: {
|
||||
200: Type.Object(jwtSchema),
|
||||
400: fastifyErrors[400],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
)
|
||||
} as const
|
||||
|
||||
export const postSigninUser: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.route<{
|
||||
Body: BodyPostSigninSchemaType
|
||||
}>({
|
||||
method: 'POST',
|
||||
url: '/users/signin',
|
||||
schema: postSigninSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { email, password } = request.body
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
})
|
||||
if (user == null) {
|
||||
throw fastify.httpErrors.badRequest('Invalid credentials.')
|
||||
}
|
||||
if (user.password == null) {
|
||||
throw fastify.httpErrors.badRequest('Invalid credentials.')
|
||||
}
|
||||
const isCorrectPassword = await bcrypt.compare(password, user.password)
|
||||
if (!isCorrectPassword) {
|
||||
throw fastify.httpErrors.badRequest('Invalid credentials.')
|
||||
}
|
||||
const accessToken = generateAccessToken({
|
||||
currentStrategy: 'local',
|
||||
id: user.id
|
||||
})
|
||||
const refreshToken = await generateRefreshToken({
|
||||
currentStrategy: 'local',
|
||||
id: user.id
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
type: 'Bearer'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
/users/signout:
|
||||
delete:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Signout the user to every connected devices'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/ForbiddenError'
|
||||
- $ref: '#/definitions/UnauthorizedError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
@ -1,23 +0,0 @@
|
||||
/users/signout:
|
||||
post:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Signout the user'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
refreshToken:
|
||||
type: 'string'
|
||||
required:
|
||||
- 'refreshToken'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
@ -1,29 +1,28 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import RefreshToken from '../../../../models/RefreshToken'
|
||||
import { application } from '../../../../application.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
|
||||
|
||||
describe('DELETE /users/signout', () => {
|
||||
it('succeeds and signout to every devices', async () => {
|
||||
const email = 'johdoe@gmail.com'
|
||||
const name = 'johndoe'
|
||||
const password = 'test'
|
||||
const userToken = await authenticateUserTest({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
shouldBeConfirmed: true
|
||||
it('succeeds', async () => {
|
||||
prismaMock.refreshToken.deleteMany.mockResolvedValue({
|
||||
count: 1
|
||||
})
|
||||
await authenticateUserTest({ name, email, password, alreadySignedUp: true })
|
||||
let refreshToken = await RefreshToken.findAll()
|
||||
expect(refreshToken.length).toEqual(2)
|
||||
await request(application)
|
||||
.delete('/users/signout')
|
||||
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
refreshToken = await RefreshToken.findAll()
|
||||
expect(refreshToken.length).toEqual(0)
|
||||
const { accessToken } = await authenticateUserTest()
|
||||
const response = await application.inject({
|
||||
method: 'DELETE',
|
||||
url: '/users/signout',
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})
|
||||
expect(response.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('fails with empty authorization header', async () => {
|
||||
const response = await application.inject({
|
||||
method: 'DELETE',
|
||||
url: '/users/signout'
|
||||
})
|
||||
expect(response.statusCode).toEqual(401)
|
||||
})
|
||||
})
|
||||
|
@ -1,35 +1,25 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import RefreshToken from '../../../../models/RefreshToken'
|
||||
import { application } from '../../../../application.js'
|
||||
import { refreshTokenExample } from '../../../../models/RefreshToken.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
|
||||
describe('POST /users/signout', () => {
|
||||
it('succeeds and signout', async () => {
|
||||
const userToken = await authenticateUserTest()
|
||||
let refreshToken = await RefreshToken.findAll()
|
||||
expect(refreshToken.length).toEqual(1)
|
||||
|
||||
await request(application)
|
||||
.post('/users/signout')
|
||||
.send({ refreshToken: userToken.refreshToken })
|
||||
.expect(200)
|
||||
|
||||
refreshToken = await RefreshToken.findAll()
|
||||
expect(refreshToken.length).toEqual(0)
|
||||
it('succeeds', async () => {
|
||||
prismaMock.refreshToken.findFirst.mockResolvedValue(refreshTokenExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signout',
|
||||
payload: { refreshToken: refreshTokenExample.token }
|
||||
})
|
||||
expect(response.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('fails with invalid refreshToken', async () => {
|
||||
await authenticateUserTest()
|
||||
let refreshToken = await RefreshToken.findAll()
|
||||
expect(refreshToken.length).toEqual(1)
|
||||
|
||||
await request(application)
|
||||
.post('/users/signout')
|
||||
.send({ refreshToken: 'some invalid token' })
|
||||
.expect(401)
|
||||
|
||||
refreshToken = await RefreshToken.findAll()
|
||||
expect(refreshToken.length).toEqual(1)
|
||||
prismaMock.refreshToken.findFirst.mockResolvedValue(null)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signout',
|
||||
payload: { refreshToken: 'somerandomtoken' }
|
||||
})
|
||||
expect(response.statusCode).toEqual(404)
|
||||
})
|
||||
})
|
||||
|
@ -1,19 +1,45 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
|
||||
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
|
||||
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
|
||||
import { deleteEveryRefreshTokens } from '../__utils__/deleteEveryRefreshTokens'
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import authenticateUser from '../../../tools/plugins/authenticateUser.js'
|
||||
|
||||
export const signoutEveryDevicesRouter = Router()
|
||||
|
||||
signoutEveryDevicesRouter.delete(
|
||||
'/users/signout',
|
||||
authenticateUser,
|
||||
async (req: Request, res: Response) => {
|
||||
if (req.user == null) {
|
||||
throw new ForbiddenError()
|
||||
const deleteSignoutSchema: FastifySchema = {
|
||||
description: 'Signout the user to every connected devices',
|
||||
tags: ['users'] as string[],
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
await deleteEveryRefreshTokens(req.user.current.id)
|
||||
res.status(200).json({})
|
||||
] as Array<{ [key: string]: [] }>,
|
||||
response: {
|
||||
200: Type.Object({}),
|
||||
400: fastifyErrors[400],
|
||||
401: fastifyErrors[401],
|
||||
403: fastifyErrors[403],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
)
|
||||
} as const
|
||||
|
||||
export const deleteSignoutUser: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.register(authenticateUser)
|
||||
|
||||
fastify.route({
|
||||
method: 'DELETE',
|
||||
url: '/users/signout',
|
||||
schema: deleteSignoutSchema,
|
||||
handler: async (request, reply) => {
|
||||
if (request.user == null) {
|
||||
throw fastify.httpErrors.forbidden()
|
||||
}
|
||||
await prisma.refreshToken.deleteMany({
|
||||
where: {
|
||||
userId: request.user.current.id
|
||||
}
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { signoutEveryDevicesRouter } from './delete'
|
||||
import { postSignoutRouter } from './post'
|
||||
|
||||
export const signoutRouter = Router()
|
||||
|
||||
signoutRouter.use('/', signoutEveryDevicesRouter)
|
||||
signoutRouter.use('/', postSignoutRouter)
|
@ -1,29 +1,52 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import RefreshToken from '../../../models/RefreshToken'
|
||||
import { UnauthorizedError } from '../../../tools/errors/UnauthorizedError'
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import { refreshTokensSchema } from '../../../models/RefreshToken.js'
|
||||
|
||||
export const postSignoutRouter = Router()
|
||||
const bodyPostSignoutSchema = Type.Object({
|
||||
refreshToken: refreshTokensSchema.token
|
||||
})
|
||||
|
||||
postSignoutRouter.post(
|
||||
'/users/signout',
|
||||
[
|
||||
body('refreshToken')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { refreshToken } = req.body as { refreshToken: string }
|
||||
const foundRefreshToken = await RefreshToken.findOne({
|
||||
where: { token: refreshToken }
|
||||
})
|
||||
if (foundRefreshToken == null) {
|
||||
throw new UnauthorizedError()
|
||||
}
|
||||
await foundRefreshToken.destroy()
|
||||
res.status(200).json({})
|
||||
type BodyPostSignoutSchemaType = Static<typeof bodyPostSignoutSchema>
|
||||
|
||||
const postSignoutSchema: FastifySchema = {
|
||||
description: 'Signout the user',
|
||||
tags: ['users'] as string[],
|
||||
body: bodyPostSignoutSchema,
|
||||
response: {
|
||||
200: Type.Object({}),
|
||||
400: fastifyErrors[400],
|
||||
404: fastifyErrors[404],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
)
|
||||
} as const
|
||||
|
||||
export const postSignoutUser: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.route<{
|
||||
Body: BodyPostSignoutSchemaType
|
||||
}>({
|
||||
method: 'POST',
|
||||
url: '/users/signout',
|
||||
schema: postSignoutSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { refreshToken } = request.body
|
||||
const token = await prisma.refreshToken.findFirst({
|
||||
where: {
|
||||
token: refreshToken
|
||||
}
|
||||
})
|
||||
if (token == null) {
|
||||
throw fastify.httpErrors.notFound()
|
||||
}
|
||||
await prisma.refreshToken.delete({
|
||||
where: {
|
||||
id: token.id
|
||||
}
|
||||
})
|
||||
reply.statusCode = 200
|
||||
return {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
/users/signup:
|
||||
post:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Signup the user'
|
||||
description: 'Allows a new user to signup, if success he would need to confirm his email.'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
email:
|
||||
type: 'string'
|
||||
format: 'email'
|
||||
name:
|
||||
type: 'string'
|
||||
minLength: 3
|
||||
maxLength: 30
|
||||
example: 'user'
|
||||
password:
|
||||
type: 'string'
|
||||
format: 'password'
|
||||
example: 'password'
|
||||
language:
|
||||
allOf:
|
||||
- $ref: '#/definitions/Language'
|
||||
theme:
|
||||
allOf:
|
||||
- $ref: '#/definitions/Theme'
|
||||
required:
|
||||
- 'email'
|
||||
- 'name'
|
||||
- 'password'
|
||||
parameters:
|
||||
- name: 'redirectURI'
|
||||
description: 'The redirect URI to redirect the user when he successfuly confirm his email (could be a signin page), if not provided it will redirect the user to a simple page with a message to tell the user he can now signin.'
|
||||
in: 'query'
|
||||
required: false
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- '201':
|
||||
description: 'User created and send an email to confirm the account'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
@ -1,124 +1,54 @@
|
||||
import request from 'supertest'
|
||||
import { application } from '../../../../application.js'
|
||||
import { userExample } from '../../../../models/User.js'
|
||||
import { userSettingsExample } from '../../../../models/UserSettings.js'
|
||||
import { prismaMock } from '../../../../__test__/setup.js'
|
||||
|
||||
import { formatErrors } from '../../../../__test__/utils/formatErrors'
|
||||
import application from '../../../../application'
|
||||
import User from '../../../../models/User'
|
||||
import { commonErrorsMessages } from '../../../../tools/configurations/constants'
|
||||
import { errorsMessages } from '../post'
|
||||
const payload = {
|
||||
name: userExample.name,
|
||||
email: userExample.email,
|
||||
password: userExample.password,
|
||||
theme: userSettingsExample.theme,
|
||||
language: userSettingsExample.language
|
||||
}
|
||||
|
||||
describe('POST /users/signup', () => {
|
||||
it('succeeds and create a new user', async () => {
|
||||
let users = await User.findAll()
|
||||
expect(users.length).toEqual(0)
|
||||
|
||||
await request(application)
|
||||
.post('/users/signup')
|
||||
.send({
|
||||
name: 'John',
|
||||
email: 'contact@test.com',
|
||||
password: 'test'
|
||||
})
|
||||
.expect(201)
|
||||
|
||||
users = await User.findAll()
|
||||
expect(users.length).toEqual(1)
|
||||
it('succeeds', async () => {
|
||||
prismaMock.user.findFirst.mockResolvedValue(null)
|
||||
prismaMock.user.create.mockResolvedValue(userExample)
|
||||
prismaMock.userSetting.create.mockResolvedValue(userSettingsExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signup',
|
||||
payload
|
||||
})
|
||||
const responseJson = response.json()
|
||||
expect(response.statusCode).toEqual(201)
|
||||
expect(responseJson.user.name).toEqual(userExample.name)
|
||||
expect(responseJson.user.email).toEqual(userExample.email)
|
||||
})
|
||||
|
||||
it('fails with invalid email', async () => {
|
||||
let users = await User.findAll()
|
||||
expect(users.length).toEqual(0)
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/signup')
|
||||
.send({
|
||||
name: 'Divlo',
|
||||
email: 'incorrect@email',
|
||||
password: 'test'
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
errorsMessages.email.mustBeValid
|
||||
)
|
||||
|
||||
users = await User.findAll()
|
||||
expect(users.length).toEqual(0)
|
||||
prismaMock.user.findFirst.mockResolvedValue(null)
|
||||
prismaMock.user.create.mockResolvedValue(userExample)
|
||||
prismaMock.userSetting.create.mockResolvedValue(userSettingsExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signup',
|
||||
payload: {
|
||||
...payload,
|
||||
email: 'incorrect-email'
|
||||
}
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('fails with invalid name', async () => {
|
||||
let users = await User.findAll()
|
||||
expect(users.length).toEqual(0)
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/signup')
|
||||
.send({
|
||||
name: 'jo',
|
||||
email: 'contact@email.com',
|
||||
password: 'test'
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
|
||||
)
|
||||
|
||||
users = await User.findAll()
|
||||
expect(users.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('fails with invalid name and invalid email', async () => {
|
||||
let users = await User.findAll()
|
||||
expect(users.length).toEqual(0)
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/signup')
|
||||
.send({
|
||||
name: 'jo',
|
||||
email: 'contact@email',
|
||||
password: 'test'
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors.length).toEqual(2)
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 }),
|
||||
errorsMessages.email.mustBeValid
|
||||
])
|
||||
)
|
||||
|
||||
users = await User.findAll()
|
||||
expect(users.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('fails with name and email already used', async () => {
|
||||
const name = 'John'
|
||||
const email = 'contact@test.com'
|
||||
await request(application)
|
||||
.post('/users/signup')
|
||||
.send({
|
||||
name,
|
||||
email,
|
||||
password: 'test'
|
||||
})
|
||||
.expect(201)
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/signup')
|
||||
.send({
|
||||
name,
|
||||
email,
|
||||
password: 'test'
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
const errors = formatErrors(response.body.errors)
|
||||
expect(errors.length).toEqual(2)
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining(['Name already used', 'Email already used'])
|
||||
)
|
||||
it('fails with already taken `name` or `email`', async () => {
|
||||
prismaMock.user.findFirst.mockResolvedValue(userExample)
|
||||
const response = await application.inject({
|
||||
method: 'POST',
|
||||
url: '/users/signup',
|
||||
payload
|
||||
})
|
||||
expect(response.statusCode).toEqual(400)
|
||||
})
|
||||
})
|
||||
|
@ -1,102 +1,92 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import UserSetting, {
|
||||
Language,
|
||||
languages,
|
||||
Theme,
|
||||
themes
|
||||
} from '../../../models/UserSetting'
|
||||
import { commonErrorsMessages } from '../../../tools/configurations/constants'
|
||||
import { sendEmail } from '../../../tools/email/sendEmail'
|
||||
import { alreadyUsedValidation } from '../../../tools/validations/alreadyUsedValidation'
|
||||
import { onlyPossibleValuesValidation } from '../../../tools/validations/onlyPossibleValuesValidation'
|
||||
import prisma from '../../../tools/database/prisma.js'
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import {
|
||||
bodyUserSchema,
|
||||
BodyUserSchemaType,
|
||||
userPublicSchema
|
||||
} from '../../../models/User.js'
|
||||
import { sendEmail } from '../../../tools/email/sendEmail.js'
|
||||
import { HOST, PORT } from '../../../tools/configurations/index.js'
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid'
|
||||
const queryPostSignupSchema = Type.Object({
|
||||
redirectURI: Type.Optional(Type.String({ format: 'uri-reference' }))
|
||||
})
|
||||
|
||||
type QueryPostSignupSchemaType = Static<typeof queryPostSignupSchema>
|
||||
|
||||
const postSignupSchema: FastifySchema = {
|
||||
description:
|
||||
'Allows a new user to signup, if success he would need to confirm his email.',
|
||||
tags: ['users'] as string[],
|
||||
body: bodyUserSchema,
|
||||
querystring: queryPostSignupSchema,
|
||||
response: {
|
||||
201: Type.Object({ user: Type.Object(userPublicSchema) }),
|
||||
400: fastifyErrors[400],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
export const signupRouter = Router()
|
||||
|
||||
signupRouter.post(
|
||||
'/users/signup',
|
||||
[
|
||||
body('email')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.isEmail()
|
||||
.withMessage(errorsMessages.email.mustBeValid)
|
||||
.custom(async (email: string) => {
|
||||
return await alreadyUsedValidation(User, 'email', email)
|
||||
}),
|
||||
body('name')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.isLength({ max: 30, min: 3 })
|
||||
.withMessage(
|
||||
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
|
||||
)
|
||||
.custom(async (name: string) => {
|
||||
return await alreadyUsedValidation(User, 'name', name)
|
||||
}),
|
||||
body('password').trim().notEmpty().isString(),
|
||||
body('theme')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.isString()
|
||||
.custom(async (theme: Theme) => {
|
||||
return await onlyPossibleValuesValidation([...themes], 'theme', theme)
|
||||
}),
|
||||
body('language')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.isString()
|
||||
.custom(async (language: Language) => {
|
||||
return await onlyPossibleValuesValidation(
|
||||
languages,
|
||||
'language',
|
||||
language
|
||||
export const postSignupUser: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.route<{
|
||||
Body: BodyUserSchemaType
|
||||
Querystring: QueryPostSignupSchemaType
|
||||
}>({
|
||||
method: 'POST',
|
||||
url: '/users/signup',
|
||||
schema: postSignupSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { name, email, password, theme, language } = request.body
|
||||
const { redirectURI } = request.query
|
||||
const userValidation = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email }, { name }]
|
||||
}
|
||||
})
|
||||
if (userValidation != null) {
|
||||
throw fastify.httpErrors.badRequest(
|
||||
'body.email or body.name already taken.'
|
||||
)
|
||||
}),
|
||||
query('redirectURI').optional({ nullable: true }).trim()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { name, email, password, theme, language } = req.body as {
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
theme?: Theme
|
||||
language?: Language
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 12)
|
||||
const temporaryToken = randomUUID()
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
temporaryToken
|
||||
}
|
||||
})
|
||||
const userSettings = await prisma.userSetting.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
theme,
|
||||
language
|
||||
}
|
||||
})
|
||||
const redirectQuery =
|
||||
redirectURI != null ? `&redirectURI=${redirectURI}` : ''
|
||||
await sendEmail({
|
||||
type: 'confirm-email',
|
||||
email,
|
||||
url: `${request.protocol}://${HOST}:${PORT}/users/confirm-email?temporaryToken=${temporaryToken}${redirectQuery}`,
|
||||
language,
|
||||
theme
|
||||
})
|
||||
reply.statusCode = 201
|
||||
return {
|
||||
user: {
|
||||
...user,
|
||||
settings: { ...userSettings }
|
||||
}
|
||||
}
|
||||
}
|
||||
const { redirectURI } = req.query as { redirectURI?: string }
|
||||
const hashedPassword = await bcrypt.hash(password, 12)
|
||||
const tempToken = uuidv4()
|
||||
const user = await User.create({
|
||||
email,
|
||||
name,
|
||||
password: hashedPassword,
|
||||
tempToken
|
||||
})
|
||||
const userSettings = await UserSetting.create({
|
||||
userId: user.id,
|
||||
theme: theme ?? 'dark',
|
||||
language: language ?? 'en'
|
||||
})
|
||||
const redirectQuery = redirectURI != null ? `&redirectURI=${redirectURI}` : ''
|
||||
await sendEmail({
|
||||
type: 'confirm-email',
|
||||
email,
|
||||
url: `${process.env.API_BASE_URL}/users/confirmEmail?tempToken=${tempToken}${redirectQuery}`,
|
||||
language: userSettings.language,
|
||||
theme: userSettings.theme
|
||||
})
|
||||
return res.status(201).json({ user })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user