From 8d705c957beb462796c2e04ef7f51e261f59ee22 Mon Sep 17 00:00:00 2001 From: yequari Date: Sat, 23 Aug 2025 14:09:34 -0700 Subject: [PATCH] Setup admin groups, admin panel view, installation process --- cmd/web/handlers_admin.go | 61 +++ cmd/web/installer.go | 69 ++++ cmd/web/main.go | 61 ++- cmd/web/middleware.go | 16 + cmd/web/routes.go | 7 +- internal/forms/forms.go | 16 +- internal/models/install.go | 62 +++ internal/models/mocks/user.go | 5 +- internal/models/user.go | 128 ++++++- migrations/000008_add_user_groups.down.sql | 2 + migrations/000008_add_user_groups.up.sql | 19 + migrations/000009_user_ban_date.down.sql | 2 + migrations/000009_user_ban_date.up.sql | 2 + migrations/000010_installation.down.sql | 1 + migrations/000010_installation.up.sql | 5 + ui/views/admin.templ | 117 ++++++ ui/views/admin_templ.go | 416 +++++++++++++++++++++ ui/views/common.templ | 24 +- ui/views/common_templ.go | 69 ++-- ui/views/install.templ | 75 ++++ ui/views/install_templ.go | 335 +++++++++++++++++ 21 files changed, 1443 insertions(+), 49 deletions(-) create mode 100644 cmd/web/handlers_admin.go create mode 100644 cmd/web/installer.go create mode 100644 internal/models/install.go create mode 100644 migrations/000008_add_user_groups.down.sql create mode 100644 migrations/000008_add_user_groups.up.sql create mode 100644 migrations/000009_user_ban_date.down.sql create mode 100644 migrations/000009_user_ban_date.up.sql create mode 100644 migrations/000010_installation.down.sql create mode 100644 migrations/000010_installation.up.sql create mode 100644 ui/views/admin.templ create mode 100644 ui/views/admin_templ.go create mode 100644 ui/views/install.templ create mode 100644 ui/views/install_templ.go diff --git a/cmd/web/handlers_admin.go b/cmd/web/handlers_admin.go new file mode 100644 index 0000000..cd21ee3 --- /dev/null +++ b/cmd/web/handlers_admin.go @@ -0,0 +1,61 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + + "git.32bit.cafe/32bitcafe/guestbook/internal/models" + "git.32bit.cafe/32bitcafe/guestbook/ui/views" +) + +func (app *application) getAdminPanelLanding(w http.ResponseWriter, r *http.Request) { + data := app.newCommonData(r) + views.AdminPanelLandingView("Admin Panel", data).Render(r.Context(), w) +} + +func (app *application) getAdminPanelAllUsers(w http.ResponseWriter, r *http.Request) { + users, err := app.users.GetAll() + if err != nil { + app.serverError(w, r, err) + return + } + data := app.newCommonData(r) + views.AdminPanelUsersView("All Users - Admin", data, users).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) 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 + } +} + +func (app *application) getAdminPanelWebsites(w http.ResponseWriter, r *http.Request) { +} diff --git a/cmd/web/installer.go b/cmd/web/installer.go new file mode 100644 index 0000000..42cdd5d --- /dev/null +++ b/cmd/web/installer.go @@ -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() +} diff --git a/cmd/web/main.go b/cmd/web/main.go index 26ff243..b9dba2f 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -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 +} diff --git a/cmd/web/middleware.go b/cmd/web/middleware.go index a2d916e..781ad0b 100644 --- a/cmd/web/middleware.go +++ b/cmd/web/middleware.go @@ -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) diff --git a/cmd/web/routes.go b/cmd/web/routes.go index e2f6509..1ba345b 100644 --- a/cmd/web/routes.go +++ b/cmd/web/routes.go @@ -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,10 @@ 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)) + return standard.Then(mux) } diff --git a/internal/forms/forms.go b/internal/forms/forms.go index ebb7b25..bfb739c 100644 --- a/internal/forms/forms.go +++ b/internal/forms/forms.go @@ -25,9 +25,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 +50,13 @@ type WebsiteSettingsForm struct { WidgetsEnabled string `schema:"gb_remote"` validator.Validator `schema:"-"` } + +type AdminUserMgmtForm struct { +} + +type InstallForm struct { + Name string `schema:"username"` + Email string `schema:"email"` + Password string `schema:"password"` + validator.Validator `schema:"-"` +} diff --git a/internal/models/install.go b/internal/models/install.go new file mode 100644 index 0000000..872d75d --- /dev/null +++ b/internal/models/install.go @@ -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 +} diff --git a/internal/models/mocks/user.go b/internal/models/mocks/user.go index 0ca4d30..25da0e3 100644 --- a/internal/models/mocks/user.go +++ b/internal/models/mocks/user.go @@ -13,7 +13,6 @@ var mockUser = models.User{ Username: "tester", Email: "test@example.com", Deleted: false, - IsBanned: false, Created: time.Now(), Settings: mockUserSettings, } @@ -120,3 +119,7 @@ func (m *UserModel) UpdateSubject(userId int64, subject string) error { } return errors.New("invalid") } + +func (m *UserModel) GetNumberOfUsers() int { + return 1 +} diff --git a/internal/models/user.go b/internal/models/user.go index 13eacc5..f7d735c 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -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 { @@ -57,6 +65,9 @@ type UserModelInterface interface { 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 } func (m *UserModel) InitializeSettingsMap() error { @@ -96,8 +107,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 +118,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 +142,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 +165,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 +189,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 +199,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 +217,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 +228,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 @@ -215,14 +244,15 @@ func (m *UserModel) Get(shortId uint64) (User, error) { } 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 +262,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 +273,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 @@ -400,3 +441,66 @@ 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 INTO users_groups (UserId, GroupId) VALUES (?, ?)` + _, 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) 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 +} diff --git a/migrations/000008_add_user_groups.down.sql b/migrations/000008_add_user_groups.down.sql new file mode 100644 index 0000000..95884c5 --- /dev/null +++ b/migrations/000008_add_user_groups.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS users_groups; +DROP TABLE IF EXISTS groups; diff --git a/migrations/000008_add_user_groups.up.sql b/migrations/000008_add_user_groups.up.sql new file mode 100644 index 0000000..99c1aab --- /dev/null +++ b/migrations/000008_add_user_groups.up.sql @@ -0,0 +1,19 @@ +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 +); diff --git a/migrations/000009_user_ban_date.down.sql b/migrations/000009_user_ban_date.down.sql new file mode 100644 index 0000000..9fa7787 --- /dev/null +++ b/migrations/000009_user_ban_date.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE users DROP COLUMN Banned; +ALTER TABLE users ADD COLUMN IsBanned boolean NOT NULL DEFAULT false; \ No newline at end of file diff --git a/migrations/000009_user_ban_date.up.sql b/migrations/000009_user_ban_date.up.sql new file mode 100644 index 0000000..df8b6ad --- /dev/null +++ b/migrations/000009_user_ban_date.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users DROP COLUMN IsBanned; +ALTER TABLE users ADD COLUMN Banned datetime NULL; \ No newline at end of file diff --git a/migrations/000010_installation.down.sql b/migrations/000010_installation.down.sql new file mode 100644 index 0000000..9760855 --- /dev/null +++ b/migrations/000010_installation.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS installed; \ No newline at end of file diff --git a/migrations/000010_installation.up.sql b/migrations/000010_installation.up.sql new file mode 100644 index 0000000..5cb45d9 --- /dev/null +++ b/migrations/000010_installation.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE installed ( + Id integer primary key autoincrement, + InstallDate datetime NOT NULL, + InstallVersion varchar(100) NULL +); \ No newline at end of file diff --git a/ui/views/admin.templ b/ui/views/admin.templ new file mode 100644 index 0000000..10247cd --- /dev/null +++ b/ui/views/admin.templ @@ -0,0 +1,117 @@ +package views + +import ( + "fmt" + "git.32bit.cafe/32bitcafe/guestbook/internal/models" + "time" +) + +templ adminBase(title string, data CommonData) { + + + + { title } - webweav.ing + + + + + + + + + +
+ Back to webweav.ing +
+
+ { children... } +
+ @commonFooter() + + +} + +templ adminSidebar() { + +} + +templ AdminPanelLandingView(title string, data CommonData) { + @adminBase(title, data) { +
+ @adminSidebar() +
+

{ title }

+

Welcome to the admin panel

+
+
+ } +} + +templ AdminPanelUsersView(title string, data CommonData, users []models.User) { + @adminBase(title, data) { +
+ @adminSidebar() +
+
+ + + + + + for _, u := range users { + {{ url := fmt.Sprintf("/admin/users/%s", shortIdToSlug(u.ShortId)) }} + + + + } + +
UsernameJoinedEmail
{ u.Username }{ u.Created.Format(time.RFC3339) }{ u.Email }
+
+
+
+ } +} + +templ AdminPanelUserMgmtView(title string, data CommonData, user models.User) { + @adminBase(title, data) { +
+ @adminSidebar() +
+
+

User Info

+
+
Username
+

{ user.Username }

+
+
+
Email
+

{ user.Email }

+
+
+
Joined
+

{ user.Created.Format(time.RFC3339) }

+
+
+
+

Groups

+
    + for _, g := range user.Groups { +
  • { fmt.Sprintf("%d %s", g, getGroupName(g)) }
  • + } +
+
+
+
+ } +} diff --git a/ui/views/admin_templ.go b/ui/views/admin_templ.go new file mode 100644 index 0000000..6bd303b --- /dev/null +++ b/ui/views/admin_templ.go @@ -0,0 +1,416 @@ +// 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 ( + "fmt" + "git.32bit.cafe/32bitcafe/guestbook/internal/models" + "time" +) + +func adminBase(title string, data CommonData) 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, "") + 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/admin.templ`, Line: 13, 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
Back to webweav.ing
") + 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, "
") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func adminSidebar() 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func AdminPanelLandingView(title string, data CommonData) 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_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var6 := 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = adminSidebar().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") + 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/admin.templ`, Line: 54, Col: 15} + } + _, 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, 9, "

Welcome to the admin panel

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = adminBase(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func AdminPanelUsersView(title string, data CommonData, users []models.User) 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_Var8 := templ.GetChildren(ctx) + if templ_7745c5c3_Var8 == nil { + templ_7745c5c3_Var8 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var9 := 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, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = adminSidebar().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, u := range users { + url := fmt.Sprintf("/admin/users/%s", shortIdToSlug(u.ShortId)) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
UsernameJoinedEmail
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/admin.templ`, Line: 74, Col: 51} + } + _, 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, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(u.Created.Format(time.RFC3339)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/admin.templ`, Line: 75, Col: 44} + } + _, 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, 15, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/admin.templ`, Line: 76, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = adminBase(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var9), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func AdminPanelUserMgmtView(title string, data CommonData, user models.User) 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_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var15 := 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, 18, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = adminSidebar().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

User Info

Username

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/admin.templ`, Line: 95, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

Email

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/admin.templ`, Line: 99, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

Joined

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(user.Created.Format(time.RFC3339)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/admin.templ`, Line: 103, Col: 44} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

Groups

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, g := range user.Groups { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d %s", g, getGroupName(g))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/admin.templ`, Line: 110, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = adminBase(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var15), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/ui/views/common.templ b/ui/views/common.templ index 666b370..720c840 100644 --- a/ui/views/common.templ +++ b/ui/views/common.templ @@ -1,9 +1,12 @@ 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" + "slices" + "strconv" + "strings" +) type CommonData struct { CurrentYear int @@ -33,6 +36,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() {

webweav.ing

@@ -54,6 +67,9 @@ templ topNav(data CommonData) {
  • All Guestbooks
  • My Websites
  • Settings
  • + if slices.Contains(data.CurrentUser.Groups, models.AdminGroup) { +
  • Admin Panel
  • + }
  • Logout
  • } else { if data.LocalAuthEnabled { diff --git a/ui/views/common_templ.go b/ui/views/common_templ.go index 6391240..7546f26 100644 --- a/ui/views/common_templ.go +++ b/ui/views/common_templ.go @@ -8,10 +8,13 @@ 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" + "slices" + "strconv" + "strings" +) type CommonData struct { CurrentYear int @@ -41,6 +44,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 +117,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: 61, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -116,36 +129,46 @@ func topNav(data CommonData) templ.Component { return templ_7745c5c3_Err } if data.IsAuthenticated { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
  • All Guestbooks
  • My Websites
  • Settings
  • All Guestbooks
  • My Websites
  • Settings
  • ") + 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, "
  • Admin Panel
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
  • Logout
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">Logout") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { if data.LocalAuthEnabled { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
  • Create an Account
  • | ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
  • Create an Account
  • | ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
  • Login
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
  • Login
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -174,7 +197,7 @@ func commonFooter() templ.Component { templ_7745c5c3_Var5 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -203,33 +226,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, "") + 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: 98, 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") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -241,25 +264,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, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.Flash != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
    ") 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: 112, 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, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -268,7 +291,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, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -276,7 +299,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, 18, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/ui/views/install.templ b/ui/views/install.templ new file mode 100644 index 0000000..c71172f --- /dev/null +++ b/ui/views/install.templ @@ -0,0 +1,75 @@ +package views + +import "git.32bit.cafe/32bitcafe/guestbook/internal/forms" + +templ installBase(title string) { + + + + { title } - webweav.ing + + + + + + +
    +

    { title }

    + { children... } +
    + @commonFooter() + + +} + +templ InitialInstallView(title string) { + @installBase(title) { +

    Installing

    + Next + } +} + +templ InstallFormView(title string, form forms.InstallForm) { + @installBase(title) { +
    +
    + Admin User +
    +
    + {{ error, exists := form.FieldErrors[""] }} + + if exists { + + } + +
    +
    + {{ error, exists = form.FieldErrors["email"] }} + + if exists { + + } + +
    +
    + {{ error, exists = form.FieldErrors["password"] }} + + if exists { + + } + +
    +
    +
    +
    + +
    +
    + } +} + +templ InstallSuccessView() { + @installBase("Success") { +

    Installation was successful. Go home.

    + } +} diff --git a/ui/views/install_templ.go b/ui/views/install_templ.go new file mode 100644 index 0000000..0c958a2 --- /dev/null +++ b/ui/views/install_templ.go @@ -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, "") + 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

    ") + 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, "

    ") + 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, "
    ") + 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, "") + 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, "

    Installing

    Next") + 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, "
    Admin User
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + error, exists := form.FieldErrors[""] + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if exists { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + error, exists = form.FieldErrors["email"] + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if exists { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + error, exists = form.FieldErrors["password"] + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if exists { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
    ") + 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, "

    Installation was successful. Go home.

    ") + 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