oef/handlers/handlers.go
2020-01-02 09:48:20 +01:00

409 lines
9.6 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"path"
"path/filepath"
"reflect"
"runtime/debug"
"strconv"
"strings"
"git.andreafazzi.eu/andrea/oef/config"
"git.andreafazzi.eu/andrea/oef/i18n"
"git.andreafazzi.eu/andrea/oef/orm"
"git.andreafazzi.eu/andrea/oef/renderer"
jwt "github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
"github.com/jinzhu/inflection"
)
type PathPattern struct {
PathPattern string
RedirectPattern string
Methods []string
Permission int
}
var (
permissions map[string]map[string]bool
)
func init() {
permissions = make(map[string]map[string]bool, 0)
}
func (pp PathPattern) RedirectPath(model string, id ...uint) string {
if len(id) > 0 {
return fmt.Sprintf(pp.RedirectPattern, model, id[0], model)
}
return fmt.Sprintf(pp.RedirectPattern, model, model)
}
func (pp PathPattern) Path(model string) string {
return fmt.Sprintf(pp.PathPattern, model)
}
func modelName(s interface{}) string {
if t := reflect.TypeOf(s); t.Kind() == reflect.Ptr {
return t.Elem().Name()
} else {
return t.Name()
}
}
func pluralizedModelName(s interface{}) string {
return inflection.Plural(strings.ToLower(modelName(s)))
}
// Generate CRUD handlers for models
func generateHandler(r *mux.Router, model interface{}) {
var (
patterns []PathPattern = []PathPattern{
PathPattern{"/%s", "", []string{"GET"}, PermissionReadAll},
PathPattern{"/%s/{id}", "", []string{"GET"}, PermissionRead},
PathPattern{"/%s/create/", "/%s/%d?format=html&tpl_layout=base&tpl_content=%s_show", []string{"GET", "POST"}, PermissionCreate},
PathPattern{"/%s/{id}/update", "/%s/%d?format=html&tpl_layout=base&tpl_content=%s_show", []string{"GET", "POST"}, PermissionUpdate},
PathPattern{"/%s/{id}/delete", "/%s?format=html&tpl_layout=base&tpl_content=%s", []string{"DELETE"}, PermissionDelete},
}
apiPatterns []PathPattern
)
// Generate API patterns prefixing "api" path
for _, p := range patterns {
apiPatterns = append(apiPatterns, PathPattern{path.Join("/", "api", p.PathPattern), "", p.Methods, p.Permission})
}
// Install standard paths
for _, pattern := range patterns {
r.Handle(
pattern.Path(
pluralizedModelName(model),
),
jwtCookie.Handler(
recoverHandler(
modelHandler(
pluralizedModelName(model),
pattern,
)))).Methods(pattern.Methods...)
}
// Install API paths
for _, pattern := range apiPatterns {
r.Handle(pattern.Path(
pluralizedModelName(model),
),
jwtHeader.Handler(
recoverHandler(
modelHandler(
pluralizedModelName(model),
pattern,
)))).Methods(pattern.Methods...)
}
// Set permissions for HTML patterns
for role, modelPermissions := range RolePermissions {
for m, perm := range modelPermissions {
if m == modelName(model) {
for _, p := range perm {
for _, pattern := range patterns {
if pattern.Permission == p {
if permissions[role] == nil {
permissions[role] = make(map[string]bool)
}
permissions[role][pattern.Path(pluralizedModelName(model))] = true
}
}
for _, pattern := range apiPatterns {
if pattern.Permission == p {
if permissions[role] == nil {
permissions[role] = make(map[string]bool)
}
permissions[role][pattern.Path(pluralizedModelName(model))] = true
}
}
}
}
}
}
}
func Handlers(models []interface{}) *mux.Router {
r := mux.NewRouter()
// Authentication
r.Handle("/login", loginHandler())
r.Handle("/logout", logoutHandler())
// School subscription
r.Handle("/subscribe", loginHandler())
// Dashboard
r.Handle("/", jwtCookie.Handler(recoverHandler(homeHandler())))
// Generate CRUD handlers
for _, model := range models {
generateHandler(r, model)
}
// Token handling
r.Handle("/get_token", tokenHandler())
// Static file server
r.PathPrefix("/").Handler(http.FileServer(http.Dir("./dist/")))
return r
}
func onError(w http.ResponseWriter, r *http.Request, err string) {
http.Redirect(w, r, "/login?tpl_layout=login&tpl_content=login", http.StatusTemporaryRedirect)
}
func respondWithStaticFile(w http.ResponseWriter, filename string) error {
f, err := ioutil.ReadFile(filepath.Join("public/html", filename))
if err != nil {
return err
}
w.Write(f)
return nil
}
func fromCookie(r *http.Request) (string, error) {
session, err := store.Get(r, "login-session")
if err != nil {
return "", nil
}
if session.Values["token"] == nil {
return "", nil
}
token := session.Values["token"].([]uint8)
return string(token), nil
}
func recoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
panicMsg := fmt.Sprintf("PANIC: %v\n\n== STACKTRACE ==\n%s", err, debug.Stack())
log.Print(panicMsg)
http.Error(w, panicMsg, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func setFlashMessage(w http.ResponseWriter, r *http.Request, key string) error {
session, err := store.Get(r, "flash-session")
if err != nil {
return err
}
session.AddFlash(i18n.FlashMessages[key][config.Config.Language])
err = session.Save(r, w)
if err != nil {
return err
}
return nil
}
func hasPermission(role, path string) bool {
if permissions[role] == nil {
return false
}
return permissions[role][path]
}
func get(w http.ResponseWriter, r *http.Request, model string, pattern PathPattern) {
format := r.URL.Query().Get("format")
getFn, err := orm.GetFunc(pattern.Path(model))
if err != nil {
log.Println("Error:", err)
respondWithError(w, r, err)
} else {
claims := r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)
role := claims["role"].(string)
if !hasPermission(role, pattern.Path(model)) {
setFlashMessage(w, r, "notAuthorized")
renderer.Render[format](w, r, fmt.Errorf("%s", "Errore di autorizzazione"))
} else {
data, err := getFn(mux.Vars(r), w, r)
if err != nil {
renderer.Render[format](w, r, err)
} else {
renderer.Render[format](w, r, data, r.URL.Query())
}
}
}
}
func post(w http.ResponseWriter, r *http.Request, model string, pattern PathPattern) {
var (
data interface{}
err error
)
respFormat := renderer.GetContentFormat(r)
postFn, err := orm.GetFunc(pattern.Path(model))
if err != nil {
respondWithError(w, r, err)
} else {
claims := r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)
role := claims["role"].(string)
if !hasPermission(role, pattern.Path(model)) {
renderer.Render[respFormat](w, r, fmt.Errorf("%s", "Errore di autorizzazione"))
} else {
data, err = postFn(mux.Vars(r), w, r)
if err != nil {
respondWithError(w, r, err)
} else if pattern.RedirectPattern != "" {
if id := mux.Vars(r)["id"]; id != "" {
modelId, _ := strconv.Atoi(id)
http.Redirect(w, r, pattern.RedirectPath(model, uint(modelId)), http.StatusSeeOther)
} else {
http.Redirect(w, r, pattern.RedirectPath(model, data.(orm.IDer).GetID()), http.StatusSeeOther)
}
} else {
renderer.Render[respFormat](w, r, data.(orm.IDer).GetID())
}
}
}
}
func delete(w http.ResponseWriter, r *http.Request, model string, pattern PathPattern) {
var data interface{}
respFormat := renderer.GetContentFormat(r)
claims := r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)
role := claims["role"].(string)
if !hasPermission(role, pattern.Path(model)) {
renderer.Render[respFormat](w, r, fmt.Errorf("%s", "Errore di autorizzazione"))
} else {
postFn, err := orm.GetFunc(pattern.Path(model))
if err != nil {
renderer.Render[r.URL.Query().Get("format")](w, r, err)
}
data, err = postFn(mux.Vars(r), w, r)
if err != nil {
renderer.Render["html"](w, r, err)
} else if pattern.RedirectPattern != "" {
var data struct {
RedirectUrl string `json:"redirect_url"`
}
data.RedirectUrl = pattern.RedirectPath(model)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
} else {
renderer.Render[respFormat](w, r, data.(orm.IDer).GetID())
}
}
}
func respondWithError(w http.ResponseWriter, r *http.Request, err error) {
respFormat := renderer.GetContentFormat(r)
w.WriteHeader(http.StatusInternalServerError)
renderer.Render[respFormat](w, r, err)
}
func modelHandler(model string, pattern PathPattern) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// Replace "api" prefix
pattern.PathPattern = strings.Replace(pattern.PathPattern, "/api", "", -1)
switch r.Method {
case "GET":
get(w, r, model, pattern)
case "POST":
post(w, r, model, pattern)
case "DELETE":
delete(w, r, model, pattern)
}
}
return http.HandlerFunc(fn)
}
func homeHandler() http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)
switch claims["role"] {
case "subscriber":
http.Redirect(
w,
r,
fmt.Sprintf(
"/schools/create/?format=html&tpl_layout=base&tpl_content=schools_add_update",
),
http.StatusSeeOther,
)
case "participant":
http.Redirect(
w,
r,
fmt.Sprintf(
"/participants/%s?format=html&tpl_layout=base&tpl_content=participants_show",
claims["user_id"].(string)),
http.StatusSeeOther,
)
case "school":
http.Redirect(
w,
r,
fmt.Sprintf(
"/schools/%s?format=html&tpl_layout=base&tpl_content=schools_show",
claims["user_id"].(string)),
http.StatusSeeOther,
)
default:
http.Redirect(
w,
r,
"/contests?format=html&tpl_layout=base&tpl_content=contests",
http.StatusSeeOther,
)
}
}
return http.HandlerFunc(fn)
}