feature: mix of bits (#439)

* feature: mix of bits

* bugfix: codestyle
This commit is contained in:
James Read 2024-10-15 08:47:51 +01:00 committed by GitHub
parent 7cc07158ab
commit de81ec00fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 223 additions and 101 deletions

View File

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

View File

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

View File

@ -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) { func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
dbConfig, ok := oauth2ProviderDatabase[providerName] dbConfig, ok := oauth2ProviderDatabase[providerName]
if ok { if ok {
assignIfEmpty(&providerConfig.Name, dbConfig.Name)
assignIfEmpty(&providerConfig.Title, dbConfig.Title)
assignIfEmpty(&providerConfig.WhoamiUrl, dbConfig.WhoamiUrl) assignIfEmpty(&providerConfig.WhoamiUrl, dbConfig.WhoamiUrl)
assignIfEmpty(&providerConfig.TokenUrl, dbConfig.TokenUrl) assignIfEmpty(&providerConfig.TokenUrl, dbConfig.TokenUrl)
assignIfEmpty(&providerConfig.AuthUrl, dbConfig.AuthUrl) assignIfEmpty(&providerConfig.AuthUrl, dbConfig.AuthUrl)
@ -53,28 +76,7 @@ func getOAuth2Config(cfg *config.Config, providerName string) (*oauth2.Config, e
config, ok := registeredProviders[providerName] config, ok := registeredProviders[providerName]
if !ok { if !ok {
providerConfig, ok := cfg.AuthOAuth2Providers[providerName] return nil, fmt.Errorf("Provider not found in config: %v", 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 config, nil return config, nil

View File

@ -7,7 +7,9 @@ import (
var oauth2ProviderDatabase = map[string]config.OAuth2Provider{ var oauth2ProviderDatabase = map[string]config.OAuth2Provider{
"github": { "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", WhoamiUrl: "https://api.github.com/user",
TokenUrl: endpoints.GitHub.TokenURL, TokenUrl: endpoints.GitHub.TokenURL,
AuthUrl: endpoints.GitHub.AuthURL, AuthUrl: endpoints.GitHub.AuthURL,

View File

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

View File

@ -33,6 +33,8 @@ type webUISettings struct {
SshFoundConfig string SshFoundConfig string
EnableCustomJs bool EnableCustomJs bool
AuthLoginUrl string AuthLoginUrl string
AuthLocalLogin bool
AuthOAuth2Providers []publicOAuth2Provider
AdditionalLinks []*config.NavigationLink AdditionalLinks []*config.NavigationLink
} }
@ -105,6 +107,26 @@ func generateThemeCss(w http.ResponseWriter, r *http.Request) {
w.Write(customThemeCss) 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) { func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
jsonRet, _ := json.Marshal(webUISettings{ jsonRet, _ := json.Marshal(webUISettings{
Rest: cfg.ExternalRestAddress + "/api/", Rest: cfg.ExternalRestAddress + "/api/",
@ -120,6 +142,8 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
SshFoundConfig: installationinfo.Runtime.SshFoundConfig, SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
EnableCustomJs: cfg.EnableCustomJs, EnableCustomJs: cfg.EnableCustomJs,
AuthLoginUrl: cfg.AuthLoginUrl, AuthLoginUrl: cfg.AuthLoginUrl,
AuthLocalLogin: true,
AuthOAuth2Providers: buildPublicOAuth2ProvidersList(cfg),
AdditionalLinks: cfg.AdditionalNavigationLinks, AdditionalLinks: cfg.AdditionalNavigationLinks,
}) })

View File

@ -92,35 +92,6 @@
</fieldset> </fieldset>
</section> </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> <noscript>
<div class = "error">Sorry, JavaScript is required to use OliveTin.</div> <div class = "error">Sorry, JavaScript is required to use OliveTin.</div>
</noscript> </noscript>
@ -129,16 +100,16 @@
<footer title = "footer"> <footer title = "footer">
<p><img title = "application icon" src = "OliveTinLogo.png" alt = "OliveTin logo" height = "1em" class = "logo" /> OliveTin</p> <p><img title = "application icon" src = "OliveTinLogo.png" alt = "OliveTin logo" height = "1em" class = "logo" /> OliveTin</p>
<p> <p>
<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> | <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> | <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>Version: <span id = "currentVersion">?</span></span> |
<span>Server connection: <span>Server connection:
<span id = "serverConnectionRest">REST</span>, <span id = "serverConnectionRest">REST</span>,
<span id = "serverConnectionWebSocket">WebSocket</span> <span id = "serverConnectionWebSocket">WebSocket</span>
</span> </span>
</p> </p>
<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> </p>
</footer> </footer>
@ -159,11 +130,11 @@
</button> </button>
</div> </div>
<div id = "execution-dialog-basics" class = "padded-content-sides"> <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>
<div id = "execution-dialog-details" class = "padded-content-sides"> <div id = "execution-dialog-details" class = "padded-content-sides">
<p> <p>
<strong>Status: </strong><span id = "execution-dialog-status">unknown</span> <strong>Status: </strong><span id = "execution-dialog-status">unknown</span>
</p> </p>
</div> </div>
@ -178,6 +149,41 @@
</div> </div>
</dialog> </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"> <template id = "tplArgumentForm">
<dialog title = "Arguments" id = "argument-popup"> <dialog title = "Arguments" id = "argument-popup">
<form class = "action-arguments padded-content"> <form class = "action-arguments padded-content">
@ -225,24 +231,24 @@
/** /**
This is the bootstrap code, which relies on very simple, old javascript 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) { 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) { if (isFatal) {
bigErrorDialog.innerHTML += '<p>You will need to refresh your browser to clear this message.</p>' bigErrorDialog.innerHTML += '<p>You will need to refresh your browser to clear this message.</p>'
} else { } else {
bigErrorDialog.innerHTML += '<p>This error message will go away automatically if the problem is solved.</p>' 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 () { window.clearBigErrors = function () {
bigErrorDialog.close() bigErrorDialog.close()
} }
</script> </script>

42
webui.dev/js/LoginForm.js Normal file
View File

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

View File

@ -10,6 +10,8 @@ import {
} from './js/marshaller.js' } from './js/marshaller.js'
import { checkWebsocketConnection } from './js/websocket.js' import { checkWebsocketConnection } from './js/websocket.js'
import { LoginForm } from './js/LoginForm.js'
function searchLogs (e) { function searchLogs (e) {
document.getElementById('searchLogsClear').disabled = false document.getElementById('searchLogsClear').disabled = false
@ -126,28 +128,42 @@ function processWebuiSettingsJson (settings) {
if (titleElem) titleElem.innerText = window.pageTitle 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 window.settings = settings
refreshDiagnostics() refreshDiagnostics()
} }
function processAdditionaLinks (links) { function processAdditionalLinks (links) {
if (links === null) { if (links === null) {
return return
} }
for (const link of links) { if (links.length > 0) {
const linkA = document.createElement('a') for (const link of links) {
linkA.href = link.Url const linkA = document.createElement('a')
linkA.innerText = link.Title linkA.href = link.Url
linkA.target = '_blank' linkA.innerText = link.Title
const linkLi = document.createElement('li') if (link.Target === '') {
linkLi.appendChild(linkA) 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)
}
} }
} }

View File

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