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 {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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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,
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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 "Если вы не отправляли это обращение, закройте страницу. В поддержку ничего не поступит."
|
||||||
|
|
|
||||||
|
|
@ -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 "Якщо ви не надсилали це звернення, закрийте сторінку. До підтримки нічого не надійде."
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user