Merge branch 'develop' of https://github.com/writeas/writefreely into fix-youtube-query-parameters
This commit is contained in:
commit
79715891fb
51 changed files with 964 additions and 257 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
||||||
[submodule "static/js/mathjax"]
|
|
||||||
path = static/js/mathjax
|
|
||||||
url = https://github.com/mathjax/MathJax.git
|
|
53
account.go
53
account.go
|
@ -49,6 +49,7 @@ type (
|
||||||
Separator template.HTML
|
Separator template.HTML
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
CanInvite bool
|
CanInvite bool
|
||||||
|
CollAlias string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -86,6 +87,11 @@ func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, 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)
|
reqJSON := IsJSON(r)
|
||||||
|
|
||||||
// Get params
|
// Get params
|
||||||
|
@ -299,24 +305,18 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
p := &struct {
|
p := &struct {
|
||||||
page.StaticPage
|
page.StaticPage
|
||||||
|
*OAuthButtons
|
||||||
To string
|
To string
|
||||||
Message template.HTML
|
Message template.HTML
|
||||||
Flashes []template.HTML
|
Flashes []template.HTML
|
||||||
LoginUsername string
|
LoginUsername string
|
||||||
OauthSlack bool
|
|
||||||
OauthWriteAs bool
|
|
||||||
OauthGitlab bool
|
|
||||||
GitlabDisplayName string
|
|
||||||
}{
|
}{
|
||||||
pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
r.FormValue("to"),
|
OAuthButtons: NewOAuthButtons(app.Config()),
|
||||||
template.HTML(""),
|
To: r.FormValue("to"),
|
||||||
[]template.HTML{},
|
Message: template.HTML(""),
|
||||||
getTempInfo(app, "login-user", r, w),
|
Flashes: []template.HTML{},
|
||||||
app.Config().SlackOauth.ClientID != "",
|
LoginUsername: getTempInfo(app, "login-user", r, w),
|
||||||
app.Config().WriteAsOauth.ClientID != "",
|
|
||||||
app.Config().GitlabOauth.ClientID != "",
|
|
||||||
config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if earlyError != "" {
|
if earlyError != "" {
|
||||||
|
@ -391,6 +391,11 @@ func login(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
var err error
|
var err error
|
||||||
var signin userCredentials
|
var signin userCredentials
|
||||||
|
|
||||||
|
if app.cfg.App.DisablePasswordAuth {
|
||||||
|
err := ErrDisabledPasswordAuth
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Log in with one-time token if one is given
|
// Log in with one-time token if one is given
|
||||||
if oneTimeToken != "" {
|
if oneTimeToken != "" {
|
||||||
log.Info("Login: Logging user in via token.")
|
log.Info("Login: Logging user in via token.")
|
||||||
|
@ -836,6 +841,7 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
||||||
Collection: c,
|
Collection: c,
|
||||||
Silenced: silenced,
|
Silenced: silenced,
|
||||||
}
|
}
|
||||||
|
obj.UserPage.CollAlias = c.Alias
|
||||||
|
|
||||||
showUserPage(w, "collection", obj)
|
showUserPage(w, "collection", obj)
|
||||||
return nil
|
return nil
|
||||||
|
@ -1015,6 +1021,7 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
||||||
TopPosts: topPosts,
|
TopPosts: topPosts,
|
||||||
Silenced: silenced,
|
Silenced: silenced,
|
||||||
}
|
}
|
||||||
|
obj.UserPage.CollAlias = c.Alias
|
||||||
if app.cfg.App.Federation {
|
if app.cfg.App.Federation {
|
||||||
folls, err := app.db.GetAPFollowers(c)
|
folls, err := app.db.GetAPFollowers(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1045,13 +1052,15 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
||||||
enableOauthSlack := app.Config().SlackOauth.ClientID != ""
|
enableOauthSlack := app.Config().SlackOauth.ClientID != ""
|
||||||
enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != ""
|
enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != ""
|
||||||
enableOauthGitLab := app.Config().GitlabOauth.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)
|
oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to get oauth accounts for settings: %s", 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."}
|
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 {
|
switch oauthAccount.Provider {
|
||||||
case "slack":
|
case "slack":
|
||||||
enableOauthSlack = false
|
enableOauthSlack = false
|
||||||
|
@ -1059,10 +1068,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
||||||
enableOauthWriteAs = false
|
enableOauthWriteAs = false
|
||||||
case "gitlab":
|
case "gitlab":
|
||||||
enableOauthGitLab = false
|
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 || len(oauthAccounts) > 0
|
displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || enableOauthGitea || len(oauthAccounts) > 0
|
||||||
|
|
||||||
obj := struct {
|
obj := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
|
@ -1076,6 +1091,10 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
||||||
OauthWriteAs bool
|
OauthWriteAs bool
|
||||||
OauthGitLab bool
|
OauthGitLab bool
|
||||||
GitLabDisplayName string
|
GitLabDisplayName string
|
||||||
|
OauthGeneric bool
|
||||||
|
OauthGenericDisplayName string
|
||||||
|
OauthGitea bool
|
||||||
|
GiteaDisplayName string
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||||
Email: fullUser.EmailClear(app.keys),
|
Email: fullUser.EmailClear(app.keys),
|
||||||
|
@ -1088,6 +1107,10 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
||||||
OauthWriteAs: enableOauthWriteAs,
|
OauthWriteAs: enableOauthWriteAs,
|
||||||
OauthGitLab: enableOauthGitLab,
|
OauthGitLab: enableOauthGitLab,
|
||||||
GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
|
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)
|
showUserPage(w, "settings", obj)
|
||||||
|
|
|
@ -494,7 +494,7 @@ func makeActivityPost(hostName string, p *activitystreams.Person, url string, m
|
||||||
|
|
||||||
r, _ := http.NewRequest("POST", url, bytes.NewBuffer(b))
|
r, _ := http.NewRequest("POST", url, bytes.NewBuffer(b))
|
||||||
r.Header.Add("Content-Type", "application/activity+json")
|
r.Header.Add("Content-Type", "application/activity+json")
|
||||||
r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
|
r.Header.Set("User-Agent", ServerUserAgent(hostName))
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
h.Write(b)
|
h.Write(b)
|
||||||
r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
||||||
|
@ -544,7 +544,7 @@ func resolveIRI(hostName, url string) ([]byte, error) {
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", url, nil)
|
r, _ := http.NewRequest("GET", url, nil)
|
||||||
r.Header.Add("Accept", "application/activity+json")
|
r.Header.Add("Accept", "application/activity+json")
|
||||||
r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
|
r.Header.Set("User-Agent", ServerUserAgent(hostName))
|
||||||
|
|
||||||
if debugging {
|
if debugging {
|
||||||
dump, err := httputil.DumpRequestOut(r, true)
|
dump, err := httputil.DumpRequestOut(r, true)
|
||||||
|
@ -699,6 +699,10 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
||||||
// I don't believe we'd ever have too many mentions in a single post that this
|
// I don't believe we'd ever have too many mentions in a single post that this
|
||||||
// could become a burden.
|
// could become a burden.
|
||||||
remoteUser, err := getRemoteUser(app, tag.HRef)
|
remoteUser, err := getRemoteUser(app, tag.HRef)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to find remote user %s. Skipping: %v", tag.HRef, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
err = makeActivityPost(app.cfg.App.Host, actor, remoteUser.Inbox, activity)
|
err = makeActivityPost(app.cfg.App.Host, actor, remoteUser.Inbox, activity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Couldn't post! %v", err)
|
log.Error("Couldn't post! %v", err)
|
||||||
|
|
12
app.go
12
app.go
|
@ -238,6 +238,7 @@ func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
p := struct {
|
p := struct {
|
||||||
page.StaticPage
|
page.StaticPage
|
||||||
|
*OAuthButtons
|
||||||
Flashes []template.HTML
|
Flashes []template.HTML
|
||||||
Banner template.HTML
|
Banner template.HTML
|
||||||
Content template.HTML
|
Content template.HTML
|
||||||
|
@ -245,6 +246,7 @@ func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
ForcedLanding bool
|
ForcedLanding bool
|
||||||
}{
|
}{
|
||||||
StaticPage: pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
|
OAuthButtons: NewOAuthButtons(app.Config()),
|
||||||
ForcedLanding: forceLanding,
|
ForcedLanding: forceLanding,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -890,3 +892,13 @@ func adminInitDatabase(app *App) error {
|
||||||
log.Info("Done.")
|
log.Info("Done.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServerUserAgent returns a User-Agent string to use in external requests. The
|
||||||
|
// hostName parameter may be left empty.
|
||||||
|
func ServerUserAgent(hostName string) string {
|
||||||
|
hostUAStr := ""
|
||||||
|
if hostName != "" {
|
||||||
|
hostUAStr = "; +" + hostName
|
||||||
|
}
|
||||||
|
return "Go (" + serverSoftware + "/" + softwareVer + hostUAStr + ")"
|
||||||
|
}
|
||||||
|
|
|
@ -81,6 +81,15 @@ type (
|
||||||
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GiteaOauthCfg 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"`
|
||||||
|
}
|
||||||
|
|
||||||
SlackOauthCfg struct {
|
SlackOauthCfg struct {
|
||||||
ClientID string `ini:"client_id"`
|
ClientID string `ini:"client_id"`
|
||||||
ClientSecret string `ini:"client_secret"`
|
ClientSecret string `ini:"client_secret"`
|
||||||
|
@ -89,6 +98,19 @@ type (
|
||||||
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
// AppCfg holds values that affect how the application functions
|
// AppCfg holds values that affect how the application functions
|
||||||
AppCfg struct {
|
AppCfg struct {
|
||||||
SiteName string `ini:"site_name"`
|
SiteName string `ini:"site_name"`
|
||||||
|
@ -131,6 +153,9 @@ type (
|
||||||
|
|
||||||
// Check for Updates
|
// Check for Updates
|
||||||
UpdateChecks bool `ini:"update_checks"`
|
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
|
// Config holds the complete configuration for running a writefreely instance
|
||||||
|
@ -141,6 +166,8 @@ type (
|
||||||
SlackOauth SlackOauthCfg `ini:"oauth.slack"`
|
SlackOauth SlackOauthCfg `ini:"oauth.slack"`
|
||||||
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"`
|
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"`
|
||||||
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"`
|
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"`
|
||||||
|
GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"`
|
||||||
|
GenericOauth GenericOauthCfg `ini:"oauth.generic"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
14
database.go
14
database.go
|
@ -14,6 +14,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/writeas/web-core/silobridge"
|
||||||
wf_db "github.com/writeas/writefreely/db"
|
wf_db "github.com/writeas/writefreely/db"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -2629,6 +2630,8 @@ type oauthAccountInfo struct {
|
||||||
Provider string
|
Provider string
|
||||||
ClientID string
|
ClientID string
|
||||||
RemoteUserID string
|
RemoteUserID string
|
||||||
|
DisplayName string
|
||||||
|
AllowDisconnect bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *datastore) GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) {
|
func (db *datastore) GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) {
|
||||||
|
@ -2691,6 +2694,17 @@ func handleFailedPostInsert(err error) error {
|
||||||
func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) {
|
func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) {
|
||||||
handle = strings.TrimLeft(handle, "@")
|
handle = strings.TrimLeft(handle, "@")
|
||||||
actorIRI := ""
|
actorIRI := ""
|
||||||
|
parts := strings.Split(handle, "@")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid handle format")
|
||||||
|
}
|
||||||
|
domain := parts[1]
|
||||||
|
|
||||||
|
// Check non-AP instances
|
||||||
|
if siloProfileURL := silobridge.Profile(parts[0], domain); siloProfileURL != "" {
|
||||||
|
return siloProfileURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
remoteUser, err := getRemoteUserFromHandle(app, handle)
|
remoteUser, err := getRemoteUserFromHandle(app, handle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// can't find using handle in the table but the table may already have this user without
|
// can't find using handle in the table but the table may already have this user without
|
||||||
|
|
|
@ -52,6 +52,8 @@ var (
|
||||||
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
||||||
|
|
||||||
ErrUserSilenced = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
|
ErrUserSilenced = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
|
||||||
|
|
||||||
|
ErrDisabledPasswordAuth = impart.HTTPError{http.StatusForbidden, "Password authentication is disabled."}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Post operation errors
|
// Post operation errors
|
||||||
|
|
9
go.mod
9
go.mod
|
@ -12,7 +12,7 @@ require (
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||||
github.com/gorilla/feeds v1.1.1
|
github.com/gorilla/feeds v1.1.1
|
||||||
github.com/gorilla/mux v1.7.4
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/gorilla/schema v1.1.0
|
github.com/gorilla/schema v1.2.0
|
||||||
github.com/gorilla/sessions v1.2.0
|
github.com/gorilla/sessions v1.2.0
|
||||||
github.com/guregu/null v3.5.0+incompatible
|
github.com/guregu/null v3.5.0+incompatible
|
||||||
github.com/hashicorp/go-multierror v1.1.0
|
github.com/hashicorp/go-multierror v1.1.0
|
||||||
|
@ -22,9 +22,8 @@ require (
|
||||||
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec
|
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec
|
||||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||||
github.com/manifoldco/promptui v0.7.0
|
github.com/manifoldco/promptui v0.7.0
|
||||||
github.com/mattn/go-colorable v0.1.0 // indirect
|
github.com/mattn/go-sqlite3 v1.14.2
|
||||||
github.com/mattn/go-sqlite3 v1.14.0
|
github.com/microcosm-cc/bluemonday v1.0.4
|
||||||
github.com/microcosm-cc/bluemonday v1.0.3
|
|
||||||
github.com/mitchellh/go-wordwrap v1.0.0
|
github.com/mitchellh/go-wordwrap v1.0.0
|
||||||
github.com/nicksnyder/go-i18n v1.10.0 // indirect
|
github.com/nicksnyder/go-i18n v1.10.0 // indirect
|
||||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||||
|
@ -47,7 +46,7 @@ require (
|
||||||
github.com/writeas/nerds v1.0.0
|
github.com/writeas/nerds v1.0.0
|
||||||
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320
|
github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320
|
||||||
github.com/writeas/slug v1.2.0
|
github.com/writeas/slug v1.2.0
|
||||||
github.com/writeas/web-core v1.2.0
|
github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c
|
||||||
github.com/writefreely/go-nodeinfo v1.2.0
|
github.com/writefreely/go-nodeinfo v1.2.0
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -77,6 +77,8 @@ github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA=
|
||||||
github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||||
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
|
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
|
||||||
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||||
|
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||||
|
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||||
|
@ -129,10 +131,14 @@ github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK86
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.2 h1:A2EQLwjYf/hfYaM20FVjs1UewCTTFR7RmjEHkLjldIA=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.2/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.3 h1:EjVH7OqbU219kdm8acbveoclh2zZFqPJTJw6VUlTLAQ=
|
github.com/microcosm-cc/bluemonday v1.0.3 h1:EjVH7OqbU219kdm8acbveoclh2zZFqPJTJw6VUlTLAQ=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
|
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||||
github.com/nicksnyder/go-i18n v1.10.0 h1:5AzlPKvXBH4qBzmZ09Ua9Gipyruv6uApMcrNZdo96+Q=
|
github.com/nicksnyder/go-i18n v1.10.0 h1:5AzlPKvXBH4qBzmZ09Ua9Gipyruv6uApMcrNZdo96+Q=
|
||||||
|
@ -210,6 +216,8 @@ github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g=
|
||||||
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
|
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
|
||||||
github.com/writeas/web-core v1.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0=
|
github.com/writeas/web-core v1.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0=
|
||||||
github.com/writeas/web-core v1.2.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
|
github.com/writeas/web-core v1.2.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
|
||||||
|
github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c h1:/aPb8WKtC+Ga/xUEcME0iX3VKBeeJ02kXCaROaZ21SE=
|
||||||
|
github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
|
||||||
github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss=
|
github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss=
|
||||||
github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
|
github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
|
||||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
||||||
|
|
10
handle.go
10
handle.go
|
@ -601,6 +601,9 @@ func (h *Handler) AllReader(f handlerFunc) http.HandlerFunc {
|
||||||
log.Info(h.app.ReqLog(r, status, time.Since(start)))
|
log.Info(h.app.ReqLog(r, status, time.Since(start)))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Allow any origin, as public endpoints are handled in here
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
if h.app.App().cfg.App.Private {
|
if h.app.App().cfg.App.Private {
|
||||||
// This instance is private, so ensure it's being accessed by a valid user
|
// This instance is private, so ensure it's being accessed by a valid user
|
||||||
// Check if authenticated with an access token
|
// Check if authenticated with an access token
|
||||||
|
@ -923,3 +926,10 @@ func sendRedirect(w http.ResponseWriter, code int, location string) int {
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cacheControl(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -170,14 +170,14 @@ func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
p := struct {
|
p := struct {
|
||||||
page.StaticPage
|
page.StaticPage
|
||||||
|
*OAuthButtons
|
||||||
Error string
|
Error string
|
||||||
Flashes []template.HTML
|
Flashes []template.HTML
|
||||||
Invite string
|
Invite string
|
||||||
OAuth *OAuthButtons
|
|
||||||
}{
|
}{
|
||||||
StaticPage: pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
|
OAuthButtons: NewOAuthButtons(app.cfg),
|
||||||
Invite: inviteCode,
|
Invite: inviteCode,
|
||||||
OAuth: NewOAuthButtons(app.cfg),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if expired {
|
if expired {
|
||||||
|
|
|
@ -32,6 +32,19 @@ nav#admin {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
&:not(.pages) {
|
||||||
|
display: block;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
a {
|
||||||
|
margin-left: 0;
|
||||||
|
.rounded(.25em);
|
||||||
|
|
||||||
|
&+a {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #333;
|
color: #333;
|
||||||
font-family: @sansFont;
|
font-family: @sansFont;
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
@proSelectedCol: #71D571;
|
@proSelectedCol: #71D571;
|
||||||
@textLinkColor: rgb(0, 0, 238);
|
@textLinkColor: rgb(0, 0, 238);
|
||||||
|
|
||||||
|
@accent: #767676;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: @serifFont;
|
font-family: @serifFont;
|
||||||
font-size-adjust: 0.5;
|
font-size-adjust: 0.5;
|
||||||
|
@ -81,7 +83,7 @@ body {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.17em;
|
font-size: 1.4em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -743,6 +745,18 @@ input, button, select.inputform, textarea.inputform, a.btn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.pager {
|
||||||
|
border: 1px solid @lightNavBorder;
|
||||||
|
font-size: .86em;
|
||||||
|
padding: .5em 1em;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: @sansFont;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
background: @lightNavBorder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div.flat-select {
|
div.flat-select {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -965,7 +979,12 @@ footer.contain-me {
|
||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
&.collections {
|
&.collections {
|
||||||
|
padding-left: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
li {
|
li {
|
||||||
&.collection {
|
&.collection {
|
||||||
a.title {
|
a.title {
|
||||||
|
@ -1095,7 +1114,8 @@ body#pad-sub #posts, .atoms {
|
||||||
}
|
}
|
||||||
.electron {
|
.electron {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-left: 0.5em;
|
font-size: 0.86em;
|
||||||
|
margin-left: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h3, h4 {
|
h3, h4 {
|
||||||
|
@ -1245,7 +1265,7 @@ header {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.singleuser {
|
&.singleuser {
|
||||||
margin: 0.5em 0.25em;
|
margin: 0.5em 1em 0.5em 0.25em;
|
||||||
nav#user-nav {
|
nav#user-nav {
|
||||||
nav > ul > li:first-child {
|
nav > ul > li:first-child {
|
||||||
img {
|
img {
|
||||||
|
@ -1253,6 +1273,9 @@ header {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.right-side {
|
||||||
|
padding-top: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.dash-nav {
|
.dash-nav {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -1547,3 +1570,26 @@ div.row {
|
||||||
pre.code-block {
|
pre.code-block {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#org-nav {
|
||||||
|
font-family: @sansFont;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
em, strong {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
&+h1 {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
a:link, a:visited, a:hover {
|
||||||
|
color: @accent;
|
||||||
|
}
|
||||||
|
a:first-child {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
a.coll-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,18 +9,64 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.row.signinbtns {
|
.row.signinbtns {
|
||||||
justify-content: space-evenly;
|
justify-content: center;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.loginbtn {
|
.loginbtn {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
margin: 0.5em;
|
||||||
|
|
||||||
#writeas-login, #gitlab-login {
|
&.btn {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 1.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&#writeas-login, &#slack-login {
|
||||||
|
img {
|
||||||
|
margin-top: -0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&#gitlab-login {
|
||||||
|
background-color: #fc6d26;
|
||||||
|
border-color: #fc6d26;
|
||||||
|
&:hover {
|
||||||
|
background-color: darken(#fc6d26, 5%);
|
||||||
|
border-color: darken(#fc6d26, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&#gitea-login {
|
||||||
|
background-color: #2ecc71;
|
||||||
|
border-color: #2ecc71;
|
||||||
|
&:hover {
|
||||||
|
background-color: #2cc26b;
|
||||||
|
border-color: #2cc26b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&#slack-login, &#gitlab-login, &#gitea-login, &#generic-oauth-login {
|
||||||
|
font-size: 0.86em;
|
||||||
|
font-family: @sansFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
&#slack-login, &#generic-oauth-login {
|
||||||
|
color: @lightTextColor;
|
||||||
|
background-color: @lightNavBG;
|
||||||
|
border-color: @lightNavBorder;
|
||||||
|
&:hover {
|
||||||
|
background-color: @lightNavHoverBG;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -127,7 +127,6 @@ textarea {
|
||||||
&.collection {
|
&.collection {
|
||||||
a.title {
|
a.title {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
background: @lightNavHoverBG;
|
background: @lightNavHoverBG;
|
||||||
}
|
}
|
||||||
&:hover > ul {
|
&:hover > ul, &.open > ul {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
&.selected {
|
&.selected {
|
||||||
|
|
70
oauth.go
70
oauth.go
|
@ -34,6 +34,10 @@ type OAuthButtons struct {
|
||||||
WriteAsEnabled bool
|
WriteAsEnabled bool
|
||||||
GitLabEnabled bool
|
GitLabEnabled bool
|
||||||
GitLabDisplayName string
|
GitLabDisplayName string
|
||||||
|
GiteaEnabled bool
|
||||||
|
GiteaDisplayName string
|
||||||
|
GenericEnabled bool
|
||||||
|
GenericDisplayName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOAuthButtons creates a new OAuthButtons struct based on our app configuration.
|
// NewOAuthButtons creates a new OAuthButtons struct based on our app configuration.
|
||||||
|
@ -43,6 +47,10 @@ func NewOAuthButtons(cfg *config.Config) *OAuthButtons {
|
||||||
WriteAsEnabled: cfg.WriteAsOauth.ClientID != "",
|
WriteAsEnabled: cfg.WriteAsOauth.ClientID != "",
|
||||||
GitLabEnabled: cfg.GitlabOauth.ClientID != "",
|
GitLabEnabled: cfg.GitlabOauth.ClientID != "",
|
||||||
GitLabDisplayName: config.OrDefaultString(cfg.GitlabOauth.DisplayName, gitlabDisplayName),
|
GitLabDisplayName: config.OrDefaultString(cfg.GitlabOauth.DisplayName, gitlabDisplayName),
|
||||||
|
GiteaEnabled: cfg.GiteaOauth.ClientID != "",
|
||||||
|
GiteaDisplayName: config.OrDefaultString(cfg.GiteaOauth.DisplayName, giteaDisplayName),
|
||||||
|
GenericEnabled: cfg.GenericOauth.ClientID != "",
|
||||||
|
GenericDisplayName: config.OrDefaultString(cfg.GenericOauth.DisplayName, genericOauthDisplayName),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +243,60 @@ 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"
|
||||||
|
|
||||||
|
var callbackProxy *callbackProxyClient = nil
|
||||||
|
if app.Config().GiteaOauth.CallbackProxy != "" {
|
||||||
|
callbackProxy = &callbackProxyClient{
|
||||||
|
server: app.Config().GiteaOauth.CallbackProxyAPI,
|
||||||
|
callbackLocation: app.Config().App.Host + "/oauth/callback/gitea",
|
||||||
|
httpClient: config.DefaultHTTPClient(),
|
||||||
|
}
|
||||||
|
callbackLocation = app.Config().GiteaOauth.CallbackProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthClient := giteaOauthClient{
|
||||||
|
ClientID: app.Config().GiteaOauth.ClientID,
|
||||||
|
ClientSecret: app.Config().GiteaOauth.ClientSecret,
|
||||||
|
ExchangeLocation: app.Config().GiteaOauth.Host + "/login/oauth/access_token",
|
||||||
|
InspectLocation: app.Config().GiteaOauth.Host + "/api/v1/user",
|
||||||
|
AuthLocation: app.Config().GiteaOauth.Host + "/login/oauth/authorize",
|
||||||
|
HttpClient: config.DefaultHTTPClient(),
|
||||||
|
CallbackLocation: callbackLocation,
|
||||||
|
}
|
||||||
|
configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauthClient oauthClient, callbackProxy *callbackProxyClient) {
|
func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauthClient oauthClient, callbackProxy *callbackProxyClient) {
|
||||||
handler := &oauthHandler{
|
handler := &oauthHandler{
|
||||||
Config: app.Config(),
|
Config: app.Config(),
|
||||||
|
@ -264,6 +326,12 @@ func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http
|
||||||
tokenResponse, err := h.oauthClient.exchangeOauthCode(ctx, code)
|
tokenResponse, err := h.oauthClient.exchangeOauthCode(ctx, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to exchangeOauthCode: %s", err)
|
log.Error("Unable to exchangeOauthCode: %s", err)
|
||||||
|
// TODO: show user friendly message if needed
|
||||||
|
// TODO: show NO message for cases like user pressing "Cancel" on authorize step
|
||||||
|
addSessionFlash(app, w, r, err.Error(), nil)
|
||||||
|
if attachUserID > 0 {
|
||||||
|
return impart.HTTPError{http.StatusFound, "/me/settings"}
|
||||||
|
}
|
||||||
return impart.HTTPError{http.StatusInternalServerError, err.Error()}
|
return impart.HTTPError{http.StatusInternalServerError, err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +422,7 @@ func (r *callbackProxyClient) register(ctx context.Context, state string) error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "writefreely")
|
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
|
114
oauth_generic.go
Normal file
114
oauth_generic.go
Normal file
|
@ -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", ServerUserAgent(""))
|
||||||
|
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", ServerUserAgent(""))
|
||||||
|
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
|
||||||
|
}
|
114
oauth_gitea.go
Normal file
114
oauth_gitea.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package writefreely
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type giteaOauthClient struct {
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
AuthLocation string
|
||||||
|
ExchangeLocation string
|
||||||
|
InspectLocation string
|
||||||
|
CallbackLocation string
|
||||||
|
HttpClient HttpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ oauthClient = giteaOauthClient{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
giteaDisplayName = "Gitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c giteaOauthClient) GetProvider() string {
|
||||||
|
return "gitea"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c giteaOauthClient) GetClientID() string {
|
||||||
|
return c.ClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c giteaOauthClient) GetCallbackLocation() string {
|
||||||
|
return c.CallbackLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c giteaOauthClient) 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 giteaOauthClient) 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", ServerUserAgent(""))
|
||||||
|
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 giteaOauthClient) 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", ServerUserAgent(""))
|
||||||
|
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
|
||||||
|
}
|
|
@ -63,7 +63,7 @@ func (c gitlabOauthClient) exchangeOauthCode(ctx context.Context, code string) (
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.WithContext(ctx)
|
req.WithContext(ctx)
|
||||||
req.Header.Set("User-Agent", "writefreely")
|
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
||||||
|
@ -92,7 +92,7 @@ func (c gitlabOauthClient) inspectOauthAccessToken(ctx context.Context, accessTo
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.WithContext(ctx)
|
req.WithContext(ctx)
|
||||||
req.Header.Set("User-Agent", "writefreely")
|
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ func (c slackOauthClient) exchangeOauthCode(ctx context.Context, code string) (*
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.WithContext(ctx)
|
req.WithContext(ctx)
|
||||||
req.Header.Set("User-Agent", "writefreely")
|
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
||||||
|
@ -140,7 +140,7 @@ func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessTok
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.WithContext(ctx)
|
req.WithContext(ctx)
|
||||||
req.Header.Set("User-Agent", "writefreely")
|
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
|
|
|
@ -244,7 +244,7 @@ func TestViewOauthCallback(t *testing.T) {
|
||||||
req, err := http.NewRequest("GET", "/oauth/callback", nil)
|
req, err := http.NewRequest("GET", "/oauth/callback", nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
err = h.viewOauthCallback(nil, rr, req)
|
err = h.viewOauthCallback(&App{cfg: app.Config(), sessionStore: app.SessionStore()}, rr, req)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
|
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
|
||||||
})
|
})
|
||||||
|
|
|
@ -62,7 +62,7 @@ func (c writeAsOauthClient) exchangeOauthCode(ctx context.Context, code string)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.WithContext(ctx)
|
req.WithContext(ctx)
|
||||||
req.Header.Set("User-Agent", "writefreely")
|
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
||||||
|
@ -91,7 +91,7 @@ func (c writeAsOauthClient) inspectOauthAccessToken(ctx context.Context, accessT
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.WithContext(ctx)
|
req.WithContext(ctx)
|
||||||
req.Header.Set("User-Agent", "writefreely")
|
req.Header.Set("User-Agent", ServerUserAgent(""))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,9 @@ form dd {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
max-width: 8em;
|
max-width: 8em;
|
||||||
}
|
}
|
||||||
|
.or {
|
||||||
|
margin-bottom: 2.5em !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
@ -73,6 +76,8 @@ form dd {
|
||||||
|
|
||||||
<div{{if not .OpenRegistration}} style="padding: 2em 0;"{{end}}>
|
<div{{if not .OpenRegistration}} style="padding: 2em 0;"{{end}}>
|
||||||
{{ if .OpenRegistration }}
|
{{ if .OpenRegistration }}
|
||||||
|
{{template "oauth-buttons" .}}
|
||||||
|
{{if not .DisablePasswordAuth}}
|
||||||
{{if .Flashes}}<ul class="errors">
|
{{if .Flashes}}<ul class="errors">
|
||||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||||
</ul>{{end}}
|
</ul>{{end}}
|
||||||
|
@ -101,6 +106,7 @@ form dd {
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<p style="font-size: 1.3em; margin: 1rem 0;">Registration is currently closed.</p>
|
<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>
|
<p>You can always sign up on <a href="https://writefreely.org/instances">another instance</a>.</p>
|
||||||
|
|
|
@ -13,25 +13,9 @@ input{margin-bottom:0.5em;}
|
||||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||||
</ul>{{end}}
|
</ul>{{end}}
|
||||||
|
|
||||||
{{ if or .OauthSlack .OauthWriteAs .OauthGitlab }}
|
{{template "oauth-buttons" .}}
|
||||||
<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>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .OauthWriteAs }}
|
|
||||||
<a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as">Sign in with <strong>Write.as</strong></a>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .OauthGitlab }}
|
|
||||||
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab">Sign in with <strong>{{.GitlabDisplayName}}</strong></a>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="or">
|
|
||||||
<p>or</p>
|
|
||||||
<hr class="short" />
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
|
{{if not .DisablePasswordAuth}}
|
||||||
<form action="/auth/login" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()">
|
<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="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 />
|
<input type="password" name="pass" placeholder="Password" {{if .LoginUsername}}autofocus{{end}} /><br />
|
||||||
|
@ -41,11 +25,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}}
|
{{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">
|
<script type="text/javascript">
|
||||||
function disableSubmit() {
|
function disableSubmit() {
|
||||||
var $btn = document.getElementById("btn-login");
|
var $btn = document.getElementById("btn-login");
|
||||||
$btn.value = "Logging in...";
|
$btn.value = "Logging in...";
|
||||||
$btn.disabled = true;
|
$btn.disabled = true;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -70,25 +70,9 @@ form dd {
|
||||||
</ul>{{end}}
|
</ul>{{end}}
|
||||||
|
|
||||||
<div id="billing">
|
<div id="billing">
|
||||||
{{ if or .OAuth.SlackEnabled .OAuth.WriteAsEnabled .OAuth.GitLabEnabled }}
|
{{template "oauth-buttons" .}}
|
||||||
<div class="row content-container signinbtns">
|
|
||||||
{{ if .OAuth.SlackEnabled }}
|
|
||||||
<a class="loginbtn" href="/oauth/slack{{if .Invite}}?invite_code={{.Invite}}{{end}}"><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>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .OAuth.WriteAsEnabled }}
|
|
||||||
<a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as{{if .Invite}}?invite_code={{.Invite}}{{end}}">Sign in with <strong>Write.as</strong></a>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .OAuth.GitLabEnabled }}
|
|
||||||
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab{{if .Invite}}?invite_code={{.Invite}}{{end}}">Sign in with <strong>{{.OAuth.GitLabDisplayName}}</strong></a>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="or">
|
|
||||||
<p>or</p>
|
|
||||||
<hr class="short" />
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
|
{{if not .DisablePasswordAuth}}
|
||||||
<form action="/auth/signup" method="POST" id="signup-form" onsubmit="return signup()">
|
<form action="/auth/signup" method="POST" id="signup-form" onsubmit="return signup()">
|
||||||
<input type="hidden" name="invite_code" value="{{.Invite}}" />
|
<input type="hidden" name="invite_code" value="{{.Invite}}" />
|
||||||
<dl class="billing">
|
<dl class="billing">
|
||||||
|
@ -112,6 +96,7 @@ form dd {
|
||||||
</dt>
|
</dt>
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018 A Bunch Tell LLC.
|
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -57,6 +57,11 @@ func PostLede(t string, includePunc bool) string {
|
||||||
c := []rune(t)
|
c := []rune(t)
|
||||||
t = string(c[:punc+iAdj])
|
t = string(c[:punc+iAdj])
|
||||||
}
|
}
|
||||||
|
punc = stringmanip.IndexRune(t, '?')
|
||||||
|
if punc > -1 {
|
||||||
|
c := []rune(t)
|
||||||
|
t = string(c[:punc+iAdj])
|
||||||
|
}
|
||||||
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
func (app *App) InitStaticRoutes(r *mux.Router) {
|
func (app *App) InitStaticRoutes(r *mux.Router) {
|
||||||
// Handle static files
|
// Handle static files
|
||||||
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
|
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
|
||||||
|
fs = cacheControl(fs)
|
||||||
app.shttp = http.NewServeMux()
|
app.shttp = http.NewServeMux()
|
||||||
app.shttp.Handle("/", fs)
|
app.shttp.Handle("/", fs)
|
||||||
r.PathPrefix("/").Handler(fs)
|
r.PathPrefix("/").Handler(fs)
|
||||||
|
@ -76,6 +77,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||||
configureSlackOauth(handler, write, apper.App())
|
configureSlackOauth(handler, write, apper.App())
|
||||||
configureWriteAsOauth(handler, write, apper.App())
|
configureWriteAsOauth(handler, write, apper.App())
|
||||||
configureGitlabOauth(handler, write, apper.App())
|
configureGitlabOauth(handler, write, apper.App())
|
||||||
|
configureGenericOauth(handler, write, apper.App())
|
||||||
|
configureGiteaOauth(handler, write, apper.App())
|
||||||
|
|
||||||
// Set up dyamic page handlers
|
// Set up dyamic page handlers
|
||||||
// Handle auth
|
// Handle auth
|
||||||
|
|
38
routes_test.go
Normal file
38
routes_test.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package writefreely
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCacheControlForStaticFiles(t *testing.T) {
|
||||||
|
app := NewApp("testdata/config.ini")
|
||||||
|
if err := app.LoadConfig(); err != nil {
|
||||||
|
t.Fatalf("Could not create an app; %v", err)
|
||||||
|
}
|
||||||
|
router := mux.NewRouter()
|
||||||
|
app.InitStaticRoutes(router)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/style.css", nil)
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
if code := rec.Result().StatusCode; code != http.StatusOK {
|
||||||
|
t.Fatalf("Could not get /style.css, got HTTP status %d", code)
|
||||||
|
}
|
||||||
|
actual := rec.Result().Header.Get("Cache-Control")
|
||||||
|
|
||||||
|
expectedDirectives := []string{
|
||||||
|
"public",
|
||||||
|
"max-age",
|
||||||
|
"immutable",
|
||||||
|
}
|
||||||
|
for _, expected := range expectedDirectives {
|
||||||
|
if !strings.Contains(actual, expected) {
|
||||||
|
t.Errorf("Expected Cache-Control header to contain '%s', but was '%s'", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
static/img/mark/gitea.png
Normal file
BIN
static/img/mark/gitea.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
static/img/mark/writeas-white.png
Normal file
BIN
static/img/mark/writeas-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
|
@ -1 +0,0 @@
|
||||||
Subproject commit 419b0a6eee7eefc0f85e47f7d4f8227ec28b8e57
|
|
1
static/js/mathjax/tex-svg-full.js
Normal file
1
static/js/mathjax/tex-svg-full.js
Normal file
File diff suppressed because one or more lines are too long
34
static/js/menu.js
Normal file
34
static/js/menu.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
var menuItems = document.querySelectorAll('li.has-submenu');
|
||||||
|
var menuTimer;
|
||||||
|
function closeMenu($menu) {
|
||||||
|
$menu.querySelector('a').setAttribute('aria-expanded', "false");
|
||||||
|
$menu.className = "has-submenu";
|
||||||
|
}
|
||||||
|
Array.prototype.forEach.call(menuItems, function(el, i){
|
||||||
|
el.addEventListener("mouseover", function(event){
|
||||||
|
let $menu = document.querySelectorAll(".has-submenu.open");
|
||||||
|
if ($menu.length > 0) {
|
||||||
|
closeMenu($menu[0]);
|
||||||
|
}
|
||||||
|
this.className = "has-submenu open";
|
||||||
|
this.querySelector('a').setAttribute('aria-expanded', "true");
|
||||||
|
clearTimeout(menuTimer);
|
||||||
|
});
|
||||||
|
el.addEventListener("mouseout", function(event){
|
||||||
|
menuTimer = setTimeout(function(event){
|
||||||
|
let $menu = document.querySelector(".has-submenu.open");
|
||||||
|
closeMenu($menu);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
el.querySelector('a').addEventListener("click", function(event){
|
||||||
|
if (this.parentNode.className == "has-submenu") {
|
||||||
|
this.parentNode.className = "has-submenu open";
|
||||||
|
this.setAttribute('aria-expanded', "true");
|
||||||
|
} else {
|
||||||
|
this.parentNode.className = "has-submenu";
|
||||||
|
this.setAttribute('aria-expanded', "false");
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
31
templates.go
31
templates.go
|
@ -11,6 +11,7 @@
|
||||||
package writefreely
|
package writefreely
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -38,6 +39,9 @@ var (
|
||||||
"localhtml": localHTML,
|
"localhtml": localHTML,
|
||||||
"tolower": strings.ToLower,
|
"tolower": strings.ToLower,
|
||||||
"title": strings.Title,
|
"title": strings.Title,
|
||||||
|
"hasPrefix": strings.HasPrefix,
|
||||||
|
"hasSuffix": strings.HasSuffix,
|
||||||
|
"dict": dict,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -85,12 +89,18 @@ func initPage(parentDir, path, key string) {
|
||||||
log.Info(" [%s] %s", key, path)
|
log.Info(" [%s] %s", key, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(
|
files := []string{
|
||||||
path,
|
path,
|
||||||
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
|
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
|
||||||
))
|
}
|
||||||
|
|
||||||
|
if key == "login.tmpl" || key == "landing.tmpl" || key == "signup.tmpl" {
|
||||||
|
files = append(files, filepath.Join(parentDir, templatesDir, "include", "oauth.tmpl"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func initUserPage(parentDir, path, key string) {
|
func initUserPage(parentDir, path, key string) {
|
||||||
|
@ -103,6 +113,7 @@ func initUserPage(parentDir, path, key string) {
|
||||||
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
|
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
|
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
|
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
|
||||||
|
filepath.Join(parentDir, templatesDir, "user", "include", "nav.tmpl"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,3 +211,19 @@ func localHTML(term, lang string) template.HTML {
|
||||||
s = strings.Replace(s, "write.as", "<a href=\"https://writefreely.org\">writefreely</a>", 1)
|
s = strings.Replace(s, "write.as", "<a href=\"https://writefreely.org\">writefreely</a>", 1)
|
||||||
return template.HTML(s)
|
return template.HTML(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// from: https://stackoverflow.com/a/18276968/1549194
|
||||||
|
func dict(values ...interface{}) (map[string]interface{}, error) {
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("dict: invalid number of parameters")
|
||||||
|
}
|
||||||
|
dict := make(map[string]interface{}, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, ok := values[i].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("dict: keys must be strings")
|
||||||
|
}
|
||||||
|
dict[key] = values[i+1]
|
||||||
|
}
|
||||||
|
return dict, nil
|
||||||
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<nav id="user-nav">
|
<nav id="user-nav">
|
||||||
{{if .Username}}
|
{{if .Username}}
|
||||||
<nav class="dropdown-nav">
|
<nav class="dropdown-nav">
|
||||||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
<ul><li class="has-submenu"><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
||||||
<li><a href="/me/settings">Account settings</a></li>
|
<li><a href="/me/settings">Account settings</a></li>
|
||||||
<li><a href="/me/export">Export</a></li>
|
<li><a href="/me/export">Export</a></li>
|
||||||
|
@ -67,6 +67,7 @@
|
||||||
{{ template "footer" . }}
|
{{ template "footer" . }}
|
||||||
|
|
||||||
{{if not .JSDisabled}}
|
{{if not .JSDisabled}}
|
||||||
|
<script type="text/javascript" src="/js/menu.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{{if .WebFonts}}
|
{{if .WebFonts}}
|
||||||
try { // Google Fonts
|
try { // Google Fonts
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
||||||
{{if or .IsOwner .SingleUser}}<nav id="manage"><ul>
|
{{if or .IsOwner .SingleUser}}<nav id="manage"><ul>
|
||||||
<li><a onclick="void(0)">☰ Menu</a>
|
<li class="has-submenu"><a onclick="void(0)">☰ Menu</a>
|
||||||
<ul>
|
<ul>
|
||||||
{{ if .IsOwner }}
|
{{ if .IsOwner }}
|
||||||
{{if .SingleUser}}
|
{{if .SingleUser}}
|
||||||
|
@ -117,6 +117,7 @@
|
||||||
<script src="/js/h.js"></script>
|
<script src="/js/h.js"></script>
|
||||||
<script src="/js/postactions.js"></script>
|
<script src="/js/postactions.js"></script>
|
||||||
<script src="/js/localdate.js"></script>
|
<script src="/js/localdate.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/menu.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var deleting = false;
|
var deleting = false;
|
||||||
function delPost(e, id, owned) {
|
function delPost(e, id, owned) {
|
||||||
|
|
37
templates/include/oauth.tmpl
Normal file
37
templates/include/oauth.tmpl
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{{define "oauth-buttons"}}
|
||||||
|
{{ if or .SlackEnabled .WriteAsEnabled .GitLabEnabled .GiteaEnabled .GenericEnabled }}
|
||||||
|
<div class="row content-container signinbtns">
|
||||||
|
{{ if .SlackEnabled }}
|
||||||
|
<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>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .WriteAsEnabled }}
|
||||||
|
<a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as">
|
||||||
|
<img src="/img/mark/writeas-white.png" />
|
||||||
|
Sign in with <strong>Write.as</strong>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .GitLabEnabled }}
|
||||||
|
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab">
|
||||||
|
<img src="/img/mark/gitlab.png" />
|
||||||
|
Sign in with <strong>{{.GitLabDisplayName}}</strong>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .GiteaEnabled }}
|
||||||
|
<a class="btn cta loginbtn" id="gitea-login" href="/oauth/gitea">
|
||||||
|
<img src="/img/mark/gitea.png" />
|
||||||
|
Sign in with <strong>{{.GiteaDisplayName}}</strong>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .GenericEnabled }}
|
||||||
|
<a class="btn cta loginbtn" id="generic-oauth-login" href="/oauth/generic">Sign in with <strong>{{.GenericDisplayName}}</strong></a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if not .DisablePasswordAuth}}
|
||||||
|
<div class="or">
|
||||||
|
<p>or</p>
|
||||||
|
<hr class="short" />
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{ end }}
|
||||||
|
{{end}}
|
|
@ -79,17 +79,20 @@
|
||||||
|
|
||||||
<!-- Include mathjax configuration -->
|
<!-- Include mathjax configuration -->
|
||||||
{{define "mathjax"}}
|
{{define "mathjax"}}
|
||||||
<script type="text/x-mathjax-config">
|
<script>
|
||||||
MathJax.Hub.Config({
|
MathJax = {
|
||||||
extensions: ["tex2jax.js"],
|
tex: {
|
||||||
jax: ["input/TeX", "output/HTML-CSS"],
|
inlineMath: [
|
||||||
tex2jax: {
|
["\\(", "\\)"],
|
||||||
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
|
['$', '$'],
|
||||||
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
|
],
|
||||||
processEscapes: true
|
displayMath: [
|
||||||
|
['$$', '$$'],
|
||||||
|
['\\[', '\\]'],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"HTML-CSS": { fonts: ["TeX"] }
|
};
|
||||||
});
|
</script>
|
||||||
|
<script type="text/javascript" id="MathJax-script" src="/js/mathjax/tex-svg-full.js" async>
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" src="/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML" async></script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
{{if not .SingleUser}}<h1><a href="/me/c/" title="View blogs"><img class="ic-24dp" src="/img/ic_blogs_dark@2x.png" /></a></h1>{{end}}
|
{{if not .SingleUser}}<h1><a href="/me/c/" title="View blogs"><img class="ic-24dp" src="/img/ic_blogs_dark@2x.png" /></a></h1>{{end}}
|
||||||
<nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul>
|
<nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul>
|
||||||
{{if .Editing}}<li>{{if .EditCollection}}<a href="{{.EditCollection.CanonicalURL}}">{{.EditCollection.Title}}</a>{{else}}<a>Draft</a>{{end}}</li>
|
{{if .Editing}}<li>{{if .EditCollection}}<a href="{{.EditCollection.CanonicalURL}}">{{.EditCollection.Title}}</a>{{else}}<a>Draft</a>{{end}}</li>
|
||||||
{{else}}<li><a id="publish-to"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
{{else}}<li class="has-submenu"><a id="publish-to"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="menu-heading">Publish to...</li>
|
<li class="menu-heading">Publish to...</li>
|
||||||
{{if .Blogs}}{{range $idx, $el := .Blogs}}
|
{{if .Blogs}}{{range $idx, $el := .Blogs}}
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
</li>{{end}}
|
</li>{{end}}
|
||||||
</ul></nav>
|
</ul></nav>
|
||||||
<nav id="font-picker" class="if-room room-3 hidden" style="margin-left:-1em"><ul>
|
<nav id="font-picker" class="if-room room-3 hidden" style="margin-left:-1em"><ul>
|
||||||
<li><a href="#" id="" onclick="return false"><img class="ic-24dp" src="/img/ic_font_dark@2x.png" /> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
<li class="has-submenu"><a href="#" id="" onclick="return false"><img class="ic-24dp" src="/img/ic_font_dark@2x.png" /> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
||||||
<ul style="text-align: center">
|
<ul style="text-align: center">
|
||||||
<li class="menu-heading">Font</li>
|
<li class="menu-heading">Font</li>
|
||||||
<li class="selected"><a class="font norm" href="#norm">Serif</a></li>
|
<li class="selected"><a class="font norm" href="#norm">Serif</a></li>
|
||||||
|
@ -66,28 +66,50 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<script src="/js/h.js"></script>
|
<script src="/js/h.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/menu.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
var btns = Array.prototype.slice.call(document.getElementById('tools').querySelectorAll('a img'));
|
|
||||||
var newTheme = '';
|
|
||||||
if (document.body.classList.contains('light')) {
|
if (document.body.classList.contains('light')) {
|
||||||
newTheme = 'dark';
|
setTheme('dark');
|
||||||
document.body.className = document.body.className.replace(/(?:^|\s)light(?!\S)/g, newTheme);
|
|
||||||
for (var i=0; i<btns.length; i++) {
|
|
||||||
btns[i].src = btns[i].src.replace('_dark@2x.png', '@2x.png');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
newTheme = 'light';
|
setTheme('light');
|
||||||
document.body.className = document.body.className.replace(/(?:^|\s)dark(?!\S)/g, newTheme);
|
}
|
||||||
|
H.set('padTheme', newTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(newTheme) {
|
||||||
|
document.body.classList.remove('light');
|
||||||
|
document.body.classList.remove('dark');
|
||||||
|
document.body.classList.add(newTheme);
|
||||||
|
var btns = Array.prototype.slice.call(document.getElementById('tools').querySelectorAll('a img'));
|
||||||
|
if (newTheme == 'light') {
|
||||||
|
// check if current theme is dark otherwise we'll get `_dark_dark@2x.png`
|
||||||
|
if (H.get('padTheme', 'auto') == 'dark'){
|
||||||
for (var i=0; i<btns.length; i++) {
|
for (var i=0; i<btns.length; i++) {
|
||||||
btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png');
|
btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (var i=0; i<btns.length; i++) {
|
||||||
|
btns[i].src = btns[i].src.replace('_dark@2x.png', '@2x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
H.set('padTheme', newTheme);
|
H.set('padTheme', newTheme);
|
||||||
}
|
}
|
||||||
if (H.get('padTheme', 'light') != 'light') {
|
|
||||||
toggleTheme();
|
if (H.get('padTheme', 'auto') == 'light') {
|
||||||
|
setTheme('light');
|
||||||
|
} else if (H.get('padTheme', 'auto') == 'dark') {
|
||||||
|
setTheme('dark');
|
||||||
|
} else {
|
||||||
|
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
if (isDarkMode) {
|
||||||
|
setTheme('dark');
|
||||||
|
} else {
|
||||||
|
setTheme('light');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var $writer = H.getEl('writer');
|
var $writer = H.getEl('writer');
|
||||||
var $btnPublish = H.getEl('publish');
|
var $btnPublish = H.getEl('publish');
|
||||||
var $btnEraseEdit = H.getEl('edited-elsewhere');
|
var $btnEraseEdit = H.getEl('edited-elsewhere');
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<nav class="pager">
|
<nav class="pager pages">
|
||||||
{{range $n := .TotalPages}}<a href="/admin/users{{if ne $n 1}}?p={{$n}}{{end}}" {{if eq $.CurPage $n}}class="selected"{{end}}>{{$n}}</a>{{end}}
|
{{range $n := .TotalPages}}<a href="/admin/users{{if ne $n 1}}?p={{$n}}{{end}}" {{if eq $.CurPage $n}}class="selected"{{end}}>{{$n}}</a>{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,12 @@ textarea.section.norm {
|
||||||
{{if .Silenced}}
|
{{if .Silenced}}
|
||||||
{{template "user-silenced"}}
|
{{template "user-silenced"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2>
|
|
||||||
|
{{template "collection-breadcrumbs" .}}
|
||||||
|
|
||||||
|
<h1>Customize</h1>
|
||||||
|
|
||||||
|
{{template "collection-nav" (dict "Alias" .Alias "Path" .Path "SingleUser" .SingleUser)}}
|
||||||
|
|
||||||
{{if .Flashes}}<ul class="errors">
|
{{if .Flashes}}<ul class="errors">
|
||||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||||
|
|
|
@ -12,16 +12,18 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<h1>Blogs</h1>
|
<h1>Blogs</h1>
|
||||||
<ul class="atoms collections">
|
<ul class="atoms collections">
|
||||||
{{range $i, $el := .Collections}}<li class="collection"><h3>
|
{{range $i, $el := .Collections}}<li class="collection">
|
||||||
<a class="title" href="/{{.Alias}}/">{{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a>
|
<div class="row lineitem">
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
<a class="title" href="/{{.Alias}}/" >{{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a>
|
||||||
|
<span class="electron" {{if .IsPrivate}}style="font-style: italic"{{end}}>{{if .IsPrivate}}private{{else}}{{.DisplayCanonicalURL}}{{end}}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<h4>
|
{{template "collection-nav" (dict "Alias" .Alias "Path" $.Path "SingleUser" $.SingleUser "CanPost" true )}}
|
||||||
<a class="action new-post" href="{{if $.Chorus}}/new{{else}}/{{end}}#{{.Alias}}">new post</a>
|
|
||||||
<a class="action" href="/me/c/{{.Alias}}">customize</a>
|
|
||||||
<a class="action" href="/me/c/{{.Alias}}/stats">stats</a>
|
|
||||||
</h4>
|
|
||||||
{{if .Description}}<p class="description">{{.Description}}</p>{{end}}
|
{{if .Description}}<p class="description">{{.Description}}</p>{{end}}
|
||||||
</li>{{end}}
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>{{end}}
|
||||||
<li id="create-collection">
|
<li id="create-collection">
|
||||||
{{if not .NewBlogsDisabled}}
|
{{if not .NewBlogsDisabled}}
|
||||||
<form method="POST" action="/api/collections" id="new-collection-form" onsubmit="return createCollection()">
|
<form method="POST" action="/api/collections" id="new-collection-form" onsubmit="return createCollection()">
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/js/menu.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
try { // Google Fonts
|
try { // Google Fonts
|
||||||
WebFontConfig = {
|
WebFontConfig = {
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
{{define "user-navigation"}}
|
{{define "user-navigation"}}
|
||||||
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}">
|
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}">
|
||||||
|
<nav id="full-nav">
|
||||||
{{if .SingleUser}}
|
{{if .SingleUser}}
|
||||||
<nav id="user-nav">
|
<nav id="user-nav">
|
||||||
<nav class="dropdown-nav">
|
<nav class="dropdown-nav">
|
||||||
<ul><li><a href="/" title="View blog" class="title">{{.SiteName}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" />
|
<ul><li><a href="/" title="View blog" class="title">{{.SiteName}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" />
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/me/c/{{.Username}}">Customize</a></li>
|
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
||||||
<li><a href="/me/c/{{.Username}}/stats">Stats</a></li>
|
<li><a href="/me/settings">Account settings</a></li>
|
||||||
<li class="separator"><hr /></li>
|
|
||||||
{{if .IsAdmin}}<li><a href="/admin">Admin</a></li>{{end}}
|
|
||||||
<li><a href="/me/settings">Settings</a></li>
|
|
||||||
<li><a href="/me/import">Import posts</a></li>
|
<li><a href="/me/import">Import posts</a></li>
|
||||||
<li><a href="/me/export">Export</a></li>
|
<li><a href="/me/export">Export</a></li>
|
||||||
<li class="separator"><hr /></li>
|
<li class="separator"><hr /></li>
|
||||||
|
@ -18,19 +16,22 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
|
<a href="/me/c/{{.Username}}" {{if and (hasPrefix .Path "/me/c/") (hasSuffix .Path .Username)}}class="selected"{{end}}>Customize</a>
|
||||||
|
<a href="/me/c/{{.Username}}/stats" {{if hasSuffix .Path "/stats"}}class="selected"{{end}}>Stats</a>
|
||||||
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>
|
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>
|
||||||
<a href="/me/new">New Post</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="right-side">
|
||||||
|
<a class="simple-btn" href="/me/new">New Post</a>
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<nav id="full-nav">
|
|
||||||
<div class="left-side">
|
<div class="left-side">
|
||||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
||||||
</div>
|
</div>
|
||||||
<nav id="user-nav">
|
<nav id="user-nav">
|
||||||
{{if .Username}}
|
{{if .Username}}
|
||||||
<nav class="dropdown-nav">
|
<nav class="dropdown-nav">
|
||||||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
<ul><li class="has-submenu"><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
||||||
<li><a href="/me/settings">Account settings</a></li>
|
<li><a href="/me/settings">Account settings</a></li>
|
||||||
<li><a href="/me/import">Import posts</a></li>
|
<li><a href="/me/import">Import posts</a></li>
|
||||||
|
@ -65,12 +66,13 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
</nav>
|
</nav>
|
||||||
{{if .Chorus}}{{if .Username}}<div class="right-side">
|
{{if .Username}}
|
||||||
<a class="simple-btn" href="/new">New Post</a>
|
<div class="right-side">
|
||||||
</div>{{end}}
|
<a class="simple-btn" href="/{{if .CollAlias}}#{{.CollAlias}}{{end}}">New Post</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</header>
|
</header>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "header"}}<!DOCTYPE HTML>
|
{{define "header"}}<!DOCTYPE HTML>
|
||||||
|
|
16
templates/user/include/nav.tmpl
Normal file
16
templates/user/include/nav.tmpl
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{{define "collection-breadcrumbs"}}
|
||||||
|
{{if and .Collection (not .SingleUser)}}<nav id="org-nav"><a href="/me/c/">Blogs</a> / <a class="coll-name" href="/{{.Collection.Alias}}/">{{.Collection.DisplayTitle}}</a></nav>{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "collection-nav"}}
|
||||||
|
{{if not .SingleUser}}
|
||||||
|
<header class="admin">
|
||||||
|
<nav class="pager">
|
||||||
|
{{if .CanPost}}<a href="{{if .SingleUser}}/me/new{{else}}/#{{.Alias}}{{end}}" class="btn gentlecta">New Post</a>{{end}}
|
||||||
|
<a href="/me/c/{{.Alias}}" {{if and (hasPrefix .Path "/me/c/") (hasSuffix .Path .Alias)}}class="selected"{{end}}>Customize</a>
|
||||||
|
<a href="/me/c/{{.Alias}}/stats" {{if hasSuffix .Path "/stats"}}class="selected"{{end}}>Stats</a>
|
||||||
|
<a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">View Blog →</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
|
@ -16,7 +16,7 @@ h3 { font-weight: normal; }
|
||||||
{{if .Silenced}}
|
{{if .Silenced}}
|
||||||
{{template "user-silenced"}}
|
{{template "user-silenced"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<h1>{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}<a href="/admin">admin settings</a>{{end}}{{end}}</h1>
|
<h1>{{if .IsLogOut}}Before you go...{{else}}Account Settings{{end}}</h1>
|
||||||
{{if .Flashes}}<ul class="errors">
|
{{if .Flashes}}<ul class="errors">
|
||||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||||
</ul>{{end}}
|
</ul>{{end}}
|
||||||
|
@ -41,6 +41,7 @@ h3 { font-weight: normal; }
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{if not .DisablePasswordAuth}}
|
||||||
<form method="post" action="/api/me/self" autocomplete="false">
|
<form method="post" action="/api/me/self" autocomplete="false">
|
||||||
<input type="hidden" name="logout" value="{{.IsLogOut}}" />
|
<input type="hidden" name="logout" value="{{.IsLogOut}}" />
|
||||||
<div class="option">
|
<div class="option">
|
||||||
|
@ -72,6 +73,7 @@ h3 { font-weight: normal; }
|
||||||
<input type="submit" value="Save changes" tabindex="4" />
|
<input type="submit" value="Save changes" tabindex="4" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{ if .OauthSection }}
|
{{ if .OauthSection }}
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -86,42 +88,65 @@ h3 { font-weight: normal; }
|
||||||
<input type="hidden" name="client_id" value="{{ $oauth_account.ClientID }}" />
|
<input type="hidden" name="client_id" value="{{ $oauth_account.ClientID }}" />
|
||||||
<input type="hidden" name="remote_user_id" value="{{ $oauth_account.RemoteUserID }}" />
|
<input type="hidden" name="remote_user_id" value="{{ $oauth_account.RemoteUserID }}" />
|
||||||
<div class="section oauth-provider">
|
<div class="section oauth-provider">
|
||||||
|
{{ 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 }}" />
|
<img src="/img/mark/{{$oauth_account.Provider}}.png" alt="{{ $oauth_account.Provider | title }}" />
|
||||||
<input type="submit" value="Remove {{ $oauth_account.Provider | title }}" />
|
<input type="submit" value="Remove {{ $oauth_account.Provider | title }}" />
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if or .OauthSlack .OauthWriteAs .OauthGitLab }}
|
{{ if or .OauthSlack .OauthWriteAs .OauthGitLab .OauthGeneric .OauthGitea }}
|
||||||
<div class="option">
|
<div class="option">
|
||||||
<h2>Link External Accounts</h2>
|
<h2>Link External Accounts</h2>
|
||||||
<p>Connect additional accounts to enable logging in with those providers, instead of using your username and password.</p>
|
<p>Connect additional accounts to enable logging in with those providers, instead of using your username and password.</p>
|
||||||
<div class="row">
|
<div class="row signinbtns">
|
||||||
{{ if .OauthWriteAs }}
|
{{ if .OauthWriteAs }}
|
||||||
<div class="section oauth-provider">
|
<div class="section oauth-provider">
|
||||||
<img src="/img/mark/writeas.png" alt="Write.as" />
|
|
||||||
<a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as?attach=t">
|
<a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as?attach=t">
|
||||||
|
<img src="/img/mark/writeas-white.png" alt="Write.as" />
|
||||||
Link <strong>Write.as</strong>
|
Link <strong>Write.as</strong>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .OauthSlack }}
|
{{ if .OauthSlack }}
|
||||||
<div class="section oauth-provider">
|
<div class="section oauth-provider">
|
||||||
|
<a class="btn cta loginbtn" id="slack-login" href="/oauth/slack?attach=t">
|
||||||
<img src="/img/mark/slack.png" alt="Slack" />
|
<img src="/img/mark/slack.png" alt="Slack" />
|
||||||
<a class="btn cta loginbtn" href="/oauth/slack?attach=t">
|
|
||||||
Link <strong>Slack</strong>
|
Link <strong>Slack</strong>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .OauthGitLab }}
|
{{ if .OauthGitLab }}
|
||||||
<div class="section oauth-provider">
|
<div class="section oauth-provider">
|
||||||
<img src="/img/mark/gitlab.png" alt="GitLab" />
|
|
||||||
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab?attach=t">
|
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab?attach=t">
|
||||||
|
<img src="/img/mark/gitlab.png" alt="GitLab" />
|
||||||
Link <strong>{{.GitLabDisplayName}}</strong>
|
Link <strong>{{.GitLabDisplayName}}</strong>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .OauthGitea }}
|
||||||
|
<div class="section oauth-provider">
|
||||||
|
<a class="btn cta loginbtn" id="gitea-login" href="/oauth/gitea?attach=t">
|
||||||
|
<img src="/img/mark/gitea.png" alt="Gitea" />
|
||||||
|
Link <strong>{{.GiteaDisplayName}}</strong>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .OauthGeneric }}
|
||||||
|
<div class="section oauth-provider">
|
||||||
|
<a class="btn cta loginbtn" id="generic-oauth-login" href="/oauth/generic?attach=t">
|
||||||
|
Link <strong>{{ .OauthGenericDisplayName }}</strong>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -20,7 +20,14 @@ td.none {
|
||||||
{{if .Silenced}}
|
{{if .Silenced}}
|
||||||
{{template "user-silenced"}}
|
{{template "user-silenced"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>
|
|
||||||
|
{{template "collection-breadcrumbs" .}}
|
||||||
|
|
||||||
|
<h1 id="posts-header">Stats</h1>
|
||||||
|
|
||||||
|
{{if .Collection}}
|
||||||
|
{{template "collection-nav" (dict "Alias" .Collection.Alias "Path" .Path "SingleUser" .SingleUser)}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<p>Stats for all time.</p>
|
<p>Stats for all time.</p>
|
||||||
|
|
||||||
|
|
1
testdata/.gitignore
vendored
Normal file
1
testdata/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
!config.ini
|
2
testdata/config.ini
vendored
Normal file
2
testdata/config.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[server]
|
||||||
|
static_parent_dir = testdata
|
3
testdata/static/style.css
vendored
Normal file
3
testdata/static/style.css
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
body {
|
||||||
|
background-color: lightblue;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue