Merge pull request #1144

patreon auth
This commit is contained in:
Umputun
2021-10-15 13:36:40 -05:00
committed by GitHub
14 changed files with 125 additions and 30 deletions
+25 -13
View File
@@ -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
} }
``` ```
+8
View File
@@ -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",
+3 -2
View File
@@ -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
+25 -1
View File
@@ -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
+35
View File
@@ -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++ {
+2
View File
@@ -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)
} }
+1
View File
@@ -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}$")
+2
View File
@@ -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
+1 -1
View File
@@ -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:
+14 -12
View File
@@ -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
} }
``` ```
+1 -1
View File
@@ -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