Skip to content

Commit 9254e92

Browse files
committed
Basic forwarding mechanism
Enable parsing of SQL time to time.Time, ErrNotFound Format Migrations Add Shortcut struct with matcher field Add Response with JSON and HTML rendering Fix headers in middlewares Signed-off-by: David Kröll <[email protected]>
1 parent 4737a8d commit 9254e92

9 files changed

+182
-21
lines changed

.env.example

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
DB_CONNECTION=mysql
2+
DB_USERNAME=
3+
DB_PASSWORD=
4+
DB_HOST=
5+
DB_DATABASE=
6+
7+
PORT=9999
8+
9+
JWT_SECRET=

models/database.go

+15-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package models
22

33
import (
44
"database/sql"
5+
"errors"
56
_ "github.com/go-sql-driver/mysql"
67
"log"
78
"sync"
@@ -11,6 +12,12 @@ import (
1112
var db Database
1213
var singleton sync.Once
1314

15+
var ErrNotFound = errors.New("not found")
16+
17+
type Relateable interface {
18+
LoadRelated() (err error)
19+
}
20+
1421
type DBModel interface {
1522
Save() (err error)
1623
Delete() (err error)
@@ -24,11 +31,18 @@ type Database struct {
2431
*sql.DB
2532
}
2633

34+
type MatchingField string
35+
36+
const (
37+
ID MatchingField = "ID"
38+
ShortIdentifier MatchingField = "ShortIdentifier"
39+
)
40+
2741
func InitDatabase(config DBConfig) *Database {
2842
// make sure this code only gets executed once
2943
singleton.Do(func() {
3044
database, err := sql.Open(config.Driver,
31-
config.Username+":"+config.Password+"@tcp("+config.Host+")/"+config.Name)
45+
config.Username+":"+config.Password+"@tcp("+config.Host+")/"+config.Name+"?parseTime=true")
3246
if err != nil {
3347
log.Fatal(err)
3448
}

models/migrations.go

+12-11
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package models
33
import "log"
44

55
const (
6-
createUsersTable string = `
7-
CREATE TABLE Users (
6+
createUsersTable string = `CREATE TABLE Users
7+
(
88
ID CHAR(36) PRIMARY KEY,
99
Email CHAR(50) NOT NULL UNIQUE,
1010
Firstname VARCHAR(50) NOT NULL,
@@ -13,21 +13,22 @@ CREATE TABLE Users (
1313
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1414
UpdatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1515
LastLogin DATETIME,
16-
CHECK (UpdatedAt >= CreatedAt)
16+
CHECK (UpdatedAt >= CreatedAt)
1717
)
1818
ENGINE = InnoDB;`
1919
dropUsersTable string = `DROP TABLE IF EXISTS Users;`
2020

21-
createShortcutsTable string = `
22-
CREATE TABLE Shortcuts (
21+
createShortcutsTable string = `CREATE TABLE Shortcuts
22+
(
2323
ID CHAR(36) PRIMARY KEY,
24-
ShortIdentifier CHAR(50) NOT NULL,
25-
RedirectionURL VARCHAR(1024) NOT NULL,
26-
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ,
24+
ShortIdentifier CHAR(50) NOT NULL UNIQUE,
25+
RedirectURL VARCHAR(1024) NOT NULL,
26+
RedirectStatus INT(3) NOT NULL,
27+
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2728
UpdatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2829
ValidThru DATETIME NOT NULL,
2930
UserID CHAR(36) NOT NULL,
30-
31+
3132
CHECK (ValidThru > CreatedAt),
3233
CHECK (UpdatedAt >= CreatedAt),
3334
@@ -37,8 +38,8 @@ CREATE TABLE Shortcuts (
3738
`
3839
dropShortcutsTable string = `DROP TABLE IF EXISTS Shortcuts;`
3940

40-
createShortcutLogsTable string = `
41-
CREATE TABLE ShortcutLog (
41+
createShortcutLogsTable string = `CREATE TABLE ShortcutLog
42+
(
4243
ShortcutID CHAR(36),
4344
IPAddress VARCHAR(39),
4445
UserAgent VARCHAR(100),

models/shortcuts.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package models
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"time"
7+
)
8+
9+
type Shortcut struct {
10+
ID string
11+
ShortIdentifer string
12+
RedirectURL string
13+
RedirectStatus int
14+
CreatedAt time.Time
15+
UpdatedAt time.Time
16+
ValidThru time.Time
17+
User *User
18+
userID string
19+
}
20+
21+
const (
22+
selectShortcut string = `SELECT * FROM Shortcuts WHERE %s = ?;`
23+
loadRelatedShortcut string = `SELECT * FROM Users WHERE ID = ?;`
24+
)
25+
26+
func ShortcutBy(matcher MatchingField, value string) (*Shortcut, error) {
27+
s := &Shortcut{}
28+
29+
// make sure user cannot parse matcher by malformed input
30+
row := db.QueryRow(fmt.Sprintf(selectShortcut, matcher), value)
31+
err := row.Scan(&s.ID, &s.ShortIdentifer, &s.RedirectURL, &s.RedirectStatus, &s.CreatedAt, &s.UpdatedAt, &s.ValidThru, &s.userID)
32+
33+
if err != nil && err != sql.ErrNoRows {
34+
return nil, err
35+
} else if err == sql.ErrNoRows {
36+
return nil, ErrNotFound
37+
}
38+
39+
return s, nil
40+
}
41+
42+
func (s *Shortcut) LoadRelated() (err error) {
43+
result := db.QueryRow(loadRelatedShortcut, s.userID)
44+
45+
if err := result.Scan(s.User.ID, s.User.Email, s.User.Firstname, s.User.Lastname, s.User.Password, s.User.CreatedAt, s.User.UpdatedAt, s.User.LastLogin); err != nil {
46+
return err
47+
}
48+
49+
return nil
50+
}

routes/html.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package routes
2+
3+
import (
4+
"html/template"
5+
"log"
6+
"net/http"
7+
)
8+
9+
var shortcutNotFound = *template.Must(template.New("shortcutNotFound").Parse(`<html lang="en">
10+
<body>
11+
<h1>404</h1>
12+
<h2>We could not find the requested page</h2>
13+
<h3>Error code: {{.Code}}</h3>
14+
15+
<p>{{.Message}}</p>
16+
</body>
17+
</html>`))
18+
19+
func (r Response) HTML(w http.ResponseWriter, statusCode int, tmpl template.Template) {
20+
w.WriteHeader(statusCode)
21+
22+
if err := tmpl.Execute(w, r); err != nil {
23+
log.Println(err)
24+
}
25+
}

routes/http_bodies.go

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
package routes
22

3-
type ErrorResponse struct {
4-
Message string `json:"message"`
3+
import (
4+
"encoding/json"
5+
"log"
6+
"net/http"
7+
)
8+
9+
type Response struct {
10+
Success bool `json:"success"`
11+
Code int `json:"code,omitempty"`
12+
Message string `json:"message,omitempty"`
13+
}
14+
15+
func (r Response) JSON(w http.ResponseWriter, statusCode int) {
16+
w.WriteHeader(statusCode)
17+
18+
err := json.NewEncoder(w).Encode(r)
19+
if err != nil {
20+
log.Println(err)
21+
}
522
}

routes/middlewares.go

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package routes
22

33
import (
4-
"encoding/json"
54
"log"
65
"net/http"
76
)
87

8+
var jsonBody = "application/json"
9+
var htmlBody = "text/html"
10+
911
// MiddlewareFunc is a custom Middleware type
1012
type MiddlewareFunc func(http.Handler) http.Handler
1113

@@ -24,15 +26,22 @@ func HeaderBinding(next http.Handler) http.Handler {
2426
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2527

2628
if r.Method == "POST" {
27-
if r.Header.Get("Content-Type") != "application/json" {
28-
_ = json.NewEncoder(w).Encode(ErrorResponse{
29-
"Wrong Content-Type in POST request. application/json expected",
30-
})
29+
if r.Header.Get("Content-Type") != jsonBody {
30+
Response{
31+
Success: false,
32+
Code: 1000,
33+
Message: "Wrong Content-Type in POST request. application/json expected",
34+
}.JSON(w, 400)
3135
return
3236
}
3337
}
3438

35-
w.Header().Set("Content-Type", "application/json")
39+
if r.Header.Get("Accept") == jsonBody {
40+
w.Header().Set("Content-Type", jsonBody)
41+
} else {
42+
w.Header().Set("Content-Type", htmlBody)
43+
}
44+
3645
next.ServeHTTP(w, r)
3746
})
3847
}

routes/router.go

+2
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ func InitRouter() (router mux.Router) {
2121
api.HandleFunc("/user", getUser).Methods("GET")
2222
api.HandleFunc("/user", updateUser).Methods("PUT")
2323

24+
router.HandleFunc("/{shortId:[a-zA-Z0-9]+}", forwardShortcut).Methods("GET")
25+
2426
return
2527
}

routes/shortcuts.go

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package routes
22

3-
import "net/http"
3+
import (
4+
"github.com/davidkroell/shortcut/models"
5+
"github.com/gorilla/mux"
6+
"net/http"
7+
)
48

59
func createShortcut(w http.ResponseWriter, r *http.Request) {
610

@@ -21,3 +25,33 @@ func updateShortcut(w http.ResponseWriter, r *http.Request) {
2125
func deleteShortcut(w http.ResponseWriter, r *http.Request) {
2226

2327
}
28+
29+
func forwardShortcut(w http.ResponseWriter, r *http.Request) {
30+
vars := mux.Vars(r)
31+
shortId := vars["shortId"]
32+
33+
shortcut, err := models.ShortcutBy(models.ShortIdentifier, shortId)
34+
if err != nil && err != models.ErrNotFound {
35+
Response{
36+
Success: false,
37+
Code: 1001,
38+
Message: "Bad Request. " + err.Error(),
39+
}.JSON(w, 400)
40+
return
41+
} else if err == models.ErrNotFound {
42+
resp := Response{
43+
Success: false,
44+
Code: 1002,
45+
Message: shortId + " not found",
46+
}
47+
48+
if w.Header().Get("Content-Type") == jsonBody {
49+
resp.JSON(w, 404)
50+
} else {
51+
resp.HTML(w, 404, shortcutNotFound)
52+
}
53+
return
54+
}
55+
56+
http.Redirect(w, r, shortcut.RedirectURL, shortcut.RedirectStatus)
57+
}

0 commit comments

Comments
 (0)