From f56373fd73bd83821e429a4dbc1ef1dc3efb0ba9 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Thu, 12 Feb 2026 17:14:40 +0100 Subject: [PATCH] People must verify their email to send messages --- .../create-private-user-message-channel.ts | 44 ++++++++++++------- .../messaging/email-verification-prompt.tsx | 26 +++++++++++ .../messaging/send-message-button.tsx | 24 +++++----- web/lib/firebase/email-verification.ts | 26 +++++++++++ web/messages/de.json | 1 + web/messages/fr.json | 1 + web/pages/messages/index.tsx | 9 +++- web/pages/settings.tsx | 27 ++---------- 8 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 web/components/messaging/email-verification-prompt.tsx create mode 100644 web/lib/firebase/email-verification.ts diff --git a/backend/api/src/create-private-user-message-channel.ts b/backend/api/src/create-private-user-message-channel.ts index a8dc6e0..fe0fd92 100644 --- a/backend/api/src/create-private-user-message-channel.ts +++ b/backend/api/src/create-private-user-message-channel.ts @@ -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)} } diff --git a/web/components/messaging/email-verification-prompt.tsx b/web/components/messaging/email-verification-prompt.tsx new file mode 100644 index 0000000..e28b7df --- /dev/null +++ b/web/components/messaging/email-verification-prompt.tsx @@ -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 ( + +

{t('messaging.email_verification_required', "You must verify your email to message people.")}

+ + + ) +} diff --git a/web/components/messaging/send-message-button.tsx b/web/components/messaging/send-message-button.tsx index 52a5626..1c2bd50 100644 --- a/web/components/messaging/send-message-button.tsx +++ b/web/components/messaging/send-message-button.tsx @@ -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: { {t('send_message.title', 'Message')} {toUser.name} - + {firebaseUser?.emailVerified ? : + + } {error} diff --git a/web/lib/firebase/email-verification.ts b/web/lib/firebase/email-verification.ts new file mode 100644 index 0000000..284fedd --- /dev/null +++ b/web/lib/firebase/email-verification.ts @@ -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") + }) +} \ No newline at end of file diff --git a/web/messages/de.json b/web/messages/de.json index 9da99a3..6b4e03f 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -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", diff --git a/web/messages/fr.json b/web/messages/fr.json index 039fbbf..438d0e7 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -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", diff --git a/web/pages/messages/index.tsx b/web/pages/messages/index.tsx index f1d6a3c..5101beb 100644 --- a/web/pages/messages/index.tsx +++ b/web/pages/messages/index.tsx @@ -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 ( @@ -36,7 +39,11 @@ export default function MessagesPage() { description={t('messages.seo.description', 'Your Messages')} url={`/messages`} /> - {currentUser && } + {currentUser && ( + firebaseUser?.emailVerified ? + + : + )} ) } diff --git a/web/pages/settings.tsx b/web/pages/settings.tsx index ecc7aa8..f4b3d12 100644 --- a/web/pages/settings.tsx +++ b/web/pages/settings.tsx @@ -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: {

{t('settings.general.account', 'Account')}

{t('settings.general.email', 'Email')}
-