Skip to content

Commit

Permalink
feat(api): Add resend otp implementation (keyshade-xyz#445)
Browse files Browse the repository at this point in the history
Co-authored-by: Rajdip Bhattacharya <[email protected]>
  • Loading branch information
2 people authored and Kiranchaudhary537 committed Oct 13, 2024
1 parent b94648c commit 2b39ab0
Show file tree
Hide file tree
Showing 18 changed files with 522 additions and 154 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ DOMAIN=localhost
FEEDBACK_FORWARD_EMAIL=

BACKEND_URL=http://localhost:4200
NEXT_PUBLIC_BACKEND_URL=http://localhost:4200
NEXT_PUBLIC_BACKEND_URL=http://localhost:4200

NODE_ENV=dev
3 changes: 2 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@nestjs/platform-socket.io": "^10.3.7",
"@nestjs/schedule": "^4.0.1",
"@nestjs/swagger": "^7.3.0",
"@nestjs/throttler": "^6.2.1",
"@nestjs/websockets": "^10.3.7",
"@socket.io/redis-adapter": "^8.3.0",
"@supabase/supabase-js": "^2.39.6",
Expand All @@ -50,7 +51,6 @@
"uuid": "^9.0.1"
},
"devDependencies": {
"reflect-metadata": "^0.2.2",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
Expand All @@ -67,6 +67,7 @@
"jest-mock-extended": "^3.0.5",
"prettier": "^3.0.0",
"prisma": "5.19.1",
"reflect-metadata": "^0.2.2",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { ConfigModule } from '@nestjs/config'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { PassportModule } from '@nestjs/passport'
import { AuthModule } from '@/auth/auth.module'
import { PrismaModule } from '@/prisma/prisma.module'
Expand All @@ -25,6 +25,7 @@ import { IntegrationModule } from '@/integration/integration.module'
import { FeedbackModule } from '@/feedback/feedback.module'
import { CacheModule } from '@/cache/cache.module'
import { WorkspaceMembershipModule } from '@/workspace-membership/workspace-membership.module'
import { seconds, ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'

@Module({
controllers: [AppController],
Expand All @@ -38,6 +39,7 @@ import { WorkspaceMembershipModule } from '@/workspace-membership/workspace-memb
abortEarly: true
}
}),

ScheduleModule.forRoot(),
PassportModule,
AuthModule,
Expand Down
14 changes: 13 additions & 1 deletion apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { GoogleOAuthStrategyFactory } from '@/config/factory/google/google-strat
import { GoogleStrategy } from '@/config/oauth-strategy/google/google.strategy'
import { GitlabOAuthStrategyFactory } from '@/config/factory/gitlab/gitlab-strategy.factory'
import { GitlabStrategy } from '@/config/oauth-strategy/gitlab/gitlab.strategy'
import { seconds, ThrottlerModule } from '@nestjs/throttler'
import { ConfigModule, ConfigService } from '@nestjs/config'

@Module({
imports: [
Expand All @@ -21,7 +23,17 @@ import { GitlabStrategy } from '@/config/oauth-strategy/gitlab/gitlab.strategy'
algorithm: 'HS256'
}
}),
UserModule
UserModule,
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => [
{
ttl: seconds(config.get('THROTTLE_TTL')),
limit: config.get('THROTTLE_LIMIT')
}
],
inject: [ConfigService]
})
],
providers: [
AuthService,
Expand Down
21 changes: 20 additions & 1 deletion apps/api/src/auth/controller/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { GoogleOAuthStrategyFactory } from '@/config/factory/google/google-strat
import { GitlabOAuthStrategyFactory } from '@/config/factory/gitlab/gitlab-strategy.factory'
import { CacheService } from '@/cache/cache.service'
import { REDIS_CLIENT } from '@/provider/redis.provider'
import { ThrottlerGuard, ThrottlerStorage } from '@nestjs/throttler'
import { Reflector } from '@nestjs/core'

describe('AuthController', () => {
let controller: AuthController
Expand Down Expand Up @@ -40,7 +42,21 @@ describe('AuthController', () => {
keys: jest.fn()
}
}
}
},
//Mocked values for throttler
{
provide: ThrottlerGuard,
useValue: { canActivate: jest.fn(() => true) } // Mocking ThrottlerGuard
},
{
provide: 'THROTTLER:MODULE_OPTIONS', // Mocking THROTTLER:MODULE_OPTIONS
useValue: {} // Empty or default value to satisfy dependency
},
{
provide: ThrottlerStorage, // Mocking Symbol(ThrottlerStorage)
useValue: {} // Empty or default value to satisfy dependency
},
Reflector
]
})
.overrideProvider(PrismaService)
Expand All @@ -53,4 +69,7 @@ describe('AuthController', () => {
it('should be defined', () => {
expect(controller).toBeDefined()
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
})
11 changes: 11 additions & 0 deletions apps/api/src/auth/controller/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
sendOAuthSuccessRedirect
} from '@/common/redirect'
import { setCookie } from '@/common/util'
import { ThrottlerGuard } from '@nestjs/throttler'

@Controller('auth')
export class AuthController {
Expand All @@ -46,6 +47,16 @@ export class AuthController {
await this.authService.sendOtp(email)
}

@Public()
@Post('resend-otp/:email')
@UseGuards(ThrottlerGuard)
async resendOtp(
@Param('email')
email: string
): Promise<void> {
return await this.authService.resendOtp(email)
}

/* istanbul ignore next */
@Public()
@Post('validate-otp')
Expand Down
14 changes: 12 additions & 2 deletions apps/api/src/auth/service/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,23 @@ export class AuthService {
}

const user = await this.createUserIfNotExists(email, AuthProvider.EMAIL_OTP)

const otp = await generateOtp(email, user.id, this.prisma)

await this.mailService.sendOtp(email, otp.code)

this.logger.log(`Login code sent to ${email}`)
}

/**
* resend a login code to the given email address after resend otp button is pressed
* @throws {BadRequestException} If the email address is invalid
* @param email The email address to resend the login code to
*/
async resendOtp(email: string): Promise<void> {
const user = await getUserByEmailOrId(email, this.prisma)
const otp = await generateOtp(email, user.id, this.prisma)
await this.mailService.sendOtp(email, otp.code)
}

/* istanbul ignore next */
/**
* Validates a login code sent to the given email address
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/common/env/env.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ const devSchema = z.object({
MINIO_BUCKET_NAME: z.string().optional(),
MINIO_USE_SSL: z.string().optional(),

FEEDBACK_FORWARD_EMAIL: z.string().email()
FEEDBACK_FORWARD_EMAIL: z.string().email(),
THROTTLE_TTL: z.string().transform((val) => parseInt(val, 10)), // Convert string to number
THROTTLE_LIMIT: z.string().transform((val) => parseInt(val, 10)) // Convert string to number
})

const prodSchema = z.object({
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/environment/list.environment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import BaseCommand from '../base.command'
import ControllerInstance from '@/util/controller-instance'
import {
CommandOption,
type CommandOption,
type CommandActionData,
type CommandArgument
} from 'src/types/command/command.types'
Expand Down
5 changes: 4 additions & 1 deletion apps/cli/src/commands/workspace/list.workspace.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import BaseCommand from '@/commands/base.command'
import { Logger } from '@/util/logger'
import ControllerInstance from '@/util/controller-instance'
import { CommandActionData, CommandOption } from '@/types/command/command.types'
import {
type CommandActionData,
type CommandOption
} from '@/types/command/command.types'
import { PAGINATION_OPTION } from '@/util/pagination-options'

export default class ListWorkspace extends BaseCommand {
Expand Down
6 changes: 3 additions & 3 deletions apps/cli/src/commands/workspace/role/get.role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ export default class GetRoleCommand extends BaseCommand {
)

if (success) {
Logger.info(`Workspace role fetched successfully!`)
Logger.info('Workspace role fetched successfully!')
Logger.info(`Workspace role: ${data.name} (${data.slug})`)
Logger.info(`Description: ${data.description || 'N/A'}`)
Logger.info(`Created at ${data.createdAt}`)
Logger.info(`Updated at ${data.updatedAt}`)
Logger.info(`Authorities:`)
Logger.info('Authorities:')
for (const authority of data.authorities) {
Logger.info(`- ${authority}`)
}
Logger.info(`Projects:`)
Logger.info('Projects:')
for (const project of data.projects) {
Logger.info(`- ${project.project.name} (${project.project.slug})`)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/workspace/role/list.role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import BaseCommand from '@/commands/base.command'
import {
type CommandActionData,
type CommandArgument,
CommandOption
type CommandOption
} from '@/types/command/command.types'
import { Logger } from '@/util/logger'
import ControllerInstance from '@/util/controller-instance'
Expand Down
8 changes: 4 additions & 4 deletions apps/cli/src/commands/workspace/role/update.role.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import BaseCommand from '@/commands/base.command'
import {
CommandOption,
type CommandOption,
type CommandActionData,
type CommandArgument
} from '@/types/command/command.types'
Expand Down Expand Up @@ -76,17 +76,17 @@ export default class UpdateRoleCommand extends BaseCommand {
)

if (success) {
Logger.info(`Workspace role updated successfully:`)
Logger.info('Workspace role updated successfully:')
Logger.info(`Workspace role: ${data.name} (${data.slug})`)
Logger.info(`Description: ${data.description || 'N/A'}`)
Logger.info(`Created at ${data.createdAt}`)
Logger.info(`Updated at ${data.updatedAt}`)
Logger.info(`Color code: ${data.colorCode}`)
Logger.info(`Authorities:`)
Logger.info('Authorities:')
for (const authority of data.authorities) {
Logger.info(`- ${authority}`)
}
Logger.info(`Projects:`)
Logger.info('Projects:')
for (const project of data.projects) {
Logger.info(`- ${project.project.name} (${project.project.slug})`)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/util/pagination-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CommandOption } from '@/types/command/command.types'
import { type CommandOption } from '@/types/command/command.types'

export const PAGINATION_OPTION: CommandOption[] = [
{
Expand Down
7 changes: 6 additions & 1 deletion apps/platform/src/app/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { useRouter } from 'next/navigation'
import { useAtom } from 'jotai'
import Cookies from 'js-cookie'
import { LoadingSVG } from '@public/svg/shared'
import { GithubSVG, GoogleSVG, KeyshadeBigSVG ,GitlabSVG} from '@public/svg/auth'
import {
GithubSVG,
GoogleSVG,
KeyshadeBigSVG,
GitlabSVG
} from '@public/svg/auth'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { authEmailAtom } from '@/store'
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/(main)/career/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ColorBGSVG } from '@public/hero'
import Link from 'next/link'
import { ColorBGSVG } from '@public/hero'
import EncryptButton from '@/components/ui/encrypt-btn'

function Career(): React.JSX.Element {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export default class WorkspaceMembershipController {
request: GetMembersRequest,
headers?: Record<string, string>
): Promise<ClientResponse<GetMembersResponse>> {
let url = parsePaginationUrl(
const url = parsePaginationUrl(
`/api/workspace-membership/${request.workspaceSlug}/members`,
request
)
Expand Down
Loading

0 comments on commit 2b39ab0

Please sign in to comment.