diff --git a/.gitignore b/.gitignore index 168b495..ebe1988 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ bin *.db tmp -tmp/* \ No newline at end of file +tmp/* +config.toml \ No newline at end of file diff --git a/config.go b/config/config.go similarity index 55% rename from config.go rename to config/config.go index 5900eb7..c172d1d 100644 --- a/config.go +++ b/config/config.go @@ -1,16 +1,29 @@ -package main +package config import ( "github.com/sirupsen/logrus" "github.com/spf13/viper" ) -func initConfig() error { +type smtp struct { + Enabled bool + Host string + Port uint16 + Username string + Password string + From string +} + +var SMTP smtp = smtp{} +var BaseURL string = "localhost" + +func InitConfig() error { viper.SetDefault("host", "[::]") viper.SetDefault("port", 8080) viper.SetDefault("db_path", "bin.db") viper.SetDefault("smtp.enabled", false) viper.SetDefault("user.registration_enabled", false) + viper.SetDefault("base_url", "localhost") viper.SetConfigName("config") viper.SetConfigType("toml") @@ -27,5 +40,16 @@ func initConfig() error { } } + SMTP = smtp{ + Enabled: viper.GetBool("smtp.enabled"), + Host: viper.GetString("smtp.host"), + Port: viper.GetUint16("smtp.port"), + Username: viper.GetString("smtp.username"), + Password: viper.GetString("smtp.password"), + From: viper.GetString("smtp.from"), + } + + BaseURL = viper.GetString("base_url") + return nil } diff --git a/controllers/email.go b/controllers/email.go new file mode 100644 index 0000000..6367e0b --- /dev/null +++ b/controllers/email.go @@ -0,0 +1,81 @@ +package controllers + +import ( + "fmt" + + "git.myrkvi.com/myrkvi/bin/config" + "git.myrkvi.com/myrkvi/bin/global" + "git.myrkvi.com/myrkvi/bin/models" + "git.myrkvi.com/myrkvi/bin/utils" + "github.com/go-mail/mail" + "github.com/labstack/echo/v4" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "gorm.io/gorm" +) + +func PostEmailHandler(c echo.Context) error { + code := c.Param("id") + adminCode := c.FormValue("adminKey") + email := c.FormValue("email") + + if !viper.GetBool("smtp.enabled") { + return utils.RenderErrorToast(c, "function not available") + } + + file := models.File{} + + result := global.DB.Where("page_key = ?", code).First(&file) + if result.Error == gorm.ErrRecordNotFound { + return utils.RenderErrorToast(c, "the file does not exist") + } else if result.Error != nil { + return utils.RenderErrorToast(c, "server error occurred") + } + + if adminCode != file.AdminKey { + return utils.RenderErrorToast(c, "incorrect deletion key") + } + + if file.Email != "" { + return utils.RenderErrorToast(c, "this file is already associated with an email") + } + + file.Email = email + + result = global.DB.Save(&file) + if result.Error != nil { + logrus.WithError(result.Error). + WithFields(logrus.Fields{ + "file code": file.PageKey, + "email": email, + }).Errorln("failed to save email for file") + + return utils.RenderErrorToast(c, "server error occurred") + } + + smtp := config.SMTP + + dial := mail.NewDialer(smtp.Host, int(smtp.Port), smtp.Username, smtp.Password) + + message := mail.NewMessage() + message.SetHeader("From", smtp.From) + message.SetHeader("To", email) + message.SetHeader("Subject", fmt.Sprintf("[bin] Deletion key for file '%s'", file.Filename)) + message.SetBody( + "text/plain", + fmt.Sprintf( + "Deletion key for the file located at %s is %s.\r\nYou can also use this link: %s", + fmt.Sprintf("%s/b/%s", config.BaseURL, file.PageKey), + file.AdminKey, + fmt.Sprintf("%s/b/%s?delcode=%s", config.BaseURL, file.PageKey, file.AdminKey), + ), + ) + + err := dial.DialAndSend(message) + if err != nil { + return utils.RenderErrorToast(c, "could not send email") + } + + return c.HTML(200, "
email sent!
") + +} diff --git a/controllers/new.go b/controllers/new.go index 5ed2c51..82d8bbf 100644 --- a/controllers/new.go +++ b/controllers/new.go @@ -28,6 +28,7 @@ func PostNewHandler(c echo.Context) error { name := c.FormValue("name") description := c.FormValue("description") lang := c.FormValue("lang") + ip := c.RealIP() if (file == nil || err != nil) && text == "" { return utils.RenderErrorToast(c, "file or text must be provided") @@ -55,7 +56,7 @@ func PostNewHandler(c echo.Context) error { } } - createdFile, err := models.CreateNewBin(global.DB, text, name, description, lang, code, adminCode) + createdFile, err := models.CreateNewBin(global.DB, text, name, description, lang, code, adminCode, ip) if err != nil { return utils.RenderErrorToast(c, "server-side error occurred") } diff --git a/controllers/handlers_partial.go b/controllers/partials.go similarity index 80% rename from controllers/handlers_partial.go rename to controllers/partials.go index 35338a1..1988981 100644 --- a/controllers/handlers_partial.go +++ b/controllers/partials.go @@ -17,3 +17,7 @@ func GetPartialTextHandler(c echo.Context) error { c.Response().Header().Add("HX-Push", "/new?text") return utils.RenderComponents(c, http.StatusOK, partials.NewTextSubmit()) } + +func GetPartialEmailForm(c echo.Context) error { + return utils.RenderComponents(c, http.StatusOK, partials.EmailForm()) +} diff --git a/go.mod b/go.mod index f985dcb..a630abd 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/gin-gonic/gin v1.9.1 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/sqlite v1.9.0 // indirect + github.com/go-mail/mail v2.3.1+incompatible // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect @@ -74,6 +75,7 @@ require ( golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/gorm v1.25.4 // indirect diff --git a/go.sum b/go.sum index cb611e0..58d2da4 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,8 @@ github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voA github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM= +github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -625,6 +627,8 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/main.go b/main.go index d4c8bf0..e99bb22 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,10 @@ package main import ( + "embed" "fmt" + "git.myrkvi.com/myrkvi/bin/config" "git.myrkvi.com/myrkvi/bin/controllers" "git.myrkvi.com/myrkvi/bin/global" "git.myrkvi.com/myrkvi/bin/models" @@ -15,13 +17,16 @@ import ( "gorm.io/gorm" ) +//go:embed static +var static embed.FS + func main() { logrus.SetLevel(logrus.DebugLevel) logrus.SetFormatter(&prefixed.TextFormatter{ FullTimestamp: true, }) logrus.Infoln("Application started.") - initConfig() + config.InitConfig() db_string := "bin.db" var err error @@ -53,15 +58,20 @@ func main() { }, })) - e.Static("/static", "static") + //e.Static("/static", "static") + fs := echo.MustSubFS(static, "./static/") + e.StaticFS("/static", fs) e.GET("/", controllers.IndexHandler) e.GET("/new", controllers.GetNewHandler) e.POST("/new", controllers.PostNewHandler) e.GET("/b/:id", controllers.GetBinHandler) e.POST("/b/:id/delete", controllers.DeleteBinHandler) + e.POST("/b/:id/email", controllers.PostEmailHandler) partial := e.Group("/partial") + partial.GET("/email", controllers.GetPartialEmailForm) + new := partial.Group("/new") new.GET("/upload", controllers.GetPartialUploadHandler) new.GET("/text", controllers.GetPartialTextHandler) diff --git a/models/file.go b/models/file.go new file mode 100644 index 0000000..dbd2ad8 --- /dev/null +++ b/models/file.go @@ -0,0 +1,33 @@ +package models + +import "gorm.io/gorm" + +type File struct { + gorm.Model + + PageKey string `gorm:"index:idx_pagekey,unique;unique"` + AdminKey string `gorm:"index:idx_adminkey"` + Filename string + Description string + Language string + Data string + SubmitterIP string + Email string `gorm:"index:idx_email"` +} + +func CreateNewBin(db *gorm.DB, text, name, description, language, key, adminKey, ip string) (File, error) { + bin := File{ + Filename: name, + Description: description, + Language: language, + PageKey: key, + AdminKey: adminKey, + Data: text, + SubmitterIP: ip, + } + result := db.Create(&bin) + if result.Error != nil { + return File{}, result.Error + } + return bin, nil +} diff --git a/models/models.go b/models/models.go index 72afabd..3943ec5 100644 --- a/models/models.go +++ b/models/models.go @@ -4,46 +4,9 @@ import ( "reflect" "github.com/sirupsen/logrus" - "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) -type File struct { - gorm.Model - - PageKey string `gorm:"index:idx_pagekey,unique;unique"` - AdminKey string `gorm:"index:idx_adminkey"` - - // The "virtual" filename. - Filename string - - // The description of the file submission. - Description string - - Language string - - // The contents of the file must be valid text. - Data string - - UserID uint - User User -} - -type User struct { - gorm.Model - - DisplayName string - Email string `gorm:"unique"` - Salt string - HashedPassword []byte -} - -func (u *User) MatchesPassword(password string) bool { - combined := append([]byte(u.Salt), []byte(password)...) - - return bcrypt.CompareHashAndPassword(u.HashedPassword, []byte(combined)) == nil -} - func DatabaseMigrations(db *gorm.DB) { tables := []interface{}{ File{}, @@ -62,19 +25,3 @@ func DatabaseMigrations(db *gorm.DB) { } logrus.Infoln("Migrations ran successfully.") } - -func CreateNewBin(db *gorm.DB, text, name, description, language, key, adminKey string) (File, error) { - bin := File{ - Filename: name, - Description: description, - Language: language, - PageKey: key, - AdminKey: adminKey, - Data: text, - } - result := db.Create(&bin) - if result.Error != nil { - return File{}, result.Error - } - return bin, nil -} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..6679288 --- /dev/null +++ b/models/user.go @@ -0,0 +1,21 @@ +package models + +import ( + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type User struct { + gorm.Model + + DisplayName string + Email string `gorm:"unique"` + Salt string + HashedPassword []byte +} + +func (u *User) MatchesPassword(password string) bool { + combined := append([]byte(u.Salt), []byte(password)...) + + return bcrypt.CompareHashAndPassword(u.HashedPassword, []byte(combined)) == nil +} diff --git a/models/models_test.go b/models/user_test.go similarity index 100% rename from models/models_test.go rename to models/user_test.go diff --git a/static/_hyperscript.min.js b/static/hyperscript.min.js similarity index 100% rename from static/_hyperscript.min.js rename to static/hyperscript.min.js diff --git a/static/tailwind.css b/static/tailwind.css index 42bebef..5c3c5e8 100644 --- a/static/tailwind.css +++ b/static/tailwind.css @@ -682,6 +682,10 @@ video { width: 6rem; } +.w-48 { + width: 12rem; +} + .w-fit { width: -moz-fit-content; width: fit-content; diff --git a/views/components/components.templ b/views/components/components.templ index af73b5c..a1646c4 100644 --- a/views/components/components.templ +++ b/views/components/components.templ @@ -61,6 +61,7 @@ templ FileUpload(text, name, id string) { templ BoostButton(label, to string) { + { label } + +} + templ SubmitFormButton(label string) { } -// templ CombineTempls(top, bottom templ.Component) { -// {! top } -// {! bottom } -// } - templ CombineTempls(comps ...templ.Component) { for _, comp := range comps { {! comp } diff --git a/views/components/components_templ.go b/views/components/components_templ.go index 2c2e5ba..48c6ef2 100644 --- a/views/components/components_templ.go +++ b/views/components/components_templ.go @@ -199,7 +199,7 @@ func BoostButton(label, to string) templ.Component { var_7 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, err = templBuffer.WriteString("") + if err != nil { + return err + } + var var_11 string = label + _, err = templBuffer.WriteString(templ.EscapeString(var_11)) + if err != nil { + return err + } + _, err = templBuffer.WriteString("") + if err != nil { + return err + } + if !templIsBuffer { + _, err = io.Copy(w, templBuffer) + } + return err + }) +} + +func SubmitFormButton(label string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) { + templBuffer, templIsBuffer := w.(*bytes.Buffer) + if !templIsBuffer { + templBuffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templBuffer) + } + ctx = templ.InitializeContext(ctx) + var_12 := templ.GetChildren(ctx) + if var_12 == nil { + var_12 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) _, err = templBuffer.WriteString("") if err != nil { return err } - var_19 := `🞬` - _, err = templBuffer.WriteString(var_19) + var_21 := `🞬` + _, err = templBuffer.WriteString(var_21) if err != nil { return err } @@ -436,8 +504,8 @@ func ToastError(message string) templ.Component { if err != nil { return err } - var var_20 string = message - _, err = templBuffer.WriteString(templ.EscapeString(var_20)) + var var_22 string = message + _, err = templBuffer.WriteString(templ.EscapeString(var_22)) if err != nil { return err } @@ -460,17 +528,17 @@ func ToastSuccess(message string) templ.Component { defer templ.ReleaseBuffer(templBuffer) } ctx = templ.InitializeContext(ctx) - var_21 := templ.GetChildren(ctx) - if var_21 == nil { - var_21 = templ.NopComponent + var_23 := templ.GetChildren(ctx) + if var_23 == nil { + var_23 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, err = templBuffer.WriteString("{ file.Description }
+{ file.Data }+
") + if err != nil { + return err + } + var var_7 string = file.Description + _, err = templBuffer.WriteString(templ.EscapeString(var_7)) + if err != nil { + return err + } + _, err = templBuffer.WriteString("
") + if err != nil { + return err + } + var var_8 string = file.Data + _, err = templBuffer.WriteString(templ.EscapeString(var_8)) + if err != nil { + return err + } + _, err = templBuffer.WriteString("
bin is a simple paste bin.
+") + if err != nil { + return err + } + var_5 := `bin is a simple paste bin.` + _, err = templBuffer.WriteString(var_5) + if err != nil { + return err + } + _, err = templBuffer.WriteString("