@torch-ui/solid 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -0
- package/package.json +67 -0
- package/src/components/actions/Button.tsx +612 -0
- package/src/components/actions/ButtonGroup.tsx +728 -0
- package/src/components/actions/Copy.tsx +98 -0
- package/src/components/actions/DarkModeToggle.tsx +80 -0
- package/src/components/actions/Link.tsx +37 -0
- package/src/components/actions/index.ts +19 -0
- package/src/components/actions/useCopyToClipboard.ts +90 -0
- package/src/components/charts/Chart.tsx +331 -0
- package/src/components/charts/Sparkline.tsx +156 -0
- package/src/components/charts/index.ts +13 -0
- package/src/components/data-display/Avatar.tsx +208 -0
- package/src/components/data-display/AvatarGroup.tsx +228 -0
- package/src/components/data-display/Badge.tsx +70 -0
- package/src/components/data-display/Carousel.tsx +214 -0
- package/src/components/data-display/ColorSwatch.tsx +56 -0
- package/src/components/data-display/DataTable.tsx +886 -0
- package/src/components/data-display/EmptyState.tsx +61 -0
- package/src/components/data-display/Image.tsx +277 -0
- package/src/components/data-display/Kbd.tsx +114 -0
- package/src/components/data-display/Persona.tsx +78 -0
- package/src/components/data-display/StatCard.tsx +338 -0
- package/src/components/data-display/Table.tsx +147 -0
- package/src/components/data-display/Tag.tsx +91 -0
- package/src/components/data-display/Timeline.tsx +200 -0
- package/src/components/data-display/TreeView.tsx +172 -0
- package/src/components/data-display/Video.tsx +95 -0
- package/src/components/data-display/avatar-utils.ts +32 -0
- package/src/components/data-display/index.ts +81 -0
- package/src/components/feedback/Loading.tsx +159 -0
- package/src/components/feedback/Progress.tsx +321 -0
- package/src/components/feedback/Skeleton.tsx +62 -0
- package/src/components/feedback/SkeletonBlocks.tsx +222 -0
- package/src/components/feedback/Toast.tsx +648 -0
- package/src/components/feedback/index.ts +44 -0
- package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
- package/src/components/feedback/password/password-strength.ts +115 -0
- package/src/components/feedback/password/password-validation-data.ts +66 -0
- package/src/components/feedback/password/password-validation.ts +93 -0
- package/src/components/forms/Autocomplete.tsx +268 -0
- package/src/components/forms/Checkbox.tsx +155 -0
- package/src/components/forms/CodeInput.tsx +237 -0
- package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
- package/src/components/forms/ColorPicker/color-utils.ts +75 -0
- package/src/components/forms/ColorPicker/index.ts +2 -0
- package/src/components/forms/DatePicker.tsx +516 -0
- package/src/components/forms/DateRangePicker.tsx +464 -0
- package/src/components/forms/FieldPicker.tsx +64 -0
- package/src/components/forms/FileUpload.tsx +614 -0
- package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
- package/src/components/forms/FilterBuilder.tsx +16 -0
- package/src/components/forms/FilterRuleRow.tsx +68 -0
- package/src/components/forms/Input.tsx +200 -0
- package/src/components/forms/MultiSelect.tsx +361 -0
- package/src/components/forms/NumberField.tsx +145 -0
- package/src/components/forms/RadioGroup.tsx +135 -0
- package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
- package/src/components/forms/ReorderableList.tsx +163 -0
- package/src/components/forms/Select.tsx +268 -0
- package/src/components/forms/Slider.tsx +260 -0
- package/src/components/forms/Switch.tsx +135 -0
- package/src/components/forms/TextArea.tsx +202 -0
- package/src/components/forms/ViewCustomizer.tsx +44 -0
- package/src/components/forms/index.ts +43 -0
- package/src/components/layout/Accordion.tsx +110 -0
- package/src/components/layout/Alert.tsx +156 -0
- package/src/components/layout/BlockQuote.tsx +70 -0
- package/src/components/layout/Card.tsx +166 -0
- package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
- package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
- package/src/components/layout/CodeBlock/prism.ts +81 -0
- package/src/components/layout/Collapsible.tsx +84 -0
- package/src/components/layout/Container.tsx +55 -0
- package/src/components/layout/Divider.tsx +64 -0
- package/src/components/layout/Form.tsx +39 -0
- package/src/components/layout/FormActions.tsx +50 -0
- package/src/components/layout/Grid.tsx +53 -0
- package/src/components/layout/PageHeading.tsx +46 -0
- package/src/components/layout/PromptWithAction.tsx +49 -0
- package/src/components/layout/Section.tsx +60 -0
- package/src/components/layout/TablePanel.tsx +24 -0
- package/src/components/layout/TableView/TableView.tsx +1018 -0
- package/src/components/layout/TableView/index.ts +3 -0
- package/src/components/layout/TableView/types.ts +51 -0
- package/src/components/layout/WizardStep.tsx +40 -0
- package/src/components/layout/WizardStepper.tsx +173 -0
- package/src/components/layout/index.ts +96 -0
- package/src/components/navigation/Breadcrumbs.tsx +66 -0
- package/src/components/navigation/DropdownMenu.tsx +86 -0
- package/src/components/navigation/MegaMenu.tsx +480 -0
- package/src/components/navigation/NavigationMenu.tsx +305 -0
- package/src/components/navigation/Pagination.tsx +298 -0
- package/src/components/navigation/Sidebar.tsx +280 -0
- package/src/components/navigation/Tabs.tsx +122 -0
- package/src/components/navigation/ViewSwitcher.tsx +314 -0
- package/src/components/navigation/index.ts +66 -0
- package/src/components/overlays/AlertDialog.tsx +174 -0
- package/src/components/overlays/ContextMenu.tsx +65 -0
- package/src/components/overlays/Dialog.tsx +279 -0
- package/src/components/overlays/Drawer.tsx +370 -0
- package/src/components/overlays/HoverCard.tsx +107 -0
- package/src/components/overlays/Popover.tsx +73 -0
- package/src/components/overlays/Tooltip.tsx +31 -0
- package/src/components/overlays/index.ts +71 -0
- package/src/components/typography/Code.tsx +72 -0
- package/src/components/typography/Icon.tsx +36 -0
- package/src/components/typography/index.ts +10 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +13 -0
- package/src/styles/theme.css +226 -0
- package/src/types/avatar-types.ts +11 -0
- package/src/types/filter-types.ts +35 -0
- package/src/utilities/classNames.ts +6 -0
- package/src/utilities/componentSize.ts +46 -0
- package/src/utilities/i18n.tsx +60 -0
- package/src/utilities/mergeRefs.ts +12 -0
- package/src/utilities/relativeDateDefault.ts +14 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
|
|
3
|
+
* Password strength indicator - minimal implementation.
|
|
4
|
+
|
|
5
|
+
* Pass password as a prop; updates reactively when parent re-renders with new value.
|
|
6
|
+
|
|
7
|
+
*
|
|
8
|
+
|
|
9
|
+
* Usage: <PasswordStrengthIndicator password={password()} showHelperText />
|
|
10
|
+
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createMemo, createUniqueId, Show, splitProps } from 'solid-js'
|
|
14
|
+
|
|
15
|
+
import { cn } from '../../../utilities/classNames'
|
|
16
|
+
|
|
17
|
+
import { getPasswordAnalysis } from './password-strength'
|
|
18
|
+
|
|
19
|
+
import type { PasswordStrength } from './password-strength'
|
|
20
|
+
|
|
21
|
+
import { Progress } from '../Progress'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const SEGMENT_COUNT = 8
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const CONFIG: Record<
|
|
30
|
+
|
|
31
|
+
PasswordStrength,
|
|
32
|
+
|
|
33
|
+
{ label: string; bg: string; textColor: string; helperText: string }
|
|
34
|
+
|
|
35
|
+
> = {
|
|
36
|
+
|
|
37
|
+
empty: { label: '', bg: '', textColor: 'text-ink-500', helperText: '' },
|
|
38
|
+
|
|
39
|
+
poor: {
|
|
40
|
+
|
|
41
|
+
label: 'Poor',
|
|
42
|
+
|
|
43
|
+
bg: 'bg-danger-500',
|
|
44
|
+
|
|
45
|
+
textColor: 'text-danger-600',
|
|
46
|
+
|
|
47
|
+
helperText: 'Your password is easily guessable. You can do better.',
|
|
48
|
+
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
fair: {
|
|
52
|
+
|
|
53
|
+
label: 'Average',
|
|
54
|
+
|
|
55
|
+
bg: 'bg-amber-500',
|
|
56
|
+
|
|
57
|
+
textColor: 'text-amber-600',
|
|
58
|
+
|
|
59
|
+
helperText: 'Getting there, but could be stronger.',
|
|
60
|
+
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
good: {
|
|
64
|
+
|
|
65
|
+
label: 'Strong',
|
|
66
|
+
|
|
67
|
+
bg: 'bg-success-500',
|
|
68
|
+
|
|
69
|
+
textColor: 'text-success-600',
|
|
70
|
+
|
|
71
|
+
helperText: 'Your password is great. Nice work!',
|
|
72
|
+
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
excellent: {
|
|
76
|
+
|
|
77
|
+
label: 'Very Strong',
|
|
78
|
+
|
|
79
|
+
bg: 'bg-success-500',
|
|
80
|
+
|
|
81
|
+
textColor: 'text-success-600',
|
|
82
|
+
|
|
83
|
+
helperText: 'Excellent password. Nice work!',
|
|
84
|
+
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
export interface PasswordStrengthIndicatorProps {
|
|
92
|
+
|
|
93
|
+
/** Current password value - pass the signal's value, e.g. password={newPassword()} */
|
|
94
|
+
|
|
95
|
+
password?: string
|
|
96
|
+
|
|
97
|
+
class?: string
|
|
98
|
+
|
|
99
|
+
/** Show helper text below the bar. Default: true. */
|
|
100
|
+
|
|
101
|
+
showHelperText?: boolean
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
export function PasswordStrengthIndicator(props: PasswordStrengthIndicatorProps) {
|
|
108
|
+
|
|
109
|
+
const [local] = splitProps(props, ['password', 'class', 'showHelperText'])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
const helperId = `psi-helper-${createUniqueId()}`
|
|
114
|
+
|
|
115
|
+
const pwd = () => local.password ?? ''
|
|
116
|
+
|
|
117
|
+
const analysis = createMemo(() => getPasswordAnalysis(pwd()))
|
|
118
|
+
|
|
119
|
+
const strength = createMemo(() => analysis().strength)
|
|
120
|
+
|
|
121
|
+
const cfg = createMemo(() => CONFIG[strength()])
|
|
122
|
+
|
|
123
|
+
const segmentValue = createMemo(() => (analysis().segmentScore / SEGMENT_COUNT) * 100)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
const missing = createMemo(() => {
|
|
128
|
+
|
|
129
|
+
const r = analysis().requirements
|
|
130
|
+
|
|
131
|
+
const m: string[] = []
|
|
132
|
+
|
|
133
|
+
if (!r.hasMinLength) m.push('at least 8 characters')
|
|
134
|
+
|
|
135
|
+
if (!r.hasUppercase) m.push('an uppercase letter')
|
|
136
|
+
|
|
137
|
+
if (!r.hasLowercase) m.push('a lowercase letter')
|
|
138
|
+
|
|
139
|
+
if (!r.hasNumber) m.push('a number')
|
|
140
|
+
|
|
141
|
+
if (!r.hasSymbol) m.push('a symbol')
|
|
142
|
+
|
|
143
|
+
return m
|
|
144
|
+
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
const helperText = () => {
|
|
150
|
+
|
|
151
|
+
if (strength() === 'empty') return 'Enter a password to see strength.'
|
|
152
|
+
|
|
153
|
+
if (strength() === 'poor' || strength() === 'fair') {
|
|
154
|
+
|
|
155
|
+
const m = missing()
|
|
156
|
+
|
|
157
|
+
if (m.length === 0) return cfg().helperText
|
|
158
|
+
|
|
159
|
+
if (m.length === 1) return `Must contain ${m[0]}.`
|
|
160
|
+
|
|
161
|
+
return `Must contain ${m.slice(0, -1).join(', ')}, and ${m[m.length - 1]}.`
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return cfg().helperText
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
const isEmpty = () => strength() === 'empty'
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
|
|
177
|
+
<div class={cn('mt-1.5', local.class)}>
|
|
178
|
+
|
|
179
|
+
<div class="flex items-center justify-between mb-1.5">
|
|
180
|
+
|
|
181
|
+
<span class="text-sm font-medium text-ink-700">Password Strength</span>
|
|
182
|
+
|
|
183
|
+
<span class={cn('text-sm font-medium', cfg().textColor)}>{cfg().label}</span>
|
|
184
|
+
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<Progress
|
|
188
|
+
|
|
189
|
+
value={segmentValue()}
|
|
190
|
+
|
|
191
|
+
segments={SEGMENT_COUNT}
|
|
192
|
+
|
|
193
|
+
fillClass={cfg().bg}
|
|
194
|
+
|
|
195
|
+
trackClass="bg-transparent"
|
|
196
|
+
|
|
197
|
+
showValueLabel={false}
|
|
198
|
+
|
|
199
|
+
aria-label={isEmpty() ? 'Password strength: not set' : `Password strength: ${cfg().label}`}
|
|
200
|
+
|
|
201
|
+
aria-describedby={local.showHelperText !== false ? helperId : undefined}
|
|
202
|
+
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
<Show when={local.showHelperText !== false}>
|
|
206
|
+
|
|
207
|
+
<p
|
|
208
|
+
|
|
209
|
+
id={helperId}
|
|
210
|
+
|
|
211
|
+
class={cn(
|
|
212
|
+
|
|
213
|
+
'mt-1.5 text-sm',
|
|
214
|
+
|
|
215
|
+
(strength() === 'poor' || strength() === 'fair') ? 'text-ink-600' : 'text-ink-500'
|
|
216
|
+
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
>
|
|
220
|
+
|
|
221
|
+
{helperText()}
|
|
222
|
+
|
|
223
|
+
</p>
|
|
224
|
+
|
|
225
|
+
</Show>
|
|
226
|
+
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password strength and requirements.
|
|
3
|
+
*
|
|
4
|
+
* Callers should NOT trim the password before passing it in; these functions
|
|
5
|
+
* operate on the raw value. isPasswordWeak (from password-validation) trims
|
|
6
|
+
* internally for its own pattern checks — that asymmetry is intentional so
|
|
7
|
+
* that leading/trailing spaces count toward length and character requirements
|
|
8
|
+
* but don't bypass weak-pattern detection.
|
|
9
|
+
*/
|
|
10
|
+
import { isPasswordWeak } from './password-validation'
|
|
11
|
+
|
|
12
|
+
export type PasswordStrength = 'empty' | 'poor' | 'fair' | 'good' | 'excellent'
|
|
13
|
+
|
|
14
|
+
export interface PasswordRequirements {
|
|
15
|
+
hasMinLength: boolean
|
|
16
|
+
hasUppercase: boolean
|
|
17
|
+
hasLowercase: boolean
|
|
18
|
+
hasNumber: boolean
|
|
19
|
+
hasSymbol: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PasswordAnalysis {
|
|
23
|
+
requirements: PasswordRequirements
|
|
24
|
+
/** Number of requirements met (0–5). */
|
|
25
|
+
met: number
|
|
26
|
+
strength: PasswordStrength
|
|
27
|
+
/** 0–8 segment score for smooth progress bar fill. */
|
|
28
|
+
segmentScore: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Single-pass analysis: requirements, strength, and segment score.
|
|
33
|
+
* Use this instead of calling getPasswordRequirements / getPasswordStrength /
|
|
34
|
+
* getPasswordSegmentScore individually which avoids redundant computation on every keystroke.
|
|
35
|
+
*/
|
|
36
|
+
export function getPasswordAnalysis(password: string): PasswordAnalysis {
|
|
37
|
+
if (password.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
requirements: { hasMinLength: false, hasUppercase: false, hasLowercase: false, hasNumber: false, hasSymbol: false },
|
|
40
|
+
met: 0,
|
|
41
|
+
strength: 'empty',
|
|
42
|
+
segmentScore: 0,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const requirements = computeRequirements(password)
|
|
47
|
+
const met = [
|
|
48
|
+
requirements.hasMinLength,
|
|
49
|
+
requirements.hasUppercase,
|
|
50
|
+
requirements.hasLowercase,
|
|
51
|
+
requirements.hasNumber,
|
|
52
|
+
requirements.hasSymbol,
|
|
53
|
+
].filter(Boolean).length
|
|
54
|
+
|
|
55
|
+
const weak = isPasswordWeak(password)
|
|
56
|
+
|
|
57
|
+
const strength: PasswordStrength =
|
|
58
|
+
weak || met < 3 ? 'poor'
|
|
59
|
+
: met === 3 ? 'fair'
|
|
60
|
+
: met === 4 ? 'good'
|
|
61
|
+
: 'excellent'
|
|
62
|
+
|
|
63
|
+
// Segment score: 0–8 for smooth progress bar fill.
|
|
64
|
+
// When weak, cap at 2 regardless of met count.
|
|
65
|
+
let segmentScore: number
|
|
66
|
+
if (weak) {
|
|
67
|
+
segmentScore = Math.min(2, Math.max(1, met))
|
|
68
|
+
} else if (met <= 2) {
|
|
69
|
+
segmentScore = met
|
|
70
|
+
} else if (met === 3) {
|
|
71
|
+
segmentScore = requirements.hasMinLength && password.length >= 10 ? 4 : 3
|
|
72
|
+
} else if (met === 4) {
|
|
73
|
+
segmentScore = requirements.hasMinLength && password.length >= 12 ? 6 : 5
|
|
74
|
+
} else {
|
|
75
|
+
segmentScore = requirements.hasMinLength && password.length >= 14 ? 8 : 7
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { requirements, met, strength, segmentScore }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Convenience wrappers (backward-compat, delegate to getPasswordAnalysis)
|
|
82
|
+
|
|
83
|
+
export function getPasswordRequirements(password: string): PasswordRequirements {
|
|
84
|
+
return getPasswordAnalysis(password).requirements
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getPasswordStrength(password: string): PasswordStrength {
|
|
88
|
+
return getPasswordAnalysis(password).strength
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Returns 0–8 for smooth segment progression. */
|
|
92
|
+
export function getPasswordSegmentScore(password: string): number {
|
|
93
|
+
return getPasswordAnalysis(password).segmentScore
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Internal
|
|
97
|
+
|
|
98
|
+
function computeRequirements(password: string): PasswordRequirements {
|
|
99
|
+
let upper = false, lower = false, digit = false, symbol = false
|
|
100
|
+
for (let i = 0; i < password.length; i++) {
|
|
101
|
+
const c = password.charCodeAt(i)
|
|
102
|
+
if (c >= 65 && c <= 90) upper = true
|
|
103
|
+
else if (c >= 97 && c <= 122) lower = true
|
|
104
|
+
else if (c >= 48 && c <= 57) digit = true
|
|
105
|
+
else if (c >= 33 && c <= 126) symbol = true
|
|
106
|
+
if (upper && lower && digit && symbol) break
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
hasMinLength: password.length >= 8,
|
|
110
|
+
hasUppercase: upper,
|
|
111
|
+
hasLowercase: lower,
|
|
112
|
+
hasNumber: digit,
|
|
113
|
+
hasSymbol: symbol,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern data for password validation (common words, keyboard walks, sequential).
|
|
3
|
+
* Edit this file to add or remove patterns; used by password-validation.ts.
|
|
4
|
+
*
|
|
5
|
+
* Matching uses `includes()`, so longer entries implicitly cover shorter substrings.
|
|
6
|
+
* Only add a new entry when it is NOT a substring of an existing one in the same list.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const COMMON_WORDS = [
|
|
10
|
+
'password',
|
|
11
|
+
'admin',
|
|
12
|
+
'welcome',
|
|
13
|
+
'letmein',
|
|
14
|
+
'welcome1',
|
|
15
|
+
'monkey',
|
|
16
|
+
'dragon',
|
|
17
|
+
'master',
|
|
18
|
+
'sunshine',
|
|
19
|
+
'princess',
|
|
20
|
+
'football',
|
|
21
|
+
'iloveyou',
|
|
22
|
+
'admin123',
|
|
23
|
+
'root',
|
|
24
|
+
'pass',
|
|
25
|
+
'passw0rd',
|
|
26
|
+
'password1',
|
|
27
|
+
'qwerty123',
|
|
28
|
+
'abc123',
|
|
29
|
+
'admin1',
|
|
30
|
+
'welcome123',
|
|
31
|
+
] as const
|
|
32
|
+
|
|
33
|
+
// Longer walks cover shorter prefixes (e.g. 'qwertyuiop' covers 'qwerty').
|
|
34
|
+
// Only unique, non-substring walks are listed.
|
|
35
|
+
export const KEYBOARD_WALKS = [
|
|
36
|
+
'qwertyuiop', // covers 'qwerty'
|
|
37
|
+
'asdfghjkl', // covers 'asdfgh', 'asdf'
|
|
38
|
+
'zxcvbnm', // covers 'zxcvbn'
|
|
39
|
+
'qazwsx',
|
|
40
|
+
'1qaz2wsx',
|
|
41
|
+
'qweasd',
|
|
42
|
+
'123qwe',
|
|
43
|
+
'qwe123',
|
|
44
|
+
'poiuyt',
|
|
45
|
+
'lkjhgf',
|
|
46
|
+
'mnbvcx',
|
|
47
|
+
'1q2w3e4r',
|
|
48
|
+
'q1w2e3r4',
|
|
49
|
+
'zaq1wsx',
|
|
50
|
+
'xsw2zaq',
|
|
51
|
+
] as const
|
|
52
|
+
|
|
53
|
+
// Full forward + reverse covers all shorter subsequences (e.g. '0123456789' covers '1234', '012', etc.).
|
|
54
|
+
// '1234567890' and '0987654321' wrap around (9→0) and are NOT substrings of the straight sequences.
|
|
55
|
+
export const SEQUENTIAL_DIGITS = [
|
|
56
|
+
'0123456789',
|
|
57
|
+
'1234567890',
|
|
58
|
+
'9876543210',
|
|
59
|
+
'0987654321',
|
|
60
|
+
] as const
|
|
61
|
+
|
|
62
|
+
// Full forward + reverse covers all shorter subsequences (e.g. 'abc', 'abcdef', 'cba', 'fedcba').
|
|
63
|
+
export const SEQUENTIAL_LETTERS = [
|
|
64
|
+
'abcdefghijklmnopqrstuvwxyz',
|
|
65
|
+
'zyxwvutsrqponmlkjihgfedcba',
|
|
66
|
+
] as const
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password validation: blocks weak patterns and returns human-readable errors.
|
|
3
|
+
* Inlined for TorchUI standalone; patterns: sequential, keyboard walks, repetitive, common words.
|
|
4
|
+
*
|
|
5
|
+
* Note: common-word check uses substring matching (`includes`), so short words like
|
|
6
|
+
* "pass" or "root" may cause false positives on longer passwords. This is intentional —
|
|
7
|
+
* passwords that embed common words are weaker even in a longer context.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
COMMON_WORDS,
|
|
11
|
+
KEYBOARD_WALKS,
|
|
12
|
+
SEQUENTIAL_DIGITS,
|
|
13
|
+
SEQUENTIAL_LETTERS,
|
|
14
|
+
} from './password-validation-data'
|
|
15
|
+
|
|
16
|
+
export interface PasswordValidationResult {
|
|
17
|
+
valid: boolean
|
|
18
|
+
error?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks the (already-trimmed) password against blocked patterns.
|
|
23
|
+
* Returns a human-readable error string if a pattern is found, or `null` if clean.
|
|
24
|
+
*/
|
|
25
|
+
function containsBlockedPattern(trimmed: string): string | null {
|
|
26
|
+
const lower = trimmed.toLowerCase()
|
|
27
|
+
|
|
28
|
+
for (const word of COMMON_WORDS) {
|
|
29
|
+
if (lower === word || lower.includes(word)) {
|
|
30
|
+
return 'Password contains a common word. Choose something more unique.'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const walk of KEYBOARD_WALKS) {
|
|
35
|
+
if (lower.includes(walk)) {
|
|
36
|
+
return 'Password contains a common keyboard pattern.'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const seq of SEQUENTIAL_DIGITS) {
|
|
41
|
+
if (lower.includes(seq)) {
|
|
42
|
+
return 'Password contains sequential numbers.'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const seq of SEQUENTIAL_LETTERS) {
|
|
47
|
+
if (lower.includes(seq)) {
|
|
48
|
+
return 'Password contains sequential letters.'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// No more than 2 identical characters in a row
|
|
53
|
+
if (/(.)\1{2,}/.test(trimmed)) {
|
|
54
|
+
return 'Password contains too many repeated characters.'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Repetitive patterns (e.g. "abcabcabc"): checks 1-3 char repeating prefix
|
|
58
|
+
if (trimmed.length >= 6) {
|
|
59
|
+
for (let len = 1; len <= 3; len++) {
|
|
60
|
+
const pattern = lower.slice(0, len)
|
|
61
|
+
let repeats = 1
|
|
62
|
+
for (let i = len; i < lower.length; i += len) {
|
|
63
|
+
if (lower.slice(i, i + len) === pattern) repeats++
|
|
64
|
+
else break
|
|
65
|
+
}
|
|
66
|
+
if (repeats >= 3 && repeats * len >= 6) {
|
|
67
|
+
return 'Password is too repetitive.'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Validate a password for form submission. Checks minimum length (8) and blocked patterns. */
|
|
76
|
+
export function validatePassword(password: string): PasswordValidationResult {
|
|
77
|
+
const p = password.trim()
|
|
78
|
+
if (p.length < 8) {
|
|
79
|
+
return { valid: false, error: 'Password must be at least 8 characters' }
|
|
80
|
+
}
|
|
81
|
+
const error = containsBlockedPattern(p)
|
|
82
|
+
return error ? { valid: false, error } : { valid: true }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a password contains blocked patterns (sequential, keyboard walks, etc.).
|
|
87
|
+
* Does NOT check length — use this for strength indicators, not form validation.
|
|
88
|
+
*/
|
|
89
|
+
export function isPasswordWeak(password: string): boolean {
|
|
90
|
+
const p = password.trim()
|
|
91
|
+
if (!p) return false // Empty is not "weak", just empty
|
|
92
|
+
return containsBlockedPattern(p) !== null
|
|
93
|
+
}
|