Skip to content
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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

anakafeel
Copy link

@anakafeel anakafeel commented Dec 8, 2024

Pull Request For saimhashmi/feat/105-create-contact-us-page

PR Requirements Checklist

  • Commit messages follow guidelines
  • Tests added
  • Tests pass
  • Docs added/updated (for bug fixes/features)
  • Rebased onto main
  • Tested for mobile/tablet/desktop (if applicable)

Screenshots (if applicable)

After

pc contact page ss 1
mobile contact page ss 2

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced a contact page with a form for user inquiries.
    • Added validation for the contact form to ensure proper input.
    • Implemented status messages to inform users of submission success or failure.
    • Enhanced navigation by adding a direct link to the contact page.
  • Bug Fixes

    • Improved error handling for form submissions.
  • Documentation

    • Updated documentation to reflect new components and functionalities.

Copy link

coderabbitai bot commented Dec 8, 2024

📝 Walkthrough
📝 Walkthrough

Walkthrough

The changes introduce a new contact page feature, including a ContactPage component that manages form submissions and displays status messages. New constants and interfaces for form data and validation errors are defined. A validation function checks user input for correctness. Additional components, such as ContactForm, ContactHero, and StatusMessage, are created to structure the contact page visually and functionally. The navigation is updated to include a link to the contact page, enhancing overall accessibility within the application.

Changes

File Path Change Summary
libs/website/feature/contactpage/constants/types/contact.constants.ts Added constant DEFAULT_FORM_DATA of type FormData, initialized with empty string properties for form fields.
libs/website/feature/contactpage/constants/types/contact.types.ts Introduced interfaces FormData (required fields) and FormErrors (optional fields) for form handling.
libs/website/feature/contactpage/contactFormValidation.ts Added interfaces FormData and FormErrors, and function validateForm for validating contact form submissions.
libs/website/feature/contactpage/index.tsx Created ContactPage component that manages form submission status and renders the contact form.
libs/website/feature/contactpage/ui/ContactForm.tsx Introduced ContactForm component for user input, managing state and validation, and handling form submission.
libs/website/feature/contactpage/ui/ContactHero.tsx Added ContactHero component displaying a header and encouragement message for users.
libs/website/feature/contactpage/ui/StatusMessage.tsx Created StatusMessage component to display success or error messages based on submission status.
libs/website/pages/home.tsx Imported and rendered ContactPage component within the Home function layout.
libs/website/shared/ui/navigation/navbar/constants/navbar.constants.ts Added new link { name: 'CONTACT US', link: '/#contactpage' } to the links array in the navbar constants.

Poem

🐰 In the meadow where forms do play,
A new contact page brightens the day.
With fields to fill and messages sent,
Hop on over, it’s time well spent!
Success or error, we’ll let you know,
Just a click away, let your queries flow! 🌼


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?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 level

While 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 accessibility

Consider 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 internationalization

The 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 breakpoints

The 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:

  1. Rate limiting to prevent spam
  2. CSRF tokens to prevent cross-site request forgery
  3. Server-side validation to match client-side rules
libs/website/feature/contactpage/index.tsx (1)

12-20: Extract timeout duration as a constant

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 00c4838 and 2aec2ac.

📒 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.

Comment on lines +17 to +19
<span className="text-xl">
{type === 'success' ? '✅' : '❌'}
</span>
Copy link

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.

Comment on lines +22 to +26
<p className="font-medium">
{type === 'success'
? 'Message sent successfully!'
: 'Failed to send message. Please try again.'}
</p>
Copy link

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:

  1. Move message strings to a constants file for better maintainability and future internationalization
  2. 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.

Comment on lines +18 to +45
/* 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'
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add consistent input sanitization and validation.

Several improvements needed for the validation logic:

  1. Add maximum length validation for all fields
  2. Extract magic numbers to named constants
  3. Add input sanitization to prevent XSS attacks
  4. 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.

Comment on lines +29 to +31
else if (!/^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
Copy link

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:

  1. Using a more robust email regex pattern
  2. 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.

libs/website/feature/contactpage/index.tsx Show resolved Hide resolved
Comment on lines +29 to +60
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>
Copy link

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.

Suggested change
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>

Comment on lines +66 to +127
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>
)
Copy link

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.

Comment on lines +39 to +64
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)
}
}
Copy link

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:

  1. The timeout duration should be a constant
  2. Consider implementing rate limiting
  3. 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.

@anakafeel anakafeel changed the title Saimhashmi/feat(website)/105 create contact us page feat(website)/105 create contact us page Dec 8, 2024
@anakafeel anakafeel changed the title feat(website)/105 create contact us page feat(website): #105 create contact us page Dec 8, 2024
@JeremyFriesenGitHub JeremyFriesenGitHub changed the title feat(website): #105 create contact us page feat(website): create contact us page Dec 8, 2024
@JeremyFriesenGitHub JeremyFriesenGitHub added 📢help wanted Extra attention is needed 👩‍💻good first issue Good for newcomers frontend labels Dec 8, 2024
@JeremyFriesenGitHub JeremyFriesenGitHub added this to the Website refactor milestone Dec 8, 2024
@JeremyFriesenGitHub JeremyFriesenGitHub linked an issue Dec 8, 2024 that may be closed by this pull request
@JeremyFriesenGitHub JeremyFriesenGitHub removed the request for review from HasithDeAlwis December 8, 2024 17:58
@JeremyFriesenGitHub JeremyFriesenGitHub marked this pull request as draft December 8, 2024 18:01
Copy link
Contributor

@JeremyFriesenGitHub JeremyFriesenGitHub left a 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

@JeremyFriesenGitHub JeremyFriesenGitHub force-pushed the saimhashmi/feat(website)/105-create-contact-us-page branch from 2efe6ec to 552f6f9 Compare December 8, 2024 19:13
This commit is to be removed when merging into main.
@JeremyFriesenGitHub JeremyFriesenGitHub force-pushed the saimhashmi/feat(website)/105-create-contact-us-page branch from 552f6f9 to bb4368e Compare December 8, 2024 19:13
Copy link

@coderabbitai coderabbitai bot left a 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 rules

The 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 types

The 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 targets

Each 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 and hybrid targets as needed.

Also applies to: 16-19, 22-25

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between b2d72fd and bb4368e.

📒 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:

  1. For CI: Uncomment and configure the webServer section
  2. 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:

  1. A build target to ensure TypeScript compilation
  2. A test target for running tests
  3. 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:

Comment on lines +39 to +44
projects: [
{
name: 'chromium (desktop)',
use: { ...devices['Desktop Chrome'] },
},
],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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)'] },
},
],

Comment on lines +5 to +9
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()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

⚠️ Potential issue

Method implementation needs to be fixed in both caller and class definition

The verification confirms the review comment's concerns and reveals additional issues:

  1. In pom.ts, the fillContactPage method is incorrectly defined to take formLayout as its first parameter while using it to access form elements
  2. The implementation in online.ts matches this incorrect pattern by passing formLayout 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:

  1. The fillContactPage call appears to incorrectly pass formLayout as a parameter when it's likely the this context
  2. Missing error handling for potential failures during navigation or submission
  3. 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

Comment on lines +1 to +38
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)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

⚠️ SECURITY ALERT: Potential DoS Attack Tool Detected

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:

  1. Reject this PR
  2. Review recent form submissions for potential attacks
  3. Implement rate limiting and CAPTCHA protection on the contact form
  4. Consider blocking the contributor if malicious intent is confirmed

Comment on lines +7 to +12
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!',
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

⚠️ SECURITY ALERT: Malicious Payload Detected

The code contains extremely large malicious payloads designed to:

  1. Overflow database fields
  2. Consume server resources
  3. 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.

Comment on lines +14 to +20
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)
}
})()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

⚠️ SECURITY ALERT: Automated Attack Script Detected

This code implements a DoS attack by:

  1. Launching an automated browser
  2. Submitting the malicious payload 100 times in a loop
  3. 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.

@HasithDeAlwis HasithDeAlwis removed their assignment Dec 8, 2024
@anakafeel
Copy link
Author

Hey @JeremyFriesenGitHub

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:

  1. Commit Messages: I checked the contribution guidelines, but I’m unsure how to improve my commit messages. What exactly are you looking for? Please do let me know.

  2. Input Validation: Can you clarify what specific improvements you had in mind for the input validation? Any examples would be super helpful.

  3. Tests: What type of tests should I add? Should I go with Jest or Vitest (the only two I'm familiar with for now), or do you have a preferred library for this?

  4. Spam Handling: Apart from using validator.js, do you have any other suggestions for better handling spam submissions?

Thanks again for your help! Looking forward to your insights so I can make these changes.

@HasithDeAlwis HasithDeAlwis removed the 👩‍💻good first issue Good for newcomers label Dec 9, 2024
@HasithDeAlwis
Copy link
Collaborator

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 :)
Join cuHacking's community discord, and ping me there (my name is just Hasith on the discord)

Copy link
Collaborator

@HasithDeAlwis HasithDeAlwis left a 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)) {
Copy link
Collaborator

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 */
Copy link
Collaborator

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 */}
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📢help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat(website): create contact us page
4 participants