Compare commits

...

3 Commits

9 changed files with 492 additions and 459 deletions

View File

@ -1,13 +1,7 @@
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"
)
@ -15,319 +9,3 @@ func (app *application) home(w http.ResponseWriter, r *http.Request) {
data := app.newCommonData(r)
views.Home("Home", data).Render(r.Context(), w)
}
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)
}
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)
}
func (app *application) getUserLogin(w http.ResponseWriter, r *http.Request) {
views.UserLogin("Login", app.newCommonData(r), forms.UserLoginForm{}).Render(r.Context(), w)
}
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)
}
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)
}
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)
}
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)
if err != nil {
app.serverError(w, r, err)
return
}
data := app.newCommonData(r)
views.GuestbookList("Guestbooks", data, guestbooks).Render(r.Context(), w)
}
func (app *application) getGuestbook(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
}
comments, err := app.guestbookComments.GetAll(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
}
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)
}
func (app *application) getGuestbookComments(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
}
comments, err := app.guestbookComments.GetAll(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)
}
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))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
data := app.newTemplateData(r)
data.Guestbook = guestbook
data.Form = forms.CommentCreateForm{}
app.render(w, r, http.StatusOK, "commentcreate.view.tmpl.html", data)
}
func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *http.Request) {
guestbookSlug := r.PathValue("id")
guestbook, err := app.guestbooks.Get(slugToShortId(guestbookSlug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
var form forms.CommentCreateForm
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 be more than 256 characters long")
form.CheckField(validator.NotBlank(form.AuthorEmail), "authorEmail", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.AuthorEmail, 256), "authorEmail", "This field cannot be more than 256 characters long")
form.CheckField(validator.NotBlank(form.AuthorSite), "authorSite", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.AuthorSite, 256), "authorSite", "This field cannot be more than 256 characters long")
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)
return
}
shortId := app.createShortId()
_, err = app.guestbookComments.Insert(shortId, 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)
}
func (app *application) updateGuestbookComment(w http.ResponseWriter, r *http.Request) {
}
func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Request) {
// slug := r.PathValue("id")
// shortId := slugToShortId(slug)
// app.guestbookComments.Delete(shortId)
}
func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request) []models.GuestbookComment {
guestbookSlug := r.PathValue("id")
guestbook, err := app.guestbooks.Get(slugToShortId(guestbookSlug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return []models.GuestbookComment{}
}
comments, err := app.guestbookComments.GetQueue(guestbook.ID)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return []models.GuestbookComment{}
}
return comments
}

View File

@ -0,0 +1,218 @@
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) 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)
if err != nil {
app.serverError(w, r, err)
return
}
data := app.newCommonData(r)
views.GuestbookList("Guestbooks", data, guestbooks).Render(r.Context(), w)
}
func (app *application) getGuestbook(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
}
comments, err := app.guestbookComments.GetAll(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
}
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)
}
func (app *application) getGuestbookComments(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
}
comments, err := app.guestbookComments.GetAll(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)
}
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))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
data := app.newTemplateData(r)
data.Guestbook = guestbook
data.Form = forms.CommentCreateForm{}
app.render(w, r, http.StatusOK, "commentcreate.view.tmpl.html", data)
}
func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *http.Request) {
guestbookSlug := r.PathValue("id")
guestbook, err := app.guestbooks.Get(slugToShortId(guestbookSlug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
var form forms.CommentCreateForm
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 be more than 256 characters long")
form.CheckField(validator.NotBlank(form.AuthorEmail), "authorEmail", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.AuthorEmail, 256), "authorEmail", "This field cannot be more than 256 characters long")
form.CheckField(validator.NotBlank(form.AuthorSite), "authorSite", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.AuthorSite, 256), "authorSite", "This field cannot be more than 256 characters long")
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)
return
}
shortId := app.createShortId()
_, err = app.guestbookComments.Insert(shortId, 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)
}
func (app *application) updateGuestbookComment(w http.ResponseWriter, r *http.Request) {
}
func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Request) {
// slug := r.PathValue("id")
// shortId := slugToShortId(slug)
// app.guestbookComments.Delete(shortId)
}
func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request) {
guestbookSlug := r.PathValue("id")
guestbook, err := app.guestbooks.Get(slugToShortId(guestbookSlug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
comments, err := app.guestbookComments.GetQueue(guestbook.ID)
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.GuestbookDashboardCommentsView("Message Queue", data, guestbook, comments).Render(r.Context(), w)
}
func (app *application) putHideGuestbookComment(w http.ResponseWriter, r *http.Request) {
}
func (app *application) putDeleteGuestbookComment(w http.ResponseWriter, r *http.Request) {
}

131
cmd/web/handlers_user.go Normal file
View File

@ -0,0 +1,131 @@
package main
import (
"errors"
"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) getUserRegister(w http.ResponseWriter, r *http.Request) {
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)
}
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)
}
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)
}
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)
}
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)
}

View File

@ -2,73 +2,68 @@ package models
import (
"database/sql"
"strconv"
"time"
)
type Guestbook struct {
ID int64
ShortId uint64
SiteUrl string
UserId int64
Created time.Time
IsDeleted bool
IsActive bool
}
func (gb Guestbook) Slug() string {
return strconv.FormatUint(gb.ShortId, 36)
ID int64
ShortId uint64
SiteUrl string
UserId int64
Created time.Time
IsDeleted bool
IsActive bool
}
type GuestbookModel struct {
DB *sql.DB
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)
stmt := `INSERT INTO guestbooks (ShortId, SiteUrl, UserId, Created, IsDeleted, IsActive)
VALUES(?, ?, ?, ?, FALSE, TRUE)`
result, err := m.DB.Exec(stmt, shortId, siteUrl, userId, time.Now().UTC())
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, siteUrl, 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 *GuestbookModel) Get(shortId uint64) (Guestbook, error) {
stmt := `SELECT Id, ShortId, SiteUrl, UserId, Created, IsDeleted, IsActive FROM guestbooks
stmt := `SELECT Id, ShortId, SiteUrl, UserId, 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)
if err != nil {
return Guestbook{}, err
}
return g, nil
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)
if err != nil {
return Guestbook{}, err
}
return g, nil
}
func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) {
stmt := `SELECT Id, ShortId, SiteUrl, UserId, Created, IsDeleted, IsActive FROM guestbooks
stmt := `SELECT Id, ShortId, SiteUrl, UserId, Created, IsDeleted, IsActive FROM guestbooks
WHERE UserId = ?`
rows, err := m.DB.Query(stmt, userId)
if err != nil {
return nil, err
}
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)
if err != nil {
return nil, err
}
guestbooks = append(guestbooks, g)
}
if err = rows.Err(); err != nil {
return nil, err
}
return guestbooks, nil
rows, err := m.DB.Query(stmt, userId)
if err != nil {
return nil, err
}
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)
if err != nil {
return nil, err
}
guestbooks = append(guestbooks, g)
}
if err = rows.Err(); err != nil {
return nil, err
}
return guestbooks, nil
}

View File

@ -11,114 +11,117 @@ import (
)
type User struct {
ID int
ShortId uint64
Username string
Email string
IsDeleted bool
IsBanned bool
HashedPassword []byte
Created time.Time
ID int
ShortId uint64
Username string
Email string
IsDeleted bool
IsBanned bool
HashedPassword []byte
Created time.Time
}
type UserModel struct {
DB *sql.DB
DB *sql.DB
}
func (m *UserModel) Insert(shortId uint64, username string, email string, password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return err
}
stmt := `INSERT INTO users (ShortId, Username, Email, IsDeleted, IsBanned, HashedPassword, Created)
VALUES (?, ?, ?, FALSE, FALSE, ?, ?)`
_, err = m.DB.Exec(stmt, shortId, username, email, hashedPassword, time.Now().UTC())
if err != nil {
if sqliteError, ok := err.(sqlite3.Error); ok {
if sqliteError.ExtendedCode == 2067 && strings.Contains(sqliteError.Error(), "Email") {
return ErrDuplicateEmail
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return err
}
return err
}
return nil
stmt := `INSERT INTO users (ShortId, Username, Email, IsDeleted, IsBanned, HashedPassword, Created)
VALUES (?, ?, ?, FALSE, FALSE, ?, ?)`
_, err = m.DB.Exec(stmt, shortId, username, email, hashedPassword, time.Now().UTC())
if err != nil {
if sqliteError, ok := err.(sqlite3.Error); ok {
if sqliteError.ExtendedCode == 2067 && strings.Contains(sqliteError.Error(), "Email") {
return ErrDuplicateEmail
}
}
return err
}
return nil
}
func (m *UserModel) Get(id uint64) (User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE ShortId = ? AND IsDeleted = FALSE`
row := m.DB.QueryRow(stmt, id)
var u User
err := row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return User{}, ErrNoRecord
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE ShortId = ? AND IsDeleted = FALSE`
row := m.DB.QueryRow(stmt, id)
var u User
err := row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return User{}, ErrNoRecord
}
return User{}, err
}
return User{}, err
}
return u, nil
return u, nil
}
func (m *UserModel) GetById(id int64) (User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE Id = ? AND IsDeleted = FALSE`
row := m.DB.QueryRow(stmt, id)
var u User
err := row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return User{}, ErrNoRecord
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE Id = ? AND IsDeleted = FALSE`
row := m.DB.QueryRow(stmt, id)
var u User
err := row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return User{}, ErrNoRecord
}
return User{}, err
}
return User{}, err
}
return u, nil
return u, nil
}
func (m *UserModel) GetAll() ([]User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE IsDeleted = FALSE`
rows, err := m.DB.Query(stmt)
var users []User
for rows.Next() {
var u User
err = rows.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE IsDeleted = FALSE`
rows, err := m.DB.Query(stmt)
if err != nil {
return nil, err
return nil, err
}
users = append(users, u)
}
if err = rows.Err(); err != nil {
return nil, err
}
return users, nil
var users []User
for rows.Next() {
var u User
err = rows.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
if err != nil {
return nil, err
}
users = append(users, u)
}
if err = rows.Err(); err != nil {
return nil, err
}
return users, nil
}
func (m *UserModel) Authenticate(email, password string) (int64, error) {
var id int64
var hashedPassword []byte
var id int64
var hashedPassword []byte
stmt := `SELECT Id, HashedPassword FROM users WHERE Email = ?`
err := m.DB.QueryRow(stmt, email).Scan(&id, &hashedPassword)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, ErrInvalidCredentials
} else {
return 0, err
stmt := `SELECT Id, HashedPassword FROM users WHERE Email = ?`
err := m.DB.QueryRow(stmt, email).Scan(&id, &hashedPassword)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, ErrInvalidCredentials
} else {
return 0, err
}
}
}
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return 0, ErrInvalidCredentials
} else {
return 0, err
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return 0, ErrInvalidCredentials
} else {
return 0, err
}
}
}
return id, nil
return id, nil
}
func (m *UserModel) Exists(id int64) (bool, error) {
var exists bool
stmt := `SELECT EXISTS(SELECT true FROM users WHERE Id = ? AND IsDeleted = False)`
err := m.DB.QueryRow(stmt, id).Scan(&exists)
return exists, err
var exists bool
stmt := `SELECT EXISTS(SELECT true FROM users WHERE Id = ? AND IsDeleted = False)`
err := m.DB.QueryRow(stmt, id).Scan(&exists)
return exists, err
}

View File

@ -12,6 +12,10 @@ type CommonData struct {
IsHtmx bool
}
func shortIdToSlug(shortId uint64) string {
return strconv.FormatUint(shortId, 36)
}
func slugToShortId(slug string) uint64 {
id, _ := strconv.ParseUint(slug, 36, 64)
return id

View File

@ -20,6 +20,10 @@ type CommonData struct {
IsHtmx bool
}
func shortIdToSlug(shortId uint64) string {
return strconv.FormatUint(shortId, 36)
}
func slugToShortId(slug string) uint64 {
id, _ := strconv.ParseUint(slug, 36, 64)
return id
@ -87,7 +91,7 @@ func topNav(data CommonData) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentUser.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 36, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 40, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@ -100,7 +104,7 @@ func topNav(data CommonData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 39, Col: 81}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 43, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@ -181,7 +185,7 @@ func base(title string, data CommonData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 62, Col: 26}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 66, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@ -211,7 +215,7 @@ func base(title string, data CommonData) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.Flash)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 73, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 77, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {

View File

@ -5,7 +5,7 @@ import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
func gbUrl(gb models.Guestbook) string {
return fmt.Sprintf("/guestbooks/%s", gb.Slug())
return fmt.Sprintf("/guestbooks/%s", shortIdToSlug(gb.ShortId))
}
templ gbCreateForm(csrf_token string) {
@ -130,7 +130,7 @@ templ GuestbookDashboardCommentsView(title string, data CommonData, guestbook mo
}
templ commentForm(data CommonData, gb models.Guestbook, form forms.CommentCreateForm) {
{{ postUrl := fmt.Sprintf("/guestbooks/%s/comments/create", gb.Slug()) }}
{{ postUrl := fmt.Sprintf("/guestbooks/%s/comments/create", shortIdToSlug(gb.ShortId)) }}
<form action={ templ.URL(postUrl) } method="post">
<input type="hidden" name="csrf_token" value={data.CSRFToken}>
<div>

View File

@ -13,7 +13,7 @@ import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
func gbUrl(gb models.Guestbook) string {
return fmt.Sprintf("/guestbooks/%s", gb.Slug())
return fmt.Sprintf("/guestbooks/%s", shortIdToSlug(gb.ShortId))
}
func gbCreateForm(csrf_token string) templ.Component {
@ -575,7 +575,7 @@ func commentForm(data CommonData, gb models.Guestbook, form forms.CommentCreateF
templ_7745c5c3_Var28 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
postUrl := fmt.Sprintf("/guestbooks/%s/comments/create", gb.Slug())
postUrl := fmt.Sprintf("/guestbooks/%s/comments/create", shortIdToSlug(gb.ShortId))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<form action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err