Pārlūkot izejas kodu

feature: mix of bits (#439)

* feature: mix of bits

* bugfix: codestyle
James Read 1 gadu atpakaļ
vecāks
revīzija
de81ec00fd

+ 4 - 2
internal/config/config.go

@@ -141,6 +141,7 @@ type Config struct {
 
 type OAuth2Provider struct {
 	Name          string
+	Title         string
 	ClientID      string
 	ClientSecret  string
 	Icon          string
@@ -152,8 +153,9 @@ type OAuth2Provider struct {
 }
 
 type NavigationLink struct {
-	Title string
-	Url   string
+	Title  string
+	Url    string
+	Target string
 }
 
 type SaveLogsConfig struct {

+ 1 - 0
internal/executor/arguments.go

@@ -138,6 +138,7 @@ func typecheckChoiceEntity(value string, arg *config.ActionArgument) error {
 // TypeSafetyCheck checks argument values match a specific type. The types are
 // defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
 // characters.
+//
 //gocyclo:ignore
 func TypeSafetyCheck(name string, value string, argumentType string) error {
 	switch argumentType {

+ 24 - 22
internal/httpservers/oauth2.go

@@ -31,10 +31,33 @@ func assignIfEmpty(target *string, value string) {
 	}
 }
 
+func oauth2Init(cfg *config.Config) {
+	for providerName, providerConfig := range cfg.AuthOAuth2Providers {
+		completeProviderConfig(providerName, providerConfig)
+
+		newConfig := &oauth2.Config{
+			ClientID:     providerConfig.ClientID,
+			ClientSecret: providerConfig.ClientSecret,
+			Scopes:       providerConfig.Scopes,
+			Endpoint: oauth2.Endpoint{
+				AuthURL:  providerConfig.AuthUrl,
+				TokenURL: providerConfig.TokenUrl,
+			},
+			RedirectURL: cfg.AuthOAuth2RedirectURL,
+		}
+
+		registeredProviders[providerName] = newConfig
+
+		log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
+	}
+}
+
 func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
 	dbConfig, ok := oauth2ProviderDatabase[providerName]
 
 	if ok {
+		assignIfEmpty(&providerConfig.Name, dbConfig.Name)
+		assignIfEmpty(&providerConfig.Title, dbConfig.Title)
 		assignIfEmpty(&providerConfig.WhoamiUrl, dbConfig.WhoamiUrl)
 		assignIfEmpty(&providerConfig.TokenUrl, dbConfig.TokenUrl)
 		assignIfEmpty(&providerConfig.AuthUrl, dbConfig.AuthUrl)
@@ -53,28 +76,7 @@ func getOAuth2Config(cfg *config.Config, providerName string) (*oauth2.Config, e
 	config, ok := registeredProviders[providerName]
 
 	if !ok {
-		providerConfig, ok := cfg.AuthOAuth2Providers[providerName]
-
-		if !ok {
-			return nil, fmt.Errorf("Provider not found in config: %v", providerName)
-		}
-
-		completeProviderConfig(providerName, providerConfig)
-
-		config = &oauth2.Config{
-			ClientID:     providerConfig.ClientID,
-			ClientSecret: providerConfig.ClientSecret,
-			Scopes:       providerConfig.Scopes,
-			Endpoint: oauth2.Endpoint{
-				AuthURL:  providerConfig.AuthUrl,
-				TokenURL: providerConfig.TokenUrl,
-			},
-			RedirectURL: cfg.AuthOAuth2RedirectURL,
-		}
-
-		registeredProviders[providerName] = config
-
-		log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
+		return nil, fmt.Errorf("Provider not found in config: %v", providerName)
 	}
 
 	return config, nil

+ 3 - 1
internal/httpservers/oauth2_providers.go

@@ -7,7 +7,9 @@ import (
 
 var oauth2ProviderDatabase = map[string]config.OAuth2Provider{
 	"github": {
-		Icon:          "github",
+		Title:         "GitHub",
+		Name:          "github",
+		Icon:          "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 128 128\"><g fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M64 5.103c-33.347 0-60.388 27.035-60.388 60.388c0 26.682 17.303 49.317 41.297 57.303c3.017.56 4.125-1.31 4.125-2.905c0-1.44-.056-6.197-.082-11.243c-16.8 3.653-20.345-7.125-20.345-7.125c-2.747-6.98-6.705-8.836-6.705-8.836c-5.48-3.748.413-3.67.413-3.67c6.063.425 9.257 6.223 9.257 6.223c5.386 9.23 14.127 6.562 17.573 5.02c.542-3.903 2.107-6.568 3.834-8.076c-13.413-1.525-27.514-6.704-27.514-29.843c0-6.593 2.36-11.98 6.223-16.21c-.628-1.52-2.695-7.662.584-15.98c0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033c11.526-7.813 16.59-6.19 16.59-6.19c3.287 8.317 1.22 14.46.593 15.98c3.872 4.23 6.215 9.617 6.215 16.21c0 23.194-14.127 28.3-27.574 29.796c2.167 1.874 4.097 5.55 4.097 11.183c0 8.08-.07 14.583-.07 16.572c0 1.607 1.088 3.49 4.148 2.897c23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z\" clip-rule=\"evenodd\"/><path d=\"M26.484 91.806c-.133.3-.605.39-1.035.185c-.44-.196-.685-.605-.543-.906c.13-.31.603-.395 1.04-.188c.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28c-.396-.42-.47-.983-.177-1.254c.298-.266.844-.14 1.24.28c.394.426.472.984.17 1.255zm2.382 3.477c-.37.258-.976.017-1.35-.52c-.37-.538-.37-1.183.01-1.44c.373-.258.97-.025 1.35.507c.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23c-.527-.487-.674-1.18-.343-1.544c.336-.366 1.045-.264 1.564.23c.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486c-.683-.207-1.13-.76-.99-1.238c.14-.477.823-.7 1.512-.485c.683.206 1.13.756.988 1.237m4.943.361c.017.498-.563.91-1.28.92c-.723.017-1.308-.387-1.315-.877c0-.503.568-.91 1.29-.924c.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117c-.7.13-1.35-.172-1.44-.653c-.086-.498.422-.997 1.122-1.126c.714-.123 1.354.17 1.444.663zm0 0\"/></g></svg>",
 		WhoamiUrl:     "https://api.github.com/user",
 		TokenUrl:      endpoints.GitHub.TokenURL,
 		AuthUrl:       endpoints.GitHub.AuthURL,

+ 2 - 0
internal/httpservers/singleFrontend.go

@@ -77,6 +77,8 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
 		})
 	}
 
+	oauth2Init(cfg)
+
 	srv := &http.Server{
 		Addr:    cfg.ListenAddressSingleHTTPFrontend,
 		Handler: mux,

+ 24 - 0
internal/httpservers/webuiServer.go

@@ -33,6 +33,8 @@ type webUISettings struct {
 	SshFoundConfig         string
 	EnableCustomJs         bool
 	AuthLoginUrl           string
+	AuthLocalLogin         bool
+	AuthOAuth2Providers    []publicOAuth2Provider
 	AdditionalLinks        []*config.NavigationLink
 }
 
@@ -105,6 +107,26 @@ func generateThemeCss(w http.ResponseWriter, r *http.Request) {
 	w.Write(customThemeCss)
 }
 
+type publicOAuth2Provider struct {
+	Name  string
+	Title string
+	Icon  string
+}
+
+func buildPublicOAuth2ProvidersList(cfg *config.Config) []publicOAuth2Provider {
+	var publicProviders []publicOAuth2Provider
+
+	for _, provider := range cfg.AuthOAuth2Providers {
+		publicProviders = append(publicProviders, publicOAuth2Provider{
+			Name:  provider.Name,
+			Title: provider.Title,
+			Icon:  provider.Icon,
+		})
+	}
+
+	return publicProviders
+}
+
 func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
 	jsonRet, _ := json.Marshal(webUISettings{
 		Rest:                   cfg.ExternalRestAddress + "/api/",
@@ -120,6 +142,8 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
 		SshFoundConfig:         installationinfo.Runtime.SshFoundConfig,
 		EnableCustomJs:         cfg.EnableCustomJs,
 		AuthLoginUrl:           cfg.AuthLoginUrl,
+		AuthLocalLogin:         true,
+		AuthOAuth2Providers:    buildPublicOAuth2ProvidersList(cfg),
 		AdditionalLinks:        cfg.AdditionalNavigationLinks,
 	})
 

+ 56 - 50
webui.dev/index.html

@@ -92,35 +92,6 @@
 				</fieldset>
 			</section>
 
-			<section id = "contentLogin" title = "Login" hidden>
-				<div class = "flex-col">
-				<form class = "box-shadow padded-content">
-					<h2>OAuth Login</h2>
-					<div>
-						<button>GitHub</button>
-						<button>Google</button>
-					</div>
-					<hr />
-					<h2>Local Login</h2>
-					<div class = "arguments">
-						<label for = "username">
-							<span>Username:</span>
-						</label>
-						<input type = "text" name = "username" />
-						<span></span>
-
-						<label for = "password">
-							<span>Password:</span>
-						</label>
-						<input type = "password" name = "password" />
-						<span></span>
-
-						<button type = "submit">Login</button>
-					</div>
-				</form>
-				</div>
-			</section>
-
 			<noscript>
 				<div class = "error">Sorry, JavaScript is required to use OliveTin.</div>
 			</noscript>
@@ -129,16 +100,16 @@
 		<footer title = "footer">
 			<p><img title = "application icon" src = "OliveTinLogo.png" alt = "OliveTin logo" height = "1em" class = "logo" /> OliveTin</p>
 			<p>
-				<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> |
-				<a href = "https://github.com/OliveTin/OliveTin/issues/new/choose" target = "_new">Raise an issue on GitHub</a> |
-				<span>Version: <span id = "currentVersion">?</span></span> |
-				<span>Server connection:
-					<span id = "serverConnectionRest">REST</span>,
-					<span id = "serverConnectionWebSocket">WebSocket</span>
-				</span>
+			<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> |
+			<a href = "https://github.com/OliveTin/OliveTin/issues/new/choose" target = "_new">Raise an issue on GitHub</a> |
+			<span>Version: <span id = "currentVersion">?</span></span> |
+			<span>Server connection:
+				<span id = "serverConnectionRest">REST</span>,
+				<span id = "serverConnectionWebSocket">WebSocket</span>
+			</span>
 			</p>
 			<p>
-				<a id = "available-version" href = "http://olivetin.app" target = "_blank" hidden>?</a>
+			<a id = "available-version" href = "http://olivetin.app" target = "_blank" hidden>?</a>
 			</p>
 		</footer>
 
@@ -159,11 +130,11 @@
 				</button>
 			</div>
 			<div id = "execution-dialog-basics" class = "padded-content-sides">
-					<strong>Duration: </strong><span id = "execution-dialog-duration">unknown</span>
+				<strong>Duration: </strong><span id = "execution-dialog-duration">unknown</span>
 			</div>
 			<div id = "execution-dialog-details" class = "padded-content-sides">
 				<p>
-					<strong>Status: </strong><span id = "execution-dialog-status">unknown</span>
+				<strong>Status: </strong><span id = "execution-dialog-status">unknown</span>
 				</p>
 			</div>
 
@@ -178,6 +149,41 @@
 			</div>
 		</dialog>
 
+		<template id = "tplLoginForm">
+			<section id = "content-login" title = "Login" hidden>
+				<div class = "flex-col">
+					<form class = "box-shadow padded-content border-radius">
+						<p class = "login-disabled">Login with OAuth or Local users is not enabled on this server.</p>
+
+						<div class = "login-oauth2" hidden>
+							<h2>OAuth Login</h2>
+						</div>
+
+						<br />
+
+						<div class = "login-local" hidden>
+							<h2>Local Login</h2>
+							<div class = "arguments">
+								<label for = "username">
+									<span>Username:</span>
+								</label>
+								<input type = "text" name = "username" />
+								<span></span>
+
+								<label for = "password">
+									<span>Password:</span>
+								</label>
+								<input type = "password" name = "password" />
+								<span></span>
+
+								<button type = "submit">Login</button>
+							</div>
+						</div>
+					</form>
+				</div>
+			</section>
+		</template>
+
 		<template id = "tplArgumentForm">
 			<dialog title = "Arguments" id = "argument-popup">
 				<form class = "action-arguments padded-content">
@@ -225,24 +231,24 @@
 
 			/**
 			This is the bootstrap code, which relies on very simple, old javascript
-		  	to at least display a helpful error message if we can't use OliveTin.
-			*/
+			to at least display a helpful error message if we can't use OliveTin.
+			 */
 			window.showBigError = function (type, friendlyType, message, isFatal) {
-			  bigErrorDialog.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/err-" + type + ".html' target = 'blank'/>" + type + " error in OliveTin Documentation</a></p>"
+				bigErrorDialog.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/err-" + type + ".html' target = 'blank'/>" + type + " error in OliveTin Documentation</a></p>"
 
-			  if (isFatal) {
-				  bigErrorDialog.innerHTML += '<p>You will need to refresh your browser to clear this message.</p>'
-			  } else {
-				  bigErrorDialog.innerHTML += '<p>This error message will go away automatically if the problem is solved.</p>'
-			  }
+				if (isFatal) {
+					bigErrorDialog.innerHTML += '<p>You will need to refresh your browser to clear this message.</p>'
+				} else {
+					bigErrorDialog.innerHTML += '<p>This error message will go away automatically if the problem is solved.</p>'
+				}
 
-			  bigErrorDialog.showModal()
+				bigErrorDialog.showModal()
 
-			  console.error('Error ' + type + ': ', message)
+				console.error('Error ' + type + ': ', message)
 			}
 
 			window.clearBigErrors = function () {
-			  bigErrorDialog.close()
+				bigErrorDialog.close()
 			}
 		</script>
 

+ 42 - 0
webui.dev/js/LoginForm.js

@@ -0,0 +1,42 @@
+export class LoginForm extends window.HTMLElement {
+  setup () {
+    const tpl = document.getElementById('tplLoginForm')
+    this.content = tpl.content.cloneNode(true)
+
+    this.appendChild(this.content)
+  }
+
+  processOAuth2Providers (providers) {
+    if (providers === null) {
+      return
+    }
+
+    if (providers.length > 0) {
+      this.querySelector('.login-oauth2').hidden = false
+      this.querySelector('.login-disabled').hidden = true
+
+      for (const provider of providers) {
+        const providerForm = document.createElement('form')
+        providerForm.method = 'GET'
+        providerForm.action = '/oauth2?provider=' + provider.Name
+
+        const providerButton = document.createElement('button')
+        providerButton.type = 'submit'
+        providerButton.innerHTML = '<span class = "oauth2-icon">' + provider.Icon + '</span> Login with ' + provider.Title
+
+        providerForm.appendChild(providerButton)
+
+        this.querySelector('.login-oauth2').appendChild(providerForm)
+      }
+    }
+  }
+
+  processLocalLogin (enabled) {
+    if (enabled) {
+      this.querySelector('.login-local').hidden = false
+      this.querySelector('.login-disabled').hidden = true
+    }
+  }
+}
+
+window.customElements.define('login-form', LoginForm)

+ 26 - 10
webui.dev/main.js

@@ -10,6 +10,8 @@ import {
 } from './js/marshaller.js'
 import { checkWebsocketConnection } from './js/websocket.js'
 
+import { LoginForm } from './js/LoginForm.js'
+
 function searchLogs (e) {
   document.getElementById('searchLogsClear').disabled = false
 
@@ -126,28 +128,42 @@ function processWebuiSettingsJson (settings) {
     if (titleElem) titleElem.innerText = window.pageTitle
   }
 
-  processAdditionaLinks(settings.AdditionalLinks)
+  processAdditionalLinks(settings.AdditionalLinks)
+
+  const loginForm = new LoginForm()
+  loginForm.setup()
+  loginForm.processOAuth2Providers(settings.AuthOAuth2Providers)
+  loginForm.processLocalLogin(settings.AuthLocalLogin)
+
+  document.getElementsByTagName('main')[0].appendChild(loginForm)
 
   window.settings = settings
 
   refreshDiagnostics()
 }
 
-function processAdditionaLinks (links) {
+function processAdditionalLinks (links) {
   if (links === null) {
     return
   }
 
-  for (const link of links) {
-    const linkA = document.createElement('a')
-    linkA.href = link.Url
-    linkA.innerText = link.Title
-    linkA.target = '_blank'
+  if (links.length > 0) {
+    for (const link of links) {
+      const linkA = document.createElement('a')
+      linkA.href = link.Url
+      linkA.innerText = link.Title
 
-    const linkLi = document.createElement('li')
-    linkLi.appendChild(linkA)
+      if (link.Target === '') {
+        linkA.target = '_blank'
+      } else {
+        linkA.target = link.Target
+      }
 
-    document.getElementById('supplemental-links').prepend(linkLi)
+      const linkLi = document.createElement('li')
+      linkLi.appendChild(linkA)
+
+      document.getElementById('supplemental-links').prepend(linkLi)
+    }
   }
 }
 

+ 41 - 16
webui.dev/style.css

@@ -177,7 +177,7 @@ nav.topbar ul li a {
 nav.sidebar ul li {
   list-style: none;
   text-align: left;
-  border-bottom: 1px inset black;
+  border-bottom: 1px solid #ccc;
 }
 
 table {
@@ -502,9 +502,12 @@ span.annotation-value {
   text-align: left;
 }
 
+.border-radius {
+  border-radius: .7em;
+}
+
 .box-shadow p {
   padding: .6em;
-  background-color: #fff;
   margin: 0;
 }
 
@@ -557,17 +560,12 @@ div.display {
   flex-direction: column;
 }
 
-#contentLogin form {
-  width: 40%;
-  place-self: center;
-}
-
-#contentLogin hr {
+#content-login hr {
   border: 0;
   border-top: 1px dashed #ccc;
 }
 
-#contentLogin button {
+#content-login button {
   display: block;
   width: 100%;
   grid-column: 1 / span 3;
@@ -576,10 +574,25 @@ div.display {
   background-color: #ced3d7;
 }
 
-#contentLogin button:last-child {
+#content-login button:last-child {
   margin-bottom: 0;
 }
 
+#content-login ul {
+  list-style: none;
+  padding: 0;
+}
+
+#content-login form {
+  width: auto;
+  place-self: center;
+}
+
+.oauth2-icon {
+  font-size: 1.8em;
+  vertical-align: middle;
+}
+
 @media screen and (width <= 600px) {
   fieldset {
     grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
@@ -622,10 +635,7 @@ div.display {
   }
 
   .box-shadow {
-    box-shadow: 0 0 6px 0 #ccc;
-  }
-
-  .box-shadow p {
+    box-shadow: 0 0 6px 0 #625c5c;
     background-color: #222;
   }
 
@@ -670,8 +680,17 @@ div.display {
     color: white;
   }
 
+  nav.sidebar ul li {
+    border-bottom: 1px solid #3e3e3e;
+  }
+
   nav ul li a:hover {
-    background-color: #666;
+    background-color: #1b5988;
+    color: white;
+  }
+
+  nav ul li a.selected {
+    background-color: #0c3351;
     color: white;
   }
 
@@ -692,7 +711,7 @@ div.display {
   }
 
   tr:hover td {
-    background-color: #666;
+    background-color: #1b5988;
   }
 
   div.toolbar {
@@ -702,4 +721,10 @@ div.display {
   div.display {
     box-shadow: 0 0 6px 0 #444;
   }
+
+  #content-login button {
+    background-color: #000;
+    border: 1px solid #666;
+    color: white;
+  }
 }