aboutsummaryrefslogtreecommitdiffstats
path: root/internal/store/users.go
diff options
context:
space:
mode:
authorgramanas <grm@eyesin.space>2026-09-12 11:24:17 +0300
committergramanas <grm@eyesin.space>2026-09-12 11:24:17 +0300
commit3eb04b1a2bdf9e53231fe862cfd76327371a9741 (patch)
treeb38b2d82a47233fd8e0bb18c59e4a8f3dd2412d7 /internal/store/users.go
downloadblogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.tar.gz
blogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.tar.bz2
blogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.zip
Initial multi-tenant blog host
Go + Postgres application serving a management dashboard on the base domain and one public blog per subdomain. Markdown posts organised in pages, form-based theme customisation, image uploads stored in Postgres, JWT cookie sessions with CSRF, superadmin user management, RSS feeds. Docker/compose deployment and a Makefile-driven dev environment with seed data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/store/users.go')
-rw-r--r--internal/store/users.go99
1 files changed, 99 insertions, 0 deletions
diff --git a/internal/store/users.go b/internal/store/users.go
new file mode 100644
index 0000000..2910888
--- /dev/null
+++ b/internal/store/users.go
@@ -0,0 +1,99 @@
+package store
+
+import (
+ "context"
+ "time"
+)
+
+const (
+ RoleSuperadmin = "superadmin"
+ RoleBlogger = "blogger"
+)
+
+type User struct {
+ ID int64
+ Username string
+ PasswordHash string
+ Role string
+ Disabled bool
+ TokenVersion int
+ CreatedAt time.Time
+}
+
+func (u *User) IsSuperadmin() bool { return u.Role == RoleSuperadmin }
+
+const userCols = `id, username, password_hash, role, disabled, token_version, created_at`
+
+func scanUser(row interface{ Scan(...any) error }) (*User, error) {
+ var u User
+ err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &u.Disabled, &u.TokenVersion, &u.CreatedAt)
+ if err != nil {
+ return nil, wrap(err)
+ }
+ return &u, nil
+}
+
+func (s *Store) CreateUser(ctx context.Context, username, passwordHash, role string) (*User, error) {
+ row := s.db.QueryRow(ctx, `INSERT INTO users (username, password_hash, role) VALUES ($1,$2,$3) RETURNING `+userCols,
+ username, passwordHash, role)
+ return scanUser(row)
+}
+
+func (s *Store) UserByID(ctx context.Context, id int64) (*User, error) {
+ return scanUser(s.db.QueryRow(ctx, `SELECT `+userCols+` FROM users WHERE id=$1`, id))
+}
+
+func (s *Store) UserByUsername(ctx context.Context, username string) (*User, error) {
+ return scanUser(s.db.QueryRow(ctx, `SELECT `+userCols+` FROM users WHERE username=$1`, username))
+}
+
+func (s *Store) CountSuperadmins(ctx context.Context) (int, error) {
+ var n int
+ err := s.db.QueryRow(ctx, `SELECT count(*) FROM users WHERE role=$1`, RoleSuperadmin).Scan(&n)
+ return n, err
+}
+
+// SetPassword replaces the hash and bumps token_version so existing sessions die.
+func (s *Store) SetPassword(ctx context.Context, id int64, passwordHash string) error {
+ _, err := s.db.Exec(ctx, `UPDATE users SET password_hash=$2, token_version=token_version+1 WHERE id=$1`, id, passwordHash)
+ return err
+}
+
+func (s *Store) SetUserDisabled(ctx context.Context, id int64, disabled bool) error {
+ _, err := s.db.Exec(ctx, `UPDATE users SET disabled=$2, token_version=token_version+1 WHERE id=$1`, id, disabled)
+ return err
+}
+
+func (s *Store) DeleteUser(ctx context.Context, id int64) error {
+ _, err := s.db.Exec(ctx, `DELETE FROM users WHERE id=$1`, id)
+ return err
+}
+
+// UserWithBlog is a row for the admin overview.
+type UserWithBlog struct {
+ User
+ BlogID *int64
+ Subdomain *string
+ BlogTitle *string
+}
+
+func (s *Store) ListUsers(ctx context.Context) ([]UserWithBlog, error) {
+ rows, err := s.db.Query(ctx, `SELECT u.id, u.username, u.password_hash, u.role, u.disabled, u.token_version, u.created_at,
+ b.id, b.subdomain, b.title
+ FROM users u LEFT JOIN blogs b ON b.owner_id = u.id
+ ORDER BY u.role, u.username`)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var out []UserWithBlog
+ for rows.Next() {
+ var r UserWithBlog
+ if err := rows.Scan(&r.ID, &r.Username, &r.PasswordHash, &r.Role, &r.Disabled, &r.TokenVersion, &r.CreatedAt,
+ &r.BlogID, &r.Subdomain, &r.BlogTitle); err != nil {
+ return nil, err
+ }
+ out = append(out, r)
+ }
+ return out, rows.Err()
+}