summaryrefslogtreecommitdiff
path: root/src/short.nim
blob: fa5486a1f35f9a63b749c6cf1f28825186200548 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import os, strutils
import std/[hashes, oids, re, times, uri]

import tiny_sqlite
import jester
import nimja/parser

import database

const allCss = staticRead("../static/all.css")
const cssRoute = "/static/all.css." & $hash(allCss)
const favicon = staticRead("../static/favicon.ico")

var db {.threadvar.}: DbConn

proc initDB() {.raises: [SqliteError].}=
  if not db.isOpen():
    db = openDatabase("data/short.db")
    if not db.Migrate():
      echo "Failed to migrate database schema"
      quit 1

func renderIndex(): string {.raises: [].} =
  var req: ShortUrl
  compileTemplateFile(getScriptDir() / "templates/index.html")

func renderAbout(): string {.raises: [].} =
  compileTemplateFile(getScriptDir() / "templates/about.html")

func renderShort(req: ShortUrl): string {.raises: [].} =
  compileTemplateFile(getScriptDir() / "templates/short.html")

func renderNoShort(req: ShortUrl): string {.raises: [].} =
  compileTemplateFile(getScriptDir() / "templates/noshort.html")

func renderError(code: int, msg: string): string {.raises: [].} =
  compileTemplateFile(getScriptDir() / "templates/error.html")

proc handleToken(token:string): (HttpCode, string) {.raises: [].} =
  try:
    let tokenRegexp = re"^[\w]{24}$"
    if not match(token, tokenRegexp):
      return (Http400, renderError(400, "Bad Request"))
  except RegexError:
    return (Http500, renderError(500, "RegexError"))
  db.CleanExpired()
  try:
    let req = db.GetUrl(token)
    if req == nil:
      return (Http404, renderNoShort(req[]))
    return (Http200, renderShort(req[]))
  except SqliteError:
    return (Http500, renderError(500, "SqliteError"))

proc handleIndexPost(params: Table[string, string]): (HttpCode, string) {.raises: [].} =
  var input: ShortUrl
  var exp: int
  for k, v in params.pairs:
    case k:
      of "title":
        try:
          let titleRegexp = re"^[\w\s]{3,64}$"
          if match(v, titleRegexp):
            input.Title = v
          else:
            echo "title"
            return (Http400, renderError(400, "Bad Request"))
        except RegexError:
          return (Http500, renderError(500, "RegexError"))
      of "url":
        try:
          discard parseUri(v)
          input.Url = v
        except:
          echo "url"
          return (Http400, renderError(400, "Bad Request"))
      of "expires":
        try:
          exp = parseInt(v)
        except ValueError:
          return (Http400, renderError(400, "Bad Request"))
        if exp < 1 or exp > 527040:
          echo "exp"
          return (Http400, renderError(400, "Bad Request"))
      of "shorten": discard
      else: return (Http400, renderError(400, "Bad Request"))
  if input.Title == "" or input.Url == "" or exp == 0:
    echo "empty"
    return (Http400, renderError(400, "Bad Request"))
  input.Token = $genOid()
  input.Created = times.now()
  input.Expires = input.Created + initDuration(minutes=exp)
  try:
    db.AddUrl(input)
  except SqliteError:
    return (Http500, renderShort(input))
  return (Http200, input.Token)

routes:
  get "/":
    resp renderIndex()
  get "/about":
    resp renderAbout()
  post "/":
    initDB()
    var (code, content) = handleIndexPost(request.params)
    if code != Http200:
      resp code, content
    else:
      redirect("/" & content)
  get "/static/favicon.ico":
    resp Http200, {"content-type": "image/x-icon"}, favicon
  get re"^/static/all\.css\.":
    resp Http200, {"content-type": "text/css"}, allcss
  get "/@token":
    initDB()
    var (code, content) = handleToken(@"token")
    resp code, content

runForever()