Compare commits

...

12 Commits

Author SHA1 Message Date
Peter Steinberger
27a4186d93 docs: update changelog for auth ui
Some checks failed
ci / test (push) Has been cancelled
2025-12-31 13:18:21 +01:00
Peter Steinberger
43013b755b chore(auth): document auth flow wait 2025-12-31 13:18:01 +01:00
salmonumbrella
891067236d
docs(auth): document login as alias for manage command
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 18:16:28 -08:00
salmonumbrella
f79c6303b0
test(auth): add test for context cancellation in waitPostSuccess
Extract inline select block into waitPostSuccess function and add
unit tests verifying context cancellation behavior for Ctrl+C support.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 18:11:54 -08:00
salmonumbrella
0c8ace4e7b
refactor(auth): standardize sync comment format
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 18:07:52 -08:00
salmonumbrella
9062e754b1
fix(auth): inject countdown value via template to eliminate sync requirement
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 18:03:57 -08:00
salmonumbrella
33ebb68210
refactor(auth): consolidate success templates into one
- Merge success_new.html content/layout into success.html
- Add animated gradient orbs from original success.html
- Use Go template conditionals to handle both cases:
  - With email/services (account manager flow)
  - Without email (simple OAuth flow)
- Delete redundant success_new.html
- Rename renderSuccessPageNew to renderSuccessPageWithDetails
- Update tests to cover both template modes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 17:15:24 -08:00
salmonumbrella
199407ac03
refactor(auth): deduplicate GitHub icon SVG via CSS mask
Replace inline SVG icons with a reusable `.icon-github` CSS class
that uses mask-image with a data URI. Each template now includes
a comment noting the other files to update if the icon changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:55:08 -08:00
salmonumbrella
9c9b8c8ff9
refactor(auth): extract magic number 30 as postSuccessDisplaySeconds constant
Add a named constant for the 30-second display delay after OAuth success.
This keeps the Go code and HTML templates in sync, with comments in the
HTML pointing to the authoritative constant definition in oauth_flow.go.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:50:23 -08:00
salmonumbrella
5373d7e028
fix(auth): add countdown timer to success_new.html
Add the same 30-second countdown JavaScript that exists in success.html
to success_new.html for consistency. The countdown shows "Closing in X
seconds..." and updates to "You can close this window." when complete.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:46:52 -08:00
salmonumbrella
ec6dd959de
refactor(auth): consolidate login and manage commands using Cobra alias
Replace duplicate newAuthLoginCmd() with Cobra alias on newAuthManageCmd().
Both commands had identical RunE logic calling googleauth.StartManageServer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:38:56 -08:00
salmonumbrella
184c3a9726
fix(auth): make post-success sleep cancellable via Ctrl+C
Replace blocking time.Sleep with select statement using time.After
and ctx.Done() so the 30-second post-success display period can be
interrupted by context cancellation (Ctrl+C).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:33:39 -08:00
10 changed files with 547 additions and 768 deletions

View File

@ -8,6 +8,8 @@
### Changed
- Auth: refreshed account manager + success UI (#20) — thanks @salmonumbrella.
## 0.4.0 - 2025-12-26
### Added

View File

@ -501,10 +501,11 @@ func newAuthManageCmd() *cobra.Command {
var timeout time.Duration
cmd := &cobra.Command{
Use: "manage",
Short: "Open accounts manager in browser",
Long: "Opens a browser-based UI to manage Google accounts, add new accounts, set defaults, and remove accounts.",
Args: cobra.NoArgs,
Use: "manage",
Aliases: []string{"login"},
Short: "Open accounts manager in browser",
Long: "Opens a browser-based UI to manage Google accounts, add new accounts, set defaults, and remove accounts.\n\nAlias: 'gog auth login' is equivalent to 'gog auth manage'.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
var services []googleauth.Service
if strings.EqualFold(strings.TrimSpace(servicesCSV), "") || strings.EqualFold(strings.TrimSpace(servicesCSV), "all") {

View File

@ -305,7 +305,7 @@ func (ms *ManageServer) handleOAuthCallback(w http.ResponseWriter, r *http.Reque
// Render success page with the new template
w.WriteHeader(http.StatusOK)
renderSuccessPageNew(w, email, serviceNames)
renderSuccessPageWithDetails(w, email, serviceNames)
}
func (ms *ManageServer) handleSetDefault(w http.ResponseWriter, r *http.Request) {
@ -381,19 +381,17 @@ func writeJSONError(w http.ResponseWriter, msg string, status int) {
_ = json.NewEncoder(w).Encode(map[string]any{"error": msg})
}
// renderSuccessPageNew renders the new success template with email and services
func renderSuccessPageNew(w http.ResponseWriter, email string, services []string) {
tmpl, err := template.New("success").Parse(successTemplateNew)
// renderSuccessPageWithDetails renders the success template with email and services
func renderSuccessPageWithDetails(w http.ResponseWriter, email string, services []string) {
tmpl, err := template.New("success").Parse(successTemplate)
if err != nil {
_, _ = w.Write([]byte("Success! You can close this window."))
return
}
data := struct {
Email string
Services []string
}{
Email: email,
Services: services,
data := successTemplateData{
Email: email,
Services: services,
CountdownSeconds: postSuccessDisplaySeconds,
}
_ = tmpl.Execute(w, data)
}

View File

@ -29,6 +29,17 @@ type AuthorizeOptions struct {
Timeout time.Duration
}
// postSuccessDisplaySeconds is the number of seconds the success page remains
// visible before the local OAuth server shuts down.
const postSuccessDisplaySeconds = 30
// successTemplateData holds data passed to the success page template.
type successTemplateData struct {
Email string
Services []string
CountdownSeconds int
}
var (
readClientCredentials = config.ReadClientCredentials
openBrowserFn = openBrowser
@ -184,14 +195,18 @@ func Authorize(ctx context.Context, opts AuthorizeOptions) (string, error) {
select {
case code := <-codeCh:
_ = srv.Close()
tok, exchangeErr := cfg.Exchange(ctx, code)
if exchangeErr != nil {
_ = srv.Close()
return "", exchangeErr
}
if tok.RefreshToken == "" {
_ = srv.Close()
return "", errors.New("no refresh token received; try again with --force-consent")
}
// Keep server running so CLI blocks until auth flow fully closes (Ctrl+C ok).
waitPostSuccess(ctx, postSuccessDisplaySeconds*time.Second)
_ = srv.Close()
return tok.RefreshToken, nil
case err := <-errCh:
_ = srv.Close()
@ -241,7 +256,10 @@ func renderSuccessPage(w http.ResponseWriter) {
_, _ = w.Write([]byte("Success! You can close this window."))
return
}
_ = tmpl.Execute(w, nil)
data := successTemplateData{
CountdownSeconds: postSuccessDisplaySeconds,
}
_ = tmpl.Execute(w, data)
}
// renderErrorPage renders the error HTML template with the given message
@ -263,3 +281,13 @@ func renderCancelledPage(w http.ResponseWriter) {
}
_ = tmpl.Execute(w, nil)
}
// waitPostSuccess waits for the specified duration or until the context is
// cancelled (e.g., via Ctrl+C). This allows the success page to remain visible
// while still supporting graceful early termination.
func waitPostSuccess(ctx context.Context, d time.Duration) {
select {
case <-time.After(d):
case <-ctx.Done():
}
}

View File

@ -289,6 +289,20 @@
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%2334A853' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11.318 12.545H7.91v-1.909h3.41v1.91zM14.728 0v6h6l-6-6zm1.363 10.636h-3.41v1.91h3.41v-1.91zm0 3.273h-3.41v1.91h3.41v-1.91zM20.727 6.5v15.864c0 .904-.732 1.636-1.636 1.636H4.909a1.636 1.636 0 0 1-1.636-1.636V1.636C3.273.732 4.005 0 4.909 0h9.318v6.5h6.5zm-3.273 2.773H6.545v7.909h10.91v-7.91zm-6.136 4.636H7.91v1.91h3.41v-1.91z'/%3E%3C/svg%3E");
}
.service-tag.people {
color: var(--g-blue);
background-color: var(--g-blue-dim);
border-color: rgba(66, 133, 244, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%234285F4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
}
.service-tag.tasks {
color: var(--g-blue);
background-color: var(--g-blue-dim);
border-color: rgba(66, 133, 244, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%234285F4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M22 5.18L10.59 16.6l-4.24-4.24 1.41-1.41 2.83 2.83 10-10L22 5.18zm-2.21 5.04c.13.57.21 1.17.21 1.78 0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8c1.58 0 3.04.46 4.28 1.25l1.44-1.44A9.9 9.9 0 0012 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10c0-1.19-.22-2.33-.6-3.39l-1.61 1.61z'/%3E%3C/svg%3E");
}
.account-actions {
display: flex;
align-items: center;
@ -547,6 +561,40 @@
opacity: 0.8;
}
.github-link {
margin-top: 12px;
}
.github-link a {
display: inline-flex;
align-items: center;
color: var(--text-muted);
text-decoration: none;
font-size: 12px;
transition: color 0.2s;
}
.github-link a:hover {
color: var(--g-blue);
}
/* GitHub icon - defined as CSS mask for reuse across templates
* SYNC: If modifying the icon, also update success.html */
.icon-github {
display: inline-block;
width: 14px;
height: 14px;
vertical-align: middle;
margin-right: 4px;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'/%3E%3C/svg%3E");
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
}
/* Loading state */
.spinner {
width: 16px;
@ -591,7 +639,7 @@
<h1>Google Accounts</h1>
<p class="subtitle">Manage your connected accounts</p>
<p class="setup-link">
<a href="https://github.com/salmonumbrella/gog-cli?tab=readme-ov-file#setup-oauth" target="_blank">
<a href="https://github.com/steipete/gogcli#quick-start" target="_blank">
First time? Set up Google Cloud credentials →
</a>
</p>
@ -650,6 +698,12 @@
<footer class="footer">
<p>You can close this window and return to your terminal.</p>
<p class="github-link">
<a href="https://github.com/steipete/gogcli" target="_blank">
<span class="icon-github"></span>
View on GitHub
</a>
</p>
</footer>
</div>

View File

@ -6,35 +6,48 @@
<title>Connected - gog</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg-deep: #0a0a0f;
--bg-card: #111118;
--bg-input: #18181f;
--border: #1f1f2e;
--text: #e8e8ed;
--text-muted: #8888a0;
--text-dim: #4a4a5a;
--google-blue: #4285F4;
--google-red: #EA4335;
--google-yellow: #FBBC05;
--google-green: #34A853;
--success: #34A853;
--success-glow: rgba(52, 168, 83, 0.15);
--bg-void: #050508;
--bg-base: #0a0a0f;
--bg-surface: #111116;
--bg-elevated: #18181d;
--bg-hover: #1f1f26;
--border: rgba(255, 255, 255, 0.06);
--border-subtle: rgba(255, 255, 255, 0.04);
--border-active: rgba(255, 255, 255, 0.12);
--text: #f4f4f5;
--text-secondary: #a1a1aa;
--text-muted: #71717a;
--text-dim: #52525b;
--g-blue: #4285F4;
--g-blue-dim: rgba(66, 133, 244, 0.12);
--g-red: #EA4335;
--g-red-dim: rgba(234, 67, 53, 0.12);
--g-yellow: #FBBC05;
--g-yellow-dim: rgba(251, 188, 5, 0.12);
--g-green: #34A853;
--g-green-dim: rgba(52, 168, 83, 0.12);
--radius-sm: 8px;
--radius: 12px;
--radius-lg: 16px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'DM Sans', -apple-system, sans-serif;
background: var(--bg-deep);
font-family: 'Sora', -apple-system, sans-serif;
background: var(--bg-void);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
position: relative;
overflow: hidden;
}
@ -43,12 +56,13 @@
body::before {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
inset: 0;
background-image:
linear-gradient(rgba(66, 133, 244, 0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(66, 133, 244, 0.015) 1px, transparent 1px);
background-size: 80px 80px;
linear-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px);
background-size: 64px 64px;
pointer-events: none;
z-index: 0;
}
/* Animated Google-colored gradient orbs */
@ -64,7 +78,7 @@
.orb-blue {
width: 600px;
height: 600px;
background: var(--google-blue);
background: var(--g-blue);
top: -20%;
left: -10%;
animation-delay: 0s;
@ -73,7 +87,7 @@
.orb-red {
width: 500px;
height: 500px;
background: var(--google-red);
background: var(--g-red);
top: 60%;
right: -15%;
animation-delay: -6s;
@ -82,7 +96,7 @@
.orb-yellow {
width: 400px;
height: 400px;
background: var(--google-yellow);
background: var(--g-yellow);
bottom: -10%;
left: 30%;
animation-delay: -12s;
@ -91,7 +105,7 @@
.orb-green {
width: 450px;
height: 450px;
background: var(--google-green);
background: var(--g-green);
top: 20%;
right: 20%;
animation-delay: -18s;
@ -105,109 +119,155 @@
}
.container {
width: 100%;
max-width: 540px;
max-width: 560px;
margin: 0 auto;
padding: 48px 24px;
position: relative;
z-index: 1;
}
/* Success Header */
.success-header {
text-align: center;
margin-bottom: 40px;
}
/* Google "G" logo with colors */
.google-logo {
width: 72px;
height: 72px;
margin: 0 auto 2rem;
animation: logoReveal 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
filter: drop-shadow(0 8px 32px rgba(66, 133, 244, 0.25));
.success-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
position: relative;
}
@keyframes logoReveal {
from { transform: scale(0) rotate(-180deg); opacity: 0; }
to { transform: scale(1) rotate(0deg); opacity: 1; }
.success-icon svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 8px 32px rgba(66, 133, 244, 0.3));
}
/* Success checkmark ring */
.success-ring {
.success-badge {
position: absolute;
top: -12px;
right: -12px;
width: 32px;
height: 32px;
background: var(--success);
bottom: -4px;
right: -4px;
width: 28px;
height: 28px;
background: var(--g-green);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: ringPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.5s both;
box-shadow: 0 4px 16px rgba(52, 168, 83, 0.4);
box-shadow: 0 0 0 4px var(--bg-void), 0 4px 12px rgba(52, 168, 83, 0.4);
}
.success-ring svg {
width: 18px;
height: 18px;
stroke: white;
stroke-width: 3;
fill: none;
}
@keyframes ringPop {
from { transform: scale(0); }
to { transform: scale(1); }
}
.logo-container {
position: relative;
display: inline-block;
.success-badge svg {
width: 16px;
height: 16px;
color: white;
}
h1 {
font-size: 2.25rem;
font-size: 28px;
font-weight: 700;
letter-spacing: -0.03em;
margin-bottom: 0.625rem;
animation: fadeSlideUp 0.5s ease 0.2s both;
background: linear-gradient(135deg, var(--text) 0%, var(--text-muted) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
}
.subtitle {
color: var(--text-muted);
font-size: 1.0625rem;
margin-bottom: 2.5rem;
animation: fadeSlideUp 0.5s ease 0.3s both;
.account-email {
font-size: 15px;
color: var(--text-secondary);
margin-bottom: 16px;
}
.subtitle strong {
color: var(--google-blue);
.account-email strong {
color: var(--g-blue);
font-weight: 600;
}
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
.services-row {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 20px;
}
/* Terminal window */
.terminal {
background: var(--bg-card);
.service-tag {
font-size: 11px;
font-weight: 500;
letter-spacing: 0.02em;
padding: 4px 10px 4px 24px;
border-radius: 99px;
border: 1px solid;
background-repeat: no-repeat;
background-position: 6px center;
background-size: 14px 14px;
}
.service-tag.calendar {
color: var(--g-blue);
background-color: var(--g-blue-dim);
border-color: rgba(66, 133, 244, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%234285F4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.316 5.684H24v12.632h-5.684V5.684zM5.684 24h12.632v-5.684H5.684V24zM18.316 5.684V0H1.895A1.894 1.894 0 0 0 0 1.895v16.421h5.684V5.684h12.632zM22.105 0h-3.289v5.184H24V1.895A1.894 1.894 0 0 0 22.105 0zM0 22.105C0 23.152.848 24 1.895 24h3.289v-5.184H0v3.289z'/%3E%3C/svg%3E");
}
.service-tag.gmail {
color: var(--g-red);
background-color: var(--g-red-dim);
border-color: rgba(234, 67, 53, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%23EA4335' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z'/%3E%3C/svg%3E");
}
.service-tag.drive {
color: var(--g-yellow);
background-color: var(--g-yellow-dim);
border-color: rgba(251, 188, 5, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%23FBBC05' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.01 1.485c-2.082 0-3.754.02-3.743.047.01.02 1.708 3.001 3.774 6.62l3.76 6.574h3.76c2.081 0 3.753-.02 3.742-.047-.005-.02-1.708-3.001-3.775-6.62l-3.76-6.574zm-4.76 1.73a789.828 789.861 0 0 0-3.63 6.319L0 15.868l1.89 3.298 1.885 3.297 3.62-6.335 3.618-6.33-1.88-3.287C8.1 4.704 7.255 3.22 7.25 3.214zm2.259 12.653-.203.348c-.114.198-.96 1.672-1.88 3.287a423.93 423.948 0 0 1-1.698 2.97c-.01.026 3.24.042 7.222.042h7.244l1.796-3.157c.992-1.734 1.85-3.23 1.906-3.323l.104-.167h-7.249z'/%3E%3C/svg%3E");
}
.service-tag.contacts {
color: var(--g-blue);
background-color: var(--g-blue-dim);
border-color: rgba(66, 133, 244, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%234285F4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 0H4v2h16V0zM4 24h16v-2H4v2zM20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 2.75c1.24 0 2.25 1.01 2.25 2.25s-1.01 2.25-2.25 2.25S9.75 10.24 9.75 9 10.76 6.75 12 6.75zM17 17H7v-1.5c0-1.67 3.33-2.5 5-2.5s5 .83 5 2.5V17z'/%3E%3C/svg%3E");
}
.service-tag.sheets {
color: var(--g-green);
background-color: var(--g-green-dim);
border-color: rgba(52, 168, 83, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%2334A853' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11.318 12.545H7.91v-1.909h3.41v1.91zM14.728 0v6h6l-6-6zm1.363 10.636h-3.41v1.91h3.41v-1.91zm0 3.273h-3.41v1.91h3.41v-1.91zM20.727 6.5v15.864c0 .904-.732 1.636-1.636 1.636H4.909a1.636 1.636 0 0 1-1.636-1.636V1.636C3.273.732 4.005 0 4.909 0h9.318v6.5h6.5zm-3.273 2.773H6.545v7.909h10.91v-7.91zm-6.136 4.636H7.91v1.91h3.41v-1.91z'/%3E%3C/svg%3E");
}
.service-tag.people {
color: var(--g-blue);
background-color: var(--g-blue-dim);
border-color: rgba(66, 133, 244, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%234285F4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
}
.service-tag.tasks {
color: var(--g-blue);
background-color: var(--g-blue-dim);
border-color: rgba(66, 133, 244, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%234285F4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M22 5.18L10.59 16.6l-4.24-4.24 1.41-1.41 2.83 2.83 10-10L22 5.18zm-2.21 5.04c.13.57.21 1.17.21 1.78 0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8c1.58 0 3.04.46 4.28 1.25l1.44-1.44A9.9 9.9 0 0012 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10c0-1.19-.22-2.33-.6-3.39l-1.61 1.61z'/%3E%3C/svg%3E");
}
/* Terminal Card */
.terminal-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 16px;
border-radius: var(--radius-lg);
overflow: hidden;
text-align: left;
animation: fadeSlideUp 0.5s ease 0.4s both;
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.02) inset;
margin-bottom: 20px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
}
.terminal-bar {
background: var(--bg-input);
padding: 0.875rem 1.125rem;
background: var(--bg-elevated);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 0.5rem;
gap: 8px;
border-bottom: 1px solid var(--border);
}
@ -230,22 +290,22 @@
flex: 1;
text-align: center;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
font-size: 11px;
color: var(--text-dim);
margin-right: 48px;
}
.terminal-body {
padding: 1.5rem;
padding: 20px;
}
.terminal-line {
display: flex;
align-items: center;
gap: 0.625rem;
gap: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
margin-bottom: 0.875rem;
font-size: 13px;
margin-bottom: 10px;
line-height: 1.5;
}
@ -254,7 +314,7 @@
}
.terminal-prompt {
color: var(--google-blue);
color: var(--g-blue);
user-select: none;
font-weight: 500;
}
@ -264,35 +324,32 @@
}
.terminal-flag {
color: var(--google-yellow);
color: var(--g-yellow);
}
.terminal-arg {
color: var(--google-green);
color: var(--g-green);
}
.terminal-output {
color: var(--text-dim);
padding-left: 1.125rem;
margin-top: -0.5rem;
margin-bottom: 0.875rem;
padding-left: 18px;
margin-top: -6px;
margin-bottom: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8125rem;
font-size: 12px;
}
.terminal-output.success {
color: var(--success);
color: var(--g-green);
}
/* Blinking cursor - 1.2s interval as requested */
.terminal-cursor {
display: inline-block;
width: 10px;
height: 20px;
background: var(--google-blue);
width: 8px;
height: 18px;
background: var(--g-blue);
animation: cursorBlink 1.2s step-end infinite;
margin-left: 2px;
vertical-align: middle;
border-radius: 1px;
}
@ -301,100 +358,215 @@
50.01%, 100% { opacity: 0; }
}
/* Info card */
.info-card {
margin-top: 2rem;
padding: 1.25rem 1.5rem;
background: rgba(66, 133, 244, 0.06);
border: 1px solid rgba(66, 133, 244, 0.12);
border-radius: 12px;
animation: fadeSlideUp 0.5s ease 0.5s both;
text-align: left;
/* Action Buttons */
.actions {
display: flex;
gap: 1rem;
gap: 10px;
margin-bottom: 24px;
}
.btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 20px;
border-radius: var(--radius);
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
text-decoration: none;
}
.btn-primary {
background: var(--bg-surface);
color: var(--text);
border: 1px solid var(--border);
}
.btn-primary:hover {
background: var(--bg-elevated);
border-color: var(--border-active);
}
.btn-secondary {
background: var(--g-blue);
color: white;
box-shadow: 0 4px 16px rgba(66, 133, 244, 0.25);
}
.btn-secondary:hover {
background: #5a9cff;
transform: translateY(-1px);
box-shadow: 0 6px 24px rgba(66, 133, 244, 0.35);
}
.btn svg {
width: 18px;
height: 18px;
}
/* Info Card */
.info-card {
padding: 16px 20px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
display: flex;
gap: 14px;
align-items: flex-start;
}
.info-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
background: rgba(66, 133, 244, 0.1);
border-radius: 10px;
width: 36px;
height: 36px;
background: var(--g-blue-dim);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
}
.info-icon svg {
width: 20px;
height: 20px;
stroke: var(--google-blue);
width: 18px;
height: 18px;
color: var(--g-blue);
}
.info-content h3 {
font-size: 0.9375rem;
.info-content h4 {
font-size: 13px;
font-weight: 600;
color: var(--text);
margin-bottom: 0.25rem;
margin-bottom: 2px;
}
.info-content p {
font-size: 0.875rem;
font-size: 13px;
color: var(--text-muted);
line-height: 1.5;
}
.info-content code {
font-family: 'JetBrains Mono', monospace;
background: rgba(66, 133, 244, 0.1);
padding: 0.125rem 0.375rem;
font-size: 12px;
background: var(--bg-elevated);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.8125rem;
color: var(--google-blue);
color: var(--g-blue);
}
/* Footer */
.footer {
margin-top: 2rem;
font-size: 0.8125rem;
margin-top: 24px;
text-align: center;
color: var(--text-muted);
animation: fadeSlideUp 0.5s ease 0.6s both;
font-size: 12px;
}
.footer p {
display: inline-block;
padding: 0.625rem 0.875rem;
background: rgba(17, 17, 24, 0.6);
border: 1px solid rgba(255, 255, 255, 0.06);
padding: 10px 14px;
background: rgba(17, 17, 22, 0.6);
border: 1px solid var(--border-subtle);
border-radius: 999px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
/* GitHub icon - defined as CSS mask for reuse
* SYNC: If modifying the icon, also update accounts.html */
.icon-github {
display: inline-block;
width: 14px;
height: 14px;
vertical-align: middle;
margin-right: 4px;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'/%3E%3C/svg%3E");
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
}
.github-link {
margin-top: 12px;
}
.github-link a {
display: inline-flex;
align-items: center;
color: var(--text-muted);
text-decoration: none;
font-size: 12px;
transition: color 0.2s;
}
.github-link a:hover {
color: var(--g-blue);
}
@media (max-width: 480px) {
.container {
padding: 32px 16px;
}
.actions {
flex-direction: column;
}
.terminal-body {
padding: 16px;
}
}
</style>
</head>
<body>
<!-- Animated gradient orbs -->
<div class="orb orb-blue"></div>
<div class="orb orb-red"></div>
<div class="orb orb-yellow"></div>
<div class="orb orb-green"></div>
<div class="container">
<div class="logo-container">
<svg class="google-logo" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
<div class="success-ring">
<svg viewBox="0 0 24 24">
<polyline points="20 6 9 17 4 12"></polyline>
<header class="success-header">
<div class="success-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
<div class="success-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
</div>
</div>
</div>
<h1>You're connected</h1>
<p class="subtitle">gog is now authorized to access <strong>Google Workspace</strong></p>
<h1>You're connected</h1>
{{if .Email}}
<p class="account-email">Authorized as <strong>{{.Email}}</strong></p>
{{else}}
<p class="account-email">gog is now authorized to access <strong>Google Workspace</strong></p>
{{end}}
<div class="terminal">
{{if .Services}}
<div class="services-row">
{{range .Services}}
<span class="service-tag {{.}}">{{.}}</span>
{{end}}
</div>
{{end}}
</header>
<div class="terminal-card">
<div class="terminal-bar">
<div class="terminal-dots">
<span class="terminal-dot close"></span>
@ -427,20 +599,60 @@
</div>
</div>
{{if .Email}}
<div class="actions">
<a href="/" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
Manage Accounts
</a>
<a href="/auth/start" class="btn btn-secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Add Another Account
</a>
</div>
{{end}}
<div class="info-card">
<div class="info-icon">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
</div>
<div class="info-content">
<h3>Return to your terminal</h3>
<h4>Return to your terminal</h4>
<p>You can close this window. Run <code>gog --help</code> to see available commands.</p>
</div>
</div>
<p class="footer">This window will close automatically.</p>
<footer class="footer">
<p>Closing in <span id="countdown">{{.CountdownSeconds}}</span> seconds...</p>
<p class="github-link">
<a href="https://github.com/steipete/gogcli" target="_blank" rel="noopener noreferrer">
<span class="icon-github"></span>
View on GitHub
</a>
</p>
</footer>
</div>
<script>
let seconds = {{.CountdownSeconds}};
const countdownEl = document.getElementById('countdown');
const interval = setInterval(() => {
seconds--;
if (seconds > 0) {
countdownEl.textContent = seconds;
} else {
clearInterval(interval);
countdownEl.parentElement.textContent = 'You can close this window.';
}
}, 1000);
</script>
</body>
</html>

View File

@ -1,584 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connected - gog</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg-void: #050508;
--bg-base: #0a0a0f;
--bg-surface: #111116;
--bg-elevated: #18181d;
--bg-hover: #1f1f26;
--border: rgba(255, 255, 255, 0.06);
--border-subtle: rgba(255, 255, 255, 0.04);
--border-active: rgba(255, 255, 255, 0.12);
--text: #f4f4f5;
--text-secondary: #a1a1aa;
--text-muted: #71717a;
--text-dim: #52525b;
--g-blue: #4285F4;
--g-blue-dim: rgba(66, 133, 244, 0.12);
--g-red: #EA4335;
--g-red-dim: rgba(234, 67, 53, 0.12);
--g-yellow: #FBBC05;
--g-yellow-dim: rgba(251, 188, 5, 0.12);
--g-green: #34A853;
--g-green-dim: rgba(52, 168, 83, 0.12);
--radius-sm: 8px;
--radius: 12px;
--radius-lg: 16px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Sora', -apple-system, sans-serif;
background: var(--bg-void);
color: var(--text);
min-height: 100vh;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
opacity: 0;
animation: pageReveal 0.5s ease-out forwards;
}
@keyframes pageReveal {
from { opacity: 0; }
to { opacity: 1; }
}
/* Success ambient glow */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 80% 60% at 50% -20%, var(--g-green-dim), transparent 60%),
radial-gradient(ellipse 60% 50% at 70% 30%, var(--g-blue-dim), transparent 50%);
pointer-events: none;
z-index: 0;
}
body::after {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px);
background-size: 64px 64px;
pointer-events: none;
z-index: 0;
}
.container {
max-width: 560px;
margin: 0 auto;
padding: 48px 24px;
position: relative;
z-index: 1;
}
/* Success Header */
.success-header {
text-align: center;
margin-bottom: 40px;
}
.success-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
position: relative;
opacity: 0;
animation: iconReveal 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
}
@keyframes iconReveal {
from {
opacity: 0;
transform: scale(0.8) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.success-icon svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 8px 32px rgba(66, 133, 244, 0.3));
}
.success-badge {
position: absolute;
bottom: -4px;
right: -4px;
width: 28px;
height: 28px;
background: var(--g-green);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 4px var(--bg-void), 0 4px 12px rgba(52, 168, 83, 0.4);
opacity: 0;
transform: scale(0);
animation: badgePop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.5s forwards;
}
@keyframes badgePop {
to {
opacity: 1;
transform: scale(1);
}
}
.success-badge svg {
width: 16px;
height: 16px;
color: white;
}
h1 {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.03em;
margin-bottom: 8px;
opacity: 0;
animation: contentReveal 0.5s ease-out 0.25s forwards;
}
@keyframes contentReveal {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.account-email {
font-size: 15px;
color: var(--text-secondary);
margin-bottom: 16px;
opacity: 0;
animation: contentReveal 0.5s ease-out 0.35s forwards;
}
.account-email strong {
color: var(--g-blue);
font-weight: 600;
}
.services-row {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 20px;
opacity: 0;
animation: contentReveal 0.5s ease-out 0.4s forwards;
}
.service-tag {
font-size: 11px;
font-weight: 500;
letter-spacing: 0.02em;
padding: 4px 10px 4px 24px;
border-radius: 99px;
border: 1px solid;
background-repeat: no-repeat;
background-position: 6px center;
background-size: 14px 14px;
}
.service-tag.calendar {
color: var(--g-blue);
background-color: var(--g-blue-dim);
border-color: rgba(66, 133, 244, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%234285F4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.316 5.684H24v12.632h-5.684V5.684zM5.684 24h12.632v-5.684H5.684V24zM18.316 5.684V0H1.895A1.894 1.894 0 0 0 0 1.895v16.421h5.684V5.684h12.632zM22.105 0h-3.289v5.184H24V1.895A1.894 1.894 0 0 0 22.105 0zM0 22.105C0 23.152.848 24 1.895 24h3.289v-5.184H0v3.289z'/%3E%3C/svg%3E");
}
.service-tag.gmail {
color: var(--g-red);
background-color: var(--g-red-dim);
border-color: rgba(234, 67, 53, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%23EA4335' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z'/%3E%3C/svg%3E");
}
.service-tag.drive {
color: var(--g-yellow);
background-color: var(--g-yellow-dim);
border-color: rgba(251, 188, 5, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%23FBBC05' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.01 1.485c-2.082 0-3.754.02-3.743.047.01.02 1.708 3.001 3.774 6.62l3.76 6.574h3.76c2.081 0 3.753-.02 3.742-.047-.005-.02-1.708-3.001-3.775-6.62l-3.76-6.574zm-4.76 1.73a789.828 789.861 0 0 0-3.63 6.319L0 15.868l1.89 3.298 1.885 3.297 3.62-6.335 3.618-6.33-1.88-3.287C8.1 4.704 7.255 3.22 7.25 3.214zm2.259 12.653-.203.348c-.114.198-.96 1.672-1.88 3.287a423.93 423.948 0 0 1-1.698 2.97c-.01.026 3.24.042 7.222.042h7.244l1.796-3.157c.992-1.734 1.85-3.23 1.906-3.323l.104-.167h-7.249z'/%3E%3C/svg%3E");
}
.service-tag.contacts {
color: var(--g-blue);
background-color: var(--g-blue-dim);
border-color: rgba(66, 133, 244, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%234285F4' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 0H4v2h16V0zM4 24h16v-2H4v2zM20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 2.75c1.24 0 2.25 1.01 2.25 2.25s-1.01 2.25-2.25 2.25S9.75 10.24 9.75 9 10.76 6.75 12 6.75zM17 17H7v-1.5c0-1.67 3.33-2.5 5-2.5s5 .83 5 2.5V17z'/%3E%3C/svg%3E");
}
.service-tag.sheets {
color: var(--g-green);
background-color: var(--g-green-dim);
border-color: rgba(52, 168, 83, 0.2);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%2334A853' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11.318 12.545H7.91v-1.909h3.41v1.91zM14.728 0v6h6l-6-6zm1.363 10.636h-3.41v1.91h3.41v-1.91zm0 3.273h-3.41v1.91h3.41v-1.91zM20.727 6.5v15.864c0 .904-.732 1.636-1.636 1.636H4.909a1.636 1.636 0 0 1-1.636-1.636V1.636C3.273.732 4.005 0 4.909 0h9.318v6.5h6.5zm-3.273 2.773H6.545v7.909h10.91v-7.91zm-6.136 4.636H7.91v1.91h3.41v-1.91z'/%3E%3C/svg%3E");
}
/* Terminal Card */
.terminal-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 20px;
opacity: 0;
animation: contentReveal 0.5s ease-out 0.45s forwards;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
}
.terminal-bar {
background: var(--bg-elevated);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--border);
}
.terminal-dots {
display: flex;
gap: 6px;
}
.terminal-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.terminal-dot.close { background: #ff5f57; }
.terminal-dot.minimize { background: #febc2e; }
.terminal-dot.maximize { background: #28c840; }
.terminal-title {
flex: 1;
text-align: center;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-dim);
margin-right: 48px;
}
.terminal-body {
padding: 20px;
}
.terminal-line {
display: flex;
align-items: center;
gap: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
margin-bottom: 10px;
line-height: 1.5;
}
.terminal-line:last-child {
margin-bottom: 0;
}
.terminal-prompt {
color: var(--g-blue);
user-select: none;
font-weight: 500;
}
.terminal-cmd {
color: var(--text);
}
.terminal-flag {
color: var(--g-yellow);
}
.terminal-arg {
color: var(--g-green);
}
.terminal-output {
color: var(--text-dim);
padding-left: 18px;
margin-top: -6px;
margin-bottom: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
}
.terminal-output.success {
color: var(--g-green);
}
.terminal-cursor {
display: inline-block;
width: 8px;
height: 18px;
background: var(--g-blue);
animation: cursorBlink 1.2s step-end infinite;
border-radius: 1px;
}
@keyframes cursorBlink {
0%, 50% { opacity: 1; }
50.01%, 100% { opacity: 0; }
}
/* Action Buttons */
.actions {
display: flex;
gap: 10px;
margin-bottom: 24px;
opacity: 0;
animation: contentReveal 0.5s ease-out 0.5s forwards;
}
.btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 20px;
border-radius: var(--radius);
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-primary {
background: var(--bg-surface);
color: var(--text);
border: 1px solid var(--border);
}
.btn-primary:hover {
background: var(--bg-elevated);
border-color: var(--border-active);
}
.btn-secondary {
background: var(--g-blue);
color: white;
box-shadow: 0 4px 16px rgba(66, 133, 244, 0.25);
}
.btn-secondary:hover {
background: #5a9cff;
transform: translateY(-1px);
box-shadow: 0 6px 24px rgba(66, 133, 244, 0.35);
}
.btn svg {
width: 18px;
height: 18px;
}
/* Info Card */
.info-card {
padding: 16px 20px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
display: flex;
gap: 14px;
align-items: flex-start;
opacity: 0;
animation: contentReveal 0.5s ease-out 0.55s forwards;
}
.info-icon {
flex-shrink: 0;
width: 36px;
height: 36px;
background: var(--g-blue-dim);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
}
.info-icon svg {
width: 18px;
height: 18px;
color: var(--g-blue);
}
.info-content h4 {
font-size: 13px;
font-weight: 600;
margin-bottom: 2px;
}
.info-content p {
font-size: 13px;
color: var(--text-muted);
line-height: 1.5;
}
.info-content code {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
background: var(--bg-elevated);
padding: 2px 6px;
border-radius: 4px;
color: var(--g-blue);
}
/* Footer */
.footer {
margin-top: 24px;
text-align: center;
color: var(--text-muted);
font-size: 12px;
opacity: 0;
animation: contentReveal 0.5s ease-out 0.6s forwards;
}
.footer p {
display: inline-block;
padding: 10px 14px;
background: rgba(17, 17, 22, 0.6);
border: 1px solid var(--border-subtle);
border-radius: 999px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
@media (max-width: 480px) {
.container {
padding: 32px 16px;
}
.actions {
flex-direction: column;
}
.terminal-body {
padding: 16px;
}
}
</style>
</head>
<body>
<div class="container">
<header class="success-header">
<div class="success-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
<div class="success-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
</div>
</div>
<h1>You're connected</h1>
<p class="account-email">Authorized as <strong>{{.Email}}</strong></p>
<div class="services-row">
{{range .Services}}
<span class="service-tag {{.}}">{{.}}</span>
{{end}}
</div>
</header>
<div class="terminal-card">
<div class="terminal-bar">
<div class="terminal-dots">
<span class="terminal-dot close"></span>
<span class="terminal-dot minimize"></span>
<span class="terminal-dot maximize"></span>
</div>
<span class="terminal-title">Terminal</span>
</div>
<div class="terminal-body">
<div class="terminal-line">
<span class="terminal-prompt">$</span>
<span class="terminal-cmd">gog</span>
<span class="terminal-arg">calendar</span>
<span class="terminal-arg">list</span>
</div>
<div class="terminal-output success">Fetching calendars...</div>
<div class="terminal-line">
<span class="terminal-prompt">$</span>
<span class="terminal-cmd">gog</span>
<span class="terminal-arg">gmail</span>
<span class="terminal-arg">search</span>
<span class="terminal-flag">--query</span>
<span class="terminal-cmd">"is:unread"</span>
</div>
<div class="terminal-output success">Found 12 messages</div>
<div class="terminal-line">
<span class="terminal-prompt">$</span>
<span class="terminal-cursor"></span>
</div>
</div>
</div>
<div class="actions">
<a href="/" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
Manage Accounts
</a>
<a href="/auth/start" class="btn btn-secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Add Another Account
</a>
</div>
<div class="info-card">
<div class="info-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
</div>
<div class="info-content">
<h4>Return to your terminal</h4>
<p>You can close this window. Run <code>gog --help</code> to see available commands.</p>
</div>
</div>
<footer class="footer">
<p>This window will close automatically.</p>
</footer>
</div>
</body>
</html>

View File

@ -5,9 +5,6 @@ import _ "embed"
//go:embed templates/accounts.html
var accountsTemplate string
//go:embed templates/success_new.html
var successTemplateNew string
//go:embed templates/success.html
var successTemplate string

View File

@ -13,11 +13,14 @@ func TestEmbeddedTemplates_Parse(t *testing.T) {
data any
}{
{name: "accounts", src: accountsTemplate, data: struct{ CSRFToken string }{CSRFToken: "csrf"}},
{name: "success_new", src: successTemplateNew, data: struct {
Email string
Services []string
}{Email: "a@b.com", Services: []string{"gmail", "drive"}}},
{name: "success", src: successTemplate, data: struct{}{}},
{name: "success_with_email", src: successTemplate, data: successTemplateData{
Email: "a@b.com",
Services: []string{"gmail", "drive"},
CountdownSeconds: 30,
}},
{name: "success_without_email", src: successTemplate, data: successTemplateData{
CountdownSeconds: 30,
}},
{name: "error", src: errorTemplate, data: struct{ Error string }{Error: "boom"}},
{name: "cancelled", src: cancelledTemplate, data: struct{}{}},
}

View File

@ -0,0 +1,68 @@
package googleauth
import (
"context"
"testing"
"time"
)
func TestWaitPostSuccess_ContextCancellation(t *testing.T) {
t.Parallel()
// Create a context that will be cancelled after a short delay
ctx, cancel := context.WithCancel(context.Background())
// Cancel context after 50ms
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
start := time.Now()
// Wait for a long duration (1 second) but expect early termination
waitPostSuccess(ctx, 1*time.Second)
elapsed := time.Since(start)
// Should complete well before the 1-second wait duration
// Allow some tolerance for timing (up to 200ms)
if elapsed >= 500*time.Millisecond {
t.Fatalf("waitPostSuccess did not respect context cancellation: took %v, expected < 500ms", elapsed)
}
}
func TestWaitPostSuccess_FullDuration(t *testing.T) {
t.Parallel()
ctx := context.Background()
start := time.Now()
// Wait for a short duration
waitPostSuccess(ctx, 100*time.Millisecond)
elapsed := time.Since(start)
// Should complete close to the specified duration
if elapsed < 90*time.Millisecond {
t.Fatalf("waitPostSuccess returned too early: %v, expected ~100ms", elapsed)
}
if elapsed > 200*time.Millisecond {
t.Fatalf("waitPostSuccess took too long: %v, expected ~100ms", elapsed)
}
}
func TestWaitPostSuccess_AlreadyCancelledContext(t *testing.T) {
t.Parallel()
// Create an already-cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
start := time.Now()
// Should return immediately since context is already cancelled
waitPostSuccess(ctx, 1*time.Second)
elapsed := time.Since(start)
// Should complete almost immediately (well under 50ms)
if elapsed >= 50*time.Millisecond {
t.Fatalf("waitPostSuccess did not return immediately for cancelled context: took %v", elapsed)
}
}