ref: 05d82d371ca4a2c56aaccdf65899e02b356a7950
author: halfwit <michaelmisch1985@gmail.com>
date: Sat Mar 28 07:39:46 PDT 2020
move router
--- /dev/null
+++ b/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/forms.go
@@ -1,0 +1,66 @@
+package router
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/olmaxmedical/olmax_go/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/handler.go
@@ -1,0 +1,145 @@
+package router
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/olmaxmedical/olmax_go/db"
+ "github.com/olmaxmedical/olmax_go/email"
+ "github.com/olmaxmedical/olmax_go/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 db.UserRole(user) {
+ case db.DoctorAuth:
+ if role != db.DoctorAuth {
+ http.Error(w, "Unauthorized", 401)
+ return
+ }
+ p.path = "doctor/profile.html"
+ data, err = getData(p, "doctor")
+ case db.PatientAuth:
+ if role != db.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, db.Access) {
+ us := d.manager.Start(w, r)
+ user, ok1 := us.Get("username").(string)
+ status, ok2 := us.Get("login").(string)
+ role, ok3 := us.Get("role").(db.Access)
+ if !ok1 || !ok2 || status != "true" {
+ status = "false"
+ }
+ if !ok3 {
+ role = db.GuestAuth
+ }
+ if status == "true" {
+ us.Set("token", db.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/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/pages.go
@@ -1,0 +1,155 @@
+package router
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "html/template"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+
+ "github.com/olmaxmedical/olmax_go/db"
+ "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)
+ 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: db.PatientAuth | db.DoctorAuth | db.GuestAuth,
+ }
+ _, err = getData(p, "")
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+ return errs
+}
+
+func getpage(p *Request, w http.ResponseWriter) {
+ var data []byte
+ var err error
+ switch db.UserRole(p.user) {
+ case db.DoctorAuth:
+ data, err = getData(p, "doctor")
+ case db.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/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/request.go
@@ -1,0 +1,35 @@
+package router
+
+import (
+ "net/http"
+
+ "github.com/olmaxmedical/olmax_go/db"
+ "github.com/olmaxmedical/olmax_go/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 db.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/run.go
@@ -1,0 +1,35 @@
+package router
+
+import (
+ "crypto/tls"
+ "net/http"
+
+ "github.com/olmaxmedical/olmax_go/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/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
+}