فهرست منبع

Web UI: post target WIP, with working gorilla flashes.

Frederic G. MARAND 5 سال پیش
والد
کامیت
e47debe6a3
12فایلهای تغییر یافته به همراه179 افزوده شده و 15 حذف شده
  1. 3 0
      cmd/kurzd/dist.config.yml
  2. 1 1
      go.mod
  3. 4 0
      go.sum
  4. 19 1
      web/get_root.go
  5. 1 1
      web/get_short.go
  6. 82 2
      web/post_target.go
  7. 2 1
      web/public/css/plusvite.css
  8. 1 1
      web/templates/201.gohtml
  9. 28 0
      web/templates/409.gohtml
  10. 8 2
      web/templates/home.gohtml
  11. 9 0
      web/templates/layout/flashes.gohtml
  12. 21 6
      web/web.go

+ 3 - 0
cmd/kurzd/dist.config.yml

@@ -11,6 +11,9 @@ web:
   listenAddress: ":3000"
   # Delay is in seconds
   refreshDelay: 5
+  # Each instance should have its own session key
+  sessionKey: this_is_a_bad_key_change_it
+  sessionName: KURZSESSID
   # Used to build absolute URLs.
   siteBaseURL: &sbu http://localhost:3000
   siteName: "Kurz"

+ 1 - 1
go.mod

@@ -3,8 +3,8 @@ module code.osinet.fr/fgm/kurz
 require (
 	github.com/BurntSushi/toml v0.3.1 // indirect
 	github.com/go-sql-driver/mysql v1.4.0
-	github.com/gorilla/context v1.1.1 // indirect
 	github.com/gorilla/mux v1.6.2
+	github.com/gorilla/sessions v1.1.3
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pressly/goose v2.4.3+incompatible

+ 4 - 0
go.sum

@@ -11,6 +11,10 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
+github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=

+ 19 - 1
web/get_root.go

@@ -7,10 +7,28 @@ import (
 	"strings"
 )
 
+const rootInputName = "form[url]"
+
 // handleGetRoot handles path /
 func handleGetRoot(w http.ResponseWriter, r *http.Request) {
+	sess, err := store.Get(r, globals.SessionName)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	data := struct {
+		Flashes    []interface{}
+		InputName  string
+		SubmitName string
+	}{
+		sess.Flashes(),
+		rootInputName,
+		"submit",
+	}
+
+	sess.Save(r, w)
 	sw := &strings.Builder{}
-	err := tmpl.ExecuteTemplate(sw, "home", nil)
+	err = tmpl.ExecuteTemplate(sw, "home", data)
 	if err != nil {
 		log.Println(err)
 		w.WriteHeader(http.StatusInternalServerError)

+ 1 - 1
web/get_short.go

@@ -100,7 +100,7 @@ func handleGetShort(w http.ResponseWriter, r *http.Request, router *mux.Router)
 	// Happy path.
 	if err == nil {
 		w.Header().Set("Location", target)
-		w.WriteHeader(http.StatusTemporaryRedirect)
+		w.WriteHeader(http.StatusSeeOther)
 		return
 	}
 

+ 82 - 2
web/post_target.go

@@ -1,8 +1,88 @@
 package web
 
-import "net/http"
+import (
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"strings"
+
+	"github.com/gorilla/sessions"
+
+	"code.osinet.fr/fgm/kurz/domain"
+	"github.com/gorilla/mux"
+)
 
 // handlePostTarget handles form POST requests to /
-func handlePostTarget(w http.ResponseWriter, r *http.Request) {
+func handlePostTarget(w http.ResponseWriter, r *http.Request, router *mux.Router) {
+	var sess *sessions.Session
+
+	sess, storeErr := store.Get(r, globals.SessionName)
+	if storeErr != nil {
+		log.Fatal()
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	r.ParseForm()
+	defer r.Body.Close()
+	rawTarget := r.PostForm.Get(rootInputName)
+	target, err := validateTarget(rawTarget)
+	if err != nil {
+		sess.AddFlash(err.Error())
+		sess.Save(r, w)
+		w.Header().Set("Location", URLFromRoute(router, RouteGetRoot, nil))
+		w.WriteHeader(http.StatusSeeOther)
+		return
+	}
+
+	short, isNew, err := domain.GetShortURL(target)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	sw := &strings.Builder{}
+	var templateName string
+	if isNew {
+		sess.AddFlash("New short URL created")
+		templateName = "201"
+	} else {
+		sess.AddFlash("Existing short URL reused")
+		templateName = "409"
+	}
+	defer sess.Save(r, w)
+
+	data := struct {
+		Flashes                 []interface{}
+		FullyQualifiedShortURL  string
+		FullyQualifiedTargetURL string
+		Globals
+	}{
+		sess.Flashes(),
+		short,
+		target,
+		globals,
+	}
+
+	err = tmpl.ExecuteTemplate(sw, templateName, data)
+	if err != nil {
+		fmt.Println(err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	if isNew {
+		w.WriteHeader(http.StatusCreated)
+	} else {
+		w.WriteHeader(http.StatusConflict)
+	}
+	io.Copy(w, strings.NewReader(sw.String()))
+}
 
+func validateTarget(raw string) (string, error) {
+	if raw == "" {
+		return "", domain.MakeError(domain.TargetInvalidError, "empty target")
+	}
+	// BUG(fgm): needs much more validation, starting with XSS.
+	return raw, nil
 }

+ 2 - 1
web/public/css/plusvite.css

@@ -72,7 +72,8 @@ form div {
   margin: 0.3em 0 0 0;
 }
 
-form ul {
+form ul,
+.flashes {
   color: red;
   margin: 0;
 }

+ 1 - 1
web/templates/201.gohtml

@@ -15,7 +15,7 @@
         <h1>Plus vite !</h1>
         <p>Vous pouvez dorénavant aller plus vite vers</p>
         <pre><a href="{{.FullyQualifiedTargetURL}}" class="url-source">{{.FullyQualifiedTargetURL}}</a></pre>
-        <p>en utilisant le nouvel URL</p>
+        <p>en utilisant l'URL court déjà existant</p>
         <pre><a href="{{.FullyQualifiedShortURL}}">{{.FullyQualifiedShortURL}}</a></pre>
         <p>pour y faire référence.</p>
         <p><a href="{{.SiteBaseURL}}">Retour</a></p>

+ 28 - 0
web/templates/409.gohtml

@@ -0,0 +1,28 @@
+{{define "409"}}
+<!DOCTYPE html>
+<html lang="fr" dir="ltr">
+<head>
+    <title>{{.SiteName}}: vers {{.FullyQualifiedTargetURL}}</title>
+    <link rel="shortcut icon" href="{{asset "" "favicon.ico"}}">
+    <link rel="stylesheet" type="text/css" href="{{asset "css" "plusvite.css"}}" />
+    <link rel="stylesheet" type="text/css" href="{{asset "css" "riff.css"}}" />
+    {{template "analytics" .}}
+    {{template "inlinecss"}}
+</head>
+
+<body>
+    <div id="box">
+        <h1>Plus vite !</h1>
+        {{template "flashes" .}}
+        <p>Vous pouvez dorénavant aller plus vite vers</p>
+        <pre><a href="{{.FullyQualifiedTargetURL}}" class="url-source">{{.FullyQualifiedTargetURL}}</a></pre>
+        <p>en utilisant le nouvel URL</p>
+        <pre><a href="{{.FullyQualifiedShortURL}}">{{.FullyQualifiedShortURL}}</a></pre>
+        <p>pour y faire référence.</p>
+        <p><a href="{{.SiteBaseURL}}">Retour</a></p>
+    </div>
+
+    {{template "footer"}}
+</body>
+</html>
+{{end}}

+ 8 - 2
web/templates/home.gohtml

@@ -19,8 +19,14 @@
             vite communiqués même au téléphone sans risque d'erreur.</p>
 
         <form method="post">
-            <div id="form"><div><label accesskey="U" for="form_url" class="required">L&#039;URL à transformer :</label><input type="url" id="form_url" name="form[url]" required="required" placeholder="http://www.osinet.fr" size="60" maxlength="240" /></div></div>
-            <input type="submit" name="submit" value="Envoyer" />
+            {{template "flashes" .}}
+            <div id="form">
+                <div>
+                    <label accesskey="U" for="form_url" class="required">L&#039;URL à transformer :</label>
+                    <input type="url" id="form_url" name="{{ .InputName }}" zrequired="required" placeholder="http://www.osinet.fr" size="60" maxlength="240" />
+                </div>
+            </div>
+            <input type="submit" name="{{ .SubmitName }}" value="Envoyer" />
         </form>
 
         <div class="notes">

+ 9 - 0
web/templates/layout/flashes.gohtml

@@ -0,0 +1,9 @@
+{{define "flashes"}}
+{{if .Flashes}}
+    <ul class="flashes">
+        {{range $flash := .Flashes}}
+            <li>{{ $flash }}</li>
+        {{end}}
+    </ul>
+{{end}}
+{{end}}

+ 21 - 6
web/web.go

@@ -9,7 +9,6 @@ package web
 
 import (
 	"errors"
-	"github.com/gorilla/mux"
 	"html/template"
 	"log"
 	"net/http"
@@ -17,6 +16,9 @@ import (
 	"path"
 	"path/filepath"
 	"strconv"
+
+	"github.com/gorilla/mux"
+	"github.com/gorilla/sessions"
 )
 
 // Route names.
@@ -28,6 +30,8 @@ const (
 
 // Content types.
 const (
+	HtmlFormType = "application/x-www-form-urlencoded"
+
 	// HtmlType is the MIME HTML type.
 	HtmlType = "text/html"
 
@@ -39,12 +43,15 @@ type Globals struct {
 	AssetsBaseURL string
 	AssetsVersion int
 	AssetsPath    string
+	SessionKey    []byte
+	SessionName   string
 	SiteBaseURL   string
 	RefreshDelay  int
 	SiteName      string
 }
 
 var globals Globals
+var store *sessions.CookieStore
 var tmpl *template.Template
 
 // SetupUI() configures Web UI routes on the passed mux.Router.
@@ -75,9 +82,11 @@ func setupControllerRoutes(router *mux.Router) {
 	}).
 		Methods("GET", "HEAD").
 		Name(RouteGetShort)
-	router.HandleFunc("/", handlePostTarget).
+	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		handlePostTarget(w, r, router)
+	}).
 		HeadersRegexp("Accept", HtmlTypeRegex).
-		Headers("Content-Type", HtmlType).
+		Headers("Content-Type", HtmlFormType).
 		Methods("POST").
 		Name(RoutePostTarget)
 	router.HandleFunc("/", handleGetRoot).
@@ -96,8 +105,10 @@ func setupTemplates(configAssetsPath string) {
 		ParseFiles(
 			base+"/201.gohtml",
 			base+"/404.gohtml",
+			base+"/409.gohtml",
 			base+"/home.gohtml",
 			layout+"/analytics.gohtml",
+			layout+"/flashes.gohtml",
 			layout+"/footer.gohtml",
 			layout+"/inlinecss.gohtml",
 		))
@@ -110,9 +121,13 @@ func SetupGlobals(c map[string]interface{}) {
 		AssetsPath:    c["assetspath"].(string),
 		AssetsVersion: c["assetsversion"].(int),
 		RefreshDelay:  c["refreshdelay"].(int),
+		SessionKey:    []byte(c["sessionkey"].(string)),
+		SessionName:   c["sessionname"].(string),
 		SiteBaseURL:   c["sitebaseurl"].(string),
 		SiteName:      c["sitename"].(string),
 	}
+
+	store = sessions.NewCookieStore(globals.SessionKey)
 }
 
 /*
@@ -123,10 +138,10 @@ To build URLs for assets, use URLForAsset().
   - ns: the assets namespace. One of "js", "css', "images".
   - path: the asset path relative to the project root
 */
-func URLFromRoute(router mux.Router, name string, params map[string]string) string {
-	log.Println(name, params)
+func URLFromRoute(router *mux.Router, name string, params map[string]string) string {
+	log.Println("Generating route:", name, params)
 	//url, err := router.Get(name).URL(params
-	return name
+	return "/"
 }
 
 /*