Skip to content

Commit a3e7cb7

Browse files
alaistercharislam
andauthored
chore: move password check to frontend (supabase#39927)
* chore: move password check to frontend * remove barrel file export and fix test import * move import inside function * Update apps/studio/lib/password-strength.ts Co-authored-by: Charis <[email protected]> * fix test --------- Co-authored-by: Charis <[email protected]>
1 parent d690b5d commit a3e7cb7

File tree

9 files changed

+43
-143
lines changed

9 files changed

+43
-143
lines changed

apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { zodResolver } from '@hookform/resolvers/zod'
2-
import { debounce } from 'lodash'
3-
import { useRef, useState } from 'react'
2+
import { useState } from 'react'
43
import { useForm } from 'react-hook-form'
54
import { toast } from 'sonner'
65
import { z } from 'zod'
@@ -10,7 +9,7 @@ import { useProjectCloneMutation } from 'data/projects/clone-mutation'
109
import { useCloneBackupsQuery } from 'data/projects/clone-query'
1110
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
1211
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
13-
import { passwordStrength } from 'lib/helpers'
12+
import { passwordStrength } from 'lib/password-strength'
1413
import { generateStrongPassword } from 'lib/project'
1514
import {
1615
Button,
@@ -86,10 +85,6 @@ export const CreateNewProjectDialog = ({
8685
},
8786
})
8887

89-
const delayedCheckPasswordStrength = useRef(
90-
debounce((value: string) => checkPasswordStrength(value), 300)
91-
).current
92-
9388
async function checkPasswordStrength(value: string) {
9489
const { message, strength } = await passwordStrength(value)
9590
setPasswordStrengthScore(strength)
@@ -99,7 +94,7 @@ export const CreateNewProjectDialog = ({
9994
const generatePassword = () => {
10095
const password = generateStrongPassword()
10196
form.setValue('password', password)
102-
delayedCheckPasswordStrength(password)
97+
checkPasswordStrength(password)
10398
}
10499

105100
return (
@@ -173,7 +168,7 @@ export const CreateNewProjectDialog = ({
173168
if (value == '') {
174169
setPasswordStrengthScore(-1)
175170
setPasswordStrengthMessage('')
176-
} else delayedCheckPasswordStrength(value)
171+
} else checkPasswordStrength(value)
177172
}}
178173
descriptionText={
179174
<PasswordStrengthBar

apps/studio/components/interfaces/ProjectCreation/DatabasePasswordInput.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { debounce } from 'lodash'
2-
import { useRef } from 'react'
31
import { UseFormReturn } from 'react-hook-form'
42

53
import Panel from 'components/ui/Panel'
64
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
7-
import passwordStrength from 'lib/password-strength'
5+
import { passwordStrength } from 'lib/password-strength'
86
import { generateStrongPassword } from 'lib/project'
97
import { FormControl_Shadcn_, FormField_Shadcn_ } from 'ui'
108
import { Input } from 'ui-patterns/DataInputs/Input'
@@ -37,15 +35,11 @@ export const DatabasePasswordInput = ({
3735
setPasswordStrengthMessage(message)
3836
}
3937

40-
const delayedCheckPasswordStrength = useRef(
41-
debounce((value) => checkPasswordStrength(value), 300)
42-
).current
43-
4438
// [Refactor] DB Password could be a common component used in multiple pages with repeated logic
4539
function generatePassword() {
4640
const password = generateStrongPassword()
4741
form.setValue('dbPass', password)
48-
delayedCheckPasswordStrength(password)
42+
checkPasswordStrength(password)
4943
}
5044

5145
return (
@@ -88,7 +82,7 @@ export const DatabasePasswordInput = ({
8882
await form.setValue('dbPassStrength', 0)
8983
await form.trigger('dbPass')
9084
} else {
91-
await delayedCheckPasswordStrength(value)
85+
await checkPasswordStrength(value)
9286
}
9387
}}
9488
/>

apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { PermissionAction } from '@supabase/shared-types/out/constants'
2-
import { debounce } from 'lodash'
3-
import { useEffect, useRef, useState } from 'react'
2+
import { useEffect, useState } from 'react'
43
import { toast } from 'sonner'
54

65
import { useParams } from 'common'
@@ -12,7 +11,7 @@ import { useDatabasePasswordResetMutation } from 'data/database/database-passwor
1211
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
1312
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
1413
import { DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
15-
import passwordStrength from 'lib/password-strength'
14+
import { passwordStrength } from 'lib/password-strength'
1615
import { generateStrongPassword } from 'lib/project'
1716
import { Button, Input, Modal } from 'ui'
1817

@@ -62,17 +61,13 @@ const ResetDbPassword = ({ disabled = false }) => {
6261
setPasswordStrengthMessage(message)
6362
}
6463

65-
const delayedCheckPasswordStrength = useRef(
66-
debounce((value) => checkPasswordStrength(value), 300)
67-
).current
68-
6964
const onDbPassChange = (e: any) => {
7065
const value = e.target.value
7166
setPassword(value)
7267
if (value == '') {
7368
setPasswordStrengthScore(-1)
7469
setPasswordStrengthMessage('')
75-
} else delayedCheckPasswordStrength(value)
70+
} else checkPasswordStrength(value)
7671
}
7772

7873
const confirmResetDbPass = async () => {
@@ -86,7 +81,7 @@ const ResetDbPassword = ({ disabled = false }) => {
8681
function generatePassword() {
8782
const password = generateStrongPassword()
8883
setPassword(password)
89-
delayedCheckPasswordStrength(password)
84+
checkPasswordStrength(password)
9085
}
9186

9287
return (

apps/studio/components/ui/PasswordStrengthBar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ const PasswordStrengthBar = ({
3535
</div>
3636
)}
3737
<p>
38-
{passwordStrengthMessage
38+
{(passwordStrengthMessage
3939
? passwordStrengthMessage
40-
: 'This is the password to your Postgres database, so it must be strong and hard to guess.'}{' '}
40+
: 'This is the password to your Postgres database, so it must be strong and hard to guess.') +
41+
' '}
4142
<span
4243
className="text-inherit underline hover:text-foreground transition-colors cursor-pointer"
4344
onClick={generateStrongPassword}

apps/studio/lib/helpers.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export { default as passwordStrength } from './password-strength'
21
export { default as uuidv4 } from './uuid'
32
import { UIEvent } from 'react'
43
import type { TablesData } from '../data/tables/tables-query'
Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,10 @@
11
import { describe, it, expect, beforeEach, vi } from 'vitest'
2-
import passwordStrength from './password-strength'
3-
import { toast } from 'sonner'
4-
5-
// Hoist the post_ mock so it's available before the module is loaded
6-
const postMock = vi.hoisted(() => vi.fn())
7-
8-
vi.mock('data/fetchers', () => ({
9-
post: postMock,
10-
}))
11-
12-
vi.mock('sonner', () => ({
13-
toast: { error: vi.fn() },
14-
}))
2+
import { passwordStrength } from './password-strength'
153

164
describe('passwordStrength', () => {
17-
beforeEach(() => {
18-
vi.clearAllMocks()
19-
})
20-
215
it('returns empty values for message, warning and strength for empty input', async () => {
226
const result = await passwordStrength('')
237
expect(result).toEqual({ message: '', warning: '', strength: 0 })
24-
expect(postMock).not.toHaveBeenCalled()
258
})
269

2710
it('returns max length message, warning, and strength 0 for password longer than 99 characters', async () => {
@@ -30,51 +13,21 @@ describe('passwordStrength', () => {
3013
expect(result.message).toMatch(/maximum length/i)
3114
expect(result.warning).toMatch(/less than 100 characters/i)
3215
expect(result.strength).toBe(0)
33-
expect(postMock).not.toHaveBeenCalled()
3416
})
3517

3618
it('returns strong score, suggestion, and empty warning for strong password', async () => {
37-
postMock.mockResolvedValue({
38-
data: {
39-
result: {
40-
score: 4,
41-
feedback: { suggestions: ['Successfully updated database password'] },
42-
},
43-
},
44-
error: null,
45-
})
46-
const result = await passwordStrength('StrongPassword123!')
19+
const result = await passwordStrength('ActuallyAStrongPassword123!')
4720
expect(result.message).toMatch(/strong/i)
48-
expect(result.message).toContain('Successfully updated database password')
21+
expect(result.message).toContain('This password is strong')
4922
expect(result.warning).toBe('')
5023
expect(result.strength).toBe(4)
5124
})
5225

5326
it('returns weak score, suggestion, and warning for weak password', async () => {
54-
postMock.mockResolvedValue({
55-
data: {
56-
result: {
57-
score: 2,
58-
feedback: {
59-
suggestions: ['Try a longer password'],
60-
warning: 'Too short',
61-
},
62-
},
63-
},
64-
error: null,
65-
})
6627
const result = await passwordStrength('weak')
6728
expect(result.message).toMatch(/not secure/i)
68-
expect(result.message).toContain('Try a longer password')
69-
expect(result.warning).toMatch(/too short/i)
29+
expect(result.message).toContain('This password is not secure enough')
7030
expect(result.warning).toMatch(/you need a stronger password/i)
71-
expect(result.strength).toBe(2)
72-
})
73-
74-
it('returns empty values and shows toast error on server error', async () => {
75-
postMock.mockResolvedValue({ data: null, error: { message: 'Server error' } })
76-
const result = await passwordStrength('any')
77-
expect(result).toEqual({ message: '', warning: '', strength: 0 })
78-
expect(toast.error).toHaveBeenCalledTimes(1)
31+
expect(result.strength).toBe(1)
7932
})
8033
})

apps/studio/lib/password-strength.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { post as post_ } from 'data/fetchers'
21
import { DEFAULT_MINIMUM_PASSWORD_STRENGTH, PASSWORD_STRENGTH } from 'lib/constants'
3-
import { toast } from 'sonner'
4-
import { ResponseError } from 'types'
52

6-
export default async function passwordStrength(value: string) {
3+
export async function passwordStrength(value: string) {
4+
// [Alaister]: Lazy load zxcvbn to avoid bundling it with the main app (it's pretty chunky)
5+
const zxcvbn = await import('zxcvbn').then((module) => module.default)
6+
77
let message: string = ''
88
let warning: string = ''
99
let strength: number = 0
@@ -13,29 +13,20 @@ export default async function passwordStrength(value: string) {
1313
message = `${PASSWORD_STRENGTH[0]} Maximum length of password exceeded`
1414
warning = `Password should be less than 100 characters`
1515
} else {
16-
const { data, error } = await post_('/platform/profile/password-check', {
17-
body: { password: value },
18-
})
19-
if (!error) {
20-
const { result } = data
21-
const resultScore = result?.score ?? 0
16+
const result = zxcvbn(value)
17+
const resultScore = result?.score ?? 0
2218

23-
const score = (PASSWORD_STRENGTH as any)[resultScore]
24-
const suggestions = result.feedback?.suggestions
25-
? result.feedback.suggestions.join(' ')
26-
: ''
19+
const score = (PASSWORD_STRENGTH as any)[resultScore]
20+
const suggestions = result.feedback?.suggestions?.join(' ') ?? ''
2721

28-
message = `${score} ${suggestions}`
29-
strength = resultScore
22+
message = `${score} ${suggestions}`
23+
strength = resultScore
3024

31-
// warning message for anything below 4 strength :string
32-
if (resultScore < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
33-
warning = `${
34-
result?.feedback?.warning ? result?.feedback?.warning + '.' : ''
35-
} You need a stronger password.`
36-
}
37-
} else {
38-
toast.error(`Failed to check password strength: ${(error as ResponseError).message}`)
25+
// warning message for anything below 4 strength :string
26+
if (resultScore < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
27+
warning = `${
28+
result?.feedback?.warning ? result?.feedback?.warning + '.' : ''
29+
} You need a stronger password.`
3930
}
4031
}
4132
}

apps/studio/pages/api/platform/profile/password-check.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useParams } from 'common'
2-
import { debounce } from 'lodash'
3-
import { ChangeEvent, useRef, useState } from 'react'
2+
import { ChangeEvent, useState } from 'react'
43
import { toast } from 'sonner'
54
import { Alert, Button, Checkbox, Input, Listbox } from 'ui'
65

@@ -18,7 +17,7 @@ import { useProjectCreateMutation } from 'data/projects/project-create-mutation'
1817
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
1918
import { BASE_PATH, PROVIDERS } from 'lib/constants'
2019
import { getInitialMigrationSQLFromGitHubRepo } from 'lib/integration-utils'
21-
import passwordStrength from 'lib/password-strength'
20+
import { passwordStrength } from 'lib/password-strength'
2221
import { generateStrongPassword } from 'lib/project'
2322
import { AWS_REGIONS } from 'shared-data'
2423
import { useIntegrationInstallationSnapshot } from 'state/integration-installation'
@@ -63,9 +62,11 @@ const CreateProject = () => {
6362

6463
const snapshot = useIntegrationInstallationSnapshot()
6564

66-
const delayedCheckPasswordStrength = useRef(
67-
debounce((value: string) => checkPasswordStrength(value), 300)
68-
).current
65+
async function checkPasswordStrength(value: string) {
66+
const { message, strength } = await passwordStrength(value)
67+
setPasswordStrengthScore(strength)
68+
setPasswordStrengthMessage(message)
69+
}
6970

7071
const { slug, next, currentProjectId: foreignProjectId, externalId } = useParams()
7172

@@ -105,19 +106,13 @@ const CreateProject = () => {
105106
if (value == '') {
106107
setPasswordStrengthScore(-1)
107108
setPasswordStrengthMessage('')
108-
} else delayedCheckPasswordStrength(value)
109-
}
110-
111-
async function checkPasswordStrength(value: string) {
112-
const { message, strength } = await passwordStrength(value)
113-
setPasswordStrengthScore(strength)
114-
setPasswordStrengthMessage(message)
109+
} else checkPasswordStrength(value)
115110
}
116111

117112
function generatePassword() {
118113
const password = generateStrongPassword()
119114
setDbPass(password)
120-
delayedCheckPasswordStrength(password)
115+
checkPasswordStrength(password)
121116
}
122117

123118
const [newProjectRef, setNewProjectRef] = useState<string | undefined>(undefined)

0 commit comments

Comments
 (0)