6 Commits
Author SHA1 Message Date
bermudalamb 0361ef35d0 Merge pull request 'docs(auth): correct the claim that QA could never run Google sign-in' (#356) from docs/correct-qa-google-claim into main
Linting / lint (push) Successful in 3m13s
SonarQube Analysis / sonarqube (push) Failing after 31m6s
Reviewed-on: #356
2026-09-11 15:16:28 -05:00
synAdminandClaude Opus 5 f5a29127fb docs(auth): correct the claim that QA could never run Google sign-in
Linting / lint (pull_request) Successful in 3m44s
SonarQube Analysis / sonarqube (pull_request) Failing after 28m29s
It can, and it does. Registering the QA callback under Authorized redirect URIs was all it took.

The claim was that qa-redefined-designs.bermudalamb.synology.me could never be registered, because Google requires a redirect URI's host to sit under a domain whose ownership has been proved by DNS, and Synology owns the domain above that one. It was inferred from #285, where Cloudflare's free tier genuinely cannot be applied to that hostname, and asserted with far more confidence than the inference supported. What was actually established is narrower: localhost is exempt from the authorized-domain rules, and a domain listed as an authorized domain has to be verified in Search Console. Whether either applied here was never checked.

It was not a harmless error. On the strength of it, QA testing of this feature was documented as blocked behind #313, the QA compose file hardcoded its credentials to empty rather than reading the stack, #345 recorded it as a constraint, and #332 closed with it written into the summary. A QA deploy was spent on it.

So the correction is left in place rather than the wrong sentences quietly deleted. A document that silently stops saying something teaches nobody why it said it, and this is the second time in this feature that a confident inference about somebody else's platform has cost a day — the first being the assumption that a passing local build said anything about another machine.

What replaces it is the thing that was always true and never written down plainly: every environment sends a redirect URI derived from its own PUBLIC_URL, and each one has to exist verbatim in the console. There is now one table listing all four, including the localhost:5173 entry that local development needs and that Phase 0 originally omitted — the omission that cost an hour of redirect_uri_mismatch before any of this.

The QA compose comment now says which URL to register rather than why it cannot be. The ops document gains the steps QA actually took, in order, with a note on why registering before setting the variables is the order that matters: a button that appears before its callback exists fails at Google, where nothing in the storefront logs explains it.

Verified: backend tsc clean, the QA compose file still parses, and no file in the tree still claims the hostname is unusable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 14:48:46 -05:00
bermudalamb c9dccfe4a2 Merge pull request 'chore(qa): read the Google credentials from the stack, like every other secret' (#355) from chore/qa-google-credentials-from-stack into main
Linting / lint (push) Successful in 3m36s
SonarQube Analysis / sonarqube (push) Failing after 30m5s
Reviewed-on: #355
2026-09-11 14:19:20 -05:00
synAdminandClaude Opus 5 903a1d8b76 chore(qa): read the Google credentials from the stack, like every other secret
Linting / lint (pull_request) Successful in 3m28s
SonarQube Analysis / sonarqube (pull_request) Failing after 28m14s
QA_GOOGLE_CLIENT_ID and QA_GOOGLE_CLIENT_SECRET were set on the QA stack and went nowhere, because #340 hardcoded the container's values empty rather than reading anything. The button stayed missing, correctly, but for a reason the file gave no way to discover: every other secret in it is read from a QA_-prefixed stack variable, and these two were the odd ones out.

So they are wired the way the rest of the file works. The deploy that prompted this cost nothing except time, and the next one would have cost the same again.

Wiring them is not the same as enabling them, and the comment now leads with that. **Leave both stack variables unset until QA moves off *.bermudalamb.synology.me.** Google refuses a redirect URI whose host is not under a domain whose ownership has been proved by DNS, and nobody can prove ownership of that one, because Synology owns the registrable domain above it — the same wall #285 hit with Cloudflare. Setting them today produces a button that fails at Google with redirect_uri_mismatch, and there is no console entry that could satisfy it.

Once #313 moves QA to qa.redefined-designs.com it is three steps and no code: set the two variables, point PUBLIC_URL at the new host, and add the matching callback under Clients in the Google Auth Platform.

Production already read its pair from the stack and is unchanged. The QA variable documentation at the top of the file gains an entry, matching the style of the others.

Verified: both files still parse as YAML, both substitutions resolve to the intended stack variables, and the compose environment guard passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 14:17:51 -05:00
bermudalamb 838749df64 Merge pull request 'fix(auth): offer Google on the sign-up tab, not only on Log In (#345)' (#354) from fix/google-button-missing-on-signup into main
Linting / lint (push) Successful in 3m19s
SonarQube Analysis / sonarqube (push) Failing after 32m51s
Reviewed-on: #354
2026-09-11 10:06:54 -05:00
synAdminandClaude Opus 5 3958bda489 fix(auth): offer Google on the sign-up tab, not only on Log In (#345)
The button was rendered inside the Log In tab's form only, so a visitor on Create Account saw no social option at all. Reported from a local run.

The mistake came from following the passkey button too closely. A passkey belongs only on Log In, and correctly so: you cannot register an account with one, since registration requires an account to register it against. Google is the opposite case. Creating an account is precisely what a new customer reaches for it to do, so leaving it off the sign-up tab hid the feature from the people it helps most — and hid it on the tab the modal opens on by default.

The label differs by tab and nothing else does. One endpoint serves both: it signs in a known identity, links a verified address, or creates an account, and the customer neither knows nor cares which will happen. So the wording matches what they came to that tab to do rather than what the server ends up doing. Both spellings are given in Google's identity guidelines alongside the mark.

The two consent checkboxes above it are deliberately not carried across. Google takes the customer off this site entirely, and a tick that survived that round trip would be a consent recorded from a form nobody submitted. They are asked again, with the same wording and through the same endpoints, on the step they land on afterwards — which is what #342 built that step for.

The end-to-end test asserts absence, like the one beside it, because local and QA have no credentials and absence is the behaviour that actually runs there.

Verified: frontend tsc, lint and build clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:06:54 -05:00
6 changed files with 151 additions and 59 deletions
+22 -17
View File
@@ -14,27 +14,32 @@
* source, and it is the one that is already correct in any environment where * source, and it is the one that is already correct in any environment where
* mail works. * mail works.
* *
* ## The consequence worth stating plainly * ## Every environment needs its own console entry
* *
* Google refuses a redirect URI whose host is not under an **authorized * Whatever this resolves to has to exist, verbatim, under Authorized redirect
* domain**, and a domain can only be authorized after ownership has been proved * URIs for the client this app uses. Google compares the two as strings, and a
* by DNS in Search Console. `localhost` is the sole exemption. * mismatch is answered with `redirect_uri_mismatch` — accurate, and silent
* about which half is wrong.
* *
* `qa-redefined-designs.bermudalamb.synology.me` therefore **cannot ever be * | Environment | Redirect URI |
* used**: Synology owns the registrable domain above it, so there is no record * | --- | --- |
* to add and nothing to prove. This is the same wall #285 hit with Cloudflare. * | Local, Vite | `http://localhost:5173/api/auth/google/callback` |
* | Local, built | `http://localhost:3000/api/auth/google/callback` |
* | QA | `https://qa-redefined-designs.bermudalamb.synology.me/api/auth/google/callback` |
* | Production | `https://redefined-designs.com/api/auth/google/callback` |
* *
* | Environment | Redirect URI | Works | * Local development needs the 5173 one, because that is where the dev server
* | --- | --- | --- | * serves the app; the 3000 one only applies when the backend serves a built
* | Local | `http://localhost:3000/...` | Yes, by exemption | * frontend.
* | QA on the Synology host | — | **No, and cannot** |
* | QA on `qa.redefined-designs.com` | `https://qa.redefined-designs.com/...` | After #313 |
* | Production | `https://redefined-designs.com/...` | After #313 |
* *
* So this feature is built and exercised locally, and QA cannot see it until QA * An earlier version of this comment claimed the QA hostname could never be
* moves onto a subdomain of the real domain. That is a `PUBLIC_URL` change and * registered, because it sits under a domain Synology owns. **That was wrong**,
* one console entry, not a code change — this module follows `PUBLIC_URL` * and it is recorded here rather than quietly deleted: it was asserted from the
* wherever it points. See #345. * shape of #285, which is a related but different problem, and it sent QA
* testing of this feature behind #313 for no reason. Adding the URI works.
*
* This module needs no change in any environment. It follows `PUBLIC_URL`
* wherever it points.
*/ */
/** The callback path. One constant, because it appears in two sentences. */ /** The callback path. One constant, because it appears in two sentences. */
+26 -12
View File
@@ -63,6 +63,12 @@
# in the notification email (#224). Absent, the email still # in the notification email (#224). Absent, the email still
# sends and simply carries no shortcuts. Its own value, not # sends and simply carries no shortcuts. Its own value, not
# production's: a link signed with it acts without a login. # production's: a link signed with it acts without a login.
# QA_GOOGLE_CLIENT_ID — optional, and all-or-nothing with the secret below:
# QA_GOOGLE_CLIENT_SECRET setting one without the other refuses to boot
# (#340). Both unset means the Google button is not offered
# at all, which is the right answer until QA's callback URL
# is registered in the Google Auth Platform. See the note
# beside the values themselves for the exact URL (#345).
# QA_REMBG_URL — optional. The background-removal sidecar, e.g. # QA_REMBG_URL — optional. The background-removal sidecar, e.g.
# http://rembg-syn:7000. Unset turns the feature off rather # http://rembg-syn:7000. Unset turns the feature off rather
# than breaking anything. The sidecar must be on the same # than breaking anything. The sidecar must be on the same
@@ -195,20 +201,28 @@ services:
# rotating it revokes every outstanding link, which is the intended way to # rotating it revokes every outstanding link, which is the intended way to
# deal with a leak. # deal with a leak.
- INTAKE_ACTION_SECRET=${QA_INTAKE_ACTION_SECRET:-} - INTAKE_ACTION_SECRET=${QA_INTAKE_ACTION_SECRET:-}
# Deliberately left empty, and it is not an oversight (#340, #345). # Read from the stack like every other QA secret, rather than hardcoded
# empty as they were in #340. Leaving them unreadable made this file the
# odd one out and cost a QA deploy: the variables were set on the stack,
# nothing read them, and the button stayed missing with no explanation.
# #
# Google refuses a redirect URI whose host is not under a domain whose # Setting these needs one thing done first: the QA callback registered
# ownership has been proved by DNS, and nobody can prove ownership of # under Authorized redirect URIs for this client in the Google Auth
# *.bermudalamb.synology.me because Synology owns the registrable domain # Platform, exactly as it appears below. Google compares the two as
# above it. Same wall as #285. So QA cannot run Google sign-in at all # strings and answers a mismatch with redirect_uri_mismatch.
# while it lives on this hostname, and setting these would only produce a
# button that fails at Google.
# #
# It becomes possible when #313 moves QA to qa.redefined-designs.com: # https://qa-redefined-designs.bermudalamb.synology.me/api/auth/google/callback
# set both here, set PUBLIC_URL to the new host, and add the matching #
# callback in the Google Auth Platform. No code change either way. # An earlier version of this comment said that URI could never be
- GOOGLE_CLIENT_ID= # registered, because Synology owns the domain above it. That was wrong,
- GOOGLE_CLIENT_SECRET= # and the correction is left here rather than removed: it was inferred
# from #285, which is a related but different problem, and it put QA
# testing of this feature behind #313 for no reason.
#
# When #313 moves QA to qa.redefined-designs.com, point PUBLIC_URL at the
# new host and register that callback too. No code change either way.
- GOOGLE_CLIENT_ID=${QA_GOOGLE_CLIENT_ID:-}
- GOOGLE_CLIENT_SECRET=${QA_GOOGLE_CLIENT_SECRET:-}
volumes: volumes:
# Separate uploads directory. Sharing production's would let a QA run # Separate uploads directory. Sharing production's would let a QA run
# write into, and a QA teardown delete, real product images. # write into, and a QA teardown delete, real product images.
+52 -28
View File
@@ -18,33 +18,42 @@ screen, so what appears there is the production identity even while testing.
| Data Access | Exactly `openid`, `email`, `profile` | | Data Access | Exactly `openid`, `email`, `profile` |
| Verification Center | Nothing to submit, and it should stay that way | | Verification Center | Nothing to submit, and it should stay that way |
## The constraint that shapes everything ## Redirect URIs, one per environment
**Google refuses a redirect URI whose host is not under an authorized domain, Every environment sends a redirect URI derived from its own `PUBLIC_URL`, and
and a domain can only be authorized after ownership is proved by DNS in Search each one has to exist verbatim under **Authorized redirect URIs** on the client
Console.** `localhost` is the only exemption. this app uses. Google compares them as strings and answers a mismatch with
`redirect_uri_mismatch`, which is accurate and says nothing about which half is
wrong.
`qa-redefined-designs.bermudalamb.synology.me` therefore cannot ever be used: | Environment | Redirect URI |
Synology owns the registrable domain above it, so there is no record to add and
nothing to prove. This is the same wall #285 hit with Cloudflare.
The consequence, stated plainly because it changes how the feature is worked on:
| Environment | Google sign-in |
| --- | --- | | --- | --- |
| Local, on `localhost` | Works, by exemption | | Local, Vite dev server | `http://localhost:5173/api/auth/google/callback` |
| QA on the Synology hostname | **Impossible**, not merely unconfigured | | Local, backend serving a build | `http://localhost:3000/api/auth/google/callback` |
| QA on `qa.redefined-designs.com` | Works, after the cutover | | QA | `https://qa-redefined-designs.bermudalamb.synology.me/api/auth/google/callback` |
| Production on `redefined-designs.com` | Works, after the cutover | | Production | `https://redefined-designs.com/api/auth/google/callback` |
So this feature is built and exercised locally, and QA cannot see it at all Local development needs the 5173 entry, because that is where the dev server
until QA moves onto a subdomain of the real domain. The Search Console property serves the app. The 3000 one applies only when the backend serves a built
for `redefined-designs.com` already covers `qa.redefined-designs.com`, because frontend, which local development does not produce.
a Domain property covers every subdomain.
`docker-compose.qa.yml` sets both credentials to empty deliberately, with a ### A correction
comment saying so, and the storefront then offers no button rather than one that
fails at Google. An earlier version of this document said the QA hostname **could never be
registered**, because it sits under a domain Synology owns rather than one we
do. That was wrong. Adding the URI works.
The claim is recorded here rather than quietly removed, because of what it
cost. It was inferred from #285, where Cloudflare genuinely cannot be applied to
that hostname, and asserted with far more confidence than the inference
supported. On the strength of it, QA testing of Google sign-in was documented as
blocked behind #313, the QA compose file hardcoded its credentials to empty, and
two issues recorded it as fact.
What is true, and is all that was ever established: `localhost` is exempt from
the authorized-domain rules, and a domain listed as an authorized domain has to
be verified in Search Console. Whether either of those actually applied to this
hostname, and how, was never checked.
## Scopes, and why publishing needs no review ## Scopes, and why publishing needs no review
@@ -60,20 +69,35 @@ walkthrough and a wait measured in weeks.** Nothing in this feature needs one.
Uploading an app logo also triggers a brand review, which is why Branding has Uploading an app logo also triggers a brand review, which is why Branding has
none. none.
## Turning it on in QA
Already done, and recorded here because the order matters.
1. Register the QA callback under **Clients**, Authorized redirect URIs:
`https://qa-redefined-designs.bermudalamb.synology.me/api/auth/google/callback`
2. Set `QA_GOOGLE_CLIENT_ID` and `QA_GOOGLE_CLIENT_SECRET` on the QA stack. Both
or neither — the backend refuses to start on one without the other, because
the failure would otherwise arrive the moment a customer presses the button.
3. Redeploy.
Registering first is the point. Setting the variables makes the button appear,
and a button that appears before its callback exists fails at Google rather than
in the storefront, where nothing in the logs explains it.
## The cutover checklist, for #313 ## The cutover checklist, for #313
1. Point QA at `qa.redefined-designs.com` and set its `PUBLIC_URL` to match. 1. Point QA at `qa.redefined-designs.com` and set its `PUBLIC_URL` to match.
2. Set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in the QA stack. Both or 2. In **Clients**, add the new QA callback:
neither — the backend refuses to start on one without the other, because the
failure would otherwise arrive the moment a customer presses the button.
3. In **Clients**, add the QA callback:
`https://qa.redefined-designs.com/api/auth/google/callback` `https://qa.redefined-designs.com/api/auth/google/callback`
4. Confirm the production callback is registered: 3. Confirm the production callback is registered:
`https://redefined-designs.com/api/auth/google/callback` `https://redefined-designs.com/api/auth/google/callback`
5. In **Audience**, move the publishing status from Testing to **In production**. 4. In **Audience**, move the publishing status from Testing to **In production**.
Do it once the domain resolves, so the home page and privacy links Google Do it once the domain resolves, so the home page and privacy links Google
shows actually answer. shows actually answer.
The old QA callback can be left registered until the hostname is retired. An
extra entry costs nothing and removing it early breaks QA for no gain.
No code changes at any step. The redirect URI is derived from `PUBLIC_URL`, so No code changes at any step. The redirect URI is derived from `PUBLIC_URL`, so
the environment variable and the console entry are the whole of it. the environment variable and the console entry are the whole of it.
+21
View File
@@ -233,6 +233,27 @@ export default function AuthForm({ mode, onModeChange, onForgotPassword, onSucce
By creating an account you agree to our{' '} By creating an account you agree to our{' '}
<a href="/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>. <a href="/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.
</Text> </Text>
{/* On this tab too, and its absence here was a bug (#345).
A passkey belongs only on Log In, because you cannot
register an account with one — but creating an account is
exactly what a new customer reaches for Google to do, so
leaving it off the sign-up tab hid the feature from the
people it helps most.
The two consent boxes above are not carried across. Google
takes the customer off this site entirely, and a tick that
survived that round trip would be a consent recorded from a
form nobody submitted. They are asked again, with the same
wording, on the step they land on (#342). */}
{googleEnabled && (
<>
<Divider plain style={{ marginBlock: 16 }}>
<Text type="secondary" style={{ fontSize: 12 }}>or</Text>
</Divider>
<GoogleSignInButton returnTo={returnTo} intent="sign-up" />
</>
)}
</Form> </Form>
) )
}, },
+13 -2
View File
@@ -37,6 +37,17 @@ function GoogleMark() {
type Props = Readonly<{ type Props = Readonly<{
/** Where to send the customer back to. Validated again on the server. */ /** Where to send the customer back to. Validated again on the server. */
returnTo: string; returnTo: string;
/**
* Which tab this sits on, which changes only the wording.
*
* One endpoint serves both: it signs in a known identity, links a verified
* address, or creates an account. The customer does not know or care which
* of those will happen, so the label matches what they came to the tab to
* do rather than what the server ends up doing.
*
* Both spellings are in Google identity guidelines alongside the mark.
*/
intent?: 'sign-in' | 'sign-up';
}>; }>;
/** /**
@@ -51,7 +62,7 @@ type Props = Readonly<{
* here. It has to be, since anyone can type the URL, and doing it in one place * here. It has to be, since anyone can type the URL, and doing it in one place
* beats doing it in two languages. See `google/returnTo.ts`. * beats doing it in two languages. See `google/returnTo.ts`.
*/ */
export default function GoogleSignInButton({ returnTo }: Props) { export default function GoogleSignInButton({ returnTo, intent = 'sign-in' }: Props) {
return ( return (
<Button <Button
block block
@@ -62,7 +73,7 @@ export default function GoogleSignInButton({ returnTo }: Props) {
window.location.assign(`/api/auth/google/start?returnTo=${encodeURIComponent(returnTo)}`); window.location.assign(`/api/auth/google/start?returnTo=${encodeURIComponent(returnTo)}`);
}} }}
> >
Sign in with Google {intent === 'sign-up' ? 'Sign up with Google' : 'Sign in with Google'}
</Button> </Button>
); );
} }
+17
View File
@@ -124,6 +124,23 @@ test.describe('Customer accounts', () => {
// So the button being ABSENT is the behaviour under test here, and it is the // So the button being ABSENT is the behaviour under test here, and it is the
// one that matters: a control that appears and then fails at Google is worse // one that matters: a control that appears and then fails at Google is worse
// than one that was never offered. // than one that was never offered.
// The sign-up tab, which #345 left it off entirely. A passkey belongs only on
// Log In, because you cannot register an account with one — but creating an
// account is exactly what a new customer reaches for Google to do, so its
// absence there hid the feature from the people it helps most.
//
// Asserted as absent for the same reason as the login one: local and QA have
// no credentials, so absence is the behaviour that actually runs here.
test('offers no Google button on the sign-up tab either, when unconfigured', async ({
authModal
}) => {
await authModal.gotoRegister();
await expect(
authModal.registerDialog.getByRole('button', { name: /with Google/i })
).toHaveCount(0);
});
test('offers no Google button when the environment is not configured for it', async ({ test('offers no Google button when the environment is not configured for it', async ({
authModal, authModal,
accountModal, accountModal,