hlfw.ca

webbing

Download patch

ref: 5e1a3e668a38b83b901f97ab8c9829b2601b2c60
parent: 19830d1a2ae03226e6f2dc487f8d1339d2d0a19f
author: halfwit <michaelmisch1985@gmail.com>
date: Thu Aug 6 05:23:15 PDT 2020

Add router code

--- a/footer.go
+++ /dev/null
@@ -1,33 +1,0 @@
-package router
-
-import (
-	"golang.org/x/text/message"
-)
-
-func footer(p *message.Printer) map[string]string {
-	return map[string]string{
-		"faq":      p.Sprintf("FAQ"),
-		"help":     p.Sprintf("Help"),
-		"banner":   p.Sprintf("Quality Healthcare"),
-		"pay":      p.Sprintf("Payment Methods"),
-		"fees":     p.Sprintf("Prices and Fees"),
-		"verify":   p.Sprintf("Verification"),
-		"appt":     p.Sprintf("Appointments"),
-		"legal":    p.Sprintf("Legal"),
-		"privacy":  p.Sprintf("Privacy Policy"),
-		"howworks": p.Sprint("How It Works"),
-		"contact":  p.Sprint("Contact Us"),
-		"pricing":  p.Sprint("Pricing"),
-		"catalog":  p.Sprint("Catalog"),
-		"appts":    p.Sprint("Appointments"),
-		"proc":     p.Sprint("Payment Procedures"),
-		"payments": p.Sprint("Payment Methods"),
-		"phone":    p.Sprint("Call toll free"),
-		"number":   p.Sprint("1(555)555-1234"),
-		"email":    p.Sprint("Email"),
-		"partHead": p.Sprintf("Work with us!"),
-		"partner":  p.Sprintf("Become A Partner"),
-		"provider": p.Sprint("Become A Provider"),
-		"copy":     p.Sprintf("Copyright 2017, 2018, 2019"),
-	}
-}
--- a/forms.go
+++ /dev/null
@@ -1,66 +1,0 @@
-package router
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/olmaxmedical/session"
-	"golang.org/x/text/message"
-)
-
-var formlist map[string]*Form
-
-// Form - POST requests
-type Form struct {
-	Access    Access
-	After     PluginMask
-	Path      string
-	Redirect  string
-	Validator func(r *http.Request, p *message.Printer) []string
-}
-
-func init() {
-	formlist = make(map[string]*Form)
-}
-
-// AddPost - Register a POST form from forms/
-func AddPost(f *Form) {
-	formlist[f.Path+".html"] = f
-}
-
-func parseForm(p *Request, w http.ResponseWriter, r *http.Request) (*Form, []string) {
-	var errors []string
-	form, ok := formlist[p.path]
-	if !ok {
-		errors = append(errors, "No such page")
-		return nil, errors
-	}
-	if errs := form.Validator(r, p.printer); len(errs) > 0 {
-		return nil, errs
-	}
-	for _, key := range pluginKey {
-		if (form.After&key) != 0 && pluginCache[key].Validate != nil {
-			if e := pluginCache[key].Validate(p); e != nil {
-				errors = append(errors, fmt.Sprint(e))
-				return nil, errors
-			}
-		}
-	}
-	return form, errors
-}
-
-func postform(p *Request, us session.Session, w http.ResponseWriter, r *http.Request) {
-	form, errors := parseForm(p, w, r)
-	if len(errors) > 0 && errors[0] != "nil" {
-		// NOTE(halfwit) this stashes previous entries, but does not work
-		// on multipart forms (with file uploads)
-		us.Set("errors", errors)
-		// Maybe store form args instead here in session
-		url := fmt.Sprintf("%s?%s", r.URL.String(), r.Form.Encode())
-		http.Redirect(w, r, url, 302)
-	}
-	if form != nil {
-		us.Set("errors", []string{})
-		http.Redirect(w, r, form.Redirect, 302)
-	}
-}
--- a/go.mod
+++ /dev/null
@@ -1,10 +1,0 @@
-module github.com/olmaxmedical/router
-
-go 1.14
-
-require (
-	github.com/olmaxmedical/database v0.0.1
-	github.com/olmaxmedical/email v0.0.1
-	github.com/olmaxmedical/session v0.0.1
-	golang.org/x/text v0.3.2
-)
--- a/go.sum
+++ /dev/null
@@ -1,14 +1,0 @@
-github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/olmaxmedical/database v0.0.0/go.mod h1:BPYEBAP3GYeSeqg5hOaH7GC8+P/VnD2OuHYounbVjQs=
-github.com/olmaxmedical/database v0.0.1 h1:cuocVljXq7cPRS9HygFg2B/WSdzWAZEHrc5uMnN+A0A=
-github.com/olmaxmedical/database v0.0.1/go.mod h1:/5Tl6/p0jpvLpj4GaoFki3wRG/3b+ipNNhM5Dyi6Zf8=
-github.com/olmaxmedical/email v0.0.1 h1:bhOERmPiUmFJqC133s+FFXucSI3dNnfDKsboDYFEbkc=
-github.com/olmaxmedical/email v0.0.1/go.mod h1:bz6en9uc6h9fyu3MW2jTwYW19ZclQ22JkcIxsl3/epc=
-github.com/olmaxmedical/session v0.0.1 h1:2xdSjpEg89+ClRFLPp/gR3lNxl4JjPAUd2Hsds++FFs=
-github.com/olmaxmedical/session v0.0.1/go.mod h1:XOVyHL+cKa5t2fLDIJtFxwEzJOa3r1hUkwlL4aybdqA=
-github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da h1:hhmnjfzz7szp75AyXxn8tDfEA0oU4REQLmpuW6zNAOY=
-github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da/go.mod h1:Q5ljvYIBpukMH+wgB8kcPV1i9NX8TqU++8GgBKq3pt0=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
--- a/handler.go
+++ /dev/null
@@ -1,145 +1,0 @@
-package router
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/olmaxmedical/database"
-	"github.com/olmaxmedical/email"
-	"github.com/olmaxmedical/session"
-	"golang.org/x/text/message"
-)
-
-// Handle specific endpoints
-type handler struct {
-	manager *session.Manager
-}
-
-func (d *handler) logout(w http.ResponseWriter, r *http.Request) {
-	d.manager.Destroy(w, r)
-	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
-	http.Redirect(w, r, "/index.html", 302)
-}
-
-func (d *handler) normal(w http.ResponseWriter, r *http.Request) {
-	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
-	if r.URL.Path == "/" {
-		http.Redirect(w, r, "/index.html", 302)
-		return
-	}
-	user, status, us, role := d.getUser(w, r)
-	p := &Request{
-		printer: userLang(r),
-		status:  status,
-		request: r,
-		user:    user,
-		role:    role,
-		session: us,
-		path:    r.URL.Path[1:],
-	}
-
-	switch r.Method {
-	case "GET":
-		getpage(p, w)
-	case "POST":
-		postform(p, us, w, r)
-	}
-}
-
-// TODO: This will require actual client data from the database to populate the page
-func (d *handler) profile(w http.ResponseWriter, r *http.Request) {
-	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
-	user, status, us, role := d.getUser(w, r)
-	if status == "false" {
-		http.Redirect(w, r, "/login.html", 302)
-		return
-	}
-	if rd, ok := us.Get("redirect").(string); ok {
-		us.Delete("redirect")
-		http.Redirect(w, r, "/"+rd, 302)
-		return
-	}
-	p := &Request{
-		printer: userLang(r),
-		status:  status,
-		session: us,
-		user:    user,
-		role:    role,
-	}
-	var data []byte
-	var err error
-	switch database.UserRole(user) {
-	case database.DoctorAuth:
-		if role != database.DoctorAuth {
-			http.Error(w, "Unauthorized", 401)
-			return
-		}
-		p.path = "doctor/profile.html"
-		data, err = getData(p, "doctor")
-	case database.PatientAuth:
-		if role != database.PatientAuth {
-			http.Error(w, "Unauthorized", 401)
-			return
-		}
-		p.path = "patient/profile.html"
-		data, err = getData(p, "patient")
-	default:
-		http.Error(w, "Forbidden", 403)
-		return
-	}
-	if err != nil {
-		http.Error(w, "Service Unavailable", 503)
-		return
-	}
-	fmt.Fprintf(w, "%s", data)
-}
-
-func (d *handler) activate(w http.ResponseWriter, r *http.Request) {
-	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
-	if len(r.URL.Path) != 46 && r.URL.Path[:9] != "/activate" {
-		http.Error(w, "Bad Request", 400)
-		return
-	}
-	email.ValidateSignupToken(w, r, r.URL.Path[10:])
-}
-
-func (d *handler) reset(w http.ResponseWriter, r *http.Request) {
-	if len(r.URL.Path) != 43 && r.URL.Path[:6] != "/reset" {
-		http.Error(w, "Bad Request", 400)
-		return
-	}
-	p := userLang(r)
-	user, _, us, _ := d.getUser(w, r)
-	token := email.NextResetToken(r.URL.Path[7:], user)
-	if token == "" {
-		us.Set("errors", [1]string{p.Sprint("Token expired")})
-		return
-	}
-	us.Set("token", token)
-	r.URL.Path = "/newpassword.html"
-	d.normal(w, r)
-}
-
-func (d *handler) getUser(w http.ResponseWriter, r *http.Request) (string, string, session.Session, database.Access) {
-	us := d.manager.Start(w, r)
-	user, ok1 := us.Get("username").(string)
-	status, ok2 := us.Get("login").(string)
-	role, ok3 := us.Get("role").(database.Access)
-	if !ok1 || !ok2 || status != "true" {
-		status = "false"
-	}
-	if !ok3 {
-		role = database.GuestAuth
-	}
-	if status == "true" {
-		us.Set("token", database.NewToken())
-	}
-	return user, status, us, role
-}
-
-func userLang(r *http.Request) *message.Printer {
-	accept := r.Header.Get("Accept-Language")
-	lang := r.FormValue("lang")
-	tag := message.MatchLanguage(lang, accept)
-	return message.NewPrinter(tag)
-}
--- a/header.go
+++ /dev/null
@@ -1,16 +1,0 @@
-package router
-
-import (
-	"golang.org/x/text/message"
-)
-
-func header(p *message.Printer, status string) map[string]string {
-	return map[string]string{
-		"home":    p.Sprint("Home"),
-		"login":   p.Sprint("Login"),
-		"logout":  p.Sprint("Logout"),
-		"signup":  p.Sprint("Sign Up"),
-		"profile": p.Sprint("Profile"),
-		"status":  status,
-	}
-}
--- a/pages.go
+++ /dev/null
@@ -1,176 +1,0 @@
-package router
-
-import (
-	"bufio"
-	"bytes"
-	"errors"
-	"fmt"
-	"html/template"
-	"net/http"
-	"os"
-	"path"
-	"strings"
-
-	"github.com/olmaxmedical/database"
-	"golang.org/x/text/message"
-)
-
-var pagecache map[string]*Page
-
-func init() {
-	pagecache = make(map[string]*Page)
-}
-
-// Access defines the access rights for a specific page
-type Access uint8
-
-const (
-	// GuestAuth - non registered user
-	GuestAuth Access = 1 << iota
-	// PatientAuth - normal user, added by registration process
-	PatientAuth
-	// DoctorAuth - manually added to the database
-	DoctorAuth
-)
-
-// Page defines what a client receives from a GET request
-type Page struct {
-	Access Access
-	Extra  PluginMask
-	CSS    string
-	Path   string
-	Data   func(p *message.Printer) map[string]interface{}
-	tmpl   *template.Template
-}
-
-// AddPage - register a *Page to the cache
-func AddPage(p *Page) {
-	pagecache[p.Path+".html"] = p
-}
-
-// ValidatePages - Walk all our templates, test them, and finally return applicable errors as an array
-func ValidatePages() []error {
-	var errs []error
-	hd := path.Join("templates", "header.tpl")
-	fd := path.Join("templates", "footer.tpl")
-	ld := path.Join("templates", "layout.tpl")
-
-	extra, err := os.Open(path.Join("templates", "plugins"))
-	if err != nil {
-		errs = append(errs, errors.New("Unable to locate templates/plugins"))
-		return errs
-	}
-
-	dirs, err := extra.Readdirnames(0)
-	if err != nil {
-		errs = append(errs, errors.New("Unable to read plugin dir"))
-	}
-
-	for n, dir := range dirs {
-		dirs[n] = path.Join("templates", "plugins", dir)
-	}
-
-	// TODO(halfwit) Validate our plugin templates here as well
-	dirs = append(dirs, hd, fd, ld)
-	printer := message.NewPrinter(message.MatchLanguage("en"))
-	for _, item := range pagecache {
-		var err error
-		tp := path.Join("templates", item.Path) + ".tpl"
-		t := template.New(path.Base(tp))
-		// TODO(halfwit) Contemplate only adding templates for plugins each page uses
-		item.tmpl, _ = t.ParseFiles(dirs...)
-		item.tmpl, err = t.ParseFiles(tp)
-		if err != nil {
-			errs = append(errs, fmt.Errorf("parsing in %s - %v", path.Dir(item.Path), err))
-			continue
-		}
-		p := &Request{
-			printer: printer,
-			path:    item.Path + ".html",
-			role:    database.PatientAuth | database.DoctorAuth | database.GuestAuth,
-		}
-
-		_, err = getData(p, "")
-		if err != nil {
-			errs = append(errs, err)
-		}
-	}
-	return errs
-}
-
-func RunPages() error {
-	printer := message.NewPrinter(message.MatchLanguage("en"))
-
-	for _, page := range pagecache {
-		data := page.Data(printer)
-		if len(data) < 1 {
-			return errors.New("no data from page")
-		}
-	}
-
-	return nil
-}
-
-func getpage(p *Request, w http.ResponseWriter) {
-	var data []byte
-	var err error
-	switch database.UserRole(p.user) {
-	case database.DoctorAuth:
-		data, err = getData(p, "doctor")
-	case database.PatientAuth:
-		data, err = getData(p, "patient")
-	default:
-		data, err = getData(p, "guest")
-	}
-	if err != nil && err.Error() == "Unauthorized" {
-		p.Session().Set("redirect", p.path)
-		http.Redirect(w, p.Request(), "/login.html", 302)
-		return
-	}
-	if err != nil {
-		http.Error(w, "Service Unavailable", 503)
-		return
-	}
-	fmt.Fprintf(w, "%s", data)
-}
-
-func getData(p *Request, in string) ([]byte, error) {
-	cache, ok := pagecache[p.path]
-	if !ok {
-		return nil, fmt.Errorf("No such page: %s", p.path)
-	}
-	if uint8(p.role)&uint8(cache.Access) == 0 {
-		return nil, errors.New("Unauthorized")
-	}
-	r := cache.Data(p.printer)
-	r["css"] = cache.CSS
-	r["header"] = header(p.printer, p.status)
-	r["footer"] = footer(p.printer)
-	r["basedir"] = getBaseDir(cache.Path)
-	// TODO(halfwit) Test chunking in to go routines if n gets too large
-	for _, key := range pluginKey {
-		if (cache.Extra&key) != 0 && pluginCache[key].Run != nil {
-			r[pluginCache[key].Name] = pluginCache[key].Run(p)
-		}
-	}
-	if p.session != nil {
-		r["username"] = p.session.Get("username")
-		if _, ok := p.session.Get("redirect").(string); ok {
-			r["redirect"] = true
-		}
-	}
-	return cache.render(r)
-}
-
-func (page *Page) render(i map[string]interface{}) ([]byte, error) {
-	var buf bytes.Buffer
-	data := bufio.NewWriter(&buf)
-	err := page.tmpl.ExecuteTemplate(data, "layout", i)
-	data.Flush()
-	return buf.Bytes(), err
-
-}
-
-func getBaseDir(fp string) string {
-	return strings.Repeat("../", strings.Count(fp, "/"))
-}
--- a/plugins.go
+++ /dev/null
@@ -1,49 +1,0 @@
-package router
-
-import (
-	"fmt"
-)
-
-// PluginMask - (Must be unique) ID for a plugin
-type PluginMask uint32
-
-// DEAD is a magic string to indicate a non-unique plugin key
-const DEAD PluginMask = 1
-
-var pluginCache map[PluginMask]*Plugin
-var pluginKey []PluginMask
-
-// Plugin - Provide extra data or functionality from GET/POST pages
-type Plugin struct {
-	Name     string
-	Run      func(p *Request) map[string]interface{}
-	Validate func(p *Request) error
-}
-
-func init() {
-	pluginCache = make(map[PluginMask]*Plugin)
-}
-
-// ValidatePlugins - Make sure that its mapping isn't redundant with any other
-// Plugins have external testing to validate they are correct
-func ValidatePlugins() []error {
-	errs := []error{}
-	for key, item := range pluginCache {
-		if item.Validate == nil {
-			continue
-		}
-		if (key & DEAD) != 0 {
-			errs = append(errs, fmt.Errorf("Error registering %s: Key requested already in use", item.Name))
-		}
-	}
-	return errs
-}
-
-// AddPlugin - Add Plugin to map by key
-func AddPlugin(p *Plugin, key PluginMask) {
-	if pluginCache[key] != nil {
-		key |= DEAD
-	}
-	pluginKey = append(pluginKey, key)
-	pluginCache[key] = p
-}
--- a/request.go
+++ /dev/null
@@ -1,35 +1,0 @@
-package router
-
-import (
-	"net/http"
-
-	"github.com/olmaxmedical/database"
-	"github.com/olmaxmedical/session"
-	"golang.org/x/text/message"
-)
-
-// Request represents an incoming GET/POST
-type Request struct {
-	printer *message.Printer
-	session session.Session
-	request *http.Request
-	user    string
-	status  string
-	path    string
-	role    database.Access
-}
-
-// Printer - returns the client's localized printer handler
-func (r *Request) Printer() *message.Printer {
-	return r.printer
-}
-
-// Session - returns the client's session
-func (r *Request) Session() session.Session {
-	return r.session
-}
-
-// Request - underlying http.Request for forms and such
-func (r *Request) Request() *http.Request {
-	return r.request
-}
--- /dev/null
+++ b/router/footer.go
@@ -1,0 +1,33 @@
+package router
+
+import (
+	"golang.org/x/text/message"
+)
+
+func footer(p *message.Printer) map[string]string {
+	return map[string]string{
+		"faq":      p.Sprintf("FAQ"),
+		"help":     p.Sprintf("Help"),
+		"banner":   p.Sprintf("Quality Healthcare"),
+		"pay":      p.Sprintf("Payment Methods"),
+		"fees":     p.Sprintf("Prices and Fees"),
+		"verify":   p.Sprintf("Verification"),
+		"appt":     p.Sprintf("Appointments"),
+		"legal":    p.Sprintf("Legal"),
+		"privacy":  p.Sprintf("Privacy Policy"),
+		"howworks": p.Sprint("How It Works"),
+		"contact":  p.Sprint("Contact Us"),
+		"pricing":  p.Sprint("Pricing"),
+		"catalog":  p.Sprint("Catalog"),
+		"appts":    p.Sprint("Appointments"),
+		"proc":     p.Sprint("Payment Procedures"),
+		"payments": p.Sprint("Payment Methods"),
+		"phone":    p.Sprint("Call toll free"),
+		"number":   p.Sprint("1(555)555-1234"),
+		"email":    p.Sprint("Email"),
+		"partHead": p.Sprintf("Work with us!"),
+		"partner":  p.Sprintf("Become A Partner"),
+		"provider": p.Sprint("Become A Provider"),
+		"copy":     p.Sprintf("Copyright 2017, 2018, 2019"),
+	}
+}
--- /dev/null
+++ b/router/forms.go
@@ -1,0 +1,66 @@
+package router
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/olmaxmedical/session"
+	"golang.org/x/text/message"
+)
+
+var formlist map[string]*Form
+
+// Form - POST requests
+type Form struct {
+	Access    Access
+	After     PluginMask
+	Path      string
+	Redirect  string
+	Validator func(r *http.Request, p *message.Printer) []string
+}
+
+func init() {
+	formlist = make(map[string]*Form)
+}
+
+// AddPost - Register a POST form from forms/
+func AddPost(f *Form) {
+	formlist[f.Path+".html"] = f
+}
+
+func parseForm(p *Request, w http.ResponseWriter, r *http.Request) (*Form, []string) {
+	var errors []string
+	form, ok := formlist[p.path]
+	if !ok {
+		errors = append(errors, "No such page")
+		return nil, errors
+	}
+	if errs := form.Validator(r, p.printer); len(errs) > 0 {
+		return nil, errs
+	}
+	for _, key := range pluginKey {
+		if (form.After&key) != 0 && pluginCache[key].Validate != nil {
+			if e := pluginCache[key].Validate(p); e != nil {
+				errors = append(errors, fmt.Sprint(e))
+				return nil, errors
+			}
+		}
+	}
+	return form, errors
+}
+
+func postform(p *Request, us session.Session, w http.ResponseWriter, r *http.Request) {
+	form, errors := parseForm(p, w, r)
+	if len(errors) > 0 && errors[0] != "nil" {
+		// NOTE(halfwit) this stashes previous entries, but does not work
+		// on multipart forms (with file uploads)
+		us.Set("errors", errors)
+		// Maybe store form args instead here in session
+		url := fmt.Sprintf("%s?%s", r.URL.String(), r.Form.Encode())
+		http.Redirect(w, r, url, 302)
+	}
+	if form != nil {
+		us.Set("errors", []string{})
+		http.Redirect(w, r, form.Redirect, 302)
+	}
+}
--- /dev/null
+++ b/router/handler.go
@@ -1,0 +1,145 @@
+package router
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/olmaxmedical/database"
+	"github.com/olmaxmedical/email"
+	"github.com/olmaxmedical/session"
+	"golang.org/x/text/message"
+)
+
+// Handle specific endpoints
+type handler struct {
+	manager *session.Manager
+}
+
+func (d *handler) logout(w http.ResponseWriter, r *http.Request) {
+	d.manager.Destroy(w, r)
+	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+	http.Redirect(w, r, "/index.html", 302)
+}
+
+func (d *handler) normal(w http.ResponseWriter, r *http.Request) {
+	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+	if r.URL.Path == "/" {
+		http.Redirect(w, r, "/index.html", 302)
+		return
+	}
+	user, status, us, role := d.getUser(w, r)
+	p := &Request{
+		printer: userLang(r),
+		status:  status,
+		request: r,
+		user:    user,
+		role:    role,
+		session: us,
+		path:    r.URL.Path[1:],
+	}
+
+	switch r.Method {
+	case "GET":
+		getpage(p, w)
+	case "POST":
+		postform(p, us, w, r)
+	}
+}
+
+// TODO: This will require actual client data from the database to populate the page
+func (d *handler) profile(w http.ResponseWriter, r *http.Request) {
+	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+	user, status, us, role := d.getUser(w, r)
+	if status == "false" {
+		http.Redirect(w, r, "/login.html", 302)
+		return
+	}
+	if rd, ok := us.Get("redirect").(string); ok {
+		us.Delete("redirect")
+		http.Redirect(w, r, "/"+rd, 302)
+		return
+	}
+	p := &Request{
+		printer: userLang(r),
+		status:  status,
+		session: us,
+		user:    user,
+		role:    role,
+	}
+	var data []byte
+	var err error
+	switch database.UserRole(user) {
+	case database.DoctorAuth:
+		if role != database.DoctorAuth {
+			http.Error(w, "Unauthorized", 401)
+			return
+		}
+		p.path = "doctor/profile.html"
+		data, err = getData(p, "doctor")
+	case database.PatientAuth:
+		if role != database.PatientAuth {
+			http.Error(w, "Unauthorized", 401)
+			return
+		}
+		p.path = "patient/profile.html"
+		data, err = getData(p, "patient")
+	default:
+		http.Error(w, "Forbidden", 403)
+		return
+	}
+	if err != nil {
+		http.Error(w, "Service Unavailable", 503)
+		return
+	}
+	fmt.Fprintf(w, "%s", data)
+}
+
+func (d *handler) activate(w http.ResponseWriter, r *http.Request) {
+	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+	if len(r.URL.Path) != 46 && r.URL.Path[:9] != "/activate" {
+		http.Error(w, "Bad Request", 400)
+		return
+	}
+	email.ValidateSignupToken(w, r, r.URL.Path[10:])
+}
+
+func (d *handler) reset(w http.ResponseWriter, r *http.Request) {
+	if len(r.URL.Path) != 43 && r.URL.Path[:6] != "/reset" {
+		http.Error(w, "Bad Request", 400)
+		return
+	}
+	p := userLang(r)
+	user, _, us, _ := d.getUser(w, r)
+	token := email.NextResetToken(r.URL.Path[7:], user)
+	if token == "" {
+		us.Set("errors", [1]string{p.Sprint("Token expired")})
+		return
+	}
+	us.Set("token", token)
+	r.URL.Path = "/newpassword.html"
+	d.normal(w, r)
+}
+
+func (d *handler) getUser(w http.ResponseWriter, r *http.Request) (string, string, session.Session, database.Access) {
+	us := d.manager.Start(w, r)
+	user, ok1 := us.Get("username").(string)
+	status, ok2 := us.Get("login").(string)
+	role, ok3 := us.Get("role").(database.Access)
+	if !ok1 || !ok2 || status != "true" {
+		status = "false"
+	}
+	if !ok3 {
+		role = database.GuestAuth
+	}
+	if status == "true" {
+		us.Set("token", database.NewToken())
+	}
+	return user, status, us, role
+}
+
+func userLang(r *http.Request) *message.Printer {
+	accept := r.Header.Get("Accept-Language")
+	lang := r.FormValue("lang")
+	tag := message.MatchLanguage(lang, accept)
+	return message.NewPrinter(tag)
+}
--- /dev/null
+++ b/router/header.go
@@ -1,0 +1,16 @@
+package router
+
+import (
+	"golang.org/x/text/message"
+)
+
+func header(p *message.Printer, status string) map[string]string {
+	return map[string]string{
+		"home":    p.Sprint("Home"),
+		"login":   p.Sprint("Login"),
+		"logout":  p.Sprint("Logout"),
+		"signup":  p.Sprint("Sign Up"),
+		"profile": p.Sprint("Profile"),
+		"status":  status,
+	}
+}
--- /dev/null
+++ b/router/pages.go
@@ -1,0 +1,176 @@
+package router
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"html/template"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+
+	"github.com/olmaxmedical/database"
+	"golang.org/x/text/message"
+)
+
+var pagecache map[string]*Page
+
+func init() {
+	pagecache = make(map[string]*Page)
+}
+
+// Access defines the access rights for a specific page
+type Access uint8
+
+const (
+	// GuestAuth - non registered user
+	GuestAuth Access = 1 << iota
+	// PatientAuth - normal user, added by registration process
+	PatientAuth
+	// DoctorAuth - manually added to the database
+	DoctorAuth
+)
+
+// Page defines what a client receives from a GET request
+type Page struct {
+	Access Access
+	Extra  PluginMask
+	CSS    string
+	Path   string
+	Data   func(p *message.Printer) map[string]interface{}
+	tmpl   *template.Template
+}
+
+// AddPage - register a *Page to the cache
+func AddPage(p *Page) {
+	pagecache[p.Path+".html"] = p
+}
+
+// ValidatePages - Walk all our templates, test them, and finally return applicable errors as an array
+func ValidatePages() []error {
+	var errs []error
+	hd := path.Join("templates", "header.tpl")
+	fd := path.Join("templates", "footer.tpl")
+	ld := path.Join("templates", "layout.tpl")
+
+	extra, err := os.Open(path.Join("templates", "plugins"))
+	if err != nil {
+		errs = append(errs, errors.New("Unable to locate templates/plugins"))
+		return errs
+	}
+
+	dirs, err := extra.Readdirnames(0)
+	if err != nil {
+		errs = append(errs, errors.New("Unable to read plugin dir"))
+	}
+
+	for n, dir := range dirs {
+		dirs[n] = path.Join("templates", "plugins", dir)
+	}
+
+	// TODO(halfwit) Validate our plugin templates here as well
+	dirs = append(dirs, hd, fd, ld)
+	printer := message.NewPrinter(message.MatchLanguage("en"))
+	for _, item := range pagecache {
+		var err error
+		tp := path.Join("templates", item.Path) + ".tpl"
+		t := template.New(path.Base(tp))
+		// TODO(halfwit) Contemplate only adding templates for plugins each page uses
+		item.tmpl, _ = t.ParseFiles(dirs...)
+		item.tmpl, err = t.ParseFiles(tp)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("parsing in %s - %v", path.Dir(item.Path), err))
+			continue
+		}
+		p := &Request{
+			printer: printer,
+			path:    item.Path + ".html",
+			role:    database.PatientAuth | database.DoctorAuth | database.GuestAuth,
+		}
+
+		_, err = getData(p, "")
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+	return errs
+}
+
+func RunPages() error {
+	printer := message.NewPrinter(message.MatchLanguage("en"))
+
+	for _, page := range pagecache {
+		data := page.Data(printer)
+		if len(data) < 1 {
+			return errors.New("no data from page")
+		}
+	}
+
+	return nil
+}
+
+func getpage(p *Request, w http.ResponseWriter) {
+	var data []byte
+	var err error
+	switch database.UserRole(p.user) {
+	case database.DoctorAuth:
+		data, err = getData(p, "doctor")
+	case database.PatientAuth:
+		data, err = getData(p, "patient")
+	default:
+		data, err = getData(p, "guest")
+	}
+	if err != nil && err.Error() == "Unauthorized" {
+		p.Session().Set("redirect", p.path)
+		http.Redirect(w, p.Request(), "/login.html", 302)
+		return
+	}
+	if err != nil {
+		http.Error(w, "Service Unavailable", 503)
+		return
+	}
+	fmt.Fprintf(w, "%s", data)
+}
+
+func getData(p *Request, in string) ([]byte, error) {
+	cache, ok := pagecache[p.path]
+	if !ok {
+		return nil, fmt.Errorf("No such page: %s", p.path)
+	}
+	if uint8(p.role)&uint8(cache.Access) == 0 {
+		return nil, errors.New("Unauthorized")
+	}
+	r := cache.Data(p.printer)
+	r["css"] = cache.CSS
+	r["header"] = header(p.printer, p.status)
+	r["footer"] = footer(p.printer)
+	r["basedir"] = getBaseDir(cache.Path)
+	// TODO(halfwit) Test chunking in to go routines if n gets too large
+	for _, key := range pluginKey {
+		if (cache.Extra&key) != 0 && pluginCache[key].Run != nil {
+			r[pluginCache[key].Name] = pluginCache[key].Run(p)
+		}
+	}
+	if p.session != nil {
+		r["username"] = p.session.Get("username")
+		if _, ok := p.session.Get("redirect").(string); ok {
+			r["redirect"] = true
+		}
+	}
+	return cache.render(r)
+}
+
+func (page *Page) render(i map[string]interface{}) ([]byte, error) {
+	var buf bytes.Buffer
+	data := bufio.NewWriter(&buf)
+	err := page.tmpl.ExecuteTemplate(data, "layout", i)
+	data.Flush()
+	return buf.Bytes(), err
+
+}
+
+func getBaseDir(fp string) string {
+	return strings.Repeat("../", strings.Count(fp, "/"))
+}
--- /dev/null
+++ b/router/plugins.go
@@ -1,0 +1,49 @@
+package router
+
+import (
+	"fmt"
+)
+
+// PluginMask - (Must be unique) ID for a plugin
+type PluginMask uint32
+
+// DEAD is a magic string to indicate a non-unique plugin key
+const DEAD PluginMask = 1
+
+var pluginCache map[PluginMask]*Plugin
+var pluginKey []PluginMask
+
+// Plugin - Provide extra data or functionality from GET/POST pages
+type Plugin struct {
+	Name     string
+	Run      func(p *Request) map[string]interface{}
+	Validate func(p *Request) error
+}
+
+func init() {
+	pluginCache = make(map[PluginMask]*Plugin)
+}
+
+// ValidatePlugins - Make sure that its mapping isn't redundant with any other
+// Plugins have external testing to validate they are correct
+func ValidatePlugins() []error {
+	errs := []error{}
+	for key, item := range pluginCache {
+		if item.Validate == nil {
+			continue
+		}
+		if (key & DEAD) != 0 {
+			errs = append(errs, fmt.Errorf("Error registering %s: Key requested already in use", item.Name))
+		}
+	}
+	return errs
+}
+
+// AddPlugin - Add Plugin to map by key
+func AddPlugin(p *Plugin, key PluginMask) {
+	if pluginCache[key] != nil {
+		key |= DEAD
+	}
+	pluginKey = append(pluginKey, key)
+	pluginCache[key] = p
+}
--- /dev/null
+++ b/router/request.go
@@ -1,0 +1,35 @@
+package router
+
+import (
+	"net/http"
+
+	"github.com/olmaxmedical/database"
+	"github.com/olmaxmedical/session"
+	"golang.org/x/text/message"
+)
+
+// Request represents an incoming GET/POST
+type Request struct {
+	printer *message.Printer
+	session session.Session
+	request *http.Request
+	user    string
+	status  string
+	path    string
+	role    database.Access
+}
+
+// Printer - returns the client's localized printer handler
+func (r *Request) Printer() *message.Printer {
+	return r.printer
+}
+
+// Session - returns the client's session
+func (r *Request) Session() session.Session {
+	return r.session
+}
+
+// Request - underlying http.Request for forms and such
+func (r *Request) Request() *http.Request {
+	return r.request
+}
--- /dev/null
+++ b/router/run.go
@@ -1,0 +1,35 @@
+package router
+
+import (
+	"crypto/tls"
+	"net/http"
+
+	"github.com/olmaxmedical/session"
+)
+
+// Route - All requests pass through here first
+func Route(manager *session.Manager) error {
+	d := &handler{
+		manager: manager,
+	}
+	css := http.FileServer(http.Dir("resources/css/"))
+	jss := http.FileServer(http.Dir("resources/scripts"))
+	img := http.FileServer(http.Dir("resources/images/"))
+	mux := http.NewServeMux()
+	mux.Handle("/css/", http.StripPrefix("/css/", css))
+	mux.Handle("/scripts/", http.StripPrefix("/scripts/", jss))
+	mux.Handle("/images/", http.StripPrefix("/images/", img))
+	mux.HandleFunc("/activate/", d.activate)
+	mux.HandleFunc("/reset/", d.reset)
+	mux.HandleFunc("/logout.html", d.logout)
+	mux.HandleFunc("/profile.html", d.profile)
+	mux.HandleFunc("/", d.normal)
+	//from https://github.com/denji/golang-tls (creative commons)
+	srv := &http.Server{
+		Addr:         ":8443",
+		Handler:      mux,
+		TLSConfig:    getTlsConfig(),
+		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
+	}
+	return srv.ListenAndServeTLS("cert.pem", "key.pem")
+}
--- /dev/null
+++ b/router/tls.go
@@ -1,0 +1,21 @@
+package router
+
+import (
+	"crypto/tls"
+)
+
+func getTlsConfig() *tls.Config {
+	tlsConfig := &tls.Config{
+		MinVersion:               tls.VersionTLS12,
+		CurvePreferences:         []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
+		PreferServerCipherSuites: true,
+		CipherSuites: []uint16{
+			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+			tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+		},
+	}
+	tlsConfig.BuildNameToCertificate()
+	return tlsConfig
+}
--- a/run.go
+++ /dev/null
@@ -1,35 +1,0 @@
-package router
-
-import (
-	"crypto/tls"
-	"net/http"
-
-	"github.com/olmaxmedical/session"
-)
-
-// Route - All requests pass through here first
-func Route(manager *session.Manager) error {
-	d := &handler{
-		manager: manager,
-	}
-	css := http.FileServer(http.Dir("resources/css/"))
-	jss := http.FileServer(http.Dir("resources/scripts"))
-	img := http.FileServer(http.Dir("resources/images/"))
-	mux := http.NewServeMux()
-	mux.Handle("/css/", http.StripPrefix("/css/", css))
-	mux.Handle("/scripts/", http.StripPrefix("/scripts/", jss))
-	mux.Handle("/images/", http.StripPrefix("/images/", img))
-	mux.HandleFunc("/activate/", d.activate)
-	mux.HandleFunc("/reset/", d.reset)
-	mux.HandleFunc("/logout.html", d.logout)
-	mux.HandleFunc("/profile.html", d.profile)
-	mux.HandleFunc("/", d.normal)
-	//from https://github.com/denji/golang-tls (creative commons)
-	srv := &http.Server{
-		Addr:         ":8443",
-		Handler:      mux,
-		TLSConfig:    getTlsConfig(),
-		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
-	}
-	return srv.ListenAndServeTLS("cert.pem", "key.pem")
-}
--- a/tls.go
+++ /dev/null
@@ -1,21 +1,0 @@
-package router
-
-import (
-	"crypto/tls"
-)
-
-func getTlsConfig() *tls.Config {
-	tlsConfig := &tls.Config{
-		MinVersion:               tls.VersionTLS12,
-		CurvePreferences:         []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
-		PreferServerCipherSuites: true,
-		CipherSuites: []uint16{
-			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
-			tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
-			tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
-			tls.TLS_RSA_WITH_AES_256_CBC_SHA,
-		},
-	}
-	tlsConfig.BuildNameToCertificate()
-	return tlsConfig
-}