diff --git a/README.md b/README.md index 6783426..c34ea2e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ For getting a development version of Fathom up & running, go through the followi 1. Get code: `git clone https://github.com/usefathom/fathom.git $GOPATH/src/github.com/usefathom/fathom` 1. Compile into binary & prepare assets: `make build` 1. (Optional) Set your [custom configuration values](https://github.com/usefathom/fathom/wiki/Configuration-file). -1. Register your user account: `fathom register --email= --password=` +1. Register your user account: `fathom user add --email= --password=` 1. Start the webserver: `fathom server` and then visit **http://localhost:8080** to access your analytics dashboard. To install and run Fathom in production, [have a look at the installation instructions](https://github.com/usefathom/fathom/wiki/Installing-&-running-Fathom). diff --git a/assets/src/js/components/CountWidget.js b/assets/src/js/components/CountWidget.js index a3b9692..f9751f2 100644 --- a/assets/src/js/components/CountWidget.js +++ b/assets/src/js/components/CountWidget.js @@ -3,6 +3,7 @@ import { h, Component } from 'preact'; import * as numbers from '../lib/numbers.js'; import { bind } from 'decko'; +import classNames from 'classnames'; const duration = 600; const easeOutQuint = function (t) { return 1+(--t)*t*t*t*t }; @@ -17,8 +18,8 @@ class CountWidget extends Component { } // TODO: Move to component of its own - @bind - countUp(fromValue, toValue) { + @bind + countUp(fromValue, toValue) { const format = this.formatValue.bind(this); const startValue = isFinite(fromValue) ? fromValue : 0; const numberEl = this.numberEl; @@ -34,7 +35,7 @@ class CountWidget extends Component { window.requestAnimationFrame(tick); } } - + window.requestAnimationFrame(tick); } @@ -46,7 +47,7 @@ class CountWidget extends Component { switch(this.props.format) { case "percentage": formattedValue = numbers.formatPercentage(value) - break; + break; default: case "number": @@ -64,7 +65,7 @@ class CountWidget extends Component { render(props, state) { return ( -
+
{props.title}
{ this.numberEl = e; }}>{this.formatValue(props.value)}
diff --git a/assets/src/js/components/DatePicker.js b/assets/src/js/components/DatePicker.js index 8a87e15..caa6693 100644 --- a/assets/src/js/components/DatePicker.js +++ b/assets/src/js/components/DatePicker.js @@ -3,6 +3,7 @@ import { h, Component } from 'preact'; import { bind } from 'decko'; import Pikadayer from './Pikadayer.js'; +import classNames from 'classnames'; const defaultPeriod = 'last-7-days'; const padZero = function(n){return n<10? '0'+n:''+n;} @@ -24,7 +25,7 @@ function getNow() { // today, yesterday, this week, last 7 days, last 30 days const availablePeriods = { - 'today': { + 'today': { label: 'Today', start: function() { const now = getNow(); @@ -35,7 +36,7 @@ const availablePeriods = { return new Date(now.getFullYear(), now.getMonth(), now.getDate()); }, }, - 'last-7-days': { + 'last-7-days': { label: 'Last 7 days', start: function() { const now = getNow(); @@ -46,7 +47,7 @@ const availablePeriods = { return new Date(now.getFullYear(), now.getMonth(), now.getDate()); }, }, - 'last-30-days': { + 'last-30-days': { label: 'Last 30 days', start: function() { const now = getNow(); @@ -57,7 +58,7 @@ const availablePeriods = { return new Date(now.getFullYear(), now.getMonth(), now.getDate()); }, }, - 'this-year': { + 'this-year': { label: 'This year', start: function() { const now = getNow(); @@ -117,7 +118,7 @@ class DatePicker extends Component { period: period || '', startDate: startDate, endDate: endDate, - before: before, + before: before, after: after, }); @@ -127,7 +128,7 @@ class DatePicker extends Component { this.props.onChange(this.state); this.timeout = null; - window.localStorage.setItem('period', this.state.period) + window.localStorage.setItem('period', this.state.period) window.history.replaceState(this.state, null, `#!${this.state.period}`) }, 2) } @@ -149,12 +150,12 @@ class DatePicker extends Component { return date.getFullYear() + '-' + padZero(date.getMonth() + 1) + '-' + padZero(date.getDate()); } - @bind + @bind setStartDate(date) { this.setDateRange(date, this.state.endDate, '') } - @bind + @bind setEndDate(date) { this.setDateRange(this.state.startDate, date, '') } @@ -189,8 +190,11 @@ class DatePicker extends Component { render(props, state) { const links = Object.keys(availablePeriods).map((id) => { let p = availablePeriods[id]; - let className = ( id == state.period ) ? 'active' : ''; - return
  • {p.label}
  • + return ( +
  • + {p.label} +
  • + ); }); return ( @@ -198,7 +202,7 @@ class DatePicker extends Component { {links}
  • - to + to
  • diff --git a/assets/src/js/components/Table.js b/assets/src/js/components/Table.js index 0abb13a..5b7ec9c 100644 --- a/assets/src/js/components/Table.js +++ b/assets/src/js/components/Table.js @@ -4,6 +4,7 @@ import { h, Component } from 'preact'; import * as numbers from '../lib/numbers.js'; import Client from '../lib/client.js'; import { bind } from 'decko'; +import classNames from 'classnames'; const dayInSeconds = 60 * 60 * 24; @@ -31,7 +32,7 @@ class Table extends Component { @bind fetchRecords(before, after) { this.setState({ loading: true }); - + Client.request(`${this.props.endpoint}?before=${before}&after=${after}&limit=${this.state.limit}`) .then((d) => { // request finished; check if timestamp range is still the one user wants to see @@ -39,7 +40,7 @@ class Table extends Component { return; } - this.setState({ + this.setState({ loading: false, records: d, }); @@ -48,7 +49,7 @@ class Table extends Component { // fetch totals too Client.request(`${this.props.endpoint}/pageviews?before=${before}&after=${after}`) .then((d) => { - this.setState({ + this.setState({ total: d }); }); @@ -59,9 +60,9 @@ class Table extends Component { const tableRows = state.records !== null && state.records.length > 0 ? state.records.map((p, i) => { let href = (p.Hostname + p.Pathname) || p.URL; - let classes = "table-row"; + let widthClass = ""; if(state.total > 0) { - classes += " w" + Math.min(98, Math.round(p.Pageviews / state.total * 100 * 2.5)); + widthClass = "w" + Math.min(98, Math.round(p.Pageviews / state.total * 100 * 2.5)); } let label = p.Pathname @@ -74,19 +75,19 @@ class Table extends Component { } return( -
    +
    {numbers.formatPretty(p.Pageviews)}
    -
    {numbers.formatPretty(p.Visitors)||"-"}
    +
    {numbers.formatPretty(p.Visitors)||"-"}
    )}) :
    Nothing here, yet.
    ; return ( -
    +
    {props.headers.map((header, i) => { - return (
    {header}
    ) - })} + return
    {header}
    + })}
    {tableRows} diff --git a/cmd/fathom/main.go b/cmd/fathom/main.go index 732533d..f756053 100644 --- a/cmd/fathom/main.go +++ b/cmd/fathom/main.go @@ -38,7 +38,7 @@ func main() { app.After = after app.Commands = []cli.Command{ serverCmd, - registerCmd, + userCmd, statsCmd, } diff --git a/cmd/fathom/register.go b/cmd/fathom/register.go deleted file mode 100644 index 31e3c01..0000000 --- a/cmd/fathom/register.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "errors" - "fmt" - - log "github.com/sirupsen/logrus" - "github.com/urfave/cli" - "github.com/usefathom/fathom/pkg/models" -) - -var registerCmd = cli.Command{ - Name: "register", - Usage: "register a new admin user", - Action: register, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "email, e", - Usage: "user email", - }, - cli.StringFlag{ - Name: "password, p", - Usage: "user password", - }, - cli.BoolFlag{ - Name: "skip-bcrypt", - Usage: "store password string as is, skipping bcrypt", - }, - }, -} - -func register(c *cli.Context) error { - email := c.String("email") - if email == "" { - return errors.New("Invalid arguments: missing email") - } - - password := c.String("password") - if password == "" { - return errors.New("Invalid arguments: missing password") - } - - user := models.NewUser(email, password) - - // set password manually if --skip-bcrypt was given - // this is used to supply an already encrypted password string - if c.Bool("skip-bcrypt") { - user.Password = password - } - - if err := app.database.SaveUser(&user); err != nil { - return fmt.Errorf("Error creating user: %s", err) - } - - log.Infof("Created user %s", user.Email) - return nil -} diff --git a/cmd/fathom/user.go b/cmd/fathom/user.go new file mode 100644 index 0000000..e95d123 --- /dev/null +++ b/cmd/fathom/user.go @@ -0,0 +1,99 @@ +package main + +import ( + "errors" + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/urfave/cli" + "github.com/usefathom/fathom/pkg/datastore" + "github.com/usefathom/fathom/pkg/models" +) + +var userCmd = cli.Command{ + Name: "user", + Usage: "manage registered admin users", + Action: userAdd, + Subcommands: []cli.Command{ + cli.Command{ + Name: "add", + Aliases: []string{"register"}, + Action: userAdd, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "email, e", + Usage: "user email", + }, + cli.StringFlag{ + Name: "password, p", + Usage: "user password", + }, + cli.BoolFlag{ + Name: "skip-bcrypt", + Usage: "store password string as-is, skipping bcrypt", + }, + }, + }, + cli.Command{ + Name: "delete", + Action: userDelete, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "email, e", + Usage: "user email", + }, + }, + }, + }, +} + +func userAdd(c *cli.Context) error { + email := c.String("email") + if email == "" { + return errors.New("Invalid arguments: missing email") + } + + password := c.String("password") + if password == "" { + return errors.New("Invalid arguments: missing password") + } + + user := models.NewUser(email, password) + + // set password manually if --skip-bcrypt was given + // this is used to supply an already encrypted password string + if c.Bool("skip-bcrypt") { + user.Password = password + } + + if err := app.database.SaveUser(&user); err != nil { + return fmt.Errorf("Error creating user: %s", err) + } + + log.Infof("Created user %s", user.Email) + return nil +} + +func userDelete(c *cli.Context) error { + email := c.String("email") + if email == "" { + return errors.New("Invalid arguments: missing email") + } + + user, err := app.database.GetUserByEmail(email) + if err != nil { + if err == datastore.ErrNoResults { + return fmt.Errorf("No user with email %s", email) + } + + return err + } + + if err := app.database.DeleteUser(user); err != nil { + return err + } + + log.Infof("Deleted user %s", user.Email) + + return nil +} diff --git a/package-lock.json b/package-lock.json index 0f0f964..e930f5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1649,6 +1649,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", diff --git a/package.json b/package.json index 7f8d02f..fc6019f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "vinyl-source-stream": "^2.0.0" }, "dependencies": { + "classnames": "^2.2.6", "d3": "^5.4.0", "d3-tip": "^0.9.1", "d3-transition": "^1.1.1", diff --git a/pkg/datastore/datastore.go b/pkg/datastore/datastore.go index 82b22f8..59270f0 100644 --- a/pkg/datastore/datastore.go +++ b/pkg/datastore/datastore.go @@ -16,6 +16,7 @@ type Datastore interface { GetUser(int64) (*models.User, error) GetUserByEmail(string) (*models.User, error) SaveUser(*models.User) error + DeleteUser(*models.User) error CountUsers() (int64, error) // site stats diff --git a/pkg/datastore/sqlstore/users.go b/pkg/datastore/sqlstore/users.go index 3c5dd6f..a838613 100644 --- a/pkg/datastore/sqlstore/users.go +++ b/pkg/datastore/sqlstore/users.go @@ -51,6 +51,13 @@ func (db *sqlstore) SaveUser(u *models.User) error { return nil } +// DeleteUser deletes the user in the datastore +func (db *sqlstore) DeleteUser(user *models.User) error { + query := db.Rebind("DELETE FROM users WHERE id = ?") + _, err := db.Exec(query, user.ID) + return err +} + // CountUsers returns the number of users func (db *sqlstore) CountUsers() (int64, error) { var c int64