Require explicit public contact confirmation
This commit is contained in:
parent
c91ce502cb
commit
d94c89e844
|
|
@ -25,6 +25,7 @@ import {LiveSocket} from "phoenix_live_view"
|
|||
import {hooks as colocatedHooks} from "phoenix-colocated/who_need_help"
|
||||
import topbar from "../vendor/topbar"
|
||||
import {Hooks, mountStaticAidMaps} from "./hooks"
|
||||
import {isProtectedFragmentTokenForForm} from "./protected_token_fragment.mjs"
|
||||
|
||||
const systemTheme = () =>
|
||||
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 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 = [
|
||||
{
|
||||
form: document.getElementById("magic-link-fragment-form"),
|
||||
|
|
@ -77,11 +71,13 @@ const activateProtectedTokenFragment = () => {
|
|||
},
|
||||
{
|
||||
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"),
|
||||
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
|
||||
|
||||
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.form.hidden = false
|
||||
if (candidate.invalidMessage instanceof HTMLElement) candidate.invalidMessage.hidden = true
|
||||
if (candidate.options instanceof HTMLElement) candidate.options.hidden = true
|
||||
candidate.form.querySelector("button")?.focus()
|
||||
}
|
||||
|
|
|
|||
26
assets/js/protected_token_fragment.mjs
Normal file
26
assets/js/protected_token_fragment.mjs
Normal 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
|
||||
}
|
||||
66
assets/js/protected_token_fragment.test.mjs
Normal file
66
assets/js/protected_token_fragment.test.mjs
Normal 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)
|
||||
})
|
||||
|
|
@ -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,
|
||||
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
|
||||
|
||||
Run both checks against the single ignored production `.env`. The first reports
|
||||
|
|
|
|||
|
|
@ -3,6 +3,57 @@
|
|||
Observed through 2026-08-26 in the local workspace. This report separates observed
|
||||
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
|
||||
|
||||
- The uncommitted production Web Push verifier hardening and service-worker
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import {
|
|||
latestMessageID,
|
||||
loginWithMagicLink,
|
||||
loginWithPassword,
|
||||
newIsolatedContext,
|
||||
projectEmail,
|
||||
registerAndConfirm,
|
||||
waitForApplicationEmailLink,
|
||||
} from "./helpers";
|
||||
|
||||
async function waitForNewEmail(
|
||||
|
|
@ -66,6 +68,204 @@ async function submitAuthenticatedSupportRequest(
|
|||
).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 ({
|
||||
browser,
|
||||
request,
|
||||
|
|
|
|||
|
|
@ -92,12 +92,9 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
with {:ok, id} <- Ecto.UUID.cast(id),
|
||||
{:ok, ^id} <-
|
||||
PublicAccess.verify(@access_salt, token, max_age: case_access_max_age_seconds()),
|
||||
%Notice{} = notice <- Repo.get(Notice, id) do
|
||||
if notice.contact_verified_at do
|
||||
{:ok, notice}
|
||||
else
|
||||
verify_legacy_confirmation(notice, token)
|
||||
end
|
||||
%Notice{contact_verified_at: verified_at} = notice when not is_nil(verified_at) <-
|
||||
Repo.get(Notice, id) do
|
||||
{:ok, notice}
|
||||
else
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
|
|
@ -272,13 +269,6 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
defp notify_verified_notice({:ok, {notice, :already_verified}}), do: {:ok, notice}
|
||||
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
|
||||
Application.fetch_env!(:who_need_help, :public_contact_verification_max_age_seconds)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -142,12 +142,9 @@ defmodule WhoNeedHelp.Support do
|
|||
with {:ok, id} <- Ecto.UUID.cast(id),
|
||||
{:ok, ^id} <-
|
||||
PublicAccess.verify(@access_salt, token, max_age: case_access_max_age_seconds()),
|
||||
%SupportRequest{} = request <- Repo.get(SupportRequest, id) do
|
||||
if request.contact_verified_at do
|
||||
{:ok, preload_conversation(request)}
|
||||
else
|
||||
verify_legacy_confirmation(request, token)
|
||||
end
|
||||
%SupportRequest{contact_verified_at: verified_at} = request
|
||||
when not is_nil(verified_at) <- Repo.get(SupportRequest, id) do
|
||||
{:ok, preload_conversation(request)}
|
||||
else
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
|
|
@ -583,18 +580,6 @@ defmodule WhoNeedHelp.Support do
|
|||
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
|
||||
Application.fetch_env!(:who_need_help, :public_contact_verification_max_age_seconds)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@
|
|||
</.button>
|
||||
</.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">
|
||||
{gettext(
|
||||
"If you did not submit this notice, close this page. Nothing will be sent for review."
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@
|
|||
</.button>
|
||||
</.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">
|
||||
{gettext(
|
||||
"If you did not submit this request, close this page. Nothing will be sent to support."
|
||||
|
|
|
|||
|
|
@ -1836,6 +1836,8 @@ msgstr ""
|
|||
msgid "The approved group has reached its capacity."
|
||||
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
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "The link is invalid or it has expired."
|
||||
|
|
@ -2522,8 +2524,8 @@ msgstr ""
|
|||
msgid "Could not unblock this user. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40
|
||||
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:35
|
||||
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:47
|
||||
#: 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
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "JavaScript is required to confirm this protected email link."
|
||||
|
|
@ -2846,6 +2848,7 @@ msgid "Back to removal form"
|
|||
msgstr ""
|
||||
|
||||
#: 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
|
||||
msgid "Back to support"
|
||||
msgstr ""
|
||||
|
|
@ -3120,6 +3123,7 @@ msgstr ""
|
|||
|
||||
#: 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_html/verify.html.heex:35
|
||||
#: lib/who_need_help_web/controllers/support_html/new.html.heex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Report content"
|
||||
|
|
@ -7901,12 +7905,12 @@ msgstr ""
|
|||
msgid "Confirm that you submitted this request before it is sent to support."
|
||||
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
|
||||
msgid "If you did not submit this notice, close this page. Nothing will be sent for review."
|
||||
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
|
||||
msgid "If you did not submit this request, close this page. Nothing will be sent to support."
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -1836,6 +1836,8 @@ msgstr "That verification provider is not supported."
|
|||
msgid "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
|
||||
#, elixir-autogen, elixir-format
|
||||
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."
|
||||
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/support_html/verify.html.heex:35
|
||||
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:47
|
||||
#: 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
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "JavaScript is required to confirm this protected email link."
|
||||
|
|
@ -2846,6 +2848,7 @@ msgid "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/verify.html.heex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "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/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
|
||||
#, elixir-autogen, elixir-format
|
||||
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."
|
||||
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
|
||||
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."
|
||||
|
||||
#: 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
|
||||
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."
|
||||
|
|
|
|||
|
|
@ -1925,6 +1925,8 @@ msgstr "Этот провайдер проверки не поддерживае
|
|||
msgid "The approved group has reached its capacity."
|
||||
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
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "The link is invalid or it has expired."
|
||||
|
|
@ -2638,8 +2640,8 @@ msgstr "Не удалось опубликовать заявку. Попроб
|
|||
msgid "Could not unblock this user. Please try again."
|
||||
msgstr "Не удалось разблокировать пользователя. Попробуйте ещё раз."
|
||||
|
||||
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40
|
||||
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:35
|
||||
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:47
|
||||
#: 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
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "JavaScript is required to confirm this protected email link."
|
||||
|
|
@ -2962,6 +2964,7 @@ msgid "Back to removal form"
|
|||
msgstr "Вернуться к форме удаления"
|
||||
|
||||
#: 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
|
||||
msgid "Back to support"
|
||||
msgstr "Вернуться в поддержку"
|
||||
|
|
@ -3236,6 +3239,7 @@ msgstr "Уведомление об удалении обновлено."
|
|||
|
||||
#: 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_html/verify.html.heex:35
|
||||
#: lib/who_need_help_web/controllers/support_html/new.html.heex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Report content"
|
||||
|
|
@ -8021,12 +8025,12 @@ msgstr "Подтвердите, что это уведомление отпра
|
|||
msgid "Confirm that you submitted this request before it is sent to support."
|
||||
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
|
||||
msgid "If you did not submit this notice, close this page. Nothing will be sent for review."
|
||||
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
|
||||
msgid "If you did not submit this request, close this page. Nothing will be sent to support."
|
||||
msgstr "Если вы не отправляли это обращение, закройте страницу. В поддержку ничего не поступит."
|
||||
|
|
|
|||
|
|
@ -1922,6 +1922,8 @@ msgstr "Цей постачальник перевірки не підтриму
|
|||
msgid "The approved group has reached its capacity."
|
||||
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
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "The link is invalid or it has expired."
|
||||
|
|
@ -2632,8 +2634,8 @@ msgstr "Не вдалося опублікувати заявку. Спробу
|
|||
msgid "Could not unblock this user. Please try again."
|
||||
msgstr "Не вдалося розблокувати користувача. Спробуйте ще раз."
|
||||
|
||||
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:40
|
||||
#: lib/who_need_help_web/controllers/support_html/verify.html.heex:35
|
||||
#: lib/who_need_help_web/controllers/content_removal_html/verify.html.heex:47
|
||||
#: 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
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "JavaScript is required to confirm this protected email link."
|
||||
|
|
@ -2956,6 +2958,7 @@ msgid "Back to removal form"
|
|||
msgstr "Повернутися до форми видалення"
|
||||
|
||||
#: 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
|
||||
msgid "Back to support"
|
||||
msgstr "Повернутися до підтримки"
|
||||
|
|
@ -3230,6 +3233,7 @@ msgstr "Повідомлення про видалення оновлено."
|
|||
|
||||
#: 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_html/verify.html.heex:35
|
||||
#: lib/who_need_help_web/controllers/support_html/new.html.heex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Report content"
|
||||
|
|
@ -8015,12 +8019,12 @@ msgstr "Підтвердьте, що це повідомлення надісл
|
|||
msgid "Confirm that you submitted this request before it is sent to support."
|
||||
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
|
||||
msgid "If you did not submit this notice, close this page. Nothing will be sent for review."
|
||||
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
|
||||
msgid "If you did not submit this request, close this page. Nothing will be sent to support."
|
||||
msgstr "Якщо ви не надсилали це звернення, закрийте сторінку. До підтримки нічого не надійде."
|
||||
|
|
|
|||
|
|
@ -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, confirmation_token)
|
||||
|
||||
assert {:error, :not_found} =
|
||||
Support.get_by_access_token(request.id, Support.access_token(request))
|
||||
|
||||
assert {:error, :not_found} =
|
||||
Support.verify_by_confirmation_token(request.id, Support.access_token(request))
|
||||
|
||||
|
|
@ -666,6 +669,12 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
|
|||
moderator_scope = moderator_scope()
|
||||
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} =
|
||||
ContentRemoval.moderate(moderator_scope, notice.id, %{
|
||||
"status" => "reviewing",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user