Add support for openid with github and facebook
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
play-with-docker
|
||||
node_modules
|
||||
docker-compose.single.yml
|
||||
lala
|
||||
|
||||
@@ -6,6 +6,12 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
oauth2FB "golang.org/x/oauth2/facebook"
|
||||
oauth2Github "golang.org/x/oauth2/github"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,12 +27,17 @@ const (
|
||||
var NameFilter = regexp.MustCompile(PWDHostPortGroupRegex)
|
||||
var AliasFilter = regexp.MustCompile(AliasPortGroupRegex)
|
||||
|
||||
var PortNumber, Key, Cert, SessionsFile, PWDContainerName, L2ContainerName, L2Subdomain, PWDCName, HashKey, SSHKeyPath, L2RouterIP, DindVolumeSize string
|
||||
var PortNumber, Key, Cert, SessionsFile, PWDContainerName, L2ContainerName, L2Subdomain, PWDCName, HashKey, SSHKeyPath, L2RouterIP, DindVolumeSize, CookieHashKey, CookieBlockKey string
|
||||
var UseLetsEncrypt, ExternalDindVolume, NoWindows bool
|
||||
var LetsEncryptCertsDir string
|
||||
var LetsEncryptDomains stringslice
|
||||
var MaxLoadAvg float64
|
||||
var ForceTLS bool
|
||||
var Providers map[string]*oauth2.Config
|
||||
var SecureCookie *securecookie.SecureCookie
|
||||
|
||||
var GithubClientID, GithubClientSecret string
|
||||
var FacebookClientID, FacebookClientSecret string
|
||||
|
||||
type stringslice []string
|
||||
|
||||
@@ -58,8 +69,46 @@ func ParseFlags() {
|
||||
flag.BoolVar(&ExternalDindVolume, "external-dind-volume", false, "Use external dind volume")
|
||||
flag.Float64Var(&MaxLoadAvg, "maxload", 100, "Maximum allowed load average before failing ping requests")
|
||||
flag.StringVar(&SSHKeyPath, "ssh_key_path", "", "SSH Private Key to use")
|
||||
flag.StringVar(&CookieHashKey, "cookie-hash-key", "", "Hash key to use to validate cookies")
|
||||
flag.StringVar(&CookieBlockKey, "cookie-block-key", "", "Block key to use to encrypt cookies")
|
||||
|
||||
flag.StringVar(&GithubClientID, "github-client-id", "", "Github OAuth Client ID")
|
||||
flag.StringVar(&GithubClientSecret, "github-client-secret", "", "Github OAuth Client Secret")
|
||||
|
||||
flag.StringVar(&FacebookClientID, "facebook-client-id", "", "Facebook OAuth Client ID")
|
||||
flag.StringVar(&FacebookClientSecret, "facebook-client-secret", "", "Facebook OAuth Client Secret")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
SecureCookie = securecookie.New([]byte(CookieHashKey), []byte(CookieBlockKey))
|
||||
|
||||
registerOAuthProviders()
|
||||
}
|
||||
|
||||
func registerOAuthProviders() {
|
||||
Providers = map[string]*oauth2.Config{}
|
||||
if GithubClientID != "" && GithubClientSecret != "" {
|
||||
conf := &oauth2.Config{
|
||||
ClientID: GithubClientID,
|
||||
ClientSecret: GithubClientSecret,
|
||||
Scopes: []string{"user:email"},
|
||||
Endpoint: oauth2Github.Endpoint,
|
||||
}
|
||||
|
||||
Providers["github"] = conf
|
||||
}
|
||||
if FacebookClientID != "" && FacebookClientSecret != "" {
|
||||
conf := &oauth2.Config{
|
||||
ClientID: FacebookClientID,
|
||||
ClientSecret: FacebookClientSecret,
|
||||
Scopes: []string{"email", "public_profile"},
|
||||
Endpoint: oauth2FB.Endpoint,
|
||||
}
|
||||
|
||||
Providers["facebook"] = conf
|
||||
}
|
||||
}
|
||||
|
||||
func GetDindImageName() string {
|
||||
dindImage := os.Getenv("DIND_IMAGE")
|
||||
defaultDindImageName := "franela/dind"
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
"github.com/play-with-docker/play-with-docker/config"
|
||||
"github.com/play-with-docker/play-with-docker/event"
|
||||
"github.com/play-with-docker/play-with-docker/pwd"
|
||||
"github.com/play-with-docker/play-with-docker/templates"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
@@ -33,9 +31,6 @@ func Bootstrap(c pwd.PWDApi, ev event.EventApi) {
|
||||
}
|
||||
|
||||
func Register(extend HandlerExtender) {
|
||||
|
||||
bypassCaptcha := len(os.Getenv("GOOGLE_RECAPTCHA_DISABLED")) > 0
|
||||
|
||||
server, err := socketio.NewServer(nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -81,17 +76,13 @@ func Register(extend HandlerExtender) {
|
||||
|
||||
// Generic routes
|
||||
r.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
if bypassCaptcha {
|
||||
http.ServeFile(rw, r, "./www/bypass.html")
|
||||
} else {
|
||||
welcome, tmplErr := templates.GetWelcomeTemplate()
|
||||
if tmplErr != nil {
|
||||
log.Fatal(tmplErr)
|
||||
}
|
||||
rw.Write(welcome)
|
||||
}
|
||||
http.ServeFile(rw, r, "./www/landing.html")
|
||||
}).Methods("GET")
|
||||
|
||||
r.HandleFunc("/oauth/providers", ListProviders).Methods("GET")
|
||||
r.HandleFunc("/oauth/providers/{provider}/login", Login).Methods("GET")
|
||||
r.HandleFunc("/oauth/providers/{provider}/callback", LoginCallback).Methods("GET")
|
||||
|
||||
corsRouter.HandleFunc("/", NewSession).Methods("POST")
|
||||
|
||||
if extend != nil {
|
||||
|
||||
40
handlers/cookie_id.go
Normal file
40
handlers/cookie_id.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/play-with-docker/play-with-docker/config"
|
||||
)
|
||||
|
||||
type CookieID struct {
|
||||
Id string `json:"id"`
|
||||
UserName string `json:"user_name"`
|
||||
UserAvatar string `json:"user_avatar"`
|
||||
}
|
||||
|
||||
func (c *CookieID) SetCookie(rw http.ResponseWriter) error {
|
||||
if encoded, err := config.SecureCookie.Encode("id", c); err == nil {
|
||||
cookie := &http.Cookie{
|
||||
Name: "id",
|
||||
Value: encoded,
|
||||
Path: "/",
|
||||
Secure: config.UseLetsEncrypt,
|
||||
}
|
||||
http.SetCookie(rw, cookie)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func ReadCookie(r *http.Request) (*CookieID, error) {
|
||||
if cookie, err := r.Cookie("id"); err == nil {
|
||||
value := &CookieID{}
|
||||
if err = config.SecureCookie.Decode("id", cookie.Value, &value); err == nil {
|
||||
return value, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
146
handlers/login.go
Normal file
146
handlers/login.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
"github.com/gorilla/mux"
|
||||
fb "github.com/huandu/facebook"
|
||||
"github.com/play-with-docker/play-with-docker/config"
|
||||
"github.com/play-with-docker/play-with-docker/pwd/types"
|
||||
)
|
||||
|
||||
func ListProviders(rw http.ResponseWriter, req *http.Request) {
|
||||
providers := []string{}
|
||||
for name, _ := range config.Providers {
|
||||
providers = append(providers, name)
|
||||
}
|
||||
json.NewEncoder(rw).Encode(providers)
|
||||
}
|
||||
|
||||
func Login(rw http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
providerName := vars["provider"]
|
||||
|
||||
provider, found := config.Providers[providerName]
|
||||
if !found {
|
||||
log.Printf("Could not find provider %s\n", providerName)
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
loginRequest, err := core.UserNewLoginRequest(providerName)
|
||||
if err != nil {
|
||||
log.Printf("Could not start a new user login request for provider %s. Got: %v\n", providerName, err)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if req.URL.Scheme != "" {
|
||||
scheme = req.URL.Scheme
|
||||
}
|
||||
host := "localhost"
|
||||
if req.URL.Host != "" {
|
||||
host = req.URL.Host
|
||||
}
|
||||
provider.RedirectURL = fmt.Sprintf("%s://%s/oauth/providers/%s/callback", scheme, host, providerName)
|
||||
url := provider.AuthCodeURL(loginRequest.Id)
|
||||
|
||||
http.Redirect(rw, req, url, http.StatusFound)
|
||||
}
|
||||
|
||||
func LoginCallback(rw http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
providerName := vars["provider"]
|
||||
|
||||
provider, found := config.Providers[providerName]
|
||||
if !found {
|
||||
log.Printf("Could not find provider %s\n", providerName)
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
query := req.URL.Query()
|
||||
|
||||
code := query.Get("code")
|
||||
loginRequestId := query.Get("state")
|
||||
|
||||
loginRequest, err := core.UserGetLoginRequest(loginRequestId)
|
||||
if err != nil {
|
||||
log.Printf("Could not get login request %s for provider %s. Got: %v\n", loginRequestId, providerName, err)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
tok, err := provider.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
log.Printf("Could not exchage code for access token for provider %s. Got: %v\n", providerName, err)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user := &types.User{Provider: providerName}
|
||||
if providerName == "github" {
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: tok.AccessToken},
|
||||
)
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
client := github.NewClient(tc)
|
||||
u, _, err := client.Users.Get(ctx, "")
|
||||
if err != nil {
|
||||
log.Printf("Could not get user from github. Got: %v\n", err)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.ProviderUserId = strconv.Itoa(u.GetID())
|
||||
user.Name = u.GetName()
|
||||
user.Avatar = u.GetAvatarURL()
|
||||
user.Email = u.GetEmail()
|
||||
} else if providerName == "facebook" {
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: tok.AccessToken},
|
||||
)
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
session := &fb.Session{
|
||||
Version: "v2.10",
|
||||
HttpClient: tc,
|
||||
}
|
||||
p := fb.Params{}
|
||||
p["fields"] = "email,name,picture"
|
||||
res, err := session.Get("/me", p)
|
||||
if err != nil {
|
||||
log.Printf("Could not get user from facebook. Got: %v\n", err)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.ProviderUserId = res.Get("id").(string)
|
||||
user.Name = res.Get("name").(string)
|
||||
user.Avatar = res.Get("picture.data.url").(string)
|
||||
user.Email = res.Get("email").(string)
|
||||
}
|
||||
|
||||
user, err = core.UserLogin(loginRequest, user)
|
||||
if err != nil {
|
||||
log.Printf("Could not login user. Got: %v\n", err)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cookieData := CookieID{Id: user.Id, UserName: user.Name, UserAvatar: user.Avatar}
|
||||
|
||||
if err := cookieData.SetCookie(rw); err != nil {
|
||||
log.Printf("Could not encode cookie. Got: %v\n", err)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(rw, req, "/", http.StatusFound)
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/play-with-docker/play-with-docker/config"
|
||||
"github.com/play-with-docker/play-with-docker/provisioner"
|
||||
"github.com/play-with-docker/play-with-docker/recaptcha"
|
||||
)
|
||||
|
||||
type NewSessionResponse struct {
|
||||
@@ -20,11 +19,17 @@ type NewSessionResponse struct {
|
||||
|
||||
func NewSession(rw http.ResponseWriter, req *http.Request) {
|
||||
req.ParseForm()
|
||||
if !recaptcha.IsHuman(req, rw) {
|
||||
|
||||
userId := ""
|
||||
if len(config.Providers) > 0 {
|
||||
cookie, err := ReadCookie(req)
|
||||
if err != nil {
|
||||
// User it not a human
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
userId = cookie.Id
|
||||
}
|
||||
|
||||
reqDur := req.Form.Get("session-duration")
|
||||
stack := req.Form.Get("stack")
|
||||
@@ -45,7 +50,7 @@ func NewSession(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
}
|
||||
duration := config.GetDuration(reqDur)
|
||||
s, err := core.SessionNew(duration, stack, stackName, imageName)
|
||||
s, err := core.SessionNew(userId, duration, stack, stackName, imageName)
|
||||
if err != nil {
|
||||
if provisioner.OutOfCapacity(err) {
|
||||
http.Redirect(rw, req, "/ooc", http.StatusFound)
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestClientNew(t *testing.T) {
|
||||
p := NewPWD(_f, _e, _s, sp, ipf)
|
||||
p.generator = _g
|
||||
|
||||
session, err := p.SessionNew(time.Hour, "", "", "")
|
||||
session, err := p.SessionNew("", time.Hour, "", "", "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
client := p.ClientNew("foobar", session)
|
||||
@@ -82,7 +82,7 @@ func TestClientCount(t *testing.T) {
|
||||
p := NewPWD(_f, _e, _s, sp, ipf)
|
||||
p.generator = _g
|
||||
|
||||
session, err := p.SessionNew(time.Hour, "", "", "")
|
||||
session, err := p.SessionNew("", time.Hour, "", "", "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
p.ClientNew("foobar", session)
|
||||
@@ -123,7 +123,7 @@ func TestClientResizeViewPort(t *testing.T) {
|
||||
p := NewPWD(_f, _e, _s, sp, ipf)
|
||||
p.generator = _g
|
||||
|
||||
session, err := p.SessionNew(time.Hour, "", "", "")
|
||||
session, err := p.SessionNew("", time.Hour, "", "", "")
|
||||
assert.Nil(t, err)
|
||||
client := p.ClientNew("foobar", session)
|
||||
_s.On("ClientFindBySessionId", "aaaabbbbcccc").Return([]*types.Client{client}, nil)
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestInstanceNew(t *testing.T) {
|
||||
p := NewPWD(_f, _e, _s, sp, ipf)
|
||||
p.generator = _g
|
||||
|
||||
session, err := p.SessionNew(time.Hour, "", "", "")
|
||||
session, err := p.SessionNew("", time.Hour, "", "", "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedInstance := types.Instance{
|
||||
@@ -138,7 +138,7 @@ func TestInstanceNew_WithNotAllowedImage(t *testing.T) {
|
||||
p := NewPWD(_f, _e, _s, sp, ipf)
|
||||
p.generator = _g
|
||||
|
||||
session, err := p.SessionNew(time.Hour, "", "", "")
|
||||
session, err := p.SessionNew("", time.Hour, "", "", "")
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -207,7 +207,7 @@ func TestInstanceNew_WithCustomHostname(t *testing.T) {
|
||||
p := NewPWD(_f, _e, _s, sp, ipf)
|
||||
p.generator = _g
|
||||
|
||||
session, err := p.SessionNew(time.Hour, "", "", "")
|
||||
session, err := p.SessionNew("", time.Hour, "", "", "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedInstance := types.Instance{
|
||||
|
||||
17
pwd/mock.go
17
pwd/mock.go
@@ -13,7 +13,7 @@ type Mock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *Mock) SessionNew(duration time.Duration, stack string, stackName, imageName string) (*types.Session, error) {
|
||||
func (m *Mock) SessionNew(userId string, duration time.Duration, stack string, stackName, imageName string) (*types.Session, error) {
|
||||
args := m.Called(duration, stack, stackName, imageName)
|
||||
return args.Get(0).(*types.Session), args.Error(1)
|
||||
}
|
||||
@@ -104,3 +104,18 @@ func (m *Mock) ClientCount() int {
|
||||
args := m.Called()
|
||||
return args.Int(0)
|
||||
}
|
||||
|
||||
func (m *Mock) UserNewLoginRequest(providerName string) (*types.LoginRequest, error) {
|
||||
args := m.Called(providerName)
|
||||
return args.Get(0).(*types.LoginRequest), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *Mock) UserGetLoginRequest(id string) (*types.LoginRequest, error) {
|
||||
args := m.Called(id)
|
||||
return args.Get(0).(*types.LoginRequest), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *Mock) UserLogin(loginRequest *types.LoginRequest, user *types.User) (*types.User, error) {
|
||||
args := m.Called(loginRequest, user)
|
||||
return args.Get(0).(*types.User), args.Error(1)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func SessionNotEmpty(e error) bool {
|
||||
}
|
||||
|
||||
type PWDApi interface {
|
||||
SessionNew(duration time.Duration, stack string, stackName, imageName string) (*types.Session, error)
|
||||
SessionNew(userId string, duration time.Duration, stack string, stackName, imageName string) (*types.Session, error)
|
||||
SessionClose(session *types.Session) error
|
||||
SessionGetSmallestViewPort(sessionId string) types.ViewPort
|
||||
SessionDeployStack(session *types.Session) error
|
||||
@@ -93,6 +93,10 @@ type PWDApi interface {
|
||||
ClientResizeViewPort(client *types.Client, cols, rows uint)
|
||||
ClientClose(client *types.Client)
|
||||
ClientCount() int
|
||||
|
||||
UserNewLoginRequest(providerName string) (*types.LoginRequest, error)
|
||||
UserGetLoginRequest(id string) (*types.LoginRequest, error)
|
||||
UserLogin(loginRequest *types.LoginRequest, user *types.User) (*types.User, error)
|
||||
}
|
||||
|
||||
func NewPWD(f docker.FactoryApi, e event.EventApi, s storage.StorageApi, sp provisioner.SessionProvisionerApi, ipf provisioner.InstanceProvisionerFactoryApi) *pwd {
|
||||
|
||||
@@ -44,7 +44,7 @@ type SessionSetupInstanceConf struct {
|
||||
Tls bool `json:"tls"`
|
||||
}
|
||||
|
||||
func (p *pwd) SessionNew(duration time.Duration, stack, stackName, imageName string) (*types.Session, error) {
|
||||
func (p *pwd) SessionNew(userId string, duration time.Duration, stack, stackName, imageName string) (*types.Session, error) {
|
||||
defer observeAction("SessionNew", time.Now())
|
||||
|
||||
s := &types.Session{}
|
||||
@@ -53,6 +53,7 @@ func (p *pwd) SessionNew(duration time.Duration, stack, stackName, imageName str
|
||||
s.ExpiresAt = s.CreatedAt.Add(duration)
|
||||
s.Ready = true
|
||||
s.Stack = stack
|
||||
s.UserId = userId
|
||||
|
||||
if s.Stack != "" {
|
||||
s.Ready = false
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestSessionNew(t *testing.T) {
|
||||
|
||||
before := time.Now()
|
||||
|
||||
s, e := p.SessionNew(time.Hour, "", "", "")
|
||||
s, e := p.SessionNew("", time.Hour, "", "", "")
|
||||
assert.Nil(t, e)
|
||||
assert.NotNil(t, s)
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestSessionNew(t *testing.T) {
|
||||
assert.WithinDuration(t, s.ExpiresAt, before.Add(time.Hour), time.Second)
|
||||
assert.True(t, s.Ready)
|
||||
|
||||
s, _ = p.SessionNew(time.Hour, "stackPath", "stackName", "imageName")
|
||||
s, _ = p.SessionNew("", time.Hour, "stackPath", "stackName", "imageName")
|
||||
|
||||
assert.Equal(t, "stackPath", s.Stack)
|
||||
assert.Equal(t, "stackName", s.StackName)
|
||||
|
||||
@@ -15,6 +15,7 @@ type Session struct {
|
||||
StackName string `json:"stack_name" bson:"stack_name"`
|
||||
ImageName string `json:"image_name" bson:"image_name"`
|
||||
Host string `json:"host" bson:"host"`
|
||||
UserId string `json:"user_id" bson:"user_id"`
|
||||
rw sync.Mutex `json:"-"`
|
||||
}
|
||||
|
||||
|
||||
15
pwd/types/user.go
Normal file
15
pwd/types/user.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package types
|
||||
|
||||
type User struct {
|
||||
Id string `json:"id" bson:"id"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
ProviderUserId string `json:"provider_user_id" bson:"provider_user_id"`
|
||||
Avatar string `json:"avatar" bson:"avatar"`
|
||||
Provider string `json:"provider" bson:"provider"`
|
||||
Email string `json:"email" bson:"email"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Id string `json:"id" bson:"id"`
|
||||
Provider string `json:"provider" bson:"provider"`
|
||||
}
|
||||
43
pwd/user.go
Normal file
43
pwd/user.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package pwd
|
||||
|
||||
import (
|
||||
"github.com/play-with-docker/play-with-docker/pwd/types"
|
||||
"github.com/play-with-docker/play-with-docker/storage"
|
||||
)
|
||||
|
||||
func (p *pwd) UserNewLoginRequest(providerName string) (*types.LoginRequest, error) {
|
||||
req := &types.LoginRequest{Id: p.generator.NewId(), Provider: providerName}
|
||||
if err := p.storage.LoginRequestPut(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (p *pwd) UserGetLoginRequest(id string) (*types.LoginRequest, error) {
|
||||
if req, err := p.storage.LoginRequestGet(id); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return req, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pwd) UserLogin(loginRequest *types.LoginRequest, user *types.User) (*types.User, error) {
|
||||
if err := p.storage.LoginRequestDelete(loginRequest.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := p.storage.UserFindByProvider(user.Provider, user.ProviderUserId)
|
||||
if err != nil {
|
||||
if storage.NotFound(err) {
|
||||
user.Id = p.generator.NewId()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
user.Id = u.Id
|
||||
}
|
||||
if err := p.storage.UserPut(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package recaptcha
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/play-with-docker/play-with-docker/config"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
func GetGoogleRecaptchaSiteKey() string {
|
||||
key := os.Getenv("GOOGLE_RECAPTCHA_SITE_KEY")
|
||||
if key == "" {
|
||||
// This is a development default. The environment variable should always be set in production.
|
||||
key = "6LeY_QsUAAAAAOlpVw4MhoLEr50h-dM80oz6M2AX"
|
||||
}
|
||||
return key
|
||||
}
|
||||
func GetGoogleRecaptchaSiteSecret() string {
|
||||
key := os.Getenv("GOOGLE_RECAPTCHA_SITE_SECRET")
|
||||
if key == "" {
|
||||
// This is a development default. The environment variable should always be set in production.
|
||||
key = "6LeY_QsUAAAAAHIALCtm0GKfk-UhtXoyJKarnRV8"
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
type recaptchaResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
var s = securecookie.New([]byte(config.HashKey), nil).MaxAge(int((1 * time.Hour).Seconds()))
|
||||
|
||||
func IsHuman(req *http.Request, rw http.ResponseWriter) bool {
|
||||
if os.Getenv("GOOGLE_RECAPTCHA_DISABLED") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
if cookie, _ := req.Cookie("session_id"); cookie != nil {
|
||||
var value string
|
||||
if err := s.Decode("session_id", cookie.Value, &value); err != nil {
|
||||
fmt.Println(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
challenge := req.Form.Get("g-recaptcha-response")
|
||||
|
||||
// Of X-Forwarded-For exists, it means we are behind a loadbalancer and we should use the real IP address of the user
|
||||
ip := req.Header.Get("X-Forwarded-For")
|
||||
if ip == "" {
|
||||
// Use the standard remote IP address of the request
|
||||
|
||||
ip = req.RemoteAddr
|
||||
}
|
||||
|
||||
parts := strings.Split(ip, ":")
|
||||
|
||||
resp, postErr := http.PostForm("https://www.google.com/recaptcha/api/siteverify", url.Values{"secret": {GetGoogleRecaptchaSiteSecret()}, "response": {challenge}, "remoteip": {parts[0]}})
|
||||
if postErr != nil {
|
||||
log.Println(postErr)
|
||||
// If there is a problem to connect to google, assume the user is a human so we don't block real users because of technical issues
|
||||
return true
|
||||
}
|
||||
|
||||
var r recaptchaResponse
|
||||
json.NewDecoder(resp.Body).Decode(&r)
|
||||
|
||||
if !r.Success {
|
||||
return false
|
||||
}
|
||||
|
||||
encoded, _ := s.Encode("session_id", xid.New().String())
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "session_id",
|
||||
Value: encoded,
|
||||
Expires: time.Now().Add(1 * time.Hour),
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
@@ -19,10 +20,13 @@ type DB struct {
|
||||
Instances map[string]*types.Instance `json:"instances"`
|
||||
Clients map[string]*types.Client `json:"clients"`
|
||||
WindowsInstances map[string]*types.WindowsInstance `json:"windows_instances"`
|
||||
LoginRequests map[string]*types.LoginRequest `json:"login_requests"`
|
||||
Users map[string]*types.User `json:"user"`
|
||||
|
||||
WindowsInstancesBySessionId map[string][]string `json:"windows_instances_by_session_id"`
|
||||
InstancesBySessionId map[string][]string `json:"instances_by_session_id"`
|
||||
ClientsBySessionId map[string][]string `json:"clients_by_session_id"`
|
||||
UsersByProvider map[string]string `json:"users_by_providers"`
|
||||
}
|
||||
|
||||
func (store *storage) SessionGet(id string) (*types.Session, error) {
|
||||
@@ -300,6 +304,56 @@ func (store *storage) ClientFindBySessionId(sessionId string) ([]*types.Client,
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func (store *storage) LoginRequestPut(loginRequest *types.LoginRequest) error {
|
||||
store.rw.Lock()
|
||||
defer store.rw.Unlock()
|
||||
|
||||
store.db.LoginRequests[loginRequest.Id] = loginRequest
|
||||
return nil
|
||||
}
|
||||
func (store *storage) LoginRequestGet(id string) (*types.LoginRequest, error) {
|
||||
store.rw.Lock()
|
||||
defer store.rw.Unlock()
|
||||
|
||||
if lr, found := store.db.LoginRequests[id]; !found {
|
||||
return nil, NotFoundError
|
||||
} else {
|
||||
return lr, nil
|
||||
}
|
||||
}
|
||||
func (store *storage) LoginRequestDelete(id string) error {
|
||||
store.rw.Lock()
|
||||
defer store.rw.Unlock()
|
||||
|
||||
delete(store.db.LoginRequests, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *storage) UserFindByProvider(providerName, providerUserId string) (*types.User, error) {
|
||||
store.rw.Lock()
|
||||
defer store.rw.Unlock()
|
||||
|
||||
if userId, found := store.db.UsersByProvider[fmt.Sprintf("%s_%s", providerName, providerUserId)]; !found {
|
||||
return nil, NotFoundError
|
||||
} else {
|
||||
if user, found := store.db.Users[userId]; !found {
|
||||
return nil, NotFoundError
|
||||
} else {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (store *storage) UserPut(user *types.User) error {
|
||||
store.rw.Lock()
|
||||
defer store.rw.Unlock()
|
||||
|
||||
store.db.UsersByProvider[fmt.Sprintf("%s_%s", user.Provider, user.ProviderUserId)] = user.Id
|
||||
store.db.Users[user.Id] = user
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *storage) load() error {
|
||||
file, err := os.Open(store.path)
|
||||
|
||||
@@ -316,9 +370,12 @@ func (store *storage) load() error {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,12 @@ func TestSessionPut(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
var loadedDB *DB
|
||||
|
||||
@@ -60,9 +63,12 @@ func TestSessionGet(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "pwd")
|
||||
@@ -96,9 +102,12 @@ func TestSessionGetAll(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "pwd")
|
||||
@@ -158,9 +167,12 @@ func TestInstanceGet(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{expectedInstance.Name: expectedInstance},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{expectedInstance.SessionId: []string{expectedInstance.Name}},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "pwd")
|
||||
@@ -209,9 +221,12 @@ func TestInstancePut(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{i.Name: i},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{i.SessionId: []string{i.Name}},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
var loadedDB *DB
|
||||
|
||||
@@ -269,9 +284,12 @@ func TestInstanceFindBySessionId(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{i1.Name: i1, i2.Name: i2},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{i1.SessionId: []string{i1.Name, i2.Name}},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "pwd")
|
||||
@@ -302,9 +320,12 @@ func TestWindowsInstanceGetAll(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{i1.Id: i1, i2.Id: i2},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{i1.SessionId: []string{i1.Id, i2.Id}},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "pwd")
|
||||
@@ -354,9 +375,12 @@ func TestWindowsInstancePut(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{i.Id: i},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{i.SessionId: []string{i.Id}},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
var loadedDB *DB
|
||||
|
||||
@@ -413,9 +437,12 @@ func TestClientGet(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{c.Id: c},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{c.SessionId: []string{c.Id}},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "pwd")
|
||||
@@ -464,9 +491,12 @@ func TestClientPut(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{c.Id: c},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{c.SessionId: []string{c.Id}},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
var loadedDB *DB
|
||||
|
||||
@@ -524,9 +554,12 @@ func TestClientFindBySessionId(t *testing.T) {
|
||||
Instances: map[string]*types.Instance{},
|
||||
Clients: map[string]*types.Client{c1.Id: c1, c2.Id: c2},
|
||||
WindowsInstances: map[string]*types.WindowsInstance{},
|
||||
LoginRequests: map[string]*types.LoginRequest{},
|
||||
Users: map[string]*types.User{},
|
||||
WindowsInstancesBySessionId: map[string][]string{},
|
||||
InstancesBySessionId: map[string][]string{},
|
||||
ClientsBySessionId: map[string][]string{c1.SessionId: []string{c1.Id, c2.Id}},
|
||||
UsersByProvider: map[string]string{},
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "pwd")
|
||||
|
||||
@@ -82,3 +82,23 @@ func (m *Mock) ClientFindBySessionId(sessionId string) ([]*types.Client, error)
|
||||
args := m.Called(sessionId)
|
||||
return args.Get(0).([]*types.Client), args.Error(1)
|
||||
}
|
||||
func (m *Mock) LoginRequestPut(loginRequest *types.LoginRequest) error {
|
||||
args := m.Called(loginRequest)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *Mock) LoginRequestGet(id string) (*types.LoginRequest, error) {
|
||||
args := m.Called(id)
|
||||
return args.Get(0).(*types.LoginRequest), args.Error(1)
|
||||
}
|
||||
func (m *Mock) LoginRequestDelete(id string) error {
|
||||
args := m.Called(id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *Mock) UserFindByProvider(providerName, providerUserId string) (*types.User, error) {
|
||||
args := m.Called(providerName, providerUserId)
|
||||
return args.Get(0).(*types.User), args.Error(1)
|
||||
}
|
||||
func (m *Mock) UserPut(user *types.User) error {
|
||||
args := m.Called(user)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
@@ -34,4 +34,11 @@ type StorageApi interface {
|
||||
ClientDelete(id string) error
|
||||
ClientCount() (int, error)
|
||||
ClientFindBySessionId(sessionId string) ([]*types.Client, error)
|
||||
|
||||
LoginRequestPut(loginRequest *types.LoginRequest) error
|
||||
LoginRequestGet(id string) (*types.LoginRequest, error)
|
||||
LoginRequestDelete(id string) error
|
||||
|
||||
UserFindByProvider(providerName, providerUserId string) (*types.User, error)
|
||||
UserPut(user *types.User) error
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
|
||||
"github.com/play-with-docker/play-with-docker/recaptcha"
|
||||
)
|
||||
|
||||
func GetWelcomeTemplate() ([]byte, error) {
|
||||
welcomeTemplate, tplErr := template.New("welcome").ParseFiles("www/welcome.html")
|
||||
if tplErr != nil {
|
||||
return nil, tplErr
|
||||
}
|
||||
var b bytes.Buffer
|
||||
tplExecuteErr := welcomeTemplate.ExecuteTemplate(&b, "GOOGLE_RECAPTCHA_SITE_KEY", recaptcha.GetGoogleRecaptchaSiteKey())
|
||||
if tplExecuteErr != nil {
|
||||
return nil, tplExecuteErr
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
});
|
||||
|
||||
|
||||
$scope.showAlert = function(title, content, parent) {
|
||||
$scope.showAlert = function(title, content, parent, cb) {
|
||||
$mdDialog.show(
|
||||
$mdDialog.alert()
|
||||
.parent(angular.element(document.querySelector(parent || '#popupContainer')))
|
||||
@@ -86,7 +86,11 @@
|
||||
.title(title)
|
||||
.textContent(content)
|
||||
.ok('Got it!')
|
||||
);
|
||||
).finally(function() {
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.resize = function(geometry) {
|
||||
@@ -206,7 +210,9 @@
|
||||
});
|
||||
|
||||
socket.on('session end', function() {
|
||||
$scope.showAlert('Session timed out!', 'Your session has expired and all of your instances have been deleted.', '#sessionEnd')
|
||||
$scope.showAlert('Session timed out!', 'Your session has expired and all of your instances have been deleted.', '#sessionEnd', function() {
|
||||
window.location.href = '/';
|
||||
});
|
||||
$scope.isAlive = false;
|
||||
});
|
||||
|
||||
|
||||
79
www/assets/landing.css
Normal file
79
www/assets/landing.css
Normal file
@@ -0,0 +1,79 @@
|
||||
/* Space out content a bit */
|
||||
body {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Everything but the jumbotron gets side spacing for mobile first views */
|
||||
.header,
|
||||
.marketing,
|
||||
.footer {
|
||||
padding-right: 1rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
/* Custom page header */
|
||||
.header {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: .05rem solid #e5e5e5;
|
||||
}
|
||||
/* Make the masthead heading the same height as the navigation */
|
||||
.header h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
/* Custom page footer */
|
||||
.footer {
|
||||
padding-top: 1.5rem;
|
||||
color: #777;
|
||||
border-top: .05rem solid #e5e5e5;
|
||||
}
|
||||
|
||||
/* Customize container */
|
||||
@media (min-width: 48em) {
|
||||
.container {
|
||||
max-width: 46rem;
|
||||
}
|
||||
}
|
||||
.container-narrow > hr {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Main marketing message and sign up button */
|
||||
.jumbotron {
|
||||
text-align: center;
|
||||
border-bottom: .05rem solid #e5e5e5;
|
||||
}
|
||||
.jumbotron .btn {
|
||||
padding: .75rem 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Supporting marketing content */
|
||||
.marketing {
|
||||
margin: 3rem 0;
|
||||
}
|
||||
.marketing p + h4 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Responsive: Portrait tablets and up */
|
||||
@media screen and (min-width: 48em) {
|
||||
/* Remove the padding we set earlier */
|
||||
.header,
|
||||
.marketing,
|
||||
.footer {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
/* Space out the masthead */
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
/* Remove the bottom border on the jumbotron for visual effect */
|
||||
.jumbotron {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
116
www/landing.html
Normal file
116
www/landing.html
Normal file
@@ -0,0 +1,116 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="PWDLanding" ng-controller="LoginController">
|
||||
<head>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular-cookies.js"></script>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>Play with Docker</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
|
||||
|
||||
<!-- Custom styles for this template -->
|
||||
<link href="/assets/landing.css" rel="stylesheet">
|
||||
<script>
|
||||
angular.module('PWDLanding', ['ngCookies'])
|
||||
.controller('LoginController', ['$cookies', '$scope', '$http', function($cookies, $scope, $http) {
|
||||
$scope.providers = [];
|
||||
$scope.loggedIn = $cookies.get('id') !== undefined;
|
||||
|
||||
$http({
|
||||
method: 'GET',
|
||||
url: '/oauth/providers'
|
||||
}).then(function(response) {
|
||||
$scope.providers = response.data;
|
||||
if ($scope.providers.length == 0) {
|
||||
$scope.loggedIn = true;
|
||||
}
|
||||
}, function(response) {
|
||||
console.log('ERROR', response);
|
||||
});
|
||||
|
||||
$scope.start = function() {
|
||||
function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, "\\$&");
|
||||
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
var stack = getParameterByName('stack');
|
||||
if (stack) {
|
||||
document.getElementById('stack').value = stack;
|
||||
}
|
||||
var stackName = getParameterByName('stack_name');
|
||||
if (stackName) {
|
||||
document.getElementById('stack_name').value = stackName;
|
||||
}
|
||||
var imageName = getParameterByName('image_name');
|
||||
if (imageName) {
|
||||
document.getElementById('image_name').value = imageName;
|
||||
}
|
||||
document.getElementById('landingForm').submit();
|
||||
}
|
||||
}]);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="header clearfix">
|
||||
<nav>
|
||||
<ul class="nav nav-pills float-right">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://github.com/play-with-docker/play-with-docker">Contribute</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="jumbotron" ng-cloak>
|
||||
<img src="https://www.docker.com/sites/default/files/Whale%20Logo332_5.png" />
|
||||
<h1 class="display-3">Play with Docker</h1>
|
||||
<p class="lead">A simple, interactive and fun playground to learn Docker</p>
|
||||
<div ng-hide="loggedIn" class="btn-group" role="group">
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Login
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<a ng-repeat="provider in providers" class="dropdown-item" href="/oauth/providers/{{provider}}/login">{{provider}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<form id="landingForm" method="POST" action="/">
|
||||
<p ng-show="loggedIn"><a class="btn btn-lg btn-success" href="#" ng-click="start()" role="button">Start</a></p>
|
||||
<input id="stack" type="hidden" name="stack" value=""/>
|
||||
<input id="stack_name" type="hidden" name="stack_name" value=""/>
|
||||
<input id="image_name" type="hidden" name="image_name" value=""/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row marketing">
|
||||
<div class="col-lg-12">
|
||||
<p>Play with Docker (PWD) is a project hacked by <a href="https://www.twitter.com/marcosnils">Marcos Liljedhal</a> and <a href="https://www.twitter.com/xetorthio">Jonathan Leibiusky</a> and sponsored by Docker Inc.</p>
|
||||
<p>PWD is a Docker playground which allows users to run Docker commands in a matter of seconds. It gives the experience of having a free Alpine Linux Virtual Machine in browser, where you can build and run Docker containers and even create clusters in <a href="https://docs.docker.com/engine/swarm/">Docker Swarm Mode</a>. Under the hood Docker-in-Docker (DinD) is used to give the effect of multiple VMs/PCs. In addition to the playground, PWD also includes a training site composed of a large set of Docker labs and quizzes from beginner to advanced level available at <a href="http://training.play-with-docker.com/">training.play-with-docker.com</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© Play with Docker 2017</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
{{define "GOOGLE_RECAPTCHA_SITE_KEY"}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Docker Playground</title>
|
||||
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css">
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
<script src='https://www.google.com/recaptcha/api.js'></script>
|
||||
</head>
|
||||
<body class="welcome">
|
||||
<div>
|
||||
<h1>Welcome!</h1>
|
||||
<h2>Before starting we need to verify you are a human</h2>
|
||||
<form id="welcomeForm" method="POST" action="/">
|
||||
<div id="recaptcha" class="g-recaptcha" data-callback="iAmHuman" data-sitekey="{{.}}"></div>
|
||||
<input id="stack" type="hidden" name="stack" value=""/>
|
||||
<input id="stack_name" type="hidden" name="stack_name" value=""/>
|
||||
<input id="image_name" type="hidden" name="image_name" value=""/>
|
||||
<button id="create" style="display:none;">Create session</button>
|
||||
</form>
|
||||
<img src="/assets/full_horizontal.svg" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function iAmHuman(resp) {
|
||||
document.getElementById('welcomeForm').submit();
|
||||
}
|
||||
function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, "\\$&");
|
||||
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
var stack = getParameterByName('stack');
|
||||
if (stack) {
|
||||
document.getElementById('stack').value = stack;
|
||||
}
|
||||
var stackName = getParameterByName('stack_name');
|
||||
if (stackName) {
|
||||
document.getElementById('stack_name').value = stackName;
|
||||
}
|
||||
var imageName = getParameterByName('image_name');
|
||||
if (imageName) {
|
||||
document.getElementById('image_name').value = imageName;
|
||||
}
|
||||
if (document.cookie.indexOf('session_id') > -1) {
|
||||
document.getElementById('create').style = "";
|
||||
document.getElementById('recaptcha').style = "display:none;";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user