Compare commits

..
Author SHA1 Message Date
Dmitry VerkhoturovandUmputun 95966f6407 add escaping of comment text in webhook default JSON template 2024-07-01 23:41:50 -05:00
Umputun 8df986e70a Add content type check for images endpoint
A check in image proxy for validating content type of requested images added. Modified the related tests to accommodate these changes.
2024-07-01 14:47:25 -05:00
dependabot[bot]andUmputun 71a6d0b385 Bump pug from 3.0.2 to 3.0.3 in /site
Bumps [pug](https://github.com/pugjs/pug) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/pugjs/pug/releases)
- [Commits](https://github.com/pugjs/pug/compare/pug@3.0.2...pug@3.0.3)

---
updated-dependencies:
- dependency-name: pug
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 12:12:14 -05:00
dependabot[bot]andUmputun 974d4aaf55 Bump ejs from 3.1.9 to 3.1.10 in /site
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 12:11:10 -05:00
dependabot[bot]andUmputun dc8d7d46cb Bump braces from 3.0.2 to 3.0.3 in /site
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 12:10:47 -05:00
dependabot[bot]andUmputun c4ace9fc0c Bump ws from 8.16.0 to 8.17.1 in /site
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 12:10:26 -05:00
Dmitry VerkhoturovandUmputun 16b07ded66 fix golangci-lint reported unused parameters in example module 2024-06-30 12:07:06 -05:00
Tomy HsiehandUmputun c04705947a 📖 docs: Update README 2024-06-22 12:04:49 -05:00
Dmitry VerkhoturovandUmputun eadd65e247 update docker images, clarify comments 2024-06-05 13:05:42 -05:00
Xin LiandUmputun 4428f79046 SubscribeByEmailForm: Ensure onInput and onClick props are typed correctly.
Resolves #1771
2024-06-04 11:06:14 -05:00
Armen MkrtchyanandUmputun bad6af87f7 Update .golangci.yml
Sorted linters alphabetically, removed duplicates
2024-05-30 11:30:19 -05:00
Edward NavarroandUmputun f7ba43e5f1 Complete and update Spanish translations 2024-05-27 15:22:09 -05:00
18 changed files with 374 additions and 287 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:16.20.2-alpine AS frontend-deps
FROM --platform=$BUILDPLATFORM node:16.20-alpine AS frontend-deps
ARG SKIP_FRONTEND_TEST
ARG SKIP_FRONTEND_BUILD
@@ -45,7 +45,7 @@ RUN \
echo 'Skip frontend build'; \
fi
FROM umputun/baseimage:buildgo-v1.12.0 as build-backend
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
ARG CI
ARG GITHUB_REF
@@ -81,7 +81,7 @@ RUN \
echo "version=$version" && \
go build -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
FROM umputun/baseimage:app-v1.12.0
FROM umputun/baseimage:app-v1.13.0
ARG GITHUB_SHA
@@ -89,7 +89,7 @@ LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
org.opencontainers.image.description="Remark42 comment engine" \
org.opencontainers.image.documentation="https://remark42.com/docs/getting-started/" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
org.opencontainers.image.source="https://github.com/umputun/remark42" \
org.opencontainers.image.title="Remark42" \
org.opencontainers.image.url="https://remark42.com/" \
org.opencontainers.image.revision="${GITHUB_SHA}"
+1 -1
View File
@@ -19,7 +19,7 @@ WORKDIR /srv/frontend/apps/remark42/
COPY ./frontend/apps/remark42/ /srv/frontend/apps/remark42/
RUN pnpm build
FROM umputun/baseimage:buildgo-v1.12.0 as build-backend
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
ARG GITHUB_TOKEN
ARG GITHUB_REF
+15 -17
View File
@@ -26,27 +26,25 @@ linters-settings:
linters:
enable:
- bodyclose
- gosimple
- staticcheck
- unused
- revive
- govet
- unconvert
- gosec
- gocyclo
- dupl
- exportloopref
- gochecknoinits
- gocritic
- gocyclo
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- prealloc
- revive
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- typecheck
- ineffassign
- stylecheck
- gochecknoinits
- exportloopref
- gocritic
- nakedret
- gosimple
- prealloc
fast: false
disable-all: true
+3 -3
View File
@@ -1,11 +1,11 @@
FROM umputun/baseimage:buildgo-v1.12.0 as build-backend
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
ADD backend /build/backend
WORKDIR /build/backend/_example/memory_store
RUN go build -o /build/bin/memory_store -ldflags "-X main.revision=0.0.0 -s -w"
FROM umputun/baseimage:app-v1.12.0
FROM umputun/baseimage:app-v1.13.0
ARG GITHUB_SHA
@@ -13,7 +13,7 @@ LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
org.opencontainers.image.description="Remark42 comment engine example JRPC memory store" \
org.opencontainers.image.documentation="https://github.com/umputun/remark42/tree/master/backend/_example/memory_store" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
org.opencontainers.image.source="https://github.com/umputun/remark42" \
org.opencontainers.image.title="Remark42 JRPC example memory store" \
org.opencontainers.image.url="https://remark42.com/" \
org.opencontainers.image.revision="${GITHUB_SHA}"
+22 -21
View File
@@ -293,17 +293,17 @@ func (m *MemData) UserDetail(req engine.UserDetailRequest) ([]engine.UserDetailE
defer m.mu.Unlock()
if req.Update == "" { // read detail value, no update requested
return m.getUserDetail(req)
return m.getUserDetail(req), nil
}
return m.setUserDetail(req)
return m.setUserDetail(req), nil
case engine.AllUserDetails:
// list of all details returned in case request is a read request
// (Update is not set) and does not have UserID or Detail set
if req.Update == "" && req.UserID == "" { // read list of all details
m.mu.Lock()
defer m.mu.Unlock()
return m.listDetails(req.Locator)
return m.listDetails(req.Locator), nil
}
return nil, fmt.Errorf("unsupported request with userdetail all")
default:
@@ -319,7 +319,8 @@ func (m *MemData) Delete(req engine.DeleteRequest) error {
switch {
case req.UserDetail != "": // delete user detail
return m.deleteUserDetail(req.Locator, req.UserID, req.UserDetail)
m.deleteUserDetail(req.Locator, req.UserID, req.UserDetail)
return nil
case req.Locator.URL != "" && req.CommentID != "" && req.UserDetail == "": // delete comment
return m.deleteComment(req.Locator, req.CommentID, req.DeleteMode)
@@ -332,7 +333,8 @@ func (m *MemData) Delete(req engine.DeleteRequest) error {
return e
}
}
return m.deleteUserDetail(req.Locator, req.UserID, engine.AllUserDetails)
m.deleteUserDetail(req.Locator, req.UserID, engine.AllUserDetails)
return nil
case req.Locator.SiteID != "" && req.Locator.URL == "" && req.CommentID == "" && req.UserID == "" && req.UserDetail == "": // delete site
if _, ok := m.posts[req.Locator.SiteID]; !ok {
@@ -437,29 +439,29 @@ func (m *MemData) setFlag(req engine.FlagRequest) (res bool, err error) {
// getUserDetail returns UserDetailEntry with requested userDetail (omitting other details)
// as an only element of the slice.
func (m *MemData) getUserDetail(req engine.UserDetailRequest) ([]engine.UserDetailEntry, error) {
func (m *MemData) getUserDetail(req engine.UserDetailRequest) []engine.UserDetailEntry {
if meta, ok := m.metaUsers[req.UserID]; ok {
if meta.SiteID != req.Locator.SiteID {
return []engine.UserDetailEntry{}, nil
return []engine.UserDetailEntry{}
}
switch req.Detail {
case engine.UserEmail:
return []engine.UserDetailEntry{{UserID: req.UserID, Email: meta.Details.Email}}, nil
return []engine.UserDetailEntry{{UserID: req.UserID, Email: meta.Details.Email}}
case engine.UserTelegram:
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: meta.Details.Telegram}}, nil
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: meta.Details.Telegram}}
}
}
return []engine.UserDetailEntry{}, nil
return []engine.UserDetailEntry{}
}
// setUserDetail sets requested userDetail, returning complete updated UserDetailEntry as an onlyIps
// element of the slice in case of success
func (m *MemData) setUserDetail(req engine.UserDetailRequest) ([]engine.UserDetailEntry, error) {
func (m *MemData) setUserDetail(req engine.UserDetailRequest) []engine.UserDetailEntry {
var entry metaUser
if meta, ok := m.metaUsers[req.UserID]; ok {
if meta.SiteID != req.Locator.SiteID {
return []engine.UserDetailEntry{}, nil
return []engine.UserDetailEntry{}
}
entry = meta
}
@@ -476,42 +478,42 @@ func (m *MemData) setUserDetail(req engine.UserDetailRequest) ([]engine.UserDeta
case engine.UserEmail:
entry.Details.Email = req.Update
m.metaUsers[req.UserID] = entry
return []engine.UserDetailEntry{{UserID: req.UserID, Email: req.Update}}, nil
return []engine.UserDetailEntry{{UserID: req.UserID, Email: req.Update}}
case engine.UserTelegram:
entry.Details.Telegram = req.Update
m.metaUsers[req.UserID] = entry
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: req.Update}}, nil
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: req.Update}}
}
return []engine.UserDetailEntry{}, nil
return []engine.UserDetailEntry{}
}
// listDetails lists all available users details for given siteID
func (m *MemData) listDetails(loc store.Locator) ([]engine.UserDetailEntry, error) {
func (m *MemData) listDetails(loc store.Locator) []engine.UserDetailEntry {
var res []engine.UserDetailEntry
for _, u := range m.metaUsers {
if u.SiteID == loc.SiteID {
res = append(res, u.Details)
}
}
return res, nil
return res
}
// deleteUserDetail deletes requested UserDetail or whole UserDetailEntry,
// deletion of the absent entry doesn't produce error.
// Trying to delete user with wrong siteID doesn't to anything and doesn't produce error.
func (m *MemData) deleteUserDetail(locator store.Locator, userID string, userDetail engine.UserDetail) error {
func (m *MemData) deleteUserDetail(locator store.Locator, userID string, userDetail engine.UserDetail) {
var entry metaUser
if meta, ok := m.metaUsers[userID]; ok {
if meta.SiteID != locator.SiteID {
return nil
return
}
entry = meta
}
if entry == (metaUser{}) || entry.Details == (engine.UserDetailEntry{}) {
// absent entry means that we should not do anything
return nil
return
}
switch userDetail {
@@ -529,7 +531,6 @@ func (m *MemData) deleteUserDetail(locator store.Locator, userID string, userDet
}
m.metaUsers[userID] = entry
return nil
}
func (m *MemData) get(loc store.Locator, commentID string) (store.Comment, error) {
+12 -2
View File
@@ -3,6 +3,7 @@ package notify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"text/template"
"time"
@@ -12,7 +13,7 @@ import (
)
const (
webhookDefaultTemplate = `{"text": "{{.Text}}"}`
webhookDefaultTemplate = `{"text": {{.Text | escapeJSONString}}}`
)
// WebhookParams contain settings for webhook notifications
@@ -49,7 +50,7 @@ func NewWebhook(params WebhookParams) (*Webhook, error) {
params.Template = webhookDefaultTemplate
}
payloadTmpl, err := template.New("webhook").Parse(params.Template)
payloadTmpl, err := template.New("webhook").Funcs(template.FuncMap{"escapeJSONString": escapeJSONString}).Parse(params.Template)
if err != nil {
return nil, fmt.Errorf("unable to parse webhook template: %w", err)
}
@@ -82,3 +83,12 @@ func (w *Webhook) SendVerification(_ context.Context, _ VerificationRequest) err
func (w *Webhook) String() string {
return fmt.Sprintf("%s to %s", w.Webhook.String(), w.url)
}
// escapeJSONString escapes string for JSON
func escapeJSONString(s string) (string, error) {
b, err := json.Marshal(s)
if err != nil {
return "", err
}
return string(b), nil
}
+29
View File
@@ -2,6 +2,9 @@ package notify
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
@@ -34,6 +37,32 @@ func TestWebhook_NewWebhook(t *testing.T) {
assert.Contains(t, err.Error(), "unable to parse webhook template")
}
// https://github.com/umputun/remark42/issues/1791
func TestWebhook_ReceiveValidJSON(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.URL.Path, "/webhook-notify")
assert.Equal(t, "POST", r.Method)
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
t.Log("received body", string(body))
assert.JSONEq(t, `{"text": "<p>testme</p>\n"}`, string(body))
}))
defer ts.Close()
wh, err := NewWebhook(WebhookParams{
URL: ts.URL + "/webhook-notify",
Headers: []string{"Content-Type:application/json,text/plain"},
})
assert.NoError(t, err)
assert.NotNil(t, wh)
f := store.NewCommentFormatter()
c := store.Comment{Text: f.FormatText("testme", false), ParentID: "1", ID: "999"}
err = wh.Send(context.Background(), Request{Comment: c})
assert.NoError(t, err)
}
func TestWebhook_Send(t *testing.T) {
wh, err := NewWebhook(WebhookParams{
URL: "bad-url",
+11 -6
View File
@@ -76,10 +76,11 @@ type Rest struct {
httpServer *http.Server
lock sync.Mutex
pubRest public
privRest private
adminRest admin
rssRest rss
pubRest public
privRest private
adminRest admin
rssRest rss
openRouteLimiter float64
}
// LoadingCache defines interface for caching
@@ -90,7 +91,7 @@ type LoadingCache interface {
}
const hardBodyLimit = 1024 * 64 // limit size of body
const openRouteLimiter = 10 // limit for open routes
const lastCommentsScope = "last"
type commentsWithInfo struct {
@@ -198,6 +199,10 @@ func (s *Rest) makeHTTPServer(address string, port int, router http.Handler) *ht
}
func (s *Rest) routes() chi.Router {
if s.openRouteLimiter == 0 {
// set the default open route limiter. Just a safety measure as it should be set by Run method anyway
s.openRouteLimiter = openRouteLimiter
}
router := chi.NewRouter()
router.Use(middleware.Throttle(1000), middleware.RealIP, R.Recoverer(log.Default()))
if !s.DisableSignature {
@@ -257,7 +262,7 @@ func (s *Rest) routes() chi.Router {
// open routes
rapi.Group(func(ropen chi.Router) {
ropen.Use(middleware.Timeout(30 * time.Second))
ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.openRouteLimiter, nil)))
ropen.Use(authMiddleware.Trace, middleware.NoCache, logInfoWithBody)
ropen.Get("/config", s.configCtrl)
ropen.Get("/find", s.pubRest.findCommentsCtrl)
+5 -3
View File
@@ -487,8 +487,9 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr
Cache: memCache,
KeyStore: astore,
},
NotifyService: notify.NopService,
EmojiEnabled: true,
NotifyService: notify.NopService,
EmojiEnabled: true,
openRouteLimiter: 100,
}
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = -5, -10
@@ -504,7 +505,8 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr
h(srv)
}
ts = httptest.NewServer(srv.routes())
routes := srv.routes()
ts = httptest.NewServer(routes)
teardown = func() {
ts.Close()
+9
View File
@@ -98,6 +98,10 @@ func (p Image) Handler(w http.ResponseWriter, r *http.Request) {
if img == nil {
img, err = p.downloadImage(context.Background(), imgURL)
if err != nil {
if strings.Contains(err.Error(), "invalid content type") {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid content type", rest.ErrImgNotFound)
return
}
rest.SendErrorJSON(w, r, http.StatusNotFound, err, "can't get image "+imgURL, rest.ErrAssetNotFound)
return
}
@@ -165,6 +169,11 @@ func (p Image) downloadImage(ctx context.Context, imgURL string) ([]byte, error)
return nil, fmt.Errorf("got unsuccessful response status %d while fetching %s", resp.StatusCode, imgURL)
}
contentType := resp.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "image/") {
return nil, fmt.Errorf("invalid content type %s", contentType)
}
imgData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read image body")
+32 -18
View File
@@ -108,27 +108,41 @@ func TestImage_Routes(t *testing.T) {
httpSrv := imgHTTPTestsServer(t)
defer httpSrv.Close()
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
t.Run("valid image", func(t *testing.T) {
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
require.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
})
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
require.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
t.Run("no image", func(t *testing.T) {
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/no-such-image.png"))
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
require.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
})
encodedImgURL = base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/no-such-image.png"))
resp, err = http.Get(ts.URL + "/?src=" + encodedImgURL)
require.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
t.Run("bad encoding", func(t *testing.T) {
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "bad encoding"))
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
require.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.Equal(t, 2, len(imageStore.LoadCalls()))
})
encodedImgURL = base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "bad encoding"))
resp, err = http.Get(ts.URL + "/?src=" + encodedImgURL)
require.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.Equal(t, 2, len(imageStore.LoadCalls()))
t.Run("non-image reference", func(t *testing.T) {
encodedImgURL := base64.URLEncoding.EncodeToString([]byte("https://google.com"))
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
require.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.Equal(t, 3, len(imageStore.LoadCalls()))
})
}
func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.40.1-jammy
FROM mcr.microsoft.com/playwright:v1.44-jammy
ENV CI true
WORKDIR /frontend
@@ -193,7 +193,7 @@ describe('<SubscribeByEmailForm/>', () => {
it('should send form by paste valid token', async () => {
const wrapper = createWrapper();
const onInputEmail = wrapper.find(Input).prop('onInput');
const onInputEmail = wrapper.find(Input).prop('onInput') as Function;
const form = wrapper.find('form');
expect(typeof onInputEmail === 'function').toBe(true);
@@ -220,7 +220,7 @@ describe('<SubscribeByEmailForm/>', () => {
it('should pass throw unsubscribe process', async () => {
const store = mockStore({ ...initialStore, user: { email_subscription: true } });
const wrapper = createWrapper(store);
const onClick = wrapper.find(Button).prop('onClick');
const onClick = wrapper.find(Button).prop('onClick') as Function;
expect(typeof onClick === 'function').toBe(true);
+48 -48
View File
@@ -1,32 +1,32 @@
{
"auth.back": "Volver",
"auth.email-address": "Dirección de correo electrónico",
"auth.back": "Regresar",
"auth.email-address": "Correo electrónico",
"auth.loading": "Cargando...",
"auth.oauth-button": "Inicie sesión con {provider}",
"auth.oauth-source": "Acceda a través de sus redes sociales",
"auth.oauth-button": "Iniciar sesión con {provider}",
"auth.oauth-source": "Inicia sesión con tus redes sociales",
"auth.open-profile": "Abrir mi perfil",
"auth.or": "o",
"auth.signin": "Acceder",
"auth.signout": "¿Salir?",
"auth.submit": "Enviar",
"auth.or": "ó",
"auth.signin": "Iniciar sesión",
"auth.signout": "¿Cerrar sesión?",
"auth.submit": "Ingresar",
"auth.symbols-restriction": "El nombre de usuario debe comenzar con una letra y contener solamente letras latinas, números, guión bajo o espacio",
"auth.telegram-check": "Marcar",
"auth.telegram-link": "Telegram bot",
"auth.telegram-message-1": "You need to authorize your account in the",
"auth.telegram-message-2": "by clicking “Start” there.",
"auth.telegram-message-3": "Click “Check” below after authorization in the Telegram bot.",
"auth.telegram-optional-qr": "o escaneando el código QR",
"auth.telegram-check": "Verificar",
"auth.telegram-link": "haciendo clic aquí",
"auth.telegram-message-1": "Abre Telegram",
"auth.telegram-message-2": "y luego selecciona la opción “Start” para iniciar la conversación con el bot.",
"auth.telegram-message-3": "Una vez autorizado por el bot, regresa aquí y haz clic en “Verificar”.",
"auth.telegram-optional-qr": "ó escaneando el código QR",
"auth.telegram-qr": "Código QR de Telegram",
"auth.user-not-found": "No se encontró el usuario",
"auth.username": "Nombre de usuario",
"authPanel.disable-comments": "Deshabilitar comentarios",
"authPanel.disabled-cookies": "Deshabilita las cookies de terceros que bloquean el acceso o abre los comentarios en una",
"authPanel.disabled-cookies": "Deshabilita el bloqueo de cookies de terceros para acceder y comentar",
"authPanel.enable-comments": "Habilitar comentarios",
"authPanel.enable-cookies": "Habilitar cookies para acceder y comentar",
"authPanel.hide-settings": "Ocultar opciones",
"authPanel.hide-settings": "Ocultar configuración",
"authPanel.new-page": "nueva página",
"authPanel.read-only": "Solo lectura",
"authPanel.show-settings": "Mostrar opciones",
"authPanel.show-settings": "Mostrar configuración",
"blockingDuration.day": "Por un día",
"blockingDuration.month": "Por un mes",
"blockingDuration.permanently": "Permanentemente",
@@ -48,39 +48,39 @@
"comment.go-to-parent": "Ir al comentario padre",
"comment.hide": "Ocultar",
"comment.hide-user-comment": "¿Quieres ocultar los comentarios de {userName}?",
"comment.paid-patreon": "Suscriptor de pago a Patreon",
"comment.pin": "Anclar",
"comment.pin-comment": "¿Quieres anclar este comentario?",
"comment.paid-patreon": "Suscriptor de pago en Patreon",
"comment.pin": "Fijar",
"comment.pin-comment": "¿Quieres fijar este comentario?",
"comment.reply": "Responder",
"comment.time": "{day} a las {time}",
"comment.toggle-verification": "Alternar verificación",
"comment.toggle-verification": "Cambiar verificación",
"comment.unblock": "Desbloquear",
"comment.unblock-user": "¿Quieres desbloquear a este usuario?",
"comment.unpin": "Desanclar",
"comment.unpin-comment": "¿Quieres desanclar este comentario?",
"comment.unpin": "Dejar de fijar",
"comment.unpin-comment": "¿Quieres dejar de fijar este comentario?",
"comment.unverified-user": "Usuario no verificado",
"comment.unverify-user": "¿Quieres quitar la verificación a {userName}?",
"comment.verified-user": "Usuario verificado",
"comment.verify-user": "¿Quieres verificar a {userName}?",
"commentForm.anonymous-uploading-disabled": "La carga de imágenes está deshabilitada para usuarios anónimos. Inicie sesión como usuario no anónimo para poder adjuntar imágenes.",
"commentForm.anonymous-uploading-disabled": "La subida de imágenes está deshabilitada para usuarios anónimos. Inicia sesión como usuario no anónimo para poder adjuntar imágenes.",
"commentForm.exceeded-size": "{fileName} excede el tamaño máximo de {maxImageSize}",
"commentForm.input-placeholder": "Tu comentario aquí",
"commentForm.input-placeholder": "Escribe tu comentario",
"commentForm.new-comment": "Nuevo comentario",
"commentForm.notice-about-styling": "El formato con <a>Markdown</a> está soportado",
"commentForm.notice-about-styling": "Puedes aplicar estilos usando <a>Markdown</a>",
"commentForm.preview": "Pre-visualizar",
"commentForm.reply": "Responder",
"commentForm.save": "Guardar",
"commentForm.send": "Enviar",
"commentForm.subscribe-by": "Suscribirse por",
"commentForm.subscribe-or": "o",
"commentForm.unauthorized-uploading-disabled": "La subida de imágenes está deshabilitada para usuarios no autenticados. Deberías acceder antes de subir imágenes.",
"commentForm.subscribe-or": "ó",
"commentForm.unauthorized-uploading-disabled": "La subida de imágenes está deshabilitada para usuarios no autenticados. Inicia sesión para subir imágenes.",
"commentForm.unexpected-error": "Algo salió mal. Por favor vuelve a intentar más tarde.",
"commentForm.upload-file-fail": "La subida de {fileName} falló con \"{errorMessage}\"",
"commentForm.uploading": "Subiendo...",
"commentForm.uploading-file": "subiendo {fileName}...",
"commentsSort.best": "Mejor",
"commentsSort.least-controversial": "Menos controversial",
"commentsSort.least-recently-updated": "Actualizado menos recientemente",
"commentsSort.least-recently-updated": "Actualizado hace tiempo",
"commentsSort.most-controversial": "Más controversial",
"commentsSort.newest": "Más nuevo",
"commentsSort.oldest": "Más antiguo",
@@ -92,7 +92,7 @@
"errors.10": "Es muy tarde para editar el comentario.",
"errors.11": "El comentario ya tiene una respuesta. No es posible editarlo.",
"errors.12": "No se ha podido guardar el resultado del voto. Por favor vuelve a intentar más tarde.",
"errors.13": "No puedes votar tu propio comentario.",
"errors.13": "No puedes votar por tu propio comentario.",
"errors.14": "Ya has votado el comentario.",
"errors.15": "Demasiados votos para el comentario.",
"errors.16": "Ya se ha alcanzado el puntaje mínimo para el comentario.",
@@ -106,15 +106,15 @@
"errors.5": "El comentario no se ha encontrado. Por favor refresca la página y vuelve a intentar.",
"errors.6": "El sitio no se ha encontrado. Por favor refresca la página y vuelve a intentar.",
"errors.7": "El usuario ha sido bloqueado.",
"errors.8": "El usuario ha sido bloqueado.",
"errors.8": "No se pueden publicar comentarios en esta página. Los comentarios son de solo lectura.",
"errors.9": "No se ha podido cambiar el comentario. Por favor vuelve a intentar más tarde.",
"errors.conflict": "Conflict.",
"errors.conflict": "Conflicto.",
"errors.failed-fetch": "No se ha podido obtener. Por favor revisa tu conexión a internet o vuelve a intentar más tarde",
"errors.forbidden": "Prohibido.",
"errors.not-authorized": "No autorizado.",
"errors.to-many-request": "Has llegado al límite de peticiones.",
"errors.unexpected-error": "Algo salió mal.",
"profile.close": "Cerrar perfil",
"profile.close": "Eliminar perfil",
"profile.request-to-delete-data": "Solicitar la eliminación de mis datos",
"retry": "Intentar de nuevo",
"root.pinned-comments": "Comentarios anclados",
@@ -136,39 +136,39 @@
"settings.unblock-user": "¿Quieres desbloquear a {userName}?",
"settings.unknown": "desconocido",
"sort-by": "Ordernar por",
"subscribeByEmail.back": "Volver",
"subscribeByEmail.back": "Regresar",
"subscribeByEmail.close": "Cerrar",
"subscribeByEmail.email": "Correo electrónico",
"subscribeByEmail.have-been-subscribed": "Has sido suscripto a actualizaciones por correo electrónico",
"subscribeByEmail.have-been-unsubscribed": "Has sido de-suscripto a actualizaciones por correo electrónico",
"subscribeByEmail.have-been-subscribed": "Te has suscrito para recibir actualizaciones por correo electrónico",
"subscribeByEmail.have-been-unsubscribed": "Te has desuscrito de las actualizaciones por correo electrónico",
"subscribeByEmail.only-registered-users": "Disponible solo para usuarios registrados",
"subscribeByEmail.submit": "Enviar",
"subscribeByEmail.subscribe": "Suscribir",
"subscribeByEmail.subscribe-by-email": "Suscribir por correo electrónico",
"subscribeByEmail.subscribe-to-replies": "Suscribir a respuestas",
"subscribeByEmail.subscribed": "Estás suscripto a actualizaciones por correo electrónico",
"subscribeByEmail.unsubscribe": "De-suscribir",
"subscribeByEmail.subscribed": "Estás suscrito a actualizaciones por correo electrónico",
"subscribeByEmail.unsubscribe": "Desuscribir",
"subscribeByRSS.button-title": "Suscribir por RSS",
"subscribeByRSS.replies": "Respuestas",
"subscribeByRSS.site": "Sitio",
"subscribeByRSS.thread": "Hilo",
"subscribeByRSS.title": "RSS",
"subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram",
"subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates",
"subscribeByTelegram.only-registered-users": "Available only for registered users",
"subscribeByTelegram.resubscribe": "Resubscribe",
"subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram",
"subscribeByTelegram.have-been-subscribed": "Te has suscrito para recibir actualizaciones por telegram",
"subscribeByTelegram.have-been-unsubscribed": "Te has desuscrito de las actualizaciones por Telegram",
"subscribeByTelegram.only-registered-users": "Disponible solo para usuarios registrados",
"subscribeByTelegram.resubscribe": "Suscribir",
"subscribeByTelegram.subscribe-by-telegram": "Suscribir por Telegram",
"subscribeByTelegram.telegram": "Telegram",
"subscribeByTelegram.unsubscribe": "Unsubscribe",
"token": "Copie y pegue el token del mensaje del correo electrónico",
"subscribeByTelegram.unsubscribe": "Desuscribir",
"token": "Copia y pega el token de verificación que te enviamos a tu correo electrónico",
"token.expired": "Token expirado",
"token.invalid": "El token no es válido",
"toolbar.attach-image": "Adjunta la imágen, arrastra y suelta, o pega desde el portapapeles",
"toolbar.bold": "Agrega texto en negrita {shortcut}",
"toolbar.code": "Inserta un código",
"toolbar.code": "Inserta un bloque de código",
"toolbar.header": "Agrega un título",
"toolbar.italic": "Agrega texto en cursiva {shortcut}",
"toolbar.link": "Agrega un link {shortcut}",
"toolbar.link": "Agrega un enlace {shortcut}",
"toolbar.ordered-list": "Agrega una lista numerada",
"toolbar.quote": "Inserta una cita",
"toolbar.unordered-list": "Agrega una lista sin numerar",
@@ -177,6 +177,6 @@
"user.my-comments": "Mis comentarios",
"vote.controversy": "Controversia: {value}",
"vote.downvote": "Votar en contra",
"vote.score": "Resultado de los votos",
"vote.score": "Total de votos",
"vote.upvote": "Votar a favor"
}
+1 -1
View File
@@ -11,7 +11,7 @@ LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
org.opencontainers.image.description="Remark42 site" \
org.opencontainers.image.documentation="https://github.com/umputun/remark42/tree/master/site" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
org.opencontainers.image.source="https://github.com/umputun/remark42" \
org.opencontainers.image.title="Remark42 site" \
org.opencontainers.image.url="https://remark42.com/"
@@ -46,4 +46,4 @@ If all goes fine, you should be able to see the following message on your Slack
You need to set `NOTIFY_ADMINS=webhook` to enable WebHook notifications on all new comments and set at least `NOTIFY_WEBHOOK_URL` for them to start working.
Additionally, you might want to set `NOTIFY_WEBHOOK_TEMPLATE` (which is Go Template, `{"text": "{{.Text}}"}` by default) and `NOTIFY_WEBHOOK_HEADERS`, which is HTTP header(s) in format `Header1:Value1,Header2:Value2,...`.
Additionally, you might want to set `NOTIFY_WEBHOOK_TEMPLATE` (which is Go Template, `{"text": {{.Text | escapeJSONString}}}` by default) and `NOTIFY_WEBHOOK_HEADERS`, which is HTTP header(s) in format `Header1:Value1,Header2:Value2,...`.
+131 -131
View File
@@ -33,137 +33,137 @@ services:
### Complete parameters list
| Command line | Environment | Default | Description |
|--------------------------------|--------------------------------|--------------------------|-----------------------------------------------------------|
| url | REMARK_URL | | URL to Remark42 server, _required_ |
| secret | SECRET | | the shared secret key used to sign JWT, should be a random, long, hard-to-guess string, _required_ |
| site | SITE | `remark` | site name(s), _multi_ |
| store.type | STORE_TYPE | `bolt` | type of storage, `bolt` or `rpc` |
| store.bolt.path | STORE_BOLT_PATH | `./var` | parent directory for the bolt files |
| store.bolt.timeout | STORE_BOLT_TIMEOUT | `30s` | boltdb access timeout |
| store.rpc.api | STORE_RPC_API | | rpc extension api url |
| store.rpc.timeout | STORE_RPC_TIMEOUT | | http timeout (default: 5s) |
| store.rpc.auth_user | STORE_RPC_AUTH_USER | | basic auth user name |
| store.rpc.auth_passwd | STORE_RPC_AUTH_PASSWD | | basic auth user password |
| admin.type | ADMIN_TYPE | `shared` | type of admin store, `shared` or `rpc` |
| admin.rpc.api | ADMIN_RPC_API | | rpc extension api url |
| admin.rpc.timeout | ADMIN_RPC_TIMEOUT | | http timeout (default: 5s) |
| admin.rpc.auth_user | ADMIN_RPC_AUTH_USER | | basic auth user name |
| admin.rpc.auth_passwd | ADMIN_RPC_AUTH_PASSWD | | basic auth user password |
| admin.rpc.secret_per_site | ADMIN_RPC_SECRET_PER_SITE | | enable JWT secret retrieval per aud, which is site_id in this case |
| admin.shared.id | ADMIN_SHARED_ID | | admin IDs (list of user IDs), _multi_ |
| admin.shared.email | ADMIN_SHARED_EMAIL | `admin@${REMARK_URL}` | admin emails, _multi_ |
| backup | BACKUP_PATH | `./var/backup` | backups location |
| max-back | MAX_BACKUP_FILES | `10` | max backup files to keep |
| cache.type | CACHE_TYPE | `mem` | type of cache, `redis_pub_sub` or `mem` or `none` |
| cache.redis_addr | CACHE_REDIS_ADDR | `127.0.0.1:6379` | address of Redis PubSub instance, turn `redis_pub_sub` cache on for distributed cache |
| cache.max.items | CACHE_MAX_ITEMS | `1000` | max number of cached items, `0` - unlimited |
| cache.max.value | CACHE_MAX_VALUE | `65536` | max size of the cached value, `0` - unlimited |
| cache.max.size | CACHE_MAX_SIZE | `50000000` | max size of all cached values, `0` - unlimited |
| avatar.type | AVATAR_TYPE | `fs` | type of avatar storage, `fs`, `bolt`, or `uri` |
| avatar.fs.path | AVATAR_FS_PATH | `./var/avatars` | avatars location for `fs` store |
| avatar.bolt.file | AVATAR_BOLT_FILE | `./var/avatars.db` | avatars `bolt` file location |
| avatar.uri | AVATAR_URI | `./var/avatars` | avatars store URI |
| avatar.rsz-lmt | AVATAR_RESIZE | `0` (disabled) | max image size for resizing avatars on save |
| image.type | IMAGE_TYPE | `fs` | type of image storage, `fs`, `bolt` or `rpc` |
| image.fs.path | IMAGE_FS_PATH | `./var/pictures` | permanent location of images |
| image.fs.staging | IMAGE_FS_STAGING | `./var/pictures.staging` | staging location of images |
| image.fs.partitions | IMAGE_FS_PARTITIONS | `100` | number of image partitions |
| image.bolt.file | IMAGE_BOLT_FILE | `/var/pictures.db` | images bolt file location |
| image.rpc.api | IMAGE_RPC_API | | rpc extension api url |
| image.rpc.timeout | IMAGE_RPC_TIMEOUT | | http timeout (default: 5s) |
| image.rpc.auth_user | IMAGE_RPC_AUTH_USER | | basic auth user name |
| image.rpc.auth_passwd | IMAGE_RPC_AUTH_PASSWD | | basic auth user password |
| image.max-size | IMAGE_MAX_SIZE | `5000000` | max size of image file |
| image.resize-width | IMAGE_RESIZE_WIDTH | `2400` | width of a resized image |
| image.resize-height | IMAGE_RESIZE_HEIGHT | `900` | height of a resized image |
| auth.ttl.jwt | AUTH_TTL_JWT | `5m` | JWT TTL |
| auth.ttl.cookie | AUTH_TTL_COOKIE | `200h` | cookie TTL |
| auth.send-jwt-header | AUTH_SEND_JWT_HEADER | `false` | send JWT as a header instead of a cookie |
| auth.same-site | AUTH_SAME_SITE | `default` | set same site policy for cookies (`default`, `none`, `lax` or `strict`) |
| auth.apple.cid | AUTH_APPLE_CID | | Apple client ID |
| auth.apple.tid | AUTH_APPLE_TID | | Apple service ID |
| auth.apple.kid | AUTH_APPLE_KID | | Private key ID |
| auth.apple.private-key-filepath | AUTH_APPLE_PRIVATE_KEY_FILEPATH | `/srv/var/apple.p8` | Private key file location |
| auth.google.cid | AUTH_GOOGLE_CID | | Google OAuth client ID |
| auth.google.csec | AUTH_GOOGLE_CSEC | | Google OAuth client secret |
| auth.facebook.cid | AUTH_FACEBOOK_CID | | Facebook OAuth client ID |
| auth.facebook.csec | AUTH_FACEBOOK_CSEC | | Facebook OAuth client secret |
| auth.microsoft.cid | AUTH_MICROSOFT_CID | | Microsoft OAuth client ID |
| auth.microsoft.csec | AUTH_MICROSOFT_CSEC | | Microsoft OAuth client secret |
| auth.github.cid | AUTH_GITHUB_CID | | GitHub OAuth client ID |
| auth.github.csec | AUTH_GITHUB_CSEC | | GitHub OAuth client secret |
| auth.twitter.cid | AUTH_TWITTER_CID | | Twitter Consumer API Key |
| auth.twitter.csec | AUTH_TWITTER_CSEC | | Twitter Consumer API Secret key |
| auth.patreon.cid | AUTH_PATREON_CID | | Patreon OAuth Client ID |
| auth.patreon.csec | AUTH_PATREON_CSEC | | Patreon OAuth Client Secret |
| auth.telegram | AUTH_TELEGRAM | `false` | Enable Telegram auth (telegram.token must be present) |
| auth.yandex.cid | AUTH_YANDEX_CID | | Yandex OAuth client ID |
| auth.yandex.csec | AUTH_YANDEX_CSEC | | Yandex OAuth client secret |
| auth.dev | AUTH_DEV | `false` | local OAuth2 server, development mode only |
| auth.anon | AUTH_ANON | `false` | enable anonymous login |
| auth.email.enable | AUTH_EMAIL_ENABLE | `false` | enable auth via email |
| auth.email.from | AUTH_EMAIL_FROM | | email from |
| auth.email.subj | AUTH_EMAIL_SUBJ | `remark42 confirmation` | email subject |
| auth.email.content-type | AUTH_EMAIL_CONTENT_TYPE | `text/html` | email content type |
| notify.users | NOTIFY_USERS | none | type of user notifications (`telegram`, `email`), _multi_ |
| notify.admins | NOTIFY_ADMINS | none | type of admin notifications (`telegram`, `slack`, `webhook` and/or `email`), _multi_ |
| notify.queue | NOTIFY_QUEUE | `100` | size of notification queue |
| notify.telegram.chan | NOTIFY_TELEGRAM_CHAN | | the ID of telegram channel for admin notifications |
| notify.slack.token | NOTIFY_SLACK_TOKEN | | Slack token |
| notify.slack.chan | NOTIFY_SLACK_CHAN | `general` | Slack channel for admin notifications |
| notify.webhook.url | NOTIFY_WEBHOOK_URL | | Webhook notification URL for admin notifications |
| notify.webhook.template | NOTIFY_WEBHOOK_TEMPLATE | `{"text": "{{.Text}}"}` | Webhook payload template |
| notify.webhook.headers | NOTIFY_WEBHOOK_HEADERS | | HTTP header in format Header1:Value1,Header2:Value2,... |
| notify.webhook.timeout | NOTIFY_WEBHOOK_TIMEOUT | `5s` | Webhook connection timeout |
| notify.email.from_address | NOTIFY_EMAIL_FROM | | from email address |
| notify.email.verification_subj | NOTIFY_EMAIL_VERIFICATION_SUBJ | `Email verification` | verification message subject |
| telegram.token | TELEGRAM_TOKEN | | Telegram token (used for auth and Telegram notifications) |
| telegram.timeout | TELEGRAM_TIMEOUT | `5s` | Telegram connection timeout |
| smtp.host | SMTP_HOST | | SMTP host |
| smtp.port | SMTP_PORT | | SMTP port |
| smtp.username | SMTP_USERNAME | | SMTP user name |
| smtp.password | SMTP_PASSWORD | | SMTP password |
| smtp.login_auth | SMTP_LOGIN_AUTH | `false | enable LOGIN auth instead of PLAIN |
| smtp.tls | SMTP_TLS | `false` | enable TLS for SMTP |
| smtp.starttls | SMTP_STARTTLS | `false` | enable StartTLS for SMTP |
| smtp.insecure_skip_verify | SMTP_INSECURE_SKIP_VERIFY | `false` | skip certificate verification for SMTP |
| smtp.timeout | SMTP_TIMEOUT | `10s` | SMTP TCP connection timeout |
| ssl.type | SSL_TYPE | none | `none`-HTTP, `static`-HTTPS, `auto`-HTTPS + le |
| ssl.port | SSL_PORT | `8443` | port for HTTPS server |
| ssl.cert | SSL_CERT | | path to the cert.pem file |
| ssl.key | SSL_KEY | | path to the key.pem file |
| ssl.acme-location | SSL_ACME_LOCATION | `./var/acme` | dir where obtained le-certs will be stored |
| ssl.acme-email | SSL_ACME_EMAIL | | admin email for receiving notifications from LE |
| max-comment | MAX_COMMENT_SIZE | `2048` | comment's size limit |
| min-comment | MIN_COMMENT_SIZE | `0` | comment's minimal size limit, `0` - unlimited |
| max-votes | MAX_VOTES | `-1` | votes limit per comment, `-1` - unlimited |
| votes-ip | VOTES_IP | `false` | restrict votes from the same IP |
| anon-vote | ANON_VOTE | `false` | allow voting for anonymous users, require VOTES_IP to be enabled as well |
| votes-ip-time | VOTES_IP_TIME | `5m` | same IP vote restriction time, `0s` - unlimited |
| low-score | LOW_SCORE | `-5` | low score threshold |
| critical-score | CRITICAL_SCORE | `-10` | critical score threshold |
| positive-score | POSITIVE_SCORE | `false` | restricts comment's score to be only positive |
| restricted-words | RESTRICTED_WORDS | | words banned in comments (can use `*`), _multi_ |
| restricted-names | RESTRICTED_NAMES | | names prohibited to use by the user, _multi_ |
| edit-time | EDIT_TIME | `5m` | edit window |
| admin-edit | ADMIN_EDIT | `false` | unlimited edit for admins |
| read-age | READONLY_AGE | | read-only age of comments, days |
| image-proxy.http2https | IMAGE_PROXY_HTTP2HTTPS | `false` | enable HTTP->HTTPS proxy for images |
| image-proxy.cache-external | IMAGE_PROXY_CACHE_EXTERNAL | `false` | enable caching external images to current image storage |
| emoji | EMOJI | `false` | enable emoji support |
| simple-view | SIMPLE_VIEW | `false` | minimized UI with basic info only |
| proxy-cors | PROXY_CORS | `false` | disable internal CORS and delegate it to proxy |
| allowed-hosts | ALLOWED_HOSTS | enable all | limit hosts/sources allowed to embed comments |
| address | REMARK_ADDRESS | all interfaces | web server listening address |
| port | REMARK_PORT | `8080` | web server port |
| web-root | REMARK_WEB_ROOT | `./web` | web server root directory |
| update-limit | UPDATE_LIMIT | `0.5` | updates/sec limit |
| subscribers-only | SUBSCRIBERS_ONLY | `false` | enable commenting only for Patreon subscribers |
| disable-signature | DISABLE_SIGNATURE | `false` | disable server signature in headers |
| disable-fancy-text-formatting | DISABLE_FANCY_HTML_FORMATTING | `false` | disable fancy comments text formatting (replacement of quotes, dashes, fractions, etc) |
| admin-passwd | ADMIN_PASSWD | none (disabled) | password for `admin` basic auth |
| dbg | DEBUG | `false` | debug mode |
| Command line | Environment | Default | Description |
|--------------------------------|--------------------------------|-------------------------|----------------------------------------------------------|
| url | REMARK_URL | | URL to Remark42 server, _required_ |
| secret | SECRET | | the shared secret key used to sign JWT, should be a random, long, hard-to-guess string, _required_ |
| site | SITE | `remark` | site name(s), _multi_ |
| store.type | STORE_TYPE | `bolt` | type of storage, `bolt` or `rpc` |
| store.bolt.path | STORE_BOLT_PATH | `./var` | parent directory for the bolt files |
| store.bolt.timeout | STORE_BOLT_TIMEOUT | `30s` | boltdb access timeout |
| store.rpc.api | STORE_RPC_API | | rpc extension api url |
| store.rpc.timeout | STORE_RPC_TIMEOUT | | http timeout (default: 5s) |
| store.rpc.auth_user | STORE_RPC_AUTH_USER | | basic auth user name |
| store.rpc.auth_passwd | STORE_RPC_AUTH_PASSWD | | basic auth user password |
| admin.type | ADMIN_TYPE | `shared` | type of admin store, `shared` or `rpc` |
| admin.rpc.api | ADMIN_RPC_API | | rpc extension api url |
| admin.rpc.timeout | ADMIN_RPC_TIMEOUT | | http timeout (default: 5s) |
| admin.rpc.auth_user | ADMIN_RPC_AUTH_USER | | basic auth user name |
| admin.rpc.auth_passwd | ADMIN_RPC_AUTH_PASSWD | | basic auth user password |
| admin.rpc.secret_per_site | ADMIN_RPC_SECRET_PER_SITE | | enable JWT secret retrieval per aud, which is site_id in this case |
| admin.shared.id | ADMIN_SHARED_ID | | admin IDs (list of user IDs), _multi_ |
| admin.shared.email | ADMIN_SHARED_EMAIL | `admin@${REMARK_URL}` | admin emails, _multi_ |
| backup | BACKUP_PATH | `./var/backup` | backups location |
| max-back | MAX_BACKUP_FILES | `10` | max backup files to keep |
| cache.type | CACHE_TYPE | `mem` | type of cache, `redis_pub_sub` or `mem` or `none` |
| cache.redis_addr | CACHE_REDIS_ADDR | `127.0.0.1:6379` | address of Redis PubSub instance, turn `redis_pub_sub` cache on for distributed cache |
| cache.max.items | CACHE_MAX_ITEMS | `1000` | max number of cached items, `0` - unlimited |
| cache.max.value | CACHE_MAX_VALUE | `65536` | max size of the cached value, `0` - unlimited |
| cache.max.size | CACHE_MAX_SIZE | `50000000` | max size of all cached values, `0` - unlimited |
| avatar.type | AVATAR_TYPE | `fs` | type of avatar storage, `fs`, `bolt`, or `uri` |
| avatar.fs.path | AVATAR_FS_PATH | `./var/avatars` | avatars location for `fs` store |
| avatar.bolt.file | AVATAR_BOLT_FILE | `./var/avatars.db` | avatars `bolt` file location |
| avatar.uri | AVATAR_URI | `./var/avatars` | avatars store URI |
| avatar.rsz-lmt | AVATAR_RESIZE | `0` (disabled) | max image size for resizing avatars on save |
| image.type | IMAGE_TYPE | `fs` | type of image storage, `fs`, `bolt` or `rpc` |
| image.fs.path | IMAGE_FS_PATH | `./var/pictures` | permanent location of images |
| image.fs.staging | IMAGE_FS_STAGING | `./var/pictures.staging` | staging location of images |
| image.fs.partitions | IMAGE_FS_PARTITIONS | `100` | number of image partitions |
| image.bolt.file | IMAGE_BOLT_FILE | `/var/pictures.db` | images bolt file location |
| image.rpc.api | IMAGE_RPC_API | | rpc extension api url |
| image.rpc.timeout | IMAGE_RPC_TIMEOUT | | http timeout (default: 5s) |
| image.rpc.auth_user | IMAGE_RPC_AUTH_USER | | basic auth user name |
| image.rpc.auth_passwd | IMAGE_RPC_AUTH_PASSWD | | basic auth user password |
| image.max-size | IMAGE_MAX_SIZE | `5000000` | max size of image file |
| image.resize-width | IMAGE_RESIZE_WIDTH | `2400` | width of a resized image |
| image.resize-height | IMAGE_RESIZE_HEIGHT | `900` | height of a resized image |
| auth.ttl.jwt | AUTH_TTL_JWT | `5m` | JWT TTL |
| auth.ttl.cookie | AUTH_TTL_COOKIE | `200h` | cookie TTL |
| auth.send-jwt-header | AUTH_SEND_JWT_HEADER | `false` | send JWT as a header instead of a cookie |
| auth.same-site | AUTH_SAME_SITE | `default` | set same site policy for cookies (`default`, `none`, `lax` or `strict`) |
| auth.apple.cid | AUTH_APPLE_CID | | Apple client ID |
| auth.apple.tid | AUTH_APPLE_TID | | Apple service ID |
| auth.apple.kid | AUTH_APPLE_KID | | Private key ID |
| auth.apple.private-key-filepath | AUTH_APPLE_PRIVATE_KEY_FILEPATH | `/srv/var/apple.p8` | Private key file location |
| auth.google.cid | AUTH_GOOGLE_CID | | Google OAuth client ID |
| auth.google.csec | AUTH_GOOGLE_CSEC | | Google OAuth client secret |
| auth.facebook.cid | AUTH_FACEBOOK_CID | | Facebook OAuth client ID |
| auth.facebook.csec | AUTH_FACEBOOK_CSEC | | Facebook OAuth client secret |
| auth.microsoft.cid | AUTH_MICROSOFT_CID | | Microsoft OAuth client ID |
| auth.microsoft.csec | AUTH_MICROSOFT_CSEC | | Microsoft OAuth client secret |
| auth.github.cid | AUTH_GITHUB_CID | | GitHub OAuth client ID |
| auth.github.csec | AUTH_GITHUB_CSEC | | GitHub OAuth client secret |
| auth.twitter.cid | AUTH_TWITTER_CID | | Twitter Consumer API Key |
| auth.twitter.csec | AUTH_TWITTER_CSEC | | Twitter Consumer API Secret key |
| auth.patreon.cid | AUTH_PATREON_CID | | Patreon OAuth Client ID |
| auth.patreon.csec | AUTH_PATREON_CSEC | | Patreon OAuth Client Secret |
| auth.telegram | AUTH_TELEGRAM | `false` | Enable Telegram auth (telegram.token must be present) |
| auth.yandex.cid | AUTH_YANDEX_CID | | Yandex OAuth client ID |
| auth.yandex.csec | AUTH_YANDEX_CSEC | | Yandex OAuth client secret |
| auth.dev | AUTH_DEV | `false` | local OAuth2 server, development mode only |
| auth.anon | AUTH_ANON | `false` | enable anonymous login |
| auth.email.enable | AUTH_EMAIL_ENABLE | `false` | enable auth via email |
| auth.email.from | AUTH_EMAIL_FROM | | email from (e.g. `john.doe@example.com` or `"John Doe"<john.doe@example.com>`) |
| auth.email.subj | AUTH_EMAIL_SUBJ | `remark42 confirmation` | email subject |
| auth.email.content-type | AUTH_EMAIL_CONTENT_TYPE | `text/html` | email content type |
| notify.users | NOTIFY_USERS | none | type of user notifications (`telegram`, `email`), _multi_ |
| notify.admins | NOTIFY_ADMINS | none | type of admin notifications (`telegram`, `slack`, `webhook` and/or `email`), _multi_ |
| notify.queue | NOTIFY_QUEUE | `100` | size of notification queue |
| notify.telegram.chan | NOTIFY_TELEGRAM_CHAN | | the ID of telegram channel for admin notifications |
| notify.slack.token | NOTIFY_SLACK_TOKEN | | Slack token |
| notify.slack.chan | NOTIFY_SLACK_CHAN | `general` | Slack channel for admin notifications |
| notify.webhook.url | NOTIFY_WEBHOOK_URL | | Webhook notification URL for admin notifications |
| notify.webhook.template | NOTIFY_WEBHOOK_TEMPLATE | `{"text": {{.Text | escapeJSONString}}}` | Webhook payload template |
| notify.webhook.headers | NOTIFY_WEBHOOK_HEADERS | | HTTP header in format Header1:Value1,Header2:Value2,... |
| notify.webhook.timeout | NOTIFY_WEBHOOK_TIMEOUT | `5s` | Webhook connection timeout |
| notify.email.from_address | NOTIFY_EMAIL_FROM | | from email address (e.g. `john.doe@example.com` or `"John Doe"<john.doe@example.com>`) |
| notify.email.verification_subj | NOTIFY_EMAIL_VERIFICATION_SUBJ | `Email verification` | verification message subject |
| telegram.token | TELEGRAM_TOKEN | | Telegram token (used for auth and Telegram notifications) |
| telegram.timeout | TELEGRAM_TIMEOUT | `5s` | Telegram connection timeout |
| smtp.host | SMTP_HOST | | SMTP host |
| smtp.port | SMTP_PORT | | SMTP port |
| smtp.username | SMTP_USERNAME | | SMTP user name |
| smtp.password | SMTP_PASSWORD | | SMTP password |
| smtp.login_auth | SMTP_LOGIN_AUTH | `false | enable LOGIN auth instead of PLAIN |
| smtp.tls | SMTP_TLS | `false` | enable TLS for SMTP |
| smtp.starttls | SMTP_STARTTLS | `false` | enable StartTLS for SMTP |
| smtp.insecure_skip_verify | SMTP_INSECURE_SKIP_VERIFY | `false` | skip certificate verification for SMTP |
| smtp.timeout | SMTP_TIMEOUT | `10s` | SMTP TCP connection timeout |
| ssl.type | SSL_TYPE | none | `none`-HTTP, `static`-HTTPS, `auto`-HTTPS + le |
| ssl.port | SSL_PORT | `8443` | port for HTTPS server |
| ssl.cert | SSL_CERT | | path to the cert.pem file |
| ssl.key | SSL_KEY | | path to the key.pem file |
| ssl.acme-location | SSL_ACME_LOCATION | `./var/acme` | dir where obtained le-certs will be stored |
| ssl.acme-email | SSL_ACME_EMAIL | | admin email for receiving notifications from LE |
| max-comment | MAX_COMMENT_SIZE | `2048` | comment's size limit |
| min-comment | MIN_COMMENT_SIZE | `0` | comment's minimal size limit, `0` - unlimited |
| max-votes | MAX_VOTES | `-1` | votes limit per comment, `-1` - unlimited |
| votes-ip | VOTES_IP | `false` | restrict votes from the same IP |
| anon-vote | ANON_VOTE | `false` | allow voting for anonymous users, require VOTES_IP to be enabled as well |
| votes-ip-time | VOTES_IP_TIME | `5m` | same IP vote restriction time, `0s` - unlimited |
| low-score | LOW_SCORE | `-5` | low score threshold |
| critical-score | CRITICAL_SCORE | `-10` | critical score threshold |
| positive-score | POSITIVE_SCORE | `false` | restricts comment's score to be only positive |
| restricted-words | RESTRICTED_WORDS | | words banned in comments (can use `*`), _multi_ |
| restricted-names | RESTRICTED_NAMES | | names prohibited to use by the user, _multi_ |
| edit-time | EDIT_TIME | `5m` | edit window |
| admin-edit | ADMIN_EDIT | `false` | unlimited edit for admins |
| read-age | READONLY_AGE | | read-only age of comments, days |
| image-proxy.http2https | IMAGE_PROXY_HTTP2HTTPS | `false` | enable HTTP->HTTPS proxy for images |
| image-proxy.cache-external | IMAGE_PROXY_CACHE_EXTERNAL | `false` | enable caching external images to current image storage |
| emoji | EMOJI | `false` | enable emoji support |
| simple-view | SIMPLE_VIEW | `false` | minimized UI with basic info only |
| proxy-cors | PROXY_CORS | `false` | disable internal CORS and delegate it to proxy |
| allowed-hosts | ALLOWED_HOSTS | enable all | limit hosts/sources allowed to embed comments |
| address | REMARK_ADDRESS | all interfaces | web server listening address |
| port | REMARK_PORT | `8080` | web server port |
| web-root | REMARK_WEB_ROOT | `./web` | web server root directory |
| update-limit | UPDATE_LIMIT | `0.5` | updates/sec limit |
| subscribers-only | SUBSCRIBERS_ONLY | `false` | enable commenting only for Patreon subscribers |
| disable-signature | DISABLE_SIGNATURE | `false` | disable server signature in headers |
| disable-fancy-text-formatting | DISABLE_FANCY_HTML_FORMATTING | `false` | disable fancy comments text formatting (replacement of quotes, dashes, fractions, etc) |
| admin-passwd | ADMIN_PASSWD | none (disabled) | password for `admin` basic auth |
| dbg | DEBUG | `false` | debug mode |
- command-line parameters are long-form `--<key>=value`, i.e., `--site=https://demo.remark42.com`
- _multi_ parameters separated by `,` in the environment or repeated with command-line keys, like `--site=s1 --site=s2 ...`
+47 -28
View File
@@ -432,11 +432,11 @@ brace-expansion@^2.0.1:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7"
@@ -705,9 +705,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
ejs@^3.1.9:
version "3.1.9"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==
version "3.1.10"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
dependencies:
jake "^10.8.5"
@@ -897,10 +897,10 @@ filelist@^1.0.4:
dependencies:
minimatch "^5.0.1"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
@@ -2046,17 +2046,17 @@ pug-attrs@^3.0.0:
js-stringify "^1.0.2"
pug-runtime "^3.0.0"
pug-code-gen@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce"
integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==
pug-code-gen@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.3.tgz#58133178cb423fe1716aece1c1da392a75251520"
integrity sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==
dependencies:
constantinople "^4.0.1"
doctypes "^1.1.0"
js-stringify "^1.0.2"
pug-attrs "^3.0.0"
pug-error "^2.0.0"
pug-runtime "^3.0.0"
pug-error "^2.1.0"
pug-runtime "^3.0.1"
void-elements "^3.1.0"
with "^7.0.0"
@@ -2065,6 +2065,11 @@ pug-error@^2.0.0:
resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5"
integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==
pug-error@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.1.0.tgz#17ea37b587b6443d4b8f148374ec27b54b406e55"
integrity sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==
pug-filters@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e"
@@ -2127,11 +2132,11 @@ pug-walk@^2.0.0:
integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==
pug@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535"
integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==
version "3.0.3"
resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.3.tgz#e18324a314cd022883b1e0372b8af3a1a99f7597"
integrity sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==
dependencies:
pug-code-gen "^3.0.2"
pug-code-gen "^3.0.3"
pug-filters "^4.0.0"
pug-lexer "^5.0.1"
pug-linker "^4.0.0"
@@ -2404,8 +2409,16 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -2459,8 +2472,14 @@ string.prototype.trimstart@^1.0.7:
define-properties "^1.2.0"
es-abstract "^1.22.1"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
name strip-ansi-cjs
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -2757,9 +2776,9 @@ wrappy@1:
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^8.13.0:
version "8.16.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==
version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
yallist@^4.0.0:
version "4.0.0"