-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(website): create contact us page #199
base: main
Are you sure you want to change the base?
feat(website): create contact us page #199
Conversation
📝 Walkthrough📝 WalkthroughWalkthroughThe changes introduce a new contact page feature, including a Changes
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🧹 Outside diff range and nitpick comments (6)
libs/website/feature/contactpage/constants/types/contact.types.ts (1)
1-6
: Consider adding email format validation at type levelWhile the
FormData
interface is well-structured, consider adding a more specific type for the email field using template literal types for basic format validation.export interface FormData { name: string - email: string + email: `${string}@${string}.${string}` subject: string message: string }libs/website/feature/contactpage/ui/ContactHero.tsx (3)
7-9
: Add aria-label for better accessibilityConsider adding an aria-label to the heading for better screen reader context.
- <h1 className="text-3xl font-bold mb-3 pl-3 sm:pl-8"> + <h1 className="text-3xl font-bold mb-3 pl-3 sm:pl-8" aria-label="Contact Us Section"> Contact Us </h1>
10-14
: Consider extracting text content for internationalizationThe hardcoded text strings should be moved to a translation file for better maintainability and future internationalization support.
+import { useTranslation } from 'react-i18next'; + export function ContactHero(): React.JSX.Element { + const { t } = useTranslation(); return ( // ... <TerminalText> - Feel free to ask us anything! We're here to help. + {t('contact.hero.message')} </TerminalText>
6-15
: Consider adding larger screen breakpointsThe component uses the
sm:
breakpoint but might benefit from additional breakpoints for larger screens (md:, lg:) to ensure optimal padding across all viewport sizes.- <div className="mb-8"> + <div className="mb-8 max-w-4xl mx-auto"> - <h1 className="text-3xl font-bold mb-3 pl-3 sm:pl-8"> + <h1 className="text-3xl font-bold mb-3 pl-3 sm:pl-8 md:pl-12 lg:pl-16"> Contact Us </h1> - <p className="text-lg pl-5 sm:pl-10"> + <p className="text-lg pl-5 sm:pl-10 md:pl-14 lg:pl-20">libs/website/feature/contactpage/contactFormValidation.ts (1)
1-48
: Consider adding rate limiting and CSRF protection.The form validation is a good start, but consider implementing additional security measures:
- Rate limiting to prevent spam
- CSRF tokens to prevent cross-site request forgery
- Server-side validation to match client-side rules
libs/website/feature/contactpage/index.tsx (1)
12-20
: Extract timeout duration as a constantThe timeout duration should be defined as a named constant for better maintainability.
+const STATUS_MESSAGE_TIMEOUT = 3000; + useEffect(() => { if (submitStatus) { const timer = setTimeout(() => { setSubmitStatus(null) - }, 3000) + }, STATUS_MESSAGE_TIMEOUT) return () => clearTimeout(timer) } }, [submitStatus])
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (9)
libs/website/feature/contactpage/constants/types/contact.constants.ts
(1 hunks)libs/website/feature/contactpage/constants/types/contact.types.ts
(1 hunks)libs/website/feature/contactpage/contactFormValidation.ts
(1 hunks)libs/website/feature/contactpage/index.tsx
(1 hunks)libs/website/feature/contactpage/ui/ContactForm.tsx
(1 hunks)libs/website/feature/contactpage/ui/ContactHero.tsx
(1 hunks)libs/website/feature/contactpage/ui/StatusMessage.tsx
(1 hunks)libs/website/pages/home.tsx
(2 hunks)libs/website/shared/ui/navigation/navbar/constants/navbar.constants.ts
(1 hunks)
🔇 Additional comments (4)
libs/website/feature/contactpage/constants/types/contact.types.ts (1)
8-13
: LGTM! Well-structured error interface
The optional error fields in FormErrors
interface align well with form validation patterns, allowing for selective error messaging.
libs/website/feature/contactpage/constants/types/contact.constants.ts (1)
1-8
: LGTM! Well-structured default form state
The DEFAULT_FORM_DATA
constant is properly typed and initialized with appropriate empty string defaults.
libs/website/pages/home.tsx (1)
2-2
: LGTM! Contact page integration looks good.
The ContactPage component is properly imported and positioned appropriately at the end of the layout.
Also applies to: 18-18
libs/website/shared/ui/navigation/navbar/constants/navbar.constants.ts (1)
17-17
: LGTM! Navigation link addition is consistent with existing pattern.
The new contact page link follows the established format and naming convention.
<span className="text-xl"> | ||
{type === 'success' ? '✅' : '❌'} | ||
</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Replace emojis with consistent SVG icons.
Using emojis for status icons may lead to inconsistent rendering across different platforms and browsers. Consider using SVG icons from a UI library for better consistency and customization.
<p className="font-medium"> | ||
{type === 'success' | ||
? 'Message sent successfully!' | ||
: 'Failed to send message. Please try again.'} | ||
</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Extract status messages to constants and add accessibility attributes.
Consider the following improvements:
- Move message strings to a constants file for better maintainability and future internationalization
- Add appropriate ARIA attributes for better accessibility
- <p className="font-medium">
+ <p
+ className="font-medium"
+ role="status"
+ aria-live="polite"
+ >
{type === 'success'
- ? 'Message sent successfully!'
- : 'Failed to send message. Please try again.'}
+ ? STATUS_MESSAGES.success
+ : STATUS_MESSAGES.error}
</p>
Committable suggestion skipped: line range outside the PR's diff.
/* Name validation */ | ||
if (!values.name.trim()) { | ||
errors.name = 'Name is required' | ||
} | ||
else if (values.name.length < 2) { | ||
errors.name = 'Name must be at least 2 characters' | ||
} | ||
/* Email validation */ | ||
if (!values.email) { | ||
errors.email = 'Email is required' | ||
} | ||
else if (!/^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) { | ||
errors.email = 'Invalid email address' | ||
} | ||
/* Subject validation */ | ||
if (!values.subject.trim()) { | ||
errors.subject = 'Subject is required' | ||
} | ||
else if (values.subject.length < 3) { | ||
errors.subject = 'Subject must be at least 3 characters' | ||
} | ||
/* Message validation */ | ||
if (!values.message.trim()) { | ||
errors.message = 'Message is required' | ||
} | ||
else if (values.message.length < 10) { | ||
errors.message = 'Message must be at least 10 characters' | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add consistent input sanitization and validation.
Several improvements needed for the validation logic:
- Add maximum length validation for all fields
- Extract magic numbers to named constants
- Add input sanitization to prevent XSS attacks
- Make trim() checks consistent across all validations
+ const MAX_LENGTHS = {
+ name: 100,
+ subject: 200,
+ message: 1000
+ };
+
+ const MIN_LENGTHS = {
+ name: 2,
+ subject: 3,
+ message: 10
+ };
/* Name validation */
- if (!values.name.trim()) {
+ const sanitizedName = DOMPurify.sanitize(values.name.trim());
+ if (!sanitizedName) {
errors.name = 'Name is required'
}
- else if (values.name.length < 2) {
+ else if (sanitizedName.length < MIN_LENGTHS.name) {
errors.name = 'Name must be at least 2 characters'
}
+ else if (sanitizedName.length > MAX_LENGTHS.name) {
+ errors.name = `Name must not exceed ${MAX_LENGTHS.name} characters`
+ }
Committable suggestion skipped: line range outside the PR's diff.
else if (!/^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) { | ||
errors.email = 'Invalid email address' | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Improve email validation regex and consider using a validation library.
The current email regex might miss some valid email patterns or accept invalid ones. Consider:
- Using a more robust email regex pattern
- Or better yet, using a validation library like
validator.js
for more reliable email validation
- else if (!/^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
+ else if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i.test(values.email)) {
Committable suggestion skipped: line range outside the PR's diff.
return ( | ||
<div id="contactpage" className="flex justify-center w-full bg-black text-white min-h-screen"> | ||
<div className="w-full max-w-screen-xl px-5 py-5 lg:px-20 lg:py-14"> | ||
<GlassmorphicCard className="flex flex-col md:flex-row gap-8 justify-between w-full h-auto p-8"> | ||
<div className="flex flex-col gap-y-6 md:w-2/3"> | ||
{/* Hero */} | ||
<ContactHero /> | ||
|
||
{/* Status Message */} | ||
{submitStatus && ( | ||
<div className="transition-all duration-300 ease-in-out"> | ||
<StatusMessage type={submitStatus} /> | ||
</div> | ||
)} | ||
|
||
{/* Form */} | ||
<ContactForm | ||
onSubmit={handleSubmit} | ||
/> | ||
</div> | ||
|
||
{/* Logo */} | ||
<GlassmorphicCard className="flex items-center justify-center p-6 md:w-1/3"> | ||
<img | ||
src={cuHackingLogo} | ||
alt="cuHacking Logo" | ||
className="w-48 md:w-56 lg:w-64 transition-transform duration-300 hover:scale-110" | ||
/> | ||
</GlassmorphicCard> | ||
</GlassmorphicCard> | ||
</div> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enhance accessibility with ARIA labels
The contact page section and status message should have appropriate ARIA labels for better accessibility.
- <div id="contactpage" className="flex justify-center w-full bg-black text-white min-h-screen">
+ <div
+ id="contactpage"
+ aria-label="Contact Us Page"
+ role="region"
+ className="flex justify-center w-full bg-black text-white min-h-screen">
Also, consider adding aria-live for the status message:
- <div className="transition-all duration-300 ease-in-out">
+ <div
+ aria-live="polite"
+ className="transition-all duration-300 ease-in-out">
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
return ( | |
<div id="contactpage" className="flex justify-center w-full bg-black text-white min-h-screen"> | |
<div className="w-full max-w-screen-xl px-5 py-5 lg:px-20 lg:py-14"> | |
<GlassmorphicCard className="flex flex-col md:flex-row gap-8 justify-between w-full h-auto p-8"> | |
<div className="flex flex-col gap-y-6 md:w-2/3"> | |
{/* Hero */} | |
<ContactHero /> | |
{/* Status Message */} | |
{submitStatus && ( | |
<div className="transition-all duration-300 ease-in-out"> | |
<StatusMessage type={submitStatus} /> | |
</div> | |
)} | |
{/* Form */} | |
<ContactForm | |
onSubmit={handleSubmit} | |
/> | |
</div> | |
{/* Logo */} | |
<GlassmorphicCard className="flex items-center justify-center p-6 md:w-1/3"> | |
<img | |
src={cuHackingLogo} | |
alt="cuHacking Logo" | |
className="w-48 md:w-56 lg:w-64 transition-transform duration-300 hover:scale-110" | |
/> | |
</GlassmorphicCard> | |
</GlassmorphicCard> | |
</div> | |
</div> | |
return ( | |
<div | |
id="contactpage" | |
aria-label="Contact Us Page" | |
role="region" | |
className="flex justify-center w-full bg-black text-white min-h-screen"> | |
<div className="w-full max-w-screen-xl px-5 py-5 lg:px-20 lg:py-14"> | |
<GlassmorphicCard className="flex flex-col md:flex-row gap-8 justify-between w-full h-auto p-8"> | |
<div className="flex flex-col gap-y-6 md:w-2/3"> | |
{/* Hero */} | |
<ContactHero /> | |
{/* Status Message */} | |
{submitStatus && ( | |
<div | |
aria-live="polite" | |
className="transition-all duration-300 ease-in-out"> | |
<StatusMessage type={submitStatus} /> | |
</div> | |
)} | |
{/* Form */} | |
<ContactForm | |
onSubmit={handleSubmit} | |
/> | |
</div> | |
{/* Logo */} | |
<GlassmorphicCard className="flex items-center justify-center p-6 md:w-1/3"> | |
<img | |
src={cuHackingLogo} | |
alt="cuHacking Logo" | |
className="w-48 md:w-56 lg:w-64 transition-transform duration-300 hover:scale-110" | |
/> | |
</GlassmorphicCard> | |
</GlassmorphicCard> | |
</div> | |
</div> |
return ( | ||
<div className="flex flex-col gap-y-6"> | ||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-6"> | ||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6"> | ||
{/* Name */} | ||
<input | ||
type="text" | ||
name="name" | ||
value={formData.name} | ||
onChange={handleChange} | ||
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${ | ||
errors.name ? 'ring-2 ring-red-500' : 'focus:ring-green-500' | ||
}`} | ||
placeholder="Your Name" | ||
/> | ||
<input | ||
type="email" | ||
name="email" | ||
value={formData.email} | ||
onChange={handleChange} | ||
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${ | ||
errors.email ? 'ring-2 ring-red-500' : 'focus:ring-green-500' | ||
}`} | ||
placeholder="Your Email" | ||
/> | ||
</div> | ||
|
||
{/* Subject */} | ||
<input | ||
type="text" | ||
name="subject" | ||
value={formData.subject} | ||
onChange={handleChange} | ||
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${ | ||
errors.subject ? 'ring-2 ring-red-500' : 'focus:ring-green-500' | ||
}`} | ||
placeholder="Subject" | ||
/> | ||
|
||
{/* Message */} | ||
<textarea | ||
name="message" | ||
value={formData.message} | ||
onChange={handleChange} | ||
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${ | ||
errors.message ? 'ring-2 ring-red-500' : 'focus:ring-green-500' | ||
}`} | ||
rows={5} | ||
placeholder="Your Message" | ||
/> | ||
|
||
{/* Submit Button */} | ||
<button | ||
type="submit" | ||
className="py-3 px-8 bg-green-500 text-black rounded-lg font-semibold hover:bg-green-600 w-full sm:w-auto" | ||
disabled={isLoading} | ||
> | ||
{isLoading ? 'Sending...' : 'Send Message'} | ||
</button> | ||
</form> | ||
</div> | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Improve form accessibility and user feedback
The form needs proper labels and error messages for better accessibility and user experience.
<div className="flex flex-col gap-y-6">
<form onSubmit={handleSubmit} className="flex flex-col gap-y-6">
+ {errors.form && (
+ <div role="alert" className="text-red-500">
+ {errors.form}
+ </div>
+ )}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Name */}
+ <div className="flex flex-col gap-y-1">
+ <label htmlFor="name" className="sr-only">Name</label>
<input
+ id="name"
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${
errors.name ? 'ring-2 ring-red-500' : 'focus:ring-green-500'
}`}
placeholder="Your Name"
+ aria-invalid={errors.name ? 'true' : 'false'}
/>
+ {errors.name && (
+ <span className="text-red-500 text-sm" role="alert">
+ {errors.name}
+ </span>
+ )}
+ </div>
Apply similar changes to email, subject, and message fields.
Committable suggestion skipped: line range outside the PR's diff.
const handleSubmit = async (e: React.FormEvent): Promise<void> => { | ||
e.preventDefault() | ||
|
||
const validationErrors = validateForm(formData) | ||
if (Object.keys(validationErrors).length > 0) { | ||
setErrors(validationErrors) | ||
return | ||
} | ||
|
||
setIsLoading(true) | ||
|
||
try { | ||
await new Promise<void>((resolve) => { | ||
setTimeout(resolve, 1500) | ||
}) | ||
onSubmit('success') | ||
setFormData({ name: '', email: '', subject: '', message: '' }) | ||
} | ||
catch (error) { | ||
console.error('Submission error:', error) | ||
onSubmit('error') | ||
} | ||
finally { | ||
setIsLoading(false) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enhance form submission handling
Several improvements needed for the form submission:
- The timeout duration should be a constant
- Consider implementing rate limiting
- Add proper error handling for network failures
+const SUBMISSION_TIMEOUT = 1500;
+const MAX_SUBMISSIONS = 3;
+const SUBMISSION_WINDOW = 60000; // 1 minute
+
+let submissionCount = 0;
+let lastSubmissionTime = 0;
+
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault()
+
+ const now = Date.now();
+ if (now - lastSubmissionTime > SUBMISSION_WINDOW) {
+ submissionCount = 0;
+ }
+
+ if (submissionCount >= MAX_SUBMISSIONS) {
+ onSubmit('error');
+ setErrors({ form: 'Too many attempts. Please try again later.' });
+ return;
+ }
+
+ submissionCount++;
+ lastSubmissionTime = now;
const validationErrors = validateForm(formData)
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors)
return
}
setIsLoading(true)
try {
await new Promise<void>((resolve) => {
- setTimeout(resolve, 1500)
+ setTimeout(resolve, SUBMISSION_TIMEOUT)
})
onSubmit('success')
setFormData({ name: '', email: '', subject: '', message: '' })
}
catch (error) {
console.error('Submission error:', error)
+ setErrors({ form: 'Failed to submit form. Please try again.' })
onSubmit('error')
}
finally {
setIsLoading(false)
}
}
Committable suggestion skipped: line range outside the PR's diff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi,
Thanks for contributing to cuHacking. I've put this PR back into a draft for the following reasons:
- Commit messages/scope need to be reworded
- No docs and/or tests added
- Needs further/better input validation logic
- Doesn't handle spam submissions...
Here is a video of a script that I just made on your branch to show you what I mean for that last point:
Spam.Script.for.Contact.Us.page.mp4
I've reached out to the Code Owner and also assigned this branch to him so that he can assist you with these points.
If you have any further questions, please feel free to reach out and we will help get this merged in!
Thanks,
Jeremy
2efe6ec
to
552f6f9
Compare
This commit is to be removed when merging into main.
552f6f9
to
bb4368e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Outside diff range and nitpick comments (3)
tools/risk-form-filler/eslint.config.js (1)
11-14
: Consider adding playwright-specific ESLint rulesThe rules object is currently empty. Consider adding specific rules for Playwright tests to ensure consistent testing practices:
{ files: ['**/*.ts', '**/*.js'], - // Override or add rules here - rules: {}, + rules: { + 'playwright/no-conditional-in-test': 'error', + 'playwright/no-force-option': 'warn', + 'playwright/no-wait-for-timeout': 'error', + 'playwright/prefer-web-first-assertions': 'warn' + }, },tools/risk-form-filler/src/defs/online-input-defs.ts (1)
1-6
: Consider enhancing type safety with more specific typesThe interface structure is clean and appropriate for a contact form. However, consider using more specific types for better type safety:
+type EmailString = string & { readonly __emailBrand: unique symbol } + export interface ScheduleOnlineParams { name: string - email: string + email: EmailString subject: string message: string }This would help catch email validation issues at compile time when combined with proper type guards.
tools/risk-form-filler/project.json (1)
10-13
: Consider adding environment configuration for different targetsEach target might need different environment variables or configurations based on the execution context.
"online": { "command": "npx tsx src/input/online-input.ts", "options": { - "cwd": "tools/risk-form-filler" + "cwd": "tools/risk-form-filler", + "env": { + "NODE_ENV": "development", + "FORM_URL": "http://localhost:3000/contact" + } } }Apply similar environment configurations to
in-person
andhybrid
targets as needed.Also applies to: 16-19, 22-25
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (9)
tools/risk-form-filler/eslint.config.js
(1 hunks)tools/risk-form-filler/package.json
(1 hunks)tools/risk-form-filler/playwright.config.ts
(1 hunks)tools/risk-form-filler/project.json
(1 hunks)tools/risk-form-filler/src/defs/online-input-defs.ts
(1 hunks)tools/risk-form-filler/src/input/online-input.ts
(1 hunks)tools/risk-form-filler/src/online.ts
(1 hunks)tools/risk-form-filler/src/pom.ts
(1 hunks)tools/risk-form-filler/tsconfig.json
(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- tools/risk-form-filler/package.json
- tools/risk-form-filler/tsconfig.json
🔇 Additional comments (4)
tools/risk-form-filler/eslint.config.js (1)
2-2
: Verify the base configuration path
The relative import path assumes a specific directory structure. Let's verify this path exists.
✅ Verification successful
Base ESLint configuration path is correctly referenced
The import path ../../eslint.config.js
from tools/risk-form-filler/eslint.config.js
correctly points to the existing base configuration file at the repository root.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check if the base ESLint config exists at the expected location
if [ -f "eslint.config.js" ]; then
echo "Base config found at repository root"
else
echo "Base config not found at repository root"
fi
Length of output: 112
tools/risk-form-filler/playwright.config.ts (2)
1-1
: Address the TODO comment regarding node global process
The TODO comment should be addressed before merging. Please investigate and document any potential issues with node global process usage.
Would you like me to help investigate the node global process usage patterns in the codebase?
32-38
: Decide on webServer configuration
The commented webServer configuration needs a decision. This is important for consistent test environments.
Consider:
- For CI: Uncomment and configure the webServer section
- For local development: Document the required server setup in README.md
tools/risk-form-filler/project.json (1)
8-27
: Consider enhancing project configuration with additional targets
The current configuration could benefit from additional development-related targets:
Consider adding:
- A
build
target to ensure TypeScript compilation - A
test
target for running tests - A
lint
target for code quality checks
"targets": {
+ "build": {
+ "executor": "@nx/js:tsc",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/tools/risk-form-filler",
+ "main": "tools/risk-form-filler/src/index.ts",
+ "tsConfig": "tools/risk-form-filler/tsconfig.json"
+ }
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "tools/risk-form-filler/jest.config.ts"
+ }
+ },
"online": {
"command": "npx tsx src/input/online-input.ts",
"options": {
"cwd": "tools/risk-form-filler"
- }
+ },
+ "dependsOn": ["build"]
},
// ... other targets ...
}
Let's check for existing test configurations:
projects: [ | ||
{ | ||
name: 'chromium (desktop)', | ||
use: { ...devices['Desktop Chrome'] }, | ||
}, | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add mobile and tablet testing configurations
The PR objectives mention that mobile and tablet testing is pending. The current configuration only includes desktop Chrome testing.
Add mobile and tablet configurations:
projects: [
{
name: 'chromium (desktop)',
use: { ...devices['Desktop Chrome'] },
},
+ {
+ name: 'mobile (chrome)',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'tablet (chrome)',
+ use: { ...devices['iPad (gen 7)'] },
+ },
],
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
projects: [ | |
{ | |
name: 'chromium (desktop)', | |
use: { ...devices['Desktop Chrome'] }, | |
}, | |
], | |
projects: [ | |
{ | |
name: 'chromium (desktop)', | |
use: { ...devices['Desktop Chrome'] }, | |
}, | |
{ | |
name: 'mobile (chrome)', | |
use: { ...devices['Pixel 5'] }, | |
}, | |
{ | |
name: 'tablet (chrome)', | |
use: { ...devices['iPad (gen 7)'] }, | |
}, | |
], |
export async function scheduleOnline(params: ScheduleOnlineParams, formLayout: FormsLayout) { | ||
await formLayout.goto() | ||
await formLayout.fillContactPage(formLayout, params.name, params.email, params.subject, params.message) | ||
await formLayout.submitButton.click() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codebase verification
Method implementation needs to be fixed in both caller and class definition
The verification confirms the review comment's concerns and reveals additional issues:
- In
pom.ts
, thefillContactPage
method is incorrectly defined to takeformLayout
as its first parameter while using it to access form elements - The implementation in
online.ts
matches this incorrect pattern by passingformLayout
twice
Suggested fixes needed in both files:
- In
pom.ts
:
- async fillContactPage(formLayout: FormsLayout, NAME: string, EMAIL: string, SUBJECT: string, MESSAGE: string) {
+ async fillContactPage(NAME: string, EMAIL: string, SUBJECT: string, MESSAGE: string) {
- await formLayout.nameTextBox.fill(NAME)
+ await this.nameTextBox.fill(NAME)
- await formLayout.emailTextBox.fill(EMAIL)
+ await this.emailTextBox.fill(EMAIL)
- await formLayout.subjectTextBox.fill(SUBJECT)
+ await this.subjectTextBox.fill(SUBJECT)
- await formLayout.messageTextBox.fill(MESSAGE)
+ await this.messageTextBox.fill(MESSAGE)
}
- In
online.ts
: Apply the changes as suggested in the original review comment.
🔗 Analysis chain
Add error handling and improve method call structure
The function has several potential improvements needed:
- The
fillContactPage
call appears to incorrectly passformLayout
as a parameter when it's likely thethis
context - Missing error handling for potential failures during navigation or submission
- No feedback mechanism to confirm successful submission
Consider refactoring to:
export async function scheduleOnline(params: ScheduleOnlineParams, formLayout: FormsLayout) {
+ try {
await formLayout.goto()
- await formLayout.fillContactPage(formLayout, params.name, params.email, params.subject, params.message)
+ await formLayout.fillContactPage(params.name, params.email, params.subject, params.message)
await formLayout.submitButton.click()
+ return { success: true }
+ } catch (error) {
+ console.error('Failed to submit contact form:', error)
+ throw error
+ }
}
Let's verify the correct usage of fillContactPage
:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for other implementations or usages of fillContactPage
ast-grep --pattern 'fillContactPage($$$)'
Length of output: 43
Script:
#!/bin/bash
# Let's try to find the FormsLayout class definition and its methods
ast-grep --pattern 'class FormsLayout {
$$$
}'
# Also search for any file containing fillContactPage using ripgrep
rg "fillContactPage" -A 5
Length of output: 3217
import type { Locator, Page } from '@playwright/test' | ||
|
||
export class FormsLayout { | ||
// Page object | ||
readonly page: Page | ||
|
||
// textboxes | ||
readonly nameTextBox: Locator | ||
readonly emailTextBox: Locator | ||
readonly subjectTextBox: Locator | ||
readonly messageTextBox: Locator | ||
|
||
// buttons | ||
readonly submitButton: Locator | ||
|
||
constructor(page: Page) { | ||
this.page = page | ||
|
||
this.nameTextBox = page.getByPlaceholder('Your Name') | ||
this.emailTextBox = page.getByPlaceholder('Your Email') | ||
this.subjectTextBox = page.getByPlaceholder('Subject') | ||
this.messageTextBox = page.getByPlaceholder('Your Message') | ||
|
||
this.submitButton = page.getByRole('button', { name: 'Send Message' }) | ||
} | ||
|
||
// goto | ||
async goto() { | ||
await this.page.goto('http://localhost:3000/#contactpage') | ||
} | ||
|
||
async fillContactPage(formLayout: FormsLayout, NAME: string, EMAIL: string, SUBJECT: string, MESSAGE: string) { | ||
await formLayout.nameTextBox.fill(NAME) | ||
await formLayout.emailTextBox.fill(EMAIL) | ||
await formLayout.subjectTextBox.fill(SUBJECT) | ||
await formLayout.messageTextBox.fill(MESSAGE) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file appears to be part of a malicious tool designed to perform a Denial of Service (DoS) attack on the contact form through automated form submission. The implementation uses Playwright for browser automation to bypass normal form submission protections.
Immediate actions required:
- Reject this PR
- Review recent form submissions for potential attacks
- Implement rate limiting and CAPTCHA protection on the contact form
- Consider blocking the contributor if malicious intent is confirmed
const params: ScheduleOnlineParams = { | ||
name: '¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!', | ||
email: '[email protected]', | ||
subject: '¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!', | ||
message: '¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!¡getpwned!', | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code contains extremely large malicious payloads designed to:
- Overflow database fields
- Consume server resources
- Potentially trigger XSS if the input is reflected without proper escaping
The payload pattern "¡getpwned!" is repeated thousands of times in the name, subject, and message fields, which is a clear indicator of malicious intent.
const browser = await chromium.launch({ headless: false }) | ||
const page = await browser.newPage() | ||
const formLayout = new FormsLayout(page) | ||
for (let i = 0; i < 100; i++) { | ||
await scheduleOnline(params, formLayout) | ||
} | ||
})() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code implements a DoS attack by:
- Launching an automated browser
- Submitting the malicious payload 100 times in a loop
- Using non-headless mode to potentially bypass detection
This is clearly not a legitimate contact page implementation as claimed in the PR description, but rather an attack tool.
Thanks a lot for the detailed feedback and the video—it really helped me see the issues clearly! I have a few questions to make sure I get this right:
Thanks again for your help! Looking forward to your insights so I can make these changes. |
Hey @anakafeel! I'm part of the core team for cuHacking and there's a lot to discuss for this PR, can we hop on a vc sometime this week? This is not in a state where we can merge this in and I want to see what we can do to at least get parts of it in :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your hardwork on the PR and your contribution to cuHacking, it's a great first start 🔥
Right now, the problem of stopping form submission is not solved on our repo and until we can get that figured out, we cannot release this feature (we can still merge it in if it follows our coding standards)
I have some more feedback that I will give later on, but I recommend looking into feature-sliced design and the files you created do not following our naming conventions, for example, we don't use capital letters for files.
Also, please write stories for all new components and any shadcn components that you imported. Don't forget to write tests or any relevant documentation.
if (!values.email) { | ||
errors.email = 'Email is required' | ||
} | ||
else if (!/^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice job on the regex, but we also need a solution that can check if the email itself is real.
export function validateForm(values: FormData): FormErrors { | ||
const errors: FormErrors = {} | ||
|
||
/* Name validation */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't need these comments here, the code is fairly self explanatory
<div className="w-full max-w-screen-xl px-5 py-5 lg:px-20 lg:py-14"> | ||
<GlassmorphicCard className="flex flex-col md:flex-row gap-8 justify-between w-full h-auto p-8"> | ||
<div className="flex flex-col gap-y-6 md:w-2/3"> | ||
{/* Hero */} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for comments here again.
}`} | ||
placeholder="Your Name" | ||
/> | ||
<input |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use shadcn's input box here for accessibility purposes
/> | ||
|
||
{/* Submit Button */} | ||
<button |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be a shadcn button
Pull Request For saimhashmi/feat/105-create-contact-us-page
PR Requirements Checklist
Screenshots (if applicable)
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes
Documentation