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. 8
      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. 2
      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

8
handlers/public.go

@ -3,6 +3,7 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/maxibanki/golang-url-shortener/handlers/auth" "github.com/maxibanki/golang-url-shortener/handlers/auth"
@ -14,6 +15,7 @@ import (
type urlUtil struct { type urlUtil struct {
URL string `binding:"required"` URL string `binding:"required"`
ID string ID string
Expiration time.Time
} }
// handleLookup is the http handler for getting the infos // 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) entry, err := h.store.GetEntryByID(id)
if err == store.ErrIDIsEmpty || err == store.ErrNoEntryFound { if err == store.ErrIDIsEmpty || err == store.ErrNoEntryFound {
return // return normal 404 error if such an error occurs return
} else if err != nil { } else if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return return
} }
if time.Now().After(entry.Public.Expiration) && !entry.Public.Expiration.IsZero() {
return
}
if err := h.store.IncreaseVisitCounter(id); err != nil { if err := h.store.IncreaseVisitCounter(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@ -73,6 +78,7 @@ func (h *Handler) handleCreate(c *gin.Context) {
id, err := h.store.CreateEntry(store.Entry{ id, err := h.store.CreateEntry(store.Entry{
Public: store.EntryPublicData{ Public: store.EntryPublicData{
URL: data.URL, URL: data.URL,
Expiration: data.Expiration,
}, },
RemoteAddr: c.ClientIP(), RemoteAddr: c.ClientIP(),
OAuthProvider: user.OAuthProvider, OAuthProvider: user.OAuthProvider,

7
static/package.json

@ -10,9 +10,10 @@
}, },
"dependencies": { "dependencies": {
"prismjs": "^1.8.4", "prismjs": "^1.8.4",
"react": "^16.0.0", "react": "^16.1.1",
"react-clipboard.js": "^1.1.2", "react-clipboard.js": "^1.1.3",
"react-dom": "^16.0.0", "react-datepicker": "^0.61.0",
"react-dom": "^16.1.1",
"react-prism": "^4.3.1", "react-prism": "^4.3.1",
"react-qr-svg": "^2.1.0", "react-qr-svg": "^2.1.0",
"react-router": "^4.2.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'; import Clipboard from 'react-clipboard.js';
export default class CardComponent extends Component { 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() { render() {
const { expireDate } = this.state
return (<Card key={this.key}> return (<Card key={this.key}>
<Card.Content> <Card.Content>
{expireDate && <Card.Header style={{ float: "right", fontSize: "1.1em" }}>
Expires in {expireDate}
</Card.Header>}
<Card.Header> <Card.Header>
{this.props.header} {this.props.header}
</Card.Header> </Card.Header>
@ -20,14 +35,14 @@ export default class CardComponent extends Component {
<Card.Content extra> <Card.Content extra>
{!this.props.showInfoURL ? <div className='ui two buttons'> {!this.props.showInfoURL ? <div className='ui two buttons'>
<Modal closeIcon trigger={<Button icon='qrcode' content='Show QR-Code' />}> <Modal closeIcon trigger={<Button icon='qrcode' content='Show QR-Code' />}>
<Modal.Header className="ui center aligned">{this.props.description}</Modal.Header> <Modal.Header className='ui center aligned'>{this.props.description}</Modal.Header>
<Modal.Content style={{ textAlign: "center" }}> <Modal.Content style={{ textAlign: 'center' }}>
<QRCode style={{ width: "75%" }} value={this.props.description} /> <QRCode style={{ width: '75%' }} value={this.props.description} />
</Modal.Content> </Modal.Content>
</Modal> </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> <div>
<Icon name="clipboard" /> <Icon name='clipboard' />
Copy to Clipboard Copy to Clipboard
</div> </div>
</Clipboard> </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 React, { Component } from 'react'
import { Input, Segment, Form, Header, Card, Button, Select, Icon } from 'semantic-ui-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 CustomCard from '../Card/Card'
import './Home.css'
export default class HomeComponent extends Component { export default class HomeComponent extends Component {
handleURLChange = (e, { value }) => this.url = value handleURLChange = (e, { value }) => this.url = value
handleCustomExpirationChange = expire => this.setState({ expiration: expire })
handleCustomIDChange = (e, { value }) => { handleCustomIDChange = (e, { value }) => {
this.customID = value this.customID = value
fetch("/api/v1/protected/lookup", { fetch("/api/v1/protected/lookup", {
@ -31,7 +36,8 @@ export default class HomeComponent extends Component {
{ text: 'Expiration', value: 'expire' } { text: 'Expiration', value: 'expire' }
], ],
setOptions: [], setOptions: [],
showCustomIDError: false showCustomIDError: false,
expiration: moment()
} }
componentDidMount() { componentDidMount() {
this.urlInput.focus() this.urlInput.focus()
@ -42,7 +48,8 @@ export default class HomeComponent extends Component {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
URL: this.url, URL: this.url,
ID: this.customID ID: this.customID,
Expiration: this.state.setOptions.indexOf("expire") > -1 ? this.state.expiration.toISOString() : undefined
}), }),
headers: { headers: {
'Authorization': window.localStorage.getItem('token'), 'Authorization': window.localStorage.getItem('token'),
@ -52,14 +59,15 @@ export default class HomeComponent extends Component {
.then(r => this.setState({ .then(r => this.setState({
links: [...this.state.links, [ links: [...this.state.links, [
r.URL, r.URL,
this.url this.url,
this.state.setOptions.indexOf("expire") > -1 ? this.state.expiration : undefined
]] ]]
})) }))
} }
} }
render() { render() {
const { links, options, setOptions, showCustomIDError } = this.state const { links, options, setOptions, showCustomIDError, expiration } = this.state
return ( return (
<div> <div>
<Segment raised> <Segment raised>
@ -73,15 +81,20 @@ export default class HomeComponent extends Component {
</Input> </Input>
</Form.Field> </Form.Field>
<Form.Group widths='equal'> <Form.Group widths='equal'>
{setOptions.indexOf("custom") > -1 && <Form.Field error={showCustomIDError}><Input label={window.location.origin + "/"} onChange={this.handleCustomIDChange} placeholder='my-shortened-url' /> {setOptions.indexOf("custom") > -1 && <Form.Field error={showCustomIDError}>
</Form.Field> <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.Group>
</Form> </Form>
</Segment> </Segment>
<Card.Group itemsPerRow="2"> <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> </Card.Group>
</div > </div >
) )

6
static/src/Lookup/Lookup.js

@ -1,5 +1,6 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Segment, Header, Form, Input, Card } from 'semantic-ui-react' import { Segment, Header, Form, Input, Card } from 'semantic-ui-react'
import moment from 'moment';
import CustomCard from '../Card/Card' import CustomCard from '../Card/Card'
@ -26,7 +27,8 @@ export default class LookupComponent extends Component {
this.url, this.url,
this.VisitCount, this.VisitCount,
res.CratedOn, res.CratedOn,
res.LastVisit res.LastVisit,
moment(res.Expiration)
]] ]]
})) }))
} }
@ -43,7 +45,7 @@ export default class LookupComponent extends Component {
</Form> </Form>
</Segment> </Segment>
<Card.Group itemsPerRow="2"> <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> </Card.Group>
</div> </div>
) )

2
store/store.go

@ -31,7 +31,7 @@ type Entry struct {
// EntryPublicData is the public part of an entry // EntryPublicData is the public part of an entry
type EntryPublicData struct { type EntryPublicData struct {
CreatedOn, LastVisit time.Time CreatedOn, LastVisit, Expiration time.Time
VisitCount int VisitCount int
URL string URL string
} }

Loading…
Cancel
Save