diff --git a/handlers/public.go b/handlers/public.go
index 7122d99..cc2dec6 100644
--- a/handlers/public.go
+++ b/handlers/public.go
@@ -1,6 +1,8 @@
package handlers
import (
+ "crypto/hmac"
+ "crypto/sha512"
"encoding/base64"
"fmt"
"net/http"
@@ -137,6 +139,14 @@ func (h *Handler) handleRecent(c *gin.Context) {
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)
}
@@ -157,7 +167,7 @@ func (h *Handler) handleDelete(c *gin.Context) {
func (h *Handler) getURLOrigin(c *gin.Context) string {
protocol := "http"
- if c.Request.TLS != nil {
+ if c.Request.TLS != nil || util.GetConfig().UseSSL {
protocol = "https"
}
return fmt.Sprintf("%s://%s", protocol, c.Request.Host)
diff --git a/static/src/Recent/Recent.js b/static/src/Recent/Recent.js
index b5c681a..490cf0b 100644
--- a/static/src/Recent/Recent.js
+++ b/static/src/Recent/Recent.js
@@ -1,5 +1,5 @@
import React, { Component } from 'react'
-import { Container, Table } from 'semantic-ui-react'
+import { Container, Table, Button, Icon } from 'semantic-ui-react'
import toastr from 'toastr'
import Moment from 'react-moment';
@@ -9,6 +9,10 @@ export default class RecentComponent extends Component {
}
componentDidMount() {
+ this.loadRecentURLs()
+ }
+
+ loadRecentURLs() {
fetch('/api/v1/protected/recent', {
method: 'POST',
headers: {
@@ -17,13 +21,20 @@ export default class RecentComponent extends Component {
})
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(recent => this.setState({ recent: recent }))
- .catch(e => e.done(res => toastr.error(`Could not fetch recent shortened URLs: ${res}`)))
+ .catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could load recent URLs: ${error.error}`)) : null)
}
onRowClick(id) {
this.props.history.push(`/visitors/${id}`)
}
+ onEntryDeletion(visit) {
+ fetch(visit.DeletionURL)
+ .then(res => res.ok ? res.json() : Promise.reject(res.json()))
+ .then(() => this.loadRecentURLs())
+ .catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not delete: ${error.error}`)) : null)
+ }
+
render() {
const { recent } = this.state
return (
@@ -35,14 +46,21 @@ export default class RecentComponent extends Component {
Created
Short URL
All Clicks
+ Delete
- {recent && Object.keys(recent).map(key =>
- {recent[key].Public.URL}
- {recent[key].Public.CreatedOn}
- {`${window.location.origin}/${key}`}
- {recent[key].Public.VisitCount}
+ {recent && Object.keys(recent).map(key =>
+ {recent[key].Public.URL}
+ {recent[key].Public.CreatedOn}
+ {`${window.location.origin}/${key}`}
+ {recent[key].Public.VisitCount}
+
)}
diff --git a/static/src/Visitors/Visitors.js b/static/src/Visitors/Visitors.js
index dce4fc9..6a1d63b 100644
--- a/static/src/Visitors/Visitors.js
+++ b/static/src/Visitors/Visitors.js
@@ -9,7 +9,7 @@ export default class VisitorComponent extends Component {
info: null
}
- componentDidMount() {
+ componentWillMount() {
this.setState({ id: this.props.match.params.id })
fetch("/api/v1/protected/lookup", {
method: "POST",
@@ -26,21 +26,24 @@ export default class VisitorComponent extends Component {
.catch(e => {
toastr.error(`Could not fetch lookup: ${e}`)
})
- this.loop = setInterval(() => {
- fetch('/api/v1/protected/visitors', {
- method: 'POST',
- body: JSON.stringify({
- ID: this.props.match.params.id
- }),
- headers: {
- 'Authorization': window.localStorage.getItem('token'),
- 'Content-Type': 'application/json'
- }
- })
- .then(res => res.ok ? res.json() : Promise.reject(res.json()))
- .then(visitors => this.setState({ visitors }))
- .catch(e => e.done(res => toastr.error(`Could not fetch visitors: ${res}`)))
- }, 1000)
+ this.reloadVisitors()
+ this.loop = setInterval(this.reloadVisitors, 1000)
+ }
+
+ reloadVisitors = () => {
+ fetch('/api/v1/protected/visitors', {
+ method: 'POST',
+ body: JSON.stringify({
+ ID: this.props.match.params.id
+ }),
+ headers: {
+ 'Authorization': window.localStorage.getItem('token'),
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(res => res.ok ? res.json() : Promise.reject(res.json()))
+ .then(visitors => this.setState({ visitors }))
+ .catch(e => e.done(res => toastr.error(`Could not fetch visitors: ${res}`)))
}
componentWillUnmount() {
diff --git a/store/store.go b/store/store.go
index 12ba8f0..a38f7ac 100644
--- a/store/store.go
+++ b/store/store.go
@@ -28,6 +28,7 @@ type Store struct {
type Entry struct {
OAuthProvider, OAuthID string
RemoteAddr string `json:",omitempty"`
+ DeletionURL string `json:",omitempty"`
Public EntryPublicData
}
diff --git a/util/config.go b/util/config.go
index b32a871..fd51c99 100644
--- a/util/config.go
+++ b/util/config.go
@@ -17,6 +17,7 @@ type Configuration struct {
ListenAddr string `yaml:"ListenAddr" env:"LISTEN_ADDR"`
BaseURL string `yaml:"BaseURL" env:"BASE_URL"`
DataDir string `yaml:"DataDir" env:"DATA_DIR"`
+ UseSSL bool `yaml:"EnableSSL" env:"USE_SSL"`
EnableDebugMode bool `yaml:"EnableDebugMode" env:"ENABLE_DEBUG_MODE"`
ShortedIDLength int `yaml:"ShortedIDLength" env:"SHORTED_ID_LENGTH"`
Google oAuthConf `yaml:"Google" env:"GOOGLE"`
@@ -35,6 +36,7 @@ var (
BaseURL: "http://localhost:3000",
DataDir: "data",
EnableDebugMode: false,
+ UseSSL: false,
ShortedIDLength: 4,
}
)
@@ -48,6 +50,8 @@ func ReadInConfig() error {
}
} else if !os.IsNotExist(err) {
return errors.Wrap(err, "could not read config file")
+ } else {
+ logrus.Info("No configuration file found, using defaults and environment variable overrides.")
}
if err := config.ApplyEnvironmentConfig(); err != nil {
return errors.Wrap(err, "could not apply environment configuration")