diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 3103e8f6..b60fa370 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -161,6 +161,7 @@ func (h *Handler) setHandlers() error { protected.POST("/create", h.handleCreate) protected.POST("/lookup", h.handleLookup) protected.GET("/recent", h.handleRecent) + protected.GET("/admin", h.handleAdmin) protected.POST("/visitors", h.handleGetVisitors) h.engine.GET("/api/v1/info", h.handleInfo) diff --git a/internal/handlers/public.go b/internal/handlers/public.go index d76835d3..9f277dea 100644 --- a/internal/handlers/public.go +++ b/internal/handlers/public.go @@ -187,6 +187,23 @@ func (h *Handler) handleRecent(c *gin.Context) { c.JSON(http.StatusOK, entries) } +func (h *Handler) handleAdmin(c *gin.Context) { + entries, err := h.store.GetAllUserEntries() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + for k, entry := range entries { + mac := hmac.New(sha512.New, util.GetPrivateKey()) + if _, err := mac.Write([]byte(k)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + entry.DeletionURL = fmt.Sprintf("%s/d/%s/%s", h.getURLOrigin(c), k, url.QueryEscape(base64.RawURLEncoding.EncodeToString(mac.Sum(nil)))) + entries[k] = entry + } + c.JSON(http.StatusOK, entries) +} + func (h *Handler) handleDelete(c *gin.Context) { givenHmac, err := base64.RawURLEncoding.DecodeString(c.Param("hash")) if err != nil { diff --git a/internal/stores/boltdb/boltdb.go b/internal/stores/boltdb/boltdb.go index 99fa9e85..f264149f 100644 --- a/internal/stores/boltdb/boltdb.go +++ b/internal/stores/boltdb/boltdb.go @@ -176,6 +176,26 @@ func (b *BoltStore) GetUserEntries(userIdentifier string) (map[string]shared.Ent return entries, errors.Wrap(err, "could not update db") } +// GetAllUserEntries returns all user entries +func (b *BoltStore) GetAllUserEntries() (map[string]shared.Entry, error) { + entries := map[string]shared.Entry{} + err := b.db.Update(func(tx *bolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(shortedIDsToUserBucket) + if err != nil { + return errors.Wrap(err, "could not create bucket") + } + return bucket.ForEach(func(k, v []byte) error { + entry, err := b.GetEntryByID(string(k)) + if err != nil { + return errors.Wrap(err, "could not get entry") + } + entries[string(k)] = *entry + return nil + }) + }) + return entries, errors.Wrap(err, "could not update db") +} + // RegisterVisitor saves the visitor in the database func (b *BoltStore) RegisterVisitor(id, visitID string, visitor shared.Visitor) error { err := b.db.Update(func(tx *bolt.Tx) error { diff --git a/internal/stores/redis/redis.go b/internal/stores/redis/redis.go index 18d92c90..0c6c9b44 100644 --- a/internal/stores/redis/redis.go +++ b/internal/stores/redis/redis.go @@ -295,6 +295,36 @@ func (r *Store) GetUserEntries(userIdentifier string) (map[string]shared.Entry, return entries, nil } +// GetAllUserEntries returns all entries for all users, in the +// form of a map of path->shared.Entry +func (r *Store) GetAllUserEntries() (map[string]shared.Entry, error) { + logrus.Debugf("Getting all entries for all users") + entries := map[string]shared.Entry{} + users := r.c.Keys(userToEntriesPrefix + "*") + for _, v := range users.Val() { + logrus.Debugf("got userEntry: %s", v) + key := v + result := r.c.SMembers(key) + if result.Err() != nil { + msg := fmt.Sprintf("Could not fetch set of entries for user '%s': %v", key, result.Err()) + logrus.Errorf(msg) + return nil, errors.Wrap(result.Err(), msg) + } + for _, v := range result.Val() { + logrus.Debugf("got entry: %s", v) + entry, err := r.GetEntryByID(string(v)) + if err != nil { + msg := fmt.Sprintf("Could not get entry '%s': %s", v, err) + logrus.Warn(msg) + } else { + entries[string(v)] = *entry + } + } + logrus.Debugf("all out of entries") + } + return entries, nil +} + // RegisterVisitor adds a shared.Visitor to the list of visits for a path. func (r *Store) RegisterVisitor(id, visitID string, visitor shared.Visitor) error { data, err := json.Marshal(visitor) diff --git a/internal/stores/shared/shared.go b/internal/stores/shared/shared.go index eca88686..8131384f 100644 --- a/internal/stores/shared/shared.go +++ b/internal/stores/shared/shared.go @@ -14,6 +14,7 @@ type Storage interface { IncreaseVisitCounter(string) error CreateEntry(Entry, string, string) error GetUserEntries(string) (map[string]Entry, error) + GetAllUserEntries() (map[string]Entry, error) RegisterVisitor(string, string, Visitor) error Close() error } diff --git a/internal/stores/store.go b/internal/stores/store.go index 64745a34..870ffd29 100644 --- a/internal/stores/store.go +++ b/internal/stores/store.go @@ -157,6 +157,15 @@ func (s *Store) GetUserEntries(oAuthProvider, oAuthID string) (map[string]shared return entries, nil } +// GetAllUserEntries returns all the shorted URL entries of an user +func (s *Store) GetAllUserEntries() (map[string]shared.Entry, error) { + entries, err := s.storage.GetAllUserEntries() + if err != nil { + return nil, errors.Wrap(err, "could not get all entries") + } + return entries, nil +} + func getUserIdentifier(oAuthProvider, oAuthID string) string { return oAuthProvider + oAuthID } diff --git a/web/src/Admin/Admin.js b/web/src/Admin/Admin.js new file mode 100644 index 00000000..410b4dcc --- /dev/null +++ b/web/src/Admin/Admin.js @@ -0,0 +1,94 @@ +import React, { Component } from 'react' +import { Container, Button, Icon } from 'semantic-ui-react' +import Moment from 'react-moment'; +import ReactTable from 'react-table' +import 'react-table/react-table.css' + +import util from '../util/util' + +export default class AllEntriesComponent extends Component { + state = { + allEntries: [], + displayURL: window.location.origin + } + + componentDidMount() { + this.getAllURLs() + fetch("/displayurl") + .then(response => response.json()) + .then(data => this.setState({displayURL: data})); + } + + getAllURLs = () => { + util.getAllURLs(allEntries => { + let parsed = []; + for (let key in allEntries) { + if ({}.hasOwnProperty.call(allEntries, key)) { + allEntries[key].ID = key; + parsed.push(allEntries[key]); + } + } + this.setState({ allEntries: parsed }) + }) + } + + onRowClick(id) { + this.props.history.push(`/visitors/${id}`) + } + + onEntryDeletion(deletionURL) { + util.deleteEntry(deletionURL, this.getAllURLs) + } + + render() { + const { allEntries } = this.state + + const columns = [{ + Header: 'Original URL', + accessor: "Public.URL" + }, { + Header: 'Created', + accessor: 'Public.CreatedOn', + Cell: props => {props.value} + }, { + Header: 'Short URL', + accessor: "ID", + Cell: props => `${this.state.displayURL}/${props.value}` + }, { + Header: 'Visitor count', + accessor: "Public.VisitCount" + + }, { + Header: 'Delete', + accessor: 'DeletionURL', + Cell: props => , + style: { textAlign: "center" } + }] + + return ( + + { + return { + onClick: (e, handleOriginal) => { + if (handleOriginal) { + handleOriginal() + } + if (!rowInfo) { + return + } + if (column.id === "DeletionURL") { + return + } + this.onRowClick(rowInfo.row.ID) + } + } + }} /> + + ) + } +} diff --git a/web/src/index.js b/web/src/index.js index 0543a4dc..e1bd35d8 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -11,6 +11,7 @@ import Home from './Home/Home' import ShareX from './ShareX/ShareX' import Lookup from './Lookup/Lookup' import Recent from './Recent/Recent' +import Admin from './Admin/Admin' import Visitors from './Visitors/Visitors' import util from './util/util' @@ -186,6 +187,7 @@ export default class BaseComponent extends Component { + diff --git a/web/src/util/util.js b/web/src/util/util.js index 0d4cc147..cd995dfd 100644 --- a/web/src/util/util.js +++ b/web/src/util/util.js @@ -61,4 +61,16 @@ export default class UtilHelper { .then(res => res.ok ? res.json() : Promise.reject(res.json())) .catch(e => this._reportError(e, "getDisplayURL")) } + static getAllURLs(cbSucc) { + fetch('/api/v1/protected/admin', { + credentials: "include", + headers: { + 'Authorization': window.localStorage.getItem('token'), + 'Content-Type': 'application/json' + } + }) + .then(res => res.ok ? res.json() : Promise.reject(res.json())) + .then(res => cbSucc ? cbSucc(res) : null) + .catch(e => this._reportError(e, "recent")) + } }