detached f6d8405

password.ts RawHistoryFolder

1import { z } from 'zod';
2
3// Dynamic password constraints configuration
4export interface PasswordConstraints {
5	minLength: number;
6	maxLength: number;
7	requireUppercase: boolean;
8	requireLowercase: boolean;
9	requireNumbers: boolean;
10	requireSpecialChars: boolean;
11	forbiddenPatterns: string[];
12}
13
14// Default password constraints
15export const DEFAULT_PASSWORD_CONSTRAINTS: PasswordConstraints = {
16	minLength: 8,
17	maxLength: 64,
18	requireUppercase: true,
19	requireLowercase: true,
20	requireNumbers: true,
21	requireSpecialChars: false,
22	forbiddenPatterns: ['password', '123456', 'qwerty', 'admin']
23};
24
25// Validation error messages
26export const PASSWORD_ERROR_MESSAGES = {
27	minLength: (min: number) => `Password must be at least ${min} characters long`,
28	maxLength: (max: number) => `Password must be at most ${max} characters long`,
29	requireUppercase: 'Password must contain at least one uppercase letter',
30	requireLowercase: 'Password must contain at least one lowercase letter',
31	requireNumbers: 'Password must contain at least one number',
32	requireSpecialChars: 'Password must contain at least one special character (!@#$%^&*)',
33	forbiddenPattern: (pattern: string) => `Password cannot contain '${pattern}'`,
34	common: 'Password is too common or weak'
35};
36
37// Create dynamic password schema based on constraints
38export function createPasswordSchema(
39	constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS
40) {
41	let schema = z
42		.string()
43		.min(constraints.minLength, PASSWORD_ERROR_MESSAGES.minLength(constraints.minLength))
44		.max(constraints.maxLength, PASSWORD_ERROR_MESSAGES.maxLength(constraints.maxLength));
45
46	// Add character type requirements
47	if (constraints.requireUppercase) {
48		schema = schema.regex(/[A-Z]/, PASSWORD_ERROR_MESSAGES.requireUppercase);
49	}
50
51	if (constraints.requireLowercase) {
52		schema = schema.regex(/[a-z]/, PASSWORD_ERROR_MESSAGES.requireLowercase);
53	}
54
55	if (constraints.requireNumbers) {
56		schema = schema.regex(/\d/, PASSWORD_ERROR_MESSAGES.requireNumbers);
57	}
58
59	if (constraints.requireSpecialChars) {
60		schema = schema.regex(
61			/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/,
62			PASSWORD_ERROR_MESSAGES.requireSpecialChars
63		);
64	}
65
66	// Add forbidden patterns check
67	if (constraints.forbiddenPatterns.length > 0) {
68		return schema.refine(
69			(password) => {
70				const lowerPassword = password.toLowerCase();
71				return !constraints.forbiddenPatterns.some((pattern) =>
72					lowerPassword.includes(pattern.toLowerCase())
73				);
74			},
75			{
76				message: PASSWORD_ERROR_MESSAGES.common
77			}
78		);
79	}
80
81	return schema;
82}
83
84// Login validation schema
85export const loginSchema = z.object({
86	username: z
87		.string()
88		.min(3, 'Username must be at least 3 characters')
89		.max(32, 'Username must be at most 32 characters')
90		.regex(
91			/^[a-zA-Z0-9_-]+$/,
92			'Username can only contain letters, numbers, underscores, and dashes'
93		),
94	password: z.string().min(1, 'Password is required')
95});
96
97// Registration validation schema (using dynamic password validation)
98export function createRegistrationSchema(
99	constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS
100) {
101	return z
102		.object({
103			suUsername: z
104				.string()
105				.min(3, 'Username must be at least 3 characters')
106				.max(32, 'Username must be at most 32 characters')
107				.regex(
108					/^[a-zA-Z0-9_-]+$/,
109					'Username can only contain letters, numbers, underscores, and dashes'
110				),
111			suPassword: createPasswordSchema(constraints),
112			suConfirmPassword: z.string(),
113			captchaAnswer: z.string().min(1, 'Please answer the PDR question'),
114			turnstileToken: z.string().optional()
115		})
116		.refine((data) => data.suPassword === data.suConfirmPassword, {
117			message: 'Passwords do not match',
118			path: ['suConfirmPassword']
119		});
120}
121
122// Utility function to validate password strength and return detailed feedback
123export function validatePasswordStrength(
124	password: string,
125	constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS
126) {
127	const errors: string[] = [];
128	const requirements: { met: boolean; message: string }[] = [];
129
130	// Length checks
131	if (password.length < constraints.minLength) {
132		errors.push(PASSWORD_ERROR_MESSAGES.minLength(constraints.minLength));
133	}
134	requirements.push({
135		met: password.length >= constraints.minLength,
136		message: `At least ${constraints.minLength} characters`
137	});
138
139	if (password.length > constraints.maxLength) {
140		errors.push(PASSWORD_ERROR_MESSAGES.maxLength(constraints.maxLength));
141	}
142
143	// Character type checks
144	if (constraints.requireUppercase) {
145		const hasUppercase = /[A-Z]/.test(password);
146		if (!hasUppercase) errors.push(PASSWORD_ERROR_MESSAGES.requireUppercase);
147		requirements.push({
148			met: hasUppercase,
149			message: 'At least one uppercase letter'
150		});
151	}
152
153	if (constraints.requireLowercase) {
154		const hasLowercase = /[a-z]/.test(password);
155		if (!hasLowercase) errors.push(PASSWORD_ERROR_MESSAGES.requireLowercase);
156		requirements.push({
157			met: hasLowercase,
158			message: 'At least one lowercase letter'
159		});
160	}
161
162	if (constraints.requireNumbers) {
163		const hasNumbers = /\d/.test(password);
164		if (!hasNumbers) errors.push(PASSWORD_ERROR_MESSAGES.requireNumbers);
165		requirements.push({
166			met: hasNumbers,
167			message: 'At least one number'
168		});
169	}
170
171	if (constraints.requireSpecialChars) {
172		const hasSpecialChars = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
173		if (!hasSpecialChars) errors.push(PASSWORD_ERROR_MESSAGES.requireSpecialChars);
174		requirements.push({
175			met: hasSpecialChars,
176			message: 'At least one special character'
177		});
178	}
179
180	// Forbidden patterns check
181	if (constraints.forbiddenPatterns.length > 0) {
182		const lowerPassword = password.toLowerCase();
183		const hasForbiddenPattern = constraints.forbiddenPatterns.some((pattern) =>
184			lowerPassword.includes(pattern.toLowerCase())
185		);
186		if (hasForbiddenPattern) {
187			errors.push(PASSWORD_ERROR_MESSAGES.common);
188		}
189		requirements.push({
190			met: !hasForbiddenPattern,
191			message: 'Not a common or weak password'
192		});
193	}
194
195	return {
196		isValid: errors.length === 0,
197		errors,
198		requirements,
199		strength: calculatePasswordStrength(password, requirements)
200	};
201}
202
203// Calculate password strength score (0-100)
204function calculatePasswordStrength(
205	password: string,
206	requirements: { met: boolean; message: string }[]
207): number {
208	const metRequirements = requirements.filter((req) => req.met).length;
209	const totalRequirements = requirements.length;
210
211	if (totalRequirements === 0) return 0;
212
213	let baseScore = (metRequirements / totalRequirements) * 80;
214
215	// Bonus points for length beyond minimum
216	if (password.length > 12) baseScore += 10;
217	if (password.length > 16) baseScore += 10;
218
219	return Math.min(100, Math.round(baseScore));
220}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 9

↑↓ moveenter openesc close