Compare commits

..
22 Commits
Author SHA1 Message Date
Dmitry VerkhoturovandUmputun 8357846818 add test JWT token generation instructions 2023-01-15 12:54:44 -06:00
Matt JacksonandUmputun 31ea91afb8 docs: added Astro w/Svelte Components Integration 2023-01-14 19:12:38 -06:00
Dmitry Verkhoturov 6616541f65 improve frontend documentation
Variables were documented in the documentation but not in the code,
and max_last_comments needed to be documented.
2023-01-10 23:53:26 +01:00
Paul MineevandUmputun 01695822bb fix: calculate correct size when no_footer=true 2023-01-10 11:27:44 -06:00
Paul MineevandUmputun f0186d1aab fix: fix no footer param 2023-01-10 11:27:44 -06:00
Dmitry VerkhoturovandUmputun 41a3359085 add the ability to set the JWS aud per site_id
Without this option, the aud is ignored.
It works only with RPC admin storage.

The shared key returned for all requests with the default shared admin
storage, so enabling that option does not affect it.
2023-01-10 11:24:41 -06:00
Dmitry Verkhoturov d6cce8df2c cleanup of the frontend code
- replace undocumented `substr` with `substring`
- remove unused code
- inline a few variables
- simplify ifs when possible
- improve saveCollapsedComments documentation
- cleanup the unused imports
- remove unused variables and types
2023-01-09 22:16:35 +01:00
Dmitry VerkhoturovandUmputun 596861a594 don't remove the twitter-tweet class from blockquote
This is needed to format the Twitter blockquotes as tweets.
2023-01-09 03:20:54 -06:00
Dmitry VerkhoturovandUmputun 385ea800a4 don't verify subscription email once more for email users
Previous behaviour is preserved for query parameters way of requesting
the subscription. The new behaviour with the possibility to confirm
the email right away without a separate /email/confirm call is enabled
only with request params sent in the request body, which was not a thing
before 27fc339e, which was merged just now and is not part
of any tagged version yet.
2023-01-09 03:17:25 -06:00
Dmitry Verkhoturov 27fc339e36 use the request body for email subscription endpoints
Previously, the endpoints were using query parameters.
After this change, the body is tried to be parsed.
2023-01-08 23:26:06 +01:00
Dmitry VerkhoturovandUmputun 61e2173f25 add e2e tests to makefile
Also, add missing entries to .dockerignore.
2023-01-08 14:31:36 -06:00
Dmitry VerkhoturovandUmputun 13a3fc3d1b move remark42 frontend nvmrc to /frontend/ 2023-01-08 14:31:15 -06:00
Dmitry VerkhoturovandUmputun 6a1b515ea9 add anti-spam documentation
It describes how anti-spam works now and its future.
2023-01-08 14:30:55 -06:00
Dmitry VerkhoturovandUmputun 82f27e6b63 fix typos in frontend code 2023-01-08 12:30:18 -06:00
Paul MineevandUmputun 6ac75031ad fix and optimize apple icon 2023-01-07 18:16:21 -06:00
Dmitry VerkhoturovandUmputun 8b7f1331ee add Apple auth provider frontend support
With distinct logos for light and dark theme from
https://devimages-cdn.apple.com/design/resources/download/Logo-Sign-in-with-Apple.dmg
2023-01-07 18:16:21 -06:00
Dmitry VerkhoturovandUmputun 099aad8475 add apple bad key test, fix key location
Previously, default location was outside of container mount.
2023-01-04 03:54:38 -06:00
Dmitry VerkhoturovandUmputun c1b3fba344 add backend support for Apple auth provider
It's a bit different from other OAuth providers and requires a
different set of options and a private key file.
2023-01-03 23:47:42 -06:00
Dmitry VerkhoturovandUmputun d7e9be99f9 make Close() calls idempotent
Previously, few of them resulted in panics when called more than once.
2023-01-03 01:41:26 -06:00
Dmitry Verkhoturov 067a8bcb21 make the email token tooltip more informative
Previously it said just "Token", but now it will provide more explicit
instructions about copying and pasting the token received by email.

Resolves #1339
2023-01-03 10:52:17 +04:00
Umputun f5569a62f1 add local analytic support 2022-12-25 18:45:31 -06:00
Jakub FridrichandUmputun 1ab1ed8a82 Added cs lang 2022-12-16 12:00:57 -06:00
117 changed files with 1112 additions and 341 deletions
+2
View File
@@ -21,8 +21,10 @@ compose-dev-backend.yml
compose-dev-frontend.yml compose-dev-frontend.yml
compose-private-backend.yml compose-private-backend.yml
compose-private-frontend.yml compose-private-frontend.yml
compose-e2e-test.yml
compose-private.yml compose-private.yml
rest-client.env.json rest-client.env.json
Makefile
# generated files # generated files
*.cov *.cov
+3
View File
@@ -49,4 +49,7 @@ rundev:
docker-compose -f compose-private.yml build docker-compose -f compose-private.yml build
docker-compose -f compose-private.yml up docker-compose -f compose-private.yml up
e2e:
docker compose -f compose-e2e-test.yml up --build --quiet-pull --exit-code-from tests
.PHONY: bin backend .PHONY: bin backend
@@ -35,7 +35,8 @@ func NewMemAdminStore(key string) *MemAdmin {
return &MemAdmin{data: map[string]AdminRec{}, key: key} return &MemAdmin{data: map[string]AdminRec{}, key: key}
} }
// Key executes find by siteID and returns substructure with secret key // Key supposed to execute find by siteID and returns substructure with secret key,
// but in this case the shared secret is used for all sites
func (m *MemAdmin) Key(_ string) (key string, err error) { func (m *MemAdmin) Key(_ string) (key string, err error) {
return m.key, nil return m.key, nil
} }
@@ -624,6 +624,7 @@ func TestMemData_DeleteComment(t *testing.T) {
func TestMemData_Close(t *testing.T) { func TestMemData_Close(t *testing.T) {
b := prepMem(t) b := prepMem(t)
assert.NoError(t, b.Close()) assert.NoError(t, b.Close())
assert.NoError(t, b.Close(), "second call should not result in panic or errors")
} }
func TestMemData_DeleteHard(t *testing.T) { func TestMemData_DeleteHard(t *testing.T) {
+1 -1
View File
@@ -6,7 +6,7 @@ require (
github.com/go-pkgz/jrpc v0.3.0 github.com/go-pkgz/jrpc v0.3.0
github.com/go-pkgz/lgr v0.10.4 github.com/go-pkgz/lgr v0.10.4
github.com/jessevdk/go-flags v1.5.0 github.com/jessevdk/go-flags v1.5.0
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.1
github.com/umputun/remark42/backend v1.10.1 github.com/umputun/remark42/backend v1.10.1
) )
+3 -1
View File
@@ -60,11 +60,13 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw=
@@ -338,6 +338,6 @@ func TestRPC_closeHndl(t *testing.T) {
api := fmt.Sprintf("http://localhost:%d/test", port) api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}} re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
err := re.Close() assert.NoError(t, re.Close())
assert.NoError(t, err) assert.NoError(t, re.Close(), "second call should not result in panic or errors")
} }
+45 -11
View File
@@ -96,16 +96,17 @@ type ServerCommand struct {
SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"` SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"`
SameSite string `long:"same-site" env:"SAME_SITE" description:"set same site policy for cookies" choice:"default" choice:"none" choice:"lax" choice:"strict" default:"default"` // nolint SameSite string `long:"same-site" env:"SAME_SITE" description:"set same site policy for cookies" choice:"default" choice:"none" choice:"lax" choice:"strict" default:"default"` // nolint
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"` Apple AppleGroup `group:"apple" namespace:"apple" env-namespace:"APPLE" description:"Apple OAuth"`
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"` Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"` Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"` Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"` Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"` Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"` Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"` Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"`
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"` Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"` Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"`
Email struct { Email struct {
Enable bool `long:"enable" env:"ENABLE" description:"enable auth via email"` Enable bool `long:"enable" env:"ENABLE" description:"enable auth via email"`
From string `long:"from" env:"FROM" description:"from email address"` From string `long:"from" env:"FROM" description:"from email address"`
@@ -133,6 +134,14 @@ type ImageProxyGroup struct {
CacheExternal bool `long:"cache-external" env:"CACHE_EXTERNAL" description:"enable caching for external images"` CacheExternal bool `long:"cache-external" env:"CACHE_EXTERNAL" description:"enable caching for external images"`
} }
// AppleGroup defines options for Apple auth params
type AppleGroup struct {
CID string `long:"cid" env:"CID" description:"Apple client ID"`
TID string `long:"tid" env:"TID" description:"Apple service ID"`
KID string `long:"kid" env:"KID" description:"Private key ID"`
PrivateKeyFilePath string `long:"private-key-filepath" env:"PRIVATE_KEY_FILEPATH" description:"Private key file location" default:"/srv/var/apple.p8"`
}
// AuthGroup defines options group for auth params // AuthGroup defines options group for auth params
type AuthGroup struct { type AuthGroup struct {
CID string `long:"cid" env:"CID" description:"OAuth client ID"` CID string `long:"cid" env:"CID" description:"OAuth client ID"`
@@ -197,7 +206,7 @@ type AdminGroup struct {
Admins []string `long:"id" env:"ID" description:"admin(s) ids" env-delim:","` Admins []string `long:"id" env:"ID" description:"admin(s) ids" env-delim:","`
Email []string `long:"email" env:"EMAIL" description:"admin emails" env-delim:","` Email []string `long:"email" env:"EMAIL" description:"admin emails" env-delim:","`
} `group:"shared" namespace:"shared" env-namespace:"SHARED"` } `group:"shared" namespace:"shared" env-namespace:"SHARED"`
RPC RPCGroup `group:"rpc" namespace:"rpc" env-namespace:"RPC"` RPC AdminRPCGroup `group:"rpc" namespace:"rpc" env-namespace:"RPC"`
} }
// TelegramGroup defines token for Telegram used in notify and auth modules // TelegramGroup defines token for Telegram used in notify and auth modules
@@ -265,6 +274,12 @@ type RPCGroup struct {
AuthPassword string `long:"auth_passwd" env:"AUTH_PASSWD" description:"basic auth user password"` AuthPassword string `long:"auth_passwd" env:"AUTH_PASSWD" description:"basic auth user password"`
} }
// AdminRPCGroup defines options for remote admin store
type AdminRPCGroup struct {
RPCGroup
SecretPerSite bool `long:"secret_per_site" env:"SECRET_PER_SITE" description:"enable JWT secret retrieval per aud, which is site_id in this case"`
}
// LoadingCache defines interface for caching // LoadingCache defines interface for caching
type LoadingCache interface { type LoadingCache interface {
Get(key cache.Key, fn func() ([]byte, error)) (data []byte, err error) // load from cache if found or put to cache and return Get(key cache.Key, fn func() ([]byte, error)) (data []byte, err error) // load from cache if found or put to cache and return
@@ -515,6 +530,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
err = s.addAuthProviders(authenticator) err = s.addAuthProviders(authenticator)
if err != nil { if err != nil {
_ = dataService.Close() _ = dataService.Close()
_ = authRefreshCache.Close()
return nil, fmt.Errorf("failed to make authenticator: %w", err) return nil, fmt.Errorf("failed to make authenticator: %w", err)
} }
@@ -554,6 +570,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
sslConfig, err := s.makeSSLConfig() sslConfig, err := s.makeSSLConfig()
if err != nil { if err != nil {
_ = dataService.Close() _ = dataService.Close()
_ = authRefreshCache.Close()
return nil, fmt.Errorf("failed to make config of ssl server params: %w", err) return nil, fmt.Errorf("failed to make config of ssl server params: %w", err)
} }
@@ -594,6 +611,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
da, errDevAuth := authenticator.DevAuth() da, errDevAuth := authenticator.DevAuth()
if errDevAuth != nil { if errDevAuth != nil {
_ = dataService.Close() _ = dataService.Close()
_ = authRefreshCache.Close()
return nil, fmt.Errorf("can't make dev oauth2 server: %w", errDevAuth) return nil, fmt.Errorf("can't make dev oauth2 server: %w", errDevAuth)
} }
devAuth = da devAuth = da
@@ -829,12 +847,27 @@ func (s *ServerCommand) makeCache() (LoadingCache, error) {
return nil, fmt.Errorf("unsupported cache type %s", s.Cache.Type) return nil, fmt.Errorf("unsupported cache type %s", s.Cache.Type)
} }
//nolint:gocyclo // simple code but many if checks
func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error { func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
providersCount := 0 providersCount := 0
if s.Auth.Telegram { if s.Auth.Telegram {
providersCount++ providersCount++
} }
if s.Auth.Apple.CID != "" && s.Auth.Apple.TID != "" && s.Auth.Apple.KID != "" {
err := authenticator.AddAppleProvider(
provider.AppleConfig{
ClientID: s.Auth.Apple.CID,
TeamID: s.Auth.Apple.TID,
KeyID: s.Auth.Apple.KID,
},
provider.LoadApplePrivateKeyFromFile(s.Auth.Apple.PrivateKeyFilePath),
)
if err != nil {
return err
}
providersCount++
}
if s.Auth.Google.CID != "" && s.Auth.Google.CSEC != "" { if s.Auth.Google.CID != "" && s.Auth.Google.CSEC != "" {
authenticator.AddProvider("google", s.Auth.Google.CID, s.Auth.Google.CSEC) authenticator.AddProvider("google", s.Auth.Google.CID, s.Auth.Google.CSEC)
providersCount++ providersCount++
@@ -1157,6 +1190,7 @@ func (s *ServerCommand) getAuthenticator(ds *service.DataStore, avas avatar.Stor
Logger: log.Default(), Logger: log.Default(),
RefreshCache: authRefreshCache, RefreshCache: authRefreshCache,
UseGravatar: true, UseGravatar: true,
AudSecrets: s.Admin.RPC.SecretPerSite,
}) })
} }
+20 -3
View File
@@ -79,7 +79,7 @@ func TestServerApp_DevMode(t *testing.T) {
waitForHTTPServerStart(port) waitForHTTPServerStart(port)
providers := app.restSrv.Authenticator.Providers() providers := app.restSrv.Authenticator.Providers()
require.Equal(t, 9+1, len(providers), "extra auth provider") require.Equal(t, 10+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))
@@ -107,7 +107,7 @@ func TestServerApp_AnonMode(t *testing.T) {
waitForHTTPServerStart(port) waitForHTTPServerStart(port)
providers := app.restSrv.Authenticator.Providers() providers := app.restSrv.Authenticator.Providers()
require.Equal(t, 9+1, len(providers), "extra auth provider for anon") require.Equal(t, 10+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")
client := http.Client{Timeout: 10 * time.Second} client := http.Client{Timeout: 10 * time.Second}
@@ -290,7 +290,8 @@ func TestServerApp_WithRemote(t *testing.T) {
port := chooseRandomUnusedPort() port := chooseRandomUnusedPort()
_, err := p.ParseArgs([]string{"--admin-passwd=password", "--cache.type=none", _, err := p.ParseArgs([]string{"--admin-passwd=password", "--cache.type=none",
"--store.type=rpc", "--store.rpc.api=http://127.0.0.1", "--store.type=rpc", "--store.rpc.api=http://127.0.0.1",
"--port=" + strconv.Itoa(port), "--admin.type=rpc", "--admin.rpc.api=http://127.0.0.1", "--avatar.fs.path=/tmp"}) "--port=" + strconv.Itoa(port), "--avatar.fs.path=/tmp",
"--admin.type=rpc", "--admin.rpc.secret_per_site", "--admin.rpc.api=http://127.0.0.1"})
require.NoError(t, err) require.NoError(t, err)
opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid" opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid"
opts.BackupLocation, opts.Image.FS.Path = "/tmp", "/tmp" opts.BackupLocation, opts.Image.FS.Path = "/tmp", "/tmp"
@@ -351,6 +352,7 @@ func TestServerApp_Failed(t *testing.T) {
assert.EqualError(t, err, "invalid remark42 url demo.remark42.com") assert.EqualError(t, err, "invalid remark42 url demo.remark42.com")
t.Log(err) t.Log(err)
// wrong store type
opts = ServerCommand{} opts = ServerCommand{}
opts.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"}) opts.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"})
@@ -374,6 +376,19 @@ func TestServerApp_Failed(t *testing.T) {
"problem subscribing to channel remark42-cache on address wrong_address: "+ "problem subscribing to channel remark42-cache on address wrong_address: "+
"dial tcp: address wrong_address: missing port in address") "dial tcp: address wrong_address: missing port in address")
t.Log(err) t.Log(err)
// wrong apple private key type
opts = ServerCommand{}
opts.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"})
p = flags.NewParser(&opts, flags.Default)
_, err = p.ParseArgs([]string{"--auth.apple.cid=123", "--auth.apple.tid=123",
"--auth.apple.kid=123", "--auth.apple.private-key-filepath=testdata/apple-bad.p8"})
assert.NoError(t, err)
_, err = opts.newServerApp(context.Background())
assert.EqualError(t, err,
"failed to make authenticator: an AppleProvider creating failed: "+
"provided private key is not ECDSA")
t.Log(err)
} }
func TestServerApp_Shutdown(t *testing.T) { func TestServerApp_Shutdown(t *testing.T) {
@@ -758,6 +773,8 @@ func prepServerApp(t *testing.T, fn func(o ServerCommand) ServerCommand) (*serve
cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation, cmd.Image.FS.Path = "/tmp/remark42_test", "fs", "/tmp/remark42_test", "/tmp/remark42_test" cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation, cmd.Image.FS.Path = "/tmp/remark42_test", "fs", "/tmp/remark42_test", "/tmp/remark42_test"
cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port) cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port)
cmd.Store.Bolt.Timeout = 10 * time.Second cmd.Store.Bolt.Timeout = 10 * time.Second
cmd.Auth.Apple.CID, cmd.Auth.Apple.KID, cmd.Auth.Apple.TID = "cid", "kid", "tid"
cmd.Auth.Apple.PrivateKeyFilePath = "testdata/apple.p8"
cmd.Auth.Github.CSEC, cmd.Auth.Github.CID = "csec", "cid" cmd.Auth.Github.CSEC, cmd.Auth.Github.CID = "csec", "cid"
cmd.Auth.Google.CSEC, cmd.Auth.Google.CID = "csec", "cid" cmd.Auth.Google.CSEC, cmd.Auth.Google.CID = "csec", "cid"
cmd.Auth.Facebook.CSEC, cmd.Auth.Facebook.CID = "csec", "cid" cmd.Auth.Facebook.CSEC, cmd.Auth.Facebook.CID = "csec", "cid"
+16
View File
@@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKNwapOQ6rQJHetP
HRlJBIh1OsOsUBiXb3rXXE3xpWAxAha0MH+UPRblOko+5T2JqIb+xKf9Vi3oTM3t
KvffaOPtzKXZauscjq6NGzA3LgeiMy6q19pvkUUOlGYK6+Xfl+B7Xw6+hBMkQuGE
nUS8nkpR5mK4ne7djIyfHFfMu4ptAgMBAAECgYA+s0PPtMq1osG9oi4xoxeAGikf
JB3eMUptP+2DYW7mRibc+ueYKhB9lhcUoKhlQUhL8bUUFVZYakP8xD21thmQqnC4
f63asad0ycteJMLb3r+z26LHuCyOdPg1pyLk3oQ32lVQHBCYathRMcVznxOG16VK
I8BFfstJTaJu0lK/wQJBANYFGusBiZsJQ3utrQMVPpKmloO2++4q1v6ZR4puDQHx
TjLjAIgrkYfwTJBLBRZxec0E7TmuVQ9uJ+wMu/+7zaUCQQDDf2xMnQqYknJoKGq+
oAnyC66UqWC5xAnQS32mlnJ632JXA0pf9pb1SXAYExB1p9Dfqd3VAwQDwBsDDgP6
HD8pAkEA0lscNQZC2TaGtKZk2hXkdcH1SKru/g3vWTkRHxfCAznJUaza1fx0wzdG
GcES1Bdez0tbW4llI5By/skZc2eE3QJAFl6fOskBbGHde3Oce0F+wdZ6XIJhEgCP
iukIcKZoZQzoiMJUoVRrA5gqnmaYDI5uRRl/y57zt6YksR3KcLUIuQJAd242M/WF
6YAZat3q/wEeETeQq1wrooew+8lHl05/Nt0cCpV48RGEhJ83pzBm3mnwHf8lTBJH
x6XroMXsmbnsEw==
-----END PRIVATE KEY-----
+6
View File
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgGH2MylyZjjRdauTk
xxXW6p8VSHqIeVRRKSJPg1xn6+KgCgYIKoZIzj0DAQehRANCAAS/mNzQ7aBbIBr3
DiHiJGIDEzi6+q3mmyhH6ZWQWFdFei2qgdyM1V6qtRPVq+yHBNSBebbR4noE/IYO
hMdWYrKn
-----END PRIVATE KEY-----
+6
View File
@@ -143,6 +143,12 @@ func (s *Service) SubmitVerification(req VerificationRequest) {
// Close queue channel and wait for completion // Close queue channel and wait for completion
func (s *Service) Close() { func (s *Service) Close() {
if s.queue != nil { if s.queue != nil {
// don't panic in case service is already closed
select {
case <-s.ctx.Done():
return
default:
}
log.Print("[DEBUG] close notifier") log.Print("[DEBUG] close notifier")
close(s.queue) close(s.queue)
close(s.verificationQueue) close(s.verificationQueue)
+2
View File
@@ -21,6 +21,8 @@ func TestService_NoDestinations(t *testing.T) {
s.Submit(Request{Comment: store.Comment{ID: "123"}}) s.Submit(Request{Comment: store.Comment{ID: "123"}})
s.Submit(Request{Comment: store.Comment{ID: "123"}}) s.Submit(Request{Comment: store.Comment{ID: "123"}})
s.Close() s.Close()
// second call should not result in panic
s.Close()
} }
func TestService_WithDestinations(t *testing.T) { func TestService_WithDestinations(t *testing.T) {
+69 -23
View File
@@ -187,7 +187,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
}{} }{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil { if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment", rest.ErrDecode) rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't read comment details from body", rest.ErrDecode)
return return
} }
@@ -307,28 +307,59 @@ func (s *private) getEmailCtrl(w http.ResponseWriter, r *http.Request) {
} }
// sendEmailConfirmationCtrl gets address and siteID from query, makes confirmation token and sends it to user. // sendEmailConfirmationCtrl gets address and siteID from query, makes confirmation token and sends it to user.
// GET /email/subscribe?site=siteID&address=someone@example.com // In case user is logged in with the same email, and auto_confirm is true, confirm it right away.
// In case of quick confirmation, "updated" is set to true, otherwise - to false.
// POST /email/subscribe with site and address in json body
//
//nolint:dupl // too hard to deduplicate that logic, as then it's tricky to use SendErrorJSON //nolint:dupl // too hard to deduplicate that logic, as then it's tricky to use SendErrorJSON
func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Request) { func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Request) {
user := rest.MustGetUserInfo(r) user := rest.MustGetUserInfo(r)
address := r.URL.Query().Get("address")
siteID := r.URL.Query().Get("site") subscribe := struct {
if address == "" { Site string
Address string
autoConfirm bool
}{autoConfirm: true}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &subscribe); err != nil {
if err != io.EOF {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode)
return
}
// old behavior fallback, reading from the query params. Auto confirm is false in this case.
subscribe.Address = r.URL.Query().Get("address")
subscribe.Site = r.URL.Query().Get("site")
subscribe.autoConfirm = false
}
if subscribe.Address == "" {
rest.SendErrorJSON(w, r, http.StatusBadRequest, rest.SendErrorJSON(w, r, http.StatusBadRequest,
fmt.Errorf("missing parameter"), "address parameter is required", rest.ErrInternal) fmt.Errorf("missing parameter"), "address parameter is required", rest.ErrInternal)
return return
} }
existingAddress, err := s.dataService.GetUserEmail(siteID, user.ID) existingAddress, getErr := s.dataService.GetUserEmail(subscribe.Site, user.ID)
if err != nil { if getErr != nil {
log.Printf("[WARN] can't read email for %s, %v", user.ID, err) log.Printf("[WARN] can't read email for %s, %v", user.ID, getErr)
} }
if address == existingAddress { if subscribe.Address == existingAddress {
rest.SendErrorJSON(w, r, http.StatusConflict, rest.SendErrorJSON(w, r, http.StatusConflict,
fmt.Errorf("already verified"), "email address is already verified for this user", rest.ErrInternal) fmt.Errorf("already verified"), "email address is already verified for this user", rest.ErrInternal)
return return
} }
// in case the user logged in with the same email as they try to subscribe with, confirm it right away
// this behavior is different from the previous one and is hidden behind the autoConfirm flag,
// which is true for the new API, and false for the old one
//
// nolint:gosec // this is not used for security purposes
if subscribe.autoConfirm &&
strings.HasPrefix(user.ID, "email_") &&
strings.TrimPrefix(user.ID, "email_") == token.HashID(sha1.New(), subscribe.Address) {
s.setEmail(w, r, user.ID, subscribe.Site, subscribe.Address)
return
}
claims := token.Claims{ claims := token.Claims{
Handshake: &token.Handshake{ID: user.ID + "::" + address}, Handshake: &token.Handshake{ID: user.ID + "::" + subscribe.Address},
StandardClaims: jwt.StandardClaims{ StandardClaims: jwt.StandardClaims{
Audience: r.URL.Query().Get("site"), Audience: r.URL.Query().Get("site"),
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
@@ -345,14 +376,14 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
s.notifyService.SubmitVerification( s.notifyService.SubmitVerification(
notify.VerificationRequest{ notify.VerificationRequest{
SiteID: siteID, SiteID: subscribe.Site,
User: user.Name, User: user.Name,
Email: address, Email: subscribe.Address,
Token: tkn, Token: tkn,
}, },
) )
render.JSON(w, r, R.JSON{"user": user, "address": address}) render.JSON(w, r, R.JSON{"user": user, "address": subscribe.Address, "updated": false})
} }
// telegramSubscribeCtrl generates and verifies telegram notification request // telegramSubscribeCtrl generates and verifies telegram notification request
@@ -416,17 +447,29 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request)
} }
// setConfirmedEmailCtrl uses provided token parameter (generated by sendEmailConfirmationCtrl) to set email and add it to user token // setConfirmedEmailCtrl uses provided token parameter (generated by sendEmailConfirmationCtrl) to set email and add it to user token
// PUT /email/confirm?site=siteID&tkn=jwt // POST /email/confirm with site and token in json body
func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request) { func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request) {
tkn := r.URL.Query().Get("tkn") user := rest.MustGetUserInfo(r)
if tkn == "" {
confirm := struct {
Site string
Token string
}{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &confirm); err != nil {
if err != io.EOF {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode)
return
}
// old behavior fallback, reading from the query params
confirm.Token = r.URL.Query().Get("tkn")
confirm.Site = r.URL.Query().Get("site")
}
if confirm.Token == "" {
rest.SendErrorJSON(w, r, http.StatusBadRequest, fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal) rest.SendErrorJSON(w, r, http.StatusBadRequest, fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal)
return return
} }
user := rest.MustGetUserInfo(r) confClaims, err := s.authenticator.TokenService().Parse(confirm.Token)
siteID := r.URL.Query().Get("site")
confClaims, err := s.authenticator.TokenService().Parse(tkn)
if err != nil { if err != nil {
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal) rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
return return
@@ -444,17 +487,20 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
return return
} }
address := elems[1] address := elems[1]
s.setEmail(w, r, user.ID, confirm.Site, address)
}
log.Printf("[DEBUG] set email for user %s", user.ID) func (s *private) setEmail(w http.ResponseWriter, r *http.Request, userID, siteID, address string) {
log.Printf("[DEBUG] set email for user %s", userID)
val, err := s.dataService.SetUserEmail(siteID, user.ID, address) val, err := s.dataService.SetUserEmail(siteID, userID, address)
if err != nil { if err != nil {
code := parseError(err, rest.ErrInternal) code := parseError(err, rest.ErrInternal)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set email for user", code) rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set email for user", code)
return return
} }
// update User.Email from the token // update User.Email field
claims, _, err := s.authenticator.TokenService().Get(r) claims, _, err := s.authenticator.TokenService().Get(r)
if err != nil { if err != nil {
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal) rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
+126 -12
View File
@@ -728,19 +728,25 @@ func TestRest_EmailAndTelegram(t *testing.T) {
responseCode int responseCode int
noAuth bool noAuth bool
cookieEmail string cookieEmail string
body string
}{ }{
{description: "issue delete request without auth", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusUnauthorized, noAuth: true}, {description: "issue delete request without auth", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusUnauthorized, noAuth: true},
{description: "issue delete request without site_id", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusBadRequest}, {description: "issue delete request without site_id", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusBadRequest},
{description: "delete non-existent user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK}, {description: "delete non-existent user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
{description: "set user email, token not set", url: "/api/v1/email/confirm?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest}, {description: "set user email, token not set", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusBadRequest, body: `{"site":"remark42"}`},
{description: "send email confirmation without address", url: "/api/v1/email/subscribe?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest}, {description: "set user email, token not set, old query param", url: "/api/v1/email/confirm?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
{description: "send email confirmation", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK}, {description: "send email confirmation without address", url: "/api/v1/email/subscribe", method: http.MethodPost, responseCode: http.StatusBadRequest, body: `{"site":"remark42"}`},
{description: "set user email, token is good", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"}, {description: "send email confirmation without address, old query param", url: "/api/v1/email/subscribe?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
{description: "send email confirmation", url: "/api/v1/email/subscribe", method: http.MethodPost, responseCode: http.StatusOK, body: `{"site":"remark42","address":"good@example.com"}`},
{description: "send email confirmation, old query param", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
{description: "set user email, token is good", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com", body: fmt.Sprintf(`{"site":"remark42","token":%q}`, goodToken)},
{description: "set user email, token is good, old query param", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
{description: "send confirmation with same address", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusConflict}, {description: "send confirmation with same address", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusConflict},
{description: "get user email", url: "/api/v1/email?site=remark42", method: http.MethodGet, responseCode: http.StatusOK}, {description: "get user email", url: "/api/v1/email?site=remark42", method: http.MethodGet, responseCode: http.StatusOK},
{description: "delete user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK}, {description: "delete user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
{description: "send another confirmation", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK}, {description: "send another confirmation", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
{description: "set user email, token is good", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"}, {description: "set user email, token is good", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com", body: fmt.Sprintf(`{"site":"remark42","token":%q}`, goodToken)},
{description: "set user email, token is good, old query param", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
{description: "unsubscribe user, no token", url: "/email/unsubscribe.html?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest}, {description: "unsubscribe user, no token", url: "/email/unsubscribe.html?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
{description: "unsubscribe user, wrong token", url: "/email/unsubscribe.html?site=remark42&tkn=jwt", method: http.MethodGet, responseCode: http.StatusForbidden}, {description: "unsubscribe user, wrong token", url: "/email/unsubscribe.html?site=remark42&tkn=jwt", method: http.MethodGet, responseCode: http.StatusForbidden},
{description: "unsubscribe user, good token", url: fmt.Sprintf("/email/unsubscribe.html?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK}, {description: "unsubscribe user, good token", url: fmt.Sprintf("/email/unsubscribe.html?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK},
@@ -761,7 +767,11 @@ func TestRest_EmailAndTelegram(t *testing.T) {
for _, x := range testData { for _, x := range testData {
x := x x := x
t.Run(x.description, func(t *testing.T) { t.Run(x.description, func(t *testing.T) {
req, err := http.NewRequest(x.method, ts.URL+x.url, http.NoBody) reqBody := io.NopCloser(strings.NewReader(x.body))
if x.body == "" {
reqBody = http.NoBody
}
req, err := http.NewRequest(x.method, ts.URL+x.url, reqBody)
require.NoError(t, err) require.NoError(t, err)
if !x.noAuth { if !x.noAuth {
req.Header.Add("X-JWT", devToken) req.Header.Add("X-JWT", devToken)
@@ -837,7 +847,11 @@ func TestRest_EmailNotification(t *testing.T) {
assert.Empty(t, mockDestination.Get()[1].Emails) assert.Empty(t, mockDestination.Get()[1].Emails)
// send confirmation token for email // send confirmation token for email
req, err = http.NewRequest(http.MethodPost, ts.URL+"/api/v1/email/subscribe?site=remark42&address=good@example.com", http.NoBody) req, err = http.NewRequest(
http.MethodPost,
ts.URL+"/api/v1/email/subscribe",
io.NopCloser(strings.NewReader(`{"site": "remark42", "address": "good@example.com"}`)),
)
require.NoError(t, err) require.NoError(t, err)
req.Header.Add("X-JWT", devToken) req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req) resp, err = client.Do(req)
@@ -852,8 +866,31 @@ func TestRest_EmailNotification(t *testing.T) {
assert.Equal(t, "good@example.com", mockDestination.GetVerify()[0].Email) assert.Equal(t, "good@example.com", mockDestination.GetVerify()[0].Email)
verificationToken := mockDestination.GetVerify()[0].Token verificationToken := mockDestination.GetVerify()[0].Token
// get user information to verify lack of the subscription
req, err = http.NewRequest(
http.MethodGet,
ts.URL+"/api/v1/user?site=remark42",
http.NoBody)
require.NoError(t, err)
req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
var clearUser store.User
err = json.Unmarshal(body, &clearUser)
assert.NoError(t, err)
assert.Equal(t, store.User{Name: "developer one", ID: "dev", EmailSubscription: false,
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, clearUser)
// verify email // verify email
req, err = http.NewRequest(http.MethodPost, ts.URL+fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", verificationToken), http.NoBody) req, err = http.NewRequest(
http.MethodPost,
ts.URL+"/api/v1/email/confirm",
io.NopCloser(strings.NewReader(fmt.Sprintf(`{"site": "remark42", "token": %q}`, verificationToken))),
)
require.NoError(t, err) require.NoError(t, err)
req.Header.Add("X-JWT", devToken) req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req) resp, err = client.Do(req)
@@ -864,7 +901,10 @@ func TestRest_EmailNotification(t *testing.T) {
require.Equal(t, http.StatusOK, resp.StatusCode, string(body)) require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
// get user information to verify the subscription // get user information to verify the subscription
req, err = http.NewRequest(http.MethodGet, ts.URL+"/api/v1/user?site=remark42", http.NoBody) req, err = http.NewRequest(
http.MethodGet,
ts.URL+"/api/v1/user?site=remark42",
http.NoBody)
require.NoError(t, err) require.NoError(t, err)
req.Header.Add("X-JWT", devToken) req.Header.Add("X-JWT", devToken)
resp, err = client.Do(req) resp, err = client.Do(req)
@@ -873,11 +913,11 @@ func TestRest_EmailNotification(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, resp.Body.Close()) require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body)) require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
var user store.User var subscribedUser store.User
err = json.Unmarshal(body, &user) err = json.Unmarshal(body, &subscribedUser)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, store.User{Name: "developer one", ID: "dev", EmailSubscription: true, assert.Equal(t, store.User{Name: "developer one", ID: "dev", EmailSubscription: true,
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, user) Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, subscribedUser)
// create child comment from another user, email notification expected // create child comment from another user, email notification expected
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(fmt.Sprintf( req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(fmt.Sprintf(
@@ -928,6 +968,80 @@ func TestRest_EmailNotification(t *testing.T) {
time.Sleep(time.Millisecond * 30) time.Sleep(time.Millisecond * 30)
require.Equal(t, 4, len(mockDestination.Get())) require.Equal(t, 4, len(mockDestination.Get()))
assert.Empty(t, mockDestination.Get()[3].Emails) assert.Empty(t, mockDestination.Get()[3].Emails)
// confirm email via subscribe call with query params, old behavior, email notification is expected
req, err = http.NewRequest(
http.MethodPost,
ts.URL+"/api/v1/email/subscribe?site=remark42&address=good@example.com",
http.NoBody,
)
require.NoError(t, err)
req.Header.Add("X-JWT", emailUserToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
// wait for mock notification Submit to kick off
time.Sleep(time.Millisecond * 30)
require.Equal(t, 2, len(mockDestination.GetVerify()), "verification email was sent")
// get email user information to verify there is no subscription yet
req, err = http.NewRequest(
http.MethodGet,
ts.URL+"/api/v1/user?site=remark42",
http.NoBody)
require.NoError(t, err)
req.Header.Add("X-JWT", emailUserToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
var unsubscribedEmailUser store.User
err = json.Unmarshal(body, &unsubscribedEmailUser)
assert.NoError(t, err)
assert.Equal(t, store.User{Name: "good@example.com test user", ID: "email_f5dfe9d2e6bd75fc74ea5fabf273b45b5baeb195", EmailSubscription: false,
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, unsubscribedEmailUser)
// confirm email via subscribe call, no email notification is expected
req, err = http.NewRequest(
http.MethodPost,
ts.URL+"/api/v1/email/subscribe",
io.NopCloser(strings.NewReader(`{"site": "remark42", "address": "good@example.com"}`)),
)
require.NoError(t, err)
req.Header.Add("X-JWT", emailUserToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
// wait for mock notification Submit to kick off
time.Sleep(time.Millisecond * 30)
require.Equal(t, 2, len(mockDestination.GetVerify()), "no new verification email was sent")
// get email user information to verify the subscription happened without the confirmation call
req, err = http.NewRequest(
http.MethodGet,
ts.URL+"/api/v1/user?site=remark42",
http.NoBody)
require.NoError(t, err)
req.Header.Add("X-JWT", emailUserToken)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
var subscribedEmailUser store.User
err = json.Unmarshal(body, &subscribedEmailUser)
assert.NoError(t, err)
assert.Equal(t, store.User{Name: "good@example.com test user", ID: "email_f5dfe9d2e6bd75fc74ea5fabf273b45b5baeb195", EmailSubscription: true,
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, subscribedEmailUser)
} }
func TestRest_TelegramNotification(t *testing.T) { func TestRest_TelegramNotification(t *testing.T) {
+5
View File
@@ -37,10 +37,15 @@ import (
"github.com/umputun/remark42/backend/app/store/service" "github.com/umputun/remark42/backend/app/store/service"
) )
// To generate a token, enter one of the tokens here into https://jwt.io, change the secret to one you're using in your test
// ("secret" in case of startupT), and alter the fields you want to be changed.
var devToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg` var devToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg`
var anonToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImFub255bW91cyB0ZXN0IHVzZXIiLCJpZCI6ImFub255bW91c190ZXN0X3VzZXIiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoiYW5vbkBleGFtcGxlLmNvbSJ9fQ.gAae2WMxZNZE5ebVboptPEyQ7Nk6EQxciNnGJ_mPOuU` var anonToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImFub255bW91cyB0ZXN0IHVzZXIiLCJpZCI6ImFub255bW91c190ZXN0X3VzZXIiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoiYW5vbkBleGFtcGxlLmNvbSJ9fQ.gAae2WMxZNZE5ebVboptPEyQ7Nk6EQxciNnGJ_mPOuU`
var emailUserToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6Imdvb2RAZXhhbXBsZS5jb20gdGVzdCB1c2VyIiwiaWQiOiJlbWFpbF9mNWRmZTlkMmU2YmQ3NWZjNzRlYTVmYWJmMjczYjQ1YjViYWViMTk1IiwicGljdHVyZSI6Imh0dHA6Ly9leGFtcGxlLmNvbS9waWMucG5nIiwiaXAiOiIxMjcuMC4wLjEiLCJlbWFpbCI6Imdvb2RAZXhhbXBsZS5jb20ifX0.vH2HN1JpuXL8okTJq1A-zGHQ-l2ILcwxvDDEmu2zwks`
var devTokenBadAud = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0Ml9iYWQiLCJleHAiOjM3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTIxODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJkZXZlbG9wZXIgb25lIiwiaWQiOiJkZXYiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoibWVAZXhhbXBsZS5jb20ifX0.FuTTocVtcxr4VjpfIICvU2yOb3su28VkDzj94H9Q3xY` var devTokenBadAud = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0Ml9iYWQiLCJleHAiOjM3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTIxODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJkZXZlbG9wZXIgb25lIiwiaWQiOiJkZXYiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoibWVAZXhhbXBsZS5jb20ifX0.FuTTocVtcxr4VjpfIICvU2yOb3su28VkDzj94H9Q3xY`
var adminUmputunToken = `eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTk1NDU5Nzk4MCwianRpIjoiOTdhMmUwYWM0ZGM3ZDVmNjkyNmQ1ZTg2MjBhY2VmOWE0MGMwIiwiaWF0IjoxNDU0NTk3NjgwLCJpc3MiOiJyZW1hcms0MiIsInVzZXIiOnsibmFtZSI6IlVtcHV0dW4iLCJpZCI6ImdpdGh1Yl9lZjBmNzA2YTciLCJwaWN0dXJlIjoiaHR0cHM6Ly9yZW1hcms0Mi5yYWRpby10LmNvbS9hcGkvdjEvYXZhdGFyL2NiNDJmZjQ5M2FkZTY5NmQ4OGEzYTU5MGYxMzZhZTllMzRkZTdjMWIuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.dZiOjWHguo9f42XCMooMcv4EmYFzifl_-LEvPZHCtks` var adminUmputunToken = `eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTk1NDU5Nzk4MCwianRpIjoiOTdhMmUwYWM0ZGM3ZDVmNjkyNmQ1ZTg2MjBhY2VmOWE0MGMwIiwiaWF0IjoxNDU0NTk3NjgwLCJpc3MiOiJyZW1hcms0MiIsInVzZXIiOnsibmFtZSI6IlVtcHV0dW4iLCJpZCI6ImdpdGh1Yl9lZjBmNzA2YTciLCJwaWN0dXJlIjoiaHR0cHM6Ly9yZW1hcms0Mi5yYWRpby10LmNvbS9hcGkvdjEvYXZhdGFyL2NiNDJmZjQ5M2FkZTY5NmQ4OGEzYTU5MGYxMzZhZTllMzRkZTdjMWIuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.dZiOjWHguo9f42XCMooMcv4EmYFzifl_-LEvPZHCtks`
+2
View File
@@ -118,6 +118,8 @@ func (c *Comment) SetDeleted(mode DeleteMode) {
func (c *Comment) Sanitize() { func (c *Comment) Sanitize() {
p := bluemonday.UGCPolicy() p := bluemonday.UGCPolicy()
p.AllowAttrs("class").Matching(regexp.MustCompile("^chroma$")).OnElements("pre") p.AllowAttrs("class").Matching(regexp.MustCompile("^chroma$")).OnElements("pre")
// special case for embedding the quotes from Twitter
p.AllowAttrs("class").Matching(regexp.MustCompile("^twitter-tweet$")).OnElements("blockquote")
// this is list of <span> tag classes which could be produced by chroma code renderer // this is list of <span> tag classes which could be produced by chroma code renderer
// source: https://github.com/alecthomas/chroma/blob/cc2dd5b/types.go#L211-L307 // source: https://github.com/alecthomas/chroma/blob/cc2dd5b/types.go#L211-L307
const codeSpanClassRegex = "^(bg|chroma|line|ln|lnt|hl|lntable|lntd|cl|w|err|x|k|kc" + const codeSpanClassRegex = "^(bg|chroma|line|ln|lnt|hl|lntable|lntd|cl|w|err|x|k|kc" +
+4
View File
@@ -88,6 +88,10 @@ func TestComment_Sanitize(t *testing.T) {
inp: Comment{Text: "blah blah", PostTitle: "<script>alert()</script>something"}, inp: Comment{Text: "blah blah", PostTitle: "<script>alert()</script>something"},
out: Comment{Text: "blah blah", PostTitle: "something"}, out: Comment{Text: "blah blah", PostTitle: "something"},
}, },
{
inp: Comment{Text: `<blockquote class="twitter-tweet"><p lang="es" dir="ltr">Silicon iMac Concept<a href="https://t.co/7ga95QxVXn">https://t.co/7ga95QxVXn</a> by <a href="https://twitter.com/marcsheep?ref_src=twsrc%5Etfw">@marcsheep</a> <a href="https://t.co/ULnVpG8w55">pic.twitter.com/ULnVpG8w55</a></p>&mdash; Andreas Storm (@avstorm) <a href="https://twitter.com/avstorm/status/1325693387798933504?ref_src=twsrc%5Etfw">November 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`, PostTitle: "Twitter quote"},
out: Comment{Text: `<blockquote class="twitter-tweet"><p lang="es" dir="ltr">Silicon iMac Concept<a href="https://t.co/7ga95QxVXn" rel="nofollow">https://t.co/7ga95QxVXn</a> by <a href="https://twitter.com/marcsheep?ref_src=twsrc%5Etfw" rel="nofollow">@marcsheep</a> <a href="https://t.co/ULnVpG8w55" rel="nofollow">pic.twitter.com/ULnVpG8w55</a></p>— Andreas Storm (@avstorm) <a href="https://twitter.com/avstorm/status/1325693387798933504?ref_src=twsrc%5Etfw" rel="nofollow">November 9, 2020</a></blockquote> `, PostTitle: "Twitter quote"},
},
} }
for n, tt := range tbl { for n, tt := range tbl {
+7
View File
@@ -911,6 +911,13 @@ func TestBoltDB_NewFailed(t *testing.T) {
assert.EqualError(t, err, "failed to make boltdb for /tmp/no-such-place/tmp.db: open /tmp/no-such-place/tmp.db: no such file or directory") assert.EqualError(t, err, "failed to make boltdb for /tmp/no-such-place/tmp.db: open /tmp/no-such-place/tmp.db: no such file or directory")
} }
func TestBoltDB_DoubleClose(t *testing.T) {
var b, teardown = prep(t)
defer teardown()
assert.NoError(t, b.Close())
assert.NoError(t, b.Close(), "second call should not result in panic or errors")
}
// makes new boltdb, put two records // makes new boltdb, put two records
func prep(t *testing.T) (b *BoltDB, teardown func()) { func prep(t *testing.T) (b *BoltDB, teardown func()) {
_ = os.Remove(testDB) _ = os.Remove(testDB)
+9 -3
View File
@@ -186,11 +186,17 @@ func TestRemote_Delete(t *testing.T) {
} }
func TestRemote_Close(t *testing.T) { func TestRemote_Close(t *testing.T) {
ts := testServer(t, `{"method":"store.close","id":1}`, `{}`) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.Contains(t, string(body), "{\"method\":\"store.close\",\"id\":")
t.Logf("req: %s", string(body))
_, _ = fmt.Fprint(w, "{}")
}))
defer ts.Close() defer ts.Close()
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}} c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
err := c.Close() assert.NoError(t, c.Close())
assert.NoError(t, err) assert.NoError(t, c.Close(), "second call should not result in panic or errors")
} }
func testServer(t *testing.T, req, resp string) *httptest.Server { func testServer(t *testing.T, req, resp string) *httptest.Server {
+8
View File
@@ -286,3 +286,11 @@ func TestCachedImgID(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "cached_images/"+Sha1Str("example.org")+"-"+Sha1Str(imgURL), img) assert.Equal(t, "cached_images/"+Sha1Str("example.org")+"-"+Sha1Str(imgURL), img)
} }
func TestService_DoubleClose(t *testing.T) {
store := StoreMock{}
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
svc.Close(context.TODO())
// second call should not result in panic
svc.Close(context.TODO())
}
+24
View File
@@ -1638,6 +1638,30 @@ func Benchmark_ServiceCreate(b *testing.B) {
} }
} }
func TestService_DoubleClose_Bolt(t *testing.T) {
dbFile := fmt.Sprintf("%s/test-remark42-%d.db", os.TempDir(), rand.Intn(9999999999))
defer func() { _ = os.Remove(dbFile) }()
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: dbFile, SiteID: "radio-t"})
svc := DataStore{Engine: boltStore, EditDuration: 50 * time.Millisecond, AdminStore: admin.NewStaticKeyStore("secret 123")}
require.NoError(t, err)
assert.NoError(t, boltStore.Close())
assert.NoError(t, boltStore.Close(), "second call should not result in panic or errors")
assert.NoError(t, svc.Close())
assert.NoError(t, svc.Close(), "second call should not result in panic or errors")
}
func TestService_DoubleClose_Static(t *testing.T) {
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks,
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second})}
b.Close()
// second call should not result in panic or errors
b.Close()
}
// makes new boltdb, put two records // makes new boltdb, put two records
func prepStoreEngine(t *testing.T) (e engine.Interface, teardown func()) { func prepStoreEngine(t *testing.T) (e engine.Interface, teardown func()) {
testDBLoc, err := os.MkdirTemp("", "test_image_r42") testDBLoc, err := os.MkdirTemp("", "test_image_r42")
+7
View File
@@ -123,3 +123,10 @@ func TestTitle_GetFailed(t *testing.T) {
} }
assert.Equal(t, int32(1), atomic.LoadInt32(&hits), "hit once, errors cached") assert.Equal(t, int32(1), atomic.LoadInt32(&hits), "hit once, errors cached")
} }
func TestTitle_DoubleClosed(t *testing.T) {
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second})
ex.Close()
// second call should not result in panic
ex.Close()
}
+5 -5
View File
@@ -11,9 +11,9 @@ require (
github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.2 github.com/go-chi/render v1.0.2
github.com/go-pkgz/auth v1.20.0 github.com/go-pkgz/auth v1.20.1-0.20230103203948-168bd5a101b7
github.com/go-pkgz/jrpc v0.3.0 github.com/go-pkgz/jrpc v0.3.0
github.com/go-pkgz/lcw v1.0.1 github.com/go-pkgz/lcw v1.0.3-0.20221226231215-a66ea7c4aff7
github.com/go-pkgz/lgr v0.10.4 github.com/go-pkgz/lgr v0.10.4
github.com/go-pkgz/notify v0.2.0 github.com/go-pkgz/notify v0.2.0
github.com/go-pkgz/repeater v1.1.3 github.com/go-pkgz/repeater v1.1.3
@@ -29,7 +29,7 @@ require (
github.com/rs/xid v1.4.0 github.com/rs/xid v1.4.0
github.com/russross/blackfriday/v2 v2.1.0 github.com/russross/blackfriday/v2 v2.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.1
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
go.uber.org/goleak v1.2.0 go.uber.org/goleak v1.2.0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
@@ -48,7 +48,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/go-oauth2/oauth2/v4 v4.5.1 // indirect github.com/go-oauth2/oauth2/v4 v4.5.1 // indirect
github.com/go-pkgz/email v0.4.0 // indirect github.com/go-pkgz/email v0.4.1 // indirect
github.com/go-pkgz/expirable-cache v0.1.0 // indirect github.com/go-pkgz/expirable-cache v0.1.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
@@ -56,7 +56,7 @@ require (
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/klauspost/compress v1.15.2 // indirect github.com/klauspost/compress v1.15.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 // indirect github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 // indirect
+17 -13
View File
@@ -69,8 +69,8 @@ github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.22.0 h1:lIHHiSkEyS1MkKHCHzN+0mWrA4YdbGdimE5iZ2sHSzo= github.com/alicebob/miniredis/v2 v2.23.1 h1:jR6wZggBxwWygeXcdNyguCOCIjPsZyNUNlAkTx2fu0U=
github.com/alicebob/miniredis/v2 v2.22.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= github.com/alicebob/miniredis/v2 v2.23.1/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
@@ -140,17 +140,19 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-oauth2/oauth2/v4 v4.5.1 h1:3vxp+cjLqDe1TbogbwtMyeHRHr1tD+ksrK7xNppYRDs= github.com/go-oauth2/oauth2/v4 v4.5.1 h1:3vxp+cjLqDe1TbogbwtMyeHRHr1tD+ksrK7xNppYRDs=
github.com/go-oauth2/oauth2/v4 v4.5.1/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ= github.com/go-oauth2/oauth2/v4 v4.5.1/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
github.com/go-pkgz/auth v1.20.0 h1:9SHkolgv9+zOI9U+8IFXra0pl/H90oE4+ciJWtXoDl4= github.com/go-pkgz/auth v1.20.1-0.20221226231300-65f433fba0f1 h1:MJA4rZAwjd+KpaR2PqrxeDPloNu9Wml1UVQjL2fOtVM=
github.com/go-pkgz/auth v1.20.0/go.mod h1:jz0djN+4XoCyieuOzc+rB9l0RdqeuiXoQfE02yonLAI= github.com/go-pkgz/auth v1.20.1-0.20221226231300-65f433fba0f1/go.mod h1:fG1CP4+LDPnebYeO1BAZg/euTQQ8cnGn+5ZrXvJfckA=
github.com/go-pkgz/auth v1.20.1-0.20230103203948-168bd5a101b7 h1:ktKI3Y3UytkBLL1cOEzJmAi3nNKeaRGOzDj51Kgqp6M=
github.com/go-pkgz/auth v1.20.1-0.20230103203948-168bd5a101b7/go.mod h1:fG1CP4+LDPnebYeO1BAZg/euTQQ8cnGn+5ZrXvJfckA=
github.com/go-pkgz/email v0.3.1-0.20221002173339-19d25a20d99c/go.mod h1:TpnmSLkQW3FyICit2hn7WIhCUDrhCX6btzz5wS3wHRI= github.com/go-pkgz/email v0.3.1-0.20221002173339-19d25a20d99c/go.mod h1:TpnmSLkQW3FyICit2hn7WIhCUDrhCX6btzz5wS3wHRI=
github.com/go-pkgz/email v0.4.0 h1:NiYCwKPR6sW8nJsOR3GyB9Yw1AQZdnY78Xnhd9SLHEs= github.com/go-pkgz/email v0.4.1 h1:2vtP2gibsSzqhz6eD5DklSp11m657XEVf17fuXaxMvk=
github.com/go-pkgz/email v0.4.0/go.mod h1:TpnmSLkQW3FyICit2hn7WIhCUDrhCX6btzz5wS3wHRI= github.com/go-pkgz/email v0.4.1/go.mod h1:BdxglsQnymzhfdbnncEE72a6DrucZHy6I+42LK2jLEc=
github.com/go-pkgz/expirable-cache v0.1.0 h1:3bw0m8vlTK8qlwz5KXuygNBTkiKRTPrAGXU0Ej2AC1g= github.com/go-pkgz/expirable-cache v0.1.0 h1:3bw0m8vlTK8qlwz5KXuygNBTkiKRTPrAGXU0Ej2AC1g=
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs= github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U= github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00= github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
github.com/go-pkgz/lcw v1.0.1 h1:svYC6LIyzRaHF3TwJ8GCS+2RkJreBfaFjoeS+UYwJBc= github.com/go-pkgz/lcw v1.0.3-0.20221226231215-a66ea7c4aff7 h1:PJ1JEt2G0Dn7OUkLEgbNIrABRcbkCPM8v58BIBOSkm8=
github.com/go-pkgz/lcw v1.0.1/go.mod h1:CPJJzunpmGToOtD0Ga82TV152eL69sYEIIPcy9fbxlU= github.com/go-pkgz/lcw v1.0.3-0.20221226231215-a66ea7c4aff7/go.mod h1:adhOCEhc8G6+Bd992MMp0h796aBxZ/b4feUGU5twkRU=
github.com/go-pkgz/lgr v0.10.4 h1:l7qyFjqEZgwRgaQQSEp6tve4A3OU80VrfzpvtEX8ngw= github.com/go-pkgz/lgr v0.10.4 h1:l7qyFjqEZgwRgaQQSEp6tve4A3OU80VrfzpvtEX8ngw=
github.com/go-pkgz/lgr v0.10.4/go.mod h1:CD0s1z6EFpIUplV067gitF77tn25JItzwHNKAPqeCF0= github.com/go-pkgz/lgr v0.10.4/go.mod h1:CD0s1z6EFpIUplV067gitF77tn25JItzwHNKAPqeCF0=
github.com/go-pkgz/notify v0.2.0 h1:mxHjcLc3goT+k1qnBPJ06PpNuVUDcu21Xy+6hEo4IaU= github.com/go-pkgz/notify v0.2.0 h1:mxHjcLc3goT+k1qnBPJ06PpNuVUDcu21Xy+6hEo4IaU=
@@ -270,8 +272,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -343,6 +345,7 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -350,8 +353,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
@@ -403,8 +407,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.mongodb.org/mongo-driver v1.10.2 h1:4Wk3cnqOrQCn0P92L3/mmurMxzdvWWs5J9jinAVKD+k= go.mongodb.org/mongo-driver v1.10.2 h1:4Wk3cnqOrQCn0P92L3/mmurMxzdvWWs5J9jinAVKD+k=
+17 -2
View File
@@ -119,12 +119,27 @@ GET {{host}}/api/v1/rss/reply?site={{site}}&user={{user}}
GET {{host}}/api/v1/avatar/blah GET {{host}}/api/v1/avatar/blah
### send confirmation token for current user to specified email. auth token for dev user for secret=12345. ### send confirmation token for current user to specified email. auth token for dev user for secret=12345.
POST {{host}}/api/v1/email/subscribe?site={{site}}&address={{email}} ### in case the user logged in with the same email, it will be confirmed right away with "updated" set to "true" in the response,
### and no email will be sent.
POST {{host}}/api/v1/email/subscribe
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4 X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
Content-Type: application/json
{
"site": "{{site}}",
"address": "{{email}}"
}
### add email for notifications for current user via token from email. auth token for dev user for secret=12345. ### add email for notifications for current user via token from email. auth token for dev user for secret=12345.
POST {{host}}/api/v1/email/confirm?site={{site}}&tkn={{token}} POST {{host}}/api/v1/email/confirm
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4 X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
Content-Type: application/json
{
"site": "{{site}}",
"token": "{{token}}"
}
### get current user email. auth token for dev user for secret=12345. ### get current user email. auth token for dev user for secret=12345.
GET {{host}}/api/v1/email?site={{site}} GET {{host}}/api/v1/email?site={{site}}
+5 -5
View File
@@ -29,17 +29,13 @@ linters:
- revive - revive
- govet - govet
- unconvert - unconvert
- megacheck - unused
- structcheck
- gas - gas
- gocyclo - gocyclo
- misspell - misspell
- unparam - unparam
- varcheck
- deadcode
- typecheck - typecheck
- ineffassign - ineffassign
- varcheck
- stylecheck - stylecheck
- gochecknoinits - gochecknoinits
- exportloopref - exportloopref
@@ -67,5 +63,9 @@ issues:
- text: "Use of weak cryptographic primitive" - text: "Use of weak cryptographic primitive"
linters: linters:
- gosec - gosec
- path: _test\.go
text: "Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server"
linters:
- gosec
exclude-use-default: false exclude-use-default: false
+4 -4
View File
@@ -153,10 +153,6 @@ Such provider acts like any other, i.e. will be registered as `/auth/local/login
The API for this provider supports both GET and POST requests: The API for this provider supports both GET and POST requests:
* GET request with user credentials provided as query params:
```
GET /auth/<name>/login?user=<user>&passwd=<password>&aud=<site_id>&session=[1|0]
```
* POST request could be encoded as application/x-www-form-urlencoded or application/json: * POST request could be encoded as application/x-www-form-urlencoded or application/json:
``` ```
POST /auth/<name>/login?session=[1|0] POST /auth/<name>/login?session=[1|0]
@@ -172,6 +168,10 @@ The API for this provider supports both GET and POST requests:
"aud": "bar", "aud": "bar",
} }
``` ```
* GET request with user credentials provided as query params, but be aware that [the https query string is not secure](https://stackoverflow.com/a/323286/633961):
```
GET /auth/<name>/login?user=<user>&passwd=<password>&aud=<site_id>&session=[1|0]
```
_note: password parameter doesn't have to be naked/real password and can be any kind of password hash prepared by caller._ _note: password parameter doesn't have to be naked/real password and can be any kind of password hash prepared by caller._
+1 -2
View File
@@ -59,7 +59,6 @@ func (gf *GridFS) Get(avatar string) (reader io.ReadCloser, size int, err error)
return io.NopCloser(buf), int(sz), nil return io.NopCloser(buf), int(sz), nil
} }
//
// ID returns a fingerprint of the avatar content. Uses MD5 because gridfs provides it directly // ID returns a fingerprint of the avatar content. Uses MD5 because gridfs provides it directly
func (gf *GridFS) ID(avatar string) (id string) { func (gf *GridFS) ID(avatar string) (id string) {
@@ -143,7 +142,7 @@ func (gf *GridFS) List() (ids []string, err error) {
return ids, nil return ids, nil
} }
// Close gridfs does nothing but satisfies interface // Close gridfs store
func (gf *GridFS) Close() error { func (gf *GridFS) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), gf.timeout) ctx, cancel := context.WithTimeout(context.Background(), gf.timeout)
defer cancel() defer cancel()
+1 -1
View File
@@ -104,7 +104,7 @@ func (fs *LocalFS) List() (ids []string, err error) {
return ids, nil return ids, nil
} }
// Close gridfs does nothing but satisfies interface // Close LocalFS does nothing but satisfies interface
func (fs *LocalFS) Close() error { func (fs *LocalFS) Close() error {
return nil return nil
} }
+5 -1
View File
@@ -208,7 +208,11 @@ func (ah *AppleHandler) initPrivateKey() error {
if err != nil { if err != nil {
return err return err
} }
ah.conf.publicKey = ah.conf.privateKey.(*ecdsa.PrivateKey).Public() publicKey, ok := ah.conf.privateKey.(*ecdsa.PrivateKey)
if !ok {
return fmt.Errorf("provided private key is not ECDSA")
}
ah.conf.publicKey = publicKey.Public()
ah.conf.clientSecret, err = ah.createClientSecret() ah.conf.clientSecret, err = ah.createClientSecret()
if err != nil { if err != nil {
return err return err
+2 -1
View File
@@ -88,7 +88,8 @@ func (c *CustomServer) Run(ctx context.Context) {
} }
c.httpServer = &http.Server{ c.httpServer = &http.Server{
Addr: fmt.Sprintf(":%s", port), Addr: fmt.Sprintf(":%s", port),
ReadHeaderTimeout: 5 * time.Second,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch { switch {
case strings.HasSuffix(r.URL.Path, "/authorize"): case strings.HasSuffix(r.URL.Path, "/authorize"):
+2 -1
View File
@@ -57,7 +57,8 @@ func (d *DevAuthServer) Run(ctx context.Context) { // nolint (gocyclo)
} }
d.httpServer = &http.Server{ d.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", d.Provider.Port), Addr: fmt.Sprintf(":%d", d.Provider.Port),
ReadHeaderTimeout: 5 * time.Second,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
d.Logf("[DEBUG] dev oauth request %s %s %+v", r.Method, r.URL, r.Header) d.Logf("[DEBUG] dev oauth request %s %s %+v", r.Method, r.URL, r.Header)
switch { switch {
+1 -1
View File
@@ -177,7 +177,7 @@ func (em *Sender) client() (c *smtp.Client, err error) {
} }
if em.tls { if em.tls {
conn, e := tls.Dial("tcp", srvAddress, tlsConf) conn, e := tls.DialWithDialer(&net.Dialer{Timeout: em.timeOut}, "tcp", srvAddress, tlsConf)
if e != nil { if e != nil {
return nil, fmt.Errorf("failed to dial smtp tls to %s: %w", srvAddress, e) return nil, fmt.Errorf("failed to dial smtp tls to %s: %w", srvAddress, e)
} }
+1 -5
View File
@@ -26,18 +26,14 @@ linters:
- revive - revive
- govet - govet
- unconvert - unconvert
- megacheck
- structcheck
- gas - gas
- gocyclo - gocyclo
- dupl - dupl
- misspell - misspell
- unparam - unparam
- varcheck - unused
- deadcode
- typecheck - typecheck
- ineffassign - ineffassign
- varcheck
- stylecheck - stylecheck
- gochecknoinits - gochecknoinits
- exportloopref - exportloopref
+23 -16
View File
@@ -30,22 +30,30 @@ Main features:
## Usage ## Usage
```go ```go
cache, err := lcw.NewLruCache(lcw.MaxKeys(500), lcw.MaxCacheSize(65536), lcw.MaxValSize(200), lcw.MaxKeySize(32)) package main
if err != nil {
panic("failed to create cache") import (
"github.com/go-pkgz/lcw"
)
func main() {
cache, err := lcw.NewLruCache(lcw.MaxKeys(500), lcw.MaxCacheSize(65536), lcw.MaxValSize(200), lcw.MaxKeySize(32))
if err != nil {
panic("failed to create cache")
}
defer cache.Close()
val, err := cache.Get("key123", func() (interface{}, error) {
res, err := getDataFromSomeSource(params) // returns string
return res, err
})
if err != nil {
panic("failed to get data")
}
s := val.(string) // cached value
} }
defer cache.Close()
val, err := cache.Get("key123", func() (lcw.Value, error) {
res, err := getDataFromSomeSource(params) // returns string
return res, err
})
if err != nil {
panic("failed to get data")
}
s := val.(string) // cached value
``` ```
### Cache with URI ### Cache with URI
@@ -73,7 +81,6 @@ Cache can be created with URIs:
that mutable values can be changed outside of cache. `ExampleLoadingCache_Mutability` illustrates that. that mutable values can be changed outside of cache. `ExampleLoadingCache_Mutability` illustrates that.
- All byte-size limits (MaxCacheSize and MaxValSize) only work for values implementing `lcw.Sizer` interface. - All byte-size limits (MaxCacheSize and MaxValSize) only work for values implementing `lcw.Sizer` interface.
- Negative limits (max options) rejected - Negative limits (max options) rejected
- `lgr.Value` wraps `interface{}` and should be converted back to the concrete type.
- The implementation started as a part of [remark42](https://github.com/umputun/remark) - The implementation started as a part of [remark42](https://github.com/umputun/remark)
and later on moved to [go-pkgz/rest](https://github.com/go-pkgz/rest/tree/master/cache) and later on moved to [go-pkgz/rest](https://github.com/go-pkgz/rest/tree/master/cache)
library and finally generalized to become `lcw`. library and finally generalized to become `lcw`.
-2
View File
@@ -6,8 +6,6 @@
// 3 flavors of cache provided - NoP (do-nothing cache), ExpirableCache (TTL based), and LruCache // 3 flavors of cache provided - NoP (do-nothing cache), ExpirableCache (TTL based), and LruCache
package lcw package lcw
//go:generate sh -c "mockery -inpkg -name LoadingCache -print > /tmp/cache-mock.tmp && mv /tmp/cache-mock.tmp cache_mock.go"
import ( import (
"fmt" "fmt"
) )
+6
View File
@@ -185,6 +185,12 @@ func (c *LoadingCache) ItemCount() int {
func (c *LoadingCache) Close() { func (c *LoadingCache) Close() {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
// don't panic in case service is already closed
select {
case <-c.done:
return
default:
}
close(c.done) close(c.done)
} }
+30
View File
@@ -0,0 +1,30 @@
linters:
enable:
- megacheck
- revive
- govet
- unconvert
- megacheck
- gas
- gocyclo
- dupl
- misspell
- unparam
- unused
- typecheck
- ineffassign
- stylecheck
- exportloopref
- gocritic
- nakedret
- gosimple
- prealloc
fast: false
disable-all: true
issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
exclude-use-default: false
+1 -2
View File
@@ -44,7 +44,7 @@ func New2Q(size int) (*TwoQueueCache, error) {
// New2QParams creates a new TwoQueueCache using the provided // New2QParams creates a new TwoQueueCache using the provided
// parameter values. // parameter values.
func New2QParams(size int, recentRatio float64, ghostRatio float64) (*TwoQueueCache, error) { func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, error) {
if size <= 0 { if size <= 0 {
return nil, fmt.Errorf("invalid size") return nil, fmt.Errorf("invalid size")
} }
@@ -138,7 +138,6 @@ func (c *TwoQueueCache) Add(key, value interface{}) {
// Add to the recently seen list // Add to the recently seen list
c.ensureSpace(false) c.ensureSpace(false)
c.recent.Add(key, value) c.recent.Add(key, value)
return
} }
// ensureSpace is used to ensure we have space in the cache // ensureSpace is used to ensure we have space in the cache
+2
View File
@@ -1,3 +1,5 @@
Copyright (c) 2014 HashiCorp, Inc.
Mozilla Public License, version 2.0 Mozilla Public License, version 2.0
1. Definitions 1. Definitions
+1 -1
View File
@@ -7,7 +7,7 @@ thread safe LRU cache. It is based on the cache in Groupcache.
Documentation Documentation
============= =============
Full docs are available on [Godoc](http://godoc.org/github.com/hashicorp/golang-lru) Full docs are available on [Godoc](https://pkg.go.dev/github.com/hashicorp/golang-lru)
Example Example
======= =======
-1
View File
@@ -173,7 +173,6 @@ func (c *ARCCache) Add(key, value interface{}) {
// Add to the recently seen list // Add to the recently seen list
c.t1.Add(key, value) c.t1.Add(key, value)
return
} }
// replace is used to adaptively evict from either T1 or T2 // replace is used to adaptively evict from either T1 or T2
+100 -19
View File
@@ -6,10 +6,17 @@ import (
"github.com/hashicorp/golang-lru/simplelru" "github.com/hashicorp/golang-lru/simplelru"
) )
const (
// DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
DefaultEvictedBufferSize = 16
)
// Cache is a thread-safe fixed size LRU cache. // Cache is a thread-safe fixed size LRU cache.
type Cache struct { type Cache struct {
lru simplelru.LRUCache lru *simplelru.LRU
lock sync.RWMutex evictedKeys, evictedVals []interface{}
onEvictedCB func(k, v interface{})
lock sync.RWMutex
} }
// New creates an LRU of the given size. // New creates an LRU of the given size.
@@ -19,30 +26,63 @@ func New(size int) (*Cache, error) {
// NewWithEvict constructs a fixed size cache with the given eviction // NewWithEvict constructs a fixed size cache with the given eviction
// callback. // callback.
func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { func NewWithEvict(size int, onEvicted func(key, value interface{})) (c *Cache, err error) {
lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted)) // create a cache with default settings
if err != nil { c = &Cache{
return nil, err onEvictedCB: onEvicted,
} }
c := &Cache{ if onEvicted != nil {
lru: lru, c.initEvictBuffers()
onEvicted = c.onEvicted
} }
return c, nil c.lru, err = simplelru.NewLRU(size, onEvicted)
return
}
func (c *Cache) initEvictBuffers() {
c.evictedKeys = make([]interface{}, 0, DefaultEvictedBufferSize)
c.evictedVals = make([]interface{}, 0, DefaultEvictedBufferSize)
}
// onEvicted save evicted key/val and sent in externally registered callback
// outside of critical section
func (c *Cache) onEvicted(k, v interface{}) {
c.evictedKeys = append(c.evictedKeys, k)
c.evictedVals = append(c.evictedVals, v)
} }
// Purge is used to completely clear the cache. // Purge is used to completely clear the cache.
func (c *Cache) Purge() { func (c *Cache) Purge() {
var ks, vs []interface{}
c.lock.Lock() c.lock.Lock()
c.lru.Purge() c.lru.Purge()
if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
ks, vs = c.evictedKeys, c.evictedVals
c.initEvictBuffers()
}
c.lock.Unlock() c.lock.Unlock()
// invoke callback outside of critical section
if c.onEvictedCB != nil {
for i := 0; i < len(ks); i++ {
c.onEvictedCB(ks[i], vs[i])
}
}
} }
// Add adds a value to the cache. Returns true if an eviction occurred. // Add adds a value to the cache. Returns true if an eviction occurred.
func (c *Cache) Add(key, value interface{}) (evicted bool) { func (c *Cache) Add(key, value interface{}) (evicted bool) {
var k, v interface{}
c.lock.Lock() c.lock.Lock()
evicted = c.lru.Add(key, value) evicted = c.lru.Add(key, value)
if c.onEvictedCB != nil && evicted {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock() c.lock.Unlock()
return evicted if c.onEvictedCB != nil && evicted {
c.onEvictedCB(k, v)
}
return
} }
// Get looks up a key's value from the cache. // Get looks up a key's value from the cache.
@@ -75,13 +115,21 @@ func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) {
// recent-ness or deleting it for being stale, and if not, adds the value. // recent-ness or deleting it for being stale, and if not, adds the value.
// Returns whether found and whether an eviction occurred. // Returns whether found and whether an eviction occurred.
func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) { func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) {
var k, v interface{}
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock()
if c.lru.Contains(key) { if c.lru.Contains(key) {
c.lock.Unlock()
return true, false return true, false
} }
evicted = c.lru.Add(key, value) evicted = c.lru.Add(key, value)
if c.onEvictedCB != nil && evicted {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock()
if c.onEvictedCB != nil && evicted {
c.onEvictedCB(k, v)
}
return false, evicted return false, evicted
} }
@@ -89,47 +137,80 @@ func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) {
// recent-ness or deleting it for being stale, and if not, adds the value. // recent-ness or deleting it for being stale, and if not, adds the value.
// Returns whether found and whether an eviction occurred. // Returns whether found and whether an eviction occurred.
func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evicted bool) { func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evicted bool) {
var k, v interface{}
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock()
previous, ok = c.lru.Peek(key) previous, ok = c.lru.Peek(key)
if ok { if ok {
c.lock.Unlock()
return previous, true, false return previous, true, false
} }
evicted = c.lru.Add(key, value) evicted = c.lru.Add(key, value)
if c.onEvictedCB != nil && evicted {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock()
if c.onEvictedCB != nil && evicted {
c.onEvictedCB(k, v)
}
return nil, false, evicted return nil, false, evicted
} }
// Remove removes the provided key from the cache. // Remove removes the provided key from the cache.
func (c *Cache) Remove(key interface{}) (present bool) { func (c *Cache) Remove(key interface{}) (present bool) {
var k, v interface{}
c.lock.Lock() c.lock.Lock()
present = c.lru.Remove(key) present = c.lru.Remove(key)
if c.onEvictedCB != nil && present {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock() c.lock.Unlock()
if c.onEvictedCB != nil && present {
c.onEvictedCB(k, v)
}
return return
} }
// Resize changes the cache size. // Resize changes the cache size.
func (c *Cache) Resize(size int) (evicted int) { func (c *Cache) Resize(size int) (evicted int) {
var ks, vs []interface{}
c.lock.Lock() c.lock.Lock()
evicted = c.lru.Resize(size) evicted = c.lru.Resize(size)
if c.onEvictedCB != nil && evicted > 0 {
ks, vs = c.evictedKeys, c.evictedVals
c.initEvictBuffers()
}
c.lock.Unlock() c.lock.Unlock()
if c.onEvictedCB != nil && evicted > 0 {
for i := 0; i < len(ks); i++ {
c.onEvictedCB(ks[i], vs[i])
}
}
return evicted return evicted
} }
// RemoveOldest removes the oldest item from the cache. // RemoveOldest removes the oldest item from the cache.
func (c *Cache) RemoveOldest() (key interface{}, value interface{}, ok bool) { func (c *Cache) RemoveOldest() (key, value interface{}, ok bool) {
var k, v interface{}
c.lock.Lock() c.lock.Lock()
key, value, ok = c.lru.RemoveOldest() key, value, ok = c.lru.RemoveOldest()
if c.onEvictedCB != nil && ok {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock() c.lock.Unlock()
if c.onEvictedCB != nil && ok {
c.onEvictedCB(k, v)
}
return return
} }
// GetOldest returns the oldest entry // GetOldest returns the oldest entry
func (c *Cache) GetOldest() (key interface{}, value interface{}, ok bool) { func (c *Cache) GetOldest() (key, value interface{}, ok bool) {
c.lock.Lock() c.lock.RLock()
key, value, ok = c.lru.GetOldest() key, value, ok = c.lru.GetOldest()
c.lock.Unlock() c.lock.RUnlock()
return return
} }
+3 -3
View File
@@ -25,7 +25,7 @@ type entry struct {
// NewLRU constructs an LRU of the given size // NewLRU constructs an LRU of the given size
func NewLRU(size int, onEvict EvictCallback) (*LRU, error) { func NewLRU(size int, onEvict EvictCallback) (*LRU, error) {
if size <= 0 { if size <= 0 {
return nil, errors.New("Must provide a positive size") return nil, errors.New("must provide a positive size")
} }
c := &LRU{ c := &LRU{
size: size, size: size,
@@ -109,7 +109,7 @@ func (c *LRU) Remove(key interface{}) (present bool) {
} }
// RemoveOldest removes the oldest item from the cache. // RemoveOldest removes the oldest item from the cache.
func (c *LRU) RemoveOldest() (key interface{}, value interface{}, ok bool) { func (c *LRU) RemoveOldest() (key, value interface{}, ok bool) {
ent := c.evictList.Back() ent := c.evictList.Back()
if ent != nil { if ent != nil {
c.removeElement(ent) c.removeElement(ent)
@@ -120,7 +120,7 @@ func (c *LRU) RemoveOldest() (key interface{}, value interface{}, ok bool) {
} }
// GetOldest returns the oldest entry // GetOldest returns the oldest entry
func (c *LRU) GetOldest() (key interface{}, value interface{}, ok bool) { func (c *LRU) GetOldest() (key, value interface{}, ok bool) {
ent := c.evictList.Back() ent := c.evictList.Back()
if ent != nil { if ent != nil {
kv := ent.Value.(*entry) kv := ent.Value.(*entry)
+3 -2
View File
@@ -1,3 +1,4 @@
// Package simplelru provides simple LRU implementation based on build-in container/list.
package simplelru package simplelru
// LRUCache is the interface for simple LRU cache. // LRUCache is the interface for simple LRU cache.
@@ -34,6 +35,6 @@ type LRUCache interface {
// Clears all cache entries. // Clears all cache entries.
Purge() Purge()
// Resizes cache, returning number evicted // Resizes cache, returning number evicted
Resize(int) int Resize(int) int
} }
+16
View File
@@ -0,0 +1,16 @@
package lru
import (
"crypto/rand"
"math"
"math/big"
"testing"
)
func getRand(tb testing.TB) int64 {
out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
tb.Fatal(err)
}
return out.Int64()
}
+6 -6
View File
@@ -65,7 +65,7 @@ github.com/go-chi/render
github.com/go-oauth2/oauth2/v4 github.com/go-oauth2/oauth2/v4
github.com/go-oauth2/oauth2/v4/errors github.com/go-oauth2/oauth2/v4/errors
github.com/go-oauth2/oauth2/v4/server github.com/go-oauth2/oauth2/v4/server
# github.com/go-pkgz/auth v1.20.0 # github.com/go-pkgz/auth v1.20.1-0.20230103203948-168bd5a101b7
## explicit; go 1.17 ## explicit; go 1.17
github.com/go-pkgz/auth github.com/go-pkgz/auth
github.com/go-pkgz/auth/avatar github.com/go-pkgz/auth/avatar
@@ -74,8 +74,8 @@ github.com/go-pkgz/auth/middleware
github.com/go-pkgz/auth/provider github.com/go-pkgz/auth/provider
github.com/go-pkgz/auth/provider/sender github.com/go-pkgz/auth/provider/sender
github.com/go-pkgz/auth/token github.com/go-pkgz/auth/token
# github.com/go-pkgz/email v0.4.0 # github.com/go-pkgz/email v0.4.1
## explicit; go 1.17 ## explicit; go 1.19
github.com/go-pkgz/email github.com/go-pkgz/email
# github.com/go-pkgz/expirable-cache v0.1.0 # github.com/go-pkgz/expirable-cache v0.1.0
## explicit; go 1.14 ## explicit; go 1.14
@@ -83,7 +83,7 @@ github.com/go-pkgz/expirable-cache
# github.com/go-pkgz/jrpc v0.3.0 # github.com/go-pkgz/jrpc v0.3.0
## explicit; go 1.16 ## explicit; go 1.16
github.com/go-pkgz/jrpc github.com/go-pkgz/jrpc
# github.com/go-pkgz/lcw v1.0.1 # github.com/go-pkgz/lcw v1.0.3-0.20221226231215-a66ea7c4aff7
## explicit; go 1.15 ## explicit; go 1.15
github.com/go-pkgz/lcw github.com/go-pkgz/lcw
github.com/go-pkgz/lcw/eventbus github.com/go-pkgz/lcw/eventbus
@@ -143,7 +143,7 @@ github.com/hashicorp/errwrap
# github.com/hashicorp/go-multierror v1.1.1 # github.com/hashicorp/go-multierror v1.1.1
## explicit; go 1.13 ## explicit; go 1.13
github.com/hashicorp/go-multierror github.com/hashicorp/go-multierror
# github.com/hashicorp/golang-lru v0.5.4 # github.com/hashicorp/golang-lru v0.6.0
## explicit; go 1.12 ## explicit; go 1.12
github.com/hashicorp/golang-lru github.com/hashicorp/golang-lru
github.com/hashicorp/golang-lru/simplelru github.com/hashicorp/golang-lru/simplelru
@@ -196,7 +196,7 @@ github.com/slack-go/slack/internal/backoff
github.com/slack-go/slack/internal/errorsx github.com/slack-go/slack/internal/errorsx
github.com/slack-go/slack/internal/timex github.com/slack-go/slack/internal/timex
github.com/slack-go/slack/slackutilsx github.com/slack-go/slack/slackutilsx
# github.com/stretchr/testify v1.8.0 # github.com/stretchr/testify v1.8.1
## explicit; go 1.13 ## explicit; go 1.13
github.com/stretchr/testify/assert github.com/stretchr/testify/assert
github.com/stretchr/testify/require github.com/stretchr/testify/require
+1
View File
@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.2 4.15c.78 0 1.76-.55 2.34-1.28.52-.66.9-1.59.9-2.52 0-.12 0-.25-.03-.35-.86.03-1.9.6-2.52 1.37-.5.59-.94 1.5-.94 2.44 0 .14.02.28.03.32l.23.02ZM7.48 18c1.06 0 1.53-.74 2.86-.74 1.34 0 1.64.72 2.82.72 1.16 0 1.94-1.12 2.67-2.22.82-1.26 1.16-2.5 1.18-2.55a3.93 3.93 0 0 1-2.3-3.64c0-2.32 1.75-3.36 1.85-3.44a3.98 3.98 0 0 0-3.4-1.78c-1.3 0-2.36.82-3.03.82-.72 0-1.67-.78-2.8-.78C5.18 4.4 3 6.25 3 9.75c0 2.17.8 4.47 1.8 5.96C5.66 16.97 6.4 18 7.47 18Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 559 B

@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.2 4.15c.78 0 1.76-.55 2.34-1.28.52-.66.9-1.59.9-2.52 0-.12 0-.25-.03-.35-.86.03-1.9.6-2.52 1.37-.5.59-.94 1.5-.94 2.44 0 .14.02.28.03.32l.23.02ZM7.48 18c1.06 0 1.53-.74 2.86-.74 1.34 0 1.64.72 2.82.72 1.16 0 1.94-1.12 2.67-2.22.82-1.26 1.16-2.5 1.18-2.55a3.93 3.93 0 0 1-2.3-3.64c0-2.32 1.75-3.36 1.85-3.44a3.98 3.98 0 0 0-3.4-1.78c-1.3 0-2.36.82-3.03.82-.72 0-1.67-.78-2.8-.78C5.18 4.4 3 6.25 3 9.75c0 2.17.8 4.47 1.8 5.96C5.66 16.97 6.4 18 7.47 18Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 559 B

-2
View File
@@ -9,8 +9,6 @@ export const getConfig = (): Promise<Config> => apiFetcher.get('/config');
export const getPostComments = (sort: Sorting) => apiFetcher.get<Tree>('/find', { url, sort, format: 'tree' }); export const getPostComments = (sort: Sorting) => apiFetcher.get<Tree>('/find', { url, sort, format: 'tree' });
export const getComment = (id: Comment['id']): Promise<Comment> => apiFetcher.get(`/id/${id}`, { url });
export const getUserComments = ( export const getUserComments = (
userId: User['id'], userId: User['id'],
config: { limit: number; skip?: number } = { limit: 10, skip: 0 } config: { limit: number; skip?: number } = { limit: 10, skip: 0 }
@@ -38,7 +38,7 @@ export const IS_STORAGE_AVAILABLE: boolean = (() => {
})(); })();
/** /**
* Defines whether iframe loaded in cross origin environment * Defines whether iframe loaded in cross-origin environment
* Useful for checking if some privacy restriction may be applied * Useful for checking if some privacy restriction may be applied
*/ */
export const IS_THIRD_PARTY: boolean = (() => { export const IS_THIRD_PARTY: boolean = (() => {
@@ -43,7 +43,3 @@ export function getCookie(name: string) {
return matches ? decodeURIComponent(matches[1]) : undefined; return matches ? decodeURIComponent(matches[1]) : undefined;
} }
export function deleteCookie(name: string) {
setCookie(name, '', { expires: -1 });
}
+2 -4
View File
@@ -23,7 +23,7 @@ type Methods = {
delete: BodylessMethod; delete: BodylessMethod;
}; };
/** JWT token received from server and will be send by each request, if it present */ /** JWT token received from server and will be sent by each request, if it is present */
let activeJwtToken: string | undefined; let activeJwtToken: string | undefined;
const createFetcher = (baseUrl: string = ''): Methods => { const createFetcher = (baseUrl: string = ''): Methods => {
@@ -69,9 +69,7 @@ const createFetcher = (baseUrl: string = ''): Methods => {
// TODO: it should be clarified when frontend gets this header and what could be in it to simplify this logic and cover by tests // TODO: it should be clarified when frontend gets this header and what could be in it to simplify this logic and cover by tests
const date = (res.headers.has('date') && res.headers.get('date')) || ''; const date = (res.headers.has('date') && res.headers.get('date')) || '';
const timestamp = isNaN(Date.parse(date)) ? 0 : Date.parse(date); const timestamp = isNaN(Date.parse(date)) ? 0 : Date.parse(date);
const timeDiff = (new Date().getTime() - timestamp) / 1000; StaticStore.serverClientTimeDiff = (new Date().getTime() - timestamp) / 1000;
StaticStore.serverClientTimeDiff = timeDiff;
// backend could update jwt in any time. so, we should handle it // backend could update jwt in any time. so, we should handle it
if (res.headers.has(JWT_HEADER)) { if (res.headers.has(JWT_HEADER)) {
@@ -26,3 +26,4 @@ export const pageTitle = rawParams.page_title;
export const url = rawParams.url; export const url = rawParams.url;
export const token = rawParams.token; export const token = rawParams.token;
export const locale = rawParams.locale || 'en'; export const locale = rawParams.locale || 'en';
export const noFooter = rawParams.no_footer === 'true';
+1 -6
View File
@@ -72,12 +72,6 @@ export interface Comment {
*/ */
hidden?: boolean; hidden?: boolean;
} }
export interface CommentsResponse {
comments: Comment[];
count: number;
}
export interface Node { export interface Node {
comment: Comment; comment: Comment;
replies?: Node[]; replies?: Node[];
@@ -97,6 +91,7 @@ export interface Tree {
} }
export type OAuthProvider = export type OAuthProvider =
| 'apple'
| 'facebook' | 'facebook'
| 'twitter' | 'twitter'
| 'google' | 'google'
@@ -3,16 +3,9 @@ import { useIntl } from 'react-intl';
import { errorMessages, RequestError } from 'utils/errorUtils'; import { errorMessages, RequestError } from 'utils/errorUtils';
import { isObject } from 'utils/is-object'; import { isObject } from 'utils/is-object';
import { parseMessage, postMessageToParent } from 'utils/post-message'; import { parseMessage, updateIframeHeight } from 'utils/post-message';
import { messages } from './auth.messsages'; import { messages } from './auth.messsages';
function handleChangeIframeSize(element: HTMLElement) {
const { top } = element.getBoundingClientRect();
const height = Math.max(window.scrollY + Math.abs(top) + element.scrollHeight + 20, document.body.offsetHeight);
postMessageToParent({ height });
}
export function useDropdown(disableClosing?: boolean) { export function useDropdown(disableClosing?: boolean) {
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const clickInsideRef = useRef<boolean>(false); const clickInsideRef = useRef<boolean>(false);
@@ -74,10 +67,10 @@ export function useDropdown(disableClosing?: boolean) {
return; return;
} }
handleChangeIframeSize(dropdownElement); updateIframeHeight(dropdownElement);
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
handleChangeIframeSize(dropdownElement); updateIframeHeight(dropdownElement);
}); });
observer.observe(dropdownElement); observer.observe(dropdownElement);
@@ -123,7 +116,7 @@ export function useErrorMessage(): [string | null, (e: unknown) => void] {
} }
const errorReason = const errorReason =
err instanceof RequestError || (isObject(err) && typeof (err as Record<string, string>).error === 'string') err instanceof RequestError || isObject(err)
? (err as Record<'error', string>).error ? (err as Record<'error', string>).error
: err instanceof Error : err instanceof Error
? err.message ? err.message
@@ -27,7 +27,7 @@ export const messages = defineMessages<string>({
}, },
token: { token: {
id: 'token', id: 'token',
defaultMessage: 'Token', defaultMessage: 'Copy and paste the token from the email',
}, },
expiredToken: { expiredToken: {
id: 'token.expired', id: 'token.expired',
@@ -74,6 +74,7 @@ describe('<Auth/>', () => {
[['facebook', 'google', 'microsoft']], [['facebook', 'google', 'microsoft']],
[['facebook', 'google', 'microsoft', 'yandex']], [['facebook', 'google', 'microsoft', 'yandex']],
[['facebook', 'google', 'microsoft', 'yandex', 'twitter']], [['facebook', 'google', 'microsoft', 'yandex', 'twitter']],
[['facebook', 'google', 'microsoft', 'yandex', 'twitter', 'apple']],
] as [OAuthProvider[]][])('should renders with %j providers', async (providers) => { ] as [OAuthProvider[]][])('should renders with %j providers', async (providers) => {
StaticStore.config.auth_providers = providers; StaticStore.config.auth_providers = providers;
@@ -158,9 +159,9 @@ describe('<Auth/>', () => {
expect(screen.getByText('Back')).toHaveClass('auth-back-button'); expect(screen.getByText('Back')).toHaveClass('auth-back-button');
expect(screen.getByTitle('Close sign-in dropdown')).toHaveClass('auth-close-button'); expect(screen.getByTitle('Close sign-in dropdown')).toHaveClass('auth-close-button');
expect(screen.getByPlaceholderText('Token')).toHaveClass('auth-token-textarea'); expect(screen.getByPlaceholderText('Copy and paste the token from the email')).toHaveClass('auth-token-textarea');
fireEvent.change(screen.getByPlaceholderText('Token'), { fireEvent.change(screen.getByPlaceholderText('Copy and paste the token from the email'), {
target: { value: 'token' }, target: { value: 'token' },
}); });
@@ -187,9 +188,9 @@ describe('<Auth/>', () => {
expect(getByText('Back')).toHaveClass('auth-back-button'); expect(getByText('Back')).toHaveClass('auth-back-button');
expect(getByTitle('Close sign-in dropdown')).toHaveClass('auth-close-button'); expect(getByTitle('Close sign-in dropdown')).toHaveClass('auth-close-button');
expect(getByPlaceholderText('Token')).toHaveClass('auth-token-textarea'); expect(getByPlaceholderText('Copy and paste the token from the email')).toHaveClass('auth-token-textarea');
fireEvent.change(getByPlaceholderText('Token'), { target: { value: 'token' } }); fireEvent.change(getByPlaceholderText('Copy and paste the token from the email'), { target: { value: 'token' } });
fireEvent.click(getByText('Submit')); fireEvent.click(getByText('Submit'));
await waitFor(() => expect(utils.getTokenInvalidReason).toBeCalled()); await waitFor(() => expect(utils.getTokenInvalidReason).toBeCalled());
@@ -1,4 +1,11 @@
export const OAUTH_DATA = { export const OAUTH_DATA = {
apple: {
name: 'Apple',
icons: {
light: require('assets/social/apple-light.svg').default as string,
dark: require('assets/social/apple-dark.svg').default as string,
},
},
facebook: require('assets/social/facebook.svg').default as string, facebook: require('assets/social/facebook.svg').default as string,
twitter: require('assets/social/twitter.svg').default as string, twitter: require('assets/social/twitter.svg').default as string,
patreon: require('assets/social/patreon.svg').default as string, patreon: require('assets/social/patreon.svg').default as string,
@@ -26,7 +26,6 @@ const emailRegexp = /[^@]+@[^.]+\..+/;
enum Step { enum Step {
Email, Email,
Token, Token,
Final,
Close, Close,
Subscribed, Subscribed,
Unsubscribed, Unsubscribed,
@@ -35,7 +34,7 @@ enum Step {
const messages = defineMessages({ const messages = defineMessages({
token: { token: {
id: 'token', id: 'token',
defaultMessage: 'Token', defaultMessage: 'Copy and paste the token from the email',
}, },
expiredToken: { expiredToken: {
id: 'token.expired', id: 'token.expired',
@@ -112,7 +112,7 @@ export class CommentForm extends Component<Props, State> {
onInput = (e: Event) => { onInput = (e: Event) => {
const { value } = e.target as HTMLInputElement; const { value } = e.target as HTMLInputElement;
const text = value.substr(0, StaticStore.config.max_comment_size); const text = value.substring(0, StaticStore.config.max_comment_size);
updatePersistedComments(this.props.id, value); updatePersistedComments(this.props.id, value);
@@ -56,7 +56,7 @@ describe('<CommentVote />', () => {
['downvote', -1, 'Vote down', 'Vote up', 'downVoteButtonActive'], ['downvote', -1, 'Vote down', 'Vote up', 'downVoteButtonActive'],
])( ])(
'should go throught voting process and communicate with store when %s button is clicked', 'should go throught voting process and communicate with store when %s button is clicked',
async (_, increment, activeButtonText, secondButtonText, activeButtonClass) => { async (_, increment, activeButtonText, secondButtonText) => {
const putCommentVoteSpy = jest const putCommentVoteSpy = jest
.spyOn(api, 'putCommentVote') .spyOn(api, 'putCommentVote')
.mockImplementationOnce(({ vote }) => Promise.resolve({ id: '1', score: 10 + vote })); .mockImplementationOnce(({ vote }) => Promise.resolve({ id: '1', score: 10 + vote }));
@@ -21,7 +21,7 @@ type Props = {
disabled?: boolean; disabled?: boolean;
}; };
export function CommentVotes({ id, votes, vote, disabled, controversy = 0 }: Props) { export function CommentVotes({ id, votes, vote, disabled }: Props) {
const intl = useIntl(); const intl = useIntl();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [loadingState, setLoadingState] = useState<{ vote: number; votes: number } | null>(null); const [loadingState, setLoadingState] = useState<{ vote: number; votes: number } | null>(null);
@@ -62,7 +62,6 @@ export interface State {
} }
export class Comment extends Component<CommentProps, State> { export class Comment extends Component<CommentProps, State> {
votingPromise: Promise<unknown> = Promise.resolve();
/** comment text node. Used in comment text copying */ /** comment text node. Used in comment text copying */
textNode = createRef<HTMLDivElement>(); textNode = createRef<HTMLDivElement>();
@@ -88,7 +87,7 @@ export class Comment extends Component<CommentProps, State> {
}; };
/** /**
* Defines whether comment made by logged in user * Defines whether comment made by logged-in user
*/ */
isCurrentUser = (): boolean => { isCurrentUser = (): boolean => {
return !this.isGuest() && this.props.data.user.id === this.props.user?.id; return !this.isGuest() && this.props.data.user.id === this.props.user?.id;
@@ -175,16 +174,6 @@ export class Comment extends Component<CommentProps, State> {
} }
}; };
onBlockUserClick = (evt: Event) => {
const target = evt.currentTarget;
if (target instanceof HTMLOptionElement) {
// we have to debounce the blockUser function calls otherwise it will be
// called 2 times (by change event and by blur event)
this.blockUser(target.value as BlockTTL);
}
};
blockUser = debounce((ttl: BlockTTL): void => { blockUser = debounce((ttl: BlockTTL): void => {
const { user } = this.props.data; const { user } = this.props.data;
const blockingDurations = getBlockingDurations(this.props.intl); const blockingDurations = getBlockingDurations(this.props.intl);
@@ -427,7 +416,7 @@ export class Comment extends Component<CommentProps, State> {
/> />
</button> </button>
)} )}
{!isAdmin && !!o.user.verified && props.view !== 'user' && ( {!isAdmin && o.user.verified && props.view !== 'user' && (
<VerificationIcon className={styles.verificationIcon} title={intl.formatMessage(messages.verifiedUser)} /> <VerificationIcon className={styles.verificationIcon} title={intl.formatMessage(messages.verifiedUser)} />
)} )}
{o.user.paid_sub && ( {o.user.paid_sub && (
@@ -560,7 +549,7 @@ function getTextSnippet(html: string) {
tmp.innerHTML = html.replace('</p><p>', ' '); tmp.innerHTML = html.replace('</p><p>', ' ');
const result = tmp.innerText || ''; const result = tmp.innerText || '';
const snippet = result.substr(0, LENGTH); const snippet = result.substring(0, LENGTH);
return snippet.length === LENGTH && result.length !== LENGTH ? `${snippet}...` : snippet; return snippet.length === LENGTH && result.length !== LENGTH ? `${snippet}...` : snippet;
} }
@@ -40,7 +40,7 @@ export function Profile() {
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [comments, setComments] = useState<CommentType[] | null>(null); const [comments, setComments] = useState<CommentType[] | null>(null);
const [commentsAmount, setCommentsAmount] = useState(0); const [commentsAmount, setCommentsAmount] = useState(0);
// store skip count in ref because it don't affect the view // store skip count in ref because it doesn't affect the view
const commentsSkipCountsRef = useRef(0); const commentsSkipCountsRef = useRef(0);
const [isSigningOut, setSigningOut] = useState(false); const [isSigningOut, setSigningOut] = useState(false);
@@ -179,7 +179,7 @@ export function Profile() {
return ( return (
<div className={clsx('profile', styles.root)} ref={rootRef}> <div className={clsx('profile', styles.root)} ref={rootRef}>
{/* disable jsx-a11y/no-static-element-interactions and jsx-a11y/click-events-have-key-events */} {/* disable jsx-a11y/no-static-element-interactions and jsx-a11y/click-events-have-key-events */}
{/* that's fine because inside of the element we have button that will throw all events and provide all of the interactions */} {/* that's fine because inside the element we have button that will throw all events and provide all the interactions */}
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
<div className={clsx('profile-close-button-wrapper', styles.closeButtonWrapper)} onClick={handleClickClose}> <div className={clsx('profile-close-button-wrapper', styles.closeButtonWrapper)} onClick={handleClickClose}>
<IconButton title={intl.formatMessage({ id: 'profile.close', defaultMessage: 'Close profile' })}> <IconButton title={intl.formatMessage({ id: 'profile.close', defaultMessage: 'Close profile' })}>
@@ -8,10 +8,9 @@ import clsx from 'clsx';
import 'styles/global.css'; import 'styles/global.css';
import type { StoreState } from 'store'; import type { StoreState } from 'store';
import { COMMENT_NODE_CLASSNAME_PREFIX, MAX_SHOWN_ROOT_COMMENTS, THEMES, IS_MOBILE } from 'common/constants'; import { COMMENT_NODE_CLASSNAME_PREFIX, MAX_SHOWN_ROOT_COMMENTS, THEMES, IS_MOBILE } from 'common/constants';
import { maxShownComments, url } from 'common/settings'; import { maxShownComments, noFooter, url } from 'common/settings';
import { import {
setUser,
fetchUser, fetchUser,
blockUser, blockUser,
unblockUser, unblockUser,
@@ -20,7 +19,7 @@ import {
unhideUser, unhideUser,
signout, signout,
} from 'store/user/actions'; } from 'store/user/actions';
import { fetchComments, addComment, updateComment, unsetCommentMode } from 'store/comments/actions'; import { fetchComments, addComment, updateComment } from 'store/comments/actions';
import { setCommentsReadOnlyState } from 'store/post-info/actions'; import { setCommentsReadOnlyState } from 'store/post-info/actions';
import { setTheme } from 'store/theme/actions'; import { setTheme } from 'store/theme/actions';
@@ -36,19 +35,12 @@ import { ConnectedComment as Comment } from 'components/comment/connected-commen
import { uploadImage, getPreview } from 'common/api'; import { uploadImage, getPreview } from 'common/api';
import { isUserAnonymous } from 'utils/isUserAnonymous'; import { isUserAnonymous } from 'utils/isUserAnonymous';
import { bindActions } from 'utils/actionBinder'; import { bindActions } from 'utils/actionBinder';
import { postMessageToParent, parseMessage } from 'utils/post-message'; import { postMessageToParent, parseMessage, updateIframeHeight } from 'utils/post-message';
import { useActions } from 'hooks/useAction'; import { useActions } from 'hooks/useAction';
import { setCollapse } from 'store/thread/actions'; import { setCollapse } from 'store/thread/actions';
import styles from './root.module.css'; import styles from './root.module.css';
/**
* Sends size of the iframe to parent window
*/
export function updateIframeHeight() {
postMessageToParent({ height: document.body.offsetHeight });
}
const mapStateToProps = (state: StoreState) => ({ const mapStateToProps = (state: StoreState) => ({
sort: state.comments.sort, sort: state.comments.sort,
isCommentsLoading: state.comments.isFetching, isCommentsLoading: state.comments.isFetching,
@@ -73,7 +65,6 @@ const mapStateToProps = (state: StoreState) => ({
const boundActions = bindActions({ const boundActions = bindActions({
fetchComments, fetchComments,
setUser,
fetchUser, fetchUser,
fetchBlockedUsers, fetchBlockedUsers,
setTheme, setTheme,
@@ -85,7 +76,6 @@ const boundActions = bindActions({
addComment, addComment,
updateComment, updateComment,
setCollapse, setCollapse,
unsetCommentMode,
signout, signout,
}); });
@@ -332,12 +322,6 @@ const CopyrightLink = (title: string) => (
</a> </a>
); );
const Copyright = () => (
<p className="root__copyright" role="contentinfo">
<FormattedMessage id="root.powered-by" defaultMessage="Powered by <a>Remark42</a>" values={{ a: CopyrightLink }} />
</p>
);
/** Root component connected to redux */ /** Root component connected to redux */
export function ConnectedRoot() { export function ConnectedRoot() {
const intl = useIntl(); const intl = useIntl();
@@ -345,23 +329,25 @@ export function ConnectedRoot() {
const actions = useActions(boundActions); const actions = useActions(boundActions);
useEffect(() => { useEffect(() => {
const observer = new ResizeObserver(updateIframeHeight); const observer = new ResizeObserver(() => updateIframeHeight());
updateIframeHeight(); updateIframeHeight();
observer.observe(document.body); observer.observe(document.body);
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
if (!window.remark_config) {
throw new Error('Remark42: Config object is undefined');
}
const { no_footer } = window.remark_config;
return ( return (
<div className={clsx(b('root', {}, { theme: props.theme }), props.theme)}> <div className={clsx(b('root', {}, { theme: props.theme }), props.theme)}>
<Root {...props} {...actions} intl={intl} /> <Root {...props} {...actions} intl={intl} />
{!no_footer && <Copyright />} {!noFooter && (
<p className="root__copyright" role="contentinfo">
<FormattedMessage
id="root.powered-by"
defaultMessage="Powered by <a>Remark42</a>"
values={{ a: CopyrightLink }}
/>
</p>
)}
</div> </div>
); );
} }
@@ -91,8 +91,7 @@ class SettingsComponent extends Component<Props, State> {
}; };
__isUserHidden = (user: User): boolean => { __isUserHidden = (user: User): boolean => {
if (this.state.unhiddenUsers.indexOf(user.id) === -1) return true; return !this.state.unhiddenUsers.includes(user.id);
return false;
}; };
render({ user, theme }: Props, { blockedUsers, unblockedUsers, unhiddenUsers }: State) { render({ user, theme }: Props, { blockedUsers, unblockedUsers, unhiddenUsers }: State) {
@@ -2,7 +2,7 @@ import { h, JSX } from 'preact';
import { forwardRef } from 'preact/compat'; import { forwardRef } from 'preact/compat';
import { useEffect, useRef } from 'preact/hooks'; import { useEffect, useRef } from 'preact/hooks';
function autoResize(textarea: HTMLTextAreaElement, onResize?: () => void) { function autoResize(textarea: HTMLTextAreaElement) {
textarea.style.height = ''; textarea.style.height = '';
textarea.style.height = `${textarea.scrollHeight}px`; textarea.style.height = `${textarea.scrollHeight}px`;
} }
+1 -3
View File
@@ -4,7 +4,5 @@ import { StoreState } from 'store';
import { Theme } from 'common/types'; import { Theme } from 'common/types';
export function useTheme() { export function useTheme() {
const theme = useSelector<StoreState, Theme>(({ theme }) => theme); return useSelector<StoreState, Theme>(({ theme }) => theme);
return theme;
} }
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "الموقع", "subscribeByRSS.site": "الموقع",
"subscribeByRSS.thread": "سلسلة ردود", "subscribeByRSS.thread": "سلسلة ردود",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "رمز", "token": "انسخ والصق الرمز المميز من البريد الإلكتروني",
"token.expired": "الرمز انتهت صلاحيته", "token.expired": "الرمز انتهت صلاحيته",
"token.invalid": "الرمز غير صالح", "token.invalid": "الرمز غير صالح",
"toolbar.attach-image": "أرفق صورةً، أو اسحبها وأسقطها، أو الصقها من الحافظة", "toolbar.attach-image": "أرفق صورةً، أو اسحبها وأسقطها، أو الصقها من الحافظة",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Сайт", "subscribeByRSS.site": "Сайт",
"subscribeByRSS.thread": "Гутарка", "subscribeByRSS.thread": "Гутарка",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Токен", "token": "Скапіруйце і ўстаўце токен з электроннага ліста",
"token.expired": "Час дзеяння токена сышоў", "token.expired": "Час дзеяння токена сышоў",
"token.invalid": "Токен несапраўдны", "token.invalid": "Токен несапраўдны",
"toolbar.attach-image": "Прымацаваць выяву, перацягніце або ўстаўце выяву з буфера абмену", "toolbar.attach-image": "Прымацаваць выяву, перацягніце або ўстаўце выяву з буфера абмену",
+3 -3
View File
@@ -152,9 +152,9 @@
"subscribeByRSS.site": "Сайт", "subscribeByRSS.site": "Сайт",
"subscribeByRSS.thread": "Нишка", "subscribeByRSS.thread": "Нишка",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Жетон", "token": "Копирайте и поставете токена от имейла",
"token.expired": "Изтекъл жетон", "token.expired": "Изтекъл токен",
"token.invalid": "Token is invalid", "token.invalid": "Токенът е невалиден",
"toolbar.attach-image": "Добави картина, премести или копирай от клипборда", "toolbar.attach-image": "Добави картина, премести или копирай от клипборда",
"toolbar.bold": "Добави удебелен текст {shortcut}", "toolbar.bold": "Добави удебелен текст {shortcut}",
"toolbar.code": "Вмъкни код", "toolbar.code": "Вмъкни код",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Site", "subscribeByRSS.site": "Site",
"subscribeByRSS.thread": "Tópico", "subscribeByRSS.thread": "Tópico",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Token", "token": "Copiar e colar a ficha do e-mail",
"token.expired": "O token expirou", "token.expired": "O token expirou",
"token.invalid": "Token inválido", "token.invalid": "Token inválido",
"toolbar.attach-image": "Anexe a imagem, arraste e solte ou cole da área de transferência", "toolbar.attach-image": "Anexe a imagem, arraste e solte ou cole da área de transferência",
+174
View File
@@ -0,0 +1,174 @@
{
"auth.back": "Zpět",
"auth.email-address": "Emailová adresa",
"auth.loading": "Načítání...",
"auth.oauth-button": "Přihlásit se pomocí {provider}",
"auth.oauth-source": "Použít sociální sítě",
"auth.open-profile": "Otevřít můj profil",
"auth.or": "nebo",
"auth.signin": "Přihlásit se",
"auth.signout": "Odhlásit se",
"auth.submit": "Odesalt",
"auth.symbols-restriction": "Uživatelské jméno musí obsahovat pouze písmena, čísla, podtržítka nebo mezery",
"auth.telegram-check": "Zkontrolovat",
"auth.telegram-link": "pomocí odkazu",
"auth.telegram-message-1": "Otevřít Telegram",
"auth.telegram-message-2": "a zde klepněte na tlačítko “Start”",
"auth.telegram-message-3": "Poté klepněte na tlačítko “Zkontrolovat”",
"auth.telegram-optional-qr": "nebo naskenujte QR kód",
"auth.telegram-qr": "Telegram QR kód",
"auth.user-not-found": "Nebyl nalezen žádný uživatel",
"auth.username": "Uživatelské jméno",
"authPanel.disable-comments": "Zakázat komentáře",
"authPanel.disabled-cookies": "Zakázat blokování souborů cookie třetích stran pro přihlášení nebo otevření komentářů",
"authPanel.enable-comments": "Povolit komentáře",
"authPanel.enable-cookies": "Povolit soubory cookie pro přihlášení a komentáře",
"authPanel.hide-settings": "Skrýt nastavení",
"authPanel.new-page": "nová stránka",
"authPanel.read-only": "Pouze pro čtení",
"authPanel.show-settings": "Zobrazit nastavení",
"blockingDuration.day": "Na den",
"blockingDuration.month": "Na měsíc",
"blockingDuration.permanently": "Trvale",
"blockingDuration.week": "Na týden",
"comment.block": "Blokovat",
"comment.block-user": "Chcete zablokovat uživatele {userName} na {duration}?",
"comment.blocked-user": "Uživatel zablokován",
"comment.blocking-period": "Doba blokování",
"comment.cancel": "Zrušit",
"comment.copied": "Zkopírováno!",
"comment.copy": "Kopírovat",
"comment.delete": "Smazat",
"comment.delete-message": "Opravdu chccete smazat tento komentář?",
"comment.deleted-comment": "Komentář byl smazán",
"comment.deleted-user": "Odstraněný uživatel",
"comment.edit": "Upravit",
"comment.edit-countdown": "Editace bude zakázána",
"comment.expired-time": "Čas pro editaci komentáře vypršel",
"comment.go-to-parent": "Přejít na nadřazený komentář",
"comment.hide": "Skrýt",
"comment.hide-user-comment": "Opravdu chcete skrýt komentář uživatele {userName}?",
"comment.paid-patreon": "Placený odběratel na Patreonu",
"comment.pin": "Připnout",
"comment.pin-comment": "Opravdu chcete připnout tento komentář?",
"comment.reply": "Odpovědět",
"comment.time": "{day} v {time}",
"comment.toggle-verification": "Přepnout ověření",
"comment.unblock": "Odblokovat",
"comment.unblock-user": "Opravdu chcete odblokovat uživatele?",
"comment.unpin": "Odepnout",
"comment.unpin-comment": "Opravdu chete odepnout komentář?",
"comment.unverified-user": "Neověřený uživatel",
"comment.unverify-user": "Opravdu chcete odebrat ověření uživateli {userName}?",
"comment.verified-user": "Ověřený uživatel",
"comment.verify-user": "Opravdu chcete uživatele {userName} ověřit?",
"commentForm.anonymous-uploading-disabled": "Nahrávání obrázků je pro anonymní uživatele zakázáno. Abyste mohli přikládat obrázky, nepřihlašujte se jako anonymní uživatelé.",
"commentForm.exceeded-size": "{fileName} překračuje limit velikosti souborů (max. {maxImageSize})",
"commentForm.input-placeholder": "Zde napište Váš komentář",
"commentForm.new-comment": "Nový komentář",
"commentForm.notice-about-styling": "Pole podporuje <a>Markdown</a> syntax",
"commentForm.preview": "Náhled",
"commentForm.reply": "Odpovědět",
"commentForm.save": "Uložit",
"commentForm.send": "Odeslat",
"commentForm.subscribe-by": "Přihlásit se k odběru",
"commentForm.subscribe-or": "nebo",
"commentForm.unauthorized-uploading-disabled": "Nahrávání obrázků je pro neautorizované uživatele zakázáno. Před nahráváním byste se měli přihlásit.",
"commentForm.unexpected-error": "Něco se pokazilo. Zkuste to prosím později.",
"commentForm.upload-file-fail": "{fileName} se nepodařilo nahrát z důvodu \"{errorMessage}\"",
"commentForm.uploading": "Nahrávání...",
"commentForm.uploading-file": "nahrávání {fileName}...",
"commentsSort.best": "Nejlepší",
"commentsSort.least-controversial": "Nejméně kontroverzní",
"commentsSort.least-recently-updated": "Nejméně aktualizované",
"commentsSort.most-controversial": "Nejvíce kontroverzní",
"commentsSort.newest": "Nejnovější",
"commentsSort.oldest": "Nejstarší",
"commentsSort.recently-updated": "Nedávno aktualizované",
"commentsSort.worst": "Nejhorší",
"empty-state": "Zatím zde nejsou žádné komentáře.",
"errors.0": "Něco se pokazilo. Zkuste provést akci pozdějin",
"errors.1": "Komentář nelze nalézt. Obnovte prosím stránku a zkuste to znovu.",
"errors.10": "Vypršel čas pro editaci komentáře",
"errors.11": "Komentář již obsahuje odpověď, editace není možná.",
"errors.12": "Nelze hlasovat pro tento komentář. Zkuste to prosím později.",
"errors.13": "Nemůžete hlasovat pro svůj vlastní komentář",
"errors.14": "Pro tento komentář jste již hlasoval",
"errors.15": "Tento komentář již obsahuje příliš mnoho hlasů",
"errors.16": "U tohoto komentáře bylo dosaženo minimálního skóre.",
"errors.17": "Akce zamítnuta. Zkuste to prosím znovu o něco později.",
"errors.18": "Requested file cannot be found.",
"errors.19": "Požadovaný soubor nelze nalézt.",
"errors.2": "Nepodařilo se zrušit příchozí požadavek.",
"errors.20": "Odeslaný obrázek nebyl nalezen. Zkuste jej nahrát znovu.",
"errors.3": "K této operaci nemáte oprávnění.",
"errors.4": "Komentář obsahuje neplatná data",
"errors.5": "Komentář nenalezen. Obnovte stránku a zkuste to znovu",
"errors.6": "Stránku nelze nalézt. Obnovte prosím stránku a zkuste to znovu.",
"errors.7": "Uživatel byl zablokován..",
"errors.8": "Nelze přidávat komentáře, protože jsou pouze pro čtení",
"errors.9": "Editace komentáře se nezdařila. Zkuste to prosím později.",
"errors.failed-fetch": "Nepodařilo se načíst. Zkontrolujte prosím své internetové připojení nebo to zkuste později.",
"errors.forbidden": "Zakázáno.",
"errors.not-authorized": "Nejste přihlášen.",
"errors.to-many-request": "Dosáhli jste maximálního limitu požadavků.",
"errors.unexpected-error": "Něco se pokazilo.",
"profile.close": "Zavřít profil",
"profile.request-to-delete-data": "Žádost o odstranění mých údajů",
"retry": "Zkusit znovu",
"root.pinned-comments": "Připnuté komentáře",
"root.powered-by": "Běží na <a>Remark42</a>",
"root.show-more": "Zobrazit více",
"settings.block": "Blokovat",
"settings.block-time": "do {day}, {time}",
"settings.block-user": "Opravdu chcete zablokovat uživatele {userName}?",
"settings.blocked-users-header": "Blokovaní uživatelé:",
"settings.blocked-users-title": "Blokovaní uživatelé",
"settings.hidden-user-header": "Skrytí uživatelé:",
"settings.hidden-users-title": "Skrytí uživatelé",
"settings.hide": "skrýt",
"settings.no-blocked-users": "Nejsou zde žádní blokovaní uživatelé",
"settings.no-hidden-users": "Nejsou zde žádní skrytí uživatelé",
"settings.permanently": "trvale",
"settings.show": "zobrazit",
"settings.unblock": "odblokovat",
"settings.unblock-user": "Chcete odblokovat uživatele {userName}?",
"settings.unknown": "neznámý",
"sort-by": "Řadit podle",
"subscribeByEmail.back": "Zpět",
"subscribeByEmail.close": "Zavřít",
"subscribeByEmail.email": "Email",
"subscribeByEmail.have-been-subscribed": "Byli jste přihlášeni k odběru aktualizací e-mailem",
"subscribeByEmail.have-been-unsubscribed": "Byli jste odhlášeni z odběru aktualizací e-mailem",
"subscribeByEmail.only-registered-users": "Dostupné pouze pro registrované uživatele",
"subscribeByEmail.submit": "Odeslat",
"subscribeByEmail.subscribe": "Přihlásit se k odběru",
"subscribeByEmail.subscribe-by-email": "Přihlášení k odběru e-mailem",
"subscribeByEmail.subscribe-to-replies": "Přihlásit se k odběru odpovědí",
"subscribeByEmail.subscribed": "Jste přihlášeni k odběru aktualizací e-mailem",
"subscribeByEmail.unsubscribe": "Odhlásit se",
"subscribeByRSS.button-title": "Odebírat pomocí RSS",
"subscribeByRSS.replies": "Odpovědi",
"subscribeByRSS.site": "Web",
"subscribeByRSS.thread": "Vlákno",
"subscribeByRSS.title": "RSS",
"token": "Zkopírujte a vložte token z e-mailu",
"token.expired": "Token expiroval",
"token.invalid": "Token je neplatný",
"toolbar.attach-image": "Obrázek",
"toolbar.bold": "Tučný text {shortcut}",
"toolbar.code": "Kód",
"toolbar.header": "Nadpis",
"toolbar.italic": "Kurzíva {shortcut}",
"toolbar.link": "Odkaz {shortcut}",
"toolbar.ordered-list": "Číslovaný seznam",
"toolbar.quote": "Citace",
"toolbar.unordered-list": "Nečíslovaný seznam",
"user.comments": "Komentáře",
"user.load-more": "Načíst více",
"user.my-comments": "Moje komentáře",
"vote.controversy": "Kontroverznost: {value}",
"vote.downvote": "Udělit negativní hlas",
"vote.score": "Výsledek hlasování",
"vote.upvote": "Udělit pozitivní hlas"
}
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Site", "subscribeByRSS.site": "Site",
"subscribeByRSS.thread": "Thread", "subscribeByRSS.thread": "Thread",
"subscribeByRSS.title": "RSS-Feed", "subscribeByRSS.title": "RSS-Feed",
"token": "Token", "token": "Kopieren und Einfügen des Tokens aus der E-Mail",
"token.expired": "Token ist abgelaufen", "token.expired": "Token ist abgelaufen",
"token.invalid": "Token ist ungültig", "token.invalid": "Token ist ungültig",
"toolbar.attach-image": "Bild als Anhang, per Drag & Drop oder aus der Zwischenablage einfügen", "toolbar.attach-image": "Bild als Anhang, per Drag & Drop oder aus der Zwischenablage einfügen",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Site", "subscribeByRSS.site": "Site",
"subscribeByRSS.thread": "Thread", "subscribeByRSS.thread": "Thread",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Token", "token": "Copy and paste the token from the email",
"token.expired": "Token is expired", "token.expired": "Token is expired",
"token.invalid": "Token is invalid", "token.invalid": "Token is invalid",
"toolbar.attach-image": "Attach the image, drag & drop or paste from clipboard", "toolbar.attach-image": "Attach the image, drag & drop or paste from clipboard",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Sitio", "subscribeByRSS.site": "Sitio",
"subscribeByRSS.thread": "Hilo", "subscribeByRSS.thread": "Hilo",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Token", "token": "Kopeeri ja kleebi sümbol e-kirjast.",
"token.expired": "Token expirado", "token.expired": "Token expirado",
"token.invalid": "El token no es válido", "token.invalid": "El token no es válido",
"toolbar.attach-image": "Adjunta la imágen, arrastra y suelta, o pega desde el portapapeles", "toolbar.attach-image": "Adjunta la imágen, arrastra y suelta, o pega desde el portapapeles",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Sivusto", "subscribeByRSS.site": "Sivusto",
"subscribeByRSS.thread": "Teema", "subscribeByRSS.thread": "Teema",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Tunnus", "token": "Kopioi ja liitä tunniste sähköpostista",
"token.expired": "Tunnus on vanhentunut", "token.expired": "Tunnus on vanhentunut",
"token.invalid": "Token is invalid", "token.invalid": "Token is invalid",
"toolbar.attach-image": "Liitä kuva, vedä ja pudota tai liitä leikepöydältä", "toolbar.attach-image": "Liitä kuva, vedä ja pudota tai liitä leikepöydältä",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Site", "subscribeByRSS.site": "Site",
"subscribeByRSS.thread": "Fil d'actualités", "subscribeByRSS.thread": "Fil d'actualités",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Jeton", "token": "Copiez et collez le jeton de l'e-mail",
"token.expired": "Le jeton a expiré", "token.expired": "Le jeton a expiré",
"token.invalid": "Le jeton n'est pas valide", "token.invalid": "Le jeton n'est pas valide",
"toolbar.attach-image": "Joindre une image, la glisser-déposer ou la coller à partir du presse-papier", "toolbar.attach-image": "Joindre une image, la glisser-déposer ou la coller à partir du presse-papier",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Sito", "subscribeByRSS.site": "Sito",
"subscribeByRSS.thread": "Argomento", "subscribeByRSS.thread": "Argomento",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Token", "token": "Copiare e incollare il token dall'e-mail",
"token.expired": "Il token è scaduto", "token.expired": "Il token è scaduto",
"token.invalid": "Il token non è valido", "token.invalid": "Il token non è valido",
"toolbar.attach-image": "Allega immagini, trascina o incolla qui", "toolbar.attach-image": "Allega immagini, trascina o incolla qui",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "サイト", "subscribeByRSS.site": "サイト",
"subscribeByRSS.thread": "スレッド", "subscribeByRSS.thread": "スレッド",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "トークン", "token": "メールに記載されているトークンをコピー&ペーストしてください。",
"token.expired": "トークンの有効期限が切れています", "token.expired": "トークンの有効期限が切れています",
"token.invalid": "トークンが無効です", "token.invalid": "トークンが無効です",
"toolbar.attach-image": "画像を添付、ドラッグアンドドロップ、またはクリップボードから貼り付けてください", "toolbar.attach-image": "画像を添付、ドラッグアンドドロップ、またはクリップボードから貼り付けてください",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "사이트", "subscribeByRSS.site": "사이트",
"subscribeByRSS.thread": "스레드", "subscribeByRSS.thread": "스레드",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "토큰", "token": "이메일에서 토큰 복사 및 붙여넣기",
"token.expired": "토큰이 만료되었습니다", "token.expired": "토큰이 만료되었습니다",
"token.invalid": "토큰이 유효하지 않습니다", "token.invalid": "토큰이 유효하지 않습니다",
"toolbar.attach-image": "이미지를 첨부하거나 드래그앤드롭하거나 클립보드에서 붙여넣으세요", "toolbar.attach-image": "이미지를 첨부하거나 드래그앤드롭하거나 클립보드에서 붙여넣으세요",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Strona", "subscribeByRSS.site": "Strona",
"subscribeByRSS.thread": "Wątek", "subscribeByRSS.thread": "Wątek",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Token", "token": "Skopiuj i wklej token z wiadomości e-mail",
"token.expired": "Wygasły token", "token.expired": "Wygasły token",
"token.invalid": "Token jest nieprawidłowy.", "token.invalid": "Token jest nieprawidłowy.",
"toolbar.attach-image": "Załącz zdjęcie, przeciągnij i upuść lub wklej ze schowka", "toolbar.attach-image": "Załącz zdjęcie, przeciągnij i upuść lub wklej ze schowka",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Сайт", "subscribeByRSS.site": "Сайт",
"subscribeByRSS.thread": "Ветка", "subscribeByRSS.thread": "Ветка",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Токен", "token": "Скопируйте и вставьте токен из электронного письма",
"token.expired": "Время действия токена истекло", "token.expired": "Время действия токена истекло",
"token.invalid": "Токен недействителен", "token.invalid": "Токен недействителен",
"toolbar.attach-image": "Прикрепить изображение, перетащить или вставить изображение из буфера обмена", "toolbar.attach-image": "Прикрепить изображение, перетащить или вставить изображение из буфера обмена",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "เว็บไซต์", "subscribeByRSS.site": "เว็บไซต์",
"subscribeByRSS.thread": "เทรด", "subscribeByRSS.thread": "เทรด",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "โทเค็น", "token": "คัดลอกและวางโทเค็นจากอีเมล",
"token.expired": "โทเค็นหมดอายุ", "token.expired": "โทเค็นหมดอายุ",
"token.invalid": "โทเค็นไม่ถูกต้อง", "token.invalid": "โทเค็นไม่ถูกต้อง",
"toolbar.attach-image": "แนบรูปภาพ หรือลากและวาง", "toolbar.attach-image": "แนบรูปภาพ หรือลากและวาง",
+3 -3
View File
@@ -152,9 +152,9 @@
"subscribeByRSS.site": "Site", "subscribeByRSS.site": "Site",
"subscribeByRSS.thread": "Başlık", "subscribeByRSS.thread": "Başlık",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Parola", "token": "E-postadaki belirteci kopyalayıp yapıştırın",
"token.expired": "Eski parola", "token.expired": "Belirtecin süresi doldu",
"token.invalid": "Geçersiz parola", "token.invalid": "Belirteç geçersiz",
"toolbar.attach-image": "Resim ekleyin, sürükleyip-bıraın veya kopyaladığınız resmi yapıştırın", "toolbar.attach-image": "Resim ekleyin, sürükleyip-bıraın veya kopyaladığınız resmi yapıştırın",
"toolbar.bold": "Kalın yazı ekle {shortcut}", "toolbar.bold": "Kalın yazı ekle {shortcut}",
"toolbar.code": "Kod ekle", "toolbar.code": "Kod ekle",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Сайт", "subscribeByRSS.site": "Сайт",
"subscribeByRSS.thread": "Гілка", "subscribeByRSS.thread": "Гілка",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Токен", "token": "Скопіюйте та вставте токен з листа",
"token.expired": "Час дії токена минув", "token.expired": "Час дії токена минув",
"token.invalid": "Токен недійсний", "token.invalid": "Токен недійсний",
"toolbar.attach-image": "Прикріпити зображення, перетягніть або вставте зображення з буфера обміну", "toolbar.attach-image": "Прикріпити зображення, перетягніть або вставте зображення з буфера обміну",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "Trang", "subscribeByRSS.site": "Trang",
"subscribeByRSS.thread": "Chủ đề", "subscribeByRSS.thread": "Chủ đề",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Token", "token": "Sao chép và dán token từ email",
"token.expired": "token đã hết hạn", "token.expired": "token đã hết hạn",
"token.invalid": "Token không hợp lệ", "token.invalid": "Token không hợp lệ",
"toolbar.attach-image": "Đính kèm hình ảnh, kéo và thả hoặc dán từ khay nhớ tạm", "toolbar.attach-image": "Đính kèm hình ảnh, kéo và thả hoặc dán từ khay nhớ tạm",
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "網站", "subscribeByRSS.site": "網站",
"subscribeByRSS.thread": "Thread", "subscribeByRSS.thread": "Thread",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "Token", "token": "從電子郵件中復制並粘貼 token",
"token.expired": "Token 已過期", "token.expired": "Token 已過期",
"token.invalid": "無效的 Token", "token.invalid": "無效的 Token",
"toolbar.attach-image": "透過托放或從剪貼簿貼上來添加圖片。", "toolbar.attach-image": "透過托放或從剪貼簿貼上來添加圖片。",
+1 -1
View File
@@ -152,7 +152,7 @@
"subscribeByRSS.site": "网站", "subscribeByRSS.site": "网站",
"subscribeByRSS.thread": "会话", "subscribeByRSS.thread": "会话",
"subscribeByRSS.title": "RSS", "subscribeByRSS.title": "RSS",
"token": "令牌", "token": "复制并粘贴电子邮件中的令牌",
"token.expired": "令牌已经过期", "token.expired": "令牌已经过期",
"token.invalid": "令牌无效", "token.invalid": "令牌无效",
"toolbar.attach-image": "附加、拖放或从剪贴板粘贴图片", "toolbar.attach-image": "附加、拖放或从剪贴板粘贴图片",
@@ -20,6 +20,8 @@ export const getCollapsedComments = (): string[] =>
}, []); }, []);
/** /**
* @param siteId site id
* @param url url of the page with comments
* @param info list of string of type "site-id_url_comment-id * @param info list of string of type "site-id_url_comment-id
*/ */
export const saveCollapsedComments = (siteId: string, url: string, info: Comment['id'][]): void => { export const saveCollapsedComments = (siteId: string, url: string, info: Comment['id'][]): void => {
+26 -20
View File
@@ -2,18 +2,39 @@ import 'jest-fetch-mock';
import type { Theme } from 'common/types'; import type { Theme } from 'common/types';
type RemarkConfig = { type RemarkConfig = {
// Hostname of Remark42 server, same as REMARK_URL in backend config, e.g. "https://demo.remark42.com".
host?: string; host?: string;
// The SITE that you passed to Remark42 instance on start of backend.
site_id: string; site_id: string;
// Optional, 'window.location.origin + window.location.pathname' by default.
// URL to the page with comments, it is used as unique identificator for comments thread
//
// Note that if you use query parameters as significant part of URL(the one that actually changes content on page)
// you will have to configure URL manually to keep query params, as 'window.location.origin + window.location.pathname'
// doesn't contain query params and hash. For example, default URL for 'https://example/com/example-post?id=1#hash'
// would be 'https://example/com/example-post'
url?: string; url?: string;
// Optional, '15' by default. Maximum number of comments that is rendered on mobile version.
max_shown_comments?: number; max_shown_comments?: number;
theme?: Theme; // Optional, '15' by default. Maximum number of comments in the last comments widget.
page_title?: string;
locale?: string;
show_email_subscription?: boolean;
max_last_comments?: number; max_last_comments?: number;
__colors__?: Record<string, string>; // Optional, 'dark' or 'light', 'light' by default. Changes UI theme.
theme?: Theme;
// Optional, 'document.title' by default. Title for current comments page.
page_title?: string;
// Optional, 'en' by default. Interface localization.
locale?: string;
// Optional, 'true' by default. Enables email subscription feature in interface when enable it from backend side,
// if you set this param in 'false' you will get notifications email notifications as admin but your users
// won't have interface for subscription
show_email_subscription?: boolean;
// Optional, 'true' by default. Enables RSS subscription feature in interface.
show_rss_subscription?: boolean;
// Optional, 'false' by default. Overrides the parameter from the backend minimized UI with basic info only.
simple_view?: boolean; simple_view?: boolean;
// Optional, 'false' by default. Hides footer with signature and links to Remark42.
no_footer?: boolean; no_footer?: boolean;
__colors__?: Record<string, string>;
}; };
declare global { declare global {
@@ -33,19 +54,4 @@ declare global {
| undefined; | undefined;
}; };
} }
namespace NodeJS {
interface Global {
Headers: typeof Headers;
localStorage: typeof Storage;
}
}
} }
/**
* Variable responsive for dynamic setting public path for
* assets. Dynamic imports with relative url will be resolved over this path.
*
* https://webpack.js.org/guides/public-path/#on-the-fly
*/
declare let __webpack_public_path__: string;
@@ -1,8 +0,0 @@
export function bench<T>(fn: () => T, label = 'bench'): T {
const d = performance.now();
const r = fn();
const dd = performance.now();
// eslint-disable-next-line no-console
console.info(label, dd - d);
return r;
}
@@ -1,22 +0,0 @@
import { Node } from 'common/types';
/**
* Function to debug node tree.
*/
export function debugNode(n: Node): Node {
const d = (n: Node, level: number): void => {
// eslint-disable-next-line no-console
console.log(
`${' '.repeat(level)}${n.comment.text.trim()} | id: ${n.comment.id} | delete: ${n.comment.delete} | pin: ${
n.comment.pin
}`
);
if (n.replies) {
for (const node of n.replies) {
d(node, level + 1);
}
}
};
d(n, 0);
return n;
}
@@ -1,2 +0,0 @@
/** type that makes certain properties of type optional */
export type Derequire<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> & Partial<Pick<T, K>>;
@@ -55,9 +55,10 @@ export async function loadLocale(locale: string): Promise<Record<string, string>
return import(/* webpackChunkName: "ar" */ '../locales/ar.json').then((res) => res.default).catch(() => enMessages); return import(/* webpackChunkName: "ar" */ '../locales/ar.json').then((res) => res.default).catch(() => enMessages);
} }
if (locale === 'zh-tw') { if (locale === 'zh-tw') {
return import(/* webpackChunkName: "zh-tw" */ '../locales/zh-tw.json') return import(/* webpackChunkName: "zh-tw" */ '../locales/zh-tw.json').then((res) => res.default).catch(() => enMessages);
.then((res) => res.default) }
.catch(() => enMessages); if (locale === 'cs') {
return import(/* webpackChunkName: "cs" */ '../locales/cs.json').then((res) => res.default).catch(() => enMessages);
} }
return enMessages; return enMessages;
@@ -36,7 +36,7 @@ export function postMessageToParent(data: ParentMessage): boolean {
* Sends message to target iframe * Sends message to target iframe
* *
* @param target iframe to send data * @param target iframe to send data
* @param data that will be send to iframe * @param data that will be sent to iframe
* @returns request success of fail * @returns request success of fail
*/ */
export function postMessageToIframe(target: HTMLIFrameElement, data: ChildMessage): boolean { export function postMessageToIframe(target: HTMLIFrameElement, data: ChildMessage): boolean {
@@ -61,3 +61,24 @@ export function parseMessage({ data }: MessageEvent): AllMessages {
return data as AllMessages; return data as AllMessages;
} }
/**
* Sends message to parent window with height of iframe
*
* @param dropdown provide dropdown element if present on screen
*/
export function updateIframeHeight(dropdown?: HTMLElement) {
let scrollHeight = 0;
// If dropdown is present on screen, we need to calculate size according to it size since it's positioned absolutely
if (dropdown) {
const { top } = dropdown.getBoundingClientRect();
// The size of shadow under the dropdown is 20px
scrollHeight = window.scrollY + Math.abs(top) + dropdown.scrollHeight + 20;
}
// The size of vertical padding on body is 12px
const bodyHeight = document.body.offsetHeight + 12;
postMessageToParent({ height: Math.max(scrollHeight, bodyHeight) });
}
@@ -1,3 +1,3 @@
export function replaceSelection(text: string, selection: [number, number], replacement: string): string { export function replaceSelection(text: string, selection: [number, number], replacement: string): string {
return text.substr(0, selection[0]) + replacement + text.substr(selection[1]); return text.substring(0, selection[0]) + replacement + text.substring(selection[1]);
} }

Some files were not shown because too many files have changed in this diff Show More