diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d319f2f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +golang_url_shortener: + image: maxibanki/golang_url_shortener + ports: + - 8080:8080 + volumes: + - ./data:/data + environment: + - GUS_BASE_URL=https://s.b0n.pl + - GUS_GOOGLE_CLIENTID= + - GUS_GOOGLE_CLIENTSECRET= + - GUS_GITHUB_CLIENTID= + - GUS_GITHUB_CLIENTSECRET= + - GUS_MICROSOFT_CLIENTID= + - GUS_MICROSOFT_CLIENTSECRET= + restart: always diff --git a/handlers/public.go b/handlers/public.go index e700812..78e7d8c 100644 --- a/handlers/public.go +++ b/handlers/public.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" "net/http" + "time" "github.com/gin-gonic/gin" "github.com/maxibanki/golang-url-shortener/handlers/auth" @@ -12,8 +13,9 @@ import ( // urlUtil is used to help in- and outgoing requests for json // un- and marshalling type urlUtil struct { - URL string `binding:"required"` - ID string + URL string `binding:"required"` + ID string + Expiration time.Time } // handleLookup is the http handler for getting the infos @@ -50,11 +52,14 @@ func (h *Handler) handleAccess(c *gin.Context) { } entry, err := h.store.GetEntryByID(id) if err == store.ErrIDIsEmpty || err == store.ErrNoEntryFound { - return // return normal 404 error if such an error occurs + return } else if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } + if time.Now().After(entry.Public.Expiration) && !entry.Public.Expiration.IsZero() { + return + } if err := h.store.IncreaseVisitCounter(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -72,7 +77,8 @@ func (h *Handler) handleCreate(c *gin.Context) { user := c.MustGet("user").(*auth.JWTClaims) id, err := h.store.CreateEntry(store.Entry{ Public: store.EntryPublicData{ - URL: data.URL, + URL: data.URL, + Expiration: data.Expiration, }, RemoteAddr: c.ClientIP(), OAuthProvider: user.OAuthProvider, diff --git a/static/package.json b/static/package.json index 7980fc0..508b472 100644 --- a/static/package.json +++ b/static/package.json @@ -10,9 +10,10 @@ }, "dependencies": { "prismjs": "^1.8.4", - "react": "^16.0.0", - "react-clipboard.js": "^1.1.2", - "react-dom": "^16.0.0", + "react": "^16.1.1", + "react-clipboard.js": "^1.1.3", + "react-datepicker": "^0.61.0", + "react-dom": "^16.1.1", "react-prism": "^4.3.1", "react-qr-svg": "^2.1.0", "react-router": "^4.2.0", diff --git a/static/src/Card/Card.js b/static/src/Card/Card.js index d2d3572..ab6f1ec 100644 --- a/static/src/Card/Card.js +++ b/static/src/Card/Card.js @@ -4,9 +4,24 @@ import { QRCode } from 'react-qr-svg'; import Clipboard from 'react-clipboard.js'; export default class CardComponent extends Component { + state = { + expireDate: null + } + componentWillMount() { + if (this.props.expireDate) { + this.setState({ expireDate: this.props.expireDate.fromNow(true) }) + setInterval(() => { + this.setState({ expireDate: this.props.expireDate.fromNow(true) }) + }, 500) + } + } render() { + const { expireDate } = this.state return ( + {expireDate && + Expires in {expireDate} + } {this.props.header} @@ -20,14 +35,14 @@ export default class CardComponent extends Component { {!this.props.showInfoURL ?
}> - {this.props.description} - - + {this.props.description} + + - +
- + Copy to Clipboard
diff --git a/static/src/Home/Home.css b/static/src/Home/Home.css new file mode 100644 index 0000000..a33a3b6 --- /dev/null +++ b/static/src/Home/Home.css @@ -0,0 +1,6 @@ +.react-datepicker__input-container, .react-datepicker-wrapper{ + display: block; +} +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list { + padding: 0 0; +} \ No newline at end of file diff --git a/static/src/Home/Home.js b/static/src/Home/Home.js index d3c18a0..d2c0242 100644 --- a/static/src/Home/Home.js +++ b/static/src/Home/Home.js @@ -1,10 +1,15 @@ import React, { Component } from 'react' import { Input, Segment, Form, Header, Card, Button, Select, Icon } from 'semantic-ui-react' +import DatePicker from 'react-datepicker'; +import moment from 'moment'; +import 'react-datepicker/dist/react-datepicker.css'; import CustomCard from '../Card/Card' +import './Home.css' export default class HomeComponent extends Component { handleURLChange = (e, { value }) => this.url = value + handleCustomExpirationChange = expire => this.setState({ expiration: expire }) handleCustomIDChange = (e, { value }) => { this.customID = value fetch("/api/v1/protected/lookup", { @@ -31,7 +36,8 @@ export default class HomeComponent extends Component { { text: 'Expiration', value: 'expire' } ], setOptions: [], - showCustomIDError: false + showCustomIDError: false, + expiration: moment() } componentDidMount() { this.urlInput.focus() @@ -42,7 +48,8 @@ export default class HomeComponent extends Component { method: 'POST', body: JSON.stringify({ URL: this.url, - ID: this.customID + ID: this.customID, + Expiration: this.state.setOptions.indexOf("expire") > -1 ? this.state.expiration.toISOString() : undefined }), headers: { 'Authorization': window.localStorage.getItem('token'), @@ -52,14 +59,15 @@ export default class HomeComponent extends Component { .then(r => this.setState({ links: [...this.state.links, [ r.URL, - this.url + this.url, + this.state.setOptions.indexOf("expire") > -1 ? this.state.expiration : undefined ]] })) } } render() { - const { links, options, setOptions, showCustomIDError } = this.state + const { links, options, setOptions, showCustomIDError, expiration } = this.state return (
@@ -73,15 +81,20 @@ export default class HomeComponent extends Component { - {setOptions.indexOf("custom") > -1 && - - } + {setOptions.indexOf("custom") > -1 && + + } + {setOptions.indexOf("expire") > -1 && + } minDate={moment()} /> + } - - {links.map((link, i) => )} + {links.map((link, i) => )}
) diff --git a/static/src/Lookup/Lookup.js b/static/src/Lookup/Lookup.js index 6366e7f..7e16950 100644 --- a/static/src/Lookup/Lookup.js +++ b/static/src/Lookup/Lookup.js @@ -1,5 +1,6 @@ import React, { Component } from 'react' import { Segment, Header, Form, Input, Card } from 'semantic-ui-react' +import moment from 'moment'; import CustomCard from '../Card/Card' @@ -26,7 +27,8 @@ export default class LookupComponent extends Component { this.url, this.VisitCount, res.CratedOn, - res.LastVisit + res.LastVisit, + moment(res.Expiration) ]] })) } @@ -43,7 +45,7 @@ export default class LookupComponent extends Component { - {links.map((link, i) => )} + {links.map((link, i) => )}
) diff --git a/store/store.go b/store/store.go index 5806118..b55c331 100644 --- a/store/store.go +++ b/store/store.go @@ -31,9 +31,9 @@ type Entry struct { // EntryPublicData is the public part of an entry type EntryPublicData struct { - CreatedOn, LastVisit time.Time - VisitCount int - URL string + CreatedOn, LastVisit, Expiration time.Time + VisitCount int + URL string } // ErrNoEntryFound is returned when no entry to a id is found