package utils import ( "database/sql" "errors" "fmt" "html/template" "io" "net/http" "os" "path" "strings" "time" dbtypes "git.andr3h3nriqu3s.com/andr3/fyp/logic/db_types" "github.com/charmbracelet/log" ) func Mul(n1 int, n2 int) int { return n1 * n2 } func Add(n1 int, n2 int) int { return n1 + n2 } func baseLoadTemplate(base string, path string) (*template.Template, any) { funcs := map[string]any{ "startsWith": strings.HasPrefix, "replace": strings.Replace, "mul": Mul, "add": Add, } return template.New(base).Funcs(funcs).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 LoadDefineTemplate(writer http.ResponseWriter, path string, base string, data AnyMap) { if data == nil { data = map[string]interface{}{ "Error": true, } } else { data["Error"] = true } funcs := map[string]any{ "startsWith": strings.HasPrefix, "mul": Mul, "replace": strings.Replace, "add": Add, } tmpl, err := template.New("").Funcs(funcs).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) if err.Code == http.StatusNotFound { if context.Mode == HTML { w.WriteHeader(309) context.Mode = HTMLFULL } LoadBasedOnAnswer(context.Mode, w, "404.html", data) return } w.WriteHeader(err.Code) 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.UserType < 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 *dbtypes.User Mode AnswerType Logger *log.Logger Db *sql.DB } func (c Context) Error400(err error, message string, w http.ResponseWriter, path string, base string, data AnyMap) *Error { c.SetReportCaller(true) c.Logger.Error(message) c.SetReportCaller(false) if err != nil { c.Logger.Errorf("Something went wrong returning with: %d\n.Err:\n", http.StatusBadRequest) c.Logger.Error(err) } if c.Mode == JSON { return &Error{http.StatusBadRequest, nil, c.AddMap(data)} } LoadDefineTemplate(w, path, base, c.AddMap(data)) return nil } func (c Context) SetReportCaller(report bool) { if report { c.Logger.SetCallerOffset(2) c.Logger.SetReportCaller(true) } else { c.Logger.SetCallerOffset(1) c.Logger.SetReportCaller(false) } } func (c Context) ErrorCode(err error, code int, data AnyMap) *Error { if code == 400 { c.SetReportCaller(true) c.Logger.Warn("When returning BadRequest(400) please use context.Error400\n") c.SetReportCaller(false) } if err != nil { c.Logger.Errorf("Something went wrong returning with: %d\n.Err:\n", code) c.Logger.Error(err) } return &Error{code, nil, c.AddMap(data)} } func (c Context) UnsafeErrorCode(err error, code int, data AnyMap) *Error { if err != nil { c.Logger.Errorf("Something went wrong returning with: %d\n.Err:\n", code) c.Logger.Error(err) } return &Error{code, nil, c.AddMap(data)} } func (c Context) Error500(err error) *Error { return c.ErrorCode(err, http.StatusInternalServerError, nil) } 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(handler *Handle, mode AnswerType, r *http.Request) (*Context, error) { var token *string logger := log.NewWithOptions(os.Stdout, log.Options{ ReportTimestamp: true, TimeFormat: time.Kitchen, Prefix: r.URL.Path, }) 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, Logger: logger, Db: handler.Db, }, nil } user, err := dbtypes.UserFromToken(x.Db, *token) if err != nil { return nil, errors.Join(err, LogoffError) } return &Context{token, user, mode, logger, handler.Db}, 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.Header().Add("HX-Redirect", path) w.WriteHeader(204) } 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 { log.Warn("This function is deprecated please use the one provided by context") // 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 { log.Warn("This function is deprecated please use the one provided by context") 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 (x Handle) ReadTypesFiles(pathTest string, baseFilePath string, fileTypes []string, contentTypes []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) found := false index := -1 for i, fileType := range fileTypes { if strings.HasSuffix(user_path, fileType) { found = true index = i break } } if !found { 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", contentTypes[index]) 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("HX-Request") == "true" || r.Header.Get("Request-Type") == "html" { ans = HTML } if r.Header.Get("Request-Type") == "htmlfull" { ans = HTMLFULL } //TODO JSON //Login state context, err := x.createContext(x, 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") port := os.Getenv("PORT") if port == "" { port = "8000" } log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) }