diff --git a/README.md b/README.md index d91f74be..d385bc17 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,11 @@ Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engi | Command line | Environment | Default | Multi | Description | | ----------------- | -------------------- | ---------------------- | ----- | ---------------------------------------------- | -| --url | REMARK_URL | `https://remark42.com` | no | url to remark server | +| --url | REMARK_URL | | no | url to remark42 server | | --bolt | BOLTDB_PATH | `./var` | no | path to data directory | | --site | SITE | `remark` | yes | site name(s) | | --admin | ADMIN | | yes | admin names (list of user ids) | +| --admin-email | ADMIN_EMAIL | `admin@${REMARK_URL}` | no | admin email | | --backup | BACKUP_PATH | `./var/backup` | no | backups location | | --max-back | MAX_BACKUP_FILES | `10` | no | max backup files to keep | | --max-cache-items | MAX_CACHE_ITEMS | `1000` | no | max number of cached items, `0` - unlimited | @@ -58,9 +59,35 @@ Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engi | --dbg | DEBUG | `false` | no | debug mode | | --dev-passwd | DEV_PASSWD | | no | password for `dev` user | +##### Required parameters -**user has to provide secret key, can be any long and hard-to-guess string.** +Most of the parameters have sane defaults and don't require customization. There are only a few parameters user has to define: +1. `SECRET` - secret key, can be any long and hard-to-guess string. +1. `REMARK_URL` - url pointing to your remark42 server, i.e. `https://demo.reamark42.com` +1. At least one pair of `REMARK__CID` and `REMARK__CSEC` defining oauth2 provider(s) + +The minimal `docker-compose.yml` has to include all required parameters: + +```yaml +version: '2' + +services: + remark: + image: umputun/remark:master + restart: always + + environment: + - REMARK_URL=https://demo.remark42.com # url pointing to your remark42 server + - USER=1001 # UID on the host machine + - SECRET=abcd-123456-xyz-$%^& # secret key + - REMARK_GITHUB_CID=12345667890 # oauth2 client ID + - REMARK_GITHUB_CSEC=abcdefg12345678 # oauth2 client secret + volumes: + - ./var:/srv/var # persistent volume to store all remark42 data +``` + + _all multi parameters separated by `,` in environment or repeated with command line key, like `--site=s1 --site=s2 ...`_ #### Register oauth2 providers @@ -426,8 +453,8 @@ _all admin calls require auth and admin privilege_ * There is no cross-site login, i.e., user's behavior can't be analyzed across independent sites running remark42. * There are no third-party analytic services involved. * User can request all information remark42 knows about and export to gz file. -* Supported complete cleanup of all information related to user activity. -* Cookie lifespan can be restricted to session-only +* Supported complete cleanup of all information related to user activity on demand. +* Cookie lifespan can be restricted to session-only. * All potentially sensitive data stored by remark42 hashed and encrypted. @@ -435,11 +462,11 @@ _all admin calls require auth and admin privilege_ * Data stored in [boltdb](https://github.com/coreos/bbolt) (embedded key/value database) files under `BOLTDB_PATH` * Each site stored in a separate boltbd file. -* In order to migrate/move remark42 to another host boltbd files should be transferred. +* In order to migrate/move remark42 to another host boltbd files as well as avatars directory `AVATAR_STORE` should be transferred. * Automatic backup process runs every 24h and exports all content in json-like format to `backup-remark-YYYYMMDD.gz`. * Authentication implemented with [jwt](https://github.com/dgrijalva/jwt-go) stored in a cookie. It uses HttpOnly, secure cookies. -* All heavy REST calls cached internally, default expiration 4h -* User's activity throttled globally (up to 1000 simultaneous requests) and limited locally (per user, up to 10 req/sec) +* All heavy REST calls cached internally in LRU cache limited by `MAX_CACHE_ITEMS` and `MAX_CACHE_SIZE`. +* User's activity throttled globally (up to 1000 simultaneous requests) and limited locally (per user, usually up to 10 req/sec) * Request timeout set to 60sec * Development mode (`--dev-password` set) allows to test remark42 without social login and with admin privileges. Adds basic-auth for username: `dev`, password: `${DEV_PASSWD}`. **should not be used in production deployment** * User can vote for the comment multiple times but only to change his/her vote. Double-voting not allowed. diff --git a/app/main.go b/app/main.go index 3aada293..b21b91cc 100644 --- a/app/main.go +++ b/app/main.go @@ -30,7 +30,7 @@ import ( type Opts struct { BoltPath string `long:"bolt" env:"BOLTDB_PATH" default:"./var" description:"parent dir for bolt files"` Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","` - RemarkURL string `long:"url" env:"REMARK_URL" default:"https://remark42.com" description:"url to remark"` + RemarkURL string `long:"url" env:"REMARK_URL" required:"true" description:"url to remark"` Admins []string `long:"admin" env:"ADMIN" description:"admin(s) names" env-delim:","` AdminEmail string `long:"admin-email" env:"ADMIN_EMAIL" default:"" description:"admin email"` DevPasswd string `long:"dev-passwd" env:"DEV_PASSWD" default:"" description:"development mode password"` @@ -111,8 +111,16 @@ func New(opts Opts) (*Application, error) { return nil, err } + if !strings.HasPrefix(opts.RemarkURL, "http://") && !strings.HasPrefix(opts.RemarkURL, "https://") { + return nil, errors.Errorf("invalid remark42 url %s", opts.RemarkURL) + } + + boltStore, err := makeBoltStore(opts.Sites, opts.BoltPath) + if err != nil { + return nil, err + } dataService := service.DataStore{ - Interface: makeBoltStore(opts.Sites, opts.BoltPath), + Interface: boltStore, EditDuration: 5 * time.Minute, Secret: opts.SecretKey, MaxCommentSize: opts.MaxCommentSize, @@ -213,16 +221,16 @@ func (a *Application) activateBackup(ctx context.Context) { } // makeBoltStore creates store for all sites -func makeBoltStore(siteNames []string, path string) engine.Interface { +func makeBoltStore(siteNames []string, path string) (engine.Interface, error) { sites := []engine.BoltSite{} for _, site := range siteNames { sites = append(sites, engine.BoltSite{SiteID: site, FileName: fmt.Sprintf("%s/%s.db", path, site)}) } result, err := engine.NewBoltDB(bolt.Options{Timeout: 30 * time.Second}, sites...) if err != nil { - log.Fatalf("[ERROR] can't initialize data store, %+v", err) + return nil, errors.Wrap(err, "can't initialize data store") } - return result + return result, nil } // mkdir -p for all dirs diff --git a/app/main_test.go b/app/main_test.go index 2999260b..31b41a71 100644 --- a/app/main_test.go +++ b/app/main_test.go @@ -44,6 +44,24 @@ func TestApplication(t *testing.T) { app.Wait() } +func TestApplicationFailed(t *testing.T) { + opts := Opts{} + p := flags.NewParser(&opts, flags.Default) + + // RO bolt location + p.ParseArgs([]string{"--secret=123456", "--url=https://demo.remark42.com", "--bolt=/dev/null"}) + _, err := New(opts) + assert.EqualError(t, err, "can't initialize data store: failed to make boltdb for /dev/null/remark.db: "+ + "open /dev/null/remark.db: not a directory") + t.Log(err) + + //p.ParseArgs([]string{"--secret=123456", "--url=https://demo.remark42.com", "--bolt=/tmp", "--backup=/not-writable"}) + //_, err = New(opts) + //assert.EqualError(t, err, "can't initialize data store: failed to make boltdb for /dev/null/remark.db: "+ + // "open /dev/null/remark.db: not a directory") + //t.Log(err) +} + func TestApplicationShutdown(t *testing.T) { app, ctx := prepApp(t, 18090, 500*time.Millisecond) st := time.Now() @@ -53,7 +71,9 @@ func TestApplicationShutdown(t *testing.T) { } func TestApplicationMainSignal(t *testing.T) { - os.Args = []string{"test", "--secret=123456", "--bolt=/tmp/xyz", "--backup=/tmp", "--avatars=/tmp", "--port=18100"} + os.Args = []string{"test", "--secret=123456", "--bolt=/tmp/xyz", "--backup=/tmp", "--avatars=/tmp", + "--port=18100", "--url=https://demo.remark42.com"} + go func() { time.Sleep(100 * time.Millisecond) syscall.Kill(syscall.Getpid(), syscall.SIGTERM)