mirror of
https://github.com/writefreely/writefreely
synced 2024-11-24 17:43:05 +00:00
Merge branch 'develop' into shorter-config-process
Move flag parsing to main.go as per the issue description
This commit is contained in:
commit
6506709fbc
33 changed files with 1106 additions and 583 deletions
11
Makefile
11
Makefile
|
@ -100,13 +100,16 @@ ui : force_look
|
|||
cd less/; $(MAKE) $(MFLAGS)
|
||||
|
||||
assets : generate
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql sqlite.sql
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
|
||||
|
||||
assets-no-sqlite: generate
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql
|
||||
|
||||
dev-assets : generate
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore -debug schema.sql sqlite.sql
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore -debug -tags="!wflib" schema.sql sqlite.sql
|
||||
|
||||
lib-assets : generate
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore -o bindata-lib.go -tags="wflib" schema.sql
|
||||
|
||||
generate :
|
||||
@hash go-bindata > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
|
@ -123,7 +126,7 @@ $(TMPBIN)/xgo: deps $(TMPBIN)
|
|||
$(GOBUILD) -o $(TMPBIN)/xgo github.com/karalabe/xgo
|
||||
|
||||
ci-assets : $(TMPBIN)/go-bindata
|
||||
$(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql sqlite.sql
|
||||
$(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
|
||||
|
||||
clean :
|
||||
-rm -rf build
|
||||
|
|
54
account.go
54
account.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -49,7 +49,7 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func NewUserPage(app *app, r *http.Request, u *User, title string, flashes []string) *UserPage {
|
||||
func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []string) *UserPage {
|
||||
up := &UserPage{
|
||||
StaticPage: pageForReq(app, r),
|
||||
PageTitle: title,
|
||||
|
@ -73,12 +73,12 @@ const (
|
|||
|
||||
var actuallyUsernameReg = regexp.MustCompile("username is actually ([a-z0-9\\-]+)\\. Please try that, instead")
|
||||
|
||||
func apiSignup(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
_, err := signup(app, w, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func signup(app *app, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||
func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
|
||||
// Get params
|
||||
|
@ -113,7 +113,7 @@ func signup(app *app, w http.ResponseWriter, r *http.Request) (*AuthUser, error)
|
|||
return signupWithRegistration(app, ur, w, r)
|
||||
}
|
||||
|
||||
func signupWithRegistration(app *app, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||
func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
|
||||
// Validate required params (alias)
|
||||
|
@ -154,7 +154,7 @@ func signupWithRegistration(app *app, signup userRegistration, w http.ResponseWr
|
|||
Created: time.Now().Truncate(time.Second).UTC(),
|
||||
}
|
||||
if signup.Email != "" {
|
||||
encEmail, err := data.Encrypt(app.keys.emailKey, signup.Email)
|
||||
encEmail, err := data.Encrypt(app.keys.EmailKey, signup.Email)
|
||||
if err != nil {
|
||||
log.Error("Unable to encrypt email: %s\n", err)
|
||||
} else {
|
||||
|
@ -229,7 +229,7 @@ func signupWithRegistration(app *app, signup userRegistration, w http.ResponseWr
|
|||
return resUser, nil
|
||||
}
|
||||
|
||||
func viewLogout(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewLogout(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
session, err := app.sessionStore.Get(r, cookieName)
|
||||
if err != nil {
|
||||
return ErrInternalCookieSession
|
||||
|
@ -268,7 +268,7 @@ func viewLogout(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.HTTPError{http.StatusFound, "/"}
|
||||
}
|
||||
|
||||
func handleAPILogout(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleAPILogout(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
accessToken := r.Header.Get("Authorization")
|
||||
if accessToken == "" {
|
||||
return ErrNoAccessToken
|
||||
|
@ -284,7 +284,7 @@ func handleAPILogout(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.HTTPError{Status: http.StatusNoContent}
|
||||
}
|
||||
|
||||
func viewLogin(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var earlyError string
|
||||
oneTimeToken := r.FormValue("with")
|
||||
if oneTimeToken != "" {
|
||||
|
@ -333,7 +333,7 @@ func viewLogin(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func webLogin(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func webLogin(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
err := login(app, w, r)
|
||||
if err != nil {
|
||||
username := r.FormValue("alias")
|
||||
|
@ -370,7 +370,7 @@ func webLogin(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
var loginAttemptUsers = sync.Map{}
|
||||
|
||||
func login(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func login(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
oneTimeToken := r.FormValue("with")
|
||||
verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "")
|
||||
|
@ -534,7 +534,7 @@ func login(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getVerboseAuthUser(app *app, token string, u *User, verbose bool) *AuthUser {
|
||||
func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser {
|
||||
resUser := &AuthUser{
|
||||
AccessToken: token,
|
||||
User: u,
|
||||
|
@ -563,7 +563,7 @@ func getVerboseAuthUser(app *app, token string, u *User, verbose bool) *AuthUser
|
|||
return resUser
|
||||
}
|
||||
|
||||
func viewExportOptions(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
// Fetch extra user data
|
||||
p := NewUserPage(app, r, u, "Export", nil)
|
||||
|
||||
|
@ -571,7 +571,7 @@ func viewExportOptions(app *app, u *User, w http.ResponseWriter, r *http.Request
|
|||
return nil
|
||||
}
|
||||
|
||||
func viewExportPosts(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
|
||||
func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
|
||||
var filename string
|
||||
var u = &User{}
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
|
@ -635,7 +635,7 @@ func viewExportPosts(app *app, w http.ResponseWriter, r *http.Request) ([]byte,
|
|||
return data, filename, err
|
||||
}
|
||||
|
||||
func viewExportFull(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
|
||||
func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
|
||||
var err error
|
||||
filename := ""
|
||||
u := getUserSession(app, r)
|
||||
|
@ -655,7 +655,7 @@ func viewExportFull(app *app, w http.ResponseWriter, r *http.Request) ([]byte, s
|
|||
return data, filename, err
|
||||
}
|
||||
|
||||
func viewMeAPI(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
uObj := struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
|
@ -679,7 +679,7 @@ func viewMeAPI(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.WriteSuccess(w, uObj, http.StatusOK)
|
||||
}
|
||||
|
||||
func viewMyPostsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
if !reqJSON {
|
||||
return ErrBadRequestedType
|
||||
|
@ -710,7 +710,7 @@ func viewMyPostsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) e
|
|||
return impart.WriteSuccess(w, p, http.StatusOK)
|
||||
}
|
||||
|
||||
func viewMyCollectionsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
if !reqJSON {
|
||||
return ErrBadRequestedType
|
||||
|
@ -724,7 +724,7 @@ func viewMyCollectionsAPI(app *app, u *User, w http.ResponseWriter, r *http.Requ
|
|||
return impart.WriteSuccess(w, p, http.StatusOK)
|
||||
}
|
||||
|
||||
func viewArticles(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
p, err := app.db.GetAnonymousPosts(u)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch anon posts: %v", err)
|
||||
|
@ -761,7 +761,7 @@ func viewArticles(app *app, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func viewCollections(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
c, err := app.db.GetCollections(u)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch collections: %v", err)
|
||||
|
@ -792,7 +792,7 @@ func viewCollections(app *app, u *User, w http.ResponseWriter, r *http.Request)
|
|||
return nil
|
||||
}
|
||||
|
||||
func viewEditCollection(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
c, err := app.db.GetCollection(vars["collection"])
|
||||
if err != nil {
|
||||
|
@ -815,7 +815,7 @@ func viewEditCollection(app *app, u *User, w http.ResponseWriter, r *http.Reques
|
|||
return nil
|
||||
}
|
||||
|
||||
func updateSettings(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
|
||||
var s userSettings
|
||||
|
@ -904,7 +904,7 @@ func updateSettings(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func updatePassphrase(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func updatePassphrase(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
accessToken := r.Header.Get("Authorization")
|
||||
if accessToken == "" {
|
||||
return ErrNoAccessToken
|
||||
|
@ -943,7 +943,7 @@ func updatePassphrase(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.WriteSuccess(w, struct{}{}, http.StatusOK)
|
||||
}
|
||||
|
||||
func viewStats(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
var c *Collection
|
||||
var err error
|
||||
vars := mux.Vars(r)
|
||||
|
@ -994,7 +994,7 @@ func viewStats(app *app, u *User, w http.ResponseWriter, r *http.Request) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func viewSettings(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
fullUser, err := app.db.GetUserByID(u.ID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get user for settings: %s", err)
|
||||
|
@ -1025,7 +1025,7 @@ func viewSettings(app *app, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func saveTempInfo(app *app, key, val string, r *http.Request, w http.ResponseWriter) error {
|
||||
func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error {
|
||||
session, err := app.sessionStore.Get(r, "t")
|
||||
if err != nil {
|
||||
return ErrInternalCookieSession
|
||||
|
@ -1039,7 +1039,7 @@ func saveTempInfo(app *app, key, val string, r *http.Request, w http.ResponseWri
|
|||
return err
|
||||
}
|
||||
|
||||
func getTempInfo(app *app, key string, r *http.Request, w http.ResponseWriter) string {
|
||||
func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) string {
|
||||
session, err := app.sessionStore.Get(r, "t")
|
||||
if err != nil {
|
||||
return ""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -24,7 +24,6 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/activity/streams"
|
||||
"github.com/writeas/httpsig"
|
||||
|
@ -63,7 +62,7 @@ func (ru *RemoteUser) AsPerson() *activitystreams.Person {
|
|||
}
|
||||
}
|
||||
|
||||
func handleFetchCollectionActivities(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Server", serverSoftware)
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
@ -81,13 +80,14 @@ func handleFetchCollectionActivities(app *app, w http.ResponseWriter, r *http.Re
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
p := c.PersonObject()
|
||||
|
||||
return impart.RenderActivityJSON(w, p, http.StatusOK)
|
||||
}
|
||||
|
||||
func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Server", serverSoftware)
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
@ -105,6 +105,7 @@ func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Reques
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if app.cfg.App.SingleUser {
|
||||
if alias != c.Alias {
|
||||
|
@ -139,7 +140,7 @@ func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Reques
|
|||
return impart.RenderActivityJSON(w, ocp, http.StatusOK)
|
||||
}
|
||||
|
||||
func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Server", serverSoftware)
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
@ -157,6 +158,7 @@ func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Req
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
accountRoot := c.FederatedAccount()
|
||||
|
||||
|
@ -184,7 +186,7 @@ func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Req
|
|||
return impart.RenderActivityJSON(w, ocp, http.StatusOK)
|
||||
}
|
||||
|
||||
func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Server", serverSoftware)
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
@ -202,6 +204,7 @@ func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Req
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
accountRoot := c.FederatedAccount()
|
||||
|
||||
|
@ -219,7 +222,7 @@ func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Req
|
|||
return impart.RenderActivityJSON(w, ocp, http.StatusOK)
|
||||
}
|
||||
|
||||
func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Server", serverSoftware)
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
@ -235,6 +238,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
|
|||
// TODO: return Reject?
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if debugging {
|
||||
dump, err := httputil.DumpRequest(r, true)
|
||||
|
@ -350,7 +354,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
|
|||
log.Error("No to! %v", err)
|
||||
return
|
||||
}
|
||||
err = makeActivityPost(p, fullActor.Inbox, am)
|
||||
err = makeActivityPost(app.cfg.App.Host, p, fullActor.Inbox, am)
|
||||
if err != nil {
|
||||
log.Error("Unable to make activity POST: %v", err)
|
||||
return
|
||||
|
@ -371,13 +375,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
|
|||
// Add follower locally, since it wasn't found before
|
||||
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
|
||||
if err != nil {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number != mySQLErrDuplicateKey {
|
||||
t.Rollback()
|
||||
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !app.db.isDuplicateKeyErr(err) {
|
||||
t.Rollback()
|
||||
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
||||
return
|
||||
|
@ -394,13 +392,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
|
|||
// Add in key
|
||||
_, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM)
|
||||
if err != nil {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number != mySQLErrDuplicateKey {
|
||||
t.Rollback()
|
||||
log.Error("Couldn't add follower keys in DB: %v\n", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !app.db.isDuplicateKeyErr(err) {
|
||||
t.Rollback()
|
||||
log.Error("Couldn't add follower keys in DB: %v\n", err)
|
||||
return
|
||||
|
@ -411,13 +403,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
|
|||
// Add follow
|
||||
_, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", c.ID, followerID)
|
||||
if err != nil {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number != mySQLErrDuplicateKey {
|
||||
t.Rollback()
|
||||
log.Error("Couldn't add follower in DB: %v\n", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !app.db.isDuplicateKeyErr(err) {
|
||||
t.Rollback()
|
||||
log.Error("Couldn't add follower in DB: %v\n", err)
|
||||
return
|
||||
|
@ -442,7 +428,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
|
|||
return nil
|
||||
}
|
||||
|
||||
func makeActivityPost(p *activitystreams.Person, url string, m interface{}) error {
|
||||
func makeActivityPost(hostName string, p *activitystreams.Person, url string, m interface{}) error {
|
||||
log.Info("POST %s", url)
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
|
@ -496,7 +482,7 @@ func makeActivityPost(p *activitystreams.Person, url string, m interface{}) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func resolveIRI(url string) ([]byte, error) {
|
||||
func resolveIRI(hostName, url string) ([]byte, error) {
|
||||
log.Info("GET %s", url)
|
||||
|
||||
r, _ := http.NewRequest("GET", url, nil)
|
||||
|
@ -532,7 +518,7 @@ func resolveIRI(url string) ([]byte, error) {
|
|||
return body, nil
|
||||
}
|
||||
|
||||
func deleteFederatedPost(app *app, p *PublicPost, collID int64) error {
|
||||
func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
|
||||
if debugging {
|
||||
log.Info("Deleting federated post!")
|
||||
}
|
||||
|
@ -566,7 +552,7 @@ func deleteFederatedPost(app *app, p *PublicPost, collID int64) error {
|
|||
na.CC = append(na.CC, f)
|
||||
}
|
||||
|
||||
err = makeActivityPost(actor, si, activitystreams.NewDeleteActivity(na))
|
||||
err = makeActivityPost(app.cfg.App.Host, actor, si, activitystreams.NewDeleteActivity(na))
|
||||
if err != nil {
|
||||
log.Error("Couldn't delete post! %v", err)
|
||||
}
|
||||
|
@ -574,7 +560,7 @@ func deleteFederatedPost(app *app, p *PublicPost, collID int64) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error {
|
||||
func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
||||
if debugging {
|
||||
if isUpdate {
|
||||
log.Info("Federating updated post!")
|
||||
|
@ -620,7 +606,7 @@ func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error {
|
|||
activity.To = na.To
|
||||
activity.CC = na.CC
|
||||
}
|
||||
err = makeActivityPost(actor, si, activity)
|
||||
err = makeActivityPost(app.cfg.App.Host, actor, si, activity)
|
||||
if err != nil {
|
||||
log.Error("Couldn't post! %v", err)
|
||||
}
|
||||
|
@ -628,7 +614,7 @@ func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getRemoteUser(app *app, actorID string) (*RemoteUser, error) {
|
||||
func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
||||
u := RemoteUser{ActorID: actorID}
|
||||
err := app.db.QueryRow("SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox)
|
||||
switch {
|
||||
|
@ -642,7 +628,7 @@ func getRemoteUser(app *app, actorID string) (*RemoteUser, error) {
|
|||
return &u, nil
|
||||
}
|
||||
|
||||
func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
|
||||
func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
|
||||
log.Info("Fetching actor %s locally", actorIRI)
|
||||
actor := &activitystreams.Person{}
|
||||
remoteUser, err := getRemoteUser(app, actorIRI)
|
||||
|
@ -651,7 +637,7 @@ func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser,
|
|||
if iErr.Status == http.StatusNotFound {
|
||||
// Fetch remote actor
|
||||
log.Info("Not found; fetching actor %s remotely", actorIRI)
|
||||
actorResp, err := resolveIRI(actorIRI)
|
||||
actorResp, err := resolveIRI(app.cfg.App.Host, actorIRI)
|
||||
if err != nil {
|
||||
log.Error("Unable to get actor! %v", err)
|
||||
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."}
|
||||
|
|
49
admin.go
49
admin.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -96,7 +96,7 @@ func (c instanceContent) UpdatedFriendly() string {
|
|||
return c.Updated.Format("January 2, 2006, 3:04 PM")
|
||||
}
|
||||
|
||||
func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
updateAppStats()
|
||||
p := struct {
|
||||
*UserPage
|
||||
|
@ -117,7 +117,7 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleViewAdminUsers(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
p := struct {
|
||||
*UserPage
|
||||
Config config.AppCfg
|
||||
|
@ -157,7 +157,7 @@ func handleViewAdminUsers(app *app, u *User, w http.ResponseWriter, r *http.Requ
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleViewAdminUser(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
if username == "" {
|
||||
|
@ -229,7 +229,7 @@ func handleViewAdminUser(app *app, u *User, w http.ResponseWriter, r *http.Reque
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
p := struct {
|
||||
*UserPage
|
||||
Config config.AppCfg
|
||||
|
@ -287,7 +287,7 @@ func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Requ
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
slug := vars["slug"]
|
||||
if slug == "" {
|
||||
|
@ -329,7 +329,7 @@ func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Reque
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["page"]
|
||||
|
||||
|
@ -347,33 +347,34 @@ func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Req
|
|||
return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
|
||||
}
|
||||
|
||||
func handleAdminUpdateConfig(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
app.cfg.App.SiteName = r.FormValue("site_name")
|
||||
app.cfg.App.SiteDesc = r.FormValue("site_desc")
|
||||
app.cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
|
||||
func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
apper.App().cfg.App.SiteName = r.FormValue("site_name")
|
||||
apper.App().cfg.App.SiteDesc = r.FormValue("site_desc")
|
||||
apper.App().cfg.App.Landing = r.FormValue("landing")
|
||||
apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
|
||||
mul, err := strconv.Atoi(r.FormValue("min_username_len"))
|
||||
if err == nil {
|
||||
app.cfg.App.MinUsernameLen = mul
|
||||
apper.App().cfg.App.MinUsernameLen = mul
|
||||
}
|
||||
mb, err := strconv.Atoi(r.FormValue("max_blogs"))
|
||||
if err == nil {
|
||||
app.cfg.App.MaxBlogs = mb
|
||||
apper.App().cfg.App.MaxBlogs = mb
|
||||
}
|
||||
app.cfg.App.Federation = r.FormValue("federation") == "on"
|
||||
app.cfg.App.PublicStats = r.FormValue("public_stats") == "on"
|
||||
app.cfg.App.Private = r.FormValue("private") == "on"
|
||||
app.cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
|
||||
if app.cfg.App.LocalTimeline && app.timeline == nil {
|
||||
apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
|
||||
apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
|
||||
apper.App().cfg.App.Private = r.FormValue("private") == "on"
|
||||
apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
|
||||
if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
|
||||
log.Info("Initializing local timeline...")
|
||||
initLocalTimeline(app)
|
||||
initLocalTimeline(apper.App())
|
||||
}
|
||||
app.cfg.App.UserInvites = r.FormValue("user_invites")
|
||||
if app.cfg.App.UserInvites == "none" {
|
||||
app.cfg.App.UserInvites = ""
|
||||
apper.App().cfg.App.UserInvites = r.FormValue("user_invites")
|
||||
if apper.App().cfg.App.UserInvites == "none" {
|
||||
apper.App().cfg.App.UserInvites = ""
|
||||
}
|
||||
|
||||
m := "?cm=Configuration+saved."
|
||||
err = config.Save(app.cfg, app.cfgFile)
|
||||
err = apper.SaveConfig(apper.App().cfg)
|
||||
if err != nil {
|
||||
m = "?cm=" + err.Error()
|
||||
}
|
||||
|
@ -418,7 +419,7 @@ func updateAppStats() {
|
|||
sysStatus.NumGC = m.NumGC
|
||||
}
|
||||
|
||||
func adminResetPassword(app *app, u *User, newPass string) error {
|
||||
func adminResetPassword(app *App, u *User, newPass string) error {
|
||||
hashedPass, err := auth.HashPass([]byte(newPass))
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
|
||||
|
|
666
app.go
666
app.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -12,9 +12,9 @@ package writefreely
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -25,18 +25,18 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/writeas/go-strip-markdown"
|
||||
"github.com/writeas/impart"
|
||||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/converter"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/key"
|
||||
"github.com/writeas/writefreely/migrations"
|
||||
"github.com/writeas/writefreely/page"
|
||||
)
|
||||
|
@ -57,27 +57,129 @@ var (
|
|||
softwareVer = "0.9.0"
|
||||
|
||||
// DEPRECATED VARS
|
||||
// TODO: pass app.cfg into GetCollection* calls so we can get these values
|
||||
// from Collection methods and we no longer need these.
|
||||
hostName string
|
||||
isSingleUser bool
|
||||
)
|
||||
|
||||
type app struct {
|
||||
// App holds data and configuration for an individual WriteFreely instance.
|
||||
type App struct {
|
||||
router *mux.Router
|
||||
shttp *http.ServeMux
|
||||
db *datastore
|
||||
cfg *config.Config
|
||||
cfgFile string
|
||||
keys *keychain
|
||||
keys *key.Keychain
|
||||
sessionStore *sessions.CookieStore
|
||||
formDecoder *schema.Decoder
|
||||
|
||||
timeline *localTimeline
|
||||
}
|
||||
|
||||
// DB returns the App's datastore
|
||||
func (app *App) DB() *datastore {
|
||||
return app.db
|
||||
}
|
||||
|
||||
// Router returns the App's router
|
||||
func (app *App) Router() *mux.Router {
|
||||
return app.router
|
||||
}
|
||||
|
||||
// Config returns the App's current configuration.
|
||||
func (app *App) Config() *config.Config {
|
||||
return app.cfg
|
||||
}
|
||||
|
||||
// SetConfig updates the App's Config to the given value.
|
||||
func (app *App) SetConfig(cfg *config.Config) {
|
||||
app.cfg = cfg
|
||||
}
|
||||
|
||||
// SetKeys updates the App's Keychain to the given value.
|
||||
func (app *App) SetKeys(k *key.Keychain) {
|
||||
app.keys = k
|
||||
}
|
||||
|
||||
// Apper is the interface for getting data into and out of a WriteFreely
|
||||
// instance (or "App").
|
||||
//
|
||||
// App returns the App for the current instance.
|
||||
//
|
||||
// LoadConfig reads an app configuration into the App, returning any error
|
||||
// encountered.
|
||||
//
|
||||
// SaveConfig persists the current App configuration.
|
||||
//
|
||||
// LoadKeys reads the App's encryption keys and loads them into its
|
||||
// key.Keychain.
|
||||
type Apper interface {
|
||||
App() *App
|
||||
|
||||
LoadConfig() error
|
||||
SaveConfig(*config.Config) error
|
||||
|
||||
LoadKeys() error
|
||||
}
|
||||
|
||||
// App returns the App
|
||||
func (app *App) App() *App {
|
||||
return app
|
||||
}
|
||||
|
||||
// LoadConfig loads and parses a config file.
|
||||
func (app *App) LoadConfig() error {
|
||||
log.Info("Loading %s configuration...", app.cfgFile)
|
||||
cfg, err := config.Load(app.cfgFile)
|
||||
if err != nil {
|
||||
log.Error("Unable to load configuration: %v", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
app.cfg = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveConfig saves the given Config to disk -- namely, to the App's cfgFile.
|
||||
func (app *App) SaveConfig(c *config.Config) error {
|
||||
return config.Save(c, app.cfgFile)
|
||||
}
|
||||
|
||||
// LoadKeys reads all needed keys from disk into the App. In order to use the
|
||||
// configured `Server.KeysParentDir`, you must call initKeyPaths(App) before
|
||||
// this.
|
||||
func (app *App) LoadKeys() error {
|
||||
var err error
|
||||
app.keys = &key.Keychain{}
|
||||
|
||||
if debugging {
|
||||
log.Info(" %s", emailKeyPath)
|
||||
}
|
||||
app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if debugging {
|
||||
log.Info(" %s", cookieAuthKeyPath)
|
||||
}
|
||||
app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if debugging {
|
||||
log.Info(" %s", cookieKeyPath)
|
||||
}
|
||||
app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleViewHome shows page at root path. Will be the Pad if logged in and the
|
||||
// catch-all landing page otherwise.
|
||||
func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
if app.cfg.App.SingleUser {
|
||||
// Render blog index
|
||||
return handleViewCollection(app, w, r)
|
||||
|
@ -90,6 +192,10 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return handleViewPad(app, w, r)
|
||||
}
|
||||
|
||||
if land := app.cfg.App.LandingPath(); land != "/" {
|
||||
return impart.HTTPError{http.StatusFound, land}
|
||||
}
|
||||
|
||||
p := struct {
|
||||
page.StaticPage
|
||||
Flashes []template.HTML
|
||||
|
@ -112,7 +218,7 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return renderPage(w, "landing.tmpl", p)
|
||||
}
|
||||
|
||||
func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error {
|
||||
func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *template.Template) error {
|
||||
p := struct {
|
||||
page.StaticPage
|
||||
ContentTitle string
|
||||
|
@ -158,7 +264,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te
|
|||
return nil
|
||||
}
|
||||
|
||||
func pageForReq(app *app, r *http.Request) page.StaticPage {
|
||||
func pageForReq(app *App, r *http.Request) page.StaticPage {
|
||||
p := page.StaticPage{
|
||||
AppCfg: app.cfg.App,
|
||||
Path: r.URL.Path,
|
||||
|
@ -187,277 +293,50 @@ func pageForReq(app *app, r *http.Request) page.StaticPage {
|
|||
return p
|
||||
}
|
||||
|
||||
var shttp = http.NewServeMux()
|
||||
var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$")
|
||||
|
||||
func Serve() {
|
||||
// General options usable with other commands
|
||||
debugPtr := flag.Bool("debug", false, "Enables debug logging.")
|
||||
configFile := flag.String("c", "config.ini", "The configuration file to use")
|
||||
// Initialize loads the app configuration and initializes templates, keys,
|
||||
// session, route handlers, and the database connection.
|
||||
func Initialize(apper Apper, debug bool) (*App, error) {
|
||||
debugging = debug
|
||||
|
||||
// Setup actions
|
||||
createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits")
|
||||
doConfig := flag.Bool("config", false, "Run the configuration process")
|
||||
configSections := flag.String("sections", "server db app", "Which sections of the configuration to go through (requires --config), " +
|
||||
"valid values are any combination of 'server', 'db' and 'app' " +
|
||||
"example: writefreely --config --sections \"db app\"")
|
||||
genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys")
|
||||
createSchema := flag.Bool("init-db", false, "Initialize app database")
|
||||
migrate := flag.Bool("migrate", false, "Migrate the database")
|
||||
apper.LoadConfig()
|
||||
|
||||
// Admin actions
|
||||
createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password")
|
||||
createUser := flag.String("create-user", "", "Create a regular user with the given username:password")
|
||||
resetPassUser := flag.String("reset-pass", "", "Reset the given user's password")
|
||||
outputVersion := flag.Bool("v", false, "Output the current version")
|
||||
flag.Parse()
|
||||
|
||||
debugging = *debugPtr
|
||||
|
||||
app := &app{
|
||||
cfgFile: *configFile,
|
||||
}
|
||||
|
||||
if *outputVersion {
|
||||
fmt.Println(serverSoftware + " " + softwareVer)
|
||||
os.Exit(0)
|
||||
} else if *createConfig {
|
||||
log.Info("Creating configuration...")
|
||||
c := config.New()
|
||||
log.Info("Saving configuration %s...", app.cfgFile)
|
||||
err := config.Save(c, app.cfgFile)
|
||||
if err != nil {
|
||||
log.Error("Unable to save configuration: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *doConfig {
|
||||
if *configSections == "" {
|
||||
*configSections = "server db app"
|
||||
}
|
||||
// let's check there aren't any garbage in the list
|
||||
configSectionsArray := strings.Split(*configSections, " ")
|
||||
for _, element := range configSectionsArray {
|
||||
if element != "server" && element != "db" && element != "app" {
|
||||
log.Error("Invalid argument to --sections. Valid arguments are only \"server\", \"db\" and \"app\"")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
d, err := config.Configure(app.cfgFile, *configSections)
|
||||
if err != nil {
|
||||
log.Error("Unable to configure: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if d.User != nil {
|
||||
app.cfg = d.Config
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
if !app.db.DatabaseInitialized() {
|
||||
err = adminInitDatabase(app)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
u := &User{
|
||||
Username: d.User.Username,
|
||||
HashedPass: d.User.HashedPass,
|
||||
Created: time.Now().Truncate(time.Second).UTC(),
|
||||
}
|
||||
|
||||
// Create blog
|
||||
log.Info("Creating user %s...\n", u.Username)
|
||||
err = app.db.CreateUser(u, app.cfg.App.SiteName)
|
||||
if err != nil {
|
||||
log.Error("Unable to create user: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info("Done!")
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *genKeys {
|
||||
errStatus := 0
|
||||
|
||||
// Read keys path from config
|
||||
loadConfig(app)
|
||||
|
||||
// Create keys dir if it doesn't exist yet
|
||||
fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir)
|
||||
if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) {
|
||||
err = os.Mkdir(fullKeysDir, 0700)
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate keys
|
||||
initKeyPaths(app)
|
||||
err := generateKey(emailKeyPath)
|
||||
if err != nil {
|
||||
errStatus = 1
|
||||
}
|
||||
err = generateKey(cookieAuthKeyPath)
|
||||
if err != nil {
|
||||
errStatus = 1
|
||||
}
|
||||
err = generateKey(cookieKeyPath)
|
||||
if err != nil {
|
||||
errStatus = 1
|
||||
}
|
||||
|
||||
os.Exit(errStatus)
|
||||
} else if *createSchema {
|
||||
loadConfig(app)
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
err := adminInitDatabase(app)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *createAdmin != "" {
|
||||
err := adminCreateUser(app, *createAdmin, true)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *createUser != "" {
|
||||
err := adminCreateUser(app, *createUser, false)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *resetPassUser != "" {
|
||||
// Connect to the database
|
||||
loadConfig(app)
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
// Fetch user
|
||||
u, err := app.db.GetUserForAuth(*resetPassUser)
|
||||
if err != nil {
|
||||
log.Error("Get user: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Prompt for new password
|
||||
prompt := promptui.Prompt{
|
||||
Templates: &promptui.PromptTemplates{
|
||||
Success: "{{ . | bold | faint }}: ",
|
||||
},
|
||||
Label: "New password",
|
||||
Mask: '*',
|
||||
}
|
||||
newPass, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Do the update
|
||||
log.Info("Updating...")
|
||||
err = adminResetPassword(app, u, newPass)
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info("Success.")
|
||||
os.Exit(0)
|
||||
} else if *migrate {
|
||||
loadConfig(app)
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
|
||||
if err != nil {
|
||||
log.Error("migrate: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
log.Info("Initializing...")
|
||||
|
||||
loadConfig(app)
|
||||
|
||||
hostName = app.cfg.App.Host
|
||||
isSingleUser = app.cfg.App.SingleUser
|
||||
app.cfg.Server.Dev = *debugPtr
|
||||
|
||||
err := initTemplates(app.cfg)
|
||||
// Load templates
|
||||
err := InitTemplates(apper.App().Config())
|
||||
if err != nil {
|
||||
log.Error("load templates: %s", err)
|
||||
os.Exit(1)
|
||||
return nil, fmt.Errorf("load templates: %s", err)
|
||||
}
|
||||
|
||||
// Load keys
|
||||
log.Info("Loading encryption keys...")
|
||||
initKeyPaths(app)
|
||||
err = initKeys(app)
|
||||
// Load keys and set up session
|
||||
initKeyPaths(apper.App()) // TODO: find a better way to do this, since it's unneeded in all Apper implementations
|
||||
err = InitKeys(apper)
|
||||
if err != nil {
|
||||
log.Error("\n%s\n", err)
|
||||
return nil, fmt.Errorf("init keys: %s", err)
|
||||
}
|
||||
apper.App().InitSession()
|
||||
|
||||
// Initialize modules
|
||||
app.sessionStore = initSession(app)
|
||||
app.formDecoder = schema.NewDecoder()
|
||||
app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString)
|
||||
app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool)
|
||||
app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString)
|
||||
app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool)
|
||||
app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64)
|
||||
app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64)
|
||||
apper.App().InitDecoder()
|
||||
|
||||
// Check database configuration
|
||||
if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") {
|
||||
log.Error("Database user or password not set.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if app.cfg.Database.Host == "" {
|
||||
app.cfg.Database.Host = "localhost"
|
||||
}
|
||||
if app.cfg.Database.Database == "" {
|
||||
app.cfg.Database.Database = "writefreely"
|
||||
}
|
||||
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
// Test database connection
|
||||
err = app.db.Ping()
|
||||
err = ConnectToDatabase(apper.App())
|
||||
if err != nil {
|
||||
log.Error("Database ping failed: %s", err)
|
||||
return nil, fmt.Errorf("connect to DB: %s", err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
handler := NewHandler(app)
|
||||
handler.SetErrorPages(&ErrorPages{
|
||||
NotFound: pages["404-general.tmpl"],
|
||||
Gone: pages["410.tmpl"],
|
||||
InternalServerError: pages["500.tmpl"],
|
||||
Blank: pages["blank.tmpl"],
|
||||
})
|
||||
|
||||
// Handle app routes
|
||||
initRoutes(handler, r, app.cfg, app.db)
|
||||
|
||||
// Handle local timeline, if enabled
|
||||
if app.cfg.App.LocalTimeline {
|
||||
if apper.App().cfg.App.LocalTimeline {
|
||||
log.Info("Initializing local timeline...")
|
||||
initLocalTimeline(app)
|
||||
initLocalTimeline(apper.App())
|
||||
}
|
||||
|
||||
// Handle static files
|
||||
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
|
||||
shttp.Handle("/", fs)
|
||||
r.PathPrefix("/").Handler(fs)
|
||||
return apper.App(), nil
|
||||
}
|
||||
|
||||
func Serve(app *App, r *mux.Router) {
|
||||
log.Info("Going to serve...")
|
||||
|
||||
isSingleUser = app.cfg.App.SingleUser
|
||||
app.cfg.Server.Dev = debugging
|
||||
|
||||
// Handle shutdown
|
||||
c := make(chan os.Signal, 2)
|
||||
|
@ -470,13 +349,12 @@ func Serve() {
|
|||
os.Exit(0)
|
||||
}()
|
||||
|
||||
http.Handle("/", r)
|
||||
|
||||
// Start web application server
|
||||
var bindAddress = app.cfg.Server.Bind
|
||||
if bindAddress == "" {
|
||||
bindAddress = "localhost"
|
||||
}
|
||||
var err error
|
||||
if app.cfg.IsSecureStandalone() {
|
||||
log.Info("Serving redirects on http://%s:80", bindAddress)
|
||||
go func() {
|
||||
|
@ -490,11 +368,11 @@ func Serve() {
|
|||
log.Info("Serving on https://%s:443", bindAddress)
|
||||
log.Info("---")
|
||||
err = http.ListenAndServeTLS(
|
||||
fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, nil)
|
||||
fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, r)
|
||||
} else {
|
||||
log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port)
|
||||
log.Info("---")
|
||||
err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), nil)
|
||||
err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), r)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Unable to start: %v", err)
|
||||
|
@ -502,17 +380,216 @@ func Serve() {
|
|||
}
|
||||
}
|
||||
|
||||
func loadConfig(app *app) {
|
||||
log.Info("Loading %s configuration...", app.cfgFile)
|
||||
cfg, err := config.Load(app.cfgFile)
|
||||
if err != nil {
|
||||
log.Error("Unable to load configuration: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
app.cfg = cfg
|
||||
func (app *App) InitDecoder() {
|
||||
// TODO: do this at the package level, instead of the App level
|
||||
// Initialize modules
|
||||
app.formDecoder = schema.NewDecoder()
|
||||
app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString)
|
||||
app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool)
|
||||
app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString)
|
||||
app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool)
|
||||
app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64)
|
||||
app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64)
|
||||
}
|
||||
|
||||
func connectToDatabase(app *app) {
|
||||
// ConnectToDatabase validates and connects to the configured database, then
|
||||
// tests the connection.
|
||||
func ConnectToDatabase(app *App) error {
|
||||
// Check database configuration
|
||||
if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") {
|
||||
return fmt.Errorf("Database user or password not set.")
|
||||
}
|
||||
if app.cfg.Database.Host == "" {
|
||||
app.cfg.Database.Host = "localhost"
|
||||
}
|
||||
if app.cfg.Database.Database == "" {
|
||||
app.cfg.Database.Database = "writefreely"
|
||||
}
|
||||
|
||||
// TODO: check err
|
||||
connectToDatabase(app)
|
||||
|
||||
// Test database connection
|
||||
err := app.db.Ping()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Database ping failed: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OutputVersion prints out the version of the application.
|
||||
func OutputVersion() {
|
||||
fmt.Println(serverSoftware + " " + softwareVer)
|
||||
}
|
||||
|
||||
// NewApp creates a new app instance.
|
||||
func NewApp(cfgFile string) *App {
|
||||
return &App{
|
||||
cfgFile: cfgFile,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateConfig creates a default configuration and saves it to the app's cfgFile.
|
||||
func CreateConfig(app *App) error {
|
||||
log.Info("Creating configuration...")
|
||||
c := config.New()
|
||||
log.Info("Saving configuration %s...", app.cfgFile)
|
||||
err := config.Save(c, app.cfgFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to save configuration: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoConfig runs the interactive configuration process.
|
||||
func DoConfig(app *App, configSections string) {
|
||||
if configSections == "" {
|
||||
configSections = "server db app"
|
||||
}
|
||||
// let's check there aren't any garbage in the list
|
||||
configSectionsArray := strings.Split(configSections, " ")
|
||||
for _, element := range configSectionsArray {
|
||||
if element != "server" && element != "db" && element != "app" {
|
||||
log.Error("Invalid argument to --sections. Valid arguments are only \"server\", \"db\" and \"app\"")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
d, err := config.Configure(app.cfgFile, configSections)
|
||||
if err != nil {
|
||||
log.Error("Unable to configure: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if d.User != nil {
|
||||
app.cfg = d.Config
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
if !app.db.DatabaseInitialized() {
|
||||
err = adminInitDatabase(app)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
u := &User{
|
||||
Username: d.User.Username,
|
||||
HashedPass: d.User.HashedPass,
|
||||
Created: time.Now().Truncate(time.Second).UTC(),
|
||||
}
|
||||
|
||||
// Create blog
|
||||
log.Info("Creating user %s...\n", u.Username)
|
||||
err = app.db.CreateUser(u, app.cfg.App.SiteName)
|
||||
if err != nil {
|
||||
log.Error("Unable to create user: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info("Done!")
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// GenerateKeyFiles creates app encryption keys and saves them into the configured KeysParentDir.
|
||||
func GenerateKeyFiles(app *App) error {
|
||||
// Read keys path from config
|
||||
app.LoadConfig()
|
||||
|
||||
// Create keys dir if it doesn't exist yet
|
||||
fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir)
|
||||
if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) {
|
||||
err = os.Mkdir(fullKeysDir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate keys
|
||||
initKeyPaths(app)
|
||||
// TODO: use something like https://github.com/hashicorp/go-multierror to return errors
|
||||
var keyErrs error
|
||||
err := generateKey(emailKeyPath)
|
||||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
err = generateKey(cookieAuthKeyPath)
|
||||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
err = generateKey(cookieKeyPath)
|
||||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
|
||||
return keyErrs
|
||||
}
|
||||
|
||||
// CreateSchema creates all database tables needed for the application.
|
||||
func CreateSchema(apper Apper) error {
|
||||
apper.LoadConfig()
|
||||
connectToDatabase(apper.App())
|
||||
defer shutdown(apper.App())
|
||||
err := adminInitDatabase(apper.App())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate runs all necessary database migrations.
|
||||
func Migrate(app *App) error {
|
||||
app.LoadConfig()
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword runs the interactive password reset process.
|
||||
func ResetPassword(app *App, username string) error {
|
||||
// Connect to the database
|
||||
app.LoadConfig()
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
// Fetch user
|
||||
u, err := app.db.GetUserForAuth(username)
|
||||
if err != nil {
|
||||
log.Error("Get user: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Prompt for new password
|
||||
prompt := promptui.Prompt{
|
||||
Templates: &promptui.PromptTemplates{
|
||||
Success: "{{ . | bold | faint }}: ",
|
||||
},
|
||||
Label: "New password",
|
||||
Mask: '*',
|
||||
}
|
||||
newPass, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Do the update
|
||||
log.Info("Updating...")
|
||||
err = adminResetPassword(app, u, newPass)
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info("Success.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func connectToDatabase(app *App) {
|
||||
log.Info("Connecting to %s database...", app.cfg.Database.Type)
|
||||
|
||||
var db *sql.DB
|
||||
|
@ -542,28 +619,20 @@ func connectToDatabase(app *app) {
|
|||
app.db = &datastore{db, app.cfg.Database.Type}
|
||||
}
|
||||
|
||||
func shutdown(app *app) {
|
||||
func shutdown(app *App) {
|
||||
log.Info("Closing database connection...")
|
||||
app.db.Close()
|
||||
}
|
||||
|
||||
func adminCreateUser(app *app, credStr string, isAdmin bool) error {
|
||||
// CreateUser creates a new admin or normal user from the given credentials.
|
||||
func CreateUser(apper Apper, username, password string, isAdmin bool) error {
|
||||
// Create an admin user with --create-admin
|
||||
creds := strings.Split(credStr, ":")
|
||||
if len(creds) != 2 {
|
||||
c := "user"
|
||||
if isAdmin {
|
||||
c = "admin"
|
||||
}
|
||||
return fmt.Errorf("usage: writefreely --create-%s username:password", c)
|
||||
}
|
||||
|
||||
loadConfig(app)
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
apper.LoadConfig()
|
||||
connectToDatabase(apper.App())
|
||||
defer shutdown(apper.App())
|
||||
|
||||
// Ensure an admin / first user doesn't already exist
|
||||
firstUser, _ := app.db.GetUserByID(1)
|
||||
firstUser, _ := apper.App().db.GetUserByID(1)
|
||||
if isAdmin {
|
||||
// Abort if trying to create admin user, but one already exists
|
||||
if firstUser != nil {
|
||||
|
@ -577,9 +646,6 @@ func adminCreateUser(app *app, credStr string, isAdmin bool) error {
|
|||
}
|
||||
|
||||
// Create the user
|
||||
username := creds[0]
|
||||
password := creds[1]
|
||||
|
||||
// Normalize and validate username
|
||||
desiredUsername := username
|
||||
username = getSlug(username, "")
|
||||
|
@ -589,8 +655,8 @@ func adminCreateUser(app *app, credStr string, isAdmin bool) error {
|
|||
usernameDesc += " (originally: " + desiredUsername + ")"
|
||||
}
|
||||
|
||||
if !author.IsValidUsername(app.cfg, username) {
|
||||
return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, app.cfg.App.MinUsernameLen)
|
||||
if !author.IsValidUsername(apper.App().cfg, username) {
|
||||
return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, apper.App().cfg.App.MinUsernameLen)
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
|
@ -610,7 +676,7 @@ func adminCreateUser(app *app, credStr string, isAdmin bool) error {
|
|||
userType = "admin"
|
||||
}
|
||||
log.Info("Creating %s %s...", userType, usernameDesc)
|
||||
err = app.db.CreateUser(u, desiredUsername)
|
||||
err = apper.App().db.CreateUser(u, desiredUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create user: %s", err)
|
||||
}
|
||||
|
@ -618,7 +684,7 @@ func adminCreateUser(app *app, credStr string, isAdmin bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func adminInitDatabase(app *app) error {
|
||||
func adminInitDatabase(app *App) error {
|
||||
schemaFileName := "schema.sql"
|
||||
if app.cfg.Database.Type == driverSQLite {
|
||||
schemaFileName = "sqlite.sql"
|
||||
|
|
105
bindata-lib.go
Normal file
105
bindata-lib.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
// +build wflib
|
||||
|
||||
package writefreely
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func bindata_read(data []byte, name string) ([]byte, error) {
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
var _schema_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x59\x5f\x6f\xa3\x38\x10\x7f\xef\xa7\xf0\xdb\xa6\x52\x23\x6d\x7a\xdd\xaa\xba\xd3\x3e\x64\x53\x76\x2f\xba\x94\xee\x25\x44\xba\x7d\x02\x03\x93\xd4\xaa\xb1\x91\x6d\x92\xe6\xdb\x9f\x8c\x49\x08\x86\x24\xd0\xdb\x3b\x71\x7d\x2a\xcc\x6f\x8c\xfd\x9b\x3f\x9e\x99\x0c\x87\x57\xc3\x21\x7a\xc4\x0a\x87\x58\xc2\xaf\x28\xd8\x0a\xa2\x60\x25\x00\xe8\x2e\xb8\x1a\x0e\xaf\xb4\x78\xf8\xce\x3f\xad\xac\xf5\x3d\x1c\x52\x40\x52\x89\x2c\x52\x99\x00\xb4\xe2\x02\xa9\xfc\x5d\x80\xa3\x08\xa4\x54\xfc\x15\x98\x34\xdf\x9b\xcc\x9d\xb1\xe7\x20\x6f\xfc\x65\xe6\xa0\xe9\x57\xe4\x3e\x7b\xc8\xf9\x6b\xba\xf0\x16\x16\x1a\x0d\xae\x10\x0a\xf2\x87\x00\x85\x84\x61\xb1\x1b\x8c\xee\xaf\x73\x05\x77\x39\x9b\xdd\x68\x71\x26\x41\xf8\x24\x0e\x10\x61\x6a\x60\x0b\x65\x16\xf3\x00\x29\xc2\x76\x5a\x3a\x2a\xa5\xe8\xd1\xf9\x3a\x5e\xce\x3c\xf4\xe1\xe3\x87\x1c\xc9\x19\xf8\x8a\x24\xd0\x0e\x1d\x09\xc0\x0a\xe2\x00\xc5\x58\x81\x56\xab\x43\x27\xcb\xf9\xdc\x71\x3d\xdf\x9b\x3e\x39\x0b\x6f\xfc\xf4\x3d\x57\x84\xb7\x94\x08\x90\x47\x8a\x7b\x7c\xf5\x40\x78\x0d\x4c\x05\x68\x83\x45\xf4\x82\xc5\xe0\xf6\xd3\xa7\xeb\x1a\xf2\xfb\x7c\xfa\x34\x9e\xff\x40\x7f\x38\x3f\xd0\xa0\xa0\xe9\xfa\xea\x1a\x39\xee\xb7\xa9\xeb\x7c\x9e\x32\xc6\x1f\xbf\x94\xfb\xf9\x7d\x3c\x5f\x38\xde\x67\x8a\x15\x61\xa3\xdf\xfe\x75\xb3\xa7\x69\xc4\x99\xd2\xa7\xb8\x6c\xf4\x12\x6b\x4c\xae\xcd\xb9\x3f\xfa\x2f\xb6\x4d\x0f\xd0\x04\x62\x92\x25\x0a\xde\x54\x7e\xb8\xf1\xc4\x73\xe6\x68\xe1\x78\x28\x53\xab\x07\x34\x79\x9e\xcd\xf4\x17\xf5\x83\x1f\x12\x66\x79\x4d\x1a\xbf\xcb\x80\x55\xce\x49\xdc\x2b\xc2\x13\xb2\x16\x58\x11\xde\x18\x68\x16\xc0\x10\xbd\x01\x21\x09\x67\x26\x78\x46\x23\x8b\x69\x03\x6f\x64\x29\x97\x0b\x90\x19\x55\x01\xca\x4d\xb0\x97\xf4\x85\x8f\x88\x53\x0a\x91\x3e\x2c\x56\x4a\x90\x30\x53\xd0\x22\xff\x34\x6a\x19\xae\x4a\xd1\xc9\x74\x73\xd0\x29\xdd\x77\x74\xfb\x60\x81\x36\x98\x66\x60\x85\x76\xdd\x7f\x93\xf0\xae\xe2\xc2\x49\x78\x57\xf3\xe2\xaa\x33\x56\xf7\x77\x73\xb4\x99\xde\xf8\x68\xb9\xc5\x57\xd8\x75\xb2\x46\x8e\x6f\x6d\x87\x34\x0b\x29\x89\xfc\x57\xd8\x05\x28\xa4\x3c\xb4\xa4\x82\x6c\xb0\x82\x13\xe2\x73\xa4\xf6\x90\xc8\x14\x4b\xb9\xe5\x22\xee\xc4\x66\xa9\xd4\x9e\xd2\x42\x25\x40\xb9\xd7\xde\x7f\xbc\xfe\x3f\xb3\x26\x20\x26\x02\x22\xd5\x89\xb5\x52\xc9\xb0\x96\x0a\xd8\xf8\x98\x12\x2c\x8f\xc2\xfd\xa3\x45\x4c\xc0\x60\x7b\x11\x54\x65\xef\x68\xdd\x1e\x52\xd7\x89\x32\x79\x74\xa1\x5b\x5e\x85\xc6\x4b\xef\xd9\x9f\xba\x93\xb9\xf3\xe4\xb8\x9e\xc9\x9f\x0d\x3c\xb5\x4f\x8d\xb5\x4a\x4a\x11\x45\x7f\x4e\xa6\x0d\x62\x90\x91\x20\xa9\xca\x2f\xcb\xc3\xfe\xee\x3b\xed\xaf\x5a\x99\xaa\x1d\x05\x5f\xbe\x00\x14\x17\xa8\x79\x9b\x7f\xa4\xb8\x51\x5b\xaf\x9c\xab\xae\xb8\x48\xf0\x51\xc9\xf8\x50\x2f\x18\x4d\xe6\x8b\x76\x8d\x35\xae\xa9\x82\xb7\xec\x4c\x35\xbd\x21\xb0\xf5\x23\x9e\xe9\xe2\xab\x41\x5e\xaf\x8d\xf4\xdb\xa5\x3b\xfd\x73\xe9\xe4\x2f\xf7\xf6\x1d\x04\x3d\xf3\xee\x94\xcb\x36\xa9\xc0\xc0\x4a\x8f\x2e\x9c\xc0\xee\x39\x68\xb6\xb6\x7c\xb8\x66\x88\x84\xc7\x64\xb5\xf3\x8b\xd6\xc6\xd4\xb9\xb7\x0d\x38\xed\x07\x3e\x4e\x53\xc0\x02\xb3\x08\x0a\xe8\x5d\x53\x67\xc2\xb8\x48\x4c\x73\x42\x31\x5b\x67\x78\xbd\x47\x37\xad\x2b\x14\xad\x38\xc1\x4f\xf0\x94\xda\x12\xcd\x97\x4a\xfd\x4b\x84\x31\x88\xfd\x94\x4b\x62\xa2\xeb\xe8\x8b\x4b\x77\x31\xfd\xe6\x3a\x8f\x0d\x8b\xef\x1b\x30\x5d\x95\x4a\x85\x93\xb4\x6d\x07\x76\xa8\xfc\x3b\x6b\x5e\x70\x7f\x3b\xdd\xfc\x93\xec\x70\xe8\x71\xba\x25\x82\x8e\xe1\x48\x62\xdf\x38\x6b\xbd\x78\xcc\xdf\xd7\x14\x4a\xa3\x0f\xca\xff\x6f\x0e\x6b\xe7\x98\xc2\x73\x0a\xd4\xde\x8f\x6e\x7a\xd5\x2b\x09\x48\xb8\x82\x15\xa7\x94\x6f\x5b\xc4\x7d\x15\x7e\xb2\x64\xaa\xf5\x4f\x46\xcf\xaf\x4c\x28\x6a\xa0\xd3\xa3\x84\xcb\x25\xbe\xf5\x81\x9e\xf1\xab\xb7\xd5\xae\xce\xb7\xf0\xf5\x21\x40\x7e\x75\x77\xe7\xf6\x6c\x1f\x70\x39\x3e\x8c\xc5\x0f\x1e\xdf\x7f\xb6\x3b\x51\x6d\xd7\x66\xc7\xec\x35\x16\x67\x91\xe2\x86\x8a\xd3\x56\x21\x2c\xe4\x6f\xe7\x00\xf2\x05\x0b\x88\xfd\x4b\xb8\xcb\xb6\xb1\xe2\x6f\x50\x6e\xaf\x37\x76\xd1\x24\x77\x99\x3d\x58\x78\x63\x9d\xb3\xe3\xcd\x86\x79\xc3\xfd\xdd\x7f\x34\x6e\xd8\x6f\xac\x97\x83\x06\xbd\x39\xc2\x36\xa4\x99\xf7\x8a\xd8\x2a\xe7\x6c\x8a\xab\x75\x4e\x7d\x44\x86\xdf\x74\x42\x90\x01\x92\x09\xa6\xf4\x64\x2d\x74\x36\xc9\xb7\x99\x0a\x13\x86\x23\x45\x36\xcd\xf3\xe9\x3e\xd1\xde\xd2\xd1\x3b\x76\x86\x5a\x85\xe1\x04\xde\xdd\x1c\x5e\x1a\x66\x54\x57\x32\x7c\x1d\x16\x32\x8f\xf5\x75\x20\xc1\x84\xe6\x5b\x2a\x7e\x9d\x68\x9c\xd3\xbf\xfb\xd7\x82\xcb\x59\xb0\xa4\x65\x50\xfe\xdf\xab\x28\x94\x26\xce\xe2\x53\x61\x78\x90\x17\xee\x90\x3f\xf9\x27\xc3\xf1\xe4\x7d\xdf\xfa\xcc\x7f\x07\x00\x00\xff\xff\xbe\x79\x68\xa8\x10\x1b\x00\x00")
|
||||
|
||||
func schema_sql() ([]byte, error) {
|
||||
return bindata_read(
|
||||
_schema_sql,
|
||||
"schema.sql",
|
||||
)
|
||||
}
|
||||
|
||||
// Asset loads and returns the asset for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func Asset(name string) ([]byte, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
return f()
|
||||
}
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
|
||||
// AssetNames returns the names of the assets.
|
||||
func AssetNames() []string {
|
||||
names := make([]string, 0, len(_bindata))
|
||||
for name := range _bindata {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() ([]byte, error){
|
||||
"schema.sql": schema_sql,
|
||||
}
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||
// AssetDir("") will return []string{"data"}.
|
||||
func AssetDir(name string) ([]string, error) {
|
||||
node := _bintree
|
||||
if len(name) != 0 {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
pathList := strings.Split(cannonicalName, "/")
|
||||
for _, p := range pathList {
|
||||
node = node.Children[p]
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.Func != nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
rv := make([]string, 0, len(node.Children))
|
||||
for name := range node.Children {
|
||||
rv = append(rv, name)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
type _bintree_t struct {
|
||||
Func func() ([]byte, error)
|
||||
Children map[string]*_bintree_t
|
||||
}
|
||||
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"schema.sql": &_bintree_t{schema_sql, map[string]*_bintree_t{
|
||||
}},
|
||||
}}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,9 +11,135 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
writefreely.Serve()
|
||||
// General options usable with other commands
|
||||
debugPtr := flag.Bool("debug", false, "Enables debug logging.")
|
||||
configFile := flag.String("c", "config.ini", "The configuration file to use")
|
||||
|
||||
// Setup actions
|
||||
createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits")
|
||||
doConfig := flag.Bool("config", false, "Run the configuration process")
|
||||
configSections := flag.String("sections", "server db app", "Which sections of the configuration to go through (requires --config), " +
|
||||
"valid values are any combination of 'server', 'db' and 'app' " +
|
||||
"example: writefreely --config --sections \"db app\"")
|
||||
genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys")
|
||||
createSchema := flag.Bool("init-db", false, "Initialize app database")
|
||||
migrate := flag.Bool("migrate", false, "Migrate the database")
|
||||
|
||||
// Admin actions
|
||||
createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password")
|
||||
createUser := flag.String("create-user", "", "Create a regular user with the given username:password")
|
||||
resetPassUser := flag.String("reset-pass", "", "Reset the given user's password")
|
||||
outputVersion := flag.Bool("v", false, "Output the current version")
|
||||
flag.Parse()
|
||||
|
||||
app := writefreely.NewApp(*configFile)
|
||||
|
||||
if *outputVersion {
|
||||
writefreely.OutputVersion()
|
||||
os.Exit(0)
|
||||
} else if *createConfig {
|
||||
err := writefreely.CreateConfig(app)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *doConfig {
|
||||
writefreely.DoConfig(app, *configSections)
|
||||
os.Exit(0)
|
||||
} else if *genKeys {
|
||||
err := writefreely.GenerateKeyFiles(app)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *createSchema {
|
||||
err := writefreely.CreateSchema(app)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *createAdmin != "" {
|
||||
username, password, err := userPass(*createAdmin, true)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
err = writefreely.CreateUser(app, username, password, true)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *createUser != "" {
|
||||
username, password, err := userPass(*createUser, false)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
err = writefreely.CreateUser(app, username, password, false)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *resetPassUser != "" {
|
||||
err := writefreely.ResetPassword(app, *resetPassUser)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
} else if *migrate {
|
||||
err := writefreely.Migrate(app)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Initialize the application
|
||||
var err error
|
||||
app, err = writefreely.Initialize(app, *debugPtr)
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Set app routes
|
||||
r := mux.NewRouter()
|
||||
writefreely.InitRoutes(app, r)
|
||||
app.InitStaticRoutes(r)
|
||||
|
||||
// Serve the application
|
||||
writefreely.Serve(app, r)
|
||||
}
|
||||
|
||||
func userPass(credStr string, isAdmin bool) (user string, pass string, err error) {
|
||||
creds := strings.Split(credStr, ":")
|
||||
if len(creds) != 2 {
|
||||
c := "user"
|
||||
if isAdmin {
|
||||
c = "admin"
|
||||
}
|
||||
err = fmt.Errorf("usage: writefreely --create-%s username:password", c)
|
||||
return
|
||||
}
|
||||
|
||||
user = creds[0]
|
||||
pass = creds[1]
|
||||
return
|
||||
}
|
||||
|
|
|
@ -54,7 +54,8 @@ type (
|
|||
PublicOwner bool `datastore:"public_owner" json:"-"`
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
db *datastore
|
||||
db *datastore
|
||||
hostName string
|
||||
}
|
||||
CollectionObj struct {
|
||||
Collection
|
||||
|
@ -211,10 +212,10 @@ func (c *Collection) DisplayCanonicalURL() string {
|
|||
|
||||
func (c *Collection) RedirectingCanonicalURL(isRedir bool) string {
|
||||
if isSingleUser {
|
||||
return hostName + "/"
|
||||
return c.hostName + "/"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s/", hostName, c.Alias)
|
||||
return fmt.Sprintf("%s/%s/", c.hostName, c.Alias)
|
||||
}
|
||||
|
||||
// PrevPageURL provides a full URL for the previous page of collection posts,
|
||||
|
@ -300,11 +301,11 @@ func (c *Collection) AvatarURL() string {
|
|||
if !isAvatarChar(fl) {
|
||||
return ""
|
||||
}
|
||||
return hostName + "/img/avatars/" + fl + ".png"
|
||||
return c.hostName + "/img/avatars/" + fl + ".png"
|
||||
}
|
||||
|
||||
func (c *Collection) FederatedAPIBase() string {
|
||||
return hostName + "/"
|
||||
return c.hostName + "/"
|
||||
}
|
||||
|
||||
func (c *Collection) FederatedAccount() string {
|
||||
|
@ -316,7 +317,7 @@ func (c *Collection) RenderMathJax() bool {
|
|||
return c.db.CollectionHasAttribute(c.ID, "render_mathjax")
|
||||
}
|
||||
|
||||
func newCollection(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
alias := r.FormValue("alias")
|
||||
title := r.FormValue("title")
|
||||
|
@ -399,7 +400,7 @@ func newCollection(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.HTTPError{http.StatusFound, redirectTo}
|
||||
}
|
||||
|
||||
func apiCheckCollectionPermissions(app *app, r *http.Request, c *Collection) (int64, error) {
|
||||
func apiCheckCollectionPermissions(app *App, r *http.Request, c *Collection) (int64, error) {
|
||||
accessToken := r.Header.Get("Authorization")
|
||||
var userID int64 = -1
|
||||
if accessToken != "" {
|
||||
|
@ -419,7 +420,7 @@ func apiCheckCollectionPermissions(app *app, r *http.Request, c *Collection) (in
|
|||
}
|
||||
|
||||
// fetchCollection handles the API endpoint for retrieving collection data.
|
||||
func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
accept := r.Header.Get("Accept")
|
||||
if strings.Contains(accept, "application/activity+json") {
|
||||
return handleFetchCollectionActivities(app, w, r)
|
||||
|
@ -434,6 +435,8 @@ func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
// Redirect users who aren't requesting JSON
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
if !reqJSON {
|
||||
|
@ -467,7 +470,7 @@ func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
// fetchCollectionPosts handles an API endpoint for retrieving a collection's
|
||||
// posts.
|
||||
func fetchCollectionPosts(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
alias := vars["alias"]
|
||||
|
||||
|
@ -475,6 +478,7 @@ func fetchCollectionPosts(app *app, w http.ResponseWriter, r *http.Request) erro
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
// Check permissions
|
||||
userID, err := apiCheckCollectionPermissions(app, r, c)
|
||||
|
@ -563,7 +567,7 @@ func processCollectionRequest(cr *collectionReq, vars map[string]string, w http.
|
|||
// domain that doesn't yet have a collection associated, or if a collection
|
||||
// requires a password. In either case, this will return nil, nil -- thus both
|
||||
// values should ALWAYS be checked to determine whether or not to continue.
|
||||
func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) {
|
||||
func processCollectionPermissions(app *App, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) {
|
||||
// Display collection if this is a collection
|
||||
var c *Collection
|
||||
var err error
|
||||
|
@ -600,6 +604,7 @@ func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.R
|
|||
}
|
||||
return nil, err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
// Update CollectionRequest to reflect owner status
|
||||
cr.isCollOwner = u != nil && u.ID == c.OwnerID
|
||||
|
@ -654,7 +659,7 @@ func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.R
|
|||
return c, nil
|
||||
}
|
||||
|
||||
func checkUserForCollection(app *app, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) {
|
||||
func checkUserForCollection(app *App, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) {
|
||||
u := getUserSession(app, r)
|
||||
return u, nil
|
||||
}
|
||||
|
@ -682,7 +687,7 @@ func getCollectionPage(vars map[string]string) int {
|
|||
}
|
||||
|
||||
// handleViewCollection displays the requested Collection
|
||||
func handleViewCollection(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
cr := &collectionReq{}
|
||||
|
||||
|
@ -788,7 +793,7 @@ func handleViewCollection(app *app, w http.ResponseWriter, r *http.Request) erro
|
|||
return err
|
||||
}
|
||||
|
||||
func handleViewCollectionTag(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
tag := vars["tag"]
|
||||
|
||||
|
@ -867,7 +872,7 @@ func handleViewCollectionTag(app *app, w http.ResponseWriter, r *http.Request) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleCollectionPostRedirect(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
slug := vars["slug"]
|
||||
|
||||
|
@ -885,7 +890,7 @@ func handleCollectionPostRedirect(app *app, w http.ResponseWriter, r *http.Reque
|
|||
return impart.HTTPError{http.StatusFound, loc}
|
||||
}
|
||||
|
||||
func existingCollection(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
vars := mux.Vars(r)
|
||||
collAlias := vars["alias"]
|
||||
|
@ -980,7 +985,7 @@ func collectionAliasFromReq(r *http.Request) string {
|
|||
return alias
|
||||
}
|
||||
|
||||
func handleWebCollectionUnlock(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleWebCollectionUnlock(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var readReq struct {
|
||||
Alias string `schema:"alias" json:"alias"`
|
||||
Pass string `schema:"password" json:"password"`
|
||||
|
@ -1047,7 +1052,7 @@ func handleWebCollectionUnlock(app *app, w http.ResponseWriter, r *http.Request)
|
|||
return impart.HTTPError{http.StatusFound, next}
|
||||
}
|
||||
|
||||
func isAuthorizedForCollection(app *app, alias string, r *http.Request) bool {
|
||||
func isAuthorizedForCollection(app *App, alias string, r *http.Request) bool {
|
||||
authd := false
|
||||
session, err := app.sessionStore.Get(r, blogPassCookieName)
|
||||
if err == nil {
|
||||
|
|
|
@ -13,6 +13,7 @@ package config
|
|||
|
||||
import (
|
||||
"gopkg.in/ini.v1"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -64,6 +65,7 @@ type (
|
|||
Theme string `ini:"theme"`
|
||||
JSDisabled bool `ini:"disable_js"`
|
||||
WebFonts bool `ini:"webfonts"`
|
||||
Landing string `ini:"landing"`
|
||||
|
||||
// Users
|
||||
SingleUser bool `ini:"single_user"`
|
||||
|
@ -134,6 +136,13 @@ func (cfg *Config) IsSecureStandalone() bool {
|
|||
return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != ""
|
||||
}
|
||||
|
||||
func (ac *AppCfg) LandingPath() string {
|
||||
if !strings.HasPrefix(ac.Landing, "/") {
|
||||
return "/" + ac.Landing
|
||||
}
|
||||
return ac.Landing
|
||||
}
|
||||
|
||||
// Load reads the given configuration file, then parses and returns it as a Config.
|
||||
func Load(fname string) (*Config, error) {
|
||||
if fname == "" {
|
||||
|
|
20
database-lib.go
Normal file
20
database-lib.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
// +build wflib
|
||||
|
||||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
* WriteFreely is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, included
|
||||
* in the LICENSE file in this source code package.
|
||||
*/
|
||||
|
||||
// This file contains dummy database funcs for when writefreely is used as a
|
||||
// library.
|
||||
|
||||
package writefreely
|
||||
|
||||
func (db *datastore) isDuplicateKeyErr(err error) bool {
|
||||
return false
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// +build !sqlite
|
||||
// +build !sqlite,!wflib
|
||||
|
||||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// +build sqlite
|
||||
// +build sqlite,!wflib
|
||||
|
||||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
|
|
13
database.go
13
database.go
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/web-core/query"
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/key"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -44,7 +45,7 @@ var (
|
|||
|
||||
type writestore interface {
|
||||
CreateUser(*User, string) error
|
||||
UpdateUserEmail(keys *keychain, userID int64, email string) error
|
||||
UpdateUserEmail(keys *key.Keychain, userID int64, email string) error
|
||||
UpdateEncryptedUserEmail(int64, []byte) error
|
||||
GetUserByID(int64) (*User, error)
|
||||
GetUserForAuth(string) (*User, error)
|
||||
|
@ -60,7 +61,7 @@ type writestore interface {
|
|||
GetTemporaryAccessToken(userID int64, validSecs int) (string, error)
|
||||
GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error)
|
||||
DeleteAccount(userID int64) (l *string, err error)
|
||||
ChangeSettings(app *app, u *User, s *userSettings) error
|
||||
ChangeSettings(app *App, u *User, s *userSettings) error
|
||||
ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error
|
||||
|
||||
GetCollections(u *User) (*[]Collection, error)
|
||||
|
@ -219,8 +220,8 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
|
|||
|
||||
// FIXME: We're returning errors inconsistently in this file. Do we use Errorf
|
||||
// for returned value, or impart?
|
||||
func (db *datastore) UpdateUserEmail(keys *keychain, userID int64, email string) error {
|
||||
encEmail, err := data.Encrypt(keys.emailKey, email)
|
||||
func (db *datastore) UpdateUserEmail(keys *key.Keychain, userID int64, email string) error {
|
||||
encEmail, err := data.Encrypt(keys.EmailKey, email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't encrypt email %s: %s\n", email, err)
|
||||
}
|
||||
|
@ -1779,13 +1780,13 @@ func (db *datastore) GetUserPostsCount(userID int64) int64 {
|
|||
|
||||
// ChangeSettings takes a User and applies the changes in the given
|
||||
// userSettings, MODIFYING THE USER with successful changes.
|
||||
func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
|
||||
func (db *datastore) ChangeSettings(app *App, u *User, s *userSettings) error {
|
||||
var errPass error
|
||||
q := query.NewUpdate()
|
||||
|
||||
// Update email if given
|
||||
if s.Email != "" {
|
||||
encEmail, err := data.Encrypt(app.keys.emailKey, s.Email)
|
||||
encEmail, err := data.Encrypt(app.keys.EmailKey, s.Email)
|
||||
if err != nil {
|
||||
log.Error("Couldn't encrypt email %s: %s\n", s.Email, err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, "Unable to encrypt email address."}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -99,7 +99,7 @@ func exportPostsZip(u *User, posts *[]PublicPost) []byte {
|
|||
return b.Bytes()
|
||||
}
|
||||
|
||||
func compileFullExport(app *app, u *User) *ExportUser {
|
||||
func compileFullExport(app *App, u *User) *ExportUser {
|
||||
exportUser := &ExportUser{
|
||||
User: u,
|
||||
}
|
||||
|
|
5
feed.go
5
feed.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -20,7 +20,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func ViewFeed(app *app, w http.ResponseWriter, req *http.Request) error {
|
||||
func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
||||
alias := collectionAliasFromReq(req)
|
||||
|
||||
// Display collection if this is a collection
|
||||
|
@ -34,6 +34,7 @@ func ViewFeed(app *app, w http.ResponseWriter, req *http.Request) error {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if c.IsPrivate() || c.IsProtected() {
|
||||
return ErrCollectionNotFound
|
||||
|
|
126
handle.go
126
handle.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -36,16 +36,17 @@ const (
|
|||
)
|
||||
|
||||
type (
|
||||
handlerFunc func(app *app, w http.ResponseWriter, r *http.Request) error
|
||||
userHandlerFunc func(app *app, u *User, w http.ResponseWriter, r *http.Request) error
|
||||
dataHandlerFunc func(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error)
|
||||
authFunc func(app *app, r *http.Request) (*User, error)
|
||||
handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error
|
||||
userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
||||
userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error
|
||||
dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error)
|
||||
authFunc func(app *App, r *http.Request) (*User, error)
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
errors *ErrorPages
|
||||
sessionStore *sessions.CookieStore
|
||||
app *app
|
||||
app Apper
|
||||
}
|
||||
|
||||
// ErrorPages hold template HTML error pages for displaying errors to the user.
|
||||
|
@ -59,7 +60,7 @@ type ErrorPages struct {
|
|||
|
||||
// NewHandler returns a new Handler instance, using the given StaticPage data,
|
||||
// and saving alias to the application's CookieStore.
|
||||
func NewHandler(app *app) *Handler {
|
||||
func NewHandler(apper Apper) *Handler {
|
||||
h := &Handler{
|
||||
errors: &ErrorPages{
|
||||
NotFound: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>404</title></head><body><p>Not found.</p></body></html>{{end}}")),
|
||||
|
@ -67,13 +68,26 @@ func NewHandler(app *app) *Handler {
|
|||
InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>500</title></head><body><p>Internal server error.</p></body></html>{{end}}")),
|
||||
Blank: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>{{.Title}}</title></head><body><p>{{.Content}}</p></body></html>{{end}}")),
|
||||
},
|
||||
sessionStore: app.sessionStore,
|
||||
app: app,
|
||||
sessionStore: apper.App().sessionStore,
|
||||
app: apper,
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// NewWFHandler returns a new Handler instance, using WriteFreely template files.
|
||||
// You MUST call writefreely.InitTemplates() before this.
|
||||
func NewWFHandler(apper Apper) *Handler {
|
||||
h := NewHandler(apper)
|
||||
h.SetErrorPages(&ErrorPages{
|
||||
NotFound: pages["404-general.tmpl"],
|
||||
Gone: pages["410.tmpl"],
|
||||
InternalServerError: pages["500.tmpl"],
|
||||
Blank: pages["blank.tmpl"],
|
||||
})
|
||||
return h
|
||||
}
|
||||
|
||||
// SetErrorPages sets the given set of ErrorPages as templates for any errors
|
||||
// that come up.
|
||||
func (h *Handler) SetErrorPages(e *ErrorPages) {
|
||||
|
@ -91,21 +105,21 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc {
|
|||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Error("%s: %s", e, debug.Stack())
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
|
||||
}()
|
||||
|
||||
u := getUserSession(h.app, r)
|
||||
u := getUserSession(h.app.App(), r)
|
||||
if u == nil {
|
||||
err := ErrNotLoggedIn
|
||||
status = err.Status
|
||||
return err
|
||||
}
|
||||
|
||||
err := f(h.app, u, w, r)
|
||||
err := f(h.app.App(), u, w, r)
|
||||
if err == nil {
|
||||
status = http.StatusOK
|
||||
} else if err, ok := err.(impart.HTTPError); ok {
|
||||
|
@ -129,14 +143,52 @@ func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc {
|
|||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Error("%s: %s", e, debug.Stack())
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()))
|
||||
}()
|
||||
|
||||
u := getUserSession(h.app, r)
|
||||
u := getUserSession(h.app.App(), r)
|
||||
if u == nil || !u.IsAdmin() {
|
||||
err := impart.HTTPError{http.StatusNotFound, ""}
|
||||
status = err.Status
|
||||
return err
|
||||
}
|
||||
|
||||
err := f(h.app.App(), u, w, r)
|
||||
if err == nil {
|
||||
status = http.StatusOK
|
||||
} else if err, ok := err.(impart.HTTPError); ok {
|
||||
status = err.Status
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return err
|
||||
}())
|
||||
}
|
||||
}
|
||||
|
||||
// AdminApper handles requests on /admin routes that require an Apper.
|
||||
func (h *Handler) AdminApper(f userApperHandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleHTTPError(w, r, func() error {
|
||||
var status int
|
||||
start := time.Now()
|
||||
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Error("%s: %s", e, debug.Stack())
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()))
|
||||
}()
|
||||
|
||||
u := getUserSession(h.app.App(), r)
|
||||
if u == nil || !u.IsAdmin() {
|
||||
err := impart.HTTPError{http.StatusNotFound, ""}
|
||||
status = err.Status
|
||||
|
@ -160,7 +212,7 @@ func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc {
|
|||
// UserAPI handles requests made in the API by the authenticated user.
|
||||
// This provides user-friendly HTML pages and actions that work in the browser.
|
||||
func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
|
||||
return h.UserAll(false, f, func(app *app, r *http.Request) (*User, error) {
|
||||
return h.UserAll(false, f, func(app *App, r *http.Request) (*User, error) {
|
||||
// Authorize user from Authorization header
|
||||
t := r.Header.Get("Authorization")
|
||||
if t == "" {
|
||||
|
@ -191,7 +243,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF
|
|||
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
|
||||
}()
|
||||
|
||||
u, err := a(h.app, r)
|
||||
u, err := a(h.app.App(), r)
|
||||
if err != nil {
|
||||
if err, ok := err.(impart.HTTPError); ok {
|
||||
status = err.Status
|
||||
|
@ -201,7 +253,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF
|
|||
return err
|
||||
}
|
||||
|
||||
err = f(h.app, u, w, r)
|
||||
err = f(h.app.App(), u, w, r)
|
||||
if err == nil {
|
||||
status = 200
|
||||
} else if err, ok := err.(impart.HTTPError); ok {
|
||||
|
@ -222,7 +274,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF
|
|||
}
|
||||
|
||||
func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc {
|
||||
return func(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
return func(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
err := f(app, w, r)
|
||||
if err != nil {
|
||||
if ie, ok := err.(impart.HTTPError); ok {
|
||||
|
@ -239,7 +291,7 @@ func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc {
|
|||
}
|
||||
|
||||
func (h *Handler) Page(n string) http.HandlerFunc {
|
||||
return h.Web(func(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
return h.Web(func(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
t, ok := pages[n]
|
||||
if !ok {
|
||||
return impart.HTTPError{http.StatusNotFound, "Page not found."}
|
||||
|
@ -264,13 +316,13 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
|
|||
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
u := getUserSession(h.app, r)
|
||||
u := getUserSession(h.app.App(), r)
|
||||
username := "None"
|
||||
if u != nil {
|
||||
username = u.Username
|
||||
}
|
||||
log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = 500
|
||||
}
|
||||
|
||||
|
@ -302,13 +354,13 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
|
|||
}
|
||||
|
||||
// TODO: pass User object to function
|
||||
err = f(h.app, w, r)
|
||||
err = f(h.app.App(), w, r)
|
||||
if err == nil {
|
||||
status = 200
|
||||
} else if httpErr, ok := err.(impart.HTTPError); ok {
|
||||
status = httpErr.Status
|
||||
if status < 300 || status > 399 {
|
||||
addSessionFlash(h.app, w, r, httpErr.Message, session)
|
||||
addSessionFlash(h.app.App(), w, r, httpErr.Message, session)
|
||||
return impart.HTTPError{http.StatusFound, r.Referer()}
|
||||
}
|
||||
} else {
|
||||
|
@ -319,7 +371,7 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
|
|||
log.Error(e)
|
||||
}
|
||||
log.Info("Web handler internal error render")
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = 500
|
||||
}
|
||||
|
||||
|
@ -338,14 +390,14 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
|
|||
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
u := getUserSession(h.app, r)
|
||||
u := getUserSession(h.app.App(), r)
|
||||
username := "None"
|
||||
if u != nil {
|
||||
username = u.Username
|
||||
}
|
||||
log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
|
||||
log.Info("Web deferred internal error render")
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = 500
|
||||
}
|
||||
|
||||
|
@ -375,7 +427,7 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
|
|||
}
|
||||
|
||||
// TODO: pass User object to function
|
||||
err := f(h.app, w, r)
|
||||
err := f(h.app.App(), w, r)
|
||||
if err == nil {
|
||||
status = 200
|
||||
} else if httpErr, ok := err.(impart.HTTPError); ok {
|
||||
|
@ -384,7 +436,7 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
|
|||
e := fmt.Sprintf("[Web handler] 500: %v", err)
|
||||
log.Error(e)
|
||||
log.Info("Web internal error render")
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = 500
|
||||
}
|
||||
|
||||
|
@ -412,7 +464,7 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc {
|
|||
|
||||
// TODO: do any needed authentication
|
||||
|
||||
err := f(h.app, w, r)
|
||||
err := f(h.app.App(), w, r)
|
||||
if err != nil {
|
||||
if err, ok := err.(impart.HTTPError); ok {
|
||||
status = err.Status
|
||||
|
@ -434,14 +486,14 @@ func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc {
|
|||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Error("%s: %s", e, debug.Stack())
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = 500
|
||||
}
|
||||
|
||||
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
|
||||
}()
|
||||
|
||||
data, filename, err := f(h.app, w, r)
|
||||
data, filename, err := f(h.app.App(), w, r)
|
||||
if err != nil {
|
||||
if err, ok := err.(impart.HTTPError); ok {
|
||||
status = err.Status
|
||||
|
@ -530,7 +582,7 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er
|
|||
page.StaticPage
|
||||
Content *template.HTML
|
||||
}{
|
||||
StaticPage: pageForReq(h.app, r),
|
||||
StaticPage: pageForReq(h.app.App(), r),
|
||||
}
|
||||
if err.Message != "" {
|
||||
co := template.HTML(err.Message)
|
||||
|
@ -540,12 +592,12 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er
|
|||
return
|
||||
} else if err.Status == http.StatusNotFound {
|
||||
w.WriteHeader(err.Status)
|
||||
h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
return
|
||||
} else if err.Status == http.StatusInternalServerError {
|
||||
w.WriteHeader(err.Status)
|
||||
log.Info("handleHTTPErorr internal error render")
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
return
|
||||
} else if err.Status == http.StatusAccepted {
|
||||
impart.WriteSuccess(w, "", err.Status)
|
||||
|
@ -556,7 +608,7 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er
|
|||
Title string
|
||||
Content template.HTML
|
||||
}{
|
||||
pageForReq(h.app, r),
|
||||
pageForReq(h.app.App(), r),
|
||||
fmt.Sprintf("Uh oh (%d)", err.Status),
|
||||
template.HTML(fmt.Sprintf("<p style=\"text-align: center\" class=\"introduction\">%s</p>", err.Message)),
|
||||
}
|
||||
|
@ -591,7 +643,7 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error)
|
|||
impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
|
||||
return
|
||||
}
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
}
|
||||
|
||||
func correctPageFromLoginAttempt(r *http.Request) string {
|
||||
|
@ -613,7 +665,7 @@ func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc {
|
|||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Error("Handler.LogHandlerFunc\n\n%s: %s", e, debug.Stack())
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
|
||||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
|
||||
status = 500
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -15,7 +15,7 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
func handleViewHostMeta(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewHostMeta(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Server", serverSoftware)
|
||||
w.Header().Set("Content-Type", "application/xrd+xml; charset=utf-8")
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ func (i Invite) ExpiresFriendly() string {
|
|||
return i.Expires.Format("January 2, 2006, 3:04 PM")
|
||||
}
|
||||
|
||||
func handleViewUserInvites(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
// Don't show page if instance doesn't allow it
|
||||
if !(app.cfg.App.UserInvites != "" && (u.IsAdmin() || app.cfg.App.UserInvites != "admin")) {
|
||||
return impart.HTTPError{http.StatusNotFound, ""}
|
||||
|
@ -73,7 +73,7 @@ func handleViewUserInvites(app *app, u *User, w http.ResponseWriter, r *http.Req
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleCreateUserInvite(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
muVal := r.FormValue("uses")
|
||||
expVal := r.FormValue("expires")
|
||||
|
||||
|
@ -106,7 +106,7 @@ func handleCreateUserInvite(app *app, u *User, w http.ResponseWriter, r *http.Re
|
|||
return impart.HTTPError{http.StatusFound, "/me/invites"}
|
||||
}
|
||||
|
||||
func handleViewInvite(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
inviteCode := mux.Vars(r)["code"]
|
||||
|
||||
i, err := app.db.GetUserInvite(inviteCode)
|
||||
|
|
63
key/key.go
Normal file
63
key/key.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
* WriteFreely is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, included
|
||||
* in the LICENSE file in this source code package.
|
||||
*/
|
||||
|
||||
// Package key holds application keys and utilities around generating them.
|
||||
package key
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
EncKeysBytes = 32
|
||||
)
|
||||
|
||||
type Keychain struct {
|
||||
EmailKey, CookieAuthKey, CookieKey []byte
|
||||
}
|
||||
|
||||
// GenerateKeys generates necessary keys for the app on the given Keychain,
|
||||
// skipping any that already exist.
|
||||
func (keys *Keychain) GenerateKeys() error {
|
||||
// Generate keys only if they don't already exist
|
||||
// TODO: use something like https://github.com/hashicorp/go-multierror to return errors
|
||||
var err, keyErrs error
|
||||
if len(keys.EmailKey) == 0 {
|
||||
keys.EmailKey, err = GenerateBytes(EncKeysBytes)
|
||||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
}
|
||||
if len(keys.CookieAuthKey) == 0 {
|
||||
keys.CookieAuthKey, err = GenerateBytes(EncKeysBytes)
|
||||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
}
|
||||
if len(keys.CookieKey) == 0 {
|
||||
keys.CookieKey, err = GenerateBytes(EncKeysBytes)
|
||||
if err != nil {
|
||||
keyErrs = err
|
||||
}
|
||||
}
|
||||
|
||||
return keyErrs
|
||||
}
|
||||
|
||||
// GenerateBytes returns securely generated random bytes.
|
||||
func GenerateBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
62
keys.go
62
keys.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,8 +11,8 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/key"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -20,8 +20,6 @@ import (
|
|||
|
||||
const (
|
||||
keysDir = "keys"
|
||||
|
||||
encKeysBytes = 32
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -30,47 +28,22 @@ var (
|
|||
cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256")
|
||||
)
|
||||
|
||||
type keychain struct {
|
||||
emailKey, cookieAuthKey, cookieKey []byte
|
||||
// InitKeys loads encryption keys into memory via the given Apper interface
|
||||
func InitKeys(apper Apper) error {
|
||||
log.Info("Loading encryption keys...")
|
||||
err := apper.LoadKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initKeyPaths(app *app) {
|
||||
func initKeyPaths(app *App) {
|
||||
emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath)
|
||||
cookieAuthKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieAuthKeyPath)
|
||||
cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath)
|
||||
}
|
||||
|
||||
func initKeys(app *app) error {
|
||||
var err error
|
||||
app.keys = &keychain{}
|
||||
|
||||
if debugging {
|
||||
log.Info(" %s", emailKeyPath)
|
||||
}
|
||||
app.keys.emailKey, err = ioutil.ReadFile(emailKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if debugging {
|
||||
log.Info(" %s", cookieAuthKeyPath)
|
||||
}
|
||||
app.keys.cookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if debugging {
|
||||
log.Info(" %s", cookieKeyPath)
|
||||
}
|
||||
app.keys.cookieKey, err = ioutil.ReadFile(cookieKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateKey generates a key at the given path used for the encryption of
|
||||
// certain user data. Because user data becomes unrecoverable without these
|
||||
// keys, this won't overwrite any existing key, and instead outputs a message.
|
||||
|
@ -85,7 +58,7 @@ func generateKey(path string) error {
|
|||
}
|
||||
|
||||
log.Info("Generating %s.", path)
|
||||
b, err := generateBytes(encKeysBytes)
|
||||
b, err := key.GenerateBytes(key.EncKeysBytes)
|
||||
if err != nil {
|
||||
log.Error("FAILED. %s. Run writefreely --gen-keys again.", err)
|
||||
return err
|
||||
|
@ -98,14 +71,3 @@ func generateKey(path string) error {
|
|||
log.Info("Success.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateBytes returns securely generated random bytes.
|
||||
func generateBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
|
6
pad.go
6
pad.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -19,7 +19,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
action := vars["action"]
|
||||
slug := vars["slug"]
|
||||
|
@ -102,7 +102,7 @@ func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleViewMeta(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
action := vars["action"]
|
||||
slug := vars["slug"]
|
||||
|
|
6
pages.go
6
pages.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -18,7 +18,7 @@ import (
|
|||
|
||||
var defaultPageUpdatedTime = time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local)
|
||||
|
||||
func getAboutPage(app *app) (*instanceContent, error) {
|
||||
func getAboutPage(app *App) (*instanceContent, error) {
|
||||
c, err := app.db.GetDynamicContent("about")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -40,7 +40,7 @@ func defaultAboutTitle(cfg *config.Config) sql.NullString {
|
|||
return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true}
|
||||
}
|
||||
|
||||
func getPrivacyPage(app *app) (*instanceContent, error) {
|
||||
func getPrivacyPage(app *App) (*instanceContent, error) {
|
||||
c, err := app.db.GetDynamicContent("privacy")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
36
posts.go
36
posts.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -262,7 +262,7 @@ func (p *Post) HasTitleLink() bool {
|
|||
return hasLink
|
||||
}
|
||||
|
||||
func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
friendlyID := vars["post"]
|
||||
|
||||
|
@ -277,7 +277,7 @@ func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return handleTemplatedPage(app, w, r, t)
|
||||
} else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" {
|
||||
// Serve static file
|
||||
shttp.ServeHTTP(w, r)
|
||||
app.shttp.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -468,7 +468,7 @@ func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
// /posts
|
||||
// /posts?collection={alias}
|
||||
// ? /collections/{alias}/posts
|
||||
func newPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
vars := mux.Vars(r)
|
||||
collAlias := vars["alias"]
|
||||
|
@ -593,7 +593,7 @@ func newPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return response
|
||||
}
|
||||
|
||||
func existingPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
vars := mux.Vars(r)
|
||||
postID := vars["post"]
|
||||
|
@ -717,7 +717,7 @@ func existingPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func deletePost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func deletePost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
friendlyID := vars["post"]
|
||||
editToken := r.FormValue("token")
|
||||
|
@ -834,7 +834,7 @@ func deletePost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
// addPost associates a post with the authenticated user.
|
||||
func addPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var ownerID int64
|
||||
|
||||
// Authenticate user
|
||||
|
@ -883,7 +883,7 @@ func addPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.WriteSuccess(w, res, http.StatusOK)
|
||||
}
|
||||
|
||||
func dispersePost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func dispersePost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var ownerID int64
|
||||
|
||||
// Authenticate user
|
||||
|
@ -927,7 +927,7 @@ type (
|
|||
)
|
||||
|
||||
// pinPost pins a post to a blog
|
||||
func pinPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var userID int64
|
||||
|
||||
// Authenticate user
|
||||
|
@ -985,7 +985,7 @@ func pinPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.WriteSuccess(w, res, http.StatusOK)
|
||||
}
|
||||
|
||||
func fetchPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
var collID int64
|
||||
var coll *Collection
|
||||
var err error
|
||||
|
@ -996,6 +996,7 @@ func fetchPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
coll.hostName = app.cfg.App.Host
|
||||
_, err = apiCheckCollectionPermissions(app, r, coll)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1034,7 +1035,7 @@ func fetchPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.WriteSuccess(w, p, http.StatusOK)
|
||||
}
|
||||
|
||||
func fetchPostProperty(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func fetchPostProperty(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
p, err := app.db.GetPostProperty(vars["post"], 0, vars["property"])
|
||||
if err != nil {
|
||||
|
@ -1056,7 +1057,7 @@ func (p *Post) processPost() PublicPost {
|
|||
|
||||
func (p *PublicPost) CanonicalURL() string {
|
||||
if p.Collection == nil || p.Collection.Alias == "" {
|
||||
return hostName + "/" + p.ID
|
||||
return p.Collection.hostName + "/" + p.ID
|
||||
}
|
||||
return p.Collection.CanonicalURL() + p.Slug.String
|
||||
}
|
||||
|
@ -1087,7 +1088,7 @@ func (p *PublicPost) ActivityObject() *activitystreams.Object {
|
|||
if isSingleUser {
|
||||
tagBaseURL = p.Collection.CanonicalURL() + "tag:"
|
||||
} else {
|
||||
tagBaseURL = fmt.Sprintf("%s/%s/tag:", hostName, p.Collection.Alias)
|
||||
tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias)
|
||||
}
|
||||
for _, t := range p.Tags {
|
||||
o.Tag = append(o.Tag, activitystreams.Tag{
|
||||
|
@ -1132,7 +1133,7 @@ func (p *SubmittedPost) isFontValid() bool {
|
|||
return valid
|
||||
}
|
||||
|
||||
func getRawPost(app *app, friendlyID string) *RawPost {
|
||||
func getRawPost(app *App, friendlyID string) *RawPost {
|
||||
var content, font, title string
|
||||
var isRTL sql.NullBool
|
||||
var lang sql.NullString
|
||||
|
@ -1152,7 +1153,7 @@ func getRawPost(app *app, friendlyID string) *RawPost {
|
|||
}
|
||||
|
||||
// TODO; return a Post!
|
||||
func getRawCollectionPost(app *app, slug, collAlias string) *RawPost {
|
||||
func getRawCollectionPost(app *App, slug, collAlias string) *RawPost {
|
||||
var id, title, content, font string
|
||||
var isRTL sql.NullBool
|
||||
var lang sql.NullString
|
||||
|
@ -1189,7 +1190,7 @@ func getRawCollectionPost(app *app, slug, collAlias string) *RawPost {
|
|||
}
|
||||
}
|
||||
|
||||
func viewCollectionPost(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
slug := vars["slug"]
|
||||
|
||||
|
@ -1200,7 +1201,7 @@ func viewCollectionPost(app *app, w http.ResponseWriter, r *http.Request) error
|
|||
|
||||
if strings.Contains(r.URL.Path, ".") && !isRaw {
|
||||
// Serve static file
|
||||
shttp.ServeHTTP(w, r)
|
||||
app.shttp.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1244,6 +1245,7 @@ func viewCollectionPost(app *app, w http.ResponseWriter, r *http.Request) error
|
|||
}
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
// Check collection permissions
|
||||
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {
|
||||
|
|
25
read.go
25
read.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -49,20 +49,20 @@ type readPublication struct {
|
|||
TotalPages int
|
||||
}
|
||||
|
||||
func initLocalTimeline(app *app) {
|
||||
func initLocalTimeline(app *App) {
|
||||
app.timeline = &localTimeline{
|
||||
postsPerPage: tlPostsPerPage,
|
||||
m: memo.New(app.db.FetchPublicPosts, 10*time.Minute),
|
||||
m: memo.New(app.FetchPublicPosts, 10*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
// satisfies memo.Func
|
||||
func (db *datastore) FetchPublicPosts() (interface{}, error) {
|
||||
func (app *App) FetchPublicPosts() (interface{}, error) {
|
||||
// Finds all public posts and posts in a public collection published during the owner's active subscription period and within the last 3 months
|
||||
rows, err := db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated
|
||||
rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated
|
||||
FROM collections c
|
||||
LEFT JOIN posts p ON p.collection_id = c.id
|
||||
WHERE c.privacy = 1 AND (p.created >= ` + db.dateSub(3, "month") + ` AND p.created <= ` + db.now() + ` AND pinned_position IS NULL)
|
||||
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL)
|
||||
ORDER BY p.created DESC`)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting from posts: %v", err)
|
||||
|
@ -82,6 +82,8 @@ func (db *datastore) FetchPublicPosts() (interface{}, error) {
|
|||
log.Error("[READ] Unable to scan row, skipping: %v", err)
|
||||
continue
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
isCollectionPost := alias.Valid
|
||||
if isCollectionPost {
|
||||
c.Alias = alias.String
|
||||
|
@ -108,7 +110,7 @@ func (db *datastore) FetchPublicPosts() (interface{}, error) {
|
|||
return posts, nil
|
||||
}
|
||||
|
||||
func viewLocalTimelineAPI(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewLocalTimelineAPI(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
updateTimelineCache(app.timeline)
|
||||
|
||||
skip, _ := strconv.Atoi(r.FormValue("skip"))
|
||||
|
@ -121,7 +123,7 @@ func viewLocalTimelineAPI(app *app, w http.ResponseWriter, r *http.Request) erro
|
|||
return impart.WriteSuccess(w, posts, http.StatusOK)
|
||||
}
|
||||
|
||||
func viewLocalTimeline(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func viewLocalTimeline(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
if !app.cfg.App.LocalTimeline {
|
||||
return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."}
|
||||
}
|
||||
|
@ -153,7 +155,7 @@ func updateTimelineCache(tl *localTimeline) {
|
|||
}
|
||||
}
|
||||
|
||||
func showLocalTimeline(app *app, w http.ResponseWriter, r *http.Request, page int, author, tag string) error {
|
||||
func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page int, author, tag string) error {
|
||||
updateTimelineCache(app.timeline)
|
||||
|
||||
pl := len(*(app.timeline.posts))
|
||||
|
@ -226,7 +228,7 @@ func (c *readPublication) PrevPageURL(n int) string {
|
|||
|
||||
// handlePostIDRedirect handles a route where a post ID is given and redirects
|
||||
// the user to the canonical post URL.
|
||||
func handlePostIDRedirect(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handlePostIDRedirect(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
postID := vars["post"]
|
||||
p, err := app.db.GetPost(postID, 0)
|
||||
|
@ -244,12 +246,13 @@ func handlePostIDRedirect(app *app, w http.ResponseWriter, r *http.Request) erro
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
// Retrieve collection information and send user to canonical URL
|
||||
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + p.Slug.String}
|
||||
}
|
||||
|
||||
func viewLocalTimelineFeed(app *app, w http.ResponseWriter, req *http.Request) error {
|
||||
func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
||||
if !app.cfg.App.LocalTimeline {
|
||||
return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."}
|
||||
}
|
||||
|
|
42
routes.go
42
routes.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -14,15 +14,30 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"github.com/writeas/go-webfinger"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writefreely/go-nodeinfo"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datastore) {
|
||||
hostSubroute := cfg.App.Host[strings.Index(cfg.App.Host, "://")+3:]
|
||||
if cfg.App.SingleUser {
|
||||
// InitStaticRoutes adds routes for serving static files.
|
||||
// TODO: this should just be a func, not method
|
||||
func (app *App) InitStaticRoutes(r *mux.Router) {
|
||||
// Handle static files
|
||||
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
|
||||
app.shttp = http.NewServeMux()
|
||||
app.shttp.Handle("/", fs)
|
||||
r.PathPrefix("/").Handler(fs)
|
||||
}
|
||||
|
||||
// InitRoutes adds dynamic routes for the given mux.Router.
|
||||
func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||
// Create handler
|
||||
handler := NewWFHandler(apper)
|
||||
|
||||
// Set up routes
|
||||
hostSubroute := apper.App().cfg.App.Host[strings.Index(apper.App().cfg.App.Host, "://")+3:]
|
||||
if apper.App().cfg.App.SingleUser {
|
||||
hostSubroute = "{domain}"
|
||||
} else {
|
||||
if strings.HasPrefix(hostSubroute, "localhost") {
|
||||
|
@ -30,7 +45,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
|
|||
}
|
||||
}
|
||||
|
||||
if cfg.App.SingleUser {
|
||||
if apper.App().cfg.App.SingleUser {
|
||||
log.Info("Adding %s routes (single user)...", hostSubroute)
|
||||
} else {
|
||||
log.Info("Adding %s routes (multi-user)...", hostSubroute)
|
||||
|
@ -40,7 +55,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
|
|||
write := r.PathPrefix("/").Subrouter()
|
||||
|
||||
// Federation endpoint configurations
|
||||
wf := webfinger.Default(wfResolver{db, cfg})
|
||||
wf := webfinger.Default(wfResolver{apper.App().db, apper.App().cfg})
|
||||
wf.NoTLSHandler = nil
|
||||
|
||||
// Federation endpoints
|
||||
|
@ -49,15 +64,15 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
|
|||
// webfinger
|
||||
write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger)))
|
||||
// nodeinfo
|
||||
niCfg := nodeInfoConfig(db, cfg)
|
||||
ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{cfg, db})
|
||||
niCfg := nodeInfoConfig(apper.App().db, apper.App().cfg)
|
||||
ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{apper.App().cfg, apper.App().db})
|
||||
write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover)))
|
||||
write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo)))
|
||||
|
||||
// Set up dyamic page handlers
|
||||
// Handle auth
|
||||
auth := write.PathPrefix("/api/auth/").Subrouter()
|
||||
if cfg.App.OpenRegistration {
|
||||
if apper.App().cfg.App.OpenRegistration {
|
||||
auth.HandleFunc("/signup", handler.All(apiSignup)).Methods("POST")
|
||||
}
|
||||
auth.HandleFunc("/login", handler.All(login)).Methods("POST")
|
||||
|
@ -130,7 +145,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
|
|||
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
|
||||
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
|
||||
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
|
||||
write.HandleFunc("/admin/update/config", handler.Admin(handleAdminUpdateConfig)).Methods("POST")
|
||||
write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")
|
||||
write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST")
|
||||
|
||||
// Handle special pages first
|
||||
|
@ -144,7 +159,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
|
|||
RouteRead(handler, readPerm, write.PathPrefix("/read").Subrouter())
|
||||
|
||||
draftEditPrefix := ""
|
||||
if cfg.App.SingleUser {
|
||||
if apper.App().cfg.App.SingleUser {
|
||||
draftEditPrefix = "/d"
|
||||
write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
|
||||
} else {
|
||||
|
@ -155,7 +170,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
|
|||
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
|
||||
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET")
|
||||
// Collections
|
||||
if cfg.App.SingleUser {
|
||||
if apper.App().cfg.App.SingleUser {
|
||||
RouteCollections(handler, write.PathPrefix("/").Subrouter())
|
||||
} else {
|
||||
write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional))
|
||||
|
@ -165,6 +180,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
|
|||
}
|
||||
write.HandleFunc(draftEditPrefix+"/{post}", handler.Web(handleViewPost, UserLevelOptional))
|
||||
write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional))
|
||||
return r
|
||||
}
|
||||
|
||||
func RouteCollections(handler *Handler, r *mux.Router) {
|
||||
|
|
96
scripts/upgrade-server.sh
Executable file
96
scripts/upgrade-server.sh
Executable file
|
@ -0,0 +1,96 @@
|
|||
#! /bin/bash
|
||||
###############################################################################
|
||||
## writefreely update script ##
|
||||
## ##
|
||||
## WARNING: running this script will overwrite any modifed assets or ##
|
||||
## template files. If you have any custom changes to these files you ##
|
||||
## should back them up FIRST. ##
|
||||
## ##
|
||||
## This must be run from the web application root directory ##
|
||||
## i.e. /var/www/writefreely, and operates under the assumption that you##
|
||||
## have not installed the binary `writefreely` in another location. ##
|
||||
###############################################################################
|
||||
#
|
||||
# Copyright © 2019 A Bunch Tell LLC.
|
||||
#
|
||||
# This file is part of WriteFreely.
|
||||
#
|
||||
# WriteFreely is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License, included
|
||||
# in the LICENSE file in this source code package.
|
||||
#
|
||||
|
||||
|
||||
# only execute as root, or use sudo
|
||||
|
||||
if [[ `id -u` -ne 0 ]]; then
|
||||
echo "You must login as root, or execute this script with sudo"
|
||||
exit 10
|
||||
fi
|
||||
|
||||
# go ahead and check for the latest release on linux
|
||||
echo "Checking for updates..."
|
||||
|
||||
url=`curl -s https://api.github.com/repos/writeas/writefreely/releases/latest | grep 'browser_' | grep linux | cut -d\" -f4`
|
||||
|
||||
# check current version
|
||||
|
||||
bin_output=`./writefreely -v`
|
||||
if [ -z "$bin_output" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current=${bin_output:12:5}
|
||||
echo "Current version is v$current"
|
||||
|
||||
# grab latest version number
|
||||
IFS='/'
|
||||
read -ra parts <<< "$url"
|
||||
|
||||
latest=${parts[-2]}
|
||||
echo "Latest release is $latest"
|
||||
|
||||
|
||||
IFS='.'
|
||||
read -ra cv <<< "$current"
|
||||
read -ra lv <<< "${latest#v}"
|
||||
|
||||
IFS=' '
|
||||
tempdir=$(mktemp -d)
|
||||
|
||||
|
||||
if [[ ${lv[0]} -gt ${cv[0]} ]]; then
|
||||
echo "New major version available."
|
||||
echo "Downloading..."
|
||||
`wget -P $tempdir -q --show-progress $url`
|
||||
elif [[ ${lv[0]} -eq ${cv[0]} ]] && [[ ${lv[1]} -gt ${cv[1]} ]]; then
|
||||
echo "New minor version available."
|
||||
echo "Downloading..."
|
||||
`wget -P $tempdir -q --show-progress $url`
|
||||
elif [[ ${lv[2]} -gt ${cv[2]} ]]; then
|
||||
echo "New patch version available."
|
||||
echo "Downloading..."
|
||||
`wget -P $tempdir -q --show-progress $url`
|
||||
else
|
||||
echo "Up to date."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
filename=${parts[-1]}
|
||||
|
||||
# extract
|
||||
echo "Extracting files..."
|
||||
tar -zxf $tempdir/$filename -C $tempdir
|
||||
|
||||
# copy files
|
||||
echo "Copying files..."
|
||||
cp -r $tempdir/{pages,static,templates,writefreely} .
|
||||
|
||||
# restart service
|
||||
echo "Restarting writefreely systemd service..."
|
||||
if `systemctl restart writefreely`; then
|
||||
echo "Success, version has been upgraded to $latest."
|
||||
else
|
||||
echo "Upgrade complete, but failed to restart service."
|
||||
exit 1
|
||||
fi
|
22
session.go
22
session.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -27,24 +27,24 @@ const (
|
|||
blogPassCookieName = "ub"
|
||||
)
|
||||
|
||||
// initSession creates the cookie store. It depends on the keychain already
|
||||
// InitSession creates the cookie store. It depends on the keychain already
|
||||
// being loaded.
|
||||
func initSession(app *app) *sessions.CookieStore {
|
||||
func (app *App) InitSession() {
|
||||
// Register complex data types we'll be storing in cookies
|
||||
gob.Register(&User{})
|
||||
|
||||
// Create the cookie store
|
||||
store := sessions.NewCookieStore(app.keys.cookieAuthKey, app.keys.cookieKey)
|
||||
store := sessions.NewCookieStore(app.keys.CookieAuthKey, app.keys.CookieKey)
|
||||
store.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: sessionLength,
|
||||
HttpOnly: true,
|
||||
Secure: strings.HasPrefix(app.cfg.App.Host, "https://"),
|
||||
}
|
||||
return store
|
||||
app.sessionStore = store
|
||||
}
|
||||
|
||||
func getSessionFlashes(app *app, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) {
|
||||
func getSessionFlashes(app *App, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) {
|
||||
var err error
|
||||
if session == nil {
|
||||
session, err = app.sessionStore.Get(r, cookieName)
|
||||
|
@ -66,7 +66,7 @@ func getSessionFlashes(app *app, w http.ResponseWriter, r *http.Request, session
|
|||
return f, nil
|
||||
}
|
||||
|
||||
func addSessionFlash(app *app, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error {
|
||||
func addSessionFlash(app *App, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error {
|
||||
var err error
|
||||
if session == nil {
|
||||
session, err = app.sessionStore.Get(r, cookieName)
|
||||
|
@ -82,7 +82,7 @@ func addSessionFlash(app *app, w http.ResponseWriter, r *http.Request, m string,
|
|||
return nil
|
||||
}
|
||||
|
||||
func getUserAndSession(app *app, r *http.Request) (*User, *sessions.Session) {
|
||||
func getUserAndSession(app *App, r *http.Request) (*User, *sessions.Session) {
|
||||
session, err := app.sessionStore.Get(r, cookieName)
|
||||
if err == nil {
|
||||
// Got the currently logged-in user
|
||||
|
@ -97,12 +97,12 @@ func getUserAndSession(app *app, r *http.Request) (*User, *sessions.Session) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func getUserSession(app *app, r *http.Request) *User {
|
||||
func getUserSession(app *App, r *http.Request) *User {
|
||||
u, _ := getUserAndSession(app, r)
|
||||
return u
|
||||
}
|
||||
|
||||
func saveUserSession(app *app, r *http.Request, w http.ResponseWriter) error {
|
||||
func saveUserSession(app *App, r *http.Request, w http.ResponseWriter) error {
|
||||
session, err := app.sessionStore.Get(r, cookieName)
|
||||
if err != nil {
|
||||
return ErrInternalCookieSession
|
||||
|
@ -127,7 +127,7 @@ func saveUserSession(app *app, r *http.Request, w http.ResponseWriter) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func getFullUserSession(app *app, r *http.Request) *User {
|
||||
func getFullUserSession(app *App, r *http.Request) *User {
|
||||
u := getUserSession(app, r)
|
||||
if u == nil {
|
||||
return nil
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -34,7 +34,7 @@ func buildSitemap(host, alias string) *stm.Sitemap {
|
|||
return sm
|
||||
}
|
||||
|
||||
func handleViewSitemap(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleViewSitemap(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
// Determine canonical blog URL
|
||||
|
@ -57,6 +57,7 @@ func handleViewSitemap(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.hostName = app.cfg.App.Host
|
||||
|
||||
if !isSubdomain {
|
||||
pre += alias + "/"
|
||||
|
|
|
@ -98,7 +98,8 @@ func initUserPage(parentDir, path, key string) {
|
|||
))
|
||||
}
|
||||
|
||||
func initTemplates(cfg *config.Config) error {
|
||||
// InitTemplates loads all template files from the configured parent dir.
|
||||
func InitTemplates(cfg *config.Config) error {
|
||||
log.Info("Loading templates...")
|
||||
tmplFiles, err := ioutil.ReadDir(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir))
|
||||
if err != nil {
|
||||
|
|
|
@ -71,6 +71,8 @@ p.docs {
|
|||
<dd>{{.Config.Host}}</dd>
|
||||
<dt>User Mode</dt>
|
||||
<dd>{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}</dd>
|
||||
<dt{{if .Config.SingleUser}} class="invisible"{{end}}>Landing Page</dt>
|
||||
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="landing" id="landing" class="inline" value="{{.Config.Landing}}" style="width: 14em;" /></dd>
|
||||
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_registration">Open Registrations</label></dt>
|
||||
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_registration" id="open_registration" {{if .Config.OpenRegistration}}checked="checked"{{end}} /></dd>
|
||||
<dt><label for="min_username_len">Minimum Username Length</label></dt>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -18,7 +18,7 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
func handleWebSignup(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
|
||||
// Get params
|
||||
|
@ -67,7 +67,7 @@ func handleWebSignup(app *app, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
// { "username": "asdf" }
|
||||
// result: { code: 204 }
|
||||
func handleUsernameCheck(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||
func handleUsernameCheck(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
||||
|
||||
// Get params
|
||||
|
@ -112,7 +112,7 @@ func handleUsernameCheck(app *app, w http.ResponseWriter, r *http.Request) error
|
|||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
|
||||
func getValidUsername(app *app, reqName, prevName string) (string, *impart.HTTPError) {
|
||||
func getValidUsername(app *App, reqName, prevName string) (string, *impart.HTTPError) {
|
||||
// Check if username is okay
|
||||
finalUsername := getSlug(reqName, "")
|
||||
if finalUsername == "" {
|
||||
|
|
5
users.go
5
users.go
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/guregu/null/zero"
|
||||
"github.com/writeas/web-core/data"
|
||||
"github.com/writeas/web-core/log"
|
||||
"github.com/writeas/writefreely/key"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -79,13 +80,13 @@ type (
|
|||
|
||||
// EmailClear decrypts and returns the user's email, caching it in the user
|
||||
// object.
|
||||
func (u *User) EmailClear(keys *keychain) string {
|
||||
func (u *User) EmailClear(keys *key.Keychain) string {
|
||||
if u.clearEmail != "" {
|
||||
return u.clearEmail
|
||||
}
|
||||
|
||||
if u.Email.Valid && u.Email.String != "" {
|
||||
email, err := data.Decrypt(keys.emailKey, []byte(u.Email.String))
|
||||
email, err := data.Decrypt(keys.EmailKey, []byte(u.Email.String))
|
||||
if err != nil {
|
||||
log.Error("Error decrypting user email: %v", err)
|
||||
} else {
|
||||
|
|
|
@ -37,6 +37,7 @@ func (wfr wfResolver) FindUser(username string, host, requestHost string, r []we
|
|||
log.Error("Unable to get blog: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
c.hostName = wfr.cfg.App.Host
|
||||
if wfr.cfg.App.SingleUser {
|
||||
// Ensure handle matches user-chosen one on single-user blogs
|
||||
if username != c.Alias {
|
||||
|
|
Loading…
Reference in a new issue