Login with generic oauth feature++pull/376/head
@@ -86,6 +86,11 @@ func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error { | |||
} | |||
func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { | |||
if app.cfg.App.DisablePasswordAuth { | |||
err := ErrDisabledPasswordAuth | |||
return nil, err | |||
} | |||
reqJSON := IsJSON(r) | |||
// Get params | |||
@@ -299,16 +304,18 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { | |||
p := &struct { | |||
page.StaticPage | |||
To string | |||
Message template.HTML | |||
Flashes []template.HTML | |||
LoginUsername string | |||
OauthSlack bool | |||
OauthWriteAs bool | |||
OauthGitlab bool | |||
GitlabDisplayName string | |||
OauthGitea bool | |||
GiteaDisplayName string | |||
To string | |||
Message template.HTML | |||
Flashes []template.HTML | |||
LoginUsername string | |||
OauthSlack bool | |||
OauthWriteAs bool | |||
OauthGitlab bool | |||
GitlabDisplayName string | |||
OauthGeneric bool | |||
OauthGenericDisplayName string | |||
OauthGitea bool | |||
GiteaDisplayName string | |||
}{ | |||
pageForReq(app, r), | |||
r.FormValue("to"), | |||
@@ -318,6 +325,8 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { | |||
app.Config().SlackOauth.ClientID != "", | |||
app.Config().WriteAsOauth.ClientID != "", | |||
app.Config().GitlabOauth.ClientID != "", | |||
config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName), | |||
app.Config().GenericOauth.ClientID != "", | |||
config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), | |||
app.Config().GiteaOauth.ClientID != "", | |||
config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), | |||
@@ -395,6 +404,11 @@ func login(app *App, w http.ResponseWriter, r *http.Request) error { | |||
var err error | |||
var signin userCredentials | |||
if app.cfg.App.DisablePasswordAuth { | |||
err := ErrDisabledPasswordAuth | |||
return err | |||
} | |||
// Log in with one-time token if one is given | |||
if oneTimeToken != "" { | |||
log.Info("Login: Logging user in via token.") | |||
@@ -1049,6 +1063,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err | |||
enableOauthSlack := app.Config().SlackOauth.ClientID != "" | |||
enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != "" | |||
enableOauthGitLab := app.Config().GitlabOauth.ClientID != "" | |||
enableOauthGeneric := app.Config().GenericOauth.ClientID != "" | |||
enableOauthGitea := app.Config().GiteaOauth.ClientID != "" | |||
oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID) | |||
@@ -1056,7 +1071,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err | |||
log.Error("Unable to get oauth accounts for settings: %s", err) | |||
return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."} | |||
} | |||
for _, oauthAccount := range oauthAccounts { | |||
for idx, oauthAccount := range oauthAccounts { | |||
switch oauthAccount.Provider { | |||
case "slack": | |||
enableOauthSlack = false | |||
@@ -1064,41 +1079,49 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err | |||
enableOauthWriteAs = false | |||
case "gitlab": | |||
enableOauthGitLab = false | |||
case "generic": | |||
oauthAccounts[idx].DisplayName = app.Config().GenericOauth.DisplayName | |||
oauthAccounts[idx].AllowDisconnect = app.Config().GenericOauth.AllowDisconnect | |||
enableOauthGeneric = false | |||
case "gitea": | |||
enableOauthGitea = false | |||
} | |||
} | |||
displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGitea || len(oauthAccounts) > 0 | |||
displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || enableOauthGitea || len(oauthAccounts) > 0 | |||
obj := struct { | |||
*UserPage | |||
Email string | |||
HasPass bool | |||
IsLogOut bool | |||
Silenced bool | |||
OauthSection bool | |||
OauthAccounts []oauthAccountInfo | |||
OauthSlack bool | |||
OauthWriteAs bool | |||
OauthGitLab bool | |||
GitLabDisplayName string | |||
OauthGitea bool | |||
GiteaDisplayName string | |||
Email string | |||
HasPass bool | |||
IsLogOut bool | |||
Silenced bool | |||
OauthSection bool | |||
OauthAccounts []oauthAccountInfo | |||
OauthSlack bool | |||
OauthWriteAs bool | |||
OauthGitLab bool | |||
GitLabDisplayName string | |||
OauthGeneric bool | |||
OauthGenericDisplayName string | |||
OauthGitea bool | |||
GiteaDisplayName string | |||
}{ | |||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes), | |||
Email: fullUser.EmailClear(app.keys), | |||
HasPass: passIsSet, | |||
IsLogOut: r.FormValue("logout") == "1", | |||
Silenced: fullUser.IsSilenced(), | |||
OauthSection: displayOauthSection, | |||
OauthAccounts: oauthAccounts, | |||
OauthSlack: enableOauthSlack, | |||
OauthWriteAs: enableOauthWriteAs, | |||
OauthGitLab: enableOauthGitLab, | |||
GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), | |||
OauthGitea: enableOauthGitea, | |||
GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), | |||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes), | |||
Email: fullUser.EmailClear(app.keys), | |||
HasPass: passIsSet, | |||
IsLogOut: r.FormValue("logout") == "1", | |||
Silenced: fullUser.IsSilenced(), | |||
OauthSection: displayOauthSection, | |||
OauthAccounts: oauthAccounts, | |||
OauthSlack: enableOauthSlack, | |||
OauthWriteAs: enableOauthWriteAs, | |||
OauthGitLab: enableOauthGitLab, | |||
GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), | |||
OauthGeneric: enableOauthGeneric, | |||
OauthGenericDisplayName: config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName), | |||
OauthGitea: enableOauthGitea, | |||
GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), | |||
} | |||
showUserPage(w, "settings", obj) | |||
@@ -243,9 +243,22 @@ func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error { | |||
Content template.HTML | |||
ForcedLanding bool | |||
OauthSlack bool | |||
OauthWriteAs bool | |||
OauthGitlab bool | |||
OauthGeneric bool | |||
OauthGenericDisplayName string | |||
GitlabDisplayName string | |||
}{ | |||
StaticPage: pageForReq(app, r), | |||
ForcedLanding: forceLanding, | |||
StaticPage: pageForReq(app, r), | |||
ForcedLanding: forceLanding, | |||
OauthSlack: app.Config().SlackOauth.ClientID != "", | |||
OauthWriteAs: app.Config().WriteAsOauth.ClientID != "", | |||
OauthGitlab: app.Config().GitlabOauth.ClientID != "", | |||
OauthGeneric: app.Config().GenericOauth.ClientID != "", | |||
OauthGenericDisplayName: config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName), | |||
GitlabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), | |||
} | |||
banner, err := getLandingBanner(app) | |||
@@ -89,6 +89,18 @@ type ( | |||
CallbackProxyAPI string `ini:"callback_proxy_api"` | |||
} | |||
GenericOauthCfg struct { | |||
ClientID string `ini:"client_id"` | |||
ClientSecret string `ini:"client_secret"` | |||
Host string `ini:"host"` | |||
DisplayName string `ini:"display_name"` | |||
CallbackProxy string `ini:"callback_proxy"` | |||
CallbackProxyAPI string `ini:"callback_proxy_api"` | |||
TokenEndpoint string `ini:"token_endpoint"` | |||
InspectEndpoint string `ini:"inspect_endpoint"` | |||
AuthEndpoint string `ini:"auth_endpoint"` | |||
AllowDisconnect bool `ini:"allow_disconnect"` | |||
} | |||
GiteaOauthCfg struct { | |||
ClientID string `ini:"client_id"` | |||
ClientSecret string `ini:"client_secret"` | |||
@@ -140,6 +152,9 @@ type ( | |||
// Check for Updates | |||
UpdateChecks bool `ini:"update_checks"` | |||
// Disable password authentication if use only Oauth | |||
DisablePasswordAuth bool `ini:"disable_password_auth"` | |||
} | |||
// Config holds the complete configuration for running a writefreely instance | |||
@@ -150,6 +165,7 @@ type ( | |||
SlackOauth SlackOauthCfg `ini:"oauth.slack"` | |||
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` | |||
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` | |||
GenericOauth GenericOauthCfg `ini:"oauth.generic"` | |||
GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"` | |||
} | |||
) | |||
@@ -2627,9 +2627,11 @@ func (db *datastore) GetIDForRemoteUser(ctx context.Context, remoteUserID, provi | |||
} | |||
type oauthAccountInfo struct { | |||
Provider string | |||
ClientID string | |||
RemoteUserID string | |||
Provider string | |||
ClientID string | |||
RemoteUserID string | |||
DisplayName string | |||
AllowDisconnect bool | |||
} | |||
func (db *datastore) GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) { | |||
@@ -52,6 +52,8 @@ var ( | |||
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."} | |||
ErrUserSilenced = impart.HTTPError{http.StatusForbidden, "Account is silenced."} | |||
ErrDisabledPasswordAuth = impart.HTTPError{http.StatusForbidden, "Password authentication is disabled."} | |||
) | |||
// Post operation errors | |||
@@ -235,6 +235,33 @@ func configureGitlabOauth(parentHandler *Handler, r *mux.Router, app *App) { | |||
} | |||
} | |||
func configureGenericOauth(parentHandler *Handler, r *mux.Router, app *App) { | |||
if app.Config().GenericOauth.ClientID != "" { | |||
callbackLocation := app.Config().App.Host + "/oauth/callback/generic" | |||
var callbackProxy *callbackProxyClient = nil | |||
if app.Config().GenericOauth.CallbackProxy != "" { | |||
callbackProxy = &callbackProxyClient{ | |||
server: app.Config().GenericOauth.CallbackProxyAPI, | |||
callbackLocation: app.Config().App.Host + "/oauth/callback/generic", | |||
httpClient: config.DefaultHTTPClient(), | |||
} | |||
callbackLocation = app.Config().GenericOauth.CallbackProxy | |||
} | |||
oauthClient := genericOauthClient{ | |||
ClientID: app.Config().GenericOauth.ClientID, | |||
ClientSecret: app.Config().GenericOauth.ClientSecret, | |||
ExchangeLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.TokenEndpoint, | |||
InspectLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.InspectEndpoint, | |||
AuthLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.AuthEndpoint, | |||
HttpClient: config.DefaultHTTPClient(), | |||
CallbackLocation: callbackLocation, | |||
} | |||
configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) | |||
} | |||
} | |||
func configureGiteaOauth(parentHandler *Handler, r *mux.Router, app *App) { | |||
if app.Config().GiteaOauth.ClientID != "" { | |||
callbackLocation := app.Config().App.Host + "/oauth/callback/gitea" | |||
@@ -0,0 +1,114 @@ | |||
package writefreely | |||
import ( | |||
"context" | |||
"errors" | |||
"net/http" | |||
"net/url" | |||
"strings" | |||
) | |||
type genericOauthClient struct { | |||
ClientID string | |||
ClientSecret string | |||
AuthLocation string | |||
ExchangeLocation string | |||
InspectLocation string | |||
CallbackLocation string | |||
HttpClient HttpClient | |||
} | |||
var _ oauthClient = genericOauthClient{} | |||
const ( | |||
genericOauthDisplayName = "OAuth" | |||
) | |||
func (c genericOauthClient) GetProvider() string { | |||
return "generic" | |||
} | |||
func (c genericOauthClient) GetClientID() string { | |||
return c.ClientID | |||
} | |||
func (c genericOauthClient) GetCallbackLocation() string { | |||
return c.CallbackLocation | |||
} | |||
func (c genericOauthClient) buildLoginURL(state string) (string, error) { | |||
u, err := url.Parse(c.AuthLocation) | |||
if err != nil { | |||
return "", err | |||
} | |||
q := u.Query() | |||
q.Set("client_id", c.ClientID) | |||
q.Set("redirect_uri", c.CallbackLocation) | |||
q.Set("response_type", "code") | |||
q.Set("state", state) | |||
q.Set("scope", "read_user") | |||
u.RawQuery = q.Encode() | |||
return u.String(), nil | |||
} | |||
func (c genericOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { | |||
form := url.Values{} | |||
form.Add("grant_type", "authorization_code") | |||
form.Add("redirect_uri", c.CallbackLocation) | |||
form.Add("scope", "read_user") | |||
form.Add("code", code) | |||
req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode())) | |||
if err != nil { | |||
return nil, err | |||
} | |||
req.WithContext(ctx) | |||
req.Header.Set("User-Agent", "writefreely") | |||
req.Header.Set("Accept", "application/json") | |||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | |||
req.SetBasicAuth(c.ClientID, c.ClientSecret) | |||
resp, err := c.HttpClient.Do(req) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if resp.StatusCode != http.StatusOK { | |||
return nil, errors.New("unable to exchange code for access token") | |||
} | |||
var tokenResponse TokenResponse | |||
if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil { | |||
return nil, err | |||
} | |||
if tokenResponse.Error != "" { | |||
return nil, errors.New(tokenResponse.Error) | |||
} | |||
return &tokenResponse, nil | |||
} | |||
func (c genericOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) { | |||
req, err := http.NewRequest("GET", c.InspectLocation, nil) | |||
if err != nil { | |||
return nil, err | |||
} | |||
req.WithContext(ctx) | |||
req.Header.Set("User-Agent", "writefreely") | |||
req.Header.Set("Accept", "application/json") | |||
req.Header.Set("Authorization", "Bearer "+accessToken) | |||
resp, err := c.HttpClient.Do(req) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if resp.StatusCode != http.StatusOK { | |||
return nil, errors.New("unable to inspect access token") | |||
} | |||
var inspectResponse InspectResponse | |||
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil { | |||
return nil, err | |||
} | |||
if inspectResponse.Error != "" { | |||
return nil, errors.New(inspectResponse.Error) | |||
} | |||
return &inspectResponse, nil | |||
} |
@@ -60,6 +60,11 @@ form dd { | |||
margin-top: 0; | |||
max-width: 8em; | |||
} | |||
#generic-oauth-login { | |||
box-sizing: border-box; | |||
font-size: 17px; | |||
white-space:nowrap; | |||
} | |||
</style> | |||
{{end}} | |||
{{define "content"}} | |||
@@ -73,6 +78,21 @@ form dd { | |||
<div{{if not .OpenRegistration}} style="padding: 2em 0;"{{end}}> | |||
{{ if .OpenRegistration }} | |||
{{ if or .OauthSlack .OauthWriteAs .OauthGitlab .OauthGeneric }} | |||
{{ if .OauthSlack }} | |||
<div class="row content-container signinbtns signinoauthbtns"><a class="loginbtn" href="/oauth/slack"><img alt="Sign in with Slack" height="40" width="172" src="/img/sign_in_with_slack.png" srcset="/img/sign_in_with_slack.png 1x, /img/sign_in_with_slack@2x.png 2x" /></a></div> | |||
{{ end }} | |||
{{ if .OauthWriteAs }} | |||
<div class="row content-container signinbtns signinoauthbtns"><a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as">Sign in with <strong>Write.as</strong></a></div> | |||
{{ end }} | |||
{{ if .OauthGitlab }} | |||
<div class="row content-container signinbtns signinoauthbtns"><a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab">Sign in with <strong>{{.GitlabDisplayName}}</strong></a></div> | |||
{{ end }} | |||
{{ if .OauthGeneric }} | |||
<div class="row content-container signinbtns signinoauthbtns"><a class="btn cta loginbtn" id="generic-oauth-login" href="/oauth/generic">Sign in with <strong>{{ .OauthGenericDisplayName }}</strong></a></div> | |||
{{ end }} | |||
{{ end }} | |||
{{if not .DisablePasswordAuth}} | |||
{{if .Flashes}}<ul class="errors"> | |||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} | |||
</ul>{{end}} | |||
@@ -101,6 +121,7 @@ form dd { | |||
</dl> | |||
</form> | |||
</div> | |||
{{end}} | |||
{{ else }} | |||
<p style="font-size: 1.3em; margin: 1rem 0;">Registration is currently closed.</p> | |||
<p>You can always sign up on <a href="https://writefreely.org/instances">another instance</a>.</p> | |||
@@ -3,6 +3,10 @@ | |||
<meta itemprop="description" content="Log in to {{.SiteName}}."> | |||
<style> | |||
input{margin-bottom:0.5em;} | |||
#generic-oauth-login { | |||
box-sizing: border-box; | |||
font-size: 17px; | |||
} | |||
</style> | |||
{{end}} | |||
{{define "content"}} | |||
@@ -13,7 +17,7 @@ input{margin-bottom:0.5em;} | |||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} | |||
</ul>{{end}} | |||
{{ if or .OauthSlack .OauthWriteAs .OauthGitlab .OauthGitea }} | |||
{{ if or .OauthSlack .OauthWriteAs .OauthGitlab .OauthGeneric .OauthGitea }} | |||
<div class="row content-container signinbtns"> | |||
{{ if .OauthSlack }} | |||
<a class="loginbtn" href="/oauth/slack"><img alt="Sign in with Slack" height="40" width="172" src="/img/sign_in_with_slack.png" srcset="/img/sign_in_with_slack.png 1x, /img/sign_in_with_slack@2x.png 2x" /></a> | |||
@@ -24,17 +28,23 @@ input{margin-bottom:0.5em;} | |||
{{ if .OauthGitlab }} | |||
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab">Sign in with <strong>{{.GitlabDisplayName}}</strong></a> | |||
{{ end }} | |||
{{ if .OauthGeneric }} | |||
<a class="btn cta loginbtn" id="generic-oauth-login" href="/oauth/generic">Sign in with <strong>{{ .OauthGenericDisplayName }}</strong></a> | |||
{{ end }} | |||
{{ if .OauthGitea }} | |||
<a class="btn cta loginbtn" id="gitea-login" href="/oauth/gitea">Sign in with <strong>{{.GiteaDisplayName}}</strong></a> | |||
{{ end }} | |||
</div> | |||
<div class="or"> | |||
<p>or</p> | |||
<hr class="short" /> | |||
</div> | |||
{{if not .DisablePasswordAuth}} | |||
<div class="or"> | |||
<p>or</p> | |||
<hr class="short" /> | |||
</div> | |||
{{end}} | |||
{{ end }} | |||
{{if not .DisablePasswordAuth}} | |||
<form action="/auth/login" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()"> | |||
<input type="text" name="alias" placeholder="Username" value="{{.LoginUsername}}" {{if not .LoginUsername}}autofocus{{end}} /><br /> | |||
<input type="password" name="pass" placeholder="Password" {{if .LoginUsername}}autofocus{{end}} /><br /> | |||
@@ -44,11 +54,12 @@ input{margin-bottom:0.5em;} | |||
{{if and (not .SingleUser) .OpenRegistration}}<p style="text-align:center;font-size:0.9em;margin:3em auto;max-width:26em;">{{if .Message}}{{.Message}}{{else}}<em>No account yet?</em> <a href="{{.SignupPath}}">Sign up</a> to start a blog.{{end}}</p>{{end}} | |||
<script type="text/javascript"> | |||
function disableSubmit() { | |||
var $btn = document.getElementById("btn-login"); | |||
$btn.value = "Logging in..."; | |||
$btn.disabled = true; | |||
} | |||
</script> | |||
<script type="text/javascript"> | |||
function disableSubmit() { | |||
var $btn = document.getElementById("btn-login"); | |||
$btn.value = "Logging in..."; | |||
$btn.disabled = true; | |||
} | |||
</script> | |||
{{end}} | |||
{{end}} |
@@ -76,6 +76,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { | |||
configureSlackOauth(handler, write, apper.App()) | |||
configureWriteAsOauth(handler, write, apper.App()) | |||
configureGitlabOauth(handler, write, apper.App()) | |||
configureGenericOauth(handler, write, apper.App()) | |||
configureGiteaOauth(handler, write, apper.App()) | |||
// Set up dyamic page handlers | |||
@@ -41,6 +41,7 @@ h3 { font-weight: normal; } | |||
</form> | |||
{{ end }} | |||
{{if not .DisablePasswordAuth}} | |||
<form method="post" action="/api/me/self" autocomplete="false"> | |||
<input type="hidden" name="logout" value="{{.IsLogOut}}" /> | |||
<div class="option"> | |||
@@ -72,6 +73,7 @@ h3 { font-weight: normal; } | |||
<input type="submit" value="Save changes" tabindex="4" /> | |||
</div> | |||
</form> | |||
{{end}} | |||
{{ if .OauthSection }} | |||
<hr /> | |||
@@ -86,14 +88,22 @@ h3 { font-weight: normal; } | |||
<input type="hidden" name="client_id" value="{{ $oauth_account.ClientID }}" /> | |||
<input type="hidden" name="remote_user_id" value="{{ $oauth_account.RemoteUserID }}" /> | |||
<div class="section oauth-provider"> | |||
<img src="/img/mark/{{$oauth_account.Provider}}.png" alt="{{ $oauth_account.Provider | title }}" /> | |||
<input type="submit" value="Remove {{ $oauth_account.Provider | title }}" /> | |||
{{ if $oauth_account.DisplayName}} | |||
{{ if $oauth_account.AllowDisconnect}} | |||
<input type="submit" value="Remove {{.DisplayName}}" /> | |||
{{else}} | |||
<a class="btn cta"><strong>{{.DisplayName}}</strong></a> | |||
{{end}} | |||
{{else}} | |||
<img src="/img/mark/{{$oauth_account.Provider}}.png" alt="{{ $oauth_account.Provider | title }}" /> | |||
<input type="submit" value="Remove {{ $oauth_account.Provider | title }}" /> | |||
{{end}} | |||
</div> | |||
</form> | |||
{{ end }} | |||
</div> | |||
{{ end }} | |||
{{ if or .OauthSlack .OauthWriteAs .OauthGitLab .OauthGitea }} | |||
{{ if or .OauthSlack .OauthWriteAs .OauthGitLab .OauthGeneric .OauthGitea }} | |||
<div class="option"> | |||
<h2>Link External Accounts</h2> | |||
<p>Connect additional accounts to enable logging in with those providers, instead of using your username and password.</p> | |||
@@ -131,6 +141,13 @@ h3 { font-weight: normal; } | |||
</div> | |||
{{ end }} | |||
</div> | |||
{{ if .OauthGeneric }} | |||
<div class="row"> | |||
<div class="section oauth-provider"> | |||
<p><a class="btn cta loginbtn" id="generic-oauth-login" href="/oauth/generic?attach=t">Link <strong>{{ .OauthGenericDisplayName }}</strong></a></p> | |||
</div> | |||
</div> | |||
{{ end }} | |||
</div> | |||
{{ end }} | |||
{{ end }} | |||