Commit Graph

22 Commits

Author SHA1 Message Date
8b3fbab7a0 fix(deploy): resolve the API base at request time, and stop shipping a secret
The Dokploy build failed at "Collecting page data for /api/assistant" with
"LOYALY_API_BASE is required in production". The guard was right; where it ran
was not.

  const BASE = resolveBase();   // evaluated on import

next build imports every route module to collect page data, the Docker builder
stage sets NODE_ENV=production, and LOYALY_API_BASE is a RUNTIME value that is
not present while an image is being built. So the check fired against the build
instead of against a misconfigured server. The previous comment claimed this
matched tokenStore's stance on AUTH_SECRET; it did not - tokenStore.key() is a
function, called per use. It is now genuinely the same shape: resolved on first
use and memoised, so building needs no platform host and serving still refuses
to guess one.

The value is also validated rather than merely present. It used to accept any
string, so platform.loyaly.ai - which serves THIS console, not the API - was
taken and only failed later as a contract_mismatch at first login. Production
is now an allowlist of exactly one origin, and the known-wrong host is rejected
everywhere with the reason attached, because "rejected" alone sends somebody
hunting for a firewall when the fix is one word in a variable. Development
stays permissive (LAN, tunnel, container host) minus that same host - nothing a
dev machine reaches is production.

  production   https://mcp.loyaly.ai only; missing, http://, platform.loyaly.ai,
               any other origin, a bare hostname and a non-http scheme all throw
  development  loopback and friends, or unset -> http://127.0.0.1:8088

An invalid or missing value is never cached, so a misconfigured process fails
identically on every request rather than once and then differently.

AUTH_SECRET is no longer an ENV line in the Dockerfile. A session-signing key
in git means anyone who can read the repo can forge a cookie for any user, and
every built image carried it in a layer `docker history` will print; Docker's
own linter flags the pattern. Both AUTH_SECRET and LOYALY_API_BASE are now
supplied by the orchestrator at runtime, and the file says so.

  ROTATE the old AUTH_SECRET - it remains in this repository's history.

Verified: docker build --no-cache with neither variable set compiles, passes
TypeScript and collects page data. The built container starts without them,
serves /login, and answers the first API call with the configuration error
naming the variable. With LOYALY_API_BASE set it reaches the real platform.
tsc clean; lint unchanged at the existing baseline.

REQUIRED in Dokploy before this deploys:
  LOYALY_API_BASE=https://mcp.loyaly.ai
  AUTH_SECRET=<openssl rand -base64 48>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161AMotQ8FxGPZ9gFGb5wiK
2026-09-17 16:41:32 +05:30
759f3b79fd fix(config): require a real API base, ship the env template, show the real team
Three configuration defects and one screen of invented people.

API base. The client defaulted to https://platform.loyaly.ai when
LOYALY_API_BASE was unset, and that host serves THIS console, not the
Behavision API - verified live: it answers /api/auth/me with the console's
own 404 HTML and a login POST with the console's own BFF envelope. So an
unset variable in production made the BFF call its own origin, which fails
looking like a broken login form rather than a misconfiguration. There is
now no remote fallback: development defaults to 127.0.0.1:8088 and
production throws, naming the variable, the way tokenStore already refuses
to run without AUTH_SECRET. A wrong host that appears to work is worse than
a startup failure that says what is missing.

The template. .env.example documented that same wrong host, and .gitignore's
`.env*` matched the template itself, so it was never committed - a fresh
clone got no template at all, for an app that cannot start in production
without AUTH_SECRET. Added `!.env.example` after the ignore rule; .env.local
and every other .env* stay ignored. The template carries placeholders only,
no values.

Team. /settings/team listed five invented people - aravind@nearle.in,
Vikram Seth, Priya Sharma - with store names no endpoint supplies and roles
that do not exist upstream, behind four controls that mutated local state
and were lost on refresh. A merchant could not tell any of it from the real
thing. It now reads GET /api/team, which the platform already serves and
scopes by session, and renders what actually comes back: name, email, role,
whether the account is still active, and last sign-in (or "Never", which is
a fact worth seeing).

The route used to map each row through toAuthUser, which reads client_name -
a field GET /api/team does not send - so organisation was undefined on every
row while active, last_login_at and created_at were discarded. ApiTeamMember
now describes that payload properly and ApiUser is left to authentication.

The screen is READ-ONLY on purpose. Accounts are born from invitations, and
that flow already exists in the platform's own web app: a manager mints a
code, the holder redeems it and chooses their own password. A second way to
create a login does not belong here, least of all on the screen that lists
them. Role changes and deactivation are supported upstream by
PATCH /api/team/{id} and are deliberately not wired: deactivating revokes
every session that person holds immediately, so it wants a confirmation step
and 409 last_owner handling, neither of which belongs in a change whose
purpose is removing invented data.

types.ts also gains the Sales/Floor/Customer interfaces. They are inert here
- nothing imports them yet - and land with this commit so the screens that
consume them arrive as one reviewable change.

Verified against the live local platform: two tenants, correct member lists
for each, and no cross-tenant leakage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161AMotQ8FxGPZ9gFGb5wiK
2026-09-17 12:05:41 +05:30
697b0d9ef2 feat(auth): unify login flow and update login visuals
One sign-in page for everyone. /login is the only entry point; the
short-lived /admin/login and /staff/login routes are gone, along with the
per-route branding that came with them. Admin, owner, manager and staff see
the same form, post the same {email, password} to the same
POST /api/auth/login, and are never asked to say who they are.

Where somebody lands is decided by the role the BACKEND returns, never by
the URL they arrived at:

  owner, manager -> /dashboard
  staff          -> /floor

roleDestination is the single map, read by all three redirect paths - the
hydrated form, the no-JavaScript form POST, and GuestGuard. GuestGuard
raced the form to a hardcoded /dashboard, so a staff member landed in a
different place depending on which effect fired first; it now resolves
through the same map.

A platform admin authenticates correctly and still gets no session here.
Every surface in this console is tenant-scoped and an admin has no tenant
(auth.go: "ClientID empty means a platform admin"). Measured against a real
admin token: /api/sites 500, /api/visits 500, /api/visitors 500, /api/team
403 "This account does not belong to a company." So the BFF declines to set
the cookie rather than handing out a dashboard of server errors, revokes the
upstream session it will not use, and says so on /login through the existing
fixed-code table. Their surface is Companies in the platform's own web app,
which this console does not link to and does not hand a token - no session
handoff exists between the two, and inventing one would mean putting a
credential in a URL.

No enumeration is given up: a wrong password for an admin is answered
exactly like every other wrong password, so the "wrong console" message only
ever reaches somebody who has already proved they own the account.

Login visuals: the hero carousel now anchors each slide independently -
slide 1 (mascot with bag) to the bottom so the white bag clears the white
caption, slide 2 (selfie booth) to the top so the arch and wordmark are not
cropped by the rounded corner.

Unchanged: the BFF, the sealed httpOnly token cookie, the signed session
cookie, refresh, logout, route protection and the open-redirect guard on
?next=.

Verified against the live local platform with real accounts for all four
roles, plus wrong-password, unknown-email, empty-field, invalid-format and
inactive-user cases, session persistence, a forced token refresh, logout,
and two-tenant isolation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161AMotQ8FxGPZ9gFGb5wiK
2026-09-16 15:45:17 +05:30
d2e090214a update the api 2026-09-10 19:25:07 +05:30
bfe4d3d2f2 update ui and remove mock data 2026-09-09 19:50:52 +05:30
5ce580dced ligth theme design fix 2026-08-10 15:33:13 +05:30
6cc3c9a0b7 update dark and ligth theme 2026-08-10 12:35:18 +05:30
f397e94418 Fix login 500: set AUTH_SECRET, revert API base override
sessionToken.ts throws when AUTH_SECRET is unset under NODE_ENV=production,
so /api/auth/login answered 500 on valid credentials while still returning
401/400 correctly on bad ones. Verified against platform.loyaly.ai, whose
responses match that signature exactly.

Also reverts NEXT_PUBLIC_API_BASE. platform.loyaly.ai is this same app
already deployed (identical /login markup), so the override pointed the
app at itself cross-origin, and that endpoint returns no CORS headers.

Verified on the production build: valid credentials 200 + session cookie,
wrong password 401, malformed 400, and the cookie opens /dashboard,
/settings, /stores and /api/stores while an unauthenticated request still
gets 307/401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:20:28 +05:30
450fa0d6a0 Point API base at platform.loyaly.ai
Set NEXT_PUBLIC_API_BASE in the builder stage so login and every other
httpClient call target the platform backend instead of the local mock
route handlers.

Set in the Dockerfile rather than Dokploy because NEXT_PUBLIC_* is
inlined into the client bundle at build time — a runtime env var has no
effect. Origin only, since authRepository appends /api/auth/login.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:12:08 +05:30
0ea52551aa build size reduse it 2026-08-06 20:59:19 +05:30
28258b5a69 fix docker port 2026-08-06 19:26:30 +05:30
f80c1d9f5d fix sales issue 2026-08-06 18:13:59 +05:30
32847159ae update sales page 2026-08-06 17:46:17 +05:30
469ad3021c fix mobile issue 2026-08-06 17:09:08 +05:30
cec6f0f412 change names for dashboard 2026-08-06 16:37:31 +05:30
11daadc0a3 comment voice icon 2026-08-06 16:13:58 +05:30
b23ea760c8 fix chatbox 2026-08-06 15:43:46 +05:30
e5f8144fb3 update ui update and fix layout issue 2026-08-06 13:21:10 +05:30
92322bff16 login page logo fix 2026-08-05 18:43:07 +05:30
9d7bbad32c dashboard design changes 2026-08-05 18:34:37 +05:30
2c8c394795 first commit 2026-08-05 15:26:50 +05:30
c000e5dc95 Initial commit from Create Next App 2026-08-05 12:41:28 +05:30