People must verify their email to send messages

This commit is contained in:
MartinBraquet
2026-02-12 17:14:40 +01:00
parent 5d9a1c1bf8
commit f56373fd73
8 changed files with 109 additions and 49 deletions

View File

@@ -1,13 +1,24 @@
import { APIError, APIHandler } from 'api/helpers/endpoint'
import { filterDefined } from 'common/util/array'
import { uniq } from 'lodash'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { addUsersToPrivateMessageChannel } from 'api/helpers/private-messages'
import { getPrivateUser, getUser } from 'shared/utils'
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {filterDefined} from 'common/util/array'
import {uniq} from 'lodash'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {addUsersToPrivateMessageChannel} from 'api/helpers/private-messages'
import {getPrivateUser, getUser} from 'shared/utils'
import * as admin from 'firebase-admin'
export const createPrivateUserMessageChannel: APIHandler<
'create-private-user-message-channel'
> = async (body, auth) => {
// Do not use auth.creds.data as its info can be staled. It comes from a client token, which refreshes hourly or so
const user = await admin.auth().getUser(auth.uid)
// console.log(JSON.stringify(user, null, 2))
if (!user?.emailVerified) {
throw new APIError(
403,
'You must verify your email to contact people.'
)
}
const userIds = uniq(body.userIds.concat(auth.uid))
const pg = createSupabaseDirectClient()
@@ -41,11 +52,12 @@ export const createPrivateUserMessageChannel: APIHandler<
const currentChannel = await pg.oneOrNone(
`
select channel_id from private_user_message_channel_members
group by channel_id
having array_agg(user_id::text) @> array[$1]::text[]
and array_agg(user_id::text) <@ array[$1]::text[]
`,
select channel_id
from private_user_message_channel_members
group by channel_id
having array_agg(user_id::text) @> array [$1]::text[]
and array_agg(user_id::text) <@ array [$1]::text[]
`,
[userIds]
)
if (currentChannel)
@@ -55,17 +67,19 @@ export const createPrivateUserMessageChannel: APIHandler<
}
const channel = await pg.one(
`insert into private_user_message_channels default values returning id`
`insert into private_user_message_channels default
values
returning id`
)
await pg.none(
`insert into private_user_message_channel_members (channel_id, user_id, role, status)
values ($1, $2, 'creator', 'joined')
`,
values ($1, $2, 'creator', 'joined')
`,
[channel.id, creatorId]
)
const memberIds = userIds.filter((id) => id !== creatorId)
await addUsersToPrivateMessageChannel(memberIds, channel.id, pg)
return { status: 'success', channelId: Number(channel.id) }
return {status: 'success', channelId: Number(channel.id)}
}

View File

@@ -0,0 +1,26 @@
import {Button} from 'web/components/buttons/button'
import {Col} from 'web/components/layout/col'
import {sendVerificationEmail} from 'web/lib/firebase/email-verification'
import clsx from 'clsx'
interface EmailVerificationPromptProps {
firebaseUser: any
t: any
className?: string
}
export function EmailVerificationPrompt(
{
firebaseUser,
t,
className
}: EmailVerificationPromptProps) {
return (
<Col className={clsx('gap-4 max-w-xl', className)}>
<h3>{t('messaging.email_verification_required', "You must verify your email to message people.")}</h3>
<Button color={'gray-outline'} onClick={() => sendVerificationEmail(firebaseUser!, t)} disabled={false}>
{t('settings.email.send_verification', 'Send verification email')}
</Button>
</Col>
)
}

View File

@@ -15,9 +15,10 @@ import {MAX_COMMENT_LENGTH} from 'common/comment'
import {CommentInputTextArea} from 'web/components/comments/comment-input'
import {Title} from 'web/components/widgets/title'
import {Row} from 'web/components/layout/row'
import {firebaseLogin} from 'web/lib/firebase/users'
import {auth, firebaseLogin} from 'web/lib/firebase/users'
import {useT} from 'web/lib/locale'
import {Tooltip} from "web/components/widgets/tooltip";
import {EmailVerificationPrompt} from 'web/components/messaging/email-verification-prompt'
export const SendMessageButton = (props: {
toUser: User
@@ -25,11 +26,12 @@ export const SendMessageButton = (props: {
includeLabel?: boolean
circleButton?: boolean
}) => {
const { toUser, currentUser, includeLabel, circleButton } = props
const {toUser, currentUser, includeLabel, circleButton} = props
const firebaseUser = auth.currentUser
const router = useRouter()
const privateUser = usePrivateUser()
const channelMemberships = useSortedPrivateMessageMemberships(currentUser?.id)
const { memberIdsByChannelId } = channelMemberships
const {memberIdsByChannelId} = channelMemberships
const t = useT()
const [openComposeModal, setOpenComposeModal] = useState(false)
@@ -123,13 +125,15 @@ export const SendMessageButton = (props: {
<Row className={'w-full'}>
<Title className={'!mb-2'}>{t('send_message.title', 'Message')} {toUser.name}</Title>
</Row>
<CommentInputTextArea
editor={editor}
user={currentUser}
submit={sendMessage}
isSubmitting={!editor || submitting}
submitOnEnter={false}
/>
{firebaseUser?.emailVerified ? <CommentInputTextArea
editor={editor}
user={currentUser}
submit={sendMessage}
isSubmitting={!editor || submitting}
submitOnEnter={false}
/> :
<EmailVerificationPrompt firebaseUser={firebaseUser} t={t} className='max-w-xl'/>
}
<span className={'text-red-500'}>{error}</span>
</Col>
</Modal>

View File

@@ -0,0 +1,26 @@
import toast from "react-hot-toast";
import {sendEmailVerification, User} from "firebase/auth";
export const sendVerificationEmail = async (
user: User,
t: any
) => {
// if (!privateUser?.email) {
// toast.error(t('settings.email.no_email', 'No email found on your account.'))
// return
// }
if (!user) {
toast.error(t('settings.email.must_sign_in', 'You must be signed in to send a verification email.'))
return
}
toast
.promise(sendEmailVerification(user), {
loading: t('settings.email.sending', 'Sending verification email...'),
success: t('settings.email.verification_sent', 'Verification email sent — check your inbox and spam.'),
error: t('settings.email.verification_failed', 'Failed to send verification email.'),
})
.catch(() => {
console.error("Failed to send verification email")
})
}

View File

@@ -822,6 +822,7 @@
"settings.email.verification_failed": "Senden der Bestätigungs-E-Mail fehlgeschlagen.",
"settings.email.verification_sent": "Bestätigungs-E-Mail gesendet prüfen Sie Ihren Posteingang und Spam-Ordner.",
"settings.email.verified": "E-Mail bestätigt ✔️",
"messaging.email_verification_required": "Sie müssen Ihre E-Mail überprüfen, um Personen zu schreiben.",
"settings.general.account": "Konto",
"settings.general.email": "E-Mail",
"settings.general.language": "Sprache",

View File

@@ -822,6 +822,7 @@
"settings.email.verification_failed": "Échec de l'envoi de l'e-mail de vérification.",
"settings.email.verification_sent": "E-mail de vérification envoyé — vérifiez votre boîte de réception et les spams.",
"settings.email.verified": "E-mail vérifié ✔️",
"messaging.email_verification_required": "Vous devez vérifier votre e-mail pour pouvoir envoyer des messages.",
"settings.general.account": "Compte",
"settings.general.email": "E-mail",
"settings.general.language": "Langue",

View File

@@ -22,12 +22,15 @@ import {BannedBadge} from 'web/components/widgets/user-link'
import {PrivateMessageChannel} from 'common/supabase/private-messages'
import {SEO} from "web/components/SEO";
import {useT} from 'web/lib/locale'
import {auth} from "web/lib/firebase/users";
import {EmailVerificationPrompt} from "web/components/messaging/email-verification-prompt";
export default function MessagesPage() {
useRedirectIfSignedOut()
const currentUser = useUser()
const firebaseUser = auth.currentUser
const t = useT()
return (
<PageBase trackPageView={'messages page'} className={'p-2'}>
@@ -36,7 +39,11 @@ export default function MessagesPage() {
description={t('messages.seo.description', 'Your Messages')}
url={`/messages`}
/>
{currentUser && <MessagesContent currentUser={currentUser}/>}
{currentUser && (
firebaseUser?.emailVerified ?
<MessagesContent currentUser={currentUser}/>
: <EmailVerificationPrompt firebaseUser={firebaseUser} t={t}/>
)}
</PageBase>
)
}

View File

@@ -12,7 +12,7 @@ import {useRedirectIfSignedOut} from "web/hooks/use-redirect-if-signed-out";
import {deleteAccount} from "web/lib/util/delete";
import router from "next/router";
import {Button} from "web/components/buttons/button";
import {sendEmailVerification, updateEmail} from 'firebase/auth';
import {updateEmail} from 'firebase/auth';
import {auth} from "web/lib/firebase/users";
import {NotificationSettings} from "web/components/notifications";
import ThemeIcon from "web/components/theme-icon";
@@ -22,6 +22,7 @@ import {AboutSettings} from "web/components/about-settings";
import {LanguagePicker} from "web/components/language/language-picker";
import {useT} from "web/lib/locale";
import HiddenProfilesModal from 'web/components/settings/hidden-profiles-modal'
import {sendVerificationEmail} from "web/lib/firebase/email-verification";
export default function NotificationsPage() {
const t = useT()
@@ -107,27 +108,6 @@ const LoadedGeneralSettings = (props: {
changeUserEmail(data.newEmail)
}
const sendVerificationEmail = async () => {
if (!privateUser?.email) {
toast.error(t('settings.email.no_email', 'No email found on your account.'))
return
}
if (!user) {
toast.error(t('settings.email.must_sign_in', 'You must be signed in to send a verification email.'))
return
}
toast
.promise(sendEmailVerification(user), {
loading: t('settings.email.sending', 'Sending verification email...'),
success: t('settings.email.verification_sent', 'Verification email sent — check your inbox and spam.'),
error: t('settings.email.verification_failed', 'Failed to send verification email.'),
})
.catch(() => {
console.log("Failed to send verification email")
})
}
const isEmailVerified = user.emailVerified
return <>
@@ -147,7 +127,8 @@ const LoadedGeneralSettings = (props: {
<h3>{t('settings.general.account', 'Account')}</h3>
<h5>{t('settings.general.email', 'Email')}</h5>
<Button color={'gray-outline'} onClick={sendVerificationEmail} disabled={!privateUser?.email || isEmailVerified}>
<Button color={'gray-outline'} onClick={() => sendVerificationEmail(user, t)}
disabled={!privateUser?.email || isEmailVerified}>
{isEmailVerified ? t('settings.email.verified', 'Email Verified ✔️') : t('settings.email.send_verification', 'Send verification email')}
</Button>