Skip to content
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

[CLI-135] feat: add custom template edition w/preview #266

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/golang/mock v1.5.0
github.com/golang/snappy v0.0.3 // indirect
github.com/google/go-cmp v0.5.5
github.com/guiguan/caster v0.0.0-20191104051807-3736c4464f38
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.11.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
Expand All @@ -23,8 +24,10 @@ require (
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pierrec/lz4/v4 v4.1.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rjeczalik/notify v0.9.2
github.com/spf13/cobra v1.1.3
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.0
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/guiguan/caster v0.0.0-20191104051807-3736c4464f38 h1:oWETJozNAt29o9b03jPJ8mjQTk8XklRXEZiXBECoNpg=
github.com/guiguan/caster v0.0.0-20191104051807-3736c4464f38/go.mod h1:giU/iWwQIOg/ND1ecR8raoyROxojrXL9osppnuI7MRY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
Expand Down Expand Up @@ -383,6 +385,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.3 h1:/dvQpkb0o1pVlSgKNQqfkavlnXaIK+hJ0LXsKRUN9D4=
github.com/pierrec/lz4/v4 v4.1.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
Expand All @@ -408,6 +412,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8=
github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
Expand Down Expand Up @@ -599,6 +605,7 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
3 changes: 2 additions & 1 deletion internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ var requiredScopes = []string{
"create:clients", "delete:clients", "read:clients", "update:clients",
"create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers",
"create:rules", "delete:rules", "read:rules", "update:rules",
"read:client_keys", "read:logs", "read:users", "update:users",
"read:client_keys", "read:logs", "read:users", "update:users", "read:branding", "update:branding",
"read:tenant_settings",
}

// RequiredScopes returns the scopes used for login.
Expand Down
4 changes: 4 additions & 0 deletions internal/auth0/auth0.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ type API struct {
ActionVersion ActionVersionAPI
ActionExecution ActionExecutionAPI
ActionBinding ActionBindingAPI
Branding BrandingAPI
Client ClientAPI
Log LogAPI
ResourceServer ResourceServerAPI
Rule RuleAPI
Tenant TenantAPI
User UserAPI
}

Expand All @@ -25,10 +27,12 @@ func NewAPI(m *management.Management) *API {
ActionVersion: m.ActionVersion,
ActionExecution: m.ActionExecution,
ActionBinding: m.ActionBinding,
Branding: m.Branding,
Client: m.Client,
Log: m.Log,
ResourceServer: m.ResourceServer,
Rule: m.Rule,
Tenant: m.Tenant,
User: m.User,
}
}
Expand Down
11 changes: 11 additions & 0 deletions internal/auth0/branding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:generate mockgen -source=branding.go -destination=branding_mock.go -package=auth0
package auth0

import "gopkg.in/auth0.v5/management"

type BrandingAPI interface {
Read(opts ...management.RequestOption) (b *management.Branding, err error)
UniversalLogin(opts ...management.RequestOption) (ul *management.BrandingUniversalLogin, err error)
SetUniversalLogin(ul *management.BrandingUniversalLogin, opts ...management.RequestOption) (err error)
DeleteUniversalLogin(opts ...management.RequestOption) (err error)
}
8 changes: 8 additions & 0 deletions internal/auth0/tenant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:generate mockgen -source=tenant.go -destination=tenant_mock.go -package=auth0
package auth0

import "gopkg.in/auth0.v5/management"

type TenantAPI interface {
Read(opts ...management.RequestOption) (t *management.Tenant, err error)
}
162 changes: 162 additions & 0 deletions internal/branding/branding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package branding

import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"path/filepath"
"text/template"
"time"

"github.com/auth0/auth0-cli/internal/open"
"github.com/guiguan/caster"
"github.com/phayes/freeport"
"github.com/rjeczalik/notify"
)

type Client struct {
Id string `json:"id"`
Name string `json:"name"`
LogoUrl string `json:"logo_url,omitempty"`
}

type TemplateData struct {
Filename string
Clients []Client
PrimaryColor string
BackgroundColor string
LogoURL string
TenantName string
Body string
}

func PreviewCustomTemplate(ctx context.Context, templateData TemplateData) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()

address := "localhost"
port, err := freeport.GetFreePort()
if err != nil {
return
}

listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", address, port))
if err != nil {
return
}

requestTimeout := 10 * time.Minute
server := &http.Server{
Handler: buildRoutes(ctx, requestTimeout, templateData),
ReadTimeout: requestTimeout + 1*time.Minute,
WriteTimeout: requestTimeout + 1*time.Minute,
}
defer server.Close()

go func() {
if err = server.Serve(listener); err != http.ErrServerClosed {
cancel()
}
}()

err = open.URL(fmt.Sprintf("http://%s:%d/data/storybook/?path=/story/universal-login--prompts", address, port))
if err == nil {
return
}

// Wait until the file is closed or input is cancelled
<-ctx.Done()
}

func buildRoutes(ctx context.Context, requestTimeout time.Duration, templateData TemplateData) *http.ServeMux {
router := http.NewServeMux()

// Long polling waiting for file changes
broadcaster := broadcastCustomTemplateChanges(ctx, templateData.Filename)
router.HandleFunc("/dynamic/events", func(w http.ResponseWriter, r *http.Request) {
changes, _ := broadcaster.Sub(r.Context(), 1)
defer broadcaster.Unsub(changes)

var err error
select {
case <-r.Context().Done():
w.WriteHeader(http.StatusGone)
_, err = w.Write([]byte("410 - Gone"))
case <-time.After(requestTimeout):
w.WriteHeader(http.StatusRequestTimeout)
_, err = w.Write([]byte("408 - Request Timeout"))
case <-changes:
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte("200 - OK"))
}

if err != nil {
http.Error(w, err.Error(), 500)
}
})

// The template file
router.HandleFunc("/dynamic/template", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, templateData.Filename)
})

jstmpl := template.Must(template.New("tenant-data.js").Funcs(template.FuncMap{
"asJS": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
}).ParseFS(tenantDataAsset, "data/tenant-data.js"))

router.HandleFunc("/dynamic/tenant-data", func(w http.ResponseWriter, r *http.Request) {
err := jstmpl.Execute(w, templateData)
if err != nil {
http.Error(w, err.Error(), 500)
}
})

// Storybook assets
router.Handle("/", http.FileServer(http.FS(templatePreviewAssets)))

return router
}

func broadcastCustomTemplateChanges(ctx context.Context, filename string) *caster.Caster {
publisher := caster.New(ctx)

dir, file := filepath.Split(filename)
c := make(chan notify.EventInfo)
if err := notify.Watch(dir, c, notify.Write); err != nil {
return publisher
}

go func() {
for eventInfo := range c {
if filepath.Base(eventInfo.Path()) == file {
publisher.Pub(true)
}
}
}()

// release resources when the file is closed or the input is cancelled
go func() {
<-ctx.Done()
notify.Stop(c)
close(c)
}()

return publisher
}

func DefaultTemplate() string {
return defaultTemplate
}

func FooterTemplate() string {
return footerTemplate
}

func ImageTemplate() string {
return imageTemplate
}
23 changes: 23 additions & 0 deletions internal/branding/branding_embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package branding

import (
"embed"
_ "embed"
)

var (
//go:embed data/storybook/*
templatePreviewAssets embed.FS

//go:embed data/tenant-data.js
tenantDataAsset embed.FS

//go:embed data/default-template.liquid
defaultTemplate string

//go:embed data/footer-template.liquid
footerTemplate string

//go:embed data/image-template.liquid
imageTemplate string
)
9 changes: 9 additions & 0 deletions internal/branding/data/default-template.liquid
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
{%- auth0:head -%}
</head>
<body>
{%- auth0:widget -%}
</body>
</html>
55 changes: 55 additions & 0 deletions internal/branding/data/footer-template.liquid
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="{{locale}}">
<head>
{%- auth0:head -%}
<style>
body {
background-image: radial-gradient(white, rgb(200, 200, 200));
}
.footer {
background-color: rgb(120, 120, 120);
position: absolute;
bottom: 0;
left: 0;
padding: 16px 0;
width: 100%;
color: white;
/* Use a high z-index for future-proofing */
z-index: 10;
}
.footer ul {
text-align: center;
}
.footer ul li {
display: inline-block;
margin: 0 4px;
}
.footer ul li:not(:first-of-type) {
margin-left: 0;
}
.footer ul li:not(:first-of-type)::before {
content: '';
display: inline-block;
vertical-align: middle;
width: 4px;
height: 4px;
margin-right: 4px;
background-color: white;
border-radius: 50%;
}
.footer a {
color: white;
}
</style>
<title>{{ prompt.screen.texts.pageTitle }}</title>
</head>
<body class="_widget-auto-layout">
{%- auth0:widget -%}
<footer class="footer">
<ul>
<li><a href="https://company.com/privacy">Privacy Policy</a></li>
<li><a href="https://company.com/terms">Terms of Service</a></li>
</ul>
</footer>
</body>
</html>
Loading