diff --git a/Gopkg.lock b/Gopkg.lock index 4d9c642b..4fe9237d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -180,7 +180,8 @@ "google", "internal", "jws", - "jwt" + "jwt", + "yandex" ] revision = "543e37812f10c46c622c9575afd7ad22f22a12ba" @@ -222,6 +223,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "dd700af9d574d75a9afc92a7dbf61485a1785c38c7b93667b1be423c4fe814d7" + inputs-digest = "e849348430ea10a86642c328c52b516e237207389cf765ff1849c8bded391d4a" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index 13964871..012bcb75 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles or any other place where readers add comments. -* Social login via Google, Facebook and Github +* Social login via Google, Facebook, Github and Yandex * Multi-level nested comments with both tree and plain presentations * Import from disqus * Markdown support @@ -30,30 +30,32 @@ Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engi #### Parameters -| Command line | Environment | Default | Multi | Description | -| ----------------- | -------------------- | ---------------------- | ----- | --------------------------------------- | -| --url | REMARK_URL | `https://remark42.com` | no | url to remark server | -| --bolt | BOLTDB_PATH | `./var` | no | path to data directory | -| --site | SITE | `remark` | yes | site name(s) | -| --admin | ADMIN | | yes | admin names (list of user ids) | -| --backup | BACKUP_PATH | `./var/backup` | no | backups location | -| --max-back | MAX_BACKUP_FILES | `10` | no | max backup files to keep | -| --max-cache-items | MAX_CACHE_ITEMS | `1000` | no | max number of cached items, 0-unlimited | -| --max-cache-value | MAX_CACHE_VALUE | `65536` | no | max size of cached value, o-unlimited | -| --avatars | AVATAR_STORE | `./var/avatars` | no | avatars location | -| --secret | SECRET | | no | secret key, required | -| --max-comment | MAX_COMMENT_SIZE | 2048 | no | comment's size limit | -| --google-cid | REMARK_GOOGLE_CID | | no | Google OAuth client ID | -| --google-csec | REMARK_GOOGLE_CSEC | | no | Google OAuth client secret | -| --facebook-cid | REMARK_FACEBOOK_CID | | no | Facebook OAuth client ID | -| --facebook-csec | REMARK_FACEBOOK_CSEC | | no | Facebook OAuth client secret | -| --github-cid | REMARK_GITHUB_CID | | no | Github OAuth client ID | -| --github-csec | REMARK_GITHUB_CSEC | | no | Github OAuth client secret | -| --low-score | LOW_SCORE | `-5` | no | Low score threshold | -| --critical-score | CRITICAL_SCORE | `-10` | no | Critical score threshold | -| --img-proxy | IMG_PROXY | `false` | no | Enable http->https proxy for images | -| --dbg | DEBUG | `false` | no | debug mode | -| --dev-passwd | DEV_PASSWD | | no | password for `dev` user | +| Command line | Environment | Default | Multi | Description | +| ----------------- | -------------------- | ---------------------- | ----- | --------------------------------------------| +| --url | REMARK_URL | `https://remark42.com` | no | url to remark server | +| --bolt | BOLTDB_PATH | `./var` | no | path to data directory | +| --site | SITE | `remark` | yes | site name(s) | +| --admin | ADMIN | | yes | admin names (list of user ids) | +| --backup | BACKUP_PATH | `./var/backup` | no | backups location | +| --max-back | MAX_BACKUP_FILES | `10` | no | max backup files to keep | +| --max-cache-items | MAX_CACHE_ITEMS | `1000` | no | max number of cached items, `0` - unlimited | +| --max-cache-value | MAX_CACHE_VALUE | `65536` | no | max size of cached value, `0` - unlimited | +| --avatars | AVATAR_STORE | `./var/avatars` | no | avatars location | +| --secret | SECRET | | no | secret key, required | +| --max-comment | MAX_COMMENT_SIZE | 2048 | no | comment's size limit | +| --google-cid | REMARK_GOOGLE_CID | | no | Google OAuth client ID | +| --google-csec | REMARK_GOOGLE_CSEC | | no | Google OAuth client secret | +| --facebook-cid | REMARK_FACEBOOK_CID | | no | Facebook OAuth client ID | +| --facebook-csec | REMARK_FACEBOOK_CSEC | | no | Facebook OAuth client secret | +| --github-cid | REMARK_GITHUB_CID | | no | Github OAuth client ID | +| --github-csec | REMARK_GITHUB_CSEC | | no | Github OAuth client secret | +| --yandex-cid | REMARK_YANDEX_CID | | no | Yandex OAuth client ID | +| --yandex-csec | REMARK_YANDEX_CSEC | | no | Yandex OAuth client secret | +| --low-score | LOW_SCORE | `-5` | no | Low score threshold | +| --critical-score | CRITICAL_SCORE | `-10` | no | Critical score threshold | +| --img-proxy | IMG_PROXY | `false` | no | Enable http->https proxy for images | +| --dbg | DEBUG | `false` | no | debug mode | +| --dev-passwd | DEV_PASSWD | | no | password for `dev` user | **user has to provide secret key, can be any long and hard-to-guess string.** @@ -99,6 +101,19 @@ _instructions for google oauth2 setup borrowed from [oauth2_proxy](https://githu 1. Under **"Facebook login"** / **"Settings"** fill "Valid OAuth redirect URIs" with your callback url constructed as domain + `/auth/facebook/callback` 1. Select **"App Review"** and turn public flag on. This step may ask you to provide a link to your privacy policy. +##### Yandex Auth Provider + +1. Create a new **"OAuth App"**: https://oauth.yandex.com/client/new +1. Fill **"App name"** for your site +1. Under **Platforms** select **"Web services"** and enter **"Callback URI #1"** constructed as domain + `/auth/yandex/callback`. ie `https://remark42.mysite.com/auth/yandex/callback` +1. Select **Permissions**. You need following permissions only from the **"Yandex.Passport API"** section: + * Access to user avatar + * Access to username, first name and surname, gender +1. Fill out the rest of fields if needed +1. Take note of the **ID** and **Password** + +For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/concepts/about-docpage/) and [Yandex.Passport](https://tech.yandex.com/passport/doc/dg/index-docpage/) API documentation. + #### Initial import from Disqus 1. Disqus provides an export of all comments on your site in a g-zipped file. This is found in your Moderation panel at Disqus Admin > Setup > Export. The export will be sent into a queue and then emailed to the address associated with your account once it's ready. Direct link to export will be something like `https://.disqus.com/admin/discussions/export/`. See [importing-exporting](https://help.disqus.com/customer/portal/articles/1104797-importing-exporting) for more details. @@ -275,7 +290,7 @@ type User struct { } ``` -_currently supported providers are `google`, `facebook` and `github`_ +_currently supported providers are `google`, `facebook`, `github` and `yandex`_ ### Commenting @@ -412,5 +427,5 @@ _all admin calls require auth and admin privilege_ * User can vote for the comment multiple times but only to change his/her vote. Double-voting not allowed. * User can edit comments in 5 mins window after creation. * User ID hashed and prefixed by oauth provider name to avoid collisions and potential abuse. -* All avatars cached locally to prevent rate limiters from google/github/facebook. +* All avatars cached locally to prevent rate limiters from google/github/facebook/yandex. * Docker build uses [publicly available](https://github.com/umputun/baseimage) base images. diff --git a/app/main.go b/app/main.go index 05e97fdc..39907355 100644 --- a/app/main.go +++ b/app/main.go @@ -56,6 +56,8 @@ type Opts struct { GithubCSEC string `long:"github-csec" env:"REMARK_GITHUB_CSEC" description:"Github OAuth client secret"` FacebookCID string `long:"facebook-cid" env:"REMARK_FACEBOOK_CID" description:"Facebook OAuth client ID"` FacebookCSEC string `long:"facebook-csec" env:"REMARK_FACEBOOK_CSEC" description:"Facebook OAuth client secret"` + YandexCID string `long:"yandex-cid" env:"REMARK_YANDEX_CID" description:"Yandex OAuth client ID"` + YandexCSEC string `long:"yandex-csec" env:"REMARK_YANDEX_CSEC" description:"Yandex OAuth client secret"` Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"` WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"` @@ -266,6 +268,9 @@ func makeAuthProviders(jwtService *auth.JWT, avatarProxy *proxy.Avatar, ds servi if opts.FacebookCID != "" && opts.FacebookCSEC != "" { providers = append(providers, auth.NewFacebook(makeParams(opts.FacebookCID, opts.FacebookCSEC))) } + if opts.YandexCID != "" && opts.YandexCSEC != "" { + providers = append(providers, auth.NewYandex(makeParams(opts.YandexCID, opts.YandexCSEC))) + } if len(providers) == 0 { log.Printf("[WARN] no auth providers defined") } diff --git a/app/main_test.go b/app/main_test.go index 287fb10b..be4cd760 100644 --- a/app/main_test.go +++ b/app/main_test.go @@ -70,6 +70,7 @@ func prepApp(t *testing.T, port int, duration time.Duration) (*Application, cont opts.GithubCSEC, opts.GithubCID = "csec", "cid" opts.GoogleCSEC, opts.GoogleCID = "csec", "cid" opts.FacebookCSEC, opts.FacebookCID = "csec", "cid" + opts.YandexCSEC, opts.YandexCID = "csec", "cid" opts.Port = port os.Remove(opts.BoltPath + "/remark.db") diff --git a/app/rest/auth/providers.go b/app/rest/auth/providers.go index d046776c..1279c89b 100644 --- a/app/rest/auth/providers.go +++ b/app/rest/auth/providers.go @@ -2,10 +2,12 @@ package auth import ( "encoding/json" + "fmt" "golang.org/x/oauth2/facebook" "golang.org/x/oauth2/github" "golang.org/x/oauth2/google" + "golang.org/x/oauth2/yandex" "github.com/umputun/remark/app/store" ) @@ -93,3 +95,32 @@ func NewFacebook(p Params) Provider { }, }) } + +// NewYandex makes yandex oauth2 provider +func NewYandex(p Params) Provider { + return initProvider(p, Provider{ + Name: "yandex", + Endpoint: yandex.Endpoint, + RedirectURL: p.RemarkURL + "/auth/yandex/callback", + Scopes: []string{}, + // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ + InfoURL: "https://login.yandex.ru/info?format=json", + MapUser: func(data userData, _ []byte) store.User { + userInfo := store.User{ + ID: "yandex_" + store.EncodeID(data.value("id")), + Name: data.value("display_name"), // using Display Name by default + } + if userInfo.Name == "" { + userInfo.Name = data.value("real_name") // using Real Name (== full name) if Display Name is empty + } + if userInfo.Name == "" { + userInfo.Name = data.value("login") // otherwise using login + } + + if data.value("default_avatar_id") != "" { + userInfo.Picture = fmt.Sprintf("https://avatars.yandex.net/get-yapic/%s/islands-200", data.value("default_avatar_id")) + } + return userInfo + }, + }) +} diff --git a/app/rest/auth/providers_test.go b/app/rest/auth/providers_test.go index d65bb295..78e89359 100644 --- a/app/rest/auth/providers_test.go +++ b/app/rest/auth/providers_test.go @@ -52,5 +52,32 @@ func TestProviders_NewFacebook(t *testing.T) { user = r.MapUser(udata, []byte(`{"picture": {"data": {"url": "http://demo.remark42.com/blah.png"} }}`)) assert.Equal(t, store.User{Name: "facebook_6e34471", ID: "facebook_6e34471f84557e1713012d64a7477c71bfdac631", Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) +} +func TestProviders_NewYandex(t *testing.T) { + r := NewYandex(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"}) + assert.Equal(t, "yandex", r.Name) + + udata := userData{"id": "1234567890", "display_name": "Vasya P", "default_avatar_id": "131652443"} + user := r.MapUser(udata, nil) + assert.Equal(t, store.User{Name: "Vasya P", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", + Picture: "https://avatars.yandex.net/get-yapic/131652443/islands-200", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) + + // "display_name": null, "default_avatar_id": null + udata = userData{"id": "1234567890", "login": "vasya", "display_name": nil, "real_name": "Vasya Pupkin", "default_avatar_id": nil} + user = r.MapUser(udata, nil) + assert.Equal(t, store.User{Name: "Vasya Pupkin", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", + Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) + + // empty "display_name", empty "default_avatar_id", empty "real_name" + udata = userData{"id": "1234567890", "login": "vasya", "display_name": "", "real_name": "", "default_avatar_id": ""} + user = r.MapUser(udata, nil) + assert.Equal(t, store.User{Name: "vasya", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", + Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) + + // "real_name": null + udata = userData{"id": "1234567890", "login": "vasya", "real_name": nil, "default_avatar_id": ""} + user = r.MapUser(udata, nil) + assert.Equal(t, store.User{Name: "vasya", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a", + Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user) } diff --git a/vendor/golang.org/x/oauth2/yandex/yandex.go b/vendor/golang.org/x/oauth2/yandex/yandex.go new file mode 100644 index 00000000..5ebf666d --- /dev/null +++ b/vendor/golang.org/x/oauth2/yandex/yandex.go @@ -0,0 +1,16 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package yandex provides constants for using OAuth2 to access Yandex APIs. +package yandex // import "golang.org/x/oauth2/yandex" + +import ( + "golang.org/x/oauth2" +) + +// Endpoint is the Yandex OAuth 2.0 endpoint. +var Endpoint = oauth2.Endpoint{ + AuthURL: "https://oauth.yandex.com/authorize", + TokenURL: "https://oauth.yandex.com/token", +} diff --git a/web/app/common/constants.js b/web/app/common/constants.js index 5580fab3..8d178aab 100644 --- a/web/app/common/constants.js +++ b/web/app/common/constants.js @@ -12,6 +12,7 @@ const PROVIDER_NAMES = { google: 'Google', facebook: 'Facebook', github: 'GitHub', + yandex: 'Yandex', }; const LS_COLLAPSE_KEY = '__remarkCollapsed'; const LS_SORT_KEY = '__remarkSort';