@@ -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.
|
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, Twitter, Facebook, Microsoft, GitHub, Yandex and Telegram
|
* Social login via Google, Twitter, Facebook, Microsoft, GitHub, Yandex, Patreon and Telegram
|
||||||
* Login via email
|
* Login via email
|
||||||
* Optional anonymous access
|
* Optional anonymous access
|
||||||
* Multi-level nested comments with both tree and plain presentations
|
* Multi-level nested comments with both tree and plain presentations
|
||||||
@@ -53,6 +53,7 @@ For admin screenshots see [Admin UI wiki](https://github.com/umputun/remark42/wi
|
|||||||
- [Telegram Auth Provider](#telegram-auth-provider)
|
- [Telegram Auth Provider](#telegram-auth-provider)
|
||||||
- [Twitter Auth provider](#twitter-auth-provider)
|
- [Twitter Auth provider](#twitter-auth-provider)
|
||||||
- [Yandex Auth provider](#yandex-auth-provider)
|
- [Yandex Auth provider](#yandex-auth-provider)
|
||||||
|
- [Patreon Auth provider](#patreon-auth-provider)
|
||||||
- [Anonymous Auth provider](#anonymous-auth-provider)
|
- [Anonymous Auth provider](#anonymous-auth-provider)
|
||||||
- [Initial import from Disqus](#initial-import-from-disqus)
|
- [Initial import from Disqus](#initial-import-from-disqus)
|
||||||
- [Initial import from WordPress](#initial-import-from-wordpress)
|
- [Initial import from WordPress](#initial-import-from-wordpress)
|
||||||
@@ -150,6 +151,8 @@ _this is the recommended way to run Remark42_
|
|||||||
| auth.github.csec | AUTH_GITHUB_CSEC | | GitHub OAuth client secret |
|
| auth.github.csec | AUTH_GITHUB_CSEC | | GitHub OAuth client secret |
|
||||||
| auth.twitter.cid | AUTH_TWITTER_CID | | Twitter Consumer API Key |
|
| auth.twitter.cid | AUTH_TWITTER_CID | | Twitter Consumer API Key |
|
||||||
| auth.twitter.csec | AUTH_TWITTER_CSEC | | Twitter Consumer API Secret key |
|
| auth.twitter.csec | AUTH_TWITTER_CSEC | | Twitter Consumer API Secret key |
|
||||||
|
| auth.patreon.cid | AUTH_PATREON_CID | | Patreon OAuth Client ID |
|
||||||
|
| auth.patreon.csec | AUTH_PATREON_CSEC | | Patreon OAuth Client Secret |
|
||||||
| auth.telegram | AUTH_TELEGRAM | | Enable Telegram auth (telegram.token must be present) |
|
| auth.telegram | AUTH_TELEGRAM | | Enable Telegram auth (telegram.token must be present) |
|
||||||
| auth.yandex.cid | AUTH_YANDEX_CID | | Yandex OAuth client ID |
|
| auth.yandex.cid | AUTH_YANDEX_CID | | Yandex OAuth client ID |
|
||||||
| auth.yandex.csec | AUTH_YANDEX_CSEC | | Yandex OAuth client secret |
|
| auth.yandex.csec | AUTH_YANDEX_CSEC | | Yandex OAuth client secret |
|
||||||
@@ -209,6 +212,7 @@ _this is the recommended way to run Remark42_
|
|||||||
| port | REMARK_PORT | `8080` | web server port |
|
| port | REMARK_PORT | `8080` | web server port |
|
||||||
| web-root | REMARK_WEB_ROOT | `./web` | web server root directory |
|
| web-root | REMARK_WEB_ROOT | `./web` | web server root directory |
|
||||||
| update-limit | UPDATE_LIMIT | `0.5` | updates/sec limit |
|
| update-limit | UPDATE_LIMIT | `0.5` | updates/sec limit |
|
||||||
|
| subscribers-only | SUBSCRIBERS_ONLY | `false` | enable commenting only for Patreon subscribers |
|
||||||
| admin-passwd | ADMIN_PASSWD | none (disabled) | password for `admin` basic auth |
|
| admin-passwd | ADMIN_PASSWD | none (disabled) | password for `admin` basic auth |
|
||||||
| dbg | DEBUG | `false` | debug mode |
|
| dbg | DEBUG | `false` | debug mode |
|
||||||
|
|
||||||
@@ -344,6 +348,12 @@ _instructions for Google OAuth2 setup borrowed from [oauth2_proxy](https://githu
|
|||||||
|
|
||||||
For more details refer to [Yandex OAuth](https://yandex.com/dev/oauth/doc/dg/concepts/about.html) and [Yandex.Passport](https://yandex.com/dev/passport/doc/dg/index.html) API documentation.
|
For more details refer to [Yandex OAuth](https://yandex.com/dev/oauth/doc/dg/concepts/about.html) and [Yandex.Passport](https://yandex.com/dev/passport/doc/dg/index.html) API documentation.
|
||||||
|
|
||||||
|
##### Patreon Auth provider
|
||||||
|
1. Create a new Patreon client https://www.patreon.com/portal/registration/register-clients
|
||||||
|
2. Fill **App Name**, **Description**
|
||||||
|
3. In the field **Redirect URIs** enter the correct URI constructed as domain + `/auth/patreon/callback`, i.e. `https://example.mysite.com/auth/patreon/callback`
|
||||||
|
4. Expand client details, take a note of the **Client ID** and **Client Secret**. Those will be used as `AUTH_PATREON_CID` and `AUTH_PATREON_CSEC`
|
||||||
|
|
||||||
##### Anonymous Auth provider
|
##### Anonymous Auth provider
|
||||||
|
|
||||||
Optionally, anonymous access can be turned on. In this case, an extra `anonymous` provider will allow logins without any social login with any name satisfying 2 conditions:
|
Optionally, anonymous access can be turned on. In this case, an extra `anonymous` provider will allow logins without any social login with any name satisfying 2 conditions:
|
||||||
@@ -662,6 +672,7 @@ type User struct {
|
|||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
Blocked bool `json:"block"`
|
Blocked bool `json:"block"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
|
PaidSub bool `json:"paid_sub"` // is paid Patreon subscriber
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -760,18 +771,19 @@ type PostInfo struct {
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
EditDuration int `json:"edit_duration"`
|
EditDuration int `json:"edit_duration"`
|
||||||
MaxCommentSize int `json:"max_comment_size"`
|
MaxCommentSize int `json:"max_comment_size"`
|
||||||
Admins []string `json:"admins"`
|
Admins []string `json:"admins"`
|
||||||
AdminEmail string `json:"admin_email"`
|
AdminEmail string `json:"admin_email"`
|
||||||
Auth []string `json:"auth_providers"`
|
Auth []string `json:"auth_providers"`
|
||||||
LowScore int `json:"low_score"`
|
LowScore int `json:"low_score"`
|
||||||
CriticalScore int `json:"critical_score"`
|
CriticalScore int `json:"critical_score"`
|
||||||
PositiveScore bool `json:"positive_score"`
|
PositiveScore bool `json:"positive_score"`
|
||||||
ReadOnlyAge int `json:"readonly_age"`
|
ReadOnlyAge int `json:"readonly_age"`
|
||||||
MaxImageSize int `json:"max_image_size"`
|
MaxImageSize int `json:"max_image_size"`
|
||||||
EmojiEnabled bool `json:"emoji_enabled"`
|
EmojiEnabled bool `json:"emoji_enabled"`
|
||||||
|
SubscribersOnly bool `json:"subscribers_only"` // enable commenting only for Patreon subscribers
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ type ServerCommand struct {
|
|||||||
SimpleView bool `long:"simpler-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"`
|
SimpleView bool `long:"simpler-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"`
|
||||||
ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"`
|
ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"`
|
||||||
AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments"`
|
AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments"`
|
||||||
|
SubscribersOnly bool `long:"subscribers-only" env:"SUBSCRIBERS_ONLY" description:"enable commenting only for Patreon subscribers"`
|
||||||
|
|
||||||
Auth struct {
|
Auth struct {
|
||||||
TTL struct {
|
TTL struct {
|
||||||
@@ -96,6 +97,7 @@ type ServerCommand struct {
|
|||||||
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
|
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
|
||||||
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
|
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
|
||||||
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
|
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
|
||||||
|
Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"`
|
||||||
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
|
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
|
||||||
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
|
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
|
||||||
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"`
|
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"`
|
||||||
@@ -291,6 +293,7 @@ func (s *ServerCommand) Execute(_ []string) error {
|
|||||||
"AUTH_MICROSOFT_CSEC",
|
"AUTH_MICROSOFT_CSEC",
|
||||||
"AUTH_TWITTER_CSEC",
|
"AUTH_TWITTER_CSEC",
|
||||||
"AUTH_YANDEX_CSEC",
|
"AUTH_YANDEX_CSEC",
|
||||||
|
"AUTH_PATREON_CSEC",
|
||||||
"TELEGRAM_TOKEN",
|
"TELEGRAM_TOKEN",
|
||||||
"SMTP_PASSWORD",
|
"SMTP_PASSWORD",
|
||||||
"ADMIN_PASSWD",
|
"ADMIN_PASSWD",
|
||||||
@@ -531,6 +534,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
|||||||
ProxyCORS: s.ProxyCORS,
|
ProxyCORS: s.ProxyCORS,
|
||||||
AllowedAncestors: s.AllowedHosts,
|
AllowedAncestors: s.AllowedHosts,
|
||||||
SendJWTHeader: s.Auth.SendJWTHeader,
|
SendJWTHeader: s.Auth.SendJWTHeader,
|
||||||
|
SubscribersOnly: s.SubscribersOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
||||||
@@ -799,6 +803,10 @@ func (s *ServerCommand) addAuthProviders(ctx context.Context, authenticator *aut
|
|||||||
authenticator.AddProvider("twitter", s.Auth.Twitter.CID, s.Auth.Twitter.CSEC)
|
authenticator.AddProvider("twitter", s.Auth.Twitter.CID, s.Auth.Twitter.CSEC)
|
||||||
providers++
|
providers++
|
||||||
}
|
}
|
||||||
|
if s.Auth.Patreon.CID != "" && s.Auth.Patreon.CSEC != "" {
|
||||||
|
authenticator.AddProvider("patreon", s.Auth.Patreon.CID, s.Auth.Patreon.CSEC)
|
||||||
|
providers++
|
||||||
|
}
|
||||||
if s.Auth.Telegram {
|
if s.Auth.Telegram {
|
||||||
telegram := &provider.TelegramHandler{
|
telegram := &provider.TelegramHandler{
|
||||||
ProviderName: "telegram",
|
ProviderName: "telegram",
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func TestServerApp_DevMode(t *testing.T) {
|
|||||||
waitForHTTPServerStart(port)
|
waitForHTTPServerStart(port)
|
||||||
|
|
||||||
providers := app.restSrv.Authenticator.Providers()
|
providers := app.restSrv.Authenticator.Providers()
|
||||||
require.Equal(t, 8+1, len(providers), "extra auth provider")
|
require.Equal(t, 9+1, len(providers), "extra auth provider")
|
||||||
assert.Equal(t, "dev", providers[len(providers)-2].Name(), "dev auth provider")
|
assert.Equal(t, "dev", providers[len(providers)-2].Name(), "dev auth provider")
|
||||||
// send ping
|
// send ping
|
||||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/v1/ping", port))
|
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/v1/ping", port))
|
||||||
@@ -105,7 +105,7 @@ func TestServerApp_AnonMode(t *testing.T) {
|
|||||||
waitForHTTPServerStart(port)
|
waitForHTTPServerStart(port)
|
||||||
|
|
||||||
providers := app.restSrv.Authenticator.Providers()
|
providers := app.restSrv.Authenticator.Providers()
|
||||||
require.Equal(t, 8+1, len(providers), "extra auth provider for anon")
|
require.Equal(t, 9+1, len(providers), "extra auth provider for anon")
|
||||||
assert.Equal(t, "anonymous", providers[len(providers)-1].Name(), "anon auth provider")
|
assert.Equal(t, "anonymous", providers[len(providers)-1].Name(), "anon auth provider")
|
||||||
|
|
||||||
// send ping
|
// send ping
|
||||||
@@ -694,6 +694,7 @@ func prepServerApp(t *testing.T, fn func(o ServerCommand) ServerCommand) (*serve
|
|||||||
cmd.Auth.Yandex.CSEC, cmd.Auth.Yandex.CID = "csec", "cid"
|
cmd.Auth.Yandex.CSEC, cmd.Auth.Yandex.CID = "csec", "cid"
|
||||||
cmd.Auth.Microsoft.CSEC, cmd.Auth.Microsoft.CID = "csec", "cid"
|
cmd.Auth.Microsoft.CSEC, cmd.Auth.Microsoft.CID = "csec", "cid"
|
||||||
cmd.Auth.Twitter.CSEC, cmd.Auth.Twitter.CID = "csec", "cid"
|
cmd.Auth.Twitter.CSEC, cmd.Auth.Twitter.CID = "csec", "cid"
|
||||||
|
cmd.Auth.Patreon.CSEC, cmd.Auth.Patreon.CID = "csec", "cid"
|
||||||
cmd.Auth.Telegram = true
|
cmd.Auth.Telegram = true
|
||||||
cmd.Telegram.Token = "token"
|
cmd.Telegram.Token = "token"
|
||||||
cmd.Auth.Email.Enable = true
|
cmd.Auth.Email.Enable = true
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type Rest struct {
|
|||||||
ProxyCORS bool
|
ProxyCORS bool
|
||||||
SendJWTHeader bool
|
SendJWTHeader bool
|
||||||
AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..."
|
AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..."
|
||||||
|
SubscribersOnly bool
|
||||||
|
|
||||||
SSLConfig SSLConfig
|
SSLConfig SSLConfig
|
||||||
httpsServer *http.Server
|
httpsServer *http.Server
|
||||||
@@ -312,7 +313,7 @@ func (s *Rest) routes() chi.Router {
|
|||||||
rapi.Group(func(rauth chi.Router) {
|
rapi.Group(func(rauth chi.Router) {
|
||||||
rauth.Use(middleware.Timeout(10 * time.Second))
|
rauth.Use(middleware.Timeout(10 * time.Second))
|
||||||
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.updateLimiter(), nil)))
|
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.updateLimiter(), nil)))
|
||||||
rauth.Use(authMiddleware.Auth, matchSiteID)
|
rauth.Use(authMiddleware.Auth, matchSiteID, subscribersOnly(s.SubscribersOnly))
|
||||||
rauth.Use(middleware.NoCache, logInfoWithBody)
|
rauth.Use(middleware.NoCache, logInfoWithBody)
|
||||||
|
|
||||||
rauth.Put("/comment/{id}", s.privRest.updateCommentCtrl)
|
rauth.Put("/comment/{id}", s.privRest.updateCommentCtrl)
|
||||||
@@ -429,6 +430,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
EmojiEnabled bool `json:"emoji_enabled"`
|
EmojiEnabled bool `json:"emoji_enabled"`
|
||||||
SimpleView bool `json:"simple_view"`
|
SimpleView bool `json:"simple_view"`
|
||||||
SendJWTHeader bool `json:"send_jwt_header"`
|
SendJWTHeader bool `json:"send_jwt_header"`
|
||||||
|
SubscribersOnly bool `json:"subscribers_only"`
|
||||||
}{
|
}{
|
||||||
Version: s.Version,
|
Version: s.Version,
|
||||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||||
@@ -447,6 +449,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
AnonVote: s.AnonVote,
|
AnonVote: s.AnonVote,
|
||||||
SimpleView: s.SimpleView,
|
SimpleView: s.SimpleView,
|
||||||
SendJWTHeader: s.SendJWTHeader,
|
SendJWTHeader: s.SendJWTHeader,
|
||||||
|
SubscribersOnly: s.SubscribersOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
cnf.Auth = []string{}
|
cnf.Auth = []string{}
|
||||||
@@ -628,6 +631,27 @@ func frameAncestors(hosts []string) func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subscribersOnly is a middleware rejecting non-paid_sub users
|
||||||
|
func subscribersOnly(enable bool) func(http.Handler) http.Handler {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if enable {
|
||||||
|
user, err := rest.GetUserInfo(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !user.PaidSub {
|
||||||
|
http.Error(w, "Access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseError(err error, defaultCode int) (code int) {
|
func parseError(err error, defaultCode int) (code int) {
|
||||||
code = defaultCode
|
code = defaultCode
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,41 @@ func TestRest_frameAncestors(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRest_subscribersOnly(t *testing.T) {
|
||||||
|
|
||||||
|
paidSubUser := &token.User{}
|
||||||
|
paidSubUser.SetPaidSub(true)
|
||||||
|
|
||||||
|
tbl := []struct {
|
||||||
|
subsOnly bool
|
||||||
|
user token.User
|
||||||
|
setUser bool
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{true, token.User{}, false, http.StatusUnauthorized},
|
||||||
|
{true, token.User{}, true, http.StatusForbidden},
|
||||||
|
{false, token.User{}, false, http.StatusOK},
|
||||||
|
{false, token.User{}, true, http.StatusOK},
|
||||||
|
{true, *paidSubUser, true, http.StatusOK},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tbl {
|
||||||
|
tt := tt
|
||||||
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||||
|
if tt.setUser {
|
||||||
|
req = token.SetUserInfo(req, tt.user)
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
h := subscribersOnly(tt.subsOnly)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
resp := w.Result()
|
||||||
|
assert.Equal(t, tt.status, resp.StatusCode)
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// randomPath pick a file or folder name which is not in use for sure
|
// randomPath pick a file or folder name which is not in use for sure
|
||||||
func randomPath(tempDir, basename, suffix string) (string, error) {
|
func randomPath(tempDir, basename, suffix string) (string, error) {
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func GetUserInfo(r *http.Request) (user store.User, err error) {
|
|||||||
Verified: u.BoolAttr("verified"),
|
Verified: u.BoolAttr("verified"),
|
||||||
Blocked: u.BoolAttr("blocked"),
|
Blocked: u.BoolAttr("blocked"),
|
||||||
SiteID: u.Audience,
|
SiteID: u.Audience,
|
||||||
|
PaidSub: u.IsPaidSub(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ func SetUserInfo(r *http.Request, user store.User) *http.Request {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
u.SetAdmin(user.Admin)
|
u.SetAdmin(user.Admin)
|
||||||
|
u.SetPaidSub(user.PaidSub)
|
||||||
|
|
||||||
return token.SetUserInfo(r, u)
|
return token.SetUserInfo(r, u)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type User struct {
|
|||||||
Verified bool `json:"verified,omitempty"`
|
Verified bool `json:"verified,omitempty"`
|
||||||
EmailSubscription bool `json:"email_subscription,omitempty"`
|
EmailSubscription bool `json:"email_subscription,omitempty"`
|
||||||
SiteID string `json:"site_id,omitempty"`
|
SiteID string `json:"site_id,omitempty"`
|
||||||
|
PaidSub bool `json:"paid_sub,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var reValidSha = regexp.MustCompile("^[a-fA-F0-9]{40}$")
|
var reValidSha = regexp.MustCompile("^[a-fA-F0-9]{40}$")
|
||||||
|
|||||||
@@ -67,5 +67,7 @@ services:
|
|||||||
- AUTH_FACEBOOK_CSEC=1111
|
- AUTH_FACEBOOK_CSEC=1111
|
||||||
- AUTH_TWITTER_CID=1111
|
- AUTH_TWITTER_CID=1111
|
||||||
- AUTH_TWITTER_CSEC=1111
|
- AUTH_TWITTER_CSEC=1111
|
||||||
|
- AUTH_PATREON_CID=1111
|
||||||
|
- AUTH_PATREON_CSEC=1111
|
||||||
volumes:
|
volumes:
|
||||||
- ./var:/srv/var
|
- ./var:/srv/var
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export interface Tree {
|
|||||||
info: PostInfo;
|
info: PostInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OAuthProvider = 'facebook' | 'twitter' | 'google' | 'yandex' | 'github' | 'microsoft' | 'dev';
|
export type OAuthProvider = 'facebook' | 'twitter' | 'google' | 'yandex' | 'github' | 'microsoft' | 'patreon' | 'dev';
|
||||||
export type FormProvider = 'email' | 'anonymous';
|
export type FormProvider = 'email' | 'anonymous';
|
||||||
export type Provider = OAuthProvider | FormProvider;
|
export type Provider = OAuthProvider | FormProvider;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="21" height="21" fill="#FF424D" xmlns="http://www.w3.org/2000/svg"><path d="M2 1h3v19H2z"/><circle cx="14" cy="8" r="7"/></svg>
|
||||||
|
After Width: | Height: | Size: 139 B |
@@ -1,6 +1,7 @@
|
|||||||
export const OAUTH_DATA = {
|
export const OAUTH_DATA = {
|
||||||
facebook: require('./assets/facebook.svg').default as string,
|
facebook: require('./assets/facebook.svg').default as string,
|
||||||
twitter: require('./assets/twitter.svg').default as string,
|
twitter: require('./assets/twitter.svg').default as string,
|
||||||
|
patreon: require('./assets/patreon.svg').default as string,
|
||||||
google: require('./assets/google.svg').default as string,
|
google: require('./assets/google.svg').default as string,
|
||||||
microsoft: require('./assets/microsoft.svg').default as string,
|
microsoft: require('./assets/microsoft.svg').default as string,
|
||||||
yandex: require('./assets/yandex.svg').default as string,
|
yandex: require('./assets/yandex.svg').default as string,
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ _instructions for Google OAuth2 setup borrowed from [oauth2_proxy](https://githu
|
|||||||
|
|
||||||
For more details refer to [Yandex OAuth](https://yandex.com/dev/oauth/doc/dg/concepts/about.html) and [Yandex.Passport](https://yandex.com/dev/passport/doc/dg/index.html) API documentation.
|
For more details refer to [Yandex OAuth](https://yandex.com/dev/oauth/doc/dg/concepts/about.html) and [Yandex.Passport](https://yandex.com/dev/passport/doc/dg/index.html) API documentation.
|
||||||
|
|
||||||
|
### Patreon Auth provider
|
||||||
|
1. Create a new Patreon client https://www.patreon.com/portal/registration/register-clients
|
||||||
|
2. Fill **App Name**, **Description**
|
||||||
|
3. In the field **Redirect URIs** enter the correct URI constructed as domain + `/auth/patreon/callback`, i.e. `https://example.mysite.com/auth/patreon/callback`
|
||||||
|
4. Expand client details, take a note of the **Client ID** and **Client Secret**. Those will be used as `AUTH_PATREON_CID` and `AUTH_PATREON_CSEC`
|
||||||
|
|
||||||
## Anonymous Auth Provider
|
## Anonymous Auth Provider
|
||||||
|
|
||||||
Optionally, anonymous access can be turned on. In this case, an extra `anonymous` provider will allow logins without any social login with any name satisfying 2 conditions:
|
Optionally, anonymous access can be turned on. In this case, an extra `anonymous` provider will allow logins without any social login with any name satisfying 2 conditions:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type User struct {
|
|||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
Blocked bool `json:"block"`
|
Blocked bool `json:"block"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
|
PaidSub bool `json:"paid_sub"` // is paid Patreon subscriber
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,18 +114,19 @@ type PostInfo struct {
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
EditDuration int `json:"edit_duration"`
|
EditDuration int `json:"edit_duration"`
|
||||||
MaxCommentSize int `json:"max_comment_size"`
|
MaxCommentSize int `json:"max_comment_size"`
|
||||||
Admins []string `json:"admins"`
|
Admins []string `json:"admins"`
|
||||||
AdminEmail string `json:"admin_email"`
|
AdminEmail string `json:"admin_email"`
|
||||||
Auth []string `json:"auth_providers"`
|
Auth []string `json:"auth_providers"`
|
||||||
LowScore int `json:"low_score"`
|
LowScore int `json:"low_score"`
|
||||||
CriticalScore int `json:"critical_score"`
|
CriticalScore int `json:"critical_score"`
|
||||||
PositiveScore bool `json:"positive_score"`
|
PositiveScore bool `json:"positive_score"`
|
||||||
ReadOnlyAge int `json:"readonly_age"`
|
ReadOnlyAge int `json:"readonly_age"`
|
||||||
MaxImageSize int `json:"max_image_size"`
|
MaxImageSize int `json:"max_image_size"`
|
||||||
EmojiEnabled bool `json:"emoji_enabled"`
|
EmojiEnabled bool `json:"emoji_enabled"`
|
||||||
|
SubscribersOnly bool `json:"subscribers_only"` // enable commenting only for Patreon subscribers
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ title: Remark42 – Privacy focused lightweight commenting engine
|
|||||||
|
|
||||||
Remark42 gives you opportunity to have 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.
|
Remark42 gives you opportunity to have 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, Twitter, Facebook, Microsoft, GitHub, Yandex and Telegram
|
* Social login via Google, Twitter, Facebook, Microsoft, GitHub, Yandex, Patreon and Telegram
|
||||||
* Login via email
|
* Login via email
|
||||||
* Optional anonymous access
|
* Optional anonymous access
|
||||||
* Multi-level nested comments with both tree and plain presentations
|
* Multi-level nested comments with both tree and plain presentations
|
||||||
|
|||||||
Reference in New Issue
Block a user