diff --git a/cmd/web/handlers_guestbook.go b/cmd/web/handlers_guestbook.go index 5234eb4..4d90132 100644 --- a/cmd/web/handlers_guestbook.go +++ b/cmd/web/handlers_guestbook.go @@ -42,79 +42,7 @@ func (app *application) getGuestbook(w http.ResponseWriter, r *http.Request) { return } data := app.newCommonData(r) - views.GuestbookView("Guestbook", data, website, website.Guestbook, comments, forms.CommentCreateForm{}).Render(r.Context(), w) -} - -func (app *application) getGuestbookSettings(w http.ResponseWriter, r *http.Request) { - slug := r.PathValue("id") - website, err := app.websites.Get(slugToShortId(slug)) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } - data := app.newCommonData(r) - views.GuestbookSettingsView(data, website).Render(r.Context(), w) -} - -func (app *application) putGuestbookSettings(w http.ResponseWriter, r *http.Request) { - slug := r.PathValue("id") - website, err := app.websites.Get(slugToShortId(slug)) - if err != nil { - if errors.Is(err, models.ErrNoRecord) { - http.NotFound(w, r) - } else { - app.serverError(w, r, err) - } - } - - var form forms.GuestbookSettingsForm - err = app.decodePostForm(r, &form) - if err != nil { - app.clientError(w, http.StatusBadRequest) - app.serverError(w, r, err) - return - } - form.CheckField(validator.PermittedValue(form.Visibility, "true", "false"), "gb_visible", "Invalid value") - form.CheckField(validator.PermittedValue(form.CommentingEnabled, models.ValidDisableDurations...), "gb_visible", "Invalid value") - form.CheckField(validator.PermittedValue(form.WidgetsEnabled, "true", "false"), "gb_remote", "Invalid value") - if !form.Valid() { - // TODO: rerender template with errors - app.clientError(w, http.StatusUnprocessableEntity) - } - - c, err := strconv.ParseBool(form.CommentingEnabled) - if err != nil { - website.Guestbook.Settings.IsCommentingEnabled = false - website.Guestbook.Settings.ReenableCommenting, err = app.durationToTime(form.CommentingEnabled) - if err != nil { - app.serverError(w, r, err) - } - } else { - website.Guestbook.Settings.IsCommentingEnabled = c - } - - // can skip error checking for these two since we verify valid values above - website.Guestbook.Settings.IsVisible, err = strconv.ParseBool(form.Visibility) - if err != nil { - app.serverError(w, r, err) - } - website.Guestbook.Settings.AllowRemoteHostAccess, err = strconv.ParseBool(form.WidgetsEnabled) - if err != nil { - app.serverError(w, r, err) - } - err = app.websites.UpdateGuestbookSettings(website.Guestbook.ID, website.Guestbook.Settings) - if err != nil { - app.serverError(w, r, err) - return - } - app.sessionManager.Put(r.Context(), "flash", "Settings changed successfully") - data := app.newCommonData(r) - w.Header().Add("HX-Refresh", "true") - views.GuestbookSettingsView(data, website).Render(r.Context(), w) - + views.GuestbookView(website.Name, data, website, website.Guestbook, comments, forms.CommentCreateForm{}).Render(r.Context(), w) } func (app *application) getGuestbookComments(w http.ResponseWriter, r *http.Request) { @@ -134,7 +62,7 @@ func (app *application) getGuestbookComments(w http.ResponseWriter, r *http.Requ return } data := app.newCommonData(r) - views.GuestbookDashboardCommentsView("Comments", data, website, website.Guestbook, comments).Render(r.Context(), w) + views.GuestbookDashboardCommentsView(fmt.Sprintf("%s - Comments", website.Name), data, website, website.Guestbook, comments).Render(r.Context(), w) } func (app *application) getGuestbookCommentsSerialized(w http.ResponseWriter, r *http.Request) { @@ -326,7 +254,7 @@ func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request) } data := app.newCommonData(r) - views.GuestbookDashboardCommentsView("Message Queue", data, website, website.Guestbook, comments).Render(r.Context(), w) + views.GuestbookDashboardCommentsView(fmt.Sprintf("%s - Comment Queue", website.Name), data, website, website.Guestbook, comments).Render(r.Context(), w) } func (app *application) getCommentTrash(w http.ResponseWriter, r *http.Request) { @@ -352,7 +280,7 @@ func (app *application) getCommentTrash(w http.ResponseWriter, r *http.Request) } data := app.newCommonData(r) - views.GuestbookDashboardCommentsView("Trash", data, website, website.Guestbook, comments).Render(r.Context(), w) + views.GuestbookDashboardCommentsView(fmt.Sprintf("%s - Comment Trash", website.Name), data, website, website.Guestbook, comments).Render(r.Context(), w) } func (app *application) putHideGuestbookComment(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/web/handlers_user.go b/cmd/web/handlers_user.go index 132f099..391eb13 100644 --- a/cmd/web/handlers_user.go +++ b/cmd/web/handlers_user.go @@ -218,19 +218,6 @@ func (app *application) postUserLogout(w http.ResponseWriter, r *http.Request) { // http.Redirect(w, r, "/", http.StatusSeeOther) } -// func (app *application) getUsersList(w http.ResponseWriter, r *http.Request) { -// // skip templ conversion for this view, which will not be available in the final app -// // something similar will be available in the admin panel -// users, err := app.users.GetAll() -// if err != nil { -// app.serverError(w, r, err) -// return -// } -// data := app.newTemplateData(r) -// data.Users = users -// app.render(w, r, http.StatusOK, "userlist.view.tmpl.html", data) -// } - func (app *application) getUser(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("id") user, err := app.users.Get(slugToShortId(slug)) diff --git a/cmd/web/handlers_website.go b/cmd/web/handlers_website.go index a2b8e9e..add16a1 100644 --- a/cmd/web/handlers_website.go +++ b/cmd/web/handlers_website.go @@ -71,7 +71,7 @@ func (app *application) getWebsiteDashboard(w http.ResponseWriter, r *http.Reque app.clientError(w, http.StatusUnauthorized) } data := app.newCommonData(r) - views.WebsiteDashboard("Guestbook", data, website).Render(r.Context(), w) + views.WebsiteDashboard(fmt.Sprintf("%s - Dashboard", website.Name), data, website).Render(r.Context(), w) } func (app *application) getWebsiteList(w http.ResponseWriter, r *http.Request) { @@ -103,3 +103,123 @@ func (app *application) getComingSoon(w http.ResponseWriter, r *http.Request) { } views.WebsiteDashboardComingSoon("Coming Soon", app.newCommonData(r), website).Render(r.Context(), w) } + +func (app *application) getWebsiteSettings(w http.ResponseWriter, r *http.Request) { + slug := r.PathValue("id") + website, err := app.websites.Get(slugToShortId(slug)) + if err != nil { + if errors.Is(err, models.ErrNoRecord) { + http.NotFound(w, r) + } else { + app.serverError(w, r, err) + } + return + } + var form forms.WebsiteSettingsForm + data := app.newCommonData(r) + views.WebsiteDashboardSettings(data, website, form).Render(r.Context(), w) +} + +func (app *application) putWebsiteSettings(w http.ResponseWriter, r *http.Request) { + slug := r.PathValue("id") + website, err := app.websites.Get(slugToShortId(slug)) + if err != nil { + if errors.Is(err, models.ErrNoRecord) { + http.NotFound(w, r) + } else { + app.serverError(w, r, err) + } + return + } + + var form forms.WebsiteSettingsForm + err = app.decodePostForm(r, &form) + if err != nil { + app.clientError(w, http.StatusBadRequest) + app.serverError(w, r, err) + return + } + form.CheckField(validator.NotBlank(form.AuthorName), "ws_author", "This field cannot be blank") + form.CheckField(validator.MaxChars(form.AuthorName, 256), "ws_author", "This field cannot exceed 256 characters") + form.CheckField(validator.NotBlank(form.SiteName), "ws_name", "This field cannot be blank") + form.CheckField(validator.MaxChars(form.SiteName, 256), "ws_name", "This field cannot exceed 256 characters") + form.CheckField(validator.NotBlank(form.SiteUrl), "ws_url", "This field cannot be blank") + form.CheckField(validator.MaxChars(form.SiteUrl, 512), "ws_url", "This field cannot exceed 512 characters") + form.CheckField(validator.Matches(form.SiteUrl, validator.WebRX), "ws_url", "This field must be a valid URL (including http:// or https://)") + form.CheckField(validator.PermittedValue(form.Visibility, "true", "false"), "gb_visible", "Invalid value") + form.CheckField(validator.PermittedValue(form.CommentingEnabled, models.ValidDisableDurations...), "gb_visible", "Invalid value") + form.CheckField(validator.PermittedValue(form.WidgetsEnabled, "true", "false"), "gb_remote", "Invalid value") + if !form.Valid() { + data := app.newCommonData(r) + views.SettingsForm(data, website, form, "").Render(r.Context(), w) + return + } + + gbSettings, err := convertFormToGuestbookSettings(form) + if err != nil { + app.serverError(w, r, err) + return + } + + err = app.websites.UpdateGuestbookSettings(website.Guestbook.ID, gbSettings) + if err != nil { + app.serverError(w, r, err) + return + } + + u, err := url.Parse(form.SiteUrl) + if err != nil { + app.serverError(w, r, err) + return + } + website.Name = form.SiteName + website.AuthorName = form.AuthorName + website.Url = u + + err = app.websites.Update(website) + if err != nil { + app.serverError(w, r, err) + return + } + + app.sessionManager.Put(r.Context(), "flash", "Settings changed successfully") + data := app.newCommonData(r) + views.SettingsForm(data, website, forms.WebsiteSettingsForm{}, "Settings changed successfully").Render(r.Context(), w) + +} + +func (app *application) deleteWebsite(w http.ResponseWriter, r *http.Request) { + slug := r.PathValue("id") + website, err := app.websites.Get(slugToShortId(slug)) + if err != nil { + if errors.Is(err, models.ErrNoRecord) { + http.NotFound(w, r) + } else { + app.serverError(w, r, err) + } + return + } + var form forms.WebsiteDeleteForm + err = app.decodePostForm(r, &form) + if err != nil { + app.serverError(w, r, err) + return + } + + form.CheckField(validator.Equals(website.Name, form.Delete), "delete", "Input must match site name exactly") + if !form.Valid() { + data := app.newCommonData(r) + views.DeleteForm(data, website, form).Render(r.Context(), w) + return + } + + err = app.websites.Delete(website.ID) + if err != nil { + app.serverError(w, r, err) + return + } + + app.sessionManager.Put(r.Context(), "flash", "Website Deleted") + w.Header().Add("HX-Redirect", "/websites") + w.WriteHeader(http.StatusOK) +} diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go index 8e66c3c..8b8f06f 100644 --- a/cmd/web/helpers.go +++ b/cmd/web/helpers.go @@ -13,6 +13,7 @@ import ( "strconv" "time" + "git.32bit.cafe/32bitcafe/guestbook/internal/forms" "git.32bit.cafe/32bitcafe/guestbook/internal/models" "git.32bit.cafe/32bitcafe/guestbook/ui/views" "github.com/gorilla/schema" @@ -125,7 +126,7 @@ func DefaultUserSettings() models.UserSettings { } } -func (app *application) durationToTime(duration string) (time.Time, error) { +func durationToTime(duration string) (time.Time, error) { var result time.Time offset, err := time.ParseDuration(duration) if err != nil { @@ -164,3 +165,28 @@ func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value strin } http.SetCookie(w, c) } + +func convertFormToGuestbookSettings(form forms.WebsiteSettingsForm) (models.GuestbookSettings, error) { + var s models.GuestbookSettings + c, err := strconv.ParseBool(form.CommentingEnabled) + if err != nil { + s.IsCommentingEnabled = false + s.ReenableCommenting, err = durationToTime(form.CommentingEnabled) + if err != nil { + return s, err + } + } else { + s.IsCommentingEnabled = c + } + + // can skip error checking for these two since we verify valid values above + s.IsVisible, err = strconv.ParseBool(form.Visibility) + if err != nil { + return s, err + } + s.AllowRemoteHostAccess, err = strconv.ParseBool(form.WidgetsEnabled) + if err != nil { + return s, err + } + return s, nil +} diff --git a/cmd/web/main.go b/cmd/web/main.go index b15619b..26ff243 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -40,6 +40,7 @@ type applicationConfig struct { localAuthEnabled bool oauth applicationOauthConfig rootUrl string + environment string } type application struct { @@ -158,7 +159,15 @@ func setupConfig(addr string) (applicationConfig, error) { oauth2Provider = os.Getenv("OAUTH2_PROVIDER") clientID = os.Getenv("OAUTH2_CLIENT_ID") clientSecret = os.Getenv("OAUTH2_CLIENT_SECRET") + environment = os.Getenv("ENVIRONMENT") ) + + if environment != "" { + c.environment = environment + } else { + c.environment = "DEV" + } + if rootUrl != "" { c.rootUrl = rootUrl } else { @@ -214,6 +223,7 @@ func setupConfig(addr string) (applicationConfig, error) { } c.oauth = o + return c, nil } diff --git a/cmd/web/routes.go b/cmd/web/routes.go index 4f32a1b..c168a11 100644 --- a/cmd/web/routes.go +++ b/cmd/web/routes.go @@ -9,7 +9,12 @@ import ( func (app *application) routes() http.Handler { mux := http.NewServeMux() - mux.Handle("GET /static/", http.FileServerFS(ui.Files)) + if app.config.environment == "PROD" { + mux.Handle("GET /static/", http.FileServerFS(ui.Files)) + } else { + fileServer := http.FileServer(http.Dir("./ui/static/")) + mux.Handle("GET /static/", http.StripPrefix("/static", fileServer)) + } mux.HandleFunc("GET /ping", ping) @@ -49,8 +54,9 @@ func (app *application) routes() http.Handler { mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/queue", protected.ThenFunc(app.getCommentQueue)) mux.Handle("DELETE /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.deleteGuestbookComment)) mux.Handle("PUT /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.putHideGuestbookComment)) - mux.Handle("GET /websites/{id}/dashboard/guestbook/settings", protected.ThenFunc(app.getGuestbookSettings)) - mux.Handle("PUT /websites/{id}/dashboard/guestbook/settings", protected.ThenFunc(app.putGuestbookSettings)) + mux.Handle("GET /websites/{id}/dashboard/settings", protected.ThenFunc(app.getWebsiteSettings)) + mux.Handle("PUT /websites/{id}/settings", protected.ThenFunc(app.putWebsiteSettings)) + mux.Handle("PUT /websites/{id}", protected.ThenFunc(app.deleteWebsite)) mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/trash", protected.ThenFunc(app.getCommentTrash)) mux.Handle("GET /websites/{id}/dashboard/guestbook/themes", protected.ThenFunc(app.getComingSoon)) mux.Handle("GET /websites/{id}/dashboard/guestbook/customize", protected.ThenFunc(app.getComingSoon)) diff --git a/internal/forms/forms.go b/internal/forms/forms.go index 30abaa3..ebb7b25 100644 --- a/internal/forms/forms.go +++ b/internal/forms/forms.go @@ -31,12 +31,20 @@ type WebsiteCreateForm struct { validator.Validator `schema:"-"` } +type WebsiteDeleteForm struct { + Delete string `schema:"delete"` + validator.Validator `schema:"-"` +} + type UserSettingsForm struct { LocalTimezone string `schema:"timezones"` validator.Validator `schema:"-"` } -type GuestbookSettingsForm struct { +type WebsiteSettingsForm struct { + SiteName string `schema:"ws_name"` + SiteUrl string `schema:"ws_url"` + AuthorName string `schema:"ws_author"` Visibility string `schema:"gb_visible"` CommentingEnabled string `schema:"gb_commenting"` WidgetsEnabled string `schema:"gb_remote"` diff --git a/internal/models/mocks/website.go b/internal/models/mocks/website.go index 1e14872..bc50020 100644 --- a/internal/models/mocks/website.go +++ b/internal/models/mocks/website.go @@ -69,6 +69,13 @@ func (m *WebsiteModel) GetAll() ([]models.Website, error) { return []models.Website{mockWebsite}, nil } +func (m *WebsiteModel) Update(w models.Website) error { + return nil +} +func (m *WebsiteModel) Delete(websiteId int64) error { + return nil +} + func (m *WebsiteModel) InitializeSettingsMap() error { return nil } diff --git a/internal/models/website.go b/internal/models/website.go index 811bedf..6385530 100644 --- a/internal/models/website.go +++ b/internal/models/website.go @@ -97,9 +97,11 @@ type WebsiteModelInterface interface { Get(shortId uint64) (Website, error) GetAllUser(userId int64) ([]Website, error) GetAll() ([]Website, error) + Update(w Website) error InitializeSettingsMap() error UpdateGuestbookSettings(guestbookId int64, settings GuestbookSettings) error UpdateSetting(guestbookId int64, setting Setting, value string) error + Delete(websiteId int64) error } func (m *WebsiteModel) Insert(shortId uint64, userId int64, siteName, siteUrl, authorName string) (int64, error) { @@ -243,7 +245,7 @@ func (m *WebsiteModel) GetAllUser(userId int64) ([]Website, error) { stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, g.Id, g.ShortId, g.Created, g.IsActive FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId - WHERE w.UserId = ?` + WHERE w.UserId = ? AND w.Deleted IS NULL` rows, err := m.DB.Query(stmt, userId) if err != nil { return nil, err @@ -272,7 +274,7 @@ func (m *WebsiteModel) GetAllUser(userId int64) ([]Website, error) { func (m *WebsiteModel) GetAll() ([]Website, error) { stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, g.Id, g.ShortId, g.Created, g.IsActive - FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId` + FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId WHERE w.Deleted IS NULL` rows, err := m.DB.Query(stmt) if err != nil { return nil, err @@ -298,6 +300,36 @@ func (m *WebsiteModel) GetAll() ([]Website, error) { return websites, nil } +func (m *WebsiteModel) Update(w Website) error { + stmt := `UPDATE websites SET Name = ?, SiteUrl = ?, AuthorName = ? WHERE ID = ?` + r, err := m.DB.Exec(stmt, w.Name, w.Url.String(), w.AuthorName, w.ID) + if err != nil { + return err + } + if rows, err := r.RowsAffected(); rows != 1 { + if err != nil { + return err + } + return errors.New("Failed to update website") + } + return nil +} + +func (m *WebsiteModel) Delete(websiteId int64) error { + stmt := `UPDATE websites SET Deleted = ? WHERE ID = ?` + r, err := m.DB.Exec(stmt, time.Now().UTC(), websiteId) + if err != nil { + return err + } + if rows, err := r.RowsAffected(); rows != 1 { + if err != nil { + return err + } + return errors.New("Failed to update website") + } + return nil +} + func (m *WebsiteModel) getGuestbookSettings(tx *sql.Tx, guestbookId int64) (GuestbookSettings, error) { stmt := `SELECT g.SettingId, a.ItemValue, g.UnconstrainedValue FROM guestbook_settings AS g LEFT JOIN allowed_setting_values AS a ON g.AllowedSettingValueId = a.Id diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 62037b2..5991605 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -57,3 +57,7 @@ func MinChars(value string, n int) bool { func Matches(value string, rx *regexp.Regexp) bool { return rx.MatchString(value) } + +func Equals(expected, actual string) bool { + return expected == actual +} diff --git a/ui/static/css/style.css b/ui/static/css/style.css index 1f291a6..ceeac55 100644 --- a/ui/static/css/style.css +++ b/ui/static/css/style.css @@ -57,7 +57,7 @@ div#dashboard { div#dashboard nav { flex: 1 1 25%; - margin-top: 2rem; + /* margin-top: 2rem; */ min-width: 0; } diff --git a/ui/views/common.templ b/ui/views/common.templ index bafe90f..e17e270 100644 --- a/ui/views/common.templ +++ b/ui/views/common.templ @@ -87,6 +87,7 @@ templ base(title string, data CommonData) { if data.Flash != "" {