Skip to content

Commit

Permalink
Implement recovery process, fix bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
PhamAnhHoang committed Feb 9, 2024
1 parent b881010 commit 4a219fd
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 21 deletions.
1 change: 0 additions & 1 deletion apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
UseGuards,
UseInterceptors
} from '@nestjs/common'
import { instanceToPlain } from 'class-transformer'

import { AuthUser } from '../user/user.decorator'
import { UserEntity } from '../entities/user.entity'
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export class AuthService {
const user = await this.userService.findOne({
where: { twoFASecret: secret }
})
if (user) {
if (!user) {
throw new UnauthorizedException(`There isn't any user with this code`)
}

Expand Down
17 changes: 7 additions & 10 deletions apps/api/src/mailer/mailer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,19 @@ export class MailerService {
'[email protected]'
)

const auth = user && pass ? { auth: { user, pass } } : {}

this.transporter = nodemailer.createTransport(
{
host,
port,
secure,
auth: {
user,
pass
}
...auth,
debug: process.env.NODE_ENV === 'development',
logger: process.env.NODE_ENV === 'development'
},
{
from: {
name: fromName,
address: fromAddress
}
from: `"${fromName}" <${fromAddress}>`
}
)
}
Expand All @@ -56,7 +54,7 @@ export class MailerService {
}

const templatesFolderPath = path.join(__dirname, './templates')
const templatePath = path.join(templatesFolderPath, templateName)
const templatePath = path.join(templatesFolderPath, `${templateName}.hbs`)

const templateSource = fs.readFileSync(templatePath, 'utf8')

Expand All @@ -83,7 +81,6 @@ export class MailerService {
html: html
})
} catch (err) {
console.error(err)
throw new HttpException(
'Email could not be sent',
HttpStatus.INTERNAL_SERVER_ERROR
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class UserService {
) {}

async create(data: Partial<UserEntity>): Promise<UserEntity> {
const user = this.userRepository.create(data)
const user = this.userRepository.create({ ...data, isTwoFAEnabled: false })

return this.userRepository.save(user)
}
Expand Down
47 changes: 47 additions & 0 deletions apps/platform/src/hooks/Recover2FAHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
HandleErrorOptions,
Recover2FAData,
auth2FARecoveryService
} from '@isomera/impl'
import { useFormik } from 'formik'
import * as Yup from 'yup'

interface Options {
onSuccess?: (response: any) => void
onError?: (error: any) => void
userEmail?: string
}

export const useRecoveryHook = (options: Options) => {
const { onSuccess, onError, userEmail } = options

const initialValues: Recover2FAData = {
code: '',
email: userEmail ?? ''
}

const validationSchema = Yup.object({
code: Yup.string().required('2FA code is required'),
email: Yup.string().email('Invalid email').required('Email is required')
})

const onSubmit = async (values: Recover2FAData) => {
try {
const response = await auth2FARecoveryService(values)
onSuccess && onSuccess(response)
} catch (error) {
onError && onError(error)
}
}

const formik = useFormik({
initialValues,
validationSchema,
onSubmit
})

return {
...formik,
isLoading: formik.isSubmitting
}
}
1 change: 0 additions & 1 deletion apps/platform/src/router/privateRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ function PrivateRoute(props: Readonly<Props>) {
return null
}

console.log({ isAuthenticated })
if (!isAuthenticated) {
return <Navigate to={pages.login.path} />
}
Expand Down
1 change: 0 additions & 1 deletion apps/platform/src/router/publicRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ function PublicRoute(props: Props) {

const { isAuthenticated, user } = useSession()

console.log({ user })
if (
isAuthenticated &&
user &&
Expand Down
12 changes: 12 additions & 0 deletions apps/platform/src/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PublicLayout } from '../layouts/public.layout'
import { PrivateLayout } from '../layouts/private.layout'
import { UserSecurityView } from '../views/user /profile-security.view'
import { Verify2FAView } from '../views/auth/auth2FA.view'
import { Recovery2FAView } from '../views/auth/recovery.view'

function Router() {
return (
Expand Down Expand Up @@ -106,6 +107,17 @@ function Router() {
}
/>

<Route
path={pages.recoverTwoFA.path}
element={
<PublicRoute>
<PublicLayout>
<Recovery2FAView />
</PublicLayout>
</PublicRoute>
}
/>

<Route
path={pages.userInfo.path}
element={
Expand Down
1 change: 0 additions & 1 deletion apps/platform/src/views/auth/auth2FA.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export const Verify2FAView = () => {
const { handleError } = useHandleErrorHook()

const onSuccess = (data: { access_token: string; refresh_token: string }) => {
console.log({ data })
loginWith2FA(data.access_token, data.refresh_token)
navigate(pages.dashboard.path)
}
Expand Down
52 changes: 52 additions & 0 deletions apps/platform/src/views/auth/recovery.view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useNavigate } from 'react-router-dom'
import useSession from '../../hooks/useSession'
import { pages, useHandleErrorHook } from '@isomera/impl'
import { useRecoveryHook } from '../../hooks/Recover2FAHook'

export const Recovery2FAView = () => {
const { loginWith2FA, user } = useSession()
const navigate = useNavigate()
const { handleError } = useHandleErrorHook()

const onSuccess = (data: { access_token: string; refresh_token: string }) => {
loginWith2FA(data.access_token, data.refresh_token)
navigate(pages.dashboard.path)
}

const {
values,
handleChange,
handleBlur,
errors,
touched,
handleSubmit,
isSubmitting
} = useRecoveryHook({
onSuccess,
onError: (error: any) => handleError(error, { view: 'recovery' }),
userEmail: user?.email
})

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="recoveryCode">Recovery Code</label>
<input
id="code"
name="code"
type="text"
onChange={handleChange}
onBlur={handleBlur}
value={values.code}
required
/>
{touched.code && errors.code && <span>{errors.code}</span>}
</div>
<div>
<button type="submit" disabled={isSubmitting}>
Recover Account
</button>
</div>
</form>
)
}
2 changes: 1 addition & 1 deletion apps/platform/src/views/auth/signIn.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const SignInView = () => {

const onSuccess = (data: UserInterface) => {
setUser(data)
console.log({ data })

if (data && data.isTwoFAEnabled && !data.isTwoFactorAuthenticated) {
navigate(pages.twoFA.path)
} else {
Expand Down
1 change: 0 additions & 1 deletion libs/dtos/src/utils/formik.validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export type Class = { new (...args: any[]): any }
export type Data = { [key: string]: string | number | boolean }

export const formikValidate = (model: Class, data: Data) => {
console.log({ model, data })
try {
transformAndValidateSync(model, data)

Expand Down
1 change: 1 addition & 0 deletions libs/impl/src/constants/apiRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const API_RESET_ROUTE = '/auth/reset'
export const API_AUTH_2FA_STEP_1 = 'auth/2fa/generate'
export const API_AUTH_2FA_STEP_2 = 'auth/2fa/turn-on'
export const API_AUTH_2FA_STEP_3 = 'auth/2fa/authenticate'
export const API_AUTH_2FA_RECOVER = 'auth/2fa/request-recovery'

// User

Expand Down
2 changes: 1 addition & 1 deletion libs/impl/src/constants/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const pages = {
path: '/two-fa'
},
recoverTwoFA: {
path: 'two-fa/recover'
path: '/two-fa/recover'
},
register: {
path: '/sign-up'
Expand Down
11 changes: 9 additions & 2 deletions libs/impl/src/hooks/error/useHandleError.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ import { toast } from 'react-toastify'
import { ErrorMessageEnum } from '@isomera/interfaces'
import { pascalToSnakeCase } from '@isomera/utils'

interface HandleErrorOptions {
view: 'login' | 'confirm' | 'register' | 'reset' | 'updatePassword' | '2fa'
export interface HandleErrorOptions {
view:
| 'login'
| 'confirm'
| 'register'
| 'reset'
| 'updatePassword'
| '2fa'
| 'recovery'
}

export const useHandleErrorHook = () => {
Expand Down
17 changes: 17 additions & 0 deletions libs/impl/src/services/auth/twoFactor.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { UserInterface } from '@isomera/interfaces'
import {
API_AUTH_2FA_RECOVER,
API_AUTH_2FA_STEP_1,
API_AUTH_2FA_STEP_2,
API_AUTH_2FA_STEP_3
Expand All @@ -24,10 +25,18 @@ export interface Auth2FAResponse extends UserInterface {
refresh_token: string
}

export interface Recover2FAResponse {
status: string
}

export interface Verify2FAData {
code: string
}

export interface Recover2FAData extends Verify2FAData {
email: string
}

export const turnOn2FAServiceStep1 = async (): Promise<GenerateQRResponse> => {
const response = await axiosInstance.post(API_AUTH_2FA_STEP_1)
return response.data
Expand All @@ -48,3 +57,11 @@ export const auth2FAService = async (

return response.data
}

export const auth2FARecoveryService = async (
data: Verify2FAData
): Promise<Auth2FAResponse> => {
const response = await axiosInstance.post(API_AUTH_2FA_RECOVER, data)

return response.data
}

0 comments on commit 4a219fd

Please sign in to comment.