aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--locale/translations.go8
-rw-r--r--locale/translations/fr_FR.json4
-rw-r--r--model/session.go56
-rw-r--r--model/token.go17
-rw-r--r--server/cookie/cookie.go40
-rw-r--r--server/core/context.go87
-rw-r--r--server/core/handler.go2
-rw-r--r--server/middleware/context_keys.go34
-rw-r--r--server/middleware/session.go68
-rw-r--r--server/middleware/token.go81
-rw-r--r--server/middleware/user_session.go78
-rw-r--r--server/routes.go4
-rw-r--r--server/template/common.go10
-rw-r--r--server/template/html/common/layout.html6
-rw-r--r--server/ui/controller/controller.go9
-rw-r--r--server/ui/controller/login.go33
-rw-r--r--server/ui/controller/oauth2.go22
-rw-r--r--server/ui/controller/session.go7
-rw-r--r--server/ui/controller/settings.go1
-rw-r--r--server/ui/controller/unread.go2
-rw-r--r--sql/schema_version_10.sql8
-rw-r--r--sql/sql.go29
-rw-r--r--storage/migration.go2
-rw-r--r--storage/session.go77
-rw-r--r--storage/token.go48
-rw-r--r--storage/user_session.go6
26 files changed, 464 insertions, 275 deletions
diff --git a/locale/translations.go b/locale/translations.go
index 64fb490..8f5f4ed 100644
--- a/locale/translations.go
+++ b/locale/translations.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-15 18:49:24.054372873 -0800 PST m=+0.026968745
+// 2017-12-16 17:48:32.323083386 -0800 PST m=+0.056720065
package locale
@@ -169,12 +169,14 @@ var translations = map[string]string{
"Fever Password": "Mot de passe pour l'API de Fever",
"Fetch original content": "Récupérer le contenu original",
"Scraper Rules": "Règles pour récupérer le contenu original",
- "Rewrite Rules": "Règles de réécriture"
+ "Rewrite Rules": "Règles de réécriture",
+ "Preferences saved!": "Préférences sauvegardées !",
+ "Your external account is now linked !": "Votre compte externe est maintenant associé !"
}
`,
}
var translationsChecksums = map[string]string{
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
- "fr_FR": "0e14d65f38ca5c5e34f1d84f6837ce8a29a4ae5f8836b384bb098222b724cb5b",
+ "fr_FR": "f52a6503ee61d1103adb280c242d438a89936b34d147d29c2502cec8b2cc9ff9",
}
diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json
index a7c34f9..5015f2a 100644
--- a/locale/translations/fr_FR.json
+++ b/locale/translations/fr_FR.json
@@ -153,5 +153,7 @@
"Fever Password": "Mot de passe pour l'API de Fever",
"Fetch original content": "Récupérer le contenu original",
"Scraper Rules": "Règles pour récupérer le contenu original",
- "Rewrite Rules": "Règles de réécriture"
+ "Rewrite Rules": "Règles de réécriture",
+ "Preferences saved!": "Préférences sauvegardées !",
+ "Your external account is now linked !": "Votre compte externe est maintenant associé !"
}
diff --git a/model/session.go b/model/session.go
new file mode 100644
index 0000000..2a7430d
--- /dev/null
+++ b/model/session.go
@@ -0,0 +1,56 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package model
+
+import (
+ "database/sql/driver"
+ "encoding/json"
+ "errors"
+ "fmt"
+)
+
+// SessionData represents the data attached to the session.
+type SessionData struct {
+ CSRF string `json:"csrf"`
+ OAuth2State string `json:"oauth2_state"`
+ FlashMessage string `json:"flash_message"`
+ FlashErrorMessage string `json:"flash_error_message"`
+}
+
+func (s SessionData) String() string {
+ return fmt.Sprintf(`CSRF="%s", "OAuth2State="%s", FlashMessage="%s", "FlashErrorMessage="%s"`,
+ s.CSRF, s.OAuth2State, s.FlashMessage, s.FlashErrorMessage)
+}
+
+// Value converts the session data to JSON.
+func (s SessionData) Value() (driver.Value, error) {
+ j, err := json.Marshal(s)
+ return j, err
+}
+
+// Scan converts raw JSON data.
+func (s *SessionData) Scan(src interface{}) error {
+ source, ok := src.([]byte)
+ if !ok {
+ return errors.New("session: unable to assert type of src")
+ }
+
+ err := json.Unmarshal(source, s)
+ if err != nil {
+ return fmt.Errorf("session: %v", err)
+ }
+
+ return err
+}
+
+// Session represents a session in the system.
+type Session struct {
+ ID string
+ Data *SessionData
+}
+
+func (s *Session) String() string {
+ return fmt.Sprintf(`ID="%s", Data="%v"`, s.ID, s.Data)
+}
diff --git a/model/token.go b/model/token.go
deleted file mode 100644
index 3c5c323..0000000
--- a/model/token.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package model
-
-import "fmt"
-
-// Token represents a CSRF token in the system.
-type Token struct {
- ID string
- Value string
-}
-
-func (t Token) String() string {
- return fmt.Sprintf(`ID="%s"`, t.ID)
-}
diff --git a/server/cookie/cookie.go b/server/cookie/cookie.go
new file mode 100644
index 0000000..d028d87
--- /dev/null
+++ b/server/cookie/cookie.go
@@ -0,0 +1,40 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package cookie
+
+import (
+ "net/http"
+ "time"
+)
+
+// Cookie names.
+const (
+ CookieSessionID = "sessionID"
+ CookieUserSessionID = "userSessionID"
+)
+
+// New create a new cookie.
+func New(name, value string, isHTTPS bool) *http.Cookie {
+ return &http.Cookie{
+ Name: name,
+ Value: value,
+ Path: "/",
+ Secure: isHTTPS,
+ HttpOnly: true,
+ }
+}
+
+// Expired returns an expired cookie.
+func Expired(name string, isHTTPS bool) *http.Cookie {
+ return &http.Cookie{
+ Name: name,
+ Value: "",
+ Path: "/",
+ Secure: isHTTPS,
+ HttpOnly: true,
+ MaxAge: -1,
+ Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
+ }
+}
diff --git a/server/core/context.go b/server/core/context.go
index 50496b3..dfd8e5b 100644
--- a/server/core/context.go
+++ b/server/core/context.go
@@ -7,6 +7,8 @@ package core
import (
"net/http"
+ "github.com/miniflux/miniflux/helper"
+ "github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/server/middleware"
@@ -18,11 +20,12 @@ import (
// Context contains helper functions related to the current request.
type Context struct {
- writer http.ResponseWriter
- request *http.Request
- store *storage.Storage
- router *mux.Router
- user *model.User
+ writer http.ResponseWriter
+ request *http.Request
+ store *storage.Storage
+ router *mux.Router
+ user *model.User
+ translator *locale.Translator
}
// IsAdminUser checks if the logged user is administrator.
@@ -35,10 +38,11 @@ func (c *Context) IsAdminUser() bool {
// UserTimezone returns the timezone used by the logged user.
func (c *Context) UserTimezone() string {
- if v := c.request.Context().Value(middleware.UserTimezoneContextKey); v != nil {
- return v.(string)
+ value := c.getContextStringValue(middleware.UserTimezoneContextKey)
+ if value == "" {
+ value = "UTC"
}
- return "UTC"
+ return value
}
// IsAuthenticated returns a boolean if the user is authenticated.
@@ -80,13 +84,68 @@ func (c *Context) UserLanguage() string {
return user.Language
}
-// CsrfToken returns the current CSRF token.
-func (c *Context) CsrfToken() string {
- if v := c.request.Context().Value(middleware.TokenContextKey); v != nil {
+// Translate translates a message in the current language.
+func (c *Context) Translate(message string, args ...interface{}) string {
+ return c.translator.GetLanguage(c.UserLanguage()).Get(message, args...)
+}
+
+// CSRF returns the current CSRF token.
+func (c *Context) CSRF() string {
+ return c.getContextStringValue(middleware.CSRFContextKey)
+}
+
+// SessionID returns the current session ID.
+func (c *Context) SessionID() string {
+ return c.getContextStringValue(middleware.SessionIDContextKey)
+}
+
+// UserSessionToken returns the current user session token.
+func (c *Context) UserSessionToken() string {
+ return c.getContextStringValue(middleware.UserSessionTokenContextKey)
+}
+
+// OAuth2State returns the current OAuth2 state.
+func (c *Context) OAuth2State() string {
+ return c.getContextStringValue(middleware.OAuth2StateContextKey)
+}
+
+// GenerateOAuth2State generate a new OAuth2 state.
+func (c *Context) GenerateOAuth2State() string {
+ state := helper.GenerateRandomString(32)
+ c.store.UpdateSessionField(c.SessionID(), "oauth2_state", state)
+ return state
+}
+
+// SetFlashMessage defines a new flash message.
+func (c *Context) SetFlashMessage(message string) {
+ c.store.UpdateSessionField(c.SessionID(), "flash_message", message)
+}
+
+// FlashMessage returns the flash message and remove it.
+func (c *Context) FlashMessage() string {
+ message := c.getContextStringValue(middleware.FlashMessageContextKey)
+ c.store.UpdateSessionField(c.SessionID(), "flash_message", "")
+ return message
+}
+
+// SetFlashErrorMessage defines a new flash error message.
+func (c *Context) SetFlashErrorMessage(message string) {
+ c.store.UpdateSessionField(c.SessionID(), "flash_error_message", message)
+}
+
+// FlashErrorMessage returns the error flash message and remove it.
+func (c *Context) FlashErrorMessage() string {
+ message := c.getContextStringValue(middleware.FlashMessageContextKey)
+ c.store.UpdateSessionField(c.SessionID(), "flash_error_message", "")
+ return message
+}
+
+func (c *Context) getContextStringValue(key *middleware.ContextKey) string {
+ if v := c.request.Context().Value(key); v != nil {
return v.(string)
}
- logger.Error("No CSRF token in context!")
+ logger.Error("[Core:Context] Missing key: %s", key)
return ""
}
@@ -96,6 +155,6 @@ func (c *Context) Route(name string, args ...interface{}) string {
}
// NewContext creates a new Context.
-func NewContext(w http.ResponseWriter, r *http.Request, store *storage.Storage, router *mux.Router) *Context {
- return &Context{writer: w, request: r, store: store, router: router}
+func NewContext(w http.ResponseWriter, r *http.Request, store *storage.Storage, router *mux.Router, translator *locale.Translator) *Context {
+ return &Context{writer: w, request: r, store: store, router: router, translator: translator}
}
diff --git a/server/core/handler.go b/server/core/handler.go
index c55cf79..a5d84af 100644
--- a/server/core/handler.go
+++ b/server/core/handler.go
@@ -36,7 +36,7 @@ func (h *Handler) Use(f HandlerFunc) http.Handler {
defer helper.ExecutionTime(time.Now(), r.URL.Path)
logger.Debug("[HTTP] %s %s", r.Method, r.URL.Path)
- ctx := NewContext(w, r, h.store, h.router)
+ ctx := NewContext(w, r, h.store, h.router, h.translator)
request := NewRequest(w, r)
response := NewResponse(w, r, h.template)
diff --git a/server/middleware/context_keys.go b/server/middleware/context_keys.go
index 3099322..31ad286 100644
--- a/server/middleware/context_keys.go
+++ b/server/middleware/context_keys.go
@@ -4,23 +4,43 @@
package middleware
-type contextKey struct {
+// ContextKey represents a context key.
+type ContextKey struct {
name string
}
+func (c ContextKey) String() string {
+ return c.name
+}
+
var (
// UserIDContextKey is the context key used to store the user ID.
- UserIDContextKey = &contextKey{"UserID"}
+ UserIDContextKey = &ContextKey{"UserID"}
// UserTimezoneContextKey is the context key used to store the user timezone.
- UserTimezoneContextKey = &contextKey{"UserTimezone"}
+ UserTimezoneContextKey = &ContextKey{"UserTimezone"}
// IsAdminUserContextKey is the context key used to store the user role.
- IsAdminUserContextKey = &contextKey{"IsAdminUser"}
+ IsAdminUserContextKey = &ContextKey{"IsAdminUser"}
// IsAuthenticatedContextKey is the context key used to store the authentication flag.
- IsAuthenticatedContextKey = &contextKey{"IsAuthenticated"}
+ IsAuthenticatedContextKey = &ContextKey{"IsAuthenticated"}
+
+ // UserSessionTokenContextKey is the context key used to store the user session ID.
+ UserSessionTokenContextKey = &ContextKey{"UserSessionToken"}
+
+ // SessionIDContextKey is the context key used to store the session ID.
+ SessionIDContextKey = &ContextKey{"SessionID"}
+
+ // CSRFContextKey is the context key used to store CSRF token.
+ CSRFContextKey = &ContextKey{"CSRF"}
+
+ // OAuth2StateContextKey is the context key used to store OAuth2 state.
+ OAuth2StateContextKey = &ContextKey{"OAuth2State"}
+
+ // FlashMessageContextKey is the context key used to store a flash message.
+ FlashMessageContextKey = &ContextKey{"FlashMessage"}
- // TokenContextKey is the context key used to store CSRF token.
- TokenContextKey = &contextKey{"CSRF"}
+ // FlashErrorMessageContextKey is the context key used to store a flash error message.
+ FlashErrorMessageContextKey = &ContextKey{"FlashErrorMessage"}
)
diff --git a/server/middleware/session.go b/server/middleware/session.go
index 3759565..2891b68 100644
--- a/server/middleware/session.go
+++ b/server/middleware/session.go
@@ -10,60 +10,66 @@ import (
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/route"
+ "github.com/miniflux/miniflux/server/cookie"
"github.com/miniflux/miniflux/storage"
-
- "github.com/gorilla/mux"
)
// SessionMiddleware represents a session middleware.
type SessionMiddleware struct {
- store *storage.Storage
- router *mux.Router
+ store *storage.Storage
}
// Handler execute the middleware.
-func (s *SessionMiddleware) Handler(next http.Handler) http.Handler {
+func (t *SessionMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- session := s.getSessionFromCookie(r)
+ var err error
+ session := t.getSessionValueFromCookie(r)
if session == nil {
logger.Debug("[Middleware:Session] Session not found")
- if s.isPublicRoute(r) {
- next.ServeHTTP(w, r)
- } else {
- http.Redirect(w, r, route.Path(s.router, "login"), http.StatusFound)
+ session, err = t.store.CreateSession()
+ if err != nil {
+ logger.Error("[Middleware:Session] %v", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
}
+
+ http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, r.URL.Scheme == "https"))
} else {
logger.Debug("[Middleware:Session] %s", session)
- ctx := r.Context()
- ctx = context.WithValue(ctx, UserIDContextKey, session.UserID)
- ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
+ }
- next.ServeHTTP(w, r.WithContext(ctx))
+ if r.Method == "POST" {
+ formValue := r.FormValue("csrf")
+ headerValue := r.Header.Get("X-Csrf-Token")
+
+ if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
+ logger.Error(`[Middleware:Session] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid or missing CSRF session!"))
+ return
+ }
}
- })
-}
-func (s *SessionMiddleware) isPublicRoute(r *http.Request) bool {
- route := mux.CurrentRoute(r)
- switch route.GetName() {
- case "login", "checkLogin", "stylesheet", "javascript", "oauth2Redirect", "oauth2Callback", "appIcon", "favicon":
- return true
- default:
- return false
- }
+ ctx := r.Context()
+ ctx = context.WithValue(ctx, SessionIDContextKey, session.ID)
+ ctx = context.WithValue(ctx, CSRFContextKey, session.Data.CSRF)
+ ctx = context.WithValue(ctx, OAuth2StateContextKey, session.Data.OAuth2State)
+ ctx = context.WithValue(ctx, FlashMessageContextKey, session.Data.FlashMessage)
+ ctx = context.WithValue(ctx, FlashErrorMessageContextKey, session.Data.FlashErrorMessage)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
}
-func (s *SessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSession {
- sessionCookie, err := r.Cookie("sessionID")
+func (t *SessionMiddleware) getSessionValueFromCookie(r *http.Request) *model.Session {
+ sessionCookie, err := r.Cookie(cookie.CookieSessionID)
if err == http.ErrNoCookie {
return nil
}
- session, err := s.store.UserSessionByToken(sessionCookie.Value)
+ session, err := t.store.Session(sessionCookie.Value)
if err != nil {
- logger.Error("[SessionMiddleware] %v", err)
+ logger.Error("[Middleware:Session] %v", err)
return nil
}
@@ -71,6 +77,6 @@ func (s *SessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSes
}
// NewSessionMiddleware returns a new SessionMiddleware.
-func NewSessionMiddleware(s *storage.Storage, r *mux.Router) *SessionMiddleware {
- return &SessionMiddleware{store: s, router: r}
+func NewSessionMiddleware(s *storage.Storage) *SessionMiddleware {
+ return &SessionMiddleware{store: s}
}
diff --git a/server/middleware/token.go b/server/middleware/token.go
deleted file mode 100644
index e1666f7..0000000
--- a/server/middleware/token.go
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package middleware
-
-import (
- "context"
- "net/http"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/storage"
-)
-
-// TokenMiddleware represents a token middleware.
-type TokenMiddleware struct {
- store *storage.Storage
-}
-
-// Handler execute the middleware.
-func (t *TokenMiddleware) Handler(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- var err error
- token := t.getTokenValueFromCookie(r)
-
- if token == nil {
- logger.Debug("[Middleware:Token] Token not found")
- token, err = t.store.CreateToken()
- if err != nil {
- logger.Error("[Middleware:Token] %v", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- cookie := &http.Cookie{
- Name: "tokenID",
- Value: token.ID,
- Path: "/",
- Secure: r.URL.Scheme == "https",
- HttpOnly: true,
- }
-
- http.SetCookie(w, cookie)
- } else {
- logger.Info("[Middleware:Token] %s", token)
- }
-
- isTokenValid := token.Value == r.FormValue("csrf") || token.Value == r.Header.Get("X-Csrf-Token")
-
- if r.Method == "POST" && !isTokenValid {
- logger.Error("[Middleware:CSRF] Invalid or missing CSRF token!")
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Invalid or missing CSRF token!"))
- } else {
- ctx := r.Context()
- ctx = context.WithValue(ctx, TokenContextKey, token.Value)
- next.ServeHTTP(w, r.WithContext(ctx))
- }
- })
-}
-
-func (t *TokenMiddleware) getTokenValueFromCookie(r *http.Request) *model.Token {
- tokenCookie, err := r.Cookie("tokenID")
- if err == http.ErrNoCookie {
- return nil
- }
-
- token, err := t.store.Token(tokenCookie.Value)
- if err != nil {
- logger.Error("[Middleware:Token] %v", err)
- return nil
- }
-
- return token
-}
-
-// NewTokenMiddleware returns a new TokenMiddleware.
-func NewTokenMiddleware(s *storage.Storage) *TokenMiddleware {
- return &TokenMiddleware{store: s}
-}
diff --git a/server/middleware/user_session.go b/server/middleware/user_session.go
new file mode 100644
index 0000000..e9bad82
--- /dev/null
+++ b/server/middleware/user_session.go
@@ -0,0 +1,78 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package middleware
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/server/cookie"
+ "github.com/miniflux/miniflux/server/route"
+ "github.com/miniflux/miniflux/storage"
+
+ "github.com/gorilla/mux"
+)
+
+// UserSessionMiddleware represents a user session middleware.
+type UserSessionMiddleware struct {
+ store *storage.Storage
+ router *mux.Router
+}
+
+// Handler execute the middleware.
+func (s *UserSessionMiddleware) Handler(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ session := s.getSessionFromCookie(r)
+
+ if session == nil {
+ logger.Debug("[Middleware:UserSession] Session not found")
+ if s.isPublicRoute(r) {
+ next.ServeHTTP(w, r)
+ } else {
+ http.Redirect(w, r, route.Path(s.router, "login"), http.StatusFound)
+ }
+ } else {
+ logger.Debug("[Middleware:UserSession] %s", session)
+ ctx := r.Context()
+ ctx = context.WithValue(ctx, UserIDContextKey, session.UserID)
+ ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
+ ctx = context.WithValue(ctx, UserSessionTokenContextKey, session.Token)
+
+ next.ServeHTTP(w, r.WithContext(ctx))
+ }
+ })
+}
+
+func (s *UserSessionMiddleware) isPublicRoute(r *http.Request) bool {
+ route := mux.CurrentRoute(r)
+ switch route.GetName() {
+ case "login", "checkLogin", "stylesheet", "javascript", "oauth2Redirect", "oauth2Callback", "appIcon", "favicon":
+ return true
+ default:
+ return false
+ }
+}
+
+func (s *UserSessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSession {
+ sessionCookie, err := r.Cookie(cookie.CookieUserSessionID)
+ if err == http.ErrNoCookie {
+ return nil
+ }
+
+ session, err := s.store.UserSessionByToken(sessionCookie.Value)
+ if err != nil {
+ logger.Error("[Middleware:UserSession] %v", err)
+ return nil
+ }
+
+ return session
+}
+
+// NewUserSessionMiddleware returns a new UserSessionMiddleware.
+func NewUserSessionMiddleware(s *storage.Storage, r *mux.Router) *UserSessionMiddleware {
+ return &UserSessionMiddleware{store: s, router: r}
+}
diff --git a/server/routes.go b/server/routes.go
index d564925..8aa849e 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -42,8 +42,8 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han
))
uiHandler := core.NewHandler(store, router, templateEngine, translator, middleware.NewChain(
- middleware.NewSessionMiddleware(store, router).Handler,
- middleware.NewTokenMiddleware(store).Handler,
+ middleware.NewUserSessionMiddleware(store, router).Handler,
+ middleware.NewSessionMiddleware(store).Handler,
))
router.Handle("/fever/", feverHandler.Use(feverController.Handler))
diff --git a/server/template/common.go b/server/template/common.go
index 555db16..6e7b8b9 100644
--- a/server/template/common.go
+++ b/server/template/common.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-15 21:24:38.377969493 -0800 PST m=+0.007061903
+// 2017-12-16 17:48:32.321995978 -0800 PST m=+0.055632657
package template
@@ -88,6 +88,12 @@ var templateCommonMap = map[string]string{
</nav>
</header>
{{ end }}
+ {{ if .flashMessage }}
+ <div class="flash-message alert alert-success">{{ .flashMessage }}</div>
+ {{ end }}
+ {{ if .flashErrorMessage }}
+ <div class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
+ {{ end }}
<main>
{{template "content" .}}
</main>
@@ -118,6 +124,6 @@ var templateCommonMap = map[string]string{
var templateCommonMapChecksums = map[string]string{
"entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27",
- "layout": "100d1ffff506b9cdd4c28233ff883c323452ea01fa224ff891d4ad69997b62b1",
+ "layout": "ff5e3d87a48e4d3aeceda4aabe6c2c2f607006c6b6e83dfcab6c5eb255a1e6f2",
"pagination": "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924",
}
diff --git a/server/template/html/common/layout.html b/server/template/html/common/layout.html
index 1675c6c..bd7836d 100644
--- a/server/template/html/common/layout.html
+++ b/server/template/html/common/layout.html
@@ -63,6 +63,12 @@
</nav>
</header>
{{ end }}
+ {{ if .flashMessage }}
+ <div class="flash-message alert alert-success">{{ .flashMessage }}</div>
+ {{ end }}
+ {{ if .flashErrorMessage }}
+ <div class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
+ {{ end }}
<main>
{{template "content" .}}
</main>
diff --git a/server/ui/controller/controller.go b/server/ui/controller/controller.go
index cfecf42..8f1f415 100644
--- a/server/ui/controller/controller.go
+++ b/server/ui/controller/controller.go
@@ -44,10 +44,11 @@ func (c *Controller) getCommonTemplateArgs(ctx *core.Context) (tplParams, error)
}
params := tplParams{
- "menu": "",
- "user": user,
- "countUnread": countUnread,
- "csrf": ctx.CsrfToken(),
+ "menu": "",
+ "user": user,
+ "countUnread": countUnread,
+ "csrf": ctx.CSRF(),
+ "flashMessage": ctx.FlashMessage(),
}
return params, nil
}
diff --git a/server/ui/controller/login.go b/server/ui/controller/login.go
index d130f4c..87b8a4e 100644
--- a/server/ui/controller/login.go
+++ b/server/ui/controller/login.go
@@ -5,10 +5,8 @@
package controller
import (
- "net/http"
- "time"
-
"github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/server/cookie"
"github.com/miniflux/miniflux/server/core"
"github.com/miniflux/miniflux/server/ui/form"
@@ -23,7 +21,7 @@ func (c *Controller) ShowLoginPage(ctx *core.Context, request *core.Request, res
}
response.HTML().Render("login", tplParams{
- "csrf": ctx.CsrfToken(),
+ "csrf": ctx.CSRF(),
})
}
@@ -32,7 +30,7 @@ func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, respon
authForm := form.NewAuthForm(request.Request())
tplParams := tplParams{
"errorMessage": "Invalid username or password.",
- "csrf": ctx.CsrfToken(),
+ "csrf": ctx.CSRF(),
}
if err := authForm.Validate(); err != nil {
@@ -60,15 +58,7 @@ func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, respon
logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
- cookie := &http.Cookie{
- Name: "sessionID",
- Value: sessionToken,
- Path: "/",
- Secure: request.IsHTTPS(),
- HttpOnly: true,
- }
-
- response.SetCookie(cookie)
+ response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, request.IsHTTPS()))
response.Redirect(ctx.Route("unread"))
}
@@ -76,21 +66,10 @@ func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, respon
func (c *Controller) Logout(ctx *core.Context, request *core.Request, response *core.Response) {
user := ctx.LoggedUser()
- sessionCookie := request.Cookie("sessionID")
- if err := c.store.RemoveUserSessionByToken(user.ID, sessionCookie); err != nil {
+ if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
logger.Error("[Controller:Logout] %v", err)
}
- cookie := &http.Cookie{
- Name: "sessionID",
- Value: "",
- Path: "/",
- Secure: request.IsHTTPS(),
- HttpOnly: true,
- MaxAge: -1,
- Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
- }
-
- response.SetCookie(cookie)
+ response.SetCookie(cookie.Expired(cookie.CookieUserSessionID, request.IsHTTPS()))
response.Redirect(ctx.Route("login"))
}
diff --git a/server/ui/controller/oauth2.go b/server/ui/controller/oauth2.go
index 56ed53c..5011b1a 100644
--- a/server/ui/controller/oauth2.go
+++ b/server/ui/controller/oauth2.go
@@ -5,11 +5,10 @@
package controller
import (
- "net/http"
-
"github.com/miniflux/miniflux/config"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/server/cookie"
"github.com/miniflux/miniflux/server/core"
"github.com/miniflux/miniflux/server/oauth2"
"github.com/tomasen/realip"
@@ -19,7 +18,7 @@ import (
func (c *Controller) OAuth2Redirect(ctx *core.Context, request *core.Request, response *core.Response) {
provider := request.StringParam("provider", "")
if provider == "" {
- logger.Error("[OAuth2] Invalid or missing provider")
+ logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
response.Redirect(ctx.Route("login"))
return
}
@@ -31,7 +30,7 @@ func (c *Controller) OAuth2Redirect(ctx *core.Context, request *core.Request, re
return
}
- response.Redirect(authProvider.GetRedirectURL(ctx.CsrfToken()))
+ response.Redirect(authProvider.GetRedirectURL(ctx.GenerateOAuth2State()))
}
// OAuth2Callback receives the authorization code and create a new session.
@@ -51,8 +50,8 @@ func (c *Controller) OAuth2Callback(ctx *core.Context, request *core.Request, re
}
state := request.QueryStringParam("state", "")
- if state != ctx.CsrfToken() {
- logger.Error("[OAuth2] Invalid state value")
+ if state == "" || state != ctx.OAuth2State() {
+ logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, ctx.OAuth2State())
response.Redirect(ctx.Route("login"))
return
}
@@ -78,6 +77,7 @@ func (c *Controller) OAuth2Callback(ctx *core.Context, request *core.Request, re
return
}
+ ctx.SetFlashMessage(ctx.Translate("Your external account is now linked !"))
response.Redirect(ctx.Route("settings"))
return
}
@@ -118,15 +118,7 @@ func (c *Controller) OAuth2Callback(ctx *core.Context, request *core.Request, re
logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username)
- cookie := &http.Cookie{
- Name: "sessionID",
- Value: sessionToken,
- Path: "/",
- Secure: request.IsHTTPS(),
- HttpOnly: true,
- }
-
- response.SetCookie(cookie)
+ response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, request.IsHTTPS()))
response.Redirect(ctx.Route("unread"))
}
diff --git a/server/ui/controller/session.go b/server/ui/controller/session.go
index a020b16..05cb29e 100644
--- a/server/ui/controller/session.go
+++ b/server/ui/controller/session.go
@@ -9,7 +9,7 @@ import (
"github.com/miniflux/miniflux/server/core"
)
-// ShowSessions shows the list of active sessions.
+// ShowSessions shows the list of active user sessions.
func (c *Controller) ShowSessions(ctx *core.Context, request *core.Request, response *core.Response) {
user := ctx.LoggedUser()
args, err := c.getCommonTemplateArgs(ctx)
@@ -24,15 +24,14 @@ func (c *Controller) ShowSessions(ctx *core.Context, request *core.Request, resp
return
}
- sessionCookie := request.Cookie("sessionID")
response.HTML().Render("sessions", args.Merge(tplParams{
"sessions": sessions,
- "currentSessionToken": sessionCookie,
+ "currentSessionToken": ctx.UserSessionToken(),
"menu": "settings",
}))
}
-// RemoveSession remove a session.
+// RemoveSession remove a user session.
func (c *Controller) RemoveSession(ctx *core.Context, request *core.Request, response *core.Response) {
user := ctx.LoggedUser()
diff --git a/server/ui/controller/settings.go b/server/ui/controller/settings.go
index af7558a..feba893 100644
--- a/server/ui/controller/settings.go
+++ b/server/ui/controller/settings.go
@@ -62,6 +62,7 @@ func (c *Controller) UpdateSettings(ctx *core.Context, request *core.Request, re
return
}
+ ctx.SetFlashMessage(ctx.Translate("Preferences saved!"))
response.Redirect(ctx.Route("settings"))
}
diff --git a/server/ui/controller/unread.go b/server/ui/controller/unread.go
index 87faafc..8cf8a38 100644
--- a/server/ui/controller/unread.go
+++ b/server/ui/controller/unread.go
@@ -44,6 +44,6 @@ func (c *Controller) ShowUnreadPage(ctx *core.Context, request *core.Request, re
"entries": entries,
"pagination": c.getPagination(ctx.Route("unread"), countUnread, offset),
"menu": "unread",
- "csrf": ctx.CsrfToken(),
+ "csrf": ctx.CSRF(),
})
}
diff --git a/sql/schema_version_10.sql b/sql/schema_version_10.sql
new file mode 100644
index 0000000..abd26d8
--- /dev/null
+++ b/sql/schema_version_10.sql
@@ -0,0 +1,8 @@
+drop table tokens;
+
+create table sessions (
+ id text not null,
+ data jsonb not null,
+ created_at timestamp with time zone not null default now(),
+ primary key(id)
+); \ No newline at end of file
diff --git a/sql/sql.go b/sql/sql.go
index a7974e2..b026243 100644
--- a/sql/sql.go
+++ b/sql/sql.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-16 12:08:03.005451004 -0800 PST m=+0.002264796
+// 2017-12-16 17:48:32.268871258 -0800 PST m=+0.002507937
package sql
@@ -108,6 +108,14 @@ create table feed_icons (
foreign key (icon_id) references icons(id) on delete cascade
);
`,
+ "schema_version_10": `drop table tokens;
+
+create table sessions (
+ id text not null,
+ data jsonb not null,
+ created_at timestamp with time zone not null default now(),
+ primary key(id)
+);`,
"schema_version_2": `create extension if not exists hstore;
alter table users add column extra hstore;
create index users_extra_idx on users using gin(extra);
@@ -147,13 +155,14 @@ alter table users add column entry_direction entry_sorting_direction default 'as
}
var SqlMapChecksums = map[string]string{
- "schema_version_1": "7be580fc8a93db5da54b2f6e87019803c33b0b0c28482c7af80cef873bdac4e2",
- "schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
- "schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
- "schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
- "schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
- "schema_version_6": "9d05b4fb223f0e60efc716add5048b0ca9c37511cf2041721e20505d6d798ce4",
- "schema_version_7": "33f298c9aa30d6de3ca28e1270df51c2884d7596f1283a75716e2aeb634cd05c",
- "schema_version_8": "9922073fc4032d8922617ec6a6a07ae8d4817846c138760fb96cb5608ab83bfc",
- "schema_version_9": "de5ba954752fe808a993feef5bf0c6f808e0a4ced5379de8bec8342678150892",
+ "schema_version_1": "7be580fc8a93db5da54b2f6e87019803c33b0b0c28482c7af80cef873bdac4e2",
+ "schema_version_10": "8faf15ddeff7c8cc305e66218face11ed92b97df2bdc2d0d7944d61441656795",
+ "schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
+ "schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
+ "schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
+ "schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
+ "schema_version_6": "9d05b4fb223f0e60efc716add5048b0ca9c37511cf2041721e20505d6d798ce4",
+ "schema_version_7": "33f298c9aa30d6de3ca28e1270df51c2884d7596f1283a75716e2aeb634cd05c",
+ "schema_version_8": "9922073fc4032d8922617ec6a6a07ae8d4817846c138760fb96cb5608ab83bfc",
+ "schema_version_9": "de5ba954752fe808a993feef5bf0c6f808e0a4ced5379de8bec8342678150892",
}
diff --git a/storage/migration.go b/storage/migration.go
index 368c567..d29c76d 100644
--- a/storage/migration.go
+++ b/storage/migration.go
@@ -12,7 +12,7 @@ import (
"github.com/miniflux/miniflux/sql"
)
-const schemaVersion = 9
+const schemaVersion = 10
// Migrate run database migrations.
func (s *Storage) Migrate() {
diff --git a/storage/session.go b/storage/session.go
new file mode 100644
index 0000000..17a63e2
--- /dev/null
+++ b/storage/session.go
@@ -0,0 +1,77 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package storage
+
+import (
+ "database/sql"
+ "fmt"
+
+ "github.com/miniflux/miniflux/helper"
+ "github.com/miniflux/miniflux/model"
+)
+
+// CreateSession creates a new session.
+func (s *Storage) CreateSession() (*model.Session, error) {
+ session := model.Session{
+ ID: helper.GenerateRandomString(32),
+ Data: &model.SessionData{CSRF: helper.GenerateRandomString(64)},
+ }
+
+ query := "INSERT INTO sessions (id, data) VALUES ($1, $2)"
+ _, err := s.db.Exec(query, session.ID, session.Data)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create session: %v", err)
+ }
+
+ return &session, nil
+}
+
+// UpdateSessionField updates only one session field.
+func (s *Storage) UpdateSessionField(sessionID, field string, value interface{}) error {
+ query := `UPDATE sessions
+ SET data = jsonb_set(data, '{%s}', to_jsonb($1::text), true)
+ WHERE id=$2`
+
+ _, err := s.db.Exec(fmt.Sprintf(query, field), value, sessionID)
+ if err != nil {
+ return fmt.Errorf("unable to update session field: %v", err)
+ }
+
+ return nil
+}
+
+// Session returns the given session.
+func (s *Storage) Session(id string) (*model.Session, error) {
+ var session model.Session
+
+ query := "SELECT id, data FROM sessions WHERE id=$1"
+ err := s.db.QueryRow(query, id).Scan(
+ &session.ID,
+ &session.Data,
+ )
+
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("session not found: %s", id)
+ } else if err != nil {
+ return nil, fmt.Errorf("unable to fetch session: %v", err)
+ }
+
+ return &session, nil
+}
+
+// FlushAllSessions removes all sessions from the database.
+func (s *Storage) FlushAllSessions() (err error) {
+ _, err = s.db.Exec(`DELETE FROM user_sessions`)
+ if err != nil {
+ return err
+ }
+
+ _, err = s.db.Exec(`DELETE FROM sessions`)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/storage/token.go b/storage/token.go
deleted file mode 100644
index c5a614a..0000000
--- a/storage/token.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package storage
-
-import (
- "database/sql"
- "fmt"
-
- "github.com/miniflux/miniflux/helper"
- "github.com/miniflux/miniflux/model"
-)
-
-// CreateToken creates a new token.
-func (s *Storage) CreateToken() (*model.Token, error) {
- token := model.Token{
- ID: helper.GenerateRandomString(32),
- Value: helper.GenerateRandomString(64),
- }
-
- query := "INSERT INTO tokens (id, value) VALUES ($1, $2)"
- _, err := s.db.Exec(query, token.ID, token.Value)
- if err != nil {
- return nil, fmt.Errorf("unable to create token: %v", err)
- }
-
- return &token, nil
-}
-
-// Token returns a Token.
-func (s *Storage) Token(id string) (*model.Token, error) {
- var token model.Token
-
- query := "SELECT id, value FROM tokens WHERE id=$1"
- err := s.db.QueryRow(query, id).Scan(
- &token.ID,
- &token.Value,
- )
-
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("token not found: %s", id)
- } else if err != nil {
- return nil, fmt.Errorf("unable to fetch token: %v", err)
- }
-
- return &token, nil
-}
diff --git a/storage/user_session.go b/storage/user_session.go
index da9ceba..ffb82fc 100644
--- a/storage/user_session.go
+++ b/storage/user_session.go
@@ -127,9 +127,3 @@ func (s *Storage) RemoveUserSessionByID(userID, sessionID int64) error {
return nil
}
-
-// FlushAllSessions removes all user sessions from the database.
-func (s *Storage) FlushAllSessions() (err error) {
- _, err = s.db.Exec(`DELETE FROM user_sessions`)
- return
-}