-
Notifications
You must be signed in to change notification settings - Fork 119
Daily RC #503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Daily RC #503
Changes from all commits
72ea0a5
f29d0cd
90ba759
79d3c58
1fd8e4a
e45e934
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import TenantCIAMEmailVerification, { | |
| import { otp6 } from "@/lib/crypto/otp"; | ||
| import { service_role } from "@/lib/supabase/server"; | ||
| import { select_lang } from "@/i18n/utils"; | ||
| import { getLocale } from "@/i18n/server"; | ||
| import { | ||
| challengeEmailStateKey, | ||
| loadChallengeEmailContext, | ||
|
|
@@ -153,12 +154,14 @@ export async function POST( | |
| : undefined; | ||
| const brand_support_contact = publisher.includes("@") ? publisher : undefined; | ||
|
|
||
| const langCandidate = | ||
| www && typeof www.lang === "string" && www.lang ? www.lang : formDoc.lang; | ||
| const emailLang: CIAMVerificationEmailLang = select_lang( | ||
| langCandidate, | ||
| supported_languages, | ||
| "en" | ||
| // Prefer the per-form document language when set; otherwise fall back to tenant/published `www.lang`. | ||
| // (Treat empty strings as "unset".) | ||
| const langCandidate = formDoc.lang?.trim() || www?.lang?.trim() || null; | ||
| // Prefer the visitor's device language. If unsupported, fall back to the form/tenant default. | ||
| const fallback_lang = select_lang(langCandidate, supported_languages, "en"); | ||
| const emailLang: CIAMVerificationEmailLang = await getLocale( | ||
| [...supported_languages], | ||
| fallback_lang | ||
| ); | ||
| const { error: resend_err } = await resend.emails.send({ | ||
| from: `${brand_name} <[email protected]>`, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,66 @@ | ||
| import React from "react"; | ||
| import PortalLogin from "./login"; | ||
| import { getLocale } from "@/i18n/server"; | ||
| import Link from "next/link"; | ||
| import { createWWWClient } from "@/lib/supabase/server"; | ||
| import type { Database } from "@app/database"; | ||
|
|
||
| export default async function CustomerPortalLoginPage() { | ||
| type Params = { | ||
| tenant: string; | ||
| }; | ||
|
|
||
| type WwwPublicRow = Database["grida_www"]["Views"]["www_public"]["Row"]; | ||
|
|
||
| async function fetchPortalTitle(tenant: string) { | ||
| const client = await createWWWClient(); | ||
|
|
||
| const { data: wwwPublic } = await client | ||
| .from("www_public") | ||
| .select("title") | ||
| .eq("name", tenant) | ||
| .single() | ||
| .returns<Pick<WwwPublicRow, "title">>(); | ||
|
|
||
| const title = | ||
| typeof wwwPublic?.title === "string" && wwwPublic.title.trim() | ||
| ? wwwPublic.title | ||
| : "Customer Portal"; | ||
|
|
||
| return title; | ||
| } | ||
|
|
||
| export default async function CustomerPortalLoginPage({ | ||
| params, | ||
| }: { | ||
| params: Promise<Params>; | ||
| }) { | ||
| const { tenant } = await params; | ||
| const locale = await getLocale(["en", "ko"]); | ||
| const title = await fetchPortalTitle(tenant); | ||
|
|
||
| return ( | ||
| <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-6 md:p-10"> | ||
| <div className="w-full max-w-sm"> | ||
| <PortalLogin locale={locale} /> | ||
| </div> | ||
| <div className="min-h-svh bg-background flex flex-col"> | ||
| <header className="h-14 border-b" data-testid="portal-login-navbar"> | ||
| <div className="mx-auto h-full w-full max-w-screen-sm px-6 flex items-center"> | ||
| <Link | ||
| href="#" | ||
| className="flex items-center gap-2 min-w-0" | ||
| data-testid="portal-login-navbar-brand-link" | ||
| > | ||
| <span | ||
| className="font-semibold truncate" | ||
| data-testid="portal-login-navbar-title" | ||
| > | ||
| {title} | ||
| </span> | ||
| </Link> | ||
| </div> | ||
| </header> | ||
| <main className="flex-1 flex items-center justify-center p-6 md:p-10"> | ||
| <div className="w-full max-w-sm"> | ||
| <PortalLogin locale={locale} /> | ||
| </div> | ||
| </main> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,12 @@ import type { Database } from "@app/database"; | |
| import type { PostgrestError } from "@supabase/supabase-js"; | ||
| import CampaignReferrerCard from "../west-campaign-referrer-card"; | ||
|
|
||
| // TODO(portal): The portal session page is not design-complete yet, but is used in production. | ||
| // Today enterprise customers typically only have one active campaign, so we temporarily redirect | ||
| // directly to the campaign page when there's only a single campaign in the portal session. | ||
| // Remove this once the session page UX is finalized and multi-campaign is common. | ||
| const TMP_SHOULD_REDIRECT_WHEN_SINGLE = true; | ||
|
|
||
| type Params = { | ||
| token: string; | ||
| }; | ||
|
|
@@ -124,6 +130,20 @@ export default async function CustomerPortalSessionPage({ | |
| console.error("[ciam]/referrer error", referrer_err); | ||
| } | ||
|
|
||
| if ( | ||
| TMP_SHOULD_REDIRECT_WHEN_SINGLE && | ||
| iam_referrers && | ||
| iam_referrers.length === 1 | ||
| ) { | ||
| const r = iam_referrers[0]; | ||
| const path = r?.campaign?.www_route_path; | ||
| const code = r?.code; | ||
| if (typeof path === "string" && path && typeof code === "string" && code) { | ||
| // FIXME: tenant url | ||
| return redirect(`${path}/t/${code}`); | ||
| } | ||
| } | ||
|
Comment on lines
+133
to
+145
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's look at the actual file and the context around lines 133-145
cat -n "editor/app/(tenant)/~/[tenant]/(p)/p/session/[token]/page.tsx" | sed -n '120,160p'Repository: gridaco/grida Length of output: 1506 🏁 Script executed: # Get the full file size to understand what we're working with
wc -l "editor/app/(tenant)/~/[tenant]/(p)/p/session/[token]/page.tsx"Repository: gridaco/grida Length of output: 122 🏁 Script executed: # Search for type definitions related to 'campaign' or 'www_route_path'
rg -i "www_route_path|campaign" --type ts --type tsx -A 2 -B 2 | head -100Repository: gridaco/grida Length of output: 84 🏁 Script executed: # Search for the iam_referrers variable source and its type
rg -i "iam_referrers" --type ts --type tsx -B 3 -A 3 | head -80Repository: gridaco/grida Length of output: 84 🏁 Script executed: # Look for existing URL/path validation patterns in the codebase
rg "(startsWith|redirect|protocol|absolute)" --type ts --type tsx -B 1 -A 1 | grep -i "path\|url\|redirect" | head -50Repository: gridaco/grida Length of output: 84 🏁 Script executed: # Search without type filter for www_route_path and campaign type info
rg "www_route_path" -A 2 -B 2Repository: gridaco/grida Length of output: 5231 🏁 Script executed: # Look for type definitions or interfaces related to campaign
rg "campaign" --type ts -A 2 -B 1 | head -150Repository: gridaco/grida Length of output: 10853 🏁 Script executed: # Search for supabase schema or type definitions
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.sql" \) | xargs grep -l "campaign_public" | head -10Repository: gridaco/grida Length of output: 576 🏁 Script executed: # Look for redirect usage and URL validation patterns throughout codebase
rg "redirect\(" -A 1 -B 1 | grep -A 1 -B 1 "startsWith\|url\|path" | head -60Repository: gridaco/grida Length of output: 2196 🏁 Script executed: # Search for URL validation helpers or utility functions
find . -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "isSafeUrl\|validateUrl\|sanitizeUrl" | head -10Repository: gridaco/grida Length of output: 94 🏁 Script executed: # Check if there are other usages of www_route_path in the session page that need attention
sed -n '160,175p' "editor/app/(tenant)/~/[tenant]/(p)/p/session/[token]/page.tsx"Repository: gridaco/grida Length of output: 534 🏁 Script executed: # Look at the schema definition for route_path to see if there are any constraints
rg "route_path" supabase/schemas/ supabase/migrations/ -A 2 -B 2Repository: gridaco/grida Length of output: 2425 Validate The Suggested guard- if (typeof path === "string" && path && typeof code === "string" && code) {
+ const isSafePath =
+ typeof path === "string" &&
+ path &&
+ path.startsWith("/") &&
+ !path.startsWith("//") &&
+ !path.includes("://");
+ if (isSafePath && typeof code === "string" && code) {
// FIXME: tenant url
return redirect(`${path}/t/${code}`);
}Note: The same vulnerable pattern appears in the JSX at line 169 ( 🤖 Prompt for AI Agents |
||
|
|
||
| console.info("[ciam]/portal session redeemed", { | ||
| tokenPreview, | ||
| tokenLength: token.length, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import TenantCIAMEmailVerification, { | |
| } from "@/theme/templates-email/ciam-verifiaction/default"; | ||
| import { otp6 } from "@/lib/crypto/otp"; | ||
| import { select_lang } from "@/i18n/utils"; | ||
| import { getLocale } from "@/i18n/server"; | ||
|
|
||
| /** | ||
| * POST /api/ciam/auth/challenge/with-email | ||
|
|
@@ -118,10 +119,11 @@ export async function POST( | |
| ? publisher | ||
| : undefined; | ||
|
|
||
| const emailLang: CIAMVerificationEmailLang = select_lang( | ||
| www.lang, | ||
| supported_languages, | ||
| "en" | ||
| // Prefer the visitor's device language. If unsupported, fall back to the tenant default. | ||
| const fallback_lang = select_lang(www.lang, supported_languages, "en"); | ||
| const emailLang: CIAMVerificationEmailLang = await getLocale( | ||
| [...supported_languages], | ||
| fallback_lang | ||
| ); | ||
| const { error: resend_err } = await resend.emails.send({ | ||
| from: `${brand_name} <[email protected]>`, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: gridaco/grida
Length of output: 2353
🌐 Web query:
Next.js Link component href="#" accessibility screen reader issues💡 Result:
Short answer: using href="#" (or an anchor with no real destination) inside a Next.js causes accessibility and focus/announcement problems for screen-reader and keyboard users. Prefer a real fragment (href="#id") or an actual URL for navigation; use a (not a link) for JS actions; and ensure semantic/proper attributes (aria-label, aria-disabled, id, passHref when needed). Sources and quick fixes below.
Why it's a problem
Recommended fixes (concise)
- If you need to jump to an element on the page, use a real fragment linking to an element id:
-
Skip to content. This will render and is announced/handled correctly. [2]
- If the UI element triggers a JS action (open modal, toggle) and is not navigation, use a
(with aria attributes) instead of a link:
Open. Buttons expose correct semantics to AT. [1][3]References
If you want, tell me the exact code you're using and I’ll show the minimal fix.
Citations:
🌐 Web query:
Next.js Link best practices when there's no destination href empty string💡 Result:
Short answer — don't render a Next.js with an empty/undefined href. Instead conditionally render a real link only when you have a destination; otherwise render a semantic non-link control (button, span with role="button", or an anchor with aria-disabled and no href) and handle click behavior/accessibility explicitly.
Concrete best practices
References
Would you like a short code example showing the conditional rendering + accessible alternative?
Replace decorative
Linkwith<div>to fix accessibility.Using
href="#"creates a non-functional anchor that confuses screen readers and keyboard navigation. Since this is a brand element with no navigation target or action, use a plain<div>instead.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents