Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions pkg/auth/auth_simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,26 @@ func (s *simpleService) handleCallback(w http.ResponseWriter, r *http.Request) {
redirectParams.Set("orgs", strings.Join(githubUser.Organizations, ","))
}

// Resolve success page display customization from config rules.
if s.cfg.SuccessPage != nil {
display := s.cfg.SuccessPage.Resolve(githubUser.Login, githubUser.Organizations)
if display.Tagline != "" {
redirectParams.Set("sp_tagline", display.Tagline)
}

if display.Media != nil {
redirectParams.Set("sp_media_type", display.Media.Type)

if display.Media.URL != "" {
redirectParams.Set("sp_media_url", display.Media.URL)
}

if display.Media.ASCIIArtBase64 != "" {
redirectParams.Set("sp_media_ascii", display.Media.ASCIIArtBase64)
}
}
}

redirectURL := fmt.Sprintf("%s?%s", pending.RedirectURI, redirectParams.Encode())
http.Redirect(w, r, redirectURL, http.StatusFound)
}
Expand Down
8 changes: 6 additions & 2 deletions pkg/auth/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,12 @@ func (c *client) startCallbackServer(_ context.Context, expectedState string, co
}

user := callbackUser{
Login: r.URL.Query().Get("login"),
AvatarURL: r.URL.Query().Get("avatar_url"),
Login: r.URL.Query().Get("login"),
AvatarURL: r.URL.Query().Get("avatar_url"),
Tagline: r.URL.Query().Get("sp_tagline"),
MediaType: r.URL.Query().Get("sp_media_type"),
MediaURL: r.URL.Query().Get("sp_media_url"),
MediaASCIIB64: r.URL.Query().Get("sp_media_ascii"),
}

if orgsParam := r.URL.Query().Get("orgs"); orgsParam != "" {
Expand Down
74 changes: 32 additions & 42 deletions pkg/auth/client/success_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,17 @@ type callbackUser struct {
Login string
AvatarURL string
Orgs []string
}

// specialUsers get ASCII art instead of the GIF.
var specialUsers = map[string]bool{
"samcm": true,
"mattevans": true,
"savid": true,
}

// pandaASCIIBase64 is the base64-encoded ASCII art shown to special users.
// To update: write your raw multiline ASCII art to a file, then:
//
// base64 < art.txt | tr -d '\n'
//
// Paste the output here.
var pandaASCIIBase64 = "KiBnIG8gYSB0IHMgZSB4ICogZyBvIGEgdCBzIGUgeCAqIGcgbyBhIHQgcyBlIHggKgpnICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBnICAKbyAvICAgICBcICAgICAgICAgICAgIFwgICAgICAgICAgICAvICAgIFwgICAgICAgbwphfCAgICAgICB8ICAgICAgICAgICAgIFwgICAgICAgICAgfCAgICAgIHwgICAgICBhCnR8ICAgICAgIGAuICAgICAgICAgICAgIHwgICAgICAgICB8ICAgICAgIDogICAgIHQKc2AgICAgICAgIHwgICAgICAgICAgICAgfCAgICAgICAgXHwgICAgICAgfCAgICAgcwplIFwgICAgICAgfCAvICAgICAgIC8gIFxcXCAgIC0tX18gXFwgICAgICAgOiAgICBlCnggIFwgICAgICBcLyAgIF8tLX5+ICAgICAgICAgIH4tLV9ffCBcICAgICB8ICAgIHggIAoqICAgXCAgICAgIFxfLX4gICAgICAgICAgICAgICAgICAgIH4tX1wgICAgfCAgICAqCmcgICAgXF8gICAgIFwgICAgICAgIF8uLS0tLS0tLS0uX19fX19fXHwgICB8ICAgIGcKbyAgICAgIFwgICAgIFxfX19fX18vLyBfIF9fXyBfIChfKF9fPiAgXCAgIHwgICAgbwphICAgICAgIFwgICAuICBDIF9fXykgIF9fX19fXyAoXyhfX19fPiAgfCAgLyAgICBhCnQgICAgICAgL1wgfCAgIEMgX19fXykvICAgICAgXCAoX19fX18+ICB8Xy8gICAgIHQKcyAgICAgIC8gL1x8ICAgQ19fX19fKSAgICAgICB8ICAoX19fPiAgIC8gIFwgICAgcwplICAgICB8ICAgKCAgIF9DX19fX18pXF9fX19fXy8gIC8vIF8vIC8gICAgIFwgICBlCnggICAgIHwgICAgXCAgfF9fICAgXFxfX19fX19fX18vLyAoX18vICAgICAgIHwgIHgKKiAgICB8IFwgICAgXF9fX18pICAgYC0tLS0gICAtLScgICAgICAgICAgICAgfCAgKgpnICAgIHwgIFxfICAgICAgICAgIF9fX1wgICAgICAgL18gICAgICAgICAgXy8gfCBnCm8gICB8ICAgICAgICAgICAgICAvICAgIHwgICAgIHwgIFwgICAgICAgICAgICB8IG8KYSAgIHwgICAgICAgICAgICAgfCAgICAvICAgICAgIFwgIFwgICAgICAgICAgIHwgYQp0ICAgfCAgICAgICAgICAvIC8gICAgfCAgICAgICAgIHwgIFwgICAgICAgICAgIHx0CnMgICB8ICAgICAgICAgLyAvICAgICAgXF9fL1xfX18vICAgIHwgICAgICAgICAgfHMKZSAgfCAgICAgICAgICAgLyAgICAgICAgfCAgICB8ICAgICAgIHwgICAgICAgICB8ZQp4ICB8ICAgICAgICAgIHwgICAgICAgICB8ICAgIHwgICAgICAgfCAgICAgICAgIHx4CiogZyBvIGEgdCBzIGUgeCAqIGcgbyBhIHQgcyBlIHggKiBnIG8gYSB0IHMgZSB4ICoK" //nolint:lll // base64 blob

// decodePandaASCII decodes the base64-encoded ASCII art.
func decodePandaASCII() string {
b, err := base64.StdEncoding.DecodeString(pandaASCIIBase64)
if err != nil {
return "(panda art failed to decode)"
}

return string(b)
// Display fields resolved by proxy success page rules.
Tagline string
MediaType string // "gif" or "ascii"
MediaURL string
MediaASCIIB64 string
}

// buildSuccessPage generates a styled HTML success page based on user info.
func buildSuccessPage(user callbackUser) string { //nolint:funlen // single HTML template
isEthPandaOps := hasOrg(user.Orgs, "ethpandaops")
isSpecialUser := specialUsers[strings.ToLower(user.Login)]
hasOrgs := len(user.Orgs) > 0

login := html.EscapeString(user.Login)
if login == "" {
Expand All @@ -59,32 +39,42 @@ func buildSuccessPage(user callbackUser) string { //nolint:funlen // single HTML
avatarHTML = `<div class="avatar avatar-fallback">` + strings.ToUpper(login[:1]) + `</div>`
}

// Build the context line — what org, what just happened.
// Build the context line — org badge or generic identity note.
contextHTML := `<span class="context-muted">GitHub identity linked</span>`
if isEthPandaOps {
contextHTML = `<span class="org-badge">ethpandaops</span>`
if hasOrgs {
contextHTML = fmt.Sprintf(`<span class="org-badge">%s</span>`,
html.EscapeString(strings.ToLower(user.Orgs[0])))
}

// Status subtitle.
statusSub := "You've successfully logged in to panda"
if isEthPandaOps {
statusSub = "You've successfully logged in to ethpandaops/panda"
if hasOrgs {
statusSub = fmt.Sprintf("You've successfully logged in to %s/panda",
strings.ToLower(user.Orgs[0]))
}

// Tagline — shown after the media block.
tagline := "You can close this window and return to your terminal."
if isEthPandaOps {
tagline = "Enjoy debugging your devnet champ"
// Tagline — from config rules or default.
tagline := user.Tagline
if tagline == "" {
tagline = "You can close this window and return to your terminal."
}

// Media — GIF or ASCII art for ethpandaops members.
// Media — rendered from config-driven display fields.
mediaHTML := ""
if isEthPandaOps {
if isSpecialUser {
art := decodePandaASCII()
mediaHTML = fmt.Sprintf(`<pre class="ascii-art">%s</pre>`, html.EscapeString(art))
} else {
mediaHTML = `<div class="media-frame"><img src="https://media1.tenor.com/m/92A2K1kvoHcAAAAd/casino-royale-bond.gif" alt="" class="gif"></div>` //nolint:lll // gif URL

switch user.MediaType {
case "ascii":
if user.MediaASCIIB64 != "" {
if art, err := base64.StdEncoding.DecodeString(user.MediaASCIIB64); err == nil {
mediaHTML = fmt.Sprintf(`<pre class="ascii-art">%s</pre>`, html.EscapeString(string(art)))
}
}
case "gif":
if user.MediaURL != "" {
mediaHTML = fmt.Sprintf(
`<div class="media-frame"><img src="%s" alt="" class="gif"></div>`,
html.EscapeString(user.MediaURL),
)
}
}

Expand Down
119 changes: 75 additions & 44 deletions pkg/auth/client/success_page_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"encoding/base64"
"os"
"path/filepath"
"strings"
Expand All @@ -10,14 +11,6 @@ import (
"github.com/stretchr/testify/require"
)

func TestDecodePandaASCII(t *testing.T) {
t.Parallel()

art := decodePandaASCII()

assert.NotEmpty(t, art)
}

func TestBuildSuccessPage_Default(t *testing.T) {
t.Parallel()

Expand All @@ -31,12 +24,12 @@ func TestBuildSuccessPage_Default(t *testing.T) {
assert.Contains(t, page, "logged in to panda")
assert.Contains(t, page, "avatar.png")
assert.Contains(t, page, "panda datasources")
assert.NotContains(t, page, "ethpandaops")
assert.NotContains(t, page, "casino-royale")
assert.NotContains(t, page, "<pre")
assert.Contains(t, page, "You can close this window and return to your terminal.")
assert.NotContains(t, page, "<pre class=\"ascii-art\"")
assert.NotContains(t, page, `<div class="media-frame">`)
}

func TestBuildSuccessPage_EthPandaOps(t *testing.T) {
func TestBuildSuccessPage_WithOrg(t *testing.T) {
t.Parallel()

page := buildSuccessPage(callbackUser{
Expand All @@ -46,55 +39,72 @@ func TestBuildSuccessPage_EthPandaOps(t *testing.T) {
})

assert.Contains(t, page, "pandafan")
assert.Contains(t, page, "Enjoy debugging your devnet champ")
assert.Contains(t, page, "ethpandaops")
assert.Contains(t, page, "casino-royale-bond.gif")
assert.NotContains(t, page, "<pre")
assert.Contains(t, page, "logged in to ethpandaops/panda")
}

func TestBuildSuccessPage_SpecialUser(t *testing.T) {
func TestBuildSuccessPage_GIFMedia(t *testing.T) {
t.Parallel()

for _, login := range []string{"samcm", "mattevans", "savid"} {
t.Run(login, func(t *testing.T) {
t.Parallel()
page := buildSuccessPage(callbackUser{
Login: "pandafan",
AvatarURL: "https://example.com/avatar.png",
Orgs: []string{"ethpandaops"},
Tagline: "Enjoy debugging your devnet champ",
MediaType: "gif",
MediaURL: "https://example.com/cool.gif",
})

assert.Contains(t, page, "Enjoy debugging your devnet champ")
assert.Contains(t, page, "cool.gif")
assert.Contains(t, page, `<div class="media-frame">`)
assert.NotContains(t, page, "<pre class=\"ascii-art\"")
}

page := buildSuccessPage(callbackUser{
Login: login,
AvatarURL: "https://example.com/avatar.png",
Orgs: []string{"ethpandaops"},
})
func TestBuildSuccessPage_ASCIIMedia(t *testing.T) {
t.Parallel()

assert.Contains(t, page, login)
assert.Contains(t, page, "Enjoy debugging your devnet champ")
assert.Contains(t, page, "<pre")
assert.NotContains(t, page, "casino-royale-bond.gif")
})
}
art := " /\\_/\\\n ( o.o )\n > ^ <"
artB64 := base64.StdEncoding.EncodeToString([]byte(art))

page := buildSuccessPage(callbackUser{
Login: "samcm",
AvatarURL: "https://example.com/avatar.png",
Orgs: []string{"ethpandaops"},
Tagline: "Enjoy debugging your devnet champ",
MediaType: "ascii",
MediaASCIIB64: artB64,
})

assert.Contains(t, page, "Enjoy debugging your devnet champ")
assert.Contains(t, page, "<pre class=\"ascii-art\"")
assert.Contains(t, page, "o.o")
assert.NotContains(t, page, `<div class="media-frame">`)
}

func TestBuildSuccessPage_SpecialUserWithoutOrg(t *testing.T) {
func TestBuildSuccessPage_InvalidASCIIBase64(t *testing.T) {
t.Parallel()

page := buildSuccessPage(callbackUser{
Login: "samcm",
Orgs: []string{"some-other-org"},
Login: "someone",
MediaType: "ascii",
MediaASCIIB64: "not-valid-base64!!!",
})

assert.Contains(t, page, "logged in to panda")
// Should gracefully skip the media block.
assert.NotContains(t, page, "<pre")
assert.NotContains(t, page, "casino-royale")
}

func TestBuildSuccessPage_CaseInsensitiveOrg(t *testing.T) {
func TestBuildSuccessPage_CustomTagline(t *testing.T) {
t.Parallel()

page := buildSuccessPage(callbackUser{
Login: "someone",
Orgs: []string{"EthPandaOps"},
Login: "someone",
Tagline: "Welcome aboard!",
})

assert.Contains(t, page, "Enjoy debugging your devnet champ")
assert.Contains(t, page, "Welcome aboard!")
assert.NotContains(t, page, "You can close this window and return to your terminal.")
}

func TestBuildSuccessPage_NoAvatar(t *testing.T) {
Expand All @@ -117,6 +127,18 @@ func TestBuildSuccessPage_EmptyLogin(t *testing.T) {
assert.Contains(t, page, "Authenticated")
}

func TestBuildSuccessPage_CaseInsensitiveOrgBadge(t *testing.T) {
t.Parallel()

page := buildSuccessPage(callbackUser{
Login: "someone",
Orgs: []string{"EthPandaOps"},
})

assert.Contains(t, page, "ethpandaops")
assert.Contains(t, page, "org-badge")
}

func TestHasOrg(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -153,20 +175,29 @@ func TestBuildSuccessPage_WritePreview(t *testing.T) {
dir := filepath.Join(os.TempDir(), "panda-auth-preview")
require.NoError(t, os.MkdirAll(dir, 0o755))

art := " /\\_/\\\n ( o.o )\n > ^ <"
artB64 := base64.StdEncoding.EncodeToString([]byte(art))

cases := map[string]callbackUser{
"default.html": {
Login: "randomdev",
AvatarURL: "https://avatars.githubusercontent.com/u/1?v=4",
},
"ethpandaops.html": {
"org_gif.html": {
Login: "pandafan",
AvatarURL: "https://avatars.githubusercontent.com/u/1?v=4",
Orgs: []string{"ethpandaops"},
Tagline: "Enjoy debugging your devnet champ",
MediaType: "gif",
MediaURL: "https://media1.tenor.com/m/92A2K1kvoHcAAAAd/casino-royale-bond.gif",
},
"special_user.html": {
Login: "samcm",
AvatarURL: "https://avatars.githubusercontent.com/u/1?v=4",
Orgs: []string{"ethpandaops"},
"ascii_art.html": {
Login: "samcm",
AvatarURL: "https://avatars.githubusercontent.com/u/1?v=4",
Orgs: []string{"ethpandaops"},
Tagline: "Enjoy debugging your devnet champ",
MediaType: "ascii",
MediaASCIIB64: artB64,
},
}

Expand Down
9 changes: 5 additions & 4 deletions pkg/auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package auth

// Config holds OAuth server configuration for a local product edge.
type Config struct {
Enabled bool `yaml:"enabled"`
GitHub *GitHubConfig `yaml:"github,omitempty"`
AllowedOrgs []string `yaml:"allowed_orgs,omitempty"`
Tokens TokensConfig `yaml:"tokens"`
Enabled bool `yaml:"enabled"`
GitHub *GitHubConfig `yaml:"github,omitempty"`
AllowedOrgs []string `yaml:"allowed_orgs,omitempty"`
Tokens TokensConfig `yaml:"tokens"`
SuccessPage *SuccessPageConfig `yaml:"success_page,omitempty"`
}

// GitHubConfig holds GitHub OAuth configuration.
Expand Down
Loading
Loading