aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/middleware/session.go7
-rw-r--r--server/oauth2/google.go70
-rw-r--r--server/oauth2/manager.go33
-rw-r--r--server/oauth2/profile.go12
-rw-r--r--server/oauth2/provider.go11
-rw-r--r--server/routes.go5
-rw-r--r--server/static/bin.go2
-rw-r--r--server/static/css.go6
-rw-r--r--server/static/css/common.css1
-rw-r--r--server/static/js.go2
-rw-r--r--server/template/common.go2
-rw-r--r--server/template/html/login.html5
-rw-r--r--server/template/template.go3
-rw-r--r--server/template/views.go9
-rw-r--r--server/ui/controller/controller.go5
-rw-r--r--server/ui/controller/login.go9
-rw-r--r--server/ui/controller/oauth2.go123
17 files changed, 290 insertions, 15 deletions
diff --git a/server/middleware/session.go b/server/middleware/session.go
index 5455972..1ab0d0a 100644
--- a/server/middleware/session.go
+++ b/server/middleware/session.go
@@ -6,11 +6,12 @@ package middleware
import (
"context"
+ "log"
+ "net/http"
+
"github.com/miniflux/miniflux2/model"
"github.com/miniflux/miniflux2/server/route"
"github.com/miniflux/miniflux2/storage"
- "log"
- "net/http"
"github.com/gorilla/mux"
)
@@ -45,7 +46,7 @@ func (s *SessionMiddleware) Handler(next http.Handler) http.Handler {
func (s *SessionMiddleware) isPublicRoute(r *http.Request) bool {
route := mux.CurrentRoute(r)
switch route.GetName() {
- case "login", "checkLogin", "stylesheet", "javascript":
+ case "login", "checkLogin", "stylesheet", "javascript", "oauth2Redirect", "oauth2Callback":
return true
default:
return false
diff --git a/server/oauth2/google.go b/server/oauth2/google.go
new file mode 100644
index 0000000..5c63c75
--- /dev/null
+++ b/server/oauth2/google.go
@@ -0,0 +1,70 @@
+// 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 oauth2
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "golang.org/x/oauth2"
+)
+
+type googleProfile struct {
+ Sub string `json:"sub"`
+ Email string `json:"email"`
+}
+
+type googleProvider struct {
+ clientID string
+ clientSecret string
+ redirectURL string
+}
+
+func (g googleProvider) GetRedirectURL(state string) string {
+ return g.config().AuthCodeURL(state)
+}
+
+func (g googleProvider) GetProfile(code string) (*Profile, error) {
+ conf := g.config()
+ ctx := context.Background()
+ token, err := conf.Exchange(ctx, code)
+ if err != nil {
+ return nil, err
+ }
+
+ client := conf.Client(ctx, token)
+ resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var user googleProfile
+ decoder := json.NewDecoder(resp.Body)
+ if err := decoder.Decode(&user); err != nil {
+ return nil, fmt.Errorf("unable to unserialize google profile: %v", err)
+ }
+
+ profile := &Profile{Key: "google_id", ID: user.Sub, Username: user.Email}
+ return profile, nil
+}
+
+func (g googleProvider) config() *oauth2.Config {
+ return &oauth2.Config{
+ RedirectURL: g.redirectURL,
+ ClientID: g.clientID,
+ ClientSecret: g.clientSecret,
+ Scopes: []string{"email"},
+ Endpoint: oauth2.Endpoint{
+ AuthURL: "https://accounts.google.com/o/oauth2/auth",
+ TokenURL: "https://accounts.google.com/o/oauth2/token",
+ },
+ }
+}
+
+func newGoogleProvider(clientID, clientSecret, redirectURL string) *googleProvider {
+ return &googleProvider{clientID: clientID, clientSecret: clientSecret, redirectURL: redirectURL}
+}
diff --git a/server/oauth2/manager.go b/server/oauth2/manager.go
new file mode 100644
index 0000000..08360a9
--- /dev/null
+++ b/server/oauth2/manager.go
@@ -0,0 +1,33 @@
+// 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 oauth2
+
+import "errors"
+
+// Manager handles OAuth2 providers.
+type Manager struct {
+ providers map[string]Provider
+}
+
+// Provider returns the given provider.
+func (m *Manager) Provider(name string) (Provider, error) {
+ if provider, found := m.providers[name]; found {
+ return provider, nil
+ }
+
+ return nil, errors.New("oauth2 provider not found")
+}
+
+// AddProvider add a new OAuth2 provider.
+func (m *Manager) AddProvider(name string, provider Provider) {
+ m.providers[name] = provider
+}
+
+// NewManager returns a new Manager.
+func NewManager(clientID, clientSecret, redirectURL string) *Manager {
+ m := &Manager{providers: make(map[string]Provider)}
+ m.AddProvider("google", newGoogleProvider(clientID, clientSecret, redirectURL))
+ return m
+}
diff --git a/server/oauth2/profile.go b/server/oauth2/profile.go
new file mode 100644
index 0000000..488ffb2
--- /dev/null
+++ b/server/oauth2/profile.go
@@ -0,0 +1,12 @@
+// 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 oauth2
+
+// Profile is the OAuth2 user profile.
+type Profile struct {
+ Key string
+ ID string
+ Username string
+}
diff --git a/server/oauth2/provider.go b/server/oauth2/provider.go
new file mode 100644
index 0000000..27ab22a
--- /dev/null
+++ b/server/oauth2/provider.go
@@ -0,0 +1,11 @@
+// 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 oauth2
+
+// Provider is an interface for OAuth2 providers.
+type Provider interface {
+ GetRedirectURL(state string) string
+ GetProfile(code string) (*Profile, error)
+}
diff --git a/server/routes.go b/server/routes.go
index 4d4a99a..7c227eb 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -29,7 +29,7 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han
templateEngine := template.NewEngine(cfg, router, translator)
apiController := api_controller.NewController(store, feedHandler)
- uiController := ui_controller.NewController(store, pool, feedHandler, opml.NewHandler(store))
+ uiController := ui_controller.NewController(cfg, store, pool, feedHandler, opml.NewHandler(store))
apiHandler := core.NewHandler(store, router, templateEngine, translator, middleware.NewMiddlewareChain(
middleware.NewBasicAuthMiddleware(store).Handler,
@@ -124,6 +124,9 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han
router.Handle("/import", uiHandler.Use(uiController.Import)).Name("import").Methods("GET")
router.Handle("/upload", uiHandler.Use(uiController.UploadOPML)).Name("uploadOPML").Methods("POST")
+ router.Handle("/oauth2/{provider}/redirect", uiHandler.Use(uiController.OAuth2Redirect)).Name("oauth2Redirect").Methods("GET")
+ router.Handle("/oauth2/{provider}/callback", uiHandler.Use(uiController.OAuth2Callback)).Name("oauth2Callback").Methods("GET")
+
router.Handle("/login", uiHandler.Use(uiController.CheckLogin)).Name("checkLogin").Methods("POST")
router.Handle("/logout", uiHandler.Use(uiController.Logout)).Name("logout").Methods("GET")
router.Handle("/", uiHandler.Use(uiController.ShowLoginPage)).Name("login").Methods("GET")
diff --git a/server/static/bin.go b/server/static/bin.go
index 557d711..f06a8d9 100644
--- a/server/static/bin.go
+++ b/server/static/bin.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-11-22 15:58:44.918764081 -0800 PST m=+0.003855762
+// 2017-11-22 22:11:44.595540312 -0800 PST m=+0.009225645
package static
diff --git a/server/static/css.go b/server/static/css.go
index 36c79f2..17ddc99 100644
--- a/server/static/css.go
+++ b/server/static/css.go
@@ -1,14 +1,14 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-11-22 15:58:44.921971582 -0800 PST m=+0.007063263
+// 2017-11-22 22:11:44.596955262 -0800 PST m=+0.010640595
package static
var Stylesheets = map[string]string{
"black": `body{background:#222;color:#efefef}h1,h2,h3{color:#aaa}a{color:#aaa}a:focus,a:hover{color:#ddd}.header li{border-color:#333}.header a{color:#ddd;font-weight:400}.header .active a{font-weight:400;color:#9b9494}.header a:focus,.header a:hover{color:rgba(82,168,236,.85)}.page-header h1{border-color:#333}.logo a:hover span{color:#555}table,th,td{border:1px solid #555}th{background:#333;color:#aaa;font-weight:400}tr:hover{background-color:#333;color:#aaa}input[type=url],input[type=password],input[type=text]{border:1px solid #555;background:#333;color:#ccc}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#efefef;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.button-primary{border-color:#444;background:#333;color:#efefef}.button-primary:hover,.button-primary:focus{border-color:#888;background:#555}.alert,.alert-success,.alert-error,.alert-info,.alert-normal{color:#efefef;background-color:#333;border-color:#444}.panel{background:#333;border-color:#555}.unread-counter{color:#bbb}.category{color:#efefef;background-color:#333;border-color:#444}.category a{color:#999}.category a:hover,.category a:focus{color:#aaa}.pagination a{color:#aaa}.pagination-bottom{border-color:#333}.item{border-color:#666;padding:4px}.item.current-item{border-width:2px;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.item-title a{font-weight:400}.item-status-read .item-title a{color:#666}.item-status-read .item-title a:focus,.item-status-read .item-title a:hover{color:rgba(82,168,236,.6)}.item-meta a:hover,.item-meta a:focus{color:#aaa}.item-meta li:after{color:#ddd}.entry header{border-color:#333}.entry header h1 a{color:#bbb}.entry-content,.entry-content p,ul{color:#999}.entry-content pre,.entry-content code{color:#fff;background:#555;border-color:#888}.entry-enclosure{border-color:#333}`,
- "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#f0f0f0;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}.login-form{margin:50px auto 0;max-width:280px}.unread-counter{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`,
+ "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#f0f0f0;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}.login-form{margin:50px auto 0;max-width:280px}.unread-counter{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`,
}
var StylesheetsChecksums = map[string]string{
"black": "38e7fee92187a036ce37f3c15fde2deff59a55c5ab693c7b8578af79d6a117d2",
- "common": "6cd39de5133732fd68a3d6c85fcbf5129edf0954c36cc872284c24202b155d1e",
+ "common": "b5f170f533d3074df7076e11a927c9195fd4dbea39adc73bedc9e92e889bcb53",
}
diff --git a/server/static/css/common.css b/server/static/css/common.css
index bfecb22..7e84997 100644
--- a/server/static/css/common.css
+++ b/server/static/css/common.css
@@ -130,6 +130,7 @@ a:hover {
padding-right: 15px;
line-height: normal;
border: none;
+ font-size: 1.0em;
}
.page-header ul {
diff --git a/server/static/js.go b/server/static/js.go
index b0d4f6c..f302bb9 100644
--- a/server/static/js.go
+++ b/server/static/js.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-11-22 15:58:44.924790514 -0800 PST m=+0.009882195
+// 2017-11-22 22:11:44.598697812 -0800 PST m=+0.012383145
package static
diff --git a/server/template/common.go b/server/template/common.go
index 53abcab..e65d611 100644
--- a/server/template/common.go
+++ b/server/template/common.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-11-22 15:58:44.944002944 -0800 PST m=+0.029094625
+// 2017-11-22 22:11:44.609659332 -0800 PST m=+0.023344665
package template
diff --git a/server/template/html/login.html b/server/template/html/login.html
index 07a3212..f692723 100644
--- a/server/template/html/login.html
+++ b/server/template/html/login.html
@@ -19,5 +19,10 @@
<button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Sign in" }}</button>
</div>
</form>
+ {{ if hasOAuth2Provider "google" }}
+ <div class="oauth2">
+ <a href="{{ route "oauth2Redirect" "provider" "google" }}">{{ t "Sign in with Google" }}</a>
+ </div>
+ {{ end }}
</section>
{{ end }}
diff --git a/server/template/template.go b/server/template/template.go
index dc5283d..5ef68d6 100644
--- a/server/template/template.go
+++ b/server/template/template.go
@@ -37,6 +37,9 @@ func (e *Engine) parseAll() {
"baseURL": func() string {
return e.cfg.Get("BASE_URL", config.DefaultBaseURL)
},
+ "hasOAuth2Provider": func(provider string) bool {
+ return e.cfg.Get("OAUTH2_PROVIDER", "") == provider
+ },
"route": func(name string, args ...interface{}) string {
return route.GetRoute(e.router, name, args...)
},
diff --git a/server/template/views.go b/server/template/views.go
index b0ad6d6..4412149 100644
--- a/server/template/views.go
+++ b/server/template/views.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-11-22 15:58:44.928001659 -0800 PST m=+0.013093340
+// 2017-11-22 22:11:44.601583424 -0800 PST m=+0.015268757
package template
@@ -796,6 +796,11 @@ var templateViewsMap = map[string]string{
<button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Sign in" }}</button>
</div>
</form>
+ {{ if hasOAuth2Provider "google" }}
+ <div class="oauth2">
+ <a href="{{ route "oauth2Redirect" "provider" "google" }}">{{ t "Sign in with Google" }}</a>
+ </div>
+ {{ end }}
</section>
{{ end }}
`,
@@ -1045,7 +1050,7 @@ var templateViewsMapChecksums = map[string]string{
"history": "947603cbde888516e62925f5d08fb0b13d930623d3ee4c690dbc22612fdda75e",
"import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f",
"integrations": "c485d6d9ed996635e55e73320610e6bcb01a41c1153e8e739ae2294b0b14b243",
- "login": "568f2f69f248048f3e55e9bbc719077a74ae23fe18f237aa40e3de37e97b7a41",
+ "login": "04f3ce79bfa5753f69e0d956c2a8999c0da549c7925634a3e8134975da0b0e0f",
"sessions": "878dbe8f8ea783b44130c495814179519fa5c3aa2666ac87508f94d58dd008bf",
"settings": "a972fb5767fd32522648149880e40607ed8bbed7a389038bbab6b08539ac2893",
"unread": "b6f9be1a72188947c75a6fdcac6ff7878db7745f9efa46318e0433102892a722",
diff --git a/server/ui/controller/controller.go b/server/ui/controller/controller.go
index 4aa6085..a8f5218 100644
--- a/server/ui/controller/controller.go
+++ b/server/ui/controller/controller.go
@@ -5,6 +5,7 @@
package controller
import (
+ "github.com/miniflux/miniflux2/config"
"github.com/miniflux/miniflux2/model"
"github.com/miniflux/miniflux2/reader/feed"
"github.com/miniflux/miniflux2/reader/opml"
@@ -25,6 +26,7 @@ func (t tplParams) Merge(d tplParams) tplParams {
// Controller contains all HTTP handlers for the user interface.
type Controller struct {
+ cfg *config.Config
store *storage.Storage
pool *scheduler.WorkerPool
feedHandler *feed.Handler
@@ -51,8 +53,9 @@ func (c *Controller) getCommonTemplateArgs(ctx *core.Context) (tplParams, error)
}
// NewController returns a new Controller.
-func NewController(store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, opmlHandler *opml.Handler) *Controller {
+func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, opmlHandler *opml.Handler) *Controller {
return &Controller{
+ cfg: cfg,
store: store,
pool: pool,
feedHandler: feedHandler,
diff --git a/server/ui/controller/login.go b/server/ui/controller/login.go
index aaaf0b6..2571f6e 100644
--- a/server/ui/controller/login.go
+++ b/server/ui/controller/login.go
@@ -5,15 +5,17 @@
package controller
import (
- "github.com/miniflux/miniflux2/server/core"
- "github.com/miniflux/miniflux2/server/ui/form"
"log"
"net/http"
"time"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/ui/form"
+
"github.com/tomasen/realip"
)
+// ShowLoginPage shows the login form.
func (c *Controller) ShowLoginPage(ctx *core.Context, request *core.Request, response *core.Response) {
if ctx.IsAuthenticated() {
response.Redirect(ctx.Route("unread"))
@@ -25,6 +27,7 @@ func (c *Controller) ShowLoginPage(ctx *core.Context, request *core.Request, res
})
}
+// CheckLogin validates the username/password and redirects the user to the unread page.
func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, response *core.Response) {
authForm := form.NewAuthForm(request.Request())
tplParams := tplParams{
@@ -49,6 +52,7 @@ func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, respon
request.Request().UserAgent(),
realip.RealIP(request.Request()),
)
+
if err != nil {
response.HTML().ServerError(err)
return
@@ -68,6 +72,7 @@ func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, respon
response.Redirect(ctx.Route("unread"))
}
+// Logout destroy the session and redirects the user to the login page.
func (c *Controller) Logout(ctx *core.Context, request *core.Request, response *core.Response) {
user := ctx.LoggedUser()
diff --git a/server/ui/controller/oauth2.go b/server/ui/controller/oauth2.go
new file mode 100644
index 0000000..c43d707
--- /dev/null
+++ b/server/ui/controller/oauth2.go
@@ -0,0 +1,123 @@
+// 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 controller
+
+import (
+ "log"
+ "net/http"
+
+ "github.com/miniflux/miniflux2/config"
+ "github.com/miniflux/miniflux2/model"
+ "github.com/miniflux/miniflux2/server/core"
+ "github.com/miniflux/miniflux2/server/oauth2"
+ "github.com/tomasen/realip"
+)
+
+// OAuth2Redirect redirects the user to the consent page to ask for permission.
+func (c *Controller) OAuth2Redirect(ctx *core.Context, request *core.Request, response *core.Response) {
+ provider := request.StringParam("provider", "")
+ if provider == "" {
+ log.Println("[OAuth2] Invalid or missing provider")
+ response.Redirect(ctx.Route("login"))
+ return
+ }
+
+ authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
+ if err != nil {
+ log.Println("[OAuth2]", err)
+ response.Redirect(ctx.Route("login"))
+ return
+ }
+
+ response.Redirect(authProvider.GetRedirectURL(ctx.CsrfToken()))
+}
+
+// OAuth2Callback receives the authorization code and create a new session.
+func (c *Controller) OAuth2Callback(ctx *core.Context, request *core.Request, response *core.Response) {
+ provider := request.StringParam("provider", "")
+ if provider == "" {
+ log.Println("[OAuth2] Invalid or missing provider")
+ response.Redirect(ctx.Route("login"))
+ return
+ }
+
+ code := request.QueryStringParam("code", "")
+ if code == "" {
+ log.Println("[OAuth2] No code received on callback")
+ response.Redirect(ctx.Route("login"))
+ return
+ }
+
+ state := request.QueryStringParam("state", "")
+ if state != ctx.CsrfToken() {
+ log.Println("[OAuth2] Invalid state value")
+ response.Redirect(ctx.Route("login"))
+ return
+ }
+
+ authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
+ if err != nil {
+ log.Println("[OAuth2]", err)
+ response.Redirect(ctx.Route("login"))
+ return
+ }
+
+ profile, err := authProvider.GetProfile(code)
+ if err != nil {
+ log.Println("[OAuth2]", err)
+ response.Redirect(ctx.Route("login"))
+ return
+ }
+
+ user, err := c.store.GetUserByExtraField(profile.Key, profile.ID)
+ if err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+
+ if user == nil {
+ user = model.NewUser()
+ user.Username = profile.Username
+ user.IsAdmin = false
+ user.Extra[profile.Key] = profile.ID
+
+ if err := c.store.CreateUser(user); err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+ }
+
+ sessionToken, err := c.store.CreateSession(
+ user.Username,
+ request.Request().UserAgent(),
+ realip.RealIP(request.Request()),
+ )
+
+ if err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+
+ log.Printf("[UI:OAuth2Callback] username=%s just logged in\n", user.Username)
+
+ cookie := &http.Cookie{
+ Name: "sessionID",
+ Value: sessionToken,
+ Path: "/",
+ Secure: request.IsHTTPS(),
+ HttpOnly: true,
+ }
+
+ response.SetCookie(cookie)
+ response.Redirect(ctx.Route("unread"))
+}
+
+func getOAuth2Manager(cfg *config.Config) *oauth2.Manager {
+ return oauth2.NewManager(
+ cfg.Get("OAUTH2_CLIENT_ID", ""),
+ cfg.Get("OAUTH2_CLIENT_SECRET", ""),
+ cfg.Get("OAUTH2_REDIRECT_URL", ""),
+ )
+}