Add user theme support and migrate frontend to HTMX templates
- Add theme column to users table with default 'tokyo-night' - Update all user queries to include theme field - Add ListUsers and UpdateUserTheme database queries - Update auth handlers to support HTMX form submissions and JSON API - Add ListUsers API endpoint - Replace embedded static files with Go templates - Update Dockerfile to copy templates directory - Redesign index.html with inline styles and HTMX forms - Update Bruno API testing requests for auth endpoints
This commit is contained in:
@@ -36,6 +36,7 @@ type Users struct {
|
||||
Email string `db:"email" json:"email"`
|
||||
Username string `db:"username" json:"username"`
|
||||
PasswordHash string `db:"password_hash" json:"password_hash"`
|
||||
Theme pgtype.Text `db:"theme" json:"theme"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ type Querier interface {
|
||||
GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error)
|
||||
GetUserByUsername(ctx context.Context, username string) (Users, error)
|
||||
ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebooks, error)
|
||||
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
||||
UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error)
|
||||
UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error)
|
||||
UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
|
||||
@@ -57,25 +57,32 @@ func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebook
|
||||
}
|
||||
|
||||
const CreateUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (email, username, password_hash)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, email, username, password_hash, created_at, updated_at
|
||||
INSERT INTO users (email, username, password_hash, theme)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, email, username, password_hash, theme, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
Email string `db:"email" json:"email"`
|
||||
Username string `db:"username" json:"username"`
|
||||
PasswordHash string `db:"password_hash" json:"password_hash"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Username string `db:"username" json:"username"`
|
||||
PasswordHash string `db:"password_hash" json:"password_hash"`
|
||||
Theme pgtype.Text `db:"theme" json:"theme"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (Users, error) {
|
||||
row := q.db.QueryRow(ctx, CreateUser, arg.Email, arg.Username, arg.PasswordHash)
|
||||
row := q.db.QueryRow(ctx, CreateUser,
|
||||
arg.Email,
|
||||
arg.Username,
|
||||
arg.PasswordHash,
|
||||
arg.Theme,
|
||||
)
|
||||
var i Users
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.Theme,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -152,13 +159,14 @@ func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgress
|
||||
}
|
||||
|
||||
const GetUser = `-- name: GetUser :one
|
||||
SELECT id, email, username, created_at, updated_at FROM users WHERE id = $1
|
||||
SELECT id, email, username, theme, created_at, updated_at FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
type GetUserRow struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Username string `db:"username" json:"username"`
|
||||
Theme pgtype.Text `db:"theme" json:"theme"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -170,6 +178,7 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, erro
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.Theme,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -177,7 +186,7 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, erro
|
||||
}
|
||||
|
||||
const GetUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE email = $1
|
||||
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, error) {
|
||||
@@ -188,6 +197,7 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, erro
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.Theme,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -195,7 +205,7 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, erro
|
||||
}
|
||||
|
||||
const GetUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
|
||||
SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE email = $1 OR username = $1
|
||||
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE email = $1 OR username = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) {
|
||||
@@ -206,6 +216,7 @@ func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (U
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.Theme,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -213,7 +224,7 @@ func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (U
|
||||
}
|
||||
|
||||
const GetUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE username = $1
|
||||
SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE username = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users, error) {
|
||||
@@ -224,6 +235,7 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.Theme,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -271,6 +283,46 @@ func (q *Queries) ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebook
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListUsers = `-- name: ListUsers :many
|
||||
SELECT id, email, username, theme, created_at, updated_at FROM users ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
type ListUsersRow struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Username string `db:"username" json:"username"`
|
||||
Theme pgtype.Text `db:"theme" json:"theme"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ListUsersRow{}
|
||||
for rows.Next() {
|
||||
var i ListUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.Theme,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const UpdateEbook = `-- name: UpdateEbook :one
|
||||
UPDATE ebooks SET
|
||||
title = $2,
|
||||
@@ -354,3 +406,17 @@ func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingPr
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const UpdateUserTheme = `-- name: UpdateUserTheme :exec
|
||||
UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdateUserThemeParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
Theme pgtype.Text `db:"theme" json:"theme"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateUserTheme, arg.ID, arg.Theme)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (email, username, password_hash)
|
||||
VALUES ($1, $2, $3)
|
||||
INSERT INTO users (email, username, password_hash, theme)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
@@ -13,7 +13,10 @@ SELECT * FROM users WHERE username = $1;
|
||||
SELECT * FROM users WHERE email = $1 OR username = $1;
|
||||
|
||||
-- name: GetUser :one
|
||||
SELECT id, email, username, created_at, updated_at FROM users WHERE id = $1;
|
||||
SELECT id, email, username, theme, created_at, updated_at FROM users WHERE id = $1;
|
||||
|
||||
-- name: ListUsers :many
|
||||
SELECT id, email, username, theme, created_at, updated_at FROM users ORDER BY created_at DESC;
|
||||
|
||||
-- name: GetEbook :one
|
||||
SELECT * FROM ebooks WHERE id = $1;
|
||||
@@ -54,4 +57,7 @@ DO UPDATE SET
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteReadingProgress :exec
|
||||
DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2;
|
||||
DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2;
|
||||
|
||||
-- name: UpdateUserTheme :exec
|
||||
UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1;
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"bookmann/internal/database"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -25,14 +26,14 @@ func NewAuthHandler(db *database.Queries, jwtSecret string) *AuthHandler {
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
Email string `form:"email" json:"email" validate:"required,email"`
|
||||
Username string `form:"username" json:"username" validate:"required,min=3,max=50"`
|
||||
Password string `form:"password" json:"password" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Login string `json:"login" validate:"required"` // email or username
|
||||
Password string `json:"password" validate:"required"`
|
||||
Login string `form:"login" json:"login" validate:"required"` // email or username
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
@@ -48,26 +49,66 @@ type UserProfile struct {
|
||||
|
||||
// Register handles POST /api/auth/register
|
||||
func (h *AuthHandler) Register(c echo.Context) error {
|
||||
var req RegisterRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
// Try form data first (HTMX), then JSON (Bruno)
|
||||
email := c.FormValue("email")
|
||||
username := c.FormValue("username")
|
||||
password := c.FormValue("password")
|
||||
|
||||
if email == "" || username == "" || password == "" {
|
||||
// Fallback to JSON binding
|
||||
req := RegisterRequest{}
|
||||
if err := c.Bind(&req); err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Invalid request</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
email = req.Email
|
||||
username = req.Username
|
||||
password = req.Password
|
||||
}
|
||||
|
||||
req := RegisterRequest{Email: email, Username: username, Password: password}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
if _, err := h.db.GetUserByEmail(c.Request().Context(), req.Email); err == nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusConflict, `<div class="text-red-500">Email already exists</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusConflict, map[string]string{"error": "email already exists"})
|
||||
}
|
||||
|
||||
if _, err := h.db.GetUserByUsername(c.Request().Context(), req.Username); err == nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusConflict, `<div class="text-red-500">Username already exists</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusConflict, map[string]string{"error": "username already exists"})
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to hash password</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
|
||||
}
|
||||
|
||||
@@ -78,15 +119,33 @@ func (h *AuthHandler) Register(c echo.Context) error {
|
||||
PasswordHash: string(hashedPassword),
|
||||
})
|
||||
if err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String())
|
||||
if err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to generate token</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
|
||||
}
|
||||
|
||||
// Check if request is from HTMX
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
// Return HTML with script to set token and redirect
|
||||
html := fmt.Sprintf(`<div class="text-green-500">Registration successful! Redirecting...</div>
|
||||
<script>
|
||||
localStorage.setItem('token', '%s');
|
||||
localStorage.setItem('user', JSON.stringify(%s));
|
||||
window.location.href = '/';
|
||||
</script>`, token, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username))
|
||||
return c.HTML(http.StatusCreated, html)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusCreated, AuthResponse{
|
||||
Token: token,
|
||||
User: UserProfile{
|
||||
@@ -99,31 +158,90 @@ func (h *AuthHandler) Register(c echo.Context) error {
|
||||
|
||||
// Login handles POST /api/auth/login
|
||||
func (h *AuthHandler) Login(c echo.Context) error {
|
||||
var req LoginRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
// Debug logging
|
||||
fmt.Printf("Login request - Content-Type: %s\n", c.Request().Header.Get("Content-Type"))
|
||||
fmt.Printf("Form values - login: %s, password: %s\n", c.FormValue("login"), c.FormValue("password"))
|
||||
|
||||
// Try form data first (HTMX), then JSON (Bruno)
|
||||
login := c.FormValue("login")
|
||||
password := c.FormValue("password")
|
||||
|
||||
if login == "" || password == "" {
|
||||
fmt.Printf("Form values empty, trying JSON bind\n")
|
||||
// Fallback to JSON binding
|
||||
req := LoginRequest{}
|
||||
if err := c.Bind(&req); err != nil {
|
||||
fmt.Printf("JSON bind error: %v\n", err)
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Invalid request</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
fmt.Printf("Validation error: %v\n", err)
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
login = req.Login
|
||||
password = req.Password
|
||||
fmt.Printf("JSON bind success - login: %s\n", login)
|
||||
}
|
||||
|
||||
req := LoginRequest{Login: login, Password: password}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
fmt.Printf("Final validation error: %v\n", err)
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Get user by email or username
|
||||
user, err := h.db.GetUserByEmailOrUsername(c.Request().Context(), req.Login)
|
||||
if err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusUnauthorized, `<div class="text-red-500">Invalid credentials</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
||||
}
|
||||
|
||||
// Check password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusUnauthorized, `<div class="text-red-500">Invalid credentials</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String())
|
||||
if err != nil {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to generate token</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
|
||||
}
|
||||
|
||||
// Check if request is from HTMX
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
// Return HTML with script to set token and redirect
|
||||
html := fmt.Sprintf(`<div class="text-green-500">Login successful! Redirecting...</div>
|
||||
<script>
|
||||
localStorage.setItem('token', '%s');
|
||||
localStorage.setItem('user', JSON.stringify(%s));
|
||||
window.location.href = '/';
|
||||
</script>`, token, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username))
|
||||
return c.HTML(http.StatusOK, html)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, AuthResponse{
|
||||
Token: token,
|
||||
User: UserProfile{
|
||||
@@ -154,6 +272,49 @@ func (h *AuthHandler) GetProfile(c echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// ListUsers handles GET /api/users
|
||||
func (h *AuthHandler) ListUsers(c echo.Context) error {
|
||||
users, err := h.db.ListUsers(c.Request().Context())
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
type UserList struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Theme string `json:"theme"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
var userList []UserList
|
||||
for _, u := range users {
|
||||
theme := ""
|
||||
if u.Theme.Valid {
|
||||
theme = u.Theme.String
|
||||
}
|
||||
createdAt := ""
|
||||
if u.CreatedAt.Valid {
|
||||
createdAt = u.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
updatedAt := ""
|
||||
if u.UpdatedAt.Valid {
|
||||
updatedAt = u.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
userList = append(userList, UserList{
|
||||
ID: uuid.UUID(u.ID.Bytes).String(),
|
||||
Email: u.Email,
|
||||
Username: u.Username,
|
||||
Theme: theme,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, userList)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) generateJWT(userID string) (string, error) {
|
||||
claims := jwtgo.MapClaims{
|
||||
"user_id": userID,
|
||||
|
||||
Reference in New Issue
Block a user