Merge pull request #123 from writeas/private-instance

Private instances

Resolves T576
This commit is contained in:
Matt Baer 2019-07-01 19:14:20 -04:00 committed by GitHub
commit 7aaff778da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 249 additions and 81 deletions

1
app.go
View File

@ -312,6 +312,7 @@ func pageForReq(app *App, r *http.Request) page.StaticPage {
p.Username = u.Username
}
}
p.CanViewReader = !app.cfg.App.Private || u != nil
return p
}

View File

@ -76,7 +76,9 @@ type (
// Federation
Federation bool `ini:"federation"`
PublicStats bool `ini:"public_stats"`
Private bool `ini:"private"`
// Access
Private bool `ini:"private"`
// Additional functions
LocalTimeline bool `ini:"local_timeline"`

229
handle.go
View File

@ -23,24 +23,52 @@ import (
"github.com/gorilla/sessions"
"github.com/writeas/impart"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"github.com/writeas/writefreely/page"
)
// UserLevel represents the required user level for accessing an endpoint
type UserLevel int
const (
UserLevelNone UserLevel = iota // user or not -- ignored
UserLevelOptional // user or not -- object fetched if user
UserLevelNoneRequired // non-user (required)
UserLevelUser // user (required)
UserLevelNoneType UserLevel = iota // user or not -- ignored
UserLevelOptionalType // user or not -- object fetched if user
UserLevelNoneRequiredType // non-user (required)
UserLevelUserType // user (required)
)
func UserLevelNone(cfg *config.Config) UserLevel {
return UserLevelNoneType
}
func UserLevelOptional(cfg *config.Config) UserLevel {
return UserLevelOptionalType
}
func UserLevelNoneRequired(cfg *config.Config) UserLevel {
return UserLevelNoneRequiredType
}
func UserLevelUser(cfg *config.Config) UserLevel {
return UserLevelUserType
}
// UserLevelReader returns the permission level required for any route where
// users can read published content.
func UserLevelReader(cfg *config.Config) UserLevel {
if cfg.App.Private {
return UserLevelUserType
}
return UserLevelOptionalType
}
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
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)
UserLevelFunc func(cfg *config.Config) UserLevel
)
type Handler struct {
@ -209,22 +237,49 @@ func (h *Handler) AdminApper(f userApperHandlerFunc) http.HandlerFunc {
}
}
func apiAuth(app *App, r *http.Request) (*User, error) {
// Authorize user from Authorization header
t := r.Header.Get("Authorization")
if t == "" {
return nil, ErrNoAccessToken
}
u := &User{ID: app.db.GetUserID(t)}
if u.ID == -1 {
return nil, ErrBadAccessToken
}
return u, nil
}
// optionaAPIAuth is used for endpoints that accept authenticated requests via
// Authorization header or cookie, unlike apiAuth. It returns a different err
// in the case where no Authorization header is present.
func optionalAPIAuth(app *App, r *http.Request) (*User, error) {
// Authorize user from Authorization header
t := r.Header.Get("Authorization")
if t == "" {
return nil, ErrNotLoggedIn
}
u := &User{ID: app.db.GetUserID(t)}
if u.ID == -1 {
return nil, ErrBadAccessToken
}
return u, nil
}
func webAuth(app *App, r *http.Request) (*User, error) {
u := getUserSession(app, r)
if u == nil {
return nil, ErrNotLoggedIn
}
return u, nil
}
// 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) {
// Authorize user from Authorization header
t := r.Header.Get("Authorization")
if t == "" {
return nil, ErrNoAccessToken
}
u := &User{ID: app.db.GetUserID(t)}
if u.ID == -1 {
return nil, ErrBadAccessToken
}
return u, nil
})
return h.UserAll(false, f, apiAuth)
}
func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc {
@ -307,7 +362,7 @@ func (h *Handler) Page(n string) http.HandlerFunc {
}, UserLevelOptional)
}
func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
func (h *Handler) WebErrors(f handlerFunc, ul UserLevelFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO: factor out this logic shared with Web()
h.handleHTTPError(w, r, func() error {
@ -331,21 +386,21 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
var session *sessions.Session
var err error
if ul != UserLevelNone {
if ul(h.app.App().cfg) != UserLevelNoneType {
session, err = h.sessionStore.Get(r, cookieName)
if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
// Cookie is required, but we can ignore this error
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err)
}
_, gotUser := session.Values[cookieUserVal].(*User)
if ul == UserLevelNoneRequired && gotUser {
if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
to := correctPageFromLoginAttempt(r)
log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
err := impart.HTTPError{http.StatusFound, to}
status = err.Status
return err
} else if ul == UserLevelUser && !gotUser {
} else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
err := ErrNotLoggedIn
status = err.Status
@ -380,9 +435,25 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
}
}
func (h *Handler) CollectionPostOrStatic(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, ".") && !isRaw(r) {
start := time.Now()
status := 200
defer func() {
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
}()
// Serve static file
h.app.App().shttp.ServeHTTP(w, r)
return
}
h.Web(viewCollectionPost, UserLevelReader)(w, r)
}
// Web handles requests made in the web application. This provides user-
// friendly HTML pages and actions that work in the browser.
func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
func (h *Handler) Web(f handlerFunc, ul UserLevelFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleHTTPError(w, r, func() error {
var status int
@ -404,21 +475,21 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
}()
if ul != UserLevelNone {
if ul(h.app.App().cfg) != UserLevelNoneType {
session, err := h.sessionStore.Get(r, cookieName)
if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
// Cookie is required, but we can ignore this error
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err)
}
_, gotUser := session.Values[cookieUserVal].(*User)
if ul == UserLevelNoneRequired && gotUser {
if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
to := correctPageFromLoginAttempt(r)
log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
err := impart.HTTPError{http.StatusFound, to}
status = err.Status
return err
} else if ul == UserLevelUser && !gotUser {
} else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
err := ErrNotLoggedIn
status = err.Status
@ -478,7 +549,65 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc {
}
}
func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc {
func (h *Handler) AllReader(f handlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleError(w, r, func() error {
status := 200
start := time.Now()
defer func() {
if e := recover(); e != nil {
log.Error("%s:\n%s", e, debug.Stack())
impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
status = 500
}
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
}()
if h.app.App().cfg.App.Private {
// This instance is private, so ensure it's being accessed by a valid user
// Check if authenticated with an access token
_, apiErr := optionalAPIAuth(h.app.App(), r)
if apiErr != nil {
if err, ok := apiErr.(impart.HTTPError); ok {
status = err.Status
} else {
status = 500
}
if apiErr == ErrNotLoggedIn {
// Fall back to web auth since there was no access token given
_, err := webAuth(h.app.App(), r)
if err != nil {
if err, ok := apiErr.(impart.HTTPError); ok {
status = err.Status
} else {
status = 500
}
return err
}
} else {
return apiErr
}
}
}
err := f(h.app.App(), w, r)
if err != nil {
if err, ok := err.(impart.HTTPError); ok {
status = err.Status
} else {
status = 500
}
}
return err
}())
}
}
func (h *Handler) Download(f dataHandlerFunc, ul UserLevelFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleHTTPError(w, r, func() error {
var status int
@ -523,27 +652,27 @@ func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc {
}
}
func (h *Handler) Redirect(url string, ul UserLevel) http.HandlerFunc {
func (h *Handler) Redirect(url string, ul UserLevelFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleHTTPError(w, r, func() error {
start := time.Now()
var status int
if ul != UserLevelNone {
if ul(h.app.App().cfg) != UserLevelNoneType {
session, err := h.sessionStore.Get(r, cookieName)
if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
// Cookie is required, but we can ignore this error
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err)
}
_, gotUser := session.Values[cookieUserVal].(*User)
if ul == UserLevelNoneRequired && gotUser {
if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
to := correctPageFromLoginAttempt(r)
log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
err := impart.HTTPError{http.StatusFound, to}
status = err.Status
return err
} else if ul == UserLevelUser && !gotUser {
} else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
err := ErrNotLoggedIn
status = err.Status
@ -673,6 +802,34 @@ func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc {
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
}()
if h.app.App().cfg.App.Private {
// This instance is private, so ensure it's being accessed by a valid user
// Check if authenticated with an access token
_, apiErr := optionalAPIAuth(h.app.App(), r)
if apiErr != nil {
if err, ok := apiErr.(impart.HTTPError); ok {
status = err.Status
} else {
status = 500
}
if apiErr == ErrNotLoggedIn {
// Fall back to web auth since there was no access token given
_, err := webAuth(h.app.App(), r)
if err != nil {
if err, ok := apiErr.(impart.HTTPError); ok {
status = err.Status
} else {
status = 500
}
return err
}
} else {
return apiErr
}
}
}
f(w, r)
return nil

View File

@ -23,10 +23,11 @@ type StaticPage struct {
HeaderNav bool
// Request values
Path string
Username string
Values map[string]string
Flashes []string
Path string
Username string
Values map[string]string
Flashes []string
CanViewReader bool
}
// SanitizeHost alters the StaticPage to contain a real hostname. This is

View File

@ -266,6 +266,8 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
friendlyID := vars["post"]
// NOTE: until this is done better, be sure to keep this in parity with
// isRaw() and viewCollectionPost()
isJSON := strings.HasSuffix(friendlyID, ".json")
isXML := strings.HasSuffix(friendlyID, ".xml")
isCSS := strings.HasSuffix(friendlyID, ".css")
@ -587,7 +589,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
// Write success now
response := impart.WriteSuccess(w, newPost, http.StatusCreated)
if newPost.Collection != nil && app.cfg.App.Federation && !newPost.Created.After(time.Now()) {
if newPost.Collection != nil && !app.cfg.App.Private && app.cfg.App.Federation && !newPost.Created.After(time.Now()) {
go federatePost(app, newPost, newPost.Collection.ID, false)
}
@ -687,7 +689,7 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
if pRes.CollectionID.Valid {
coll, err := app.db.GetCollectionBy("id = ?", pRes.CollectionID.Int64)
if err == nil && app.cfg.App.Federation {
if err == nil && !app.cfg.App.Private && app.cfg.App.Federation {
coll.hostName = app.cfg.App.Host
pRes.Collection = &CollectionObj{Collection: *coll}
go federatePost(app, pRes, pRes.Collection.ID, true)
@ -828,7 +830,7 @@ func deletePost(app *App, w http.ResponseWriter, r *http.Request) error {
if t != nil {
t.Commit()
}
if coll != nil && app.cfg.App.Federation {
if coll != nil && !app.cfg.App.Private && app.cfg.App.Federation {
go deleteFederatedPost(app, pp, collID.Int64)
}
@ -872,7 +874,7 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
return err
}
if app.cfg.App.Federation {
if !app.cfg.App.Private && app.cfg.App.Federation {
for _, pRes := range *res {
if pRes.Code != http.StatusOK {
continue
@ -1193,21 +1195,29 @@ func getRawCollectionPost(app *App, slug, collAlias string) *RawPost {
}
}
func isRaw(r *http.Request) bool {
vars := mux.Vars(r)
slug := vars["slug"]
// NOTE: until this is done better, be sure to keep this in parity with
// isRaw in viewCollectionPost() and handleViewPost()
isJSON := strings.HasSuffix(slug, ".json")
isXML := strings.HasSuffix(slug, ".xml")
isMarkdown := strings.HasSuffix(slug, ".md")
return strings.HasSuffix(slug, ".txt") || isJSON || isXML || isMarkdown
}
func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
slug := vars["slug"]
// NOTE: until this is done better, be sure to keep this in parity with
// isRaw() and handleViewPost()
isJSON := strings.HasSuffix(slug, ".json")
isXML := strings.HasSuffix(slug, ".xml")
isMarkdown := strings.HasSuffix(slug, ".md")
isRaw := strings.HasSuffix(slug, ".txt") || isJSON || isXML || isMarkdown
if strings.Contains(r.URL.Path, ".") && !isRaw {
// Serve static file
app.shttp.ServeHTTP(w, r)
return nil
}
cr := &collectionReq{}
err := processCollectionRequest(cr, vars, w, r)
if err != nil {

View File

@ -60,7 +60,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
// Federation endpoints
// host-meta
write.HandleFunc("/.well-known/host-meta", handler.Web(handleViewHostMeta, UserLevelOptional))
write.HandleFunc("/.well-known/host-meta", handler.Web(handleViewHostMeta, UserLevelReader))
// webfinger
write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger)))
// nodeinfo
@ -112,28 +112,28 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
// Handle collections
write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST")
apiColls := write.PathPrefix("/api/collections/").Subrouter()
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(fetchCollection)).Methods("GET")
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.AllReader(fetchCollection)).Methods("GET")
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE")
apiColls.HandleFunc("/{alias}/posts", handler.All(fetchCollectionPosts)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts", handler.AllReader(fetchCollectionPosts)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts", handler.All(newPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/posts/{post}", handler.All(fetchPost)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts/{post}", handler.AllReader(fetchPost)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/posts/{post}/{property}", handler.All(fetchPostProperty)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts/{post}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET")
apiColls.HandleFunc("/{alias}/collect", handler.All(addPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/pin", handler.All(pinPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/unpin", handler.All(pinPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/inbox", handler.All(handleFetchCollectionInbox)).Methods("POST")
apiColls.HandleFunc("/{alias}/outbox", handler.All(handleFetchCollectionOutbox)).Methods("GET")
apiColls.HandleFunc("/{alias}/following", handler.All(handleFetchCollectionFollowing)).Methods("GET")
apiColls.HandleFunc("/{alias}/followers", handler.All(handleFetchCollectionFollowers)).Methods("GET")
apiColls.HandleFunc("/{alias}/outbox", handler.AllReader(handleFetchCollectionOutbox)).Methods("GET")
apiColls.HandleFunc("/{alias}/following", handler.AllReader(handleFetchCollectionFollowing)).Methods("GET")
apiColls.HandleFunc("/{alias}/followers", handler.AllReader(handleFetchCollectionFollowers)).Methods("GET")
// Handle posts
write.HandleFunc("/api/posts", handler.All(newPost)).Methods("POST")
posts := write.PathPrefix("/api/posts/").Subrouter()
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(fetchPost)).Methods("GET")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.AllReader(fetchPost)).Methods("GET")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST", "PUT")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(deletePost)).Methods("DELETE")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}/{property}", handler.All(fetchPostProperty)).Methods("GET")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET")
posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST")
posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST")
@ -152,11 +152,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))
write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelNoneRequired)).Methods("GET")
// TODO: show a reader-specific 404 page if the function is disabled
// TODO: change this based on configuration for either public or private-to-this-instance
readPerm := UserLevelOptional
write.HandleFunc("/read", handler.Web(viewLocalTimeline, readPerm))
RouteRead(handler, readPerm, write.PathPrefix("/read").Subrouter())
write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader))
RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter())
draftEditPrefix := ""
if apper.App().cfg.App.SingleUser {
@ -173,8 +170,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
if apper.App().cfg.App.SingleUser {
RouteCollections(handler, write.PathPrefix("/").Subrouter())
} else {
write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional))
write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelOptional))
write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelReader))
write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelReader))
RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter())
// Posts
}
@ -184,19 +181,19 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
}
func RouteCollections(handler *Handler, r *mux.Router) {
r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelOptional))
r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional))
r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelOptional))
r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional))
r.HandleFunc("/sitemap.xml", handler.All(handleViewSitemap))
r.HandleFunc("/feed/", handler.All(ViewFeed))
r.HandleFunc("/{slug}", handler.Web(viewCollectionPost, UserLevelOptional))
r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader))
r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader))
r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap))
r.HandleFunc("/feed/", handler.AllReader(ViewFeed))
r.HandleFunc("/{slug}", handler.CollectionPostOrStatic)
r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser))
r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser))
r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelOptional)).Methods("GET")
r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelReader)).Methods("GET")
}
func RouteRead(handler *Handler, readPerm UserLevel, r *mux.Router) {
func RouteRead(handler *Handler, readPerm UserLevelFunc, r *mux.Router) {
r.HandleFunc("/api/posts", handler.Web(viewLocalTimelineAPI, readPerm))
r.HandleFunc("/p/{page}", handler.Web(viewLocalTimeline, readPerm))
r.HandleFunc("/feed/", handler.Web(viewLocalTimelineFeed, readPerm))

View File

@ -18,7 +18,7 @@
<nav id="user-nav">
<nav class="tabs">
<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a>
{{if and (not .SingleUser) .LocalTimeline}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}}
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}}
{{if and (not .SingleUser) (not .Username)}}<a href="/login"{{if eq .Path "/login"}} class="selected"{{end}}>Log in</a>{{end}}
</nav>
</nav>

View File

@ -16,7 +16,7 @@
<h3><a class="home" href="/">{{.SiteName}}</a></h3>
<ul>
<li><a href="/about">about</a></li>
{{if and (not .SingleUser) .LocalTimeline}}<a href="/read">reader</a>{{end}}
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read">reader</a>{{end}}
<li><a href="/privacy">privacy</a></li>
</ul>
</div>

View File

@ -44,7 +44,7 @@
<label><input type="radio" name="visibility" id="visibility-unlisted" value="0" {{if .IsUnlisted}}checked="checked"{{end}} />
Unlisted
</label>
<p>This blog is visible to anyone with its link.</p>
<p>This blog is visible to {{if .Private}}any registered user on this instance{{else}}anyone with its link{{end}}.</p>
</li>
<li>
<label class="option-text"><input type="radio" name="visibility" id="visibility-private" value="2" {{if .IsPrivate}}checked="checked"{{end}} />
@ -62,7 +62,7 @@
<label class="option-text{{if not .LocalTimeline}} disabled{{end}}"><input type="radio" name="visibility" id="visibility-public" value="1" {{if .IsPublic}}checked="checked"{{end}} {{if not .LocalTimeline}}disabled="disabled"{{end}} />
Public
</label>
{{if .LocalTimeline}}<p>This blog is displayed on the public <a href="/read">reader</a>, and to anyone with its link.</p>
{{if .LocalTimeline}}<p>This blog is displayed on the public <a href="/read">reader</a>, and is visible to {{if .Private}}any registered user on this instance{{else}}anyone with its link{{end}}.</p>
{{else}}<p>The public reader is currently turned off for this community.</p>{{end}}
</li>
</ul>