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 #267

Merged
merged 24 commits into from
Apr 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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 @@ -9,10 +9,12 @@ require (
github.com/briandowns/spinner v1.12.1-0.20210421154440-eac1b899fcf1
github.com/charmbracelet/glamour v0.2.0
github.com/fatih/color v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/getsentry/sentry-go v0.10.0
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 @@ -34,6 +36,7 @@ require (
github.com/zalando/go-keyring v0.1.1
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
golang.org/x/text v0.3.5 // indirect
Expand Down
4 changes: 4 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 @@ -595,6 +597,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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=
Expand Down
4 changes: 3 additions & 1 deletion internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ 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:users", "update:users",
"read:branding", "update:branding",
"read:client_keys", "read:logs", "read:tenant_settings",
}

// RequiredScopes returns the scopes used for login.
Expand Down
3 changes: 3 additions & 0 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ func TestRequiredScopes(t *testing.T) {
t.Run("verify special scopes", func(t *testing.T) {
list := []string{
"read:client_keys", "read:logs",
"read:users", "update:users",
"read:branding", "update:branding",
"read:client_keys", "read:logs", "read:tenant_settings",
}

for _, v := range list {
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)
}
174 changes: 174 additions & 0 deletions internal/branding/branding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package branding

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

"github.com/auth0/auth0-cli/internal/open"
"github.com/fsnotify/fsnotify"
"github.com/guiguan/caster"
)

// Client is a minimal representation of an auth0 Client as defined in the
// management API. This is used within the branding machinery to populate the
// tenant data.
type Client struct {
ID string `json:"id"`
Name string `json:"name"`
LogoURL string `json:"logo_url,omitempty"`
}

// TemplateData contains all the variables we project onto our embedded go
// template. These variables largely resemble the same ones in the auth0
// branding template.
type TemplateData struct {
Filename string
Clients []Client
PrimaryColor string
BackgroundColor string
LogoURL string
TenantName string
Body string
}

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

listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
defer listener.Close()

// Long polling waiting for file changes
broadcaster, err := broadcastCustomTemplateChanges(ctx, data.Filename)
if err != nil {
return err
}

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

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

u := &url.URL{
Scheme: "http",
Host: listener.Addr().String(),
Path: "/data/storybook/",
RawQuery: (url.Values{
"path": []string{"/story/universal-login--prompts"},
}).Encode(),
}

if err := open.URL(u.String()); err != nil {
return err
}

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

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

router.HandleFunc("/dynamic/events", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

changes, _ := broadcaster.Sub(ctx, 1)
defer broadcaster.Unsub(changes)

writeStatus := func(w http.ResponseWriter, code int) {
msg := fmt.Sprintf("%d - %s", code, http.StatusText(http.StatusGone))
http.Error(w, msg, code)
}

select {
case <-ctx.Done():
writeStatus(w, http.StatusGone)
case <-time.After(requestTimeout):
writeStatus(w, http.StatusRequestTimeout)
case <-changes:
writeStatus(w, http.StatusOK)
}
})

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

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

router.HandleFunc("/dynamic/tenant-data", func(w http.ResponseWriter, r *http.Request) {
err := jstmpl.Execute(w, data)
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, error) {
publisher := caster.New(ctx)

watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}

go func() {
for {
select {
case _, ok := <-watcher.Events:
if !ok {
return
}
publisher.Pub(true)

case _, ok := <-watcher.Errors:
if !ok {
return
}
}
}
}()

go func() {
<-ctx.Done()
watcher.Close()
publisher.Close()
}()

if err := watcher.Add(filepath.Dir(filename)); err != nil {
return nil, err
}

return publisher, nil
}
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 string

//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