From 96b06a87345e5d331acb57facc280696c5d46ffc Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Sun, 26 Nov 2017 15:36:59 +0100 Subject: [PATCH] Added UseSSL config option and added deletion button to the recent URLs table --- handlers/public.go | 12 ++++++++++- static/src/Recent/Recent.js | 32 +++++++++++++++++++++++------- static/src/Visitors/Visitors.js | 35 ++++++++++++++++++--------------- store/store.go | 1 + util/config.go | 4 ++++ 5 files changed, 60 insertions(+), 24 deletions(-) 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")