Add user database migration, core reusable components, and layout structure
This commit is contained in:
@@ -1,7 +1,110 @@
|
||||
package handlers
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"workorders/internal/config"
|
||||
"workorders/internal/model"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
db *sqlx.DB
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewAuthHandler(db *sqlx.DB, cfg *config.Config) *AuthHandler {
|
||||
return &AuthHandler{db: db, cfg: cfg}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := decode(r, &body); err != nil || body.Username == "" || body.Password == "" {
|
||||
respondError(w, http.StatusBadRequest, "username and password required")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
err := h.db.Get(&user,
|
||||
`SELECT * FROM users WHERE (username=@p1 OR email=@p1) AND active=1`,
|
||||
body.Username)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "invalid credentials")
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(body.Password)); err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "invalid credentials")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := generateToken(user, h.cfg.JWTSecret)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "token generation failed")
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Exec(`UPDATE users SET last_login=GETUTCDATE() WHERE id=@p1`, user.ID)
|
||||
|
||||
respond(w, http.StatusOK, map[string]any{
|
||||
"token": token,
|
||||
"user": publicUser(user),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
user := userFromCtx(r)
|
||||
|
||||
var dbUser model.User
|
||||
if err := h.db.Get(&dbUser, `SELECT * FROM users WHERE id=@p1 AND active=1`, user.UserID); err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := generateToken(dbUser, h.cfg.JWTSecret)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "token generation failed")
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, http.StatusOK, map[string]any{"token": token})
|
||||
}
|
||||
|
||||
func Me(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, http.StatusOK, userFromCtx(r))
|
||||
}
|
||||
|
||||
func generateToken(user model.User, secret string) (string, error) {
|
||||
claims := model.LocalClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: strconv.Itoa(user.ID),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(8 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
DisplayName: user.DisplayName,
|
||||
Role: user.Role,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
func publicUser(u model.User) map[string]any {
|
||||
return map[string]any{
|
||||
"id": u.ID,
|
||||
"username": u.Username,
|
||||
"email": u.Email,
|
||||
"display_name": u.DisplayName,
|
||||
"role": u.Role,
|
||||
"avatar_url": u.AvatarURL,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user