Implement Admin Panel #43

Merged
yequari merged 8 commits from admin-panel into dev 2026-01-03 18:48:40 +00:00
29 changed files with 3043 additions and 115 deletions

277
cmd/web/handlers_admin.go Normal file
View File

@ -0,0 +1,277 @@
package main
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"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) getAdminPanelLanding(w http.ResponseWriter, r *http.Request) {
websites, err := app.websites.GetCount()
if err != nil {
app.serverError(w, r, err)
return
}
users, err := app.users.GetCount()
if err != nil {
app.serverError(w, r, err)
return
}
comments, err := app.guestbookComments.GetCount()
if err != nil {
app.serverError(w, r, err)
return
}
stats := views.AdminStat{
WebsiteCount: websites,
UserCount: users,
CommentCount: comments,
}
data := app.newCommonData(r)
views.AdminPanelLandingView("Admin Panel", data, stats).Render(r.Context(), w)
}
func (app *application) getAdminPanelAllUsers(w http.ResponseWriter, r *http.Request) {
page := r.URL.Query().Get("page")
count := r.URL.Query().Get("count")
var pageNum int64 = 1
var pageSize int64 = 5
var err error
if page != "" {
pageNum, err = strconv.ParseInt(page, 10, 0)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
}
if count != "" {
pageSize, err = strconv.ParseInt(count, 10, 0)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
}
users, err := app.users.GetAllPage(pageNum, pageSize)
if err != nil {
app.serverError(w, r, err)
return
}
total, err := app.users.GetCount()
if err != nil {
app.serverError(w, r, err)
return
}
data := app.newCommonData(r)
views.AdminPanelUsersView("All Users - Admin", data, users, pageNum, pageSize, total).Render(r.Context(), w)
}
func (app *application) getAdminPanelUser(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
u, 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.AdminPanelUserMgmtView(fmt.Sprintf("User Management - %s", u.Username), data, u).Render(r.Context(), w)
}
func (app *application) getAdminPanelUserMgmtDetail(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
u, 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
}
commonData := app.newCommonData(r)
views.AdminPanelUserMgmtDetail(commonData.CSRFToken, u).Render(r.Context(), w)
}
func (app *application) getAdminPanelUserMgmtForm(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
u, 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
}
var form forms.AdminUserMgmtForm
form.Username = u.Username
form.Email = u.Email
data := app.newCommonData(r)
views.AdminPanelUserMgmtEditForm(data.CSRFToken, form, u, []models.UserGroupId{}).Render(r.Context(), w)
}
func (app *application) putAdminPanelUserMgmtForm(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
u, 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
}
var form forms.AdminUserMgmtForm
err = app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
}
form.CheckField(validator.NotBlank(form.Username), "admin_username", "This field cannot be blank")
form.CheckField(validator.NotBlank(form.Email), "admin_useremail", "This field cannot be blank")
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "admin_useremail", "Please provide a valid email address")
if !form.Valid() {
data := app.newCommonData(r)
w.WriteHeader(http.StatusUnprocessableEntity)
views.AdminPanelUserMgmtEditForm(data.CSRFToken, form, u, []models.UserGroupId{}).Render(r.Context(), w)
return
}
updatedUser := u
updatedUser.Username = form.Username
updatedUser.Email = form.Email
err = app.users.UpdateUser(updatedUser)
if err != nil {
app.serverError(w, r, err)
return
}
commonData := app.newCommonData(r)
views.AdminPanelUserMgmtDetail(commonData.CSRFToken, updatedUser).Render(r.Context(), w)
}
func (app *application) putAdminPanelBanUser(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
u, 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
}
err = app.users.BanUser(u.ID)
if err != nil {
app.serverError(w, r, err)
return
}
u.Banned = time.Now()
commonData := app.newCommonData(r)
views.AdminPanelUserMgmtDetail(commonData.CSRFToken, u).Render(r.Context(), w)
}
func (app *application) putAdminPanelUnbanUser(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
u, 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
}
err = app.users.UnbanUser(u.ID)
if err != nil {
app.serverError(w, r, err)
return
}
u.Banned = time.Time{}
commonData := app.newCommonData(r)
views.AdminPanelUserMgmtDetail(commonData.CSRFToken, u).Render(r.Context(), w)
}
func (app *application) getAdminPanelWebsites(w http.ResponseWriter, r *http.Request) {
page := r.URL.Query().Get("page")
count := r.URL.Query().Get("count")
var pageNum int64 = 1
var pageSize int64 = 5
var err error
if page != "" {
pageNum, err = strconv.ParseInt(page, 10, 0)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
}
if count != "" {
pageSize, err = strconv.ParseInt(count, 10, 0)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
}
websites, err := app.websites.GetAllPage(pageNum, pageSize)
if err != nil {
app.serverError(w, r, err)
return
}
total, err := app.websites.GetCount()
if err != nil {
app.serverError(w, r, err)
return
}
commonData := app.newCommonData(r)
views.AdminPanelAllWebsitesView("All websites", commonData, websites, pageNum, pageSize, total).Render(r.Context(), w)
}
func (app *application) getAdminPanelWebsiteDetails(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
page := r.URL.Query().Get("page")
count := r.URL.Query().Get("count")
var pageNum int64 = 1
var pageSize int64 = 25
var err error
if page != "" {
pageNum, err = strconv.ParseInt(page, 10, 0)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
}
if count != "" {
pageSize, err = strconv.ParseInt(count, 10, 0)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
}
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
app.serverError(w, r, err)
return
}
total, err := app.guestbookComments.GetAllCount(website.Guestbook.ID)
if err != nil {
app.serverError(w, r, err)
return
}
comments, err := app.guestbookComments.GetAllPage(website.Guestbook.ID, pageNum, pageSize)
if err != nil {
app.serverError(w, r, err)
return
}
commonData := app.newCommonData(r)
views.AdminPanelWebsiteDetailView(fmt.Sprintf("Admin - %s", website.Name), commonData, website, comments, pageNum, pageSize, total).Render(r.Context(), w)
}

69
cmd/web/installer.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"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"
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
"github.com/justinas/alice"
)
func (i *appInstaller) installRoutes() http.Handler {
mux := http.NewServeMux()
if i.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)
standard := alice.New(i.app.recoverPanic, i.app.logRequest, commonHeaders)
mux.Handle("/{$}", standard.ThenFunc(i.getInstallHomepage))
mux.Handle("GET /install", standard.ThenFunc(i.getInstallForm))
mux.Handle("POST /install", standard.ThenFunc(i.postInstallForm))
return standard.Then(mux)
}
func (i *appInstaller) getInstallHomepage(w http.ResponseWriter, r *http.Request) {
views.InitialInstallView("Installation").Render(r.Context(), w)
}
func (i *appInstaller) getInstallForm(w http.ResponseWriter, r *http.Request) {
var form forms.InstallForm
views.InstallFormView("Installation - Settings", form).Render(r.Context(), w)
}
func (i *appInstaller) postInstallForm(w http.ResponseWriter, r *http.Request) {
var form forms.InstallForm
err := i.app.decodePostForm(r, &form)
if err != nil {
i.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() {
w.WriteHeader(http.StatusUnprocessableEntity)
views.InstallFormView("User Registration", form).Render(r.Context(), w)
return
}
sId := i.app.createShortId()
err = i.app.users.Insert(sId, form.Name, form.Email, form.Password, DefaultUserSettings())
if err != nil {
i.app.serverError(w, r, err)
}
err = i.app.users.AddUserToGroup(1, models.AdminGroup)
if err != nil {
i.app.serverError(w, r, err)
}
views.InstallSuccessView().Render(r.Context(), w)
i.srv.Close()
}

View File

@ -56,6 +56,12 @@ type application struct {
timezones []string
}
type appInstaller struct {
app *application
srv *http.Server
installModel models.InstallModelInterface
}
func main() {
addr := flag.String("addr", ":3000", "HTTP network address")
dsn := flag.String("dsn", "guestbook.db", "data source name")
@ -102,6 +108,29 @@ func main() {
timezones: getAvailableTimezones(),
}
tlsConfig := &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
}
installer := &appInstaller{
app: app,
installModel: &models.InstallModel{DB: db},
}
installer.srv = &http.Server{
Addr: *addr,
Handler: installer.installRoutes(),
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
TLSConfig: tlsConfig,
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
err = runInstaller(installer)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
err = app.users.InitializeSettingsMap()
if err != nil {
logger.Error(err.Error())
@ -113,10 +142,6 @@ func main() {
os.Exit(1)
}
tlsConfig := &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
}
srv := &http.Server{
Addr: *addr,
Handler: app.routes(),
@ -273,3 +298,31 @@ func walkTzDir(path string, zones []string) []string {
return zones
}
func runInstaller(i *appInstaller) error {
i.app.logger.Info("Performing migrations")
err := i.installModel.SetupDatabase()
if err != nil {
return err
}
installed, _ := i.installModel.GetInstalledFlag()
if installed {
return nil
}
i.app.logger.Info("App not installed, running installer...")
i.app.logger.Info("Starting installation server", slog.Any("addr", i.srv.Addr))
if i.app.debug {
err = i.srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem")
} else {
err = i.srv.ListenAndServe()
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
i.app.logger.Info("Installation complete")
err = i.installModel.SetInstalledFlag()
if err != nil {
return err
}
return nil
}

View File

@ -4,7 +4,9 @@ import (
"context"
"fmt"
"net/http"
"slices"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"github.com/justinas/nosurf"
)
@ -56,6 +58,17 @@ func (app *application) requireAuthentication(next http.Handler) http.Handler {
})
}
func (app *application) requireAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := app.getCurrentUser(r)
if !slices.Contains(user.Groups, models.AdminGroup) {
app.clientError(w, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func noSurf(next http.Handler) http.Handler {
csrfHandler := nosurf.New(next)
csrfHandler.SetBaseCookie(http.Cookie{
@ -84,6 +97,9 @@ func (app *application) authenticate(next http.Handler) http.Handler {
app.serverError(w, r, err)
return
}
if !user.Banned.IsZero() {
http.Redirect(w, r, "/banned", http.StatusSeeOther)
}
if exists {
ctx := context.WithValue(r.Context(), isAuthenticatedContextKey, true)
ctx = context.WithValue(ctx, userNameContextKey, user)

View File

@ -39,13 +39,11 @@ func (app *application) routes() http.Handler {
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 /users/settings", protected.ThenFunc(app.getUserSettings))
mux.Handle("PUT /users/settings", protected.ThenFunc(app.putUserSettings))
mux.Handle("GET /guestbooks", protected.ThenFunc(app.getAllGuestbooks))
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))
@ -60,5 +58,17 @@ func (app *application) routes() http.Handler {
mux.Handle("GET /websites/{id}/dashboard/guestbook/themes", protected.ThenFunc(app.getComingSoon))
mux.Handle("GET /websites/{id}/dashboard/guestbook/customize", protected.ThenFunc(app.getComingSoon))
adminOnly := protected.Append(app.requireAdmin)
mux.Handle("GET /admin", adminOnly.ThenFunc(app.getAdminPanelLanding))
mux.Handle("GET /admin/users", adminOnly.ThenFunc(app.getAdminPanelAllUsers))
mux.Handle("GET /admin/users/{id}", adminOnly.ThenFunc(app.getAdminPanelUser))
mux.Handle("GET /admin/users/{id}/edit", adminOnly.ThenFunc(app.getAdminPanelUserMgmtForm))
mux.Handle("GET /admin/users/{id}/detail", adminOnly.ThenFunc(app.getAdminPanelUserMgmtDetail))
mux.Handle("PUT /admin/users/{id}/edit", adminOnly.ThenFunc(app.putAdminPanelUserMgmtForm))
mux.Handle("PUT /admin/users/{id}/ban", adminOnly.ThenFunc(app.putAdminPanelBanUser))
mux.Handle("PUT /admin/users/{id}/unban", adminOnly.ThenFunc(app.putAdminPanelUnbanUser))
mux.Handle("GET /admin/websites", adminOnly.ThenFunc(app.getAdminPanelWebsites))
mux.Handle("GET /admin/websites/{id}", adminOnly.ThenFunc(app.getAdminPanelWebsiteDetails))
return standard.Then(mux)
}

View File

@ -1,6 +1,9 @@
package forms
import "git.32bit.cafe/32bitcafe/guestbook/internal/validator"
import (
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"git.32bit.cafe/32bitcafe/guestbook/internal/validator"
)
type UserRegistrationForm struct {
Name string `schema:"username"`
@ -25,9 +28,9 @@ type CommentCreateForm struct {
}
type WebsiteCreateForm struct {
Name string `schema:"sitename"`
SiteUrl string `schema:"siteurl"`
AuthorName string `schema:"authorname"`
Name string `schema:"ws_name"`
SiteUrl string `schema:"ws_url"`
AuthorName string `schema:"ws_author"`
validator.Validator `schema:"-"`
}
@ -50,3 +53,17 @@ type WebsiteSettingsForm struct {
WidgetsEnabled string `schema:"gb_remote"`
validator.Validator `schema:"-"`
}
type AdminUserMgmtForm struct {
Username string `schema:"admin_username"`
Email string `schema:"admin_useremail"`
Groups []models.UserGroupId `schema:"admin_usergroups"`
validator.Validator `schema:"-"`
}
type InstallForm struct {
Name string `schema:"username"`
Email string `schema:"email"`
Password string `schema:"password"`
validator.Validator `schema:"-"`
}

View File

@ -33,10 +33,13 @@ type GuestbookCommentModel struct {
type GuestbookCommentModelInterface interface {
Insert(shortId uint64, guestbookId, parentId int64, authorName, authorEmail, authorSite, commentText, pageUrl string, isPublished bool) (int64, error)
Get(shortId uint64) (GuestbookComment, error)
GetCount() (int64, error)
GetVisible(guestbookId int64) ([]GuestbookComment, error)
GetVisibleSerialized(guestbookId int64) ([]GuestbookCommentSerialized, error)
GetDeleted(guestbookId int64) ([]GuestbookComment, error)
GetAll(guestbookId int64) ([]GuestbookComment, error)
GetAllCount(guestbookId int64) (int64, error)
GetAllPage(guestbookId int64, pageNum int64, pageSize int64) ([]GuestbookComment, error)
UpdateComment(comment *GuestbookComment) error
}
@ -74,6 +77,20 @@ func (m *GuestbookCommentModel) Get(shortId uint64) (GuestbookComment, error) {
return c, nil
}
func (m *GuestbookCommentModel) GetCount() (int64, error) {
stmt := `SELECT COUNT(*) FROM guestbook_comments WHERE Deleted IS NULL`
row := m.DB.QueryRow(stmt)
var result int64
err := row.Scan(&result)
if err != nil {
return -1, err
}
if err = row.Err(); err != nil {
return -1, err
}
return result, nil
}
func (m *GuestbookCommentModel) GetVisible(guestbookId int64) ([]GuestbookComment, error) {
stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished
@ -180,6 +197,47 @@ func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, e
return comments, nil
}
func (m *GuestbookCommentModel) GetAllCount(guestbookId int64) (int64, error) {
stmt := `SELECT COUNT(*) FROM guestbook_comments WHERE GuestbookId = ? AND Deleted IS NULL`
row := m.DB.QueryRow(stmt, guestbookId)
var result int64
err := row.Scan(&result)
if err != nil {
return -1, err
}
if err = row.Err(); err != nil {
return -1, err
}
return result, nil
}
func (m *GuestbookCommentModel) GetAllPage(guestbookId int64, pageNum int64, pageSize int64) ([]GuestbookComment, error) {
stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished
FROM guestbook_comments
WHERE GuestbookId = ? AND Deleted IS NULL
ORDER BY Created DESC
LIMIT ? OFFSET ?`
rows, err := m.DB.Query(stmt, guestbookId, pageSize, (pageNum-1)*pageSize)
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)
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
SET CommentText = ?,

View File

@ -0,0 +1,62 @@
package models
import (
"database/sql"
"errors"
"time"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/mattn/go-sqlite3"
)
type InstallModel struct {
DB *sql.DB
}
type InstallModelInterface interface {
SetupDatabase() error
SetInstalledFlag() error
GetInstalledFlag() (bool, error)
}
func (m *InstallModel) SetupDatabase() error {
driver, err := sqlite3.WithInstance(m.DB, &sqlite3.Config{})
if err != nil {
return err
}
mi, err := migrate.NewWithDatabaseInstance("file://migrations", "sqlite", driver)
if err != nil {
return err
}
err = mi.Up()
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
func (m *InstallModel) SetInstalledFlag() error {
stmt := `INSERT INTO installed (InstallDate) VALUES (?)`
_, err := m.DB.Exec(stmt, time.Now().UTC())
if err != nil {
return err
}
return nil
}
func (m *InstallModel) GetInstalledFlag() (bool, error) {
stmt := `SELECT InstallDate FROM installed`
row := m.DB.QueryRow(stmt)
var d time.Time
err := row.Scan(&d)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
} else {
return false, err
}
}
return true, nil
}

View File

@ -13,7 +13,6 @@ var mockUser = models.User{
Username: "tester",
Email: "test@example.com",
Deleted: false,
IsBanned: false,
Created: time.Now(),
Settings: mockUserSettings,
}
@ -26,6 +25,31 @@ type UserModel struct {
Settings map[string]models.Setting
}
func (m *UserModel) AddUserToGroup(userId int64, groupId models.UserGroupId) error {
//TODO implement me
panic("implement me")
}
func (m *UserModel) BanUser(userId int64) error {
//TODO implement me
panic("implement me")
}
func (m *UserModel) UpdateUser(u models.User) error {
//TODO implement me
panic("implement me")
}
func (m *UserModel) UpdatePassword(userId int64, password string) error {
//TODO implement me
panic("implement me")
}
func (m *UserModel) Delete(userId int64) error {
//TODO implement me
panic("implement me")
}
func (m *UserModel) InitializeSettingsMap() error {
return nil
}
@ -120,3 +144,7 @@ func (m *UserModel) UpdateSubject(userId int64, subject string) error {
}
return errors.New("invalid")
}
func (m *UserModel) GetNumberOfUsers() int {
return 1
}

View File

@ -56,6 +56,14 @@ func (m *WebsiteModel) GetAllUser(userId int64) ([]models.Website, error) {
return []models.Website{mockWebsite}, nil
}
func GetCountUser(userId int64) (int64, error) {
return 1, nil
}
func (m *WebsiteModel) GetAllUserPage(userId int64, pageNum int64) ([]models.Website, error) {
return []models.Website{mockWebsite}, nil
}
func (m *WebsiteModel) GetById(id int64) (models.Website, error) {
switch id {
case 1:
@ -69,6 +77,14 @@ func (m *WebsiteModel) GetAll() ([]models.Website, error) {
return []models.Website{mockWebsite}, nil
}
func GetCount() (int64, error) {
return 1, nil
}
func (m *WebsiteModel) GetAllPage(pageNum int64) ([]models.Website, error) {
return []models.Website{mockWebsite}, nil
}
func (m *WebsiteModel) Update(w models.Website) error {
return nil
}

View File

@ -10,6 +10,13 @@ import (
"golang.org/x/crypto/bcrypt"
)
type UserGroupId int64
const (
AdminGroup UserGroupId = 1
UserGroup UserGroupId = 2
)
type UserSettings struct {
LocalTimezone *time.Location
}
@ -24,10 +31,11 @@ type User struct {
Username string
Email string
Deleted bool
IsBanned bool
HashedPassword []byte
Created time.Time
Banned time.Time
Settings UserSettings
Groups []UserGroupId
}
type UserModel struct {
@ -48,15 +56,24 @@ type UserModelInterface interface {
Insert(shortId uint64, username string, email string, password string, settings UserSettings) error
InsertWithoutPassword(shortId uint64, username string, email string, subject string, settings UserSettings) (int64, error)
Get(shortId uint64) (User, error)
GetCount() (int64, error)
GetById(id int64) (User, error)
GetByEmail(email string) (int64, error)
GetBySubject(subject string) (int64, error)
GetAll() ([]User, error)
GetAllPage(pageNum, pageSize int64) ([]User, error)
Authenticate(email, password string) (int64, error)
Exists(id int64) (bool, error)
UpdateUserSettings(userId int64, settings UserSettings) error
UpdateSetting(userId int64, setting Setting, value string) error
UpdateSubject(userId int64, subject string) error
GetNumberOfUsers() int
AddUserToGroup(userId int64, groupId UserGroupId) error
BanUser(userId int64) error
UnbanUser(userId int64) error
UpdateUser(u User) error
UpdatePassword(userId int64, password string) error
Delete(userId int64) error
}
func (m *UserModel) InitializeSettingsMap() error {
@ -96,8 +113,8 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo
if err != nil {
return err
}
stmt := `INSERT INTO users (ShortId, Username, Email, IsBanned, HashedPassword, Created)
VALUES (?, ?, ?, FALSE, ?, ?)`
stmt := `INSERT INTO users (ShortId, Username, Email, HashedPassword, Created)
VALUES (?, ?, ?, ?, ?)`
tx, err := m.DB.Begin()
if err != nil {
return err
@ -107,7 +124,8 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return err
}
if sqliteError, ok := err.(sqlite3.Error); ok {
var sqliteError sqlite3.Error
if errors.As(err, &sqliteError) {
if sqliteError.ExtendedCode == 2067 && strings.Contains(sqliteError.Error(), "Email") {
return ErrDuplicateEmail
}
@ -130,17 +148,20 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo
}
return err
}
err = m.addUserToGroup(tx, id, UserGroup)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (m *UserModel) InsertWithoutPassword(shortId uint64, username string, email string, subject string, settings UserSettings) (int64, error) {
stmt := `INSERT INTO users (ShortId, Username, Email, IsBanned, OIDCSubject, Created)
VALUES (?, ?, ?, FALSE, ?, ?)`
stmt := `INSERT INTO users (ShortId, Username, Email, OIDCSubject, Created)
VALUES (?, ?, ?, ?, ?)`
tx, err := m.DB.Begin()
if err != nil {
return -1, err
@ -150,7 +171,8 @@ func (m *UserModel) InsertWithoutPassword(shortId uint64, username string, email
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return -1, err
}
if sqliteError, ok := err.(sqlite3.Error); ok {
var sqliteError sqlite3.Error
if errors.As(err, &sqliteError) {
if sqliteError.ExtendedCode == 2067 && strings.Contains(sqliteError.Error(), "Email") {
return -1, ErrDuplicateEmail
}
@ -173,6 +195,7 @@ func (m *UserModel) InsertWithoutPassword(shortId uint64, username string, email
}
return -1, err
}
err = m.addUserToGroup(tx, id, UserGroup)
err = tx.Commit()
if err != nil {
return -1, err
@ -182,14 +205,15 @@ func (m *UserModel) InsertWithoutPassword(shortId uint64, username string, email
}
func (m *UserModel) Get(shortId uint64) (User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE ShortId = ? AND Deleted IS NULL`
stmt := `SELECT Id, ShortId, Username, Email, Created, Banned FROM users WHERE ShortId = ? AND Deleted IS NULL`
tx, err := m.DB.Begin()
if err != nil {
return User{}, err
}
row := tx.QueryRow(stmt, shortId)
var u User
err = row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
var b sql.NullTime
err = row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created, &b)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return User{}, err
@ -199,6 +223,9 @@ func (m *UserModel) Get(shortId uint64) (User, error) {
}
return User{}, err
}
if b.Valid {
u.Banned = b.Time
}
settings, err := m.getSettings(tx, u.ID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
@ -207,6 +234,14 @@ func (m *UserModel) Get(shortId uint64) (User, error) {
return User{}, err
}
u.Settings = settings
groups, err := m.getGroups(tx, u.ID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return User{}, err
}
return User{}, err
}
u.Groups = groups
err = tx.Commit()
if err != nil {
return User{}, err
@ -214,15 +249,30 @@ func (m *UserModel) Get(shortId uint64) (User, error) {
return u, nil
}
func (m *UserModel) GetCount() (int64, error) {
stmt := `SELECT COUNT(*) FROM users WHERE Deleted IS NULL`
row := m.DB.QueryRow(stmt)
var result int64
err := row.Scan(&result)
if err != nil {
return -1, err
}
if err = row.Err(); err != nil {
return -1, err
}
return result, nil
}
func (m *UserModel) GetById(id int64) (User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE Id = ? AND Deleted IS NULL`
stmt := `SELECT Id, ShortId, Username, Email, Created, Banned FROM users WHERE Id = ? AND Deleted IS NULL`
tx, err := m.DB.Begin()
if err != nil {
return User{}, err
}
row := tx.QueryRow(stmt, id)
var u User
err = row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
var b sql.NullTime
err = row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created, &b)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return User{}, err
@ -232,6 +282,9 @@ func (m *UserModel) GetById(id int64) (User, error) {
}
return User{}, err
}
if b.Valid {
u.Banned = b.Time
}
settings, err := m.getSettings(tx, u.ID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
@ -240,6 +293,14 @@ func (m *UserModel) GetById(id int64) (User, error) {
return User{}, err
}
u.Settings = settings
groups, err := m.getGroups(tx, u.ID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return User{}, err
}
return User{}, err
}
u.Groups = groups
err = tx.Commit()
if err != nil {
return User{}, err
@ -268,6 +329,27 @@ func (m *UserModel) GetAll() ([]User, error) {
return users, nil
}
func (m *UserModel) GetAllPage(pageNum, pageSize int64) ([]User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE DELETED IS NULL LIMIT ? OFFSET ?`
rows, err := m.DB.Query(stmt, pageSize, (pageNum-1)*pageSize)
if err != nil {
return nil, err
}
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
@ -400,3 +482,115 @@ func (m *UserModel) UpdateSetting(userId int64, setting Setting, value string) e
}
return nil
}
func (m *UserModel) GetNumberOfUsers() int {
stmt := `SELECT COUNT(Id) FROM users WHERE Deleted IS NULL;`
row := m.DB.QueryRow(stmt)
var count int
err := row.Scan(&count)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
count = 0
} else {
count = 1
}
}
return count
}
func (m *UserModel) AddUserToGroup(userId int64, groupId UserGroupId) error {
stmt := `INSERT OR IGNORE INTO users_groups (UserId, GroupId) VALUES (?, ?)`
_, err := m.DB.Exec(stmt, userId, groupId)
if err != nil {
return err
}
return nil
}
func (m *UserModel) RemoveUserFromGroup(userId int64, groupId UserGroupId) error {
stmt := `DELETE FROM users_groups WHERE UserId = ? AND GroupId = ?`
_, err := m.DB.Exec(stmt, userId, groupId)
if err != nil {
return err
}
return nil
}
func (m *UserModel) BanUser(userId int64) error {
stmt := `UPDATE users SET Banned=? WHERE Id=?`
_, err := m.DB.Exec(stmt, time.Now().UTC().Format(time.RFC3339), userId)
if err != nil {
return err
}
return nil
}
func (m *UserModel) UnbanUser(userId int64) error {
stmt := `UPDATE users SET Banned=NULL WHERE Id=?`
_, err := m.DB.Exec(stmt, userId)
if err != nil {
return err
}
return nil
}
func (m *UserModel) UpdateUser(u User) error {
stmt := `UPDATE users SET Email=?, Username=? WHERE Id=?`
_, err := m.DB.Exec(stmt, u.Email, u.Username, u.ID)
if err != nil {
return err
}
return nil
}
func (m *UserModel) UpdatePassword(userId int64, password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return err
}
stmt := `UPDATE users SET HashedPassword=? WHERE Id=?`
_, err = m.DB.Exec(stmt, hashedPassword, userId)
if err != nil {
return err
}
return nil
}
func (m *UserModel) Delete(userId int64) error {
stmt := `UPDATE users SET Deleted=? WHERE Id=?`
_, err := m.DB.Exec(stmt, time.Now().UTC().Format(time.RFC3339), userId)
if err != nil {
return err
}
return nil
}
func (m *UserModel) addUserToGroup(tx *sql.Tx, userId int64, groupId UserGroupId) error {
stmt := `INSERT INTO users_groups (UserId, GroupId) VALUES (?, ?)`
_, err := tx.Exec(stmt, userId, groupId)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return err
}
return err
}
return nil
}
func (m *UserModel) getGroups(tx *sql.Tx, userId int64) ([]UserGroupId, error) {
stmt := `SELECT DISTINCT GroupId FROM users_groups WHERE UserId = ?`
rows, err := tx.Query(stmt, userId)
result := make([]UserGroupId, 0, 10)
if err != nil {
return result, err
}
for rows.Next() {
var g UserGroupId
err = rows.Scan(&g)
if err != nil {
return result, err
}
result = append(result, g)
}
return result, nil
}

View File

@ -95,7 +95,11 @@ type WebsiteModelInterface interface {
Insert(shortId uint64, userId int64, siteName, siteUrl, authorName string) (int64, error)
Get(shortId uint64) (Website, error)
GetAllUser(userId int64) ([]Website, error)
GetCountUser(userId int64) (int64, error)
GetAllUserPage(userId int64, pageNum int64, pageSize int64) ([]Website, error)
GetAll() ([]Website, error)
GetCount() (int64, error)
GetAllPage(pageNum int64, pageSize int64) ([]Website, error)
Update(w Website) error
InitializeSettingsMap() error
UpdateGuestbookSettings(guestbookId int64, settings GuestbookSettings) error
@ -270,6 +274,51 @@ func (m *WebsiteModel) GetAllUser(userId int64) ([]Website, error) {
return websites, nil
}
func (m *WebsiteModel) GetCountUser(userId int64) (int64, error) {
stmt := `SELECT COUNT(*) FROM websites WHERE UserId = ? AND Deleted IS NULL`
row := m.DB.QueryRow(stmt, userId)
var result int64
err := row.Scan(&result)
if err != nil {
return -1, err
}
if err = row.Err(); err != nil {
return -1, err
}
return result, nil
}
func (m *WebsiteModel) GetAllUserPage(userId int64, pageNum int64, pageSize 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 = ? AND w.Deleted IS NULL
LIMIT ? OFFSET ?`
rows, err := m.DB.Query(stmt, userId, pageSize, (pageNum-1)*pageSize)
if err != nil {
return nil, err
}
var websites []Website
for rows.Next() {
var w Website
var u string
err := rows.Scan(&w.ID, &w.ShortId, &w.Name, &u, &w.AuthorName, &w.UserId, &w.Created,
&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive)
if err != nil {
return nil, err
}
w.Url, err = url.Parse(u)
if err != nil {
return nil, err
}
websites = append(websites, w)
}
if err = rows.Err(); err != nil {
return nil, err
}
return websites, nil
}
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
@ -299,6 +348,50 @@ func (m *WebsiteModel) GetAll() ([]Website, error) {
return websites, nil
}
func (m *WebsiteModel) GetCount() (int64, error) {
stmt := `SELECT COUNT(*) FROM websites WHERE Deleted IS NULL`
row := m.DB.QueryRow(stmt)
var result int64
err := row.Scan(&result)
if err != nil {
return -1, err
}
if err = row.Err(); err != nil {
return -1, err
}
return result, nil
}
func (m *WebsiteModel) GetAllPage(pageNum int64, pageSize 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.Deleted IS NULL
LIMIT ? OFFSET ?`
rows, err := m.DB.Query(stmt, pageSize, (pageNum-1)*pageSize)
if err != nil {
return nil, err
}
var websites []Website
for rows.Next() {
var w Website
var u string
err := rows.Scan(&w.ID, &w.ShortId, &w.Name, &u, &w.AuthorName, &w.UserId, &w.Created,
&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive)
if err != nil {
return nil, err
}
w.Url, err = url.Parse(u)
if err != nil {
return nil, err
}
websites = append(websites, w)
}
if err = rows.Err(); err != nil {
return nil, err
}
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)

View File

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS users_groups;
DROP TABLE IF EXISTS groups;

View File

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS groups (
Id integer PRIMARY KEY NOT NULL,
Description varchar(256)
);
INSERT INTO groups (Id, Description)
VALUES (1, 'admin'),(2, 'user');
CREATE TABLE IF NOT EXISTS users_groups (
Id integer primary key autoincrement,
UserId integer NOT NULL,
GroupId integer NOT NULL,
FOREIGN KEY (UserId) REFERENCES users(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
FOREIGN KEY (GroupId) REFERENCES groups(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
UNIQUE(UserId, GroupId)
);

View File

@ -0,0 +1,2 @@
ALTER TABLE users DROP COLUMN Banned;
ALTER TABLE users ADD COLUMN IsBanned boolean NOT NULL DEFAULT false;

View File

@ -0,0 +1,2 @@
ALTER TABLE users DROP COLUMN IsBanned;
ALTER TABLE users ADD COLUMN Banned datetime NULL;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS installed;

View File

@ -0,0 +1,5 @@
CREATE TABLE installed (
Id integer primary key autoincrement,
InstallDate datetime NOT NULL,
InstallVersion varchar(100) NULL
);

View File

@ -0,0 +1,5 @@
DROP INDEX websites_users;
DROP INDEX guestbooks_websites;
DROP INDEX comments_guestbooks;
DROP INDEX web_settings_websites;
DROP INDEX user_settings_users;

View File

@ -0,0 +1,5 @@
CREATE INDEX websites_users ON websites(UserId);
CREATE INDEX guestbooks_websites ON guestbooks(WebsiteId);
CREATE INDEX comments_guestbooks ON guestbook_comments(GuestbookId);
CREATE INDEX web_settings_websites ON guestbook_settings(GuestbookId);
CREATE INDEX user_settings_users ON user_settings(UserId);

View File

@ -319,6 +319,7 @@ label {
input[type="text"],
input[type="email"],
input[type="url"],
input[type="password"],
textarea,
select {
width: 100%;
@ -708,6 +709,54 @@ ul#websites li {
margin-top: var(--space-2xl);
}
/* Admin Panel */
.admin-section {
background-color: var(--color-surface);
padding: var(--space-xl);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
margin-bottom: var(--space-xl);
}
.admin-section table {
width: 100%;
}
.admin-section th {
border-bottom: 1px solid var(--color-border);
}
.admin-section td {
padding: var(--space-sm);
text-align: center;
}
.admin-flex {
display: flex;
justify-content: space-between;
gap: var(--space-sm);
}
.admin-info {
flex: 1;
text-align: center;
font-size: x-large;
}
/* Pagination */
.pagination {
width: 100%;
display: flex;
justify-content: center;
}
.pagination-btn {
background-color: var(--color-surface);
padding: var(--space-sm);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
margin: var(--space-xs);
}
/* Utilities */
hr {
border: none;

277
ui/views/admin.templ Normal file
View File

@ -0,0 +1,277 @@
package views
import (
"fmt"
"git.32bit.cafe/32bitcafe/guestbook/internal/forms"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"slices"
"strings"
"time"
)
type AdminStat struct {
WebsiteCount int64
UserCount int64
CommentCount int64
}
templ adminBase(title string, data CommonData) {
<!DOCTYPE html>
<html lang="en">
<head>
<title>{ title } - webweav.ing</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="htmx-config" content={ `{"includeIndicatorStyles":false}` }/>
<link href="/static/css/style.css" rel="stylesheet"/>
<link href="/static/fontawesome/css/fontawesome.css" rel="stylesheet"/>
<link href="/static/fontawesome/css/solid.css" rel="stylesheet"/>
<script src="/static/js/htmx.min.js"></script>
</head>
<body>
<header>
<a href="/">Back to webweav.ing</a>
</header>
<main>
{ children... }
</main>
@commonFooter()
</body>
</html>
}
templ adminSidebar() {
<nav aria-label="Admin panel navigation">
<div>
<section>
<h3>Administration</h3>
<ul role="list">
<li><a href="/admin">Dashboard</a></li>
<li><a href="/admin/users">Users</a></li>
<li><a href="/admin/websites">Websites</a></li>
</ul>
</section>
</div>
</nav>
}
templ AdminPanelLandingView(title string, data CommonData, stats AdminStat) {
@adminBase(title, data) {
<div id="dashboard">
@adminSidebar()
<div>
<section class="admin-section">
<h1>{ title }</h1>
<p>Welcome to the admin panel</p>
</section>
<section class="admin-flex">
<section class="admin-section admin-info">
<h2>Users</h2>
<p>{ fmt.Sprintf("%d", stats.UserCount) }</p>
</section>
<section class="admin-section admin-info">
<h2>Websites</h2>
<p>{ fmt.Sprintf("%d", stats.WebsiteCount) }</p>
</section>
<section class="admin-section admin-info">
<h2>Comments</h2>
<p>{ fmt.Sprintf("%d", stats.CommentCount) }</p>
</section>
</section>
</div>
</div>
}
}
templ AdminPanelUsersView(title string, data CommonData, users []models.User, pageNum, pageSize, total int64) {
@adminBase(title, data) {
<div id="dashboard">
@adminSidebar()
<div>
<section class="admin-section">
<table>
<th>Username</th>
<th>Joined</th>
<th>Email</th>
for _, u := range users {
<tr>
{{ url := fmt.Sprintf("/admin/users/%s", shortIdToSlug(u.ShortId)) }}
<td><a href={ templ.URL(url) }>{ u.Username }</a></td>
<td>{ u.Created.Format(time.RFC3339) }</td>
<td>{ u.Email }</td>
</tr>
}
</table>
</section>
@pagination("/admin/users", pageNum, total, pageSize)
</div>
</div>
}
}
templ AdminPanelUserMgmtDetail(csrfToken string, user models.User) {
<div id="user-info">
<form>
<input type="hidden" name="csrf_token" value={ csrfToken }/>
<section>
<h3>User Info</h3>
<div>
<h5>Username</h5>
<p>{ user.Username }</p>
</div>
<div>
<h5>Email</h5>
<p>{ user.Email }</p>
</div>
<div>
<h5>Joined</h5>
<p>{ user.Created.Format(time.RFC3339) }</p>
</div>
</section>
<section>
<h3>Groups</h3>
<ul>
for _, g := range user.Groups {
<li>{ fmt.Sprintf("%s", getGroupName(g)) }</li>
}
</ul>
</section>
<section>
<h3>Actions</h3>
{{ getFormUrl := fmt.Sprintf("/admin/users/%s/edit", shortIdToSlug(user.ShortId)) }}
{{ putBanUrl := fmt.Sprintf("/admin/users/%s/ban", shortIdToSlug(user.ShortId)) }}
{{ putUnbanUrl := fmt.Sprintf("/admin/users/%s/unban", shortIdToSlug(user.ShortId)) }}
{{ deleteUrl := fmt.Sprintf("/admin/users/%s", shortIdToSlug(user.ShortId)) }}
<button type="button" hx-get={ getFormUrl } hx-target="#user-info">Edit</button>
if user.ID != 1 {
if user.Banned.IsZero() {
<button type="button" hx-put={ putBanUrl } hx-confirm="Are you sure you want to ban this user?" hx-target="#user-info" class="danger">Ban</button>
} else {
<button type="button" hx-put={ putUnbanUrl } hx-confirm="Are you sure you want to unban this user?" hx-target="#user-info">Unban</button>
}
<button type="button" hx-delete={ deleteUrl } hx-confirm="Are you sure you want to delete this user? This is irreversible" class="danger">Delete</button>
}
</section>
</form>
</div>
}
templ AdminPanelUserMgmtView(title string, data CommonData, user models.User) {
@adminBase(title, data) {
<div id="dashboard">
@adminSidebar()
@AdminPanelUserMgmtDetail(data.CSRFToken, user)
</div>
}
}
templ AdminPanelUserMgmtEditForm(csrfToken string, form forms.AdminUserMgmtForm, user models.User, groups []models.UserGroupId) {
<div id="user-info">
<form>
<input type="hidden" name="csrf_token" value={ csrfToken }/>
<fieldset>
<h3>User Info</h3>
<div>
<h5>Username</h5>
<input type="text" name="admin_username" id="username" value={ form.Username } required/>
</div>
<div>
<h5>Email</h5>
<input type="text" name="admin_useremail" id="useremail" value={ form.Email } required/>
</div>
<div>
<h5>Joined</h5>
<p>{ user.Created.Format(time.RFC3339) }</p>
</div>
</fieldset>
<section>
<fieldset>
<h3>Groups</h3>
{{ isAdmin := slices.Contains(user.Groups, models.AdminGroup) }}
<input type="checkbox" name="admin_usergroup_admin" id="usergroup_admin" checked?={ isAdmin } disabled?={ user.ID == 1 }/>
<label for="usergroup_admin">Administrator</label>
<input type="checkbox" name="admin_usergroup_user" id="usergroup_user" checked disabled/>
<label for="usergroup_user">User</label>
</fieldset>
</section>
<section>
<h3>Actions</h3>
{{ putFormUrl := fmt.Sprintf("/admin/users/%s/edit", shortIdToSlug(user.ShortId)) }}
{{ getDetailUrl := fmt.Sprintf("/admin/users/%s/detail", shortIdToSlug(user.ShortId)) }}
<button type="button" hx-put={ putFormUrl } hx-target="#user-info">Save</button>
<button type="reset" hx-get={ getDetailUrl } hx-target="#user-info">Cancel</button>
</section>
</form>
</div>
}
templ AdminPanelAllWebsitesView(title string, data CommonData, websites []models.Website, pageNum, pageCount, total int64) {
@adminBase(title, data) {
<div id="dashboard">
@adminSidebar()
<div>
<section class="admin-section">
<table>
<th>Site Name</th>
<th>Owner</th>
<th>URL</th>
<th>Created</th>
<th>Guestbook</th>
for _, w := range websites {
<tr>
{{ detailUrl := fmt.Sprintf("/admin/websites/%s", shortIdToSlug(w.ShortId)) }}
<td><a href={ templ.SafeURL(detailUrl) }>{ w.Name }</a></td>
<td>{ w.AuthorName }</td>
<td><a href={ templ.SafeURL(w.Url.String()) }>{ w.Url.String() }</a></td>
<td>{ w.Created.Format(time.RFC1123) }</td>
{{ gbUrl := fmt.Sprintf("/websites/%s/guestbook", shortIdToSlug(w.ShortId)) }}
<td><a href={ templ.SafeURL(gbUrl) }>View</a></td>
</tr>
}
</table>
</section>
@pagination("/admin/websites", pageNum, total, pageCount)
</div>
</div>
}
}
func truncateComment(s string, n int) string {
words := strings.Fields(s)
if len(words) < n {
return s
}
truncated := words[:n]
truncated = append(truncated, "...")
return strings.Join(truncated, " ")
}
templ AdminPanelWebsiteDetailView(title string, data CommonData, website models.Website, comments []models.GuestbookComment, pageNum, pageAmount, total int64) {
@adminBase(title, data) {
<div id="dashboard">
@adminSidebar()
<div>
<section class="admin-section">
<table>
<th>Author</th>
<th>Created</th>
<th>Email</th>
<th>Homepage</th>
<th>Comment</th>
for _, c := range comments {
<tr>
<td>{ c.AuthorName }</td>
<td>{ c.Created.Format(time.RFC1123) }</td>
<td>{ c.AuthorEmail }</td>
<td>{ c.AuthorSite }</td>
<td>{ truncateComment(c.CommentText, 15) }</td>
</tr>
}
</table>
</section>
{{ url := fmt.Sprintf("/admin/websites/%s", shortIdToSlug(website.ShortId)) }}
@pagination(url, pageNum, total, pageAmount)
</div>
</div>
}
}

1048
ui/views/admin_templ.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,13 @@
package views
import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "strconv"
import "fmt"
import "strings"
import (
"fmt"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"math"
"slices"
"strconv"
"strings"
)
type CommonData struct {
CurrentYear int
@ -33,6 +37,16 @@ func externalUrl(url string) string {
return url
}
func getGroupName(id models.UserGroupId) string {
switch id {
case 1:
return "Admin"
case 2:
return "User"
}
return "Unknown"
}
templ commonHeader() {
<header>
<h1><a href="/">webweav.ing</a></h1>
@ -54,6 +68,9 @@ templ topNav(data CommonData) {
<li><a href="/guestbooks">All Guestbooks</a></li>
<li><a href="/websites">My Websites</a></li>
<li><a href="/users/settings">Settings</a></li>
if slices.Contains(data.CurrentUser.Groups, models.AdminGroup) {
<li><a href="/admin">Admin Panel</a></li>
}
<li><a href="#" hx-post="/users/logout" hx-headers={ hxHeaders }>Logout</a></li>
} else {
if data.LocalAuthEnabled {
@ -101,3 +118,35 @@ templ base(title string, data CommonData) {
</body>
</html>
}
templ pagination(baseUrl string, currentPage int64, recordsAmount int64, recordsPerPage int64) {
{{ totalPages := int64(math.Ceil(float64(recordsAmount) / float64(recordsPerPage))) }}
<div class="pagination">
<span class="pagination-btn">
if currentPage > 1 {
{{ url := fmt.Sprintf("%s?page=%d&count=%d", baseUrl, currentPage-1, recordsPerPage) }}
<a href={ templ.URL(url) }>Prev</a>
} else {
Prev
}
</span>
for i := range totalPages {
<span class="pagination-btn">
{{ url := fmt.Sprintf("%s?page=%d&count=%d", baseUrl, i+1, recordsPerPage) }}
if i+1 == currentPage {
{ fmt.Sprintf("%d",i+1) }
} else {
<a href={ templ.URL(url) }>{ fmt.Sprintf("%d",i+1) }</a>
}
</span>
}
<span class="pagination-btn">
if currentPage < totalPages {
{{ url := fmt.Sprintf("%s?page=%d&count=%d", baseUrl, currentPage+1, recordsPerPage) }}
<a href={ templ.URL(url) }>Next</a>
} else {
Next
}
</span>
</div>
}

View File

@ -8,10 +8,14 @@ package views
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "strconv"
import "fmt"
import "strings"
import (
"fmt"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"math"
"slices"
"strconv"
"strings"
)
type CommonData struct {
CurrentYear int
@ -41,6 +45,16 @@ func externalUrl(url string) string {
return url
}
func getGroupName(id models.UserGroupId) string {
switch id {
case 1:
return "Admin"
case 2:
return "User"
}
return "Unknown"
}
func commonHeader() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@ -104,7 +118,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: 48, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 62, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@ -116,36 +130,46 @@ func topNav(data CommonData) templ.Component {
return templ_7745c5c3_Err
}
if data.IsAuthenticated {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<li><a href=\"/guestbooks\">All Guestbooks</a></li><li><a href=\"/websites\">My Websites</a></li><li><a href=\"/users/settings\">Settings</a></li><li><a href=\"#\" hx-post=\"/users/logout\" hx-headers=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<li><a href=\"/guestbooks\">All Guestbooks</a></li><li><a href=\"/websites\">My Websites</a></li><li><a href=\"/users/settings\">Settings</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if slices.Contains(data.CurrentUser.Groups, models.AdminGroup) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<li><a href=\"/admin\">Admin Panel</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " <li><a href=\"#\" hx-post=\"/users/logout\" hx-headers=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hxHeaders)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 57, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 74, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Logout</a></li>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">Logout</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
if data.LocalAuthEnabled {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<li><a href=\"/users/register\">Create an Account</a></li>| ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<li><a href=\"/users/register\">Create an Account</a></li>| ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " <li><a href=\"/users/login\">Login</a></li>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " <li><a href=\"/users/login\">Login</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</ul></nav>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</ul></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -174,7 +198,7 @@ func commonFooter() templ.Component {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<footer><p>A <a href=\"https://32bit.cafe\" rel=\"noopener\">32bit.cafe</a> Project</p><ul class=\"footer-links\"><li><a href=\"/about\">About</a></li><li><a href=\"/help\">Help</a></li></ul></footer>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<footer><p>A <a href=\"https://32bit.cafe\" rel=\"noopener\">32bit.cafe</a> Project</p><ul class=\"footer-links\"><li><a href=\"/about\">About</a></li><li><a href=\"/help\">Help</a></li></ul></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -203,33 +227,33 @@ func base(title string, data CommonData) templ.Component {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<!doctype html><html lang=\"en\"><head><title>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<!doctype html><html lang=\"en\"><head><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 82, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 99, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " - webweav.ing</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"htmx-config\" content=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " - webweav.ing</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"htmx-config\" content=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(`{"includeIndicatorStyles":false}`)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 85, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 102, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><link href=\"/static/fontawesome/css/fontawesome.css\" rel=\"stylesheet\"><link href=\"/static/fontawesome/css/solid.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><link href=\"/static/fontawesome/css/fontawesome.css\" rel=\"stylesheet\"><link href=\"/static/fontawesome/css/solid.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -241,25 +265,25 @@ func base(title string, data CommonData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<main role=\"main\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<main role=\"main\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Flash != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"notice flash\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"notice flash\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, 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: 96, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 113, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -268,7 +292,7 @@ func base(title string, data CommonData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</main>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -276,7 +300,140 @@ func base(title string, data CommonData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func pagination(baseUrl string, currentPage int64, recordsAmount int64, recordsPerPage int64) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
totalPages := int64(math.Ceil(float64(recordsAmount) / float64(recordsPerPage)))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"pagination\"><span class=\"pagination-btn\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if currentPage > 1 {
url := fmt.Sprintf("%s?page=%d&count=%d", baseUrl, currentPage-1, recordsPerPage)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 templ.SafeURL = templ.URL(url)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var11)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">Prev</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Prev")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for i := range totalPages {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"pagination-btn\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
url := fmt.Sprintf("%s?page=%d&count=%d", baseUrl, i+1, recordsPerPage)
if i+1 == currentPage {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i+1))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 137, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 templ.SafeURL = templ.URL(url)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var13)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i+1))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 139, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<span class=\"pagination-btn\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if currentPage < totalPages {
url := fmt.Sprintf("%s?page=%d&count=%d", baseUrl, currentPage+1, recordsPerPage)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 templ.SafeURL = templ.URL(url)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var15)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">Next</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Next")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

75
ui/views/install.templ Normal file
View File

@ -0,0 +1,75 @@
package views
import "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
templ installBase(title string) {
<!DOCTYPE html>
<html lang="en">
<head>
<title>{ title } - webweav.ing</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link href="/static/css/style.css" rel="stylesheet"/>
<script src="/static/js/htmx.min.js"></script>
</head>
<body>
<main>
<h1>{ title }</h1>
{ children... }
</main>
@commonFooter()
</body>
</html>
}
templ InitialInstallView(title string) {
@installBase(title) {
<p>Installing</p>
<a href="/install">Next</a>
}
}
templ InstallFormView(title string, form forms.InstallForm) {
@installBase(title) {
<form action="/install" method="post">
<fieldset>
<legend>Admin User</legend>
<div class="form-group">
<div>
{{ error, exists := form.FieldErrors[""] }}
<label for="username">Username: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="username" id="username" value={ form.Name } required/>
</div>
<div>
{{ error, exists = form.FieldErrors["email"] }}
<label for="email">Email: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="email" id="email" value={ form.Email } required/>
</div>
<div>
{{ error, exists = form.FieldErrors["password"] }}
<label for="password">Password: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="password" name="password" id="password"/>
</div>
</div>
</fieldset>
<div>
<input type="submit" value="Install"/>
</div>
</form>
}
}
templ InstallSuccessView() {
@installBase("Success") {
<p>Installation was successful. Go <a href="/">home</a>.</p>
}
}

335
ui/views/install_templ.go Normal file
View File

@ -0,0 +1,335 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
func installBase(title string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/install.templ`, Line: 9, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - webweav.ing</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body><main><h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/install.templ`, Line: 17, Col: 15}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = commonFooter().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func InitialInstallView(title string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p>Installing</p><a href=\"/install\">Next</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = installBase(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func InstallFormView(title string, form forms.InstallForm) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var7 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<form action=\"/install\" method=\"post\"><fieldset><legend>Admin User</legend><div class=\"form-group\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists := form.FieldErrors[""]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<label for=\"username\">Username: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/install.templ`, Line: 42, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<input type=\"text\" name=\"username\" id=\"username\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(form.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/install.templ`, Line: 44, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" required></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists = form.FieldErrors["email"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<label for=\"email\">Email: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/install.templ`, Line: 50, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<input type=\"text\" name=\"email\" id=\"email\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/install.templ`, Line: 52, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" required></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists = form.FieldErrors["password"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<label for=\"password\">Password: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/install.templ`, Line: 58, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<input type=\"password\" name=\"password\" id=\"password\"></div></div></fieldset><div><input type=\"submit\" value=\"Install\"></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = installBase(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var7), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func InstallSuccessView() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var14 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p>Installation was successful. Go <a href=\"/\">home</a>.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = installBase("Success").Render(templ.WithChildren(ctx, templ_7745c5c3_Var14), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -9,25 +9,27 @@ templ UserLogin(title string, data CommonData, form forms.UserLoginForm) {
@base(title, data) {
<h1>Login</h1>
<form action="/users/login" method="POST" novalidate>
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
for _, error := range form.NonFieldErrors {
<div class="error">{ error }</div>
}
<div>
<label>Email: </label>
{{ error, exists := form.FieldErrors["email"] }}
if exists {
<label class="error">{ error }</label>
<div class="form-group">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
for _, error := range form.NonFieldErrors {
<div class="error">{ error }</div>
}
<input type="email" name="email" value={ form.Email }/>
</div>
<div>
<label>Password: </label>
{{ error, exists = form.FieldErrors["password"] }}
if exists {
<label class="error">{ error }</label>
}
<input type="password" name="password"/>
<div>
<label>Email: </label>
{{ error, exists := form.FieldErrors["email"] }}
if exists {
<label class="error">{ error }</label>
}
<input type="email" name="email" value={ form.Email }/>
</div>
<div>
<label>Password: </label>
{{ error, exists = form.FieldErrors["password"] }}
if exists {
<label class="error">{ error }</label>
}
<input type="password" name="password"/>
</div>
</div>
<div>
<input type="submit" value="Login"/>
@ -44,29 +46,31 @@ templ UserRegistration(title string, data CommonData, form forms.UserRegistratio
<h1>User Registration</h1>
<form action="/users/register" method="post">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
<div>
{{ error, exists := form.FieldErrors["name"] }}
<label for="username">Username: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="username" id="username" value={ form.Name } required/>
</div>
<div>
{{ error, exists = form.FieldErrors["email"] }}
<label for="email">Email: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="email" id="email" value={ form.Email } required/>
</div>
<div>
{{ error, exists = form.FieldErrors["password"] }}
<label for="password">Password: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="password" name="password" id="password"/>
<div class="form-group">
<div>
{{ error, exists := form.FieldErrors["name"] }}
<label for="username">Username: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="username" id="username" value={ form.Name } required/>
</div>
<div>
{{ error, exists = form.FieldErrors["email"] }}
<label for="email">Email: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="email" id="email" value={ form.Email } required/>
</div>
<div>
{{ error, exists = form.FieldErrors["password"] }}
<label for="password">Password: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="password" name="password" id="password"/>
</div>
</div>
<div>
<input type="submit" value="Register"/>
@ -90,7 +94,6 @@ templ UserSettingsView(data CommonData, timezones []string) {
<form hx-put="/users/settings">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
<fieldset>
<legend id="user-settings-heading">User Settings</legend>
<div class="form-group">
<label>Local Timezone</label>
<select name="timezones" id="timezone-select">

View File

@ -46,14 +46,14 @@ func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Co
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1>Login</h1><form action=\"/users/login\" method=\"POST\" novalidate><input type=\"hidden\" name=\"csrf_token\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1>Login</h1><form action=\"/users/login\" method=\"POST\" novalidate><div class=\"form-group\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 12, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 13, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@ -71,7 +71,7 @@ func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Co
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 14, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 15, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@ -95,7 +95,7 @@ func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Co
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 20, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 21, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@ -113,7 +113,7 @@ func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Co
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 22, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 23, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@ -132,7 +132,7 @@ func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Co
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 28, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 29, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@ -143,7 +143,7 @@ func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Co
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"password\" name=\"password\"></div><div><input type=\"submit\" value=\"Login\"> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"password\" name=\"password\"></div></div><div><input type=\"submit\" value=\"Login\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -207,13 +207,13 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 46, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 48, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><div class=\"form-group\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -230,7 +230,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 51, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 54, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@ -248,7 +248,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(form.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 53, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 56, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@ -271,7 +271,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 59, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 62, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@ -289,7 +289,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 61, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 64, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@ -312,7 +312,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 67, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 70, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@ -323,7 +323,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<input type=\"password\" name=\"password\" id=\"password\"></div><div><input type=\"submit\" value=\"Register\"></div></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<input type=\"password\" name=\"password\" id=\"password\"></div></div><div><input type=\"submit\" value=\"Register\"></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -377,7 +377,7 @@ func UserProfile(title string, data CommonData, user models.User) templ.Componen
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 80, Col: 21}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 84, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@ -390,7 +390,7 @@ func UserProfile(title string, data CommonData, user models.User) templ.Componen
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 81, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 85, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@ -451,13 +451,13 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 91, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"><fieldset><legend id=\"user-settings-heading\">User Settings</legend><div class=\"form-group\"><label>Local Timezone</label> <select name=\"timezones\" id=\"timezone-select\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"><fieldset><div class=\"form-group\"><label>Local Timezone</label> <select name=\"timezones\" id=\"timezone-select\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -470,7 +470,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 99, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 102, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
@ -483,7 +483,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 99, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 102, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@ -501,7 +501,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 101, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 104, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@ -514,7 +514,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 101, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 104, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {