From b706db6e8b06c6f91e90c2823d693e5e0c517474 Mon Sep 17 00:00:00 2001 From: sunshinexcode <24xinhui@163.com> Date: Tue, 7 Jul 2026 14:52:43 +0800 Subject: [PATCH 1/3] feat(auth): add branded OAuth callback pages --- internal/cli/auth.go | 20 +++-- internal/cli/oauth_pages.go | 149 +++++++++++++++++++++++++++++++ internal/cli/oauth_pages_test.go | 70 +++++++++++++++ 3 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 internal/cli/oauth_pages.go create mode 100644 internal/cli/oauth_pages_test.go diff --git a/internal/cli/auth.go b/internal/cli/auth.go index 46bae71..8e8863d 100644 --- a/internal/cli/auth.go +++ b/internal/cli/auth.go @@ -53,7 +53,9 @@ func (a *App) login(noBrowser bool, region string, progress progressEmitter) (ma } timeout = time.Duration(parsed) * time.Millisecond } - callback, err := waitForOAuthCallback(state, timeout) + callback, err := waitForOAuthCallbackWithPage(state, timeout, callbackPageConfig{ + Region: loginRegion, + }) if err != nil { return nil, err } @@ -284,6 +286,10 @@ type callbackPayload struct { } func waitForOAuthCallback(expectedState string, timeout time.Duration) (*callbackServer, error) { + return waitForOAuthCallbackWithPage(expectedState, timeout, callbackPageConfig{}) +} + +func waitForOAuthCallbackWithPage(expectedState string, timeout time.Duration, page callbackPageConfig) (*callbackServer, error) { wait := make(chan callbackPayload, 1) errs := make(chan error, 1) mux := http.NewServeMux() @@ -313,18 +319,22 @@ func waitForOAuthCallback(expectedState string, timeout time.Duration) (*callbac code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") oauthErr := r.URL.Query().Get("error") + w.Header().Set("Content-Type", "text/html; charset=utf-8") switch { case oauthErr != "": - http.Error(w, "Agora CLI login failed. Return to the terminal for details.", http.StatusBadRequest) + w.WriteHeader(http.StatusBadRequest) + _, _ = io.WriteString(w, renderOAuthCallbackErrorPage(page, "Agora CLI login failed", "Return to the terminal for details.")) errs <- fmt.Errorf("OAuth authorization failed: %s", oauthErr) case code == "" || state == "": - http.Error(w, "Agora CLI login callback was missing required fields.", http.StatusBadRequest) + w.WriteHeader(http.StatusBadRequest) + _, _ = io.WriteString(w, renderOAuthCallbackErrorPage(page, "Missing callback fields", "The OAuth callback did not include the required code and state fields.")) case state != expectedState: - http.Error(w, "Agora CLI login state mismatch.", http.StatusBadRequest) + w.WriteHeader(http.StatusBadRequest) + _, _ = io.WriteString(w, renderOAuthCallbackErrorPage(page, "Login state mismatch", "Return to the terminal and restart login to protect this session.")) errs <- errors.New("OAuth state mismatch.") default: w.WriteHeader(http.StatusOK) - _, _ = io.WriteString(w, `Agora CLI Login Complete

Agora CLI login complete

You can close this browser window and return to your terminal.

`) + _, _ = io.WriteString(w, renderOAuthCallbackSuccessPage(page)) wait <- callbackPayload{Code: code, State: state} } }) diff --git a/internal/cli/oauth_pages.go b/internal/cli/oauth_pages.go new file mode 100644 index 0000000..ca405af --- /dev/null +++ b/internal/cli/oauth_pages.go @@ -0,0 +1,149 @@ +package cli + +import ( + "html" + "strings" +) + +// callbackPageConfig carries the login region used to brand the browser callback page. +type callbackPageConfig struct { + Region string +} + +// loginPageContent contains the region-specific copy rendered on the OAuth callback page. +type loginPageContent struct { + PageClass string + Title string + Message string + ActionLabel string + PrimaryAction string + Safety string +} + +// renderOAuthCallbackSuccessPage renders the browser page shown after a successful OAuth callback. +func renderOAuthCallbackSuccessPage(config callbackPageConfig) string { + content := loginPageContentForRegion(config.Region) + return renderOAuthCallbackPage(content, false, "", "") +} + +// renderOAuthCallbackErrorPage renders a branded browser page for OAuth callback errors. +func renderOAuthCallbackErrorPage(config callbackPageConfig, title, message string) string { + content := loginPageContentForRegion(config.Region) + return renderOAuthCallbackPage(content, true, title, message) +} + +// loginPageContentForRegion returns the final login-page copy for the active control-plane region. +func loginPageContentForRegion(region string) loginPageContent { + if normalizeContextRegion(region) == regionCN { + return loginPageContent{ + PageClass: "shengwang-page", + Title: "你已成功登录声网 CLI", + Message: "此浏览器步骤已完成。CLI 现在可以将此账号作为当前本地配置继续使用。", + ActionLabel: "回到终端后确认当前登录状态。", + PrimaryAction: "agora auth status", + Safety: "你可以关闭此页面,回到终端继续操作。", + } + } + + return loginPageContent{ + PageClass: "agora-page", + Title: "You are now authenticated with Agora CLI", + Message: "This browser step completed successfully. The CLI can now use this account as your active local configuration.", + ActionLabel: "Return to your terminal and confirm the active account.", + PrimaryAction: "agora auth status", + Safety: "You can close this window and return to your terminal.", + } +} + +// renderOAuthCallbackPage builds the complete OAuth callback HTML document. +func renderOAuthCallbackPage(content loginPageContent, isError bool, errorTitle, errorMessage string) string { + title := content.Title + message := content.Message + if isError { + title = valueOrDefault(errorTitle, "Login could not be completed") + message = valueOrDefault(errorMessage, "Return to your terminal for details.") + } + + var b strings.Builder + b.WriteString(`Agora CLI Login
`) + b.WriteString(brandLogoHTML(content.PageClass)) + b.WriteString(`CLI

`) + b.WriteString(escapeText(title)) + b.WriteString(`

`) + b.WriteString(escapeText(message)) + b.WriteString(`

`) + if !isError { + b.WriteString(``) + } + b.WriteString(`

`) + b.WriteString(escapeText(content.Safety)) + b.WriteString(`

`) + + return b.String() +} + +// loginPageCSS returns the shared CSS for the final global and China login pages. +func loginPageCSS() string { + return ` +*{box-sizing:border-box} +body{margin:0;min-height:100vh;font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} +.page{min-height:100vh;display:grid;place-items:center;padding:90px 28px 48px;position:relative;overflow:hidden;color:var(--ink);background:var(--bg)} +.page:before{content:"";position:absolute;inset:0;pointer-events:none} +.card{width:min(900px,100%);position:relative;z-index:2;min-height:auto;padding:42px;border:1px solid var(--line);border-radius:30px;background:var(--panel);box-shadow:var(--shadow);backdrop-filter:blur(18px)} +.brand{display:inline-flex;align-items:center;gap:10px;color:var(--ink);font-size:20px;font-weight:950;letter-spacing:-.02em} +.brand-logo{display:inline-flex;align-items:center;flex:0 0 auto} +.brand-logo svg{display:block;width:100%;height:100%} +.brand-logo img{display:block;width:100%;height:auto} +.brand-logo-agora{width:86px;max-height:34px} +.brand-logo-cn{justify-content:center;width:58px;height:58px;border-radius:16px;color:#fff;background:radial-gradient(circle at 22% 18%,rgba(255,255,255,.32),transparent 26%),linear-gradient(135deg,#1469ff 0%,#0f8fff 48%,#16c7b7 100%);box-shadow:0 16px 36px rgba(20,105,255,.22)} +.brand-logo-cn svg{width:42px;height:auto} +.hero{margin-top:54px;max-width:880px} +h1{margin:0;max-width:820px;font-size:clamp(34px,3.8vw,54px);line-height:1.12;letter-spacing:-.045em} +.message{max-width:780px;margin:24px 0 0;color:var(--muted);font-size:clamp(15px,1.3vw,18px);line-height:1.7} +.next{display:grid;grid-template-columns:1fr auto;gap:18px;align-items:center;margin-top:34px;padding:15px 18px;border:1px solid var(--next-line);border-radius:18px;background:var(--next-bg);box-shadow:var(--next-shadow)} +.next p{margin:0;color:var(--next-text);font-size:clamp(14px,1.25vw,16px);font-weight:650;letter-spacing:-.02em} +code{padding:0;border-radius:0;background:transparent;color:var(--primary);font:850 clamp(14px,1.25vw,16px) ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap} +.safety{margin:24px 0 0;color:var(--muted);font-size:14px;line-height:1.65} +.is-error .next{display:none} +.agora-page{--ink:#f8fbff;--muted:#a9b8cd;--primary:#35d6a5;--line:rgba(255,255,255,.14);--panel:rgba(9,20,38,.54);--shadow:0 24px 90px rgba(0,0,0,.22);--next-line:rgba(255,255,255,.1);--next-bg:rgba(5,11,20,.72);--next-text:#dbe7ff;--next-shadow:none;--bg:radial-gradient(circle at 12% 12%,rgba(53,214,165,.28),transparent 30%),radial-gradient(circle at 88% 16%,rgba(122,167,255,.22),transparent 32%),radial-gradient(circle at 54% 100%,rgba(62,84,255,.18),transparent 36%),linear-gradient(150deg,#050b14 0%,#101b33 48%,#07111f 100%)} +.agora-page:before{opacity:.2;background-image:linear-gradient(rgba(255,255,255,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.08) 1px,transparent 1px);background-size:44px 44px;mask-image:radial-gradient(circle at 50% 48%,black,transparent 72%)} +.shengwang-page{--ink:#10233f;--muted:#5f6f85;--primary:#1469ff;--line:rgba(24,56,96,.14);--panel:linear-gradient(135deg,rgba(255,255,255,.96),rgba(255,255,255,.78)),radial-gradient(circle at 0% 0%,rgba(20,105,255,.08),transparent 34%);--shadow:0 30px 100px rgba(16,35,63,.16),0 1px 0 rgba(255,255,255,.78) inset;--next-line:rgba(24,56,96,.16);--next-bg:#f8fafc;--next-text:#5f6f85;--next-shadow:0 12px 40px rgba(20,61,112,.06);--bg:radial-gradient(circle at 18% 12%,rgba(20,105,255,.34),transparent 28%),radial-gradient(circle at 85% 78%,rgba(22,199,183,.24),transparent 30%),linear-gradient(135deg,#e8f1ff 0%,#f8fbff 42%,#e8fff9 100%)} +.shengwang-page:before{background:linear-gradient(rgba(10,24,48,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(10,24,48,.045) 1px,transparent 1px),radial-gradient(circle at 50% 0%,rgba(5,8,18,.1),transparent 34%);background-size:44px 44px,44px 44px,auto;opacity:.8;mask-image:radial-gradient(circle at 50% 48%,black,transparent 78%)} +@media (max-width:900px){ + .page{padding-top:72px} + .card{padding:28px} + .next{grid-template-columns:1fr} + code{white-space:normal;word-break:break-word} +} +` +} + +// brandLogoHTML returns the official region-specific brand mark used in the callback page. +func brandLogoHTML(pageClass string) string { + if pageClass == "shengwang-page" { + return `` + } + + return `` +} + +// escapeText escapes user-visible text before it is inserted into HTML. +func escapeText(value string) string { + return html.EscapeString(value) +} + +// escapeAttr escapes attribute values before they are inserted into HTML. +func escapeAttr(value string) string { + return html.EscapeString(value) +} diff --git a/internal/cli/oauth_pages_test.go b/internal/cli/oauth_pages_test.go new file mode 100644 index 0000000..d08ae9d --- /dev/null +++ b/internal/cli/oauth_pages_test.go @@ -0,0 +1,70 @@ +package cli + +import ( + "strings" + "testing" +) + +func TestOAuthCallbackSuccessPageUsesFinalGlobalDesign(t *testing.T) { + html := renderOAuthCallbackSuccessPage(callbackPageConfig{Region: regionGlobal}) + + for _, want := range []string{ + "agora-page", + "Agora%20Logo%20Crisp.webp", + "You are now authenticated with Agora CLI", + "This browser step completed successfully. The CLI can now use this account as your active local configuration.", + "agora auth status", + "You can close this window and return to your terminal.", + } { + if !strings.Contains(html, want) { + t.Fatalf("expected global login page to contain %q", want) + } + } + + for _, forbidden := range []string{"agora whoami", "global-minimal", "global-deck"} { + if strings.Contains(html, forbidden) { + t.Fatalf("global login page should not contain legacy variant content %q", forbidden) + } + } +} + +func TestOAuthCallbackSuccessPageUsesFinalChinaDesign(t *testing.T) { + html := renderOAuthCallbackSuccessPage(callbackPageConfig{Region: regionCN}) + + for _, want := range []string{ + "shengwang-page", + "brand-logo-cn", + "你已成功登录声网 CLI", + "此浏览器步骤已完成。CLI 现在可以将此账号作为当前本地配置继续使用。", + "agora auth status", + "你可以关闭此页面,回到终端继续操作。", + } { + if !strings.Contains(html, want) { + t.Fatalf("expected China login page to contain %q", want) + } + } + + for _, forbidden := range []string{"agora whoami", "cn-console", "cn-silk"} { + if strings.Contains(html, forbidden) { + t.Fatalf("China login page should not contain legacy variant content %q", forbidden) + } + } +} + +func TestOAuthCallbackErrorPageKeepsRegionBranding(t *testing.T) { + html := renderOAuthCallbackErrorPage(callbackPageConfig{Region: regionCN}, "Login state mismatch", "Return to the terminal and restart login.") + + for _, want := range []string{ + "shengwang-page is-error", + "Login state mismatch", + "Return to the terminal and restart login.", + } { + if !strings.Contains(html, want) { + t.Fatalf("expected error login page to contain %q", want) + } + } + + if strings.Contains(html, "agora auth status") { + t.Fatal("error login page should not show success action command") + } +} From 4c69e17f632471f8224e71090e9ec77c8aa8d36f Mon Sep 17 00:00:00 2001 From: sunshinexcode <24xinhui@163.com> Date: Tue, 7 Jul 2026 15:08:39 +0800 Subject: [PATCH 2/3] fix(oauth): isolate callback page branding by region --- internal/cli/oauth_pages.go | 58 ++++++++++++++++++-------------- internal/cli/oauth_pages_test.go | 22 +++++++++++- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/internal/cli/oauth_pages.go b/internal/cli/oauth_pages.go index ca405af..7ac4e18 100644 --- a/internal/cli/oauth_pages.go +++ b/internal/cli/oauth_pages.go @@ -66,7 +66,7 @@ func renderOAuthCallbackPage(content loginPageContent, isError bool, errorTitle, var b strings.Builder b.WriteString(`Agora CLI Login
Date: Tue, 7 Jul 2026 15:29:05 +0800 Subject: [PATCH 3/3] fix(auth): tune OAuth login page branding --- internal/cli/oauth_pages.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cli/oauth_pages.go b/internal/cli/oauth_pages.go index 7ac4e18..e60ea3d 100644 --- a/internal/cli/oauth_pages.go +++ b/internal/cli/oauth_pages.go @@ -101,7 +101,7 @@ func loginPageCSS(pageClass string) string { body{margin:0;min-height:100vh;font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased} .page{min-height:100vh;display:grid;place-items:center;padding:64px 24px;color:var(--ink);background:var(--bg)} .card{width:min(760px,100%);padding:40px;border:1px solid var(--line);border-radius:20px;background:var(--panel);box-shadow:var(--shadow)} -.brand{display:inline-flex;align-items:center;gap:10px;color:var(--ink);font-size:16px;font-weight:700;letter-spacing:-.01em} +.brand{display:inline-flex;align-items:center;gap:10px;color:var(--ink);font-size:22px;font-weight:800;letter-spacing:-.01em} .brand-logo{display:inline-flex;align-items:center;flex:0 0 auto} .brand-logo svg{display:block;width:100%;height:100%} .brand-logo img{display:block;width:100%;height:auto} @@ -124,9 +124,9 @@ code{padding:5px 9px;border:1px solid var(--code-line);border-radius:8px;backgro if pageClass == "shengwang-page" { b.WriteString(` -.shengwang-page{--ink:#152033;--muted:#617083;--primary:#1469ff;--brand:#1469ff;--line:#dce7f5;--panel:#fff;--shadow:0 20px 55px rgba(21,45,85,.1);--next-line:#dce7f5;--next-bg:#f7fbff;--next-text:#46576c;--code-line:#cfe0f8;--code-bg:#eef6ff;--bg:linear-gradient(180deg,#f6faff 0%,#fff 58%,#f9fbfd 100%)} -.brand-logo-cn{width:88px;height:auto;color:var(--brand)} -.brand-logo-cn svg{width:88px;height:auto} +.shengwang-page{--ink:#152033;--muted:#617083;--primary:#0b9dfd;--brand:#0b9dfd;--line:#dce7f5;--panel:#fff;--shadow:0 20px 55px rgba(21,45,85,.1);--next-line:#dce7f5;--next-bg:#f7fbff;--next-text:#46576c;--code-line:#cfe0f8;--code-bg:#eef6ff;--bg:linear-gradient(180deg,#f6faff 0%,#fff 58%,#f9fbfd 100%)} +.brand-logo-cn{width:58px;height:auto;color:var(--brand)} +.brand-logo-cn svg{width:58px;height:auto} `) return b.String() }