Browse Source

Added link expiration: closes #22

dependabot/npm_and_yarn/web/prismjs-1.21.0
Max Schmitt 8 years ago
parent
commit
93574983cb
  1. 15
      docker-compose.yml
  2. 14
      handlers/public.go
  3. 7
      static/package.json
  4. 25
      static/src/Card/Card.js
  5. 6
      static/src/Home/Home.css
  6. 31
      static/src/Home/Home.js
  7. 6
      static/src/Lookup/Lookup.js
  8. 6
      store/store.go

15
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

14
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,

7
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",

25
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 (<Card key={this.key}>
<Card.Content>
{expireDate && <Card.Header style={{ float: "right", fontSize: "1.1em" }}>
Expires in {expireDate}
</Card.Header>}
<Card.Header>
{this.props.header}
</Card.Header>
@ -20,14 +35,14 @@ export default class CardComponent extends Component {
<Card.Content extra>
{!this.props.showInfoURL ? <div className='ui two buttons'>
<Modal closeIcon trigger={<Button icon='qrcode' content='Show QR-Code' />}>
<Modal.Header className="ui center aligned">{this.props.description}</Modal.Header>
<Modal.Content style={{ textAlign: "center" }}>
<QRCode style={{ width: "75%" }} value={this.props.description} />
<Modal.Header className='ui center aligned'>{this.props.description}</Modal.Header>
<Modal.Content style={{ textAlign: 'center' }}>
<QRCode style={{ width: '75%' }} value={this.props.description} />
</Modal.Content>
</Modal>
<Clipboard component="button" className="ui button" data-clipboard-text={this.props.description} button-title="Copy the Shortened URL to the Clipboard">
<Clipboard component='button' className='ui button' data-clipboard-text={this.props.description} button-title='Copy the Shortened URL to the Clipboard'>
<div>
<Icon name="clipboard" />
<Icon name='clipboard' />
Copy to Clipboard
</div>
</Clipboard>

6
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;
}

31
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 (
<div>
<Segment raised>
@ -73,15 +81,20 @@ export default class HomeComponent extends Component {
</Input>
</Form.Field>
<Form.Group widths='equal'>
{setOptions.indexOf("custom") > -1 && <Form.Field error={showCustomIDError}><Input label={window.location.origin + "/"} onChange={this.handleCustomIDChange} placeholder='my-shortened-url' />
</Form.Field>
}
{setOptions.indexOf("custom") > -1 && <Form.Field error={showCustomIDError}>
<Input label={window.location.origin + "/"} onChange={this.handleCustomIDChange} placeholder='my-shortened-url' />
</Form.Field>}
{setOptions.indexOf("expire") > -1 && <Form.Field>
<DatePicker showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="LLL" onChange={this.handleCustomExpirationChange} selected={expiration} customInput={<Input label="Expiration" />} minDate={moment()} />
</Form.Field>}
</Form.Group>
</Form>
</Segment>
<Card.Group itemsPerRow="2">
{links.map((link, i) => <CustomCard key={i} header={new URL(link[1]).hostname} metaHeader={link[1]} description={link[0]} />)}
{links.map((link, i) => <CustomCard key={i} header={new URL(link[1]).hostname} expireDate={link[2]} metaHeader={link[1]} description={link[0]} />)}
</Card.Group>
</div >
)

6
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 {
</Form>
</Segment>
<Card.Group itemsPerRow="2">
{links.map((link, i) => <CustomCard key={i} header={new URL(link[0]).hostname} metaHeader={link[1]} description={link[0]} showInfoURL/>)}
{links.map((link, i) => <CustomCard key={i} header={new URL(link[0]).hostname} metaHeader={link[1]} description={link[0]} expireDate={link[5]} showInfoURL/>)}
</Card.Group>
</div>
)

6
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

Loading…
Cancel
Save