Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/four-cameras-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@example/erp": patch
"@genseki/next": patch
"@genseki/plugins": patch
---

Fix Forgot password and Change phone bugs in phone plugin
4 changes: 2 additions & 2 deletions examples/erp/genseki/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export class MyContext implements Contextable<User> {
getUserSchema() {
return z.object({
id: z.string(),
name: z.string().optional(),
email: z.email().optional(),
name: z.string().nullish(),
email: z.email().nullish(),
})
}

Expand Down
46 changes: 44 additions & 2 deletions examples/erp/http.rest
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,50 @@ POST http://localhost:3000/api/auth/phone/forgot-password/otp/verify
Content-Type: application/json

{
"token": "token9",
"token": "2c3d6dc3-05c5-45e1-8a58-a1484010ee36",
"phone": "0812345678",
"pin": "9",
"refCode": "ref9"
"refCode": "a8fc31"
}

###

POST http://localhost:3000/api/auth/phone/forgot-password/reset
Content-Type: application/json

{
"token": "fbe2e5a8-7d7c-4320-a210-d756091b5c7e",
"password": "new-password"
}

###

POST http://localhost:3000/api/auth/phone/login
Content-Type: application/json

{
"phone": "0812345679",
"password": "password"
}

###

POST http://localhost:3000/api/auth/phone/change/otp
Content-Type: application/json
Cookie: GENSEKI_SESSION=b44e3ba0-076e-4f80-9876-38d4d3d61a61

{
"oldPhoneNumber": "0812345679",
"newPhoneNumber": "0812345677"
}

###

POST http://localhost:3000/api/auth/phone/change/otp/verify
Content-Type: application/json

{
"refCode": "b05a92",
"token": "4c6981ad-6fdf-47b2-b020-2b1b55aa1dc0",
"pin": "8"
}
1 change: 1 addition & 0 deletions packages/next/src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ async function makeApiRoute(
{
status: error?.status ?? 500,
body: {
...error?.body,
message: error?.body?.message ?? 'Internal Server Error',
},
},
Expand Down
17 changes: 6 additions & 11 deletions packages/plugins/src/phone/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,7 @@ export function phone<
path: '/auth/phone/forgot-password/reset',
body: z.object({
token: z.string().min(1),
oldPassword: z.string().min(1),
newPassword: z.string().min(1),
password: z.string().min(1),
}),
responses: {
200: z.object({
Expand All @@ -268,13 +267,9 @@ export function phone<
500: z.looseObject({
code: z.enum([
'INVALID_OR_EXPIRED_VERIFICATION_TOKEN',
'ACCOUNT_NOT_FOUND',
'ACCOUNT_NOT_SUPPORTED',
'OLD_PASSWORD_INCORRECT',
'NEW_PASSWORD_SAME_AS_OLD',
'INTERNAL_SERVER_ERROR',
'FAILED_TO_HASH_PASSWORD',
'FAILED_TO_UPDATE_PASSWORD',
'FAILED_TO_DELETE_PASSWORD_VERIFICATION',
'FAILED_TO_DELETE_VERIFICATION',
]),
message: z.string(),
}),
Expand All @@ -283,7 +278,7 @@ export function phone<
async ({ body }) => {
const response = await service.resetPassword({
token: body.token,
password: { old: body.oldPassword, new: body.newPassword },
password: body.password,
})

if (response.isErr()) {
Expand All @@ -304,7 +299,7 @@ export function phone<
context,
{
method: 'POST',
path: '/auth/phone/change',
path: '/auth/phone/change/otp',
body: z.object({
oldPhoneNumber: z.string().min(1),
newPhoneNumber: z.string().min(1),
Expand Down Expand Up @@ -359,7 +354,7 @@ export function phone<
context,
{
method: 'POST',
path: '/auth/phone/change/verify',
path: '/auth/phone/change/otp/verify',
body: z.object({
refCode: z.string(),
token: z.string(),
Expand Down
78 changes: 10 additions & 68 deletions packages/plugins/src/phone/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export class PhoneService<
const [existingUser, userWithPhone, count] = await Promise.all([
this.store.getUserById(userId),
this.store.getUserByPhone(phone.new),
this.store.countActiveChangePhoneNumberVerification(phone.old),
this.store.countActiveChangePhoneNumberVerification(userId),
])

if (!existingUser) {
Expand Down Expand Up @@ -564,7 +564,7 @@ export class PhoneService<
attempt: attempt.value,
})
}
// TODO: Should we delete the verification

const accountId = await this.store.getCredentialAccountIdByUserId(verification.value.userId)

if (!accountId) {
Expand All @@ -583,7 +583,7 @@ export class PhoneService<
return ok({ token: resetPasswordVerification })
}

async resetPassword(payload: { token: string; password: { old: string; new: string } }) {
async resetPassword(payload: { token: string; password: string }) {
const verification = await this.store.getResetPasswordVerification(payload.token)

if (!verification) {
Expand All @@ -593,80 +593,22 @@ export class PhoneService<
})
}

const account = await this.store.getAccountById(verification.value.accountId)

if (!account) {
return err({
code: 'ACCOUNT_NOT_FOUND' as const,
message: 'Account not found',
})
}

if (account.provider !== AccountProvider.CREDENTIAL) {
return err({
code: 'ACCOUNT_NOT_SUPPORTED' as const,
message: 'This account does not support password login',
})
}

const existingPassword = account.password

if (!existingPassword) {
return err({
code: 'ACCOUNT_NOT_SUPPORTED' as const,
message: 'This account does not need to change password',
})
}

const oldPasswordVerification = await Password.verifyPassword(
payload.password.old,
existingPassword
)
.then((result) => ok(result))
.catch((error) => err(error))

if (oldPasswordVerification.isErr()) {
return err({
code: 'INTERNAL_SERVER_ERROR' as const,
message: 'Old Password checking logic throw error. Please check the log for more details',
cause: oldPasswordVerification.error,
})
}

if (!oldPasswordVerification.value) {
return err({
code: 'OLD_PASSWORD_INCORRECT' as const,
message: 'Old password is incorrect',
})
}

const newPasswordVerification = await Password.verifyPassword(
payload.password.new,
existingPassword
)
const hashedPassword = await Password.hashPassword(payload.password)
.then((result) => ok(result))
.catch((error) => err(error))

if (newPasswordVerification.isErr()) {
return err({
code: 'INTERNAL_SERVER_ERROR' as const,
message: 'New Password checking logic throw error. Please check the log for more details',
cause: newPasswordVerification.error,
})
}

if (newPasswordVerification.value) {
if (hashedPassword.isErr()) {
return err({
code: 'NEW_PASSWORD_SAME_AS_OLD' as const,
message: 'New password must be different from the old one',
code: 'FAILED_TO_HASH_PASSWORD' as const,
message: 'Failed to hash password',
cause: hashedPassword.error,
})
}

const accountId = verification.value.accountId
const hashedPassword = await Password.hashPassword(payload.password.new)

const updateResult = await this.store
.updateAccountPassword(accountId, hashedPassword)
.updateAccountPassword(verification.value.accountId, hashedPassword.value)
.then((result) => ok(result))
.catch((error) => err(error))

Expand All @@ -685,7 +627,7 @@ export class PhoneService<

if (deleteResult.isErr()) {
return err({
code: 'FAILED_TO_DELETE_PASSWORD_VERIFICATION' as const,
code: 'FAILED_TO_DELETE_VERIFICATION' as const,
message: 'Internal Server Error. Please see logs for more details',
cause: deleteResult.error,
})
Expand Down
7 changes: 4 additions & 3 deletions packages/plugins/src/phone/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export abstract class PhoneStore<TSignUpBodySchema extends BaseSignUpBodySchema>
await this.prisma.verification.create({
data: {
id: data.id,
identifier: `change-phone:${data.value.oldPhone}:${data.refCode}`,
identifier: `change-phone:${data.value.userId}:${data.refCode}`,
value: JSON.stringify(data.value),
expiredAt: data.expiredAt,
},
Expand All @@ -237,10 +237,10 @@ export abstract class PhoneStore<TSignUpBodySchema extends BaseSignUpBodySchema>
return attempt
}

async countActiveChangePhoneNumberVerification(phone: string) {
async countActiveChangePhoneNumberVerification(userId: string) {
const count = await this.prisma.verification.count({
where: {
identifier: { contains: `change-phone:${phone}` },
identifier: { contains: `change-phone:${userId}` },
expiredAt: { gte: new Date() },
},
})
Expand Down Expand Up @@ -371,6 +371,7 @@ export abstract class PhoneStore<TSignUpBodySchema extends BaseSignUpBodySchema>
identifier: `reset-password:${data.value.accountId}`,
},
create: {
identifier: `reset-password:${data.value.accountId}`,
value: JSON.stringify(data.value),
expiredAt: data.expiredAt,
},
Expand Down
Loading