package main import ( "bufio" _ "embed" "fmt" "io" "net/http" "os" "path" "time" "tinygram/internal/secrets" "github.com/BurntSushi/toml" "github.com/google/uuid" "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type Post struct { CreatedAt time.Time Description string ImageID string } type Config struct { SessionSecret string DbPath string AssetPath string PasswordFilepath string } //go:embed prod.toml.agebox var prodenv string //go:embed dev.toml.agebox var devenv string var config Config func main() { secret, err := secrets.DecryptSecret(prodenv) if err != nil { log.Errorf("could not decrypt a secret", err) os.Exit(1) } fmt.Println(secret) _, err = toml.Decode(secret, &config) if err != nil { fmt.Printf("could not parse config %v\n", err) os.Exit(1) } dbPath := config.DbPath if dbPath == "" { dbPath = "tinygram.db" } sessionSecret := config.SessionSecret if sessionSecret == "" { fmt.Println("NEED TO PROVIDE A SECRET") os.Exit(1) } passwordFilePath := config.PasswordFilepath if passwordFilePath == "" { passwordFilePath = "password.txt" } assetsPath := config.AssetPath if assetsPath == "" { assetsPath = "assets" } swedenTime, _ := time.LoadLocation("Europe/Stockholm") e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.Secure()) e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ TokenLookup: "form:_csrf", })) e.Use(session.Middleware(sessions.NewCookieStore([]byte(sessionSecret)))) db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) db.Exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout = 5000;") db.AutoMigrate(Post{}) if err != nil { fmt.Printf("opening db: %v", err) return } e.Static("/static", assetsPath) e.GET("/", func(c echo.Context) error { var posts []Post db.Order("created_at DESC").Limit(5).Find(&posts) component := index(posts, swedenTime) err := component.Render(c.Request().Context(), c.Response().Writer) if err != nil { return err } return nil }) e.GET("/login", func(c echo.Context) error { component := loginPage(c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)) err := component.Render(c.Request().Context(), c.Response().Writer) if err != nil { return err } return nil }) e.POST("/login", func(c echo.Context) error { // read password file, check content, add session if correct file, err := os.Open(passwordFilePath) if err != nil { c.Response().Header().Set("HX-Redirect", "/") return c.NoContent(http.StatusUnauthorized) } // check provided password against file s := bufio.NewScanner(file) s.Scan() expected := s.Text() formPass := c.FormValue("password") if expected != formPass { c.Response().Header().Set("HX-Redirect", "/") return c.NoContent(http.StatusUnauthorized) } sess, _ := session.Get("session", c) sess.Options = &sessions.Options{ Path: "/", MaxAge: 86400 * 7, HttpOnly: true, } sess.Values["user"] = "admin" sess.Save(c.Request(), c.Response()) c.Response().Header().Set("HX-Redirect", "/upload") return c.NoContent(http.StatusOK) }) e.GET("/upload", func(c echo.Context) error { sess, _ := session.Get("session", c) if sess.Values["user"] != "admin" { return c.Redirect(http.StatusSeeOther, "/login") } component := uploadPage(c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)) err := component.Render(c.Request().Context(), c.Response().Writer) if err != nil { return err } return nil }) e.POST("/upload", func(c echo.Context) error { sess, _ := session.Get("session", c) if sess.Values["user"] != "admin" { return c.Redirect(http.StatusSeeOther, "/login") } file, err := c.FormFile("file") if err != nil { return err } src, err := file.Open() if err != nil { return err } defer src.Close() filename, err := uuid.NewRandom() if err != nil { return err } dst, err := os.Create(path.Join(assetsPath, filename.String())) //RANDOMIZE if err != nil { return err } defer dst.Close() // Copy if _, err = io.Copy(dst, src); err != nil { return err } description := c.FormValue("description") post := Post{ Description: description, ImageID: "/static/" + filename.String(), } db.Create(&post) c.Response().Header().Set("HX-Redirect", "/") return c.NoContent(http.StatusOK) }) e.GET("/posts", func(c echo.Context) error { after, err := time.Parse(time.RFC3339, c.QueryParam("after")) if err != nil { return err } var ps []Post db.Order("created_at DESC").Limit(5).Where("created_at < ?", after).Find(&ps) component := posts(ps, swedenTime) err = component.Render(c.Request().Context(), c.Response().Writer) if err != nil { return err } return nil }) e.Logger.Fatal(e.Start(":8080")) }