Chore/spam page (#29854)

* Add spam page

* Add select

* Add experimental flag in next.config to see if it resolves the build issue

* Add slack webhook call

* updated endpoint

* revert

* update

* fix placeholder styling in Select

* clean

* Update [ref].tsx

* Update select.tsx

* revert

* protect form and endpoint from bad ref abuse

* Delete spam api route

* Add api route back

* Basic api route

* Rename

* rename route

* Add as route handler

* Stub in supabase update

* Add the proper supabase project

* Rename spam to opt-out

* Swap anon for service role key

* Rename env var

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2024-11-13 14:09:24 -03:30
1 parent 12b6389090
commit f7292dce8c
5 files changed
+271 -2

No files matched your search

@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_EMAIL_ABUSE_URL as string
const supabaseServiceKey = process.env.EMAIL_ABUSE_SERVICE_KEY as string
export async function POST(req: NextRequest, { params }: { params: { ref: string } }) {
const ref = params.ref
const { reason, email } = await req.json()
const supabase = createClient(supabaseUrl, supabaseServiceKey)
if (!ref) {
return NextResponse.json(
{ error: 'Bad Request: Missing or invalid project reference.' },
{ status: 400 }
)
}
const refPattern = /^[a-zA-Z]{20}$/
const refIsInvalid = !refPattern.test(ref)
if (refIsInvalid) {
return NextResponse.json(
{ error: 'Bad Request: Missing or invalid project reference.' },
{ status: 400 }
)
}
try {
const { error: supabaseError } = await supabase
.from('manual_reports')
.insert([{ project_ref: ref, reason, email }])
if (supabaseError) throw new Error(`Supabase error: ${supabaseError.message}`)
const response = await fetch(process.env.EMAIL_REPORT_SLACK_WEBHOOK as string, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: `New report from: ${ref} \n\n ${reason}` }),
})
if (!response.ok) throw new Error('Failed to send to Slack')
return NextResponse.json(
{ message: 'Thank you! We have received your report.' },
{ status: 200 }
)
} catch (error) {
const errorMessage = (error as Error).message
return NextResponse.json(
{ error: `Failure: Could not send post to Slack. Error: ${errorMessage}` },
{ status: 500 }
)
}
}
+196
View File
@@ -0,0 +1,196 @@
import { zodResolver } from '@hookform/resolvers/zod'
import Layout from 'components/Layouts/Default'
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import {
Button,
cn,
Form_Shadcn_,
FormField_Shadcn_,
FormMessage_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { z } from 'zod'
const formSchema = z.object({
ref: z.string(),
reason: z.string().optional(),
})
export default function OptOutPage() {
const router = useRouter()
const ref = router.query.ref as string
const email = router.query.email as string | undefined
const [formMessage, setFormMessage] = useState<string | null>(null)
const [submissionType, setSubmissionType] = useState<'success' | 'error' | null>(null)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
ref: ref,
reason: undefined,
},
})
const { register } = form
// Ensure ref is a string
const refString = typeof ref !== 'string'
const refPattern = /^[a-zA-Z]{20}$/
const refIsInvalid = !refString && !refPattern.test(ref)
const SelectItemContainerClasses = 'flex flex-col text-sm items-start'
const onSubmit = async (data: z.infer<typeof formSchema>) => {
try {
setSubmissionType(null)
setFormMessage('Submitting...')
const response = await fetch(`/api-v2/opt-out/${data.ref}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ reason: data.reason, email: email }),
})
const message = await response.text()
if (refIsInvalid) {
setFormMessage('Error: Invalid project reference.')
setSubmissionType('error')
} else if (response.ok) {
setFormMessage(message)
setSubmissionType('success')
} else {
console.error('Form submission failed:', message)
setFormMessage('Error: Form submission failed.')
setSubmissionType('error')
}
} catch (error) {
console.error('Error submitting form:', error)
setFormMessage('Error: An unexpected error occurred.')
setSubmissionType('error')
}
}
function OptOutLayout({ children }: { children: React.ReactNode }) {
return (
<>
<NextSeo title="Supabase | Opt Out" description="Opt out of receiving emails." />
<Layout className="overflow-visible mx-auto max-w-xl mt-24 flex flex-col gap-8">
{children}
</Layout>
</>
)
}
useEffect(() => {
form.reset({ ref: ref, reason: '' })
}, [ref])
return (
<>
<OptOutLayout>
<div>
<span className="label">Opt out</span>
<h2 className="h4 !m-0">Receive an unexpected email from Supabase?</h2>
<p className="text-foreground-light">Please report it here so we can investigate.</p>
</div>
{refIsInvalid && (
<Admonition type="warning" title="Invalid project reference">
<p>The project reference you entered is invalid.</p>
<p>Please check the URL and try again.</p>
</Admonition>
)}
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="gap-4 max-w-xs flex flex-col">
<input hidden value={form.getValues('ref')} readOnly {...register('ref')} />
<FormField_Shadcn_
name="reason"
control={form.control}
render={({ field }) => (
<FormItemLayout label="Choose reason for reporting" labelOptional="Optional">
<Select_Shadcn_ onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger_Shadcn_ size="medium">
<SelectValue_Shadcn_ placeholder="Select a reason (optional)">
{field.value}
</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
<SelectItem_Shadcn_ value="phishing">
<div className={SelectItemContainerClasses}>
<span>Phishing</span>
<span className="text-foreground-light text-xs">
Attempt to obtain sensitive information
</span>
</div>
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="advertisement">
<div className={SelectItemContainerClasses}>
<span>Advertisement</span>
<span className="text-foreground-light text-xs">
Unwanted promotional content
</span>
</div>
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="malware">
<div className={SelectItemContainerClasses}>
<span>Malware</span>
<span className="text-foreground-light text-xs">
Contains harmful software
</span>
</div>
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="scam">
<div className={SelectItemContainerClasses}>
<span>Scam</span>
<span className="text-foreground-light text-xs">
Fraudulent or deceptive content
</span>
</div>
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="other">
<div className={SelectItemContainerClasses}>
<span>Other</span>
<span className="text-foreground-light text-xs">
Any other type of spam
</span>
</div>
</SelectItem_Shadcn_>
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
)}
/>
<Button htmlType="submit" size="small" disabled={submissionType === 'success'}>
Report spam
</Button>
<FormMessage_Shadcn_
className={cn(
'border-l pl-3',
submissionType === 'error'
? 'border-destructive'
: 'border-foreground-muted text-foreground'
)}
>
{formMessage}
</FormMessage_Shadcn_>
</form>
</Form_Shadcn_>
</OptOutLayout>
</>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
const OptOutIndex = () => {
const router = useRouter()
// We don't want anyone landing here directly
useEffect(() => {
router.push('/')
}, [router])
return null
}
export default OptOutIndex
@@ -47,7 +47,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex w-full items-center justify-between rounded-md border border-strong hover:border-stronger bg-alternative dark:bg-muted hover:bg-selection text-xs ring-offset-background-control placeholder:text-foreground-muted focus:outline-none ring-border-control focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200',
'flex w-full items-center justify-between rounded-md border border-strong hover:border-stronger bg-alternative dark:bg-muted hover:bg-selection text-xs ring-offset-background-control data-[placeholder]:text-foreground-lighter focus:outline-none ring-border-control focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200',
'data-[state=open]:bg-selection data-[state=open]:border-stronger',
SelectTriggerVariants({ size }),
className
+3 -1
View File
@@ -70,7 +70,9 @@
"LIVE_SUPABASE_COM_SERVICE_ROLE_KEY",
"VERCEL_URL",
"HUBSPOT_PORTAL_ID",
"HUBSPOT_ENTERPRISE_FORM_GUID"
"HUBSPOT_ENTERPRISE_FORM_GUID",
"EMAIL_REPORT_SLACK_WEBHOOK",
"EMAIL_ABUSE_SERVICE_KEY"
],
"outputs": [".next/**", "!.next/cache/**", ".contentlayer/**"]
},