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"))
+ }
}