Skip to content

Commit

Permalink
Enable user verify email by using verification code (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
byn9826 authored Jul 19, 2024
1 parent 969af1e commit 15c21a6
Show file tree
Hide file tree
Showing 47 changed files with 1,176 additions and 807 deletions.
1 change: 1 addition & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface GetUserInfo {
email: string | null;
firstName?: string | null;
lastName?: string | null;
emailVerified: boolean;
createdAt: string;
updatedAt: string;
}
Expand Down
20 changes: 14 additions & 6 deletions server/src/configs/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum Error {
CanNotCreateUser = 'Failed to create user',
CanNotCreateConsent = 'Failed to create consent',
WrongCode = 'Invalid code',
CodeExpired = 'Code expired',
WrongCodeVerifier = 'Invalid code_verifier',
WrongGrantType = 'Invalid grant_type',
WrongRefreshToken = 'Invalid refresh_token',
Expand All @@ -17,13 +18,13 @@ export enum Error {

export enum Message {
AuthFailed = 'Authentication Failed',
LogoutSuccess = 'Logged out successfully',
EmailIsRequired = 'Email is required.',
WrongEmailFormat = 'Wrong email format.',
PasswordIsRequired = 'Password is required.',
PasswordNotMatch = 'The password and confirm password do not match.',
FirstNameIsEmpty = 'First name can not be empty.',
LastNameIsEmpty = 'Last name can not be empty.',
VerificationCodeLength = 'Verification code can only be 8 characters',
WeakPassword = 'Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one special character.',
}

Expand Down Expand Up @@ -51,14 +52,21 @@ export enum AuthorizeAccountPage {
SignUpBtn = 'Confirm',
}

export enum EmailVerificationEmail {
export enum VerifyEmailPage {
Title = 'Verify your email',
Desc = 'Enter your verification code received by email',
VerifyBtn = 'Verify',
Success = 'Verification Success! You can close this page now.',
}

export enum CommonContent {
PoweredBy = 'Powered by Melody Auth',
}

export enum EmailVerificationTemplate {
Subject = 'Welcome to Melody Auth, please verify your email address',
Title = 'Welcome to Melody Auth',
Desc = 'Thanks for signing up! Please verify your email address with us, your verification code is',
ExpiryText = 'This link will be expired after 2 hour',
VerifyBtn = 'Verify your email',
}

export enum CommonPage {
PoweredBy = 'Powered by Melody Auth',
}
24 changes: 24 additions & 0 deletions server/src/dtos/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,27 @@ export class PostLogoutReqBodyDto {
this.postLogoutRedirectUri = dto.postLogoutRedirectUri.trim()
}
}

export class GetVerifyEmailReqQueryDto {
@IsString()
@IsNotEmpty()
id: string

constructor (dto: GetVerifyEmailReqQueryDto) {
this.id = dto.id.trim()
}
}

export class PostVerifyEmailReqBodyDto extends GetVerifyEmailReqQueryDto {
@IsString()
@Length(
8,
8,
)
code: string

constructor (dto: PostVerifyEmailReqBodyDto) {
super(dto)
this.code = dto.code.trim()
}
}
17 changes: 16 additions & 1 deletion server/src/dtos/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export enum TokenGrantType {
ClientCredentials = 'client_credentials',
}

const parseScopes = (scope: string[]) => scope.map((s) => s.trim().toLowerCase())
const parseScopes = (scopes: string[]) => scopes.map((s) => s.trim().toLowerCase())

export class GetAuthorizeReqQueryDto {
@IsString()
Expand Down Expand Up @@ -113,3 +113,18 @@ export class PostTokenClientCredentialsReqBodyDto {
this.scopes = parseScopes(dto.scopes)
}
}

export class GetLogoutReqBodyDto {
@IsString()
@IsNotEmpty()
postLogoutRedirectUri: string

@IsString()
@IsNotEmpty()
clientId: string

constructor (dto: GetLogoutReqBodyDto) {
this.clientId = dto.clientId.trim()
this.postLogoutRedirectUri = dto.postLogoutRedirectUri.trim()
}
}
2 changes: 1 addition & 1 deletion server/src/handlers/getAuthorizeReq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const parse = async (c: Context<typeConfig.Context>) => {
await validateUtil.dto(queryDto)

const app = await appService.verifySPAClientRequest(
c.env.DB,
c,
queryDto.clientId,
queryDto.redirectUri,
)
Expand Down
3 changes: 2 additions & 1 deletion server/src/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * as postLogoutReqHandler from 'handlers/postLogoutReq'
export * as logoutReqHandler from 'handlers/logoutReq'
export * as postTokenReqHandler from 'handlers/postTokenReq'
export * as getAuthorizeReqHandler from 'handlers/getAuthorizeReq'
export * as postAuthorizeReqHandler from 'handlers/postAuthorizeReq'
export * as verifyEmailReqHandler from 'handlers/verifyEmailReq'
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Context } from 'hono'
import { typeConfig } from 'configs'
import { identityDto } from 'dtos'
import {
identityDto, oauthDto,
} from 'dtos'
import { validateUtil } from 'utils'

export const parse = async (c: Context<typeConfig.Context>) => {
export const parsePost = async (c: Context<typeConfig.Context>) => {
const reqBody = await c.req.parseBody()
const bodyDto = new identityDto.PostLogoutReqBodyDto({
refreshToken: String(reqBody.refresh_token),
Expand All @@ -14,3 +16,13 @@ export const parse = async (c: Context<typeConfig.Context>) => {
await validateUtil.dto(bodyDto)
return bodyDto
}

export const parseGet = async (c: Context<typeConfig.Context>) => {
const queryDto = new oauthDto.GetLogoutReqBodyDto({
clientId: c.req.query('client_id') ?? '',
postLogoutRedirectUri: c.req.query('post_logout_redirect_uri') ?? '',
})

await validateUtil.dto(queryDto)
return queryDto
}
13 changes: 10 additions & 3 deletions server/src/handlers/postAuthorizeReq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ export const parseAccount = async (
c: Context<typeConfig.Context>, namesIsRequired: boolean,
) => {
const reqBody = await c.req.json()
const parsedBody = {
...reqBody,
scopes: reqBody.scope.split(' '),
}

const bodyDto = namesIsRequired
? new identityDto.PostAuthorizeReqBodyWithRequiredNamesDto(reqBody)
: new identityDto.PostAuthorizeReqBodyWithNamesDto(reqBody)
? new identityDto.PostAuthorizeReqBodyWithRequiredNamesDto(parsedBody)
: new identityDto.PostAuthorizeReqBodyWithNamesDto(parsedBody)
await validateUtil.dto(bodyDto)

return bodyDto
Expand All @@ -19,7 +23,10 @@ export const parseAccount = async (
export const parsePassword = async (c: Context<typeConfig.Context>) => {
const reqBody = await c.req.json()

const bodyDto = new identityDto.PostAuthorizeReqBodyWithPasswordDto(reqBody)
const bodyDto = new identityDto.PostAuthorizeReqBodyWithPasswordDto({
...reqBody,
scopes: reqBody.scope.split(' '),
})
await validateUtil.dto(bodyDto)

return bodyDto
Expand Down
23 changes: 23 additions & 0 deletions server/src/handlers/verifyEmailReq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Context } from 'hono'
import { typeConfig } from 'configs'
import { identityDto } from 'dtos'
import { validateUtil } from 'utils'

export const parseGet = async (c: Context<typeConfig.Context>) => {
const queryDto = new identityDto.GetVerifyEmailReqQueryDto({ id: c.req.query('id') ?? '' })
await validateUtil.dto(queryDto)

return queryDto
}

export const parsePost = async (c: Context<typeConfig.Context>) => {
const reqBody = await c.req.json()

const queryDto = new identityDto.PostVerifyEmailReqBodyDto({
id: String(reqBody.id),
code: String(reqBody.code),
})
await validateUtil.dto(queryDto)

return queryDto
}
2 changes: 1 addition & 1 deletion server/src/middlewares/csrf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from 'configs'
import { formatUtil } from 'utils'

export const oAuthAuthorize = async (
export const serverOrigin = async (
c: Context<typeConfig.Context>, next: Next,
) => {
const origin = c.req.header('origin')
Expand Down
12 changes: 7 additions & 5 deletions server/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export interface Update {
lastName?: string | null;
deletedAt?: string | null;
updatedAt?: string | null;
emailVerified?: number;
emailVerificationCode?: string | null;
emailVerificationCodeExpiresOn?: number | null;
}

const TableName = dbConfig.TableName.User
Expand Down Expand Up @@ -119,19 +122,18 @@ export const update = async (
...update,
updatedAt: timeUtil.getDbCurrentTime(),
}
const updateKeys: (keyof Update)[] = ['password', 'firstName', 'lastName', 'deletedAt', 'updatedAt']
updateKeys.forEach((
key, index,
) => {
const updateKeys: (keyof Update)[] = ['password', 'firstName', 'lastName', 'deletedAt', 'updatedAt', 'emailVerified', 'emailVerificationCode', 'emailVerificationCodeExpiresOn']
updateKeys.forEach((key) => {
const value = parsedUpdate[key]
if (value === undefined) return
setQueries.push(`${key} = $${index + 1}`)
setQueries.push(`${key} = $${setQueries.length + 1}`)
binds.push(value)
})

binds.push(id)
const query = `UPDATE ${TableName} set ${setQueries.join(',')} where id = $${setQueries.length + 1}`
const stmt = db.prepare(query).bind(...binds)

const result = await stmt.run()
if (!result.success) return null
return getById(
Expand Down
Loading

0 comments on commit 15c21a6

Please sign in to comment.