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 7 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
2 changes: 2 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.0
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 Down
2 changes: 2 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
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)
}
170 changes: 170 additions & 0 deletions internal/branding/branding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package branding

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

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

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()

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

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/data/storybook/?path=/story/universal-login--prompts", address))
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)
},
}).Parse(tenantDataAsset))

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)

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

go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
publisher.Pub(true)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Fatal(err)
}
}
}()

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

err = watcher.Add(filename)
if err != nil {
log.Fatal(err)
}

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 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>
33 changes: 33 additions & 0 deletions internal/branding/data/image-template.liquid
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="{{locale}}">
<head>
{%- auth0:head -%}
<style>
body {
background-image: url("https://images.unsplash.com/photo-1592450865877-e3a318ec3522?ixlib=rb-1.2.1&auto=format&fit=crop&w=2255&q=80");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.prompt-wrapper {
position: relative;
display: flex;
align-items: center;
width: 480px;
height: 100%;
justify-content: center;
background-color: rgb(60,60,60);
}
</style>
<title>{{ prompt.screen.texts.pageTitle }}</title>
</head>
<body>
{% if prompt.name == "login" or prompt.name == "signup" %}
<div class="prompt-wrapper">
{%- auth0:widget -%}
</div>
{% else %}
{%- auth0:widget -%}
{% endif %}
</body>
</html>

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Prism: Lightweight, robust, elegant syntax highlighting
*
* @license MIT <https://opensource.org/licenses/MIT>
* @author Lea Verou <https://lea.verou.me>
* @namespace
* @public
*/

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading