Require explicit public contact confirmation

This commit is contained in:
SimpleTest 2026-08-26 04:50:16 +03:00
parent c91ce502cb
commit d94c89e844
15 changed files with 446 additions and 56 deletions

View File

@ -25,6 +25,7 @@ import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/who_need_help" import {hooks as colocatedHooks} from "phoenix-colocated/who_need_help"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
import {Hooks, mountStaticAidMaps} from "./hooks" import {Hooks, mountStaticAidMaps} from "./hooks"
import {isProtectedFragmentTokenForForm} from "./protected_token_fragment.mjs"
const systemTheme = () => const systemTheme = () =>
matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
@ -58,13 +59,6 @@ matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const activateProtectedTokenFragment = () => { const activateProtectedTokenFragment = () => {
if (!window.location.hash.startsWith("#token=")) return
const token = new URLSearchParams(window.location.hash.slice(1)).get("token")
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}`)
if (!token || !/^[A-Za-z0-9_-]{43}$/.test(token)) return
const candidates = [ const candidates = [
{ {
form: document.getElementById("magic-link-fragment-form"), form: document.getElementById("magic-link-fragment-form"),
@ -77,11 +71,13 @@ const activateProtectedTokenFragment = () => {
}, },
{ {
form: document.getElementById("support-confirmation-fragment-form"), form: document.getElementById("support-confirmation-fragment-form"),
input: document.getElementById("support-confirmation-fragment-token") input: document.getElementById("support-confirmation-fragment-token"),
invalidMessage: document.getElementById("support-confirmation-fragment-invalid")
}, },
{ {
form: document.getElementById("content-removal-confirmation-fragment-form"), form: document.getElementById("content-removal-confirmation-fragment-form"),
input: document.getElementById("content-removal-confirmation-fragment-token") input: document.getElementById("content-removal-confirmation-fragment-token"),
invalidMessage: document.getElementById("content-removal-confirmation-fragment-invalid")
} }
] ]
@ -91,8 +87,26 @@ const activateProtectedTokenFragment = () => {
if (!candidate) return if (!candidate) return
const hasTokenFragment = window.location.hash.startsWith("#token=")
const token = hasTokenFragment
? new URLSearchParams(window.location.hash.slice(1)).get("token")
: null
if (hasTokenFragment) {
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}`)
}
if (!isProtectedFragmentTokenForForm(candidate.form.id, token)) {
if (candidate.invalidMessage instanceof HTMLElement) {
candidate.invalidMessage.hidden = false
}
return
}
candidate.input.value = token candidate.input.value = token
candidate.form.hidden = false candidate.form.hidden = false
if (candidate.invalidMessage instanceof HTMLElement) candidate.invalidMessage.hidden = true
if (candidate.options instanceof HTMLElement) candidate.options.hidden = true if (candidate.options instanceof HTMLElement) candidate.options.hidden = true
candidate.form.querySelector("button")?.focus() candidate.form.querySelector("button")?.focus()
} }

View File

@ -0,0 +1,26 @@
const rawTokenPattern = /^[A-Za-z0-9_-]{43}$/
const phoenixSignedTokenPattern = /^SFMyNTY\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/
const phoenixSignedTokenForms = new Set([
"support-confirmation-fragment-form",
"content-removal-confirmation-fragment-form"
])
const rawTokenForms = new Set([
"magic-link-fragment-form",
"email-change-fragment-form"
])
export const isProtectedFragmentTokenForForm = (formID, token) => {
if (typeof token !== "string") return false
if (phoenixSignedTokenForms.has(formID)) {
return phoenixSignedTokenPattern.test(token)
}
if (rawTokenForms.has(formID)) {
return rawTokenPattern.test(token)
}
return false
}

View File

@ -0,0 +1,66 @@
import assert from "node:assert/strict"
import test from "node:test"
import {isProtectedFragmentTokenForForm} from "./protected_token_fragment.mjs"
const rawToken = "a".repeat(43)
const signedToken = `SFMyNTY.${"b".repeat(64)}.${"c".repeat(43)}`
test("raw account tokens remain limited to one 43-character segment", () => {
assert.equal(
isProtectedFragmentTokenForForm("magic-link-fragment-form", rawToken),
true
)
assert.equal(
isProtectedFragmentTokenForForm("email-change-fragment-form", rawToken),
true
)
assert.equal(
isProtectedFragmentTokenForForm("magic-link-fragment-form", signedToken),
false
)
})
test("support and removal forms accept Phoenix SHA-256 signed tokens", () => {
assert.equal(
isProtectedFragmentTokenForForm("support-confirmation-fragment-form", signedToken),
true
)
assert.equal(
isProtectedFragmentTokenForForm(
"content-removal-confirmation-fragment-form",
signedToken
),
true
)
assert.equal(
isProtectedFragmentTokenForForm("support-confirmation-fragment-form", rawToken),
false
)
})
test("malformed signed tokens are rejected", () => {
const candidates = [
`SFMyNTY.${"b".repeat(64)}.${"c".repeat(42)}`,
`SFMyNTY.${"b".repeat(64)}.${"c".repeat(44)}`,
`SFMyNTY.${"b".repeat(64)}.${"c".repeat(42)}+`,
`SFMyNTY..${"c".repeat(43)}`,
`SFMyNTY.${"b".repeat(64)}.${"c".repeat(43)}?extra=1`,
`token=${signedToken}`
]
for (const candidate of candidates) {
assert.equal(
isProtectedFragmentTokenForForm(
"support-confirmation-fragment-form",
candidate
),
false
)
}
})
test("unknown forms do not inherit a token format", () => {
assert.equal(isProtectedFragmentTokenForForm("unknown-form", rawToken), false)
assert.equal(isProtectedFragmentTokenForForm("unknown-form", signedToken), false)
})

View File

@ -73,6 +73,25 @@ volume, temporary image, or quality state. These changes are recorded in local
commits only; public Git, the frozen hackathon test deployment, shared Caddy, commits only; public Git, the frozen hackathon test deployment, shared Caddy,
and production were not modified by this quality rerun. and production were not modified by this quality rerun.
The subsequent public-contact hardening candidate passed an initial complete
isolated quality pipeline with 491 ExUnit tests and 26 JavaScript asset tests. A
later focused correction made token formats explicit per form and added a
visible invalid-link state when the protected fragment is missing. The final
complete quality rerun passed 491 ExUnit and 27 JavaScript asset tests, every
configured quality/security gate, and the production-image scan with zero
detected vulnerabilities. Its isolated unit completed in 4 minutes 25.214
seconds with a measured 242.6 MiB memory peak and zero swap. The focused
support/legal browser replay passed all
15 checks across Chromium, Firefox, and WebKit: missing fragments produced an
actionable error without flashing on valid links; anonymous support and
content-removal submissions both used query-free signed fragments, required a
visible POST confirmation, and opened the corresponding private case; and the
authenticated staff-queue scenario passed. The final browser unit measured a
58.8 MiB memory peak and zero swap. Exact cleanup left no run-owned container,
network, volume, or temporary image. The candidate remains local only because
production still has unexpired confirmation messages generated with the
preceding URL contract.
## 2. Verify production configuration without exposing secrets ## 2. Verify production configuration without exposing secrets
Run both checks against the single ignored production `.env`. The first reports Run both checks against the single ignored production `.env`. The first reports

View File

@ -3,6 +3,57 @@
Observed through 2026-08-26 in the local workspace. This report separates observed Observed through 2026-08-26 in the local workspace. This report separates observed
results from product limits and unknown production properties. results from product limits and unknown production properties.
## Protected support and content-removal email confirmation on 2026-08-26
- Browser inspection found that the shared fragment bootstrap accepted only raw
43-character account tokens, while anonymous support and content-removal
confirmation messages use a Phoenix signed token. The browser therefore hid
the explicit confirmation form even when the email contained a valid fragment.
The candidate now validates raw account tokens and Phoenix signed public-contact
tokens against separate, form-specific contracts.
- The anonymous support and content-removal browser scenarios submitted each
public form, obtained its generated message from the isolated Mailpit instance,
verified that the action URL contained no query token and did contain a
`#token=SFMyNTY...` fragment, confirmed through the visible POST form, and
opened the corresponding private case. Together with the authenticated staff
queue scenario, the initial support/legal file passed in Chromium, Firefox,
and WebKit: nine checks in total. A later focused correction rejected token
formats for unknown forms and added an explicit invalid-link state for a
missing protected fragment. The final browser replay passed all 15 checks in
the same three engines, including verifying that a valid fragment does not
flash the invalid state. The JavaScript asset suite separately passed all 27
tests. The final browser unit
`codex-heavy-e2e-public-contact-no-flash-20260826-051539-1512049.service`
measured a 58.8 MiB memory peak and zero swap. Exact cleanup found no
container, network, volume, or temporary image owned by run
`20260826021539-1512057`.
- Public support and content-removal access now requires an already verified
contact. A GET carrying an older access token no longer verifies or mutates an
unverified record; focused context tests cover both record types.
- The complete isolated quality unit
`codex-heavy-quality-protected-email-links-20260826-044358-690415.service`
exited successfully after 4 minutes 9.899 seconds with a measured 312.6 MiB
memory peak and no swap. ExUnit reported 491 passing tests; all configured
quality and security gates passed; and the final production-image scan reported
zero detected vulnerabilities. Exact cleanup left no run-owned container,
network, volume, or temporary image.
- After the missing-fragment UX and explicit per-form token contracts were
added, the complete isolated quality unit
`codex-heavy-wnh-public-contact-final-quality-r2-20260826-052517-1775390.service`
passed 491 ExUnit tests, 27 JavaScript asset tests, every configured
quality/security gate, release and recovery drills, and all image scans. The
final production-image scan reported zero detected vulnerabilities. The unit
completed in 4 minutes 25.214 seconds with a measured 242.6 MiB memory peak
and zero swap. Exact cleanup found no Compose container, network, volume,
temporary image, or quality state owned by run
`20260826022517-1775400`.
- A read-only production count observed 30 unverified support records and no
content-removal notices. Their current confirmation messages use the older
query-token link format and remain valid for up to 24 hours. The newest observed
support record was created at 2026-08-26 00:06 UTC. The candidate was therefore
not deployed: production, the frozen hackathon test, shared Caddy, Google Play,
and public Git were not changed by this verification.
## Local candidate and operational recheck on 2026-08-26 ## Local candidate and operational recheck on 2026-08-26
- The uncommitted production Web Push verifier hardening and service-worker - The uncommitted production Web Push verifier hardening and service-worker

View File

@ -5,8 +5,10 @@ import {
latestMessageID, latestMessageID,
loginWithMagicLink, loginWithMagicLink,
loginWithPassword, loginWithPassword,
newIsolatedContext,
projectEmail, projectEmail,
registerAndConfirm, registerAndConfirm,
waitForApplicationEmailLink,
} from "./helpers"; } from "./helpers";
async function waitForNewEmail( async function waitForNewEmail(
@ -66,6 +68,204 @@ async function submitAuthenticatedSupportRequest(
).toBeVisible(); ).toBeVisible();
} }
test("support confirmation explains a missing protected fragment", async ({
browser,
}) => {
const context = await newIsolatedContext(browser);
const page = await context.newPage();
const assertBrowserClean = captureBrowserFailures(page);
await page.goto(
"/support/cases/00000000-0000-4000-8000-000000000001/verify",
);
await expect(page.locator("#support-confirmation-fragment-form")).toBeHidden();
await expect(
page.locator("#support-confirmation-fragment-invalid"),
).toBeVisible();
await expect(
page.getByText("The link is invalid or it has expired."),
).toBeVisible();
await expect(page.getByRole("link", { name: "Back to support" })).toHaveAttribute(
"href",
"/support",
);
assertBrowserClean();
await context.close();
});
test("content-removal confirmation explains a missing protected fragment", async ({
browser,
}) => {
const context = await newIsolatedContext(browser);
const page = await context.newPage();
const assertBrowserClean = captureBrowserFailures(page);
await page.goto(
"/legal/content-removal/00000000-0000-4000-8000-000000000002/verify",
);
await expect(
page.locator("#content-removal-confirmation-fragment-form"),
).toBeHidden();
await expect(
page.locator("#content-removal-confirmation-fragment-invalid"),
).toBeVisible();
await expect(
page.getByText("The link is invalid or it has expired."),
).toBeVisible();
await expect(
page
.locator("#content-removal-confirmation-fragment-invalid")
.getByRole("link", { name: "Report content" }),
).toHaveAttribute("href", "/legal/content-removal");
assertBrowserClean();
await context.close();
});
test("anonymous support confirmation opens from the protected email fragment", async ({
browser,
request,
}, testInfo) => {
const email = projectEmail("anonymous-support", testInfo.project.name);
const subject = `Anonymous support confirmation [${testInfo.project.name}]`;
const context = await newIsolatedContext(browser);
const page = await context.newPage();
const assertBrowserClean = captureBrowserFailures(page);
const previousMessageID = await latestMessageID(request, email);
await page.goto("/support");
await page
.getByLabel("What do you need help with?")
.selectOption("technical_issue");
await page.getByLabel("Contact email").fill(email);
await page.getByLabel("Subject").fill(subject);
await page
.getByLabel("Describe the problem")
.fill("Browser E2E verifies the protected Phoenix.Token fragment before support sees the request.");
await page.getByRole("button", { name: "Send support request" }).click();
await expect(page).toHaveURL(/\/support\/received\?reference=SUP-/);
await expect(page.getByRole("heading", { name: "Check your email" })).toBeVisible();
const confirmationLink = await waitForApplicationEmailLink(
request,
email,
"/support/cases/",
previousMessageID,
);
const confirmationURL = new URL(confirmationLink);
expect(confirmationURL.pathname).toMatch(/\/support\/cases\/[0-9a-f-]+\/verify$/);
expect(confirmationURL.search).toBe("");
expect(confirmationURL.hash).toMatch(/^#token=SFMyNTY\./);
await page.goto(confirmationLink);
const confirmationForm = page.locator("#support-confirmation-fragment-form");
await expect(confirmationForm).toBeVisible();
await expect(
page.locator("#support-confirmation-fragment-invalid"),
).toBeHidden();
await expect(page.locator("#support-confirmation-fragment-token")).toHaveValue(
/^SFMyNTY\./,
);
await confirmationForm
.getByRole("button", { name: "Confirm support request" })
.click();
await expect(page).toHaveURL(/\/support\/cases\/[0-9a-f-]+\?token=/);
await expect(
page.getByText("Your email was confirmed and the request was sent to support."),
).toBeVisible();
await expect(page.getByRole("heading", { name: subject })).toBeVisible();
assertBrowserClean();
await context.close();
});
test("anonymous content-removal confirmation opens from the protected email fragment", async ({
browser,
request,
}, testInfo) => {
const email = projectEmail("anonymous-removal", testInfo.project.name);
const context = await newIsolatedContext(browser);
const page = await context.newPage();
const assertBrowserClean = captureBrowserFailures(page);
const previousMessageID = await latestMessageID(request, email);
await page.goto("/legal/content-removal");
await page.getByLabel("Reason").selectOption("privacy_violation");
await page
.getByLabel("Your name or organisation")
.fill("Anonymous removal E2E");
await page.getByLabel("Contact email").fill(email);
await page
.getByLabel("Your relationship to the affected person or rights holder")
.selectOption("self");
await page
.getByLabel("Exact content URLs — one per line")
.fill(`${process.env.BASE_URL}/requests/anonymous-removal-e2e`);
await page
.getByLabel("Why do you believe this content should be removed?")
.fill("Browser E2E verifies explicit confirmation before legal review.");
await page
.getByLabel("Electronic signature (type your full name)")
.fill("Anonymous removal E2E");
await page
.getByLabel(/I believe in good faith that the identified content/)
.check();
await page
.getByLabel(/I confirm that this notice is accurate and complete/)
.check();
await page.getByRole("button", { name: "Submit removal notice" }).click();
await expect(page).toHaveURL(
/\/legal\/content-removal\/received\?reference=REM-/,
);
const confirmationLink = await waitForApplicationEmailLink(
request,
email,
"/legal/content-removal/",
previousMessageID,
);
const confirmationURL = new URL(confirmationLink);
expect(confirmationURL.pathname).toMatch(
/\/legal\/content-removal\/[0-9a-f-]+\/verify$/,
);
expect(confirmationURL.search).toBe("");
expect(confirmationURL.hash).toMatch(/^#token=SFMyNTY\./);
await page.goto(confirmationLink);
const confirmationForm = page.locator(
"#content-removal-confirmation-fragment-form",
);
await expect(confirmationForm).toBeVisible();
await expect(
page.locator("#content-removal-confirmation-fragment-invalid"),
).toBeHidden();
await expect(
page.locator("#content-removal-confirmation-fragment-token"),
).toHaveValue(/^SFMyNTY\./);
await confirmationForm
.getByRole("button", { name: "Confirm content-removal notice" })
.click();
await expect(page).toHaveURL(
/\/legal\/content-removal\/[0-9a-f-]+\?token=/,
);
await expect(
page.getByText("Your email was confirmed and the notice was sent for review."),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Content-removal notice" }),
).toBeVisible();
assertBrowserClean();
await context.close();
});
test("authenticated support and legal notices reach the scoped staff queues", async ({ test("authenticated support and legal notices reach the scoped staff queues", async ({
browser, browser,
request, request,

View File

@ -92,12 +92,9 @@ defmodule WhoNeedHelp.ContentRemoval do
with {:ok, id} <- Ecto.UUID.cast(id), with {:ok, id} <- Ecto.UUID.cast(id),
{:ok, ^id} <- {:ok, ^id} <-
PublicAccess.verify(@access_salt, token, max_age: case_access_max_age_seconds()), PublicAccess.verify(@access_salt, token, max_age: case_access_max_age_seconds()),
%Notice{} = notice <- Repo.get(Notice, id) do %Notice{contact_verified_at: verified_at} = notice when not is_nil(verified_at) <-
if notice.contact_verified_at do Repo.get(Notice, id) do
{:ok, notice} {:ok, notice}
else
verify_legacy_confirmation(notice, token)
end
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -272,13 +269,6 @@ defmodule WhoNeedHelp.ContentRemoval do
defp notify_verified_notice({:ok, {notice, :already_verified}}), do: {:ok, notice} defp notify_verified_notice({:ok, {notice, :already_verified}}), do: {:ok, notice}
defp notify_verified_notice(result), do: result defp notify_verified_notice(result), do: result
defp verify_legacy_confirmation(%Notice{id: id} = notice, token) do
case PublicAccess.verify(@access_salt, token, max_age: contact_verification_max_age_seconds()) do
{:ok, ^id} -> verify_contact(notice)
_error -> {:error, :not_found}
end
end
defp contact_verification_max_age_seconds do defp contact_verification_max_age_seconds do
Application.fetch_env!(:who_need_help, :public_contact_verification_max_age_seconds) Application.fetch_env!(:who_need_help, :public_contact_verification_max_age_seconds)
end end

View File

@ -142,12 +142,9 @@ defmodule WhoNeedHelp.Support do
with {:ok, id} <- Ecto.UUID.cast(id), with {:ok, id} <- Ecto.UUID.cast(id),
{:ok, ^id} <- {:ok, ^id} <-
PublicAccess.verify(@access_salt, token, max_age: case_access_max_age_seconds()), PublicAccess.verify(@access_salt, token, max_age: case_access_max_age_seconds()),
%SupportRequest{} = request <- Repo.get(SupportRequest, id) do %SupportRequest{contact_verified_at: verified_at} = request
if request.contact_verified_at do when not is_nil(verified_at) <- Repo.get(SupportRequest, id) do
{:ok, preload_conversation(request)} {:ok, preload_conversation(request)}
else
verify_legacy_confirmation(request, token)
end
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -583,18 +580,6 @@ defmodule WhoNeedHelp.Support do
end end
end end
defp verify_legacy_confirmation(%SupportRequest{id: id} = request, token) do
case PublicAccess.verify(@access_salt, token, max_age: contact_verification_max_age_seconds()) do
{:ok, ^id} ->
with {:ok, verified} <- verify_contact(request) do
{:ok, preload_conversation(verified)}
end
_error ->
{:error, :not_found}
end
end
defp contact_verification_max_age_seconds do defp contact_verification_max_age_seconds do
Application.fetch_env!(:who_need_help, :public_contact_verification_max_age_seconds) Application.fetch_env!(:who_need_help, :public_contact_verification_max_age_seconds)
end end

View File

@ -29,6 +29,13 @@
</.button> </.button>
</.form> </.form>
<div id="content-removal-confirmation-fragment-invalid" class="alert alert-error mt-5" hidden>
<span>{gettext("The link is invalid or it has expired.")}</span>
<.link navigate={~p"/legal/content-removal"} class="btn btn-sm btn-outline">
{gettext("Report content")}
</.link>
</div>
<p class="mt-5 text-sm text-base-content/70"> <p class="mt-5 text-sm text-base-content/70">
{gettext( {gettext(
"If you did not submit this notice, close this page. Nothing will be sent for review." "If you did not submit this notice, close this page. Nothing will be sent for review."

View File

@ -24,6 +24,13 @@
</.button> </.button>
</.form> </.form>
<div id="support-confirmation-fragment-invalid" class="alert alert-error mt-5" hidden>
<span>{gettext("The link is invalid or it has expired.")}</span>
<.link navigate={~p"/support"} class="btn btn-sm btn-outline">
{gettext("Back to support")}
</.link>
</div>
<p class="mt-5 text-sm text-base-content/70"> <p class="mt-5 text-sm text-base-content/70">
{gettext( {gettext(
"If you did not submit this request, close this page. Nothing will be sent to support." "If you did not submit this request, close this page. Nothing will be sent to support."

View File

@ -1836,6 +1836,8 @@ msgstr ""
msgid "The approved group has reached its capacity." msgid "The approved group has reached its capacity."
msgstr "" msgstr ""
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:33
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:28
#: lib/who_need_help_web/controllers/user_session_controller.ex:50 #: lib/who_need_help_web/controllers/user_session_controller.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "The link is invalid or it has expired." msgid "The link is invalid or it has expired."
@ -2522,8 +2524,8 @@ msgstr ""
msgid "Could not unblock this user. Please try again." msgid "Could not unblock this user. Please try again."
msgstr "" msgstr ""
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40 #: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:47
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:35 #: lib/who_need_help_web/controllers/support_html/verify.html.heex:42
#: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:27 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "JavaScript is required to confirm this protected email link." msgid "JavaScript is required to confirm this protected email link."
@ -2846,6 +2848,7 @@ msgid "Back to removal form"
msgstr "" msgstr ""
#: lib/who_need_help_web/controllers/support_html/received.html.heex:34 #: lib/who_need_help_web/controllers/support_html/received.html.heex:34
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to support" msgid "Back to support"
msgstr "" msgstr ""
@ -3120,6 +3123,7 @@ msgstr ""
#: lib/who_need_help_web/components/layouts.ex:201 #: lib/who_need_help_web/components/layouts.ex:201
#: lib/who_need_help_web/controllers/content_removal_controller.ex:151 #: lib/who_need_help_web/controllers/content_removal_controller.ex:151
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:35
#: lib/who_need_help_web/controllers/support_html/new.html.heex:16 #: lib/who_need_help_web/controllers/support_html/new.html.heex:16
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Report content" msgid "Report content"
@ -7901,12 +7905,12 @@ msgstr ""
msgid "Confirm that you submitted this request before it is sent to support." msgid "Confirm that you submitted this request before it is sent to support."
msgstr "" msgstr ""
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:33 #: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you did not submit this notice, close this page. Nothing will be sent for review." msgid "If you did not submit this notice, close this page. Nothing will be sent for review."
msgstr "" msgstr ""
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:28 #: lib/who_need_help_web/controllers/support_html/verify.html.heex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you did not submit this request, close this page. Nothing will be sent to support." msgid "If you did not submit this request, close this page. Nothing will be sent to support."
msgstr "" msgstr ""

View File

@ -1836,6 +1836,8 @@ msgstr "That verification provider is not supported."
msgid "The approved group has reached its capacity." msgid "The approved group has reached its capacity."
msgstr "The approved group has reached its capacity." msgstr "The approved group has reached its capacity."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:33
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:28
#: lib/who_need_help_web/controllers/user_session_controller.ex:50 #: lib/who_need_help_web/controllers/user_session_controller.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "The link is invalid or it has expired." msgid "The link is invalid or it has expired."
@ -2522,8 +2524,8 @@ msgstr "Could not publish the request. Please try again."
msgid "Could not unblock this user. Please try again." msgid "Could not unblock this user. Please try again."
msgstr "Could not unblock this user. Please try again." msgstr "Could not unblock this user. Please try again."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40 #: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:47
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:35 #: lib/who_need_help_web/controllers/support_html/verify.html.heex:42
#: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:27 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "JavaScript is required to confirm this protected email link." msgid "JavaScript is required to confirm this protected email link."
@ -2846,6 +2848,7 @@ msgid "Back to removal form"
msgstr "Back to removal form" msgstr "Back to removal form"
#: lib/who_need_help_web/controllers/support_html/received.html.heex:34 #: lib/who_need_help_web/controllers/support_html/received.html.heex:34
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to support" msgid "Back to support"
msgstr "Back to support" msgstr "Back to support"
@ -3120,6 +3123,7 @@ msgstr "Removal notice updated."
#: lib/who_need_help_web/components/layouts.ex:201 #: lib/who_need_help_web/components/layouts.ex:201
#: lib/who_need_help_web/controllers/content_removal_controller.ex:151 #: lib/who_need_help_web/controllers/content_removal_controller.ex:151
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:35
#: lib/who_need_help_web/controllers/support_html/new.html.heex:16 #: lib/who_need_help_web/controllers/support_html/new.html.heex:16
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Report content" msgid "Report content"
@ -7901,12 +7905,12 @@ msgstr "Confirm that you submitted this notice before it is sent for review."
msgid "Confirm that you submitted this request before it is sent to support." msgid "Confirm that you submitted this request before it is sent to support."
msgstr "Confirm that you submitted this request before it is sent to support." msgstr "Confirm that you submitted this request before it is sent to support."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:33 #: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you did not submit this notice, close this page. Nothing will be sent for review." msgid "If you did not submit this notice, close this page. Nothing will be sent for review."
msgstr "If you did not submit this notice, close this page. Nothing will be sent for review." msgstr "If you did not submit this notice, close this page. Nothing will be sent for review."
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:28 #: lib/who_need_help_web/controllers/support_html/verify.html.heex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you did not submit this request, close this page. Nothing will be sent to support." msgid "If you did not submit this request, close this page. Nothing will be sent to support."
msgstr "If you did not submit this request, close this page. Nothing will be sent to support." msgstr "If you did not submit this request, close this page. Nothing will be sent to support."

View File

@ -1925,6 +1925,8 @@ msgstr "Этот провайдер проверки не поддерживае
msgid "The approved group has reached its capacity." msgid "The approved group has reached its capacity."
msgstr "Одобренная группа уже заполнена." msgstr "Одобренная группа уже заполнена."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:33
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:28
#: lib/who_need_help_web/controllers/user_session_controller.ex:50 #: lib/who_need_help_web/controllers/user_session_controller.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "The link is invalid or it has expired." msgid "The link is invalid or it has expired."
@ -2638,8 +2640,8 @@ msgstr "Не удалось опубликовать заявку. Попроб
msgid "Could not unblock this user. Please try again." msgid "Could not unblock this user. Please try again."
msgstr "Не удалось разблокировать пользователя. Попробуйте ещё раз." msgstr "Не удалось разблокировать пользователя. Попробуйте ещё раз."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40 #: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:47
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:35 #: lib/who_need_help_web/controllers/support_html/verify.html.heex:42
#: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:27 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "JavaScript is required to confirm this protected email link." msgid "JavaScript is required to confirm this protected email link."
@ -2962,6 +2964,7 @@ msgid "Back to removal form"
msgstr "Вернуться к форме удаления" msgstr "Вернуться к форме удаления"
#: lib/who_need_help_web/controllers/support_html/received.html.heex:34 #: lib/who_need_help_web/controllers/support_html/received.html.heex:34
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to support" msgid "Back to support"
msgstr "Вернуться в поддержку" msgstr "Вернуться в поддержку"
@ -3236,6 +3239,7 @@ msgstr "Уведомление об удалении обновлено."
#: lib/who_need_help_web/components/layouts.ex:201 #: lib/who_need_help_web/components/layouts.ex:201
#: lib/who_need_help_web/controllers/content_removal_controller.ex:151 #: lib/who_need_help_web/controllers/content_removal_controller.ex:151
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:35
#: lib/who_need_help_web/controllers/support_html/new.html.heex:16 #: lib/who_need_help_web/controllers/support_html/new.html.heex:16
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Report content" msgid "Report content"
@ -8021,12 +8025,12 @@ msgstr "Подтвердите, что это уведомление отпра
msgid "Confirm that you submitted this request before it is sent to support." msgid "Confirm that you submitted this request before it is sent to support."
msgstr "Подтвердите, что это обращение отправили вы, прежде чем оно поступит в поддержку." msgstr "Подтвердите, что это обращение отправили вы, прежде чем оно поступит в поддержку."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:33 #: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you did not submit this notice, close this page. Nothing will be sent for review." msgid "If you did not submit this notice, close this page. Nothing will be sent for review."
msgstr "Если вы не отправляли это уведомление, закройте страницу. На рассмотрение ничего не поступит." msgstr "Если вы не отправляли это уведомление, закройте страницу. На рассмотрение ничего не поступит."
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:28 #: lib/who_need_help_web/controllers/support_html/verify.html.heex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you did not submit this request, close this page. Nothing will be sent to support." msgid "If you did not submit this request, close this page. Nothing will be sent to support."
msgstr "Если вы не отправляли это обращение, закройте страницу. В поддержку ничего не поступит." msgstr "Если вы не отправляли это обращение, закройте страницу. В поддержку ничего не поступит."

View File

@ -1922,6 +1922,8 @@ msgstr "Цей постачальник перевірки не підтриму
msgid "The approved group has reached its capacity." msgid "The approved group has reached its capacity."
msgstr "Схвалена група вже заповнена." msgstr "Схвалена група вже заповнена."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:33
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:28
#: lib/who_need_help_web/controllers/user_session_controller.ex:50 #: lib/who_need_help_web/controllers/user_session_controller.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "The link is invalid or it has expired." msgid "The link is invalid or it has expired."
@ -2632,8 +2634,8 @@ msgstr "Не вдалося опублікувати заявку. Спробу
msgid "Could not unblock this user. Please try again." msgid "Could not unblock this user. Please try again."
msgstr "Не вдалося розблокувати користувача. Спробуйте ще раз." msgstr "Не вдалося розблокувати користувача. Спробуйте ще раз."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40 #: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:47
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:35 #: lib/who_need_help_web/controllers/support_html/verify.html.heex:42
#: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:27 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "JavaScript is required to confirm this protected email link." msgid "JavaScript is required to confirm this protected email link."
@ -2956,6 +2958,7 @@ msgid "Back to removal form"
msgstr "Повернутися до форми видалення" msgstr "Повернутися до форми видалення"
#: lib/who_need_help_web/controllers/support_html/received.html.heex:34 #: lib/who_need_help_web/controllers/support_html/received.html.heex:34
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to support" msgid "Back to support"
msgstr "Повернутися до підтримки" msgstr "Повернутися до підтримки"
@ -3230,6 +3233,7 @@ msgstr "Повідомлення про видалення оновлено."
#: lib/who_need_help_web/components/layouts.ex:201 #: lib/who_need_help_web/components/layouts.ex:201
#: lib/who_need_help_web/controllers/content_removal_controller.ex:151 #: lib/who_need_help_web/controllers/content_removal_controller.ex:151
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:35
#: lib/who_need_help_web/controllers/support_html/new.html.heex:16 #: lib/who_need_help_web/controllers/support_html/new.html.heex:16
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Report content" msgid "Report content"
@ -8015,12 +8019,12 @@ msgstr "Підтвердьте, що це повідомлення надісл
msgid "Confirm that you submitted this request before it is sent to support." msgid "Confirm that you submitted this request before it is sent to support."
msgstr "Підтвердьте, що це звернення надіслали ви, перш ніж воно надійде до підтримки." msgstr "Підтвердьте, що це звернення надіслали ви, перш ніж воно надійде до підтримки."
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:33 #: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you did not submit this notice, close this page. Nothing will be sent for review." msgid "If you did not submit this notice, close this page. Nothing will be sent for review."
msgstr "Якщо ви не надсилали це повідомлення, закрийте сторінку. На розгляд нічого не надійде." msgstr "Якщо ви не надсилали це повідомлення, закрийте сторінку. На розгляд нічого не надійде."
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:28 #: lib/who_need_help_web/controllers/support_html/verify.html.heex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you did not submit this request, close this page. Nothing will be sent to support." msgid "If you did not submit this request, close this page. Nothing will be sent to support."
msgstr "Якщо ви не надсилали це звернення, закрийте сторінку. До підтримки нічого не надійде." msgstr "Якщо ви не надсилали це звернення, закрийте сторінку. До підтримки нічого не надійде."

View File

@ -72,6 +72,9 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
assert {:error, :not_found} = Support.get_by_access_token(request.id, "invalid") assert {:error, :not_found} = Support.get_by_access_token(request.id, "invalid")
assert {:error, :not_found} = Support.get_by_access_token(request.id, confirmation_token) assert {:error, :not_found} = Support.get_by_access_token(request.id, confirmation_token)
assert {:error, :not_found} =
Support.get_by_access_token(request.id, Support.access_token(request))
assert {:error, :not_found} = assert {:error, :not_found} =
Support.verify_by_confirmation_token(request.id, Support.access_token(request)) Support.verify_by_confirmation_token(request.id, Support.access_token(request))
@ -666,6 +669,12 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
moderator_scope = moderator_scope() moderator_scope = moderator_scope()
assert ContentRemoval.paginate_for_staff(moderator_scope).entries == [] assert ContentRemoval.paginate_for_staff(moderator_scope).entries == []
assert {:error, :not_found} =
ContentRemoval.get_by_access_token(
notice.id,
ContentRemoval.access_token(notice)
)
assert {:error, :not_found} = assert {:error, :not_found} =
ContentRemoval.moderate(moderator_scope, notice.id, %{ ContentRemoval.moderate(moderator_scope, notice.id, %{
"status" => "reviewing", "status" => "reviewing",