Skip to content

Commit

Permalink
Merge branch 'main' of github.com:interledger/testnet into gatehub-user
Browse files Browse the repository at this point in the history
  • Loading branch information
dragosp1011 committed Sep 16, 2024
2 parents 1dfae0b + 8bbe88f commit 411a1f2
Show file tree
Hide file tree
Showing 24 changed files with 319 additions and 73 deletions.
2 changes: 2 additions & 0 deletions docker/prod/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ WALLET_BACKEND_FROM_EMAIL=
WALLET_BACKEND_SEND_EMAIL=
WALLET_BACKEND_AUTH_DOMAIN=
WALLET_BACKEND_RATE_API_KEY=
WALLET_BACKEND_GATEHUB_ACCESS_KEY=
WALLET_BACKEND_GATEHUB_SECRET_KEY=

# BOUTIQUE
BOUTIQUE_BACKEND_PORT=
Expand Down
2 changes: 2 additions & 0 deletions docker/prod/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ services:
AUTH_DOMAIN: ${WALLET_BACKEND_AUTH_DOMAIN}
RATE_API_KEY: ${WALLET_BACKEND_RATE_API_KEY}
REDIS_URL: ${WALLET_BACKEND_REDIS_URL}
GATEHUB_ACCESS_KEY: ${WALLET_BACKEND_GATEHUB_ACCESS_KEY}
GATEHUB_SECRET_KEY: ${WALLET_BACKEND_GATEHUB_SECRET_KEY}
networks:
- testnet
ports:
Expand Down
2 changes: 1 addition & 1 deletion packages/boutique/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/jest": "^29.5.13",
"@types/node": "^20.12.11",
"jest": "^29.7.0",
"node-mocks-http": "^1.16.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/boutique/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.45",
"tailwindcss": "^3.4.10",
"tailwindcss": "^3.4.11",
"typescript": "^5.6.2",
"vite": "^5.4.3"
}
Expand Down
1 change: 1 addition & 0 deletions packages/shared/backend/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './bad-request'
export * from './conflict'
export * from './not-verified'
export * from './not-found'
export * from './unauthorized'
export * from './forbidden'
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/backend/src/errors/not-verified.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BaseError } from './base'

export class NotVerified extends BaseError {
constructor(message?: string) {
super(403, message ?? 'Not Validated', { email: 'Not verified' })
Object.setPrototypeOf(this, NotVerified.prototype)
}
}
6 changes: 3 additions & 3 deletions packages/wallet/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@faker-js/faker": "^9.0.0",
"@faker-js/faker": "^9.0.1",
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/typescript": "^4.0.9",
"@graphql-codegen/typescript-operations": "^4.2.3",
"@types/cors": "^2.8.17",
"@types/crypto-js": "^4.2.2",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/jest": "^29.5.13",
"@types/node": "^20.12.11",
"@types/uuid": "^10.0.0",
"jest": "^29.7.0",
"node-mocks-http": "^1.16.0",
"testcontainers": "^10.13.0",
"testcontainers": "^10.13.1",
"ts-jest": "^29.2.5",
"tsc-alias": "^1.8.10",
"typescript": "^5.6.2"
Expand Down
1 change: 1 addition & 0 deletions packages/wallet/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export class App {
router.get('/reset-password/:token/validate', userController.checkToken)
router.post('/reset-password/:token', userController.resetPassword)
router.post('/verify-email/:token', authController.verifyEmail)
router.post('/resend-verify-email', authController.resendVerifyEmail)
router.patch('/change-password', isAuth, userController.changePassword)

// Me Endpoint
Expand Down
32 changes: 31 additions & 1 deletion packages/wallet/backend/src/auth/controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { validate } from '@/shared/validate'
import { NextFunction, Request } from 'express'
import { AuthService } from './service'
import { logInSchema, signUpSchema } from './validation'
import {
logInSchema,
signUpSchema,
resendVerifyEmailSchema
} from './validation'
import { UserService } from '@/user/service'
import { Controller, toSuccessResponse, Unauthorized } from '@shared/backend'

interface IAuthController {
signUp: Controller
logIn: Controller
verifyEmail: Controller
resendVerifyEmail: Controller
}

export class AuthController implements IAuthController {
Expand Down Expand Up @@ -95,4 +100,29 @@ export class AuthController implements IAuthController {
next(e)
}
}

resendVerifyEmail = async (
req: Request,
res: CustomResponse,
next: NextFunction
) => {
try {
const {
body: { email }
} = await validate(resendVerifyEmailSchema, req)

await this.authService.resendVerifyEmail({ email })

res
.status(201)
.json(
toSuccessResponse(
undefined,
'Verification email has been sent successfully'
)
)
} catch (e) {
next(e)
}
}
}
37 changes: 33 additions & 4 deletions packages/wallet/backend/src/auth/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import type { UserService } from '@/user/service'
import { getRandomToken, hashToken } from '@/utils/helpers'
import { EmailService } from '@/email/service'
import { Logger } from 'winston'
import { Unauthorized } from '@shared/backend'
import { Unauthorized, NotVerified } from '@shared/backend'

interface resendVerifyEmailArgs {
email: string
}
interface AuthorizeArgs {
email: string
password: string
Expand Down Expand Up @@ -54,21 +57,47 @@ export class AuthService implements IAuthService {
return user
}

public async resendVerifyEmail({
email
}: resendVerifyEmailArgs): Promise<void> {
const user = await this.userService.getByEmail(email)

// TODO: Prevent timing attacks
if (!user) {
this.logger.info(
`Invalid account on resend verify account email: ${email}`
)
return
}
const token = getRandomToken()
await this.userService.resetVerifyEmailToken({
email,
verifyEmailToken: hashToken(token)
})

await this.emailService.sendVerifyEmail(email, token).catch((e) => {
this.logger.error(
`Error on sending verify email for user ${user.email}`,
e
)
})
}

public async authorize(args: AuthorizeArgs): Promise<AuthorizeResult> {
const user = await this.userService.getByEmail(args.email)

// TODO: Prevent timing attacks
if (!user) {
throw new Unauthorized('Invalid credentials')
throw new Unauthorized('Invalid credentials.')
}

const isValid = await user.verifyPassword(args.password)
if (!isValid) {
throw new Unauthorized('Invalid credentials')
throw new Unauthorized('Invalid credentials.')
}

if (!user.isEmailVerified) {
throw new Unauthorized('Email address is not verified')
throw new NotVerified('Email address is not verified.')
}

const session = await user.$relatedQuery('sessions').insertGraphAndFetch({
Expand Down
6 changes: 6 additions & 0 deletions packages/wallet/backend/src/auth/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ export const logInSchema = z.object({
password: z.string()
})
})

export const resendVerifyEmailSchema = z.object({
body: z.object({
email: z.string().email()
})
})
16 changes: 8 additions & 8 deletions packages/wallet/backend/src/gatehub/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export class GateHubClient {
IFRAME_TYPE,
(managerUserId: string) => Promise<string>
> = {
deposit: this.getDepositUrl,
withdrawal: this.getWithdrawalUrl,
exchange: this.getExchangeUrl,
onboarding: this.getOnboardingUrl
deposit: this.getDepositUrl.bind(this),
withdrawal: this.getWithdrawalUrl.bind(this),
exchange: this.getExchangeUrl.bind(this),
onboarding: this.getOnboardingUrl.bind(this)
}
constructor(
private env: Env,
Expand Down Expand Up @@ -66,7 +66,7 @@ export class GateHubClient {
}

async getWithdrawalUrl(managerUserId: string): Promise<string> {
const token = this.getIframeAuthorizationToken(
const token = await this.getIframeAuthorizationToken(
this.clientIds.onOffRamp,
DEFAULT_APP_SCOPE,
managerUserId
Expand All @@ -76,7 +76,7 @@ export class GateHubClient {
}

async getDepositUrl(managerUserId: string): Promise<string> {
const token = this.getIframeAuthorizationToken(
const token = await this.getIframeAuthorizationToken(
this.clientIds.onOffRamp,
DEFAULT_APP_SCOPE,
managerUserId
Expand All @@ -86,7 +86,7 @@ export class GateHubClient {
}

async getOnboardingUrl(managerUserId: string): Promise<string> {
const token = this.getIframeAuthorizationToken(
const token = await this.getIframeAuthorizationToken(
this.clientIds.onboarding,
ONBOARDING_APP_SCOPE,
managerUserId
Expand All @@ -96,7 +96,7 @@ export class GateHubClient {
}

async getExchangeUrl(managerUserId: string): Promise<string> {
const token = this.getIframeAuthorizationToken(
const token = await this.getIframeAuthorizationToken(
this.clientIds.exchange,
DEFAULT_APP_SCOPE,
managerUserId
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/backend/src/gatehub/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class GateHubController implements IGateHubController {
try {
const userId = req.session.user.id
const iframeType: IFRAME_TYPE = req.params.type as IFRAME_TYPE
const url = this.gateHubService.getIframeUrl(iframeType, userId)
const url = await this.gateHubService.getIframeUrl(iframeType, userId)
res.status(200).json(toSuccessResponse({ url }))
} catch (e) {
next(e)
Expand Down
29 changes: 26 additions & 3 deletions packages/wallet/backend/src/user/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ interface CreateUserArgs {
verifyEmailToken: string
}

interface VerifyEmailArgs {
email: string
verifyEmailToken: string
}

interface IUserService {
create: (args: CreateUserArgs) => Promise<User>
getByEmail(email: string): Promise<User | undefined>
getById(id: string): Promise<User | undefined>
requestResetPassword(email: string): Promise<void>
resetPassword(token: string, password: string): Promise<void>
validateToken(token: string): Promise<boolean>
resetVerifyEmailToken: (args: VerifyEmailArgs) => Promise<void>
}

export class UserService implements IUserService {
Expand Down Expand Up @@ -66,9 +72,9 @@ export class UserService implements IUserService {
public async requestResetPassword(email: string): Promise<void> {
const user = await this.getByEmail(email)

if (!user?.isEmailVerified) {
if (!user) {
this.logger.info(
`Reset email not sent. User with email ${email} not found (or not verified)`
`Reset email not sent. User with email ${email} not found`
)
return
}
Expand All @@ -93,7 +99,8 @@ export class UserService implements IUserService {
await User.query().findById(user.id).patch({
newPassword: password,
passwordResetExpiresAt: null,
passwordResetToken: null
passwordResetToken: null,
isEmailVerified: true
})
}

Expand Down Expand Up @@ -219,6 +226,22 @@ export class UserService implements IUserService {
})
}

public async resetVerifyEmailToken(args: VerifyEmailArgs): Promise<void> {
const user = await this.getByEmail(args.email)

if (!user) {
this.logger.info(
`Invalid account on resend verify account email: ${args.email}`
)
return
}

await User.query().findById(user.id).patch({
isEmailVerified: false,
verifyEmailToken: args.verifyEmailToken
})
}

private async getUserByToken(token: string): Promise<User | undefined> {
const passwordResetToken = hashToken(token)

Expand Down
5 changes: 2 additions & 3 deletions packages/wallet/backend/src/user/validation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { z } from 'zod'
import { emailSchema } from '@wallet/shared'

export const forgotPasswordSchema = z.object({
body: z.object({
email: z.string().email({ message: 'Email is required' })
})
body: emailSchema
})

export const resetPasswordSchema = z.object({
Expand Down
Loading

0 comments on commit 411a1f2

Please sign in to comment.