diff --git a/.gitignore b/.gitignore
index a6090ec..fc51efe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ go.work
.air.toml
/tmp
tls/
+test.db.old
+.gitignore
+.nvim/session
diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go
index 20225b3..d929ba8 100644
--- a/cmd/web/handlers.go
+++ b/cmd/web/handlers.go
@@ -2,10 +2,14 @@ package main
import (
"net/http"
+
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
)
func (app *application) home(w http.ResponseWriter, r *http.Request) {
- data := app.newCommonData(r)
- views.Home("Home", data).Render(r.Context(), w)
+ if app.isAuthenticated(r) {
+ http.Redirect(w, r, "/websites", http.StatusSeeOther)
+ return
+ }
+ views.Home("Home", app.newCommonData(r)).Render(r.Context(), w)
}
diff --git a/cmd/web/handlers_guestbook.go b/cmd/web/handlers_guestbook.go
index be24010..fb25a96 100644
--- a/cmd/web/handlers_guestbook.go
+++ b/cmd/web/handlers_guestbook.go
@@ -11,35 +11,6 @@ import (
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
)
-func (app *application) getGuestbookCreate(w http.ResponseWriter, r *http.Request) {
- data := app.newCommonData(r)
- views.GuestbookCreate("New Guestbook", data).Render(r.Context(), w)
-}
-
-func (app *application) postGuestbookCreate(w http.ResponseWriter, r *http.Request) {
- userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
- err := r.ParseForm()
- if err != nil {
- app.serverError(w, r, err)
- return
- }
- siteUrl := r.Form.Get("siteurl")
- shortId := app.createShortId()
- _, err = app.guestbooks.Insert(shortId, siteUrl, userId)
- if err != nil {
- app.serverError(w, r, err)
- return
- }
- app.sessionManager.Put(r.Context(), "flash", "Guestbook successfully created!")
- if r.Header.Get("HX-Request") == "true" {
- w.Header().Add("HX-Trigger", "newGuestbook")
- data := app.newTemplateData(r)
- app.renderHTMX(w, r, http.StatusOK, "guestbookcreatebutton.part.html", data)
- return
- }
- http.Redirect(w, r, fmt.Sprintf("/guestbooks/%s", shortIdToSlug(shortId)), http.StatusSeeOther)
-}
-
func (app *application) getGuestbookList(w http.ResponseWriter, r *http.Request) {
userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
guestbooks, err := app.guestbooks.GetAll(userId)
@@ -53,7 +24,7 @@ func (app *application) getGuestbookList(w http.ResponseWriter, r *http.Request)
func (app *application) getGuestbook(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
- guestbook, err := app.guestbooks.Get(slugToShortId(slug))
+ website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
@@ -62,42 +33,18 @@ func (app *application) getGuestbook(w http.ResponseWriter, r *http.Request) {
}
return
}
- comments, err := app.guestbookComments.GetAll(guestbook.ID)
+ comments, err := app.guestbookComments.GetAll(website.Guestbook.ID)
if err != nil {
app.serverError(w, r, err)
return
}
data := app.newCommonData(r)
- views.GuestbookView("Guestbook", data, guestbook, comments, forms.CommentCreateForm{}).Render(r.Context(), w)
-}
-
-func (app *application) getGuestbookDashboard(w http.ResponseWriter, r *http.Request) {
- slug := r.PathValue("id")
- guestbook, err := app.guestbooks.Get(slugToShortId(slug))
- if err != nil {
- if errors.Is(err, models.ErrNoRecord) {
- http.NotFound(w, r)
- } else {
- app.serverError(w, r, err)
- }
- return
- }
- user := app.getCurrentUser(r)
- if user.ID != guestbook.UserId {
- app.clientError(w, http.StatusUnauthorized)
- }
- comments, err := app.guestbookComments.GetAll(guestbook.ID)
- if err != nil {
- app.serverError(w, r, err)
- return
- }
- data := app.newCommonData(r)
- views.GuestbookDashboardView("Guestbook", data, guestbook, comments).Render(r.Context(), w)
+ views.GuestbookView("Guestbook", data, website, website.Guestbook, comments, forms.CommentCreateForm{}).Render(r.Context(), w)
}
func (app *application) getGuestbookComments(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
- guestbook, err := app.guestbooks.Get(slugToShortId(slug))
+ website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
@@ -106,19 +53,19 @@ func (app *application) getGuestbookComments(w http.ResponseWriter, r *http.Requ
}
return
}
- comments, err := app.guestbookComments.GetAll(guestbook.ID)
+ comments, err := app.guestbookComments.GetAll(website.Guestbook.ID)
if err != nil {
app.serverError(w, r, err)
return
}
data := app.newCommonData(r)
- views.GuestbookDashboardCommentsView("Comments", data, guestbook, comments).Render(r.Context(), w)
+ views.GuestbookDashboardCommentsView("Comments", data, website, website.Guestbook, comments).Render(r.Context(), w)
}
func (app *application) getGuestbookCommentCreate(w http.ResponseWriter, r *http.Request) {
// TODO: This will be the embeddable form
slug := r.PathValue("id")
- guestbook, err := app.guestbooks.Get(slugToShortId(slug))
+ website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
@@ -127,15 +74,15 @@ func (app *application) getGuestbookCommentCreate(w http.ResponseWriter, r *http
}
return
}
- data := app.newTemplateData(r)
- data.Guestbook = guestbook
- data.Form = forms.CommentCreateForm{}
- app.render(w, r, http.StatusOK, "commentcreate.view.tmpl.html", data)
+
+ data := app.newCommonData(r)
+ form := forms.CommentCreateForm{}
+ views.CreateGuestbookComment("New Comment", data, website, website.Guestbook, form).Render(r.Context(), w)
}
func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *http.Request) {
- guestbookSlug := r.PathValue("id")
- guestbook, err := app.guestbooks.Get(slugToShortId(guestbookSlug))
+ slug := r.PathValue("id")
+ website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
@@ -161,21 +108,24 @@ func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *htt
form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
if !form.Valid() {
- data := app.newTemplateData(r)
- data.Guestbook = guestbook
- data.Form = form
- app.render(w, r, http.StatusUnprocessableEntity, "commentcreate.view.tmpl.html", data)
+ comments, err := app.guestbookComments.GetAll(website.Guestbook.ID)
+ if err != nil {
+ app.serverError(w, r, err)
+ return
+ }
+ data := app.newCommonData(r)
+ views.GuestbookView("Guestbook", data, website, website.Guestbook, comments, forms.CommentCreateForm{}).Render(r.Context(), w)
return
}
shortId := app.createShortId()
- _, err = app.guestbookComments.Insert(shortId, guestbook.ID, 0, form.AuthorName, form.AuthorEmail, form.AuthorSite, form.Content, "", true)
+ _, err = app.guestbookComments.Insert(shortId, website.Guestbook.ID, 0, form.AuthorName, form.AuthorEmail, form.AuthorSite, form.Content, "", true)
if err != nil {
app.serverError(w, r, err)
return
}
// app.sessionManager.Put(r.Context(), "flash", "Comment successfully posted!")
- http.Redirect(w, r, fmt.Sprintf("/guestbooks/%s", guestbookSlug), http.StatusSeeOther)
+ http.Redirect(w, r, fmt.Sprintf("/websites/%s/guestbook", slug), http.StatusSeeOther)
}
func (app *application) updateGuestbookComment(w http.ResponseWriter, r *http.Request) {
@@ -188,8 +138,8 @@ func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Re
}
func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request) {
- guestbookSlug := r.PathValue("id")
- guestbook, err := app.guestbooks.Get(slugToShortId(guestbookSlug))
+ slug := r.PathValue("id")
+ website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
@@ -199,7 +149,7 @@ func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request)
return
}
- comments, err := app.guestbookComments.GetQueue(guestbook.ID)
+ comments, err := app.guestbookComments.GetQueue(website.Guestbook.ID)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
@@ -210,7 +160,7 @@ func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request)
}
data := app.newCommonData(r)
- views.GuestbookDashboardCommentsView("Message Queue", data, guestbook, comments).Render(r.Context(), w)
+ views.GuestbookDashboardCommentsView("Message Queue", 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 f0884d8..b2b0163 100644
--- a/cmd/web/handlers_user.go
+++ b/cmd/web/handlers_user.go
@@ -4,128 +4,127 @@ import (
"errors"
"net/http"
- "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
+ "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"git.32bit.cafe/32bitcafe/guestbook/internal/validator"
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
)
func (app *application) getUserRegister(w http.ResponseWriter, r *http.Request) {
- form := forms.UserRegistrationForm{}
- data := app.newCommonData(r)
- views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
+ form := forms.UserRegistrationForm{}
+ data := app.newCommonData(r)
+ views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
}
-
func (app *application) getUserLogin(w http.ResponseWriter, r *http.Request) {
- views.UserLogin("Login", app.newCommonData(r), forms.UserLoginForm{}).Render(r.Context(), w)
+ views.UserLogin("Login", app.newCommonData(r), forms.UserLoginForm{}).Render(r.Context(), w)
}
func (app *application) postUserRegister(w http.ResponseWriter, r *http.Request) {
- var form forms.UserRegistrationForm
- err := app.decodePostForm(r, &form)
- if err != nil {
- app.clientError(w, http.StatusBadRequest)
- return
- }
- form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank")
- form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
- form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
- form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
- form.CheckField(validator.MinChars(form.Password, 8), "password", "This field must be at least 8 characters long")
- if !form.Valid() {
- data := app.newCommonData(r)
- w.WriteHeader(http.StatusUnprocessableEntity)
- views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
- return
- }
- shortId := app.createShortId()
- err = app.users.Insert(shortId, form.Name, form.Email, form.Password)
- if err != nil {
- if errors.Is(err, models.ErrDuplicateEmail) {
- form.AddFieldError("email", "Email address is already in use")
- data := app.newCommonData(r)
- w.WriteHeader(http.StatusUnprocessableEntity)
- views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
- } else {
- app.serverError(w, r, err)
- }
- return
- }
- app.sessionManager.Put(r.Context(), "flash", "Registration successful. Please log in.")
- http.Redirect(w, r, "/users/login", http.StatusSeeOther)
+ var form forms.UserRegistrationForm
+ err := app.decodePostForm(r, &form)
+ if err != nil {
+ app.clientError(w, http.StatusBadRequest)
+ return
+ }
+ form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank")
+ form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
+ form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
+ form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
+ form.CheckField(validator.MinChars(form.Password, 8), "password", "This field must be at least 8 characters long")
+ if !form.Valid() {
+ data := app.newCommonData(r)
+ w.WriteHeader(http.StatusUnprocessableEntity)
+ views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
+ return
+ }
+ shortId := app.createShortId()
+ err = app.users.Insert(shortId, form.Name, form.Email, form.Password)
+ if err != nil {
+ if errors.Is(err, models.ErrDuplicateEmail) {
+ form.AddFieldError("email", "Email address is already in use")
+ data := app.newCommonData(r)
+ w.WriteHeader(http.StatusUnprocessableEntity)
+ views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
+ } else {
+ app.serverError(w, r, err)
+ }
+ return
+ }
+ app.sessionManager.Put(r.Context(), "flash", "Registration successful. Please log in.")
+ http.Redirect(w, r, "/users/login", http.StatusSeeOther)
}
func (app *application) postUserLogin(w http.ResponseWriter, r *http.Request) {
- var form forms.UserLoginForm
- err := app.decodePostForm(r, &form)
- if err != nil {
- app.clientError(w, http.StatusBadRequest)
- }
- form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
- form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
- form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
- if !form.Valid() {
- data := app.newCommonData(r)
- w.WriteHeader(http.StatusUnprocessableEntity)
- views.UserLogin("Login", data, form).Render(r.Context(), w)
- return
- }
- id, err := app.users.Authenticate(form.Email, form.Password)
- if err != nil {
- if errors.Is(err, models.ErrInvalidCredentials) {
- form.AddNonFieldError("Email or password is incorrect")
- data := app.newCommonData(r)
- views.UserLogin("Login", data, form).Render(r.Context(), w)
- } else {
- app.serverError(w, r, err)
- }
- return
- }
- err = app.sessionManager.RenewToken(r.Context())
- if err != nil {
- app.serverError(w, r, err)
- return
- }
- app.sessionManager.Put(r.Context(), "authenticatedUserId", id)
- http.Redirect(w, r, "/", http.StatusSeeOther)
+ var form forms.UserLoginForm
+ err := app.decodePostForm(r, &form)
+ if err != nil {
+ app.clientError(w, http.StatusBadRequest)
+ }
+ form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
+ form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
+ form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
+ if !form.Valid() {
+ data := app.newCommonData(r)
+ w.WriteHeader(http.StatusUnprocessableEntity)
+ views.UserLogin("Login", data, form).Render(r.Context(), w)
+ return
+ }
+ id, err := app.users.Authenticate(form.Email, form.Password)
+ if err != nil {
+ if errors.Is(err, models.ErrInvalidCredentials) {
+ form.AddNonFieldError("Email or password is incorrect")
+ data := app.newCommonData(r)
+ views.UserLogin("Login", data, form).Render(r.Context(), w)
+ } else {
+ app.serverError(w, r, err)
+ }
+ return
+ }
+ err = app.sessionManager.RenewToken(r.Context())
+ if err != nil {
+ app.serverError(w, r, err)
+ return
+ }
+ app.sessionManager.Put(r.Context(), "authenticatedUserId", id)
+ http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (app *application) postUserLogout(w http.ResponseWriter, r *http.Request) {
- err := app.sessionManager.RenewToken(r.Context())
- if err != nil {
- app.serverError(w, r, err)
- return
- }
- app.sessionManager.Remove(r.Context(), "authenticatedUserId")
- app.sessionManager.Put(r.Context(), "flash", "You've been logged out successfully!")
- http.Redirect(w, r, "/", http.StatusSeeOther)
+ err := app.sessionManager.RenewToken(r.Context())
+ if err != nil {
+ app.serverError(w, r, err)
+ return
+ }
+ app.sessionManager.Remove(r.Context(), "authenticatedUserId")
+ app.sessionManager.Put(r.Context(), "flash", "You've been logged out successfully!")
+ 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) 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))
- if err != nil {
- if errors.Is(err, models.ErrNoRecord) {
- http.NotFound(w, r)
- } else {
- app.serverError(w, r, err)
- }
- return
- }
- data := app.newCommonData(r)
- views.UserProfile(user.Username, data, user).Render(r.Context(), w)
+ slug := r.PathValue("id")
+ user, err := app.users.Get(slugToShortId(slug))
+ if err != nil {
+ if errors.Is(err, models.ErrNoRecord) {
+ http.NotFound(w, r)
+ } else {
+ app.serverError(w, r, err)
+ }
+ return
+ }
+ data := app.newCommonData(r)
+ views.UserProfile(user.Username, data, user).Render(r.Context(), w)
}
diff --git a/cmd/web/handlers_website.go b/cmd/web/handlers_website.go
new file mode 100644
index 0000000..93df46f
--- /dev/null
+++ b/cmd/web/handlers_website.go
@@ -0,0 +1,92 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
+ "git.32bit.cafe/32bitcafe/guestbook/internal/models"
+ "git.32bit.cafe/32bitcafe/guestbook/internal/validator"
+ "git.32bit.cafe/32bitcafe/guestbook/ui/views"
+)
+
+func (app *application) getWebsiteCreate(w http.ResponseWriter, r *http.Request) {
+ form := forms.WebsiteCreateForm{}
+ data := app.newCommonData(r)
+ views.WebsiteCreate("Add Website", data, form).Render(r.Context(), w)
+}
+
+func (app *application) postWebsiteCreate(w http.ResponseWriter, r *http.Request) {
+ userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
+ var form forms.WebsiteCreateForm
+ err := app.decodePostForm(r, &form)
+ if err != nil {
+ app.clientError(w, http.StatusBadRequest)
+ return
+ }
+
+ form.CheckField(validator.NotBlank(form.AuthorName), "authorName", "This field cannot be blank")
+ form.CheckField(validator.MaxChars(form.AuthorName, 256), "authorName", "This field cannot exceed 256 characters")
+ form.CheckField(validator.NotBlank(form.Name), "sitename", "This field cannot be blank")
+ form.CheckField(validator.MaxChars(form.Name, 256), "sitename", "This field cannot exceed 256 characters")
+ form.CheckField(validator.NotBlank(form.SiteUrl), "siteurl", "This field cannot be blank")
+ form.CheckField(validator.MaxChars(form.SiteUrl, 512), "siteurl", "This field cannot exceed 512 characters")
+
+ if !form.Valid() {
+ data := app.newCommonData(r)
+ w.WriteHeader(http.StatusUnprocessableEntity)
+ views.WebsiteCreate("Add a Website", data, form).Render(r.Context(), w)
+ }
+ websiteShortID := app.createShortId()
+ websiteId, err := app.websites.Insert(websiteShortID, userId, form.Name, form.SiteUrl, form.AuthorName)
+ if err != nil {
+ app.serverError(w, r, err)
+ return
+ }
+ // TODO: how to handle website creation success but guestbook creation failure?
+ guestbookShortID := app.createShortId()
+ _, err = app.guestbooks.Insert(guestbookShortID, userId, websiteId)
+ if err != nil {
+ app.serverError(w, r, err)
+ return
+ }
+ app.sessionManager.Put(r.Context(), "flash", "Website successfully registered!")
+ if r.Header.Get("HX-Request") == "true" {
+ w.Header().Add("HX-Trigger", "newWebsite")
+ views.WebsiteCreateButton().Render(r.Context(), w)
+ return
+ }
+ http.Redirect(w, r, fmt.Sprintf("/websites/%s", shortIdToSlug(websiteShortID)), http.StatusSeeOther)
+}
+
+func (app *application) getWebsiteDashboard(w http.ResponseWriter, r *http.Request) {
+ slug := r.PathValue("id")
+ user := app.getCurrentUser(r)
+ 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
+ }
+ if user.ID != website.UserId {
+ app.clientError(w, http.StatusUnauthorized)
+ }
+ data := app.newCommonData(r)
+ views.WebsiteDashboard("Guestbook", data, website).Render(r.Context(), w)
+}
+
+func (app *application) getWebsiteList(w http.ResponseWriter, r *http.Request) {
+
+ userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
+ websites, err := app.websites.GetAll(userId)
+ if err != nil {
+ app.serverError(w, r, err)
+ return
+ }
+ data := app.newCommonData(r)
+ views.WebsiteList("My Websites", data, websites).Render(r.Context(), w)
+}
diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go
index e19001c..164f603 100644
--- a/cmd/web/helpers.go
+++ b/cmd/web/helpers.go
@@ -9,133 +9,101 @@ import (
"time"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
+ "git.32bit.cafe/32bitcafe/guestbook/ui/views"
"github.com/gorilla/schema"
+ "github.com/justinas/nosurf"
)
func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) {
- var (
- method = r.Method
- uri = r.URL.RequestURI()
- )
+ var (
+ method = r.Method
+ uri = r.URL.RequestURI()
+ )
- app.logger.Error(err.Error(), "method", method, "uri", uri)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ app.logger.Error(err.Error(), "method", method, "uri", uri)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
func (app *application) clientError(w http.ResponseWriter, status int) {
- http.Error(w, http.StatusText(status), status)
+ http.Error(w, http.StatusText(status), status)
}
-func (app *application) renderHTMX(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) {
- ts, ok := app.templateCacheHTMX[page]
- if !ok {
- err := fmt.Errorf("the template %s does not exist", page)
- app.serverError(w, r, err)
- return
- }
-
- w.WriteHeader(status)
- err := ts.Execute(w, data)
- if err != nil {
- app.serverError(w, r, err)
- }
+func (app *application) nextSequence() uint16 {
+ val := app.sequence
+ if app.sequence == math.MaxUint16 {
+ app.sequence = 0
+ } else {
+ app.sequence += 1
+ }
+ return val
}
-func (app *application) renderFullPage(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) {
- ts, ok := app.templateCache[page]
- if !ok {
- err := fmt.Errorf("the template %s does not exist", page)
- app.serverError(w, r, err)
- return
- }
-
- w.WriteHeader(status)
- err := ts.ExecuteTemplate(w, "base", data)
- if err != nil {
- app.serverError(w, r, err)
- }
-}
-
-func (app *application) render(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) {
- ts, ok := app.templateCache[page]
- if !ok {
- err := fmt.Errorf("the template %s does not exist", page)
- app.serverError(w, r, err)
- return
- }
-
- w.WriteHeader(status)
- err := ts.ExecuteTemplate(w, "base", data)
- if err != nil {
- app.serverError(w, r, err)
- }
-}
-
-func (app *application) nextSequence () uint16 {
- val := app.sequence
- if app.sequence == math.MaxUint16 {
- app.sequence = 0
- } else {
- app.sequence += 1
- }
- return val
-}
-
-func (app *application) createShortId () uint64 {
- now := time.Now().UTC()
- epoch, err := time.Parse(time.RFC822Z, "01 Jan 20 00:00 -0000")
- if err != nil {
- fmt.Println(err)
- return 0
- }
- d := now.Sub(epoch)
- ms := d.Milliseconds()
- seq := app.nextSequence()
- return (uint64(ms) & 0x0FFFFFFFFFFFFFFF) | (uint64(seq) << 48)
+func (app *application) createShortId() uint64 {
+ now := time.Now().UTC()
+ epoch, err := time.Parse(time.RFC822Z, "01 Jan 20 00:00 -0000")
+ if err != nil {
+ fmt.Println(err)
+ return 0
+ }
+ d := now.Sub(epoch)
+ ms := d.Milliseconds()
+ seq := app.nextSequence()
+ return (uint64(ms) & 0x0FFFFFFFFFFFFFFF) | (uint64(seq) << 48)
}
func shortIdToSlug(id uint64) string {
- slug := strconv.FormatUint(id, 36)
- return slug
+ slug := strconv.FormatUint(id, 36)
+ return slug
}
func slugToShortId(slug string) uint64 {
- id, _ := strconv.ParseUint(slug, 36, 64)
- return id
+ id, _ := strconv.ParseUint(slug, 36, 64)
+ return id
}
func (app *application) decodePostForm(r *http.Request, dst any) error {
- err := r.ParseForm()
- if err != nil {
- return err
- }
+ err := r.ParseForm()
+ if err != nil {
+ return err
+ }
- err = app.formDecoder.Decode(dst, r.PostForm)
- if err != nil {
- var multiErrors *schema.MultiError
- if !errors.As(err, &multiErrors) {
- panic(err)
- }
- return err
- }
- return nil
+ err = app.formDecoder.Decode(dst, r.PostForm)
+ if err != nil {
+ var multiErrors *schema.MultiError
+ if !errors.As(err, &multiErrors) {
+ panic(err)
+ }
+ return err
+ }
+ return nil
}
func (app *application) isAuthenticated(r *http.Request) bool {
- isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool)
- if !ok {
- return false
- }
- return isAuthenticated
+ isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool)
+ if !ok {
+ return false
+ }
+ return isAuthenticated
}
func (app *application) getCurrentUser(r *http.Request) *models.User {
- if !app.isAuthenticated(r) {
- return nil
- }
- user, ok := r.Context().Value(userNameContextKey).(models.User)
- if !ok {
- return nil
- }
- return &user
+ if !app.isAuthenticated(r) {
+ return nil
+ }
+ user, ok := r.Context().Value(userNameContextKey).(models.User)
+ if !ok {
+ return nil
+ }
+ return &user
+}
+
+func (app *application) newCommonData(r *http.Request) views.CommonData {
+ return views.CommonData{
+ CurrentYear: time.Now().Year(),
+ Flash: app.sessionManager.PopString(r.Context(), "flash"),
+ IsAuthenticated: app.isAuthenticated(r),
+ CSRFToken: nosurf.Token(r),
+ CurrentUser: app.getCurrentUser(r),
+ IsHtmx: r.Header.Get("Hx-Request") == "true",
+ }
}
diff --git a/cmd/web/main.go b/cmd/web/main.go
index 4e715de..7a6d51b 100644
--- a/cmd/web/main.go
+++ b/cmd/web/main.go
@@ -7,7 +7,6 @@ import (
"log/slog"
"net/http"
"os"
- "text/template"
"time"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
@@ -18,90 +17,76 @@ import (
)
type application struct {
- sequence uint16
- logger *slog.Logger
- templateCache map[string]*template.Template
- templateCacheHTMX map[string]*template.Template
- guestbooks *models.GuestbookModel
- users *models.UserModel
- guestbookComments *models.GuestbookCommentModel
- sessionManager *scs.SessionManager
- formDecoder *schema.Decoder
+ sequence uint16
+ logger *slog.Logger
+ websites *models.WebsiteModel
+ guestbooks *models.GuestbookModel
+ users *models.UserModel
+ guestbookComments *models.GuestbookCommentModel
+ sessionManager *scs.SessionManager
+ formDecoder *schema.Decoder
}
func main() {
- addr := flag.String("addr", ":3000", "HTTP network address")
- dsn := flag.String("dsn", "guestbook.db", "data source name")
- flag.Parse()
+ addr := flag.String("addr", ":3000", "HTTP network address")
+ dsn := flag.String("dsn", "guestbook.db", "data source name")
+ flag.Parse()
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
+ logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
- db, err := openDB(*dsn)
- if err != nil {
- logger.Error(err.Error())
- os.Exit(1)
- }
- defer db.Close()
+ db, err := openDB(*dsn)
+ if err != nil {
+ logger.Error(err.Error())
+ os.Exit(1)
+ }
+ defer db.Close()
- templateCache, err := newTemplateCache()
- if err != nil {
- logger.Error(err.Error())
- os.Exit(1)
- }
+ sessionManager := scs.New()
+ sessionManager.Store = sqlite3store.New(db)
+ sessionManager.Lifetime = 12 * time.Hour
- templateCacheHTMX, err := newHTMXTemplateCache()
- if err != nil {
- logger.Error(err.Error())
- os.Exit(1)
- }
+ formDecoder := schema.NewDecoder()
+ formDecoder.IgnoreUnknownKeys(true)
- sessionManager := scs.New()
- sessionManager.Store = sqlite3store.New(db)
- sessionManager.Lifetime = 12 * time.Hour
+ app := &application{
+ sequence: 0,
+ logger: logger,
+ sessionManager: sessionManager,
+ websites: &models.WebsiteModel{DB: db},
+ guestbooks: &models.GuestbookModel{DB: db},
+ users: &models.UserModel{DB: db},
+ guestbookComments: &models.GuestbookCommentModel{DB: db},
+ formDecoder: formDecoder,
+ }
- formDecoder := schema.NewDecoder()
- formDecoder.IgnoreUnknownKeys(true)
+ tlsConfig := &tls.Config{
+ CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
+ }
- app := &application{
- sequence: 0,
- templateCache: templateCache,
- templateCacheHTMX: templateCacheHTMX,
- logger: logger,
- sessionManager: sessionManager,
- guestbooks: &models.GuestbookModel{DB: db},
- users: &models.UserModel{DB: db},
- guestbookComments: &models.GuestbookCommentModel{DB: db},
- formDecoder: formDecoder,
- }
+ srv := &http.Server{
+ Addr: *addr,
+ Handler: app.routes(),
+ ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
+ TLSConfig: tlsConfig,
+ IdleTimeout: time.Minute,
+ ReadTimeout: 5 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ }
- tlsConfig := &tls.Config{
- CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
- }
+ logger.Info("Starting server", slog.Any("addr", *addr))
- srv := &http.Server {
- Addr: *addr,
- Handler: app.routes(),
- ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
- TLSConfig: tlsConfig,
- IdleTimeout: time.Minute,
- ReadTimeout: 5 * time.Second,
- WriteTimeout: 10 * time.Second,
- }
-
- logger.Info("Starting server", slog.Any("addr", *addr))
-
- err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem")
- logger.Error(err.Error())
- os.Exit(1)
+ err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem")
+ logger.Error(err.Error())
+ os.Exit(1)
}
func openDB(dsn string) (*sql.DB, error) {
- db, err := sql.Open("sqlite3", dsn)
- if err != nil {
- return nil, err
- }
- if err = db.Ping(); err != nil {
- return nil, err
- }
- return db, nil
+ db, err := sql.Open("sqlite3", dsn)
+ if err != nil {
+ return nil, err
+ }
+ if err = db.Ping(); err != nil {
+ return nil, err
+ }
+ return db, nil
}
diff --git a/cmd/web/routes.go b/cmd/web/routes.go
index 81c8b17..fd45458 100644
--- a/cmd/web/routes.go
+++ b/cmd/web/routes.go
@@ -7,35 +7,33 @@ import (
)
func (app *application) routes() http.Handler {
- mux := http.NewServeMux()
- fileServer := http.FileServer(http.Dir("./ui/static"))
- mux.Handle("GET /static/", http.StripPrefix("/static", fileServer))
-
- dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
- standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders)
+ mux := http.NewServeMux()
+ fileServer := http.FileServer(http.Dir("./ui/static"))
+ mux.Handle("GET /static/", http.StripPrefix("/static", fileServer))
- mux.Handle("/{$}", dynamic.ThenFunc(app.home))
- mux.Handle("POST /guestbooks/{id}/comments/create", standard.ThenFunc(app.postGuestbookCommentCreate))
- mux.Handle("GET /guestbooks/{id}", dynamic.ThenFunc(app.getGuestbook))
- mux.Handle("GET /users/register", dynamic.ThenFunc(app.getUserRegister))
- mux.Handle("POST /users/register", dynamic.ThenFunc(app.postUserRegister))
- mux.Handle("GET /users/login", dynamic.ThenFunc(app.getUserLogin))
- mux.Handle("POST /users/login", dynamic.ThenFunc(app.postUserLogin))
+ dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
+ standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders)
- protected := dynamic.Append(app.requireAuthentication)
+ mux.Handle("/{$}", dynamic.ThenFunc(app.home))
+ mux.Handle("POST /websites/{id}/guestbook/comments/create", standard.ThenFunc(app.postGuestbookCommentCreate))
+ mux.Handle("GET /websites/{id}/guestbook", dynamic.ThenFunc(app.getGuestbook))
+ mux.Handle("GET /users/register", dynamic.ThenFunc(app.getUserRegister))
+ mux.Handle("POST /users/register", dynamic.ThenFunc(app.postUserRegister))
+ mux.Handle("GET /users/login", dynamic.ThenFunc(app.getUserLogin))
+ mux.Handle("POST /users/login", dynamic.ThenFunc(app.postUserLogin))
- mux.Handle("GET /users", protected.ThenFunc(app.getUsersList))
- mux.Handle("GET /users/{id}", protected.ThenFunc(app.getUser))
- mux.Handle("POST /users/logout", protected.ThenFunc(app.postUserLogout))
- mux.Handle("GET /guestbooks", protected.ThenFunc(app.getGuestbookList))
- mux.Handle("GET /guestbooks/create", protected.ThenFunc(app.getGuestbookCreate))
- mux.Handle("POST /guestbooks/create", protected.ThenFunc(app.postGuestbookCreate))
- mux.Handle("GET /guestbooks/{id}/dashboard", protected.ThenFunc(app.getGuestbookDashboard))
- mux.Handle("GET /guestbooks/{id}/dashboard/comments", protected.ThenFunc(app.getGuestbookComments))
- mux.Handle("GET /guestbooks/{id}/comments/create", protected.ThenFunc(app.getGuestbookCommentCreate))
- mux.Handle("GET /guestbooks/{id}/dashboard/comments/queue", protected.ThenFunc(app.getCommentQueue))
+ protected := dynamic.Append(app.requireAuthentication)
+ // mux.Handle("GET /users", protected.ThenFunc(app.getUsersList))
+ mux.Handle("GET /users/{id}", protected.ThenFunc(app.getUser))
+ mux.Handle("POST /users/logout", protected.ThenFunc(app.postUserLogout))
+ mux.Handle("GET /websites", protected.ThenFunc(app.getWebsiteList))
+ mux.Handle("GET /websites/create", protected.ThenFunc(app.getWebsiteCreate))
+ mux.Handle("POST /websites/create", protected.ThenFunc(app.postWebsiteCreate))
+ mux.Handle("GET /websites/{id}/dashboard", protected.ThenFunc(app.getWebsiteDashboard))
+ mux.Handle("GET /websites/{id}/dashboard/guestbook/comments", protected.ThenFunc(app.getGuestbookComments))
+ mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/queue", protected.ThenFunc(app.getCommentQueue))
+ mux.Handle("GET /websites/{id}/guestbook/comments/create", protected.ThenFunc(app.getGuestbookCommentCreate))
- return standard.Then(mux)
+ return standard.Then(mux)
}
-
diff --git a/cmd/web/templates.go b/cmd/web/templates.go
deleted file mode 100644
index 7d2908b..0000000
--- a/cmd/web/templates.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package main
-
-import (
- "net/http"
- "path/filepath"
- "text/template"
- "time"
-
- "git.32bit.cafe/32bitcafe/guestbook/internal/models"
- "git.32bit.cafe/32bitcafe/guestbook/ui/views"
- "github.com/justinas/nosurf"
-)
-
-type templateData struct {
- CurrentYear int
- User models.User
- Users []models.User
- Guestbook models.Guestbook
- Guestbooks []models.Guestbook
- Comment models.GuestbookComment
- Comments []models.GuestbookComment
- Flash string
- Form any
- IsAuthenticated bool
- CSRFToken string
- CurrentUser *models.User
-}
-
-func humanDate(t time.Time) string {
- return t.Format("02 Jan 2006 at 15:04")
-}
-
-var functions = template.FuncMap {
- "humanDate": humanDate,
- "shortIdToSlug": shortIdToSlug,
- "slugToShortId": slugToShortId,
-}
-
-func newHTMXTemplateCache() (map[string]*template.Template, error) {
- cache := map[string]*template.Template{}
- pages, err := filepath.Glob("./ui/html/htmx/*.part.html")
- if err != nil {
- return nil, err
- }
- for _, page := range pages {
- name := filepath.Base(page)
- ts, err := template.New(name).Funcs(functions).ParseFiles(page)
- if err != nil {
- return nil, err
- }
- cache[name] = ts
- }
- return cache, nil
-}
-
-func newTemplateCache() (map[string]*template.Template, error) {
- cache := map[string]*template.Template{}
- pages, err := filepath.Glob("./ui/html/pages/*.tmpl.html")
- if err != nil {
- return nil, err
- }
- for _, page := range pages {
- name := filepath.Base(page)
- ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.tmpl.html")
- if err != nil {
- return nil, err
- }
- ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl.html")
- if err != nil {
- return nil, err
- }
- ts, err = ts.ParseFiles(page)
- if err != nil {
- return nil, err
- }
- cache[name] = ts
- }
- return cache, nil
-}
-
-func (app *application) newCommonData(r *http.Request) views.CommonData {
- return views.CommonData {
- CurrentYear: time.Now().Year(),
- Flash: app.sessionManager.PopString(r.Context(), "flash"),
- IsAuthenticated: app.isAuthenticated(r),
- CSRFToken: nosurf.Token(r),
- CurrentUser: app.getCurrentUser(r),
- IsHtmx: r.Header.Get("Hx-Request") == "true",
- }
-}
-
-func (app *application) newTemplateData(r *http.Request) templateData {
- return templateData {
- CurrentYear: time.Now().Year(),
- Flash: app.sessionManager.PopString(r.Context(), "flash"),
- IsAuthenticated: app.isAuthenticated(r),
- CSRFToken: nosurf.Token(r),
- CurrentUser: app.getCurrentUser(r),
- }
-}
diff --git a/db/create-session-table-sqlite.sql b/db/create-session-table-sqlite.sql
deleted file mode 100644
index 5dba4f5..0000000
--- a/db/create-session-table-sqlite.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-CREATE TABLE sessions (
- token CHAR(43) primary key,
- data BLOB NOT NULL,
- expiry TEXT NOT NULL
-);
diff --git a/db/create-tables-sqlite.sql b/db/create-tables-sqlite.sql
index 499e068..7b6a121 100644
--- a/db/create-tables-sqlite.sql
+++ b/db/create-tables-sqlite.sql
@@ -9,10 +9,24 @@ CREATE TABLE users (
Created datetime NOT NULL
);
+CREATE TABLE websites (
+ Id integer primary key autoincrement,
+ ShortId integer UNIQUE NOT NULL,
+ Name varchar(256) NOT NULL,
+ SiteUrl varchar(512) NOT NULL,
+ AuthorName varchar(512) NOT NULL,
+ UserId integer NOT NULL,
+ Created datetime NOT NULL,
+ Deleted datetime,
+ FOREIGN KEY (UserId) REFERENCES users(Id)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
CREATE TABLE guestbooks (
Id integer primary key autoincrement,
ShortId integer UNIQUE NOT NULL,
- SiteUrl varchar(512) NOT NULL,
+ WebsiteId integer UNIQUE NOT NULL,
UserId integer NOT NULL,
Created datetime NOT NULL,
IsDeleted boolean NOT NULL DEFAULT FALSE,
@@ -20,6 +34,9 @@ CREATE TABLE guestbooks (
FOREIGN KEY (UserId) REFERENCES users(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
+ FOREIGN KEY (WebsiteId) REFERENCES websites(Id)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
);
CREATE TABLE guestbook_comments (
@@ -44,3 +61,9 @@ CREATE TABLE guestbook_comments (
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
+
+CREATE TABLE sessions (
+ token CHAR(43) primary key,
+ data BLOB NOT NULL,
+ expiry TEXT NOT NULL
+);
diff --git a/internal/forms/forms.go b/internal/forms/forms.go
index fd811f4..8d5664f 100644
--- a/internal/forms/forms.go
+++ b/internal/forms/forms.go
@@ -3,23 +3,29 @@ package forms
import "git.32bit.cafe/32bitcafe/guestbook/internal/validator"
type UserRegistrationForm struct {
- Name string `schema:"username"`
- Email string `schema:"email"`
- Password string `schema:"password"`
- validator.Validator `schema:"-"`
+ Name string `schema:"username"`
+ Email string `schema:"email"`
+ Password string `schema:"password"`
+ validator.Validator `schema:"-"`
}
type UserLoginForm struct {
- Email string `schema:"email"`
- Password string `schema:"password"`
- validator.Validator `schema:"-"`
+ Email string `schema:"email"`
+ Password string `schema:"password"`
+ validator.Validator `schema:"-"`
}
type CommentCreateForm struct {
- AuthorName string `schema:"authorname"`
- AuthorEmail string `schema:"authoremail"`
- AuthorSite string `schema:"authorsite"`
- Content string `schema:"content,required"`
- validator.Validator `schema:"-"`
+ AuthorName string `schema:"authorname"`
+ AuthorEmail string `schema:"authoremail"`
+ AuthorSite string `schema:"authorsite"`
+ Content string `schema:"content,required"`
+ validator.Validator `schema:"-"`
}
+type WebsiteCreateForm struct {
+ Name string `schema:"sitename"`
+ SiteUrl string `schema:"siteurl"`
+ AuthorName string `schema:"authorname"`
+ validator.Validator `schema:"-"`
+}
diff --git a/internal/models/guestbook.go b/internal/models/guestbook.go
index 133330a..a216572 100644
--- a/internal/models/guestbook.go
+++ b/internal/models/guestbook.go
@@ -8,8 +8,8 @@ import (
type Guestbook struct {
ID int64
ShortId uint64
- SiteUrl string
UserId int64
+ WebsiteId int64
Created time.Time
IsDeleted bool
IsActive bool
@@ -19,10 +19,10 @@ type GuestbookModel struct {
DB *sql.DB
}
-func (m *GuestbookModel) Insert(shortId uint64, siteUrl string, userId int64) (int64, error) {
- stmt := `INSERT INTO guestbooks (ShortId, SiteUrl, UserId, Created, IsDeleted, IsActive)
+func (m *GuestbookModel) Insert(shortId uint64, userId int64, websiteId int64) (int64, error) {
+ stmt := `INSERT INTO guestbooks (ShortId, UserId, WebsiteId, Created, IsDeleted, IsActive)
VALUES(?, ?, ?, ?, FALSE, TRUE)`
- result, err := m.DB.Exec(stmt, shortId, siteUrl, userId, time.Now().UTC())
+ result, err := m.DB.Exec(stmt, shortId, userId, websiteId, time.Now().UTC())
if err != nil {
return -1, err
}
@@ -34,11 +34,11 @@ func (m *GuestbookModel) Insert(shortId uint64, siteUrl string, userId int64) (i
}
func (m *GuestbookModel) Get(shortId uint64) (Guestbook, error) {
- stmt := `SELECT Id, ShortId, SiteUrl, UserId, Created, IsDeleted, IsActive FROM guestbooks
+ stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, IsDeleted, IsActive FROM guestbooks
WHERE ShortId = ?`
row := m.DB.QueryRow(stmt, shortId)
var g Guestbook
- err := row.Scan(&g.ID, &g.ShortId, &g.SiteUrl, &g.UserId, &g.Created, &g.IsDeleted, &g.IsActive)
+ err := row.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &g.IsDeleted, &g.IsActive)
if err != nil {
return Guestbook{}, err
}
@@ -47,7 +47,7 @@ func (m *GuestbookModel) Get(shortId uint64) (Guestbook, error) {
}
func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) {
- stmt := `SELECT Id, ShortId, SiteUrl, UserId, Created, IsDeleted, IsActive FROM guestbooks
+ stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, IsDeleted, IsActive FROM guestbooks
WHERE UserId = ?`
rows, err := m.DB.Query(stmt, userId)
if err != nil {
@@ -56,7 +56,7 @@ func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) {
var guestbooks []Guestbook
for rows.Next() {
var g Guestbook
- err = rows.Scan(&g.ID, &g.ShortId, &g.SiteUrl, &g.UserId, &g.Created, &g.IsDeleted, &g.IsActive)
+ err = rows.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &g.IsDeleted, &g.IsActive)
if err != nil {
return nil, err
}
diff --git a/internal/models/guestbookcomment.go b/internal/models/guestbookcomment.go
index 05442f5..e936304 100644
--- a/internal/models/guestbookcomment.go
+++ b/internal/models/guestbookcomment.go
@@ -6,110 +6,110 @@ import (
)
type GuestbookComment struct {
- ID int64
- ShortId uint64
- GuestbookId int64
- ParentId int64
- AuthorName string
- AuthorEmail string
- AuthorSite string
- CommentText string
- PageUrl string
- Created time.Time
- IsPublished bool
- IsDeleted bool
+ ID int64
+ ShortId uint64
+ GuestbookId int64
+ ParentId int64
+ AuthorName string
+ AuthorEmail string
+ AuthorSite string
+ CommentText string
+ PageUrl string
+ Created time.Time
+ IsPublished bool
+ IsDeleted bool
}
type GuestbookCommentModel struct {
- DB *sql.DB
+ DB *sql.DB
}
func (m *GuestbookCommentModel) Insert(shortId uint64, guestbookId, parentId int64, authorName,
- authorEmail, authorSite, commentText, pageUrl string, isPublished bool) (int64, error) {
- stmt := `INSERT INTO guestbook_comments (ShortId, GuestbookId, ParentId, AuthorName,
+ authorEmail, authorSite, commentText, pageUrl string, isPublished bool) (int64, error) {
+ stmt := `INSERT INTO guestbook_comments (ShortId, GuestbookId, ParentId, AuthorName,
AuthorEmail, AuthorSite, CommentText, PageUrl, Created, IsPublished, IsDeleted)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE)`
- result, err := m.DB.Exec(stmt, shortId, guestbookId, parentId, authorName, authorEmail,
- authorSite, commentText, pageUrl, time.Now().UTC(), isPublished)
- if err != nil {
- return -1, err
- }
- id, err := result.LastInsertId()
- if err != nil {
- return -1, err
- }
- return id, nil
+ result, err := m.DB.Exec(stmt, shortId, guestbookId, parentId, authorName, authorEmail,
+ authorSite, commentText, pageUrl, time.Now().UTC(), isPublished)
+ if err != nil {
+ return -1, err
+ }
+ id, err := result.LastInsertId()
+ if err != nil {
+ return -1, err
+ }
+ return id, nil
}
func (m *GuestbookCommentModel) Get(shortId uint64) (GuestbookComment, error) {
- stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
+ stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished, IsDeleted FROM guestbook_comments WHERE ShortId = ?`
- row := m.DB.QueryRow(stmt, shortId)
- var c GuestbookComment
- err := row.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
- if err != nil {
- return GuestbookComment{}, err
- }
- return c, nil
+ row := m.DB.QueryRow(stmt, shortId)
+ var c GuestbookComment
+ err := row.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
+ if err != nil {
+ return GuestbookComment{}, err
+ }
+ return c, nil
}
func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, error) {
- stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
+ stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished, IsDeleted
FROM guestbook_comments
WHERE GuestbookId = ? AND IsDeleted = FALSE AND IsPublished = TRUE
ORDER BY Created DESC`
- rows, err := m.DB.Query(stmt, guestbookId)
- if err != nil {
- return nil, err
- }
- var comments []GuestbookComment
- for rows.Next() {
- var c GuestbookComment
- err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
+ rows, err := m.DB.Query(stmt, guestbookId)
if err != nil {
- return nil, err
+ return nil, err
}
- comments = append(comments, c)
- }
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return comments, nil
+ var comments []GuestbookComment
+ for rows.Next() {
+ var c GuestbookComment
+ err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
+ if err != nil {
+ return nil, err
+ }
+ comments = append(comments, c)
+ }
+ if err = rows.Err(); err != nil {
+ return nil, err
+ }
+ return comments, nil
}
func (m *GuestbookCommentModel) GetQueue(guestbookId int64) ([]GuestbookComment, error) {
- stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
+ stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished, IsDeleted
FROM guestbook_comments
WHERE GuestbookId = ? AND IsDeleted = FALSE AND IsPublished = FALSE
ORDER BY Created DESC`
- rows, err := m.DB.Query(stmt, guestbookId)
- if err != nil {
- return nil, err
- }
- var comments []GuestbookComment
- for rows.Next() {
- var c GuestbookComment
- err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
+ rows, err := m.DB.Query(stmt, guestbookId)
if err != nil {
- return nil, err
+ return nil, err
}
- comments = append(comments, c)
- }
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return comments, nil
+ var comments []GuestbookComment
+ for rows.Next() {
+ var c GuestbookComment
+ err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &c.IsDeleted)
+ if err != nil {
+ return nil, err
+ }
+ comments = append(comments, c)
+ }
+ if err = rows.Err(); err != nil {
+ return nil, err
+ }
+ return comments, nil
}
func (m *GuestbookCommentModel) UpdateComment(comment *GuestbookComment) error {
- stmt := `UPDATE guestbook_comments (CommentText, PageUrl, IsPublished, IsDeleted)
+ stmt := `UPDATE guestbook_comments (CommentText, PageUrl, IsPublished, IsDeleted)
VALUES (?, ?, ?, ?)
WHERE Id = ?`
- _, err := m.DB.Exec(stmt, comment.CommentText, comment.PageUrl, comment.IsPublished, comment.IsDeleted, comment.ID)
- if err != nil {
- return err
- }
- return nil
+ _, err := m.DB.Exec(stmt, comment.CommentText, comment.PageUrl, comment.IsPublished, comment.IsDeleted, comment.ID)
+ if err != nil {
+ return err
+ }
+ return nil
}
diff --git a/internal/models/website.go b/internal/models/website.go
new file mode 100644
index 0000000..1b9da9f
--- /dev/null
+++ b/internal/models/website.go
@@ -0,0 +1,102 @@
+package models
+
+import (
+ "database/sql"
+ "time"
+)
+
+type Website struct {
+ ID int64
+ ShortId uint64
+ Name string
+ SiteUrl string
+ AuthorName string
+ UserId int64
+ Created time.Time
+ Deleted time.Time
+ Guestbook Guestbook
+}
+
+type WebsiteModel struct {
+ DB *sql.DB
+}
+
+func (m *WebsiteModel) Insert(shortId uint64, userId int64, siteName, siteUrl, authorName string) (int64, error) {
+ stmt := `INSERT INTO websites (ShortId, Name, SiteUrl, AuthorName, UserId, Created)
+ VALUES (?, ?, ?, ?, ?, ?)`
+ result, err := m.DB.Exec(stmt, shortId, siteName, siteUrl, authorName, userId, time.Now().UTC())
+ if err != nil {
+ return -1, err
+ }
+ id, err := result.LastInsertId()
+ if err != nil {
+ return -1, err
+ }
+ return id, nil
+}
+
+func (m *WebsiteModel) Get(shortId uint64) (Website, error) {
+ stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, w.Deleted,
+ g.Id, g.ShortId, g.Created, g.IsDeleted, g.IsActive
+ FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
+ WHERE w.ShortId = ?`
+ row := m.DB.QueryRow(stmt, shortId)
+ var t sql.NullTime
+ var w Website
+ err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created, &t,
+ &w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsDeleted, &w.Guestbook.IsActive)
+ if err != nil {
+ return Website{}, err
+ }
+ // handle if Deleted is null
+ if t.Valid {
+ w.Deleted = t.Time
+ }
+ return w, nil
+}
+
+func (m *WebsiteModel) GetById(id int64) (Website, error) {
+ stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, w.Deleted,
+ g.Id, g.ShortId, g.Created, g.IsDeleted, g.IsActive
+ FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
+ WHERE w.Id = ?`
+ row := m.DB.QueryRow(stmt, id)
+ var t sql.NullTime
+ var w Website
+ err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created, &t,
+ &w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsDeleted, &w.Guestbook.IsActive)
+ if err != nil {
+ return Website{}, err
+ }
+ // handle if Deleted is null
+ if t.Valid {
+ w.Deleted = t.Time
+ }
+ return w, nil
+}
+
+func (m *WebsiteModel) GetAll(userId int64) ([]Website, error) {
+ stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created, w.Deleted,
+ g.Id, g.ShortId, g.Created, g.IsDeleted, g.IsActive
+ FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
+ WHERE w.UserId = ?`
+ rows, err := m.DB.Query(stmt, userId)
+ if err != nil {
+ return nil, err
+ }
+ var websites []Website
+ for rows.Next() {
+ var t sql.NullTime
+ var w Website
+ err := rows.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created, &t,
+ &w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsDeleted, &w.Guestbook.IsActive)
+ if err != nil {
+ return nil, err
+ }
+ websites = append(websites, w)
+ }
+ if err = rows.Err(); err != nil {
+ return nil, err
+ }
+ return websites, nil
+}
diff --git a/ui/html/base.tmpl.html b/ui/html/base.tmpl.html
deleted file mode 100644
index 1a88342..0000000
--- a/ui/html/base.tmpl.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{{ define "base" }}
-
-
-
- {{ template "title" }} - webweav.ing
-
-
-
-
-
-
-
- {{ template "nav" . }}
-
- {{ with .Flash }}
- {{ . }}
- {{ end }}
- {{ template "main" . }}
-
-
-
-
-{{ end }}
diff --git a/ui/html/htmx/guestbookcreate.part.html b/ui/html/htmx/guestbookcreate.part.html
deleted file mode 100644
index 8f4988c..0000000
--- a/ui/html/htmx/guestbookcreate.part.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/ui/html/htmx/guestbookcreatebutton.part.html b/ui/html/htmx/guestbookcreatebutton.part.html
deleted file mode 100644
index 4c13ddc..0000000
--- a/ui/html/htmx/guestbookcreatebutton.part.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/html/htmx/guestbooklist.part.html b/ui/html/htmx/guestbooklist.part.html
deleted file mode 100644
index e67ec68..0000000
--- a/ui/html/htmx/guestbooklist.part.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/ui/html/htmx/newguestbook.part.html b/ui/html/htmx/newguestbook.part.html
deleted file mode 100644
index 89bbb56..0000000
--- a/ui/html/htmx/newguestbook.part.html
+++ /dev/null
@@ -1,5 +0,0 @@
-{{ with .Guestbook }}
-
- {{ with .SiteUrl }}{{ . }}{{ else }}Untitled{{ end }}
-
-{{ end }}
diff --git a/ui/html/pages/commentcreate.view.tmpl.html b/ui/html/pages/commentcreate.view.tmpl.html
deleted file mode 100644
index 56ecba8..0000000
--- a/ui/html/pages/commentcreate.view.tmpl.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{{ define "title" }}New Comment{{ end }}
-{{ define "main" }}
-
-{{ end }}
diff --git a/ui/html/pages/guestbook.view.tmpl.html b/ui/html/pages/guestbook.view.tmpl.html
deleted file mode 100644
index 36815d2..0000000
--- a/ui/html/pages/guestbook.view.tmpl.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{{ define "title" }} Guestbook View {{ end }}
-{{ define "main" }}
-Guestbook for {{ .Guestbook.SiteUrl }}
-
- New Comment
-
-{{ range .Comments }}
-
-
{{ .AuthorName }}
- {{ .Created.Local.Format "01-02-2006 03:04PM" }}
-
- {{ .CommentText }}
-
-
-{{ else }}
-No comments yet!
-{{ end }}
-{{ end }}
diff --git a/ui/html/pages/guestbookcreate.view.tmpl.html b/ui/html/pages/guestbookcreate.view.tmpl.html
deleted file mode 100644
index 313bb3a..0000000
--- a/ui/html/pages/guestbookcreate.view.tmpl.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{{ define "title" }}Create a Guestbook{{ end }}
-{{ define "main" }}
-{{ template "guestbookcreate" }}
-{{ end }}
diff --git a/ui/html/pages/guestbooklist.view.tmpl.html b/ui/html/pages/guestbooklist.view.tmpl.html
deleted file mode 100644
index 59857e4..0000000
--- a/ui/html/pages/guestbooklist.view.tmpl.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{{ define "title" }} Guestbooks {{ end }}
-{{ define "main" }}
-Guestbooks run by {{ .User.Username }}
-
-
-
-
-{{ end }}
diff --git a/ui/html/pages/home.tmpl.html b/ui/html/pages/home.tmpl.html
deleted file mode 100644
index eab8f4e..0000000
--- a/ui/html/pages/home.tmpl.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{{ define "title" }}Home{{ end }}
-{{ define "main" }}
-{{ if .IsAuthenticated }}
-Tools
-
- Guestbooks
-
-{{ else }}
-Welcome
-Welcome to webweav.ing, a collection of webmastery tools created by the 32-Bit Cafe.
-{{ end }}
-{{ end }}
diff --git a/ui/html/pages/login.view.tmpl.html b/ui/html/pages/login.view.tmpl.html
deleted file mode 100644
index 7c49690..0000000
--- a/ui/html/pages/login.view.tmpl.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{{define "title"}}Login{{end}}
-{{define "main"}}
-
-{{end}}
diff --git a/ui/html/pages/user.view.tmpl.html b/ui/html/pages/user.view.tmpl.html
deleted file mode 100644
index 88f1d30..0000000
--- a/ui/html/pages/user.view.tmpl.html
+++ /dev/null
@@ -1,5 +0,0 @@
-{{ define "title" }}{{ .User.Username }}{{ end }}
-{{ define "main" }}
-{{ .User.Username }}
-{{ .User.Email }}
-{{ end }}
diff --git a/ui/html/pages/usercreate.view.tmpl.html b/ui/html/pages/usercreate.view.tmpl.html
deleted file mode 100644
index bea95fb..0000000
--- a/ui/html/pages/usercreate.view.tmpl.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{ define "title" }}User Registration{{ end }}
-{{ define "main" }}
-
-{{ end }}
diff --git a/ui/html/pages/userlist.view.tmpl.html b/ui/html/pages/userlist.view.tmpl.html
deleted file mode 100644
index 69fb054..0000000
--- a/ui/html/pages/userlist.view.tmpl.html
+++ /dev/null
@@ -1,9 +0,0 @@
-{{ define "title" }}Users{{ end }}
-{{ define "main" }}
- Users
- {{ range .Users }}
-
- {{ .Username }}
-
- {{ end }}
-{{ end }}
diff --git a/ui/html/partials/guestbookcreate.part.tmpl.html b/ui/html/partials/guestbookcreate.part.tmpl.html
deleted file mode 100644
index 55ce6a4..0000000
--- a/ui/html/partials/guestbookcreate.part.tmpl.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{{ define "guestbookcreate" }}
-
-{{ end }}
diff --git a/ui/html/partials/nav.tmpl.html b/ui/html/partials/nav.tmpl.html
deleted file mode 100644
index 7fcf69e..0000000
--- a/ui/html/partials/nav.tmpl.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{{ define "nav" }}
-
-{{ end }}
diff --git a/ui/views/common.templ b/ui/views/common.templ
index b89b543..3f081a3 100644
--- a/ui/views/common.templ
+++ b/ui/views/common.templ
@@ -30,14 +30,13 @@ templ commonHeader() {
templ topNav(data CommonData) {