111 lines
2.8 KiB
Go
111 lines
2.8 KiB
Go
package handlers
|
|
|
|
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,
|
|
}
|
|
}
|