package main import ( "database/sql" "errors" "fmt" "html/template" "io" "log" "net/http" "os" "path" "strings" "time" ) func baseLoadTemplate(base string, path string) (*template.Template, any) { return template.New(base).ParseFiles( "./views/"+base, "./views/"+path, "./views/partials/header.html", ) } func loadTemplate(path string) (*template.Template, any) { return baseLoadTemplate("layout.html", path) } func LoadView(writer http.ResponseWriter, path string, data interface{}) { tmpl, err := loadTemplate(path) if err != nil { fmt.Printf("Failed to load view %s\n", path) fmt.Println(err) if path == "500.html" { writer.Write([]byte("

Failed to load 500.html check console for more info

")) } else { LoadView(writer, "500.html", nil) } return } if err := tmpl.Execute(writer, data); err != nil { fmt.Printf("Failed to load view %s\n", path) fmt.Println(err) writer.WriteHeader(http.StatusInternalServerError) if path == "500.html" { writer.Write([]byte("

Failed to load 500.html check console for more info

")) } else { LoadView(writer, "500.html", nil) } return } } /** Only returns the html without template */ func LoadHtml(writer http.ResponseWriter, path string, data interface{}) { tmpl, err := baseLoadTemplate("html.html", path) if err != nil { fmt.Printf("Failed to load template %s\n", path) fmt.Println(err) writer.WriteHeader(http.StatusInternalServerError) if path == "500.html" { writer.Write([]byte("

Failed to load 500.html check console for more info

")) } else { LoadHtml(writer, "500.html", nil) } return } if err := tmpl.Execute(writer, data); err != nil { fmt.Printf("Failed to execute template %s\n", path) fmt.Println(err) writer.WriteHeader(http.StatusInternalServerError) if path == "500.html" { writer.Write([]byte("

Failed to load 500.html check console for more info

")) } else { LoadHtml(writer, "500.html", nil) } return } } func LoadError(writer http.ResponseWriter, path string, base string, data AnyMap) { if data == nil { data = map[string]interface{} { "Error": true, } } else { data["Error"] = true } tmpl, err := template.New("").Parse("{{template \"" + base + "\" . }}") if err != nil { panic("Lol") } tmpl, err = tmpl.ParseFiles( "./views/"+path, "./views/partials/header.html", ) if err != nil { fmt.Printf("Failed to load template %s\n", path) fmt.Println(err) writer.WriteHeader(http.StatusInternalServerError) if path == "500.html" { writer.Write([]byte("

Failed to load 500.html check console for more info

")) } else { LoadHtml(writer, "500.html", nil) } return } if err := tmpl.Execute(writer, data); err != nil { fmt.Printf("Failed to execute template %s\n", path) fmt.Println(err) writer.WriteHeader(http.StatusInternalServerError) if path == "500.html" { writer.Write([]byte("

Failed to load 500.html check console for more info

")) } else { LoadHtml(writer, "500.html", nil) } return } } type AnyMap = map[string]interface{} type Error struct { code int msg *string data AnyMap } type AnswerType int const ( NORMAL AnswerType = 1 << iota HTML JSON HTMLFULL ) func LoadBasedOnAnswer(ans AnswerType, w http.ResponseWriter, path string, data map[string]interface{}) { if ans == NORMAL { LoadView(w, path, data) return } else if ans == HTML { LoadHtml(w, path, data) return } else if ans == HTMLFULL { if data == nil { LoadHtml(w, path, map[string]interface{}{ "App": true, }) } else { data["App"] = true LoadHtml(w, path, data) } return } else if ans == JSON { panic("TODO JSON!") } else { panic("unreachable") } } type HandleFunc struct { path string mode AnswerType fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error } type Handler interface { New() Startup() Get(fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) Post(fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) } type Handle struct { db *sql.DB gets []HandleFunc posts []HandleFunc deletes []HandleFunc } func decodeBody(r *http.Request) (string, *Error) { body, err := io.ReadAll(r.Body) if err == nil { return "", &Error{code: http.StatusBadRequest} } return string(body[:]), nil } func handleError(err *Error, w http.ResponseWriter, context *Context) { if err != nil { data := context.addMap(err.data) w.WriteHeader(err.code) if err.code == http.StatusNotFound { LoadBasedOnAnswer(context.Mode, w, "404.html", data) return } if err.code == http.StatusBadRequest { LoadBasedOnAnswer(context.Mode, w, "400.html", data) return } if err.msg != nil { w.Write([]byte(*err.msg)) } } } func (x *Handle) Get(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.gets = append(x.gets, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn}) } func (x *Handle) GetHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.gets = append(x.gets, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn}) } func (x *Handle) GetJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.gets = append(x.gets, HandleFunc{path, JSON, fn}) } func (x *Handle) Post(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.posts = append(x.posts, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn}) } func (x *Handle) PostHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.posts = append(x.posts, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn}) } func (x *Handle) PostJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.posts = append(x.posts, HandleFunc{path, JSON, fn}) } func (x *Handle) Delete(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.deletes = append(x.deletes, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn}) } func (x *Handle) DeleteHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.deletes = append(x.deletes, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn}) } func (x *Handle) DeleteJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { x.deletes = append(x.deletes, HandleFunc{path, JSON, fn}) } func (x *Handle) handleGets(w http.ResponseWriter, r *http.Request, context *Context) { for _, s := range x.gets { if s.path == r.URL.Path && context.Mode&s.mode != 0 { handleError(s.fn(w, r, context), w, context) return } } if context.Mode != HTMLFULL { w.WriteHeader(http.StatusNotFound) } LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil)) } func (x *Handle) handlePosts(w http.ResponseWriter, r *http.Request, context *Context) { for _, s := range x.posts { if s.path == r.URL.Path && context.Mode&s.mode != 0 { handleError(s.fn(w, r, context), w, context) return } } if context.Mode != HTMLFULL { w.WriteHeader(http.StatusNotFound) } LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil)) } func (x *Handle) handleDeletes(w http.ResponseWriter, r *http.Request, context *Context) { for _, s := range x.deletes { if s.path == r.URL.Path && context.Mode&s.mode != 0 { handleError(s.fn(w, r, context), w, context) return } } if context.Mode != HTMLFULL { w.WriteHeader(http.StatusNotFound) } LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil)) } func checkAuthLevel(authLevel int, w http.ResponseWriter, r *http.Request, c *Context) bool { if authLevel > 0 { if c.requireAuth(w, r) { logoff(c.Mode, w, r) return false } if c.User.user_type < authLevel { notAuth(c.Mode, w, r) return false } } return true } func AnswerTemplate(path string, data AnyMap, authLevel int) func(w http.ResponseWriter, r *http.Request, c *Context) *Error { return func(w http.ResponseWriter, r *http.Request, c *Context) *Error { if !checkAuthLevel(authLevel, w, r, c) { return nil } LoadBasedOnAnswer(c.Mode, w, path, c.addMap(data)) return nil } } type Context struct { Token *string User *User Mode AnswerType } func (c Context) addMap(m AnyMap) AnyMap { if m == nil { return map[string]interface{}{ "Context": c, } } m["Context"] = c return m } func (c *Context) requireAuth(w http.ResponseWriter, r *http.Request) bool { if c.User == nil { return true } return false } var LogoffError = errors.New("Invalid token!") func (x Handle) createContext(mode AnswerType, r *http.Request) (*Context, error) { var token *string for _, r := range r.Cookies() { if r.Name == "auth" { token = &r.Value } } // TODO check that the token is still valid if token == nil { return &Context{ Mode: mode, }, nil } user, err := userFromToken(x.db, *token) if err != nil { return nil, errors.Join(err, LogoffError) } return &Context{token, user, mode}, nil } // TODO check if I can use http.Redirect func redirect(path string, mode AnswerType, w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", path) if mode == JSON { w.WriteHeader(http.StatusSeeOther) w.Write([]byte(path)) return } if mode&(HTMLFULL|HTML) != 0 { w.WriteHeader(http.StatusSeeOther) w.Write([]byte(path)) } else { w.WriteHeader(http.StatusSeeOther) } } func logoff(mode AnswerType, w http.ResponseWriter, r *http.Request) { // Delete cookie cookie := &http.Cookie{ Name: "auth", Value: "", Expires: time.Unix(0, 0), } http.SetCookie(w, cookie) redirect("/login", mode, w, r) } func notAuth(mode AnswerType, w http.ResponseWriter, r *http.Request) { if mode == JSON { w.WriteHeader(http.StatusForbidden) w.Write([]byte("\"You can not access this resource!\"")) return } if mode&(HTMLFULL|HTML) != 0 { w.WriteHeader(http.StatusForbidden) w.Write([]byte("You can not access this resource!")) } else { w.WriteHeader(http.StatusForbidden) } } func (x Handle) staticFiles(pathTest string, fileType string, contentType string) { http.HandleFunc(pathTest, func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path[len(pathTest):] if !strings.HasSuffix(path, fileType) { w.WriteHeader(http.StatusNotFound) w.Write([]byte("File not found")) return } t, err := template.ParseFiles("./views" + pathTest + path) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Failed to load template")) return } w.Header().Set("Content-Type", contentType+"; charset=utf-8") t.Execute(w, nil) }) } func errorCode(err error, code int, data AnyMap) *Error { // TODO Improve Logging if err != nil { fmt.Printf("Something went wrong returning with: %d\n.Err:\n", code) fmt.Println(err) } return &Error{code, nil, data} } func error500(err error) *Error { return errorCode(err, http.StatusInternalServerError, nil) } func (x Handle) readFiles(pathTest string, baseFilePath string, fileType string, contentType string) { http.HandleFunc(pathTest, func(w http.ResponseWriter, r *http.Request) { user_path := r.URL.Path[len(pathTest):] fmt.Printf("Requested path: %s\n", user_path) if !strings.HasSuffix(user_path, fileType) { w.WriteHeader(http.StatusNotFound) w.Write([]byte("File not found")) return } bytes, err := os.ReadFile(path.Join(baseFilePath, pathTest, user_path)) if err != nil { fmt.Println(err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Failed to load file")) return } w.Header().Set("Content-Type", contentType) w.Write(bytes) }) } func NewHandler(db *sql.DB) *Handle { var gets []HandleFunc var posts []HandleFunc var deletes []HandleFunc x := &Handle{db, gets, posts, deletes} http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Decide answertype ans := NORMAL if r.Header.Get("Request-Type") == "htmlfull" { ans = HTMLFULL } if r.Header.Get("Request-Type") == "html" { ans = HTML } //TODO JSON //Login state context, err := x.createContext(ans, r) if err != nil { logoff(ans, w, r) return } if r.Method == "GET" { x.handleGets(w, r, context) return } if r.Method == "POST" { x.handlePosts(w, r, context) return } if r.Method == "DELETE" { x.handleDeletes(w, r, context) return } panic("TODO handle method: " + r.Method) }) return x } func (x Handle) Startup() { fmt.Printf("Starting up!\n") log.Fatal(http.ListenAndServe(":8000", nil)) }