Full auth handlers, refactored swagger and some other changes

This commit is contained in:
2026-03-18 20:51:30 +05:00
parent 91312d5797
commit cf2c133ba6
11 changed files with 279 additions and 91 deletions

View File

@@ -1,10 +1,23 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"log/slog"
"net/http"
"time"
"github.com/anxi0uz/logiflow/internal/api"
"github.com/anxi0uz/logiflow/internal/models"
storage "github.com/anxi0uz/logiflow/pkg"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/gosimple/slug"
"github.com/huandu/go-sqlbuilder"
"github.com/redis/go-redis/v9"
"golang.org/x/crypto/bcrypt"
)
type Claims struct {
@@ -16,11 +29,247 @@ type Claims struct {
}
func (s *Server) AuthLogin(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req api.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.JSON(w, r, http.StatusBadRequest, "Ошибка при получении данных", "error")
return
}
user, err := storage.GetOne[models.User](ctx, s.DB, "users", func(sb *sqlbuilder.SelectBuilder) {
sb.Where(sb.Equal("email", req.Email))
})
if err != nil {
slog.WarnContext(ctx, "user with that email not found in db", slog.String("Email", string(req.Email)), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusBadRequest, "Пользователь с таким Email не найден", "error")
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
slog.WarnContext(ctx, "Failed login to account with", slog.String("email:", string(req.Email)), slog.String("password from request", req.Password))
s.JSON(w, r, http.StatusBadRequest, "Неверный пароль", "error")
return
}
s.issueTokens(w, r, user)
}
func (s *Server) AuthLogout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
cookie, err := r.Cookie("refresh_token")
if err != nil {
s.JSON(w, r, http.StatusUnauthorized, "Missing refresh token", "error")
return
}
refreshStr := cookie.Value
if refreshStr == "" {
s.JSON(w, r, http.StatusUnauthorized, "Empty refresh token", "error")
return
}
refreshKey := "refresh_token:" + refreshStr
if err := s.Redis.Del(ctx, refreshKey).Err(); err != nil {
slog.ErrorContext(ctx, "Error while removing refresh token from redis", slog.String("token", refreshStr), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusUnauthorized, "Internal server error", "error")
return
}
tokenStr := r.Header.Get("Authorization")
if tokenStr == "" {
s.JSON(w, r, http.StatusUnauthorized, "missing token", "error")
return
}
tokenKey := "access_token:" + tokenStr
if err := s.Redis.Del(ctx, tokenKey).Err(); err != nil {
slog.ErrorContext(ctx, "Error while removing access token from redis", slog.String("token", tokenStr), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusUnauthorized, "Internal server error", "error")
return
}
}
func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
cookie, err := r.Cookie("refresh_token")
if err != nil {
s.JSON(w, r, http.StatusUnauthorized, "Missing refresh token", "error")
return
}
refreshStr := cookie.Value
if refreshStr == "" {
s.JSON(w, r, http.StatusUnauthorized, "Empty refresh token", "error")
return
}
key := "refresh_token:" + refreshStr
if _, err := s.Redis.Get(ctx, key).Result(); err == redis.Nil {
s.JSON(w, r, http.StatusUnauthorized, "No active refresh token", "error")
return
}
claimsValue := ctx.Value("user")
claims, ok := claimsValue.(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error parsing claims", slog.Any("claims", claims))
s.JSON(w, r, http.StatusInternalServerError, nil, "internal server error")
return
}
userID := claims.ID
if userID == uuid.Nil {
s.JSON(w, r, http.StatusUnauthorized, "Missing user id in token", "error")
return
}
user, err := storage.GetOne[models.User](ctx, s.DB, "users", func(sb *sqlbuilder.SelectBuilder) {
sb.Where(sb.Equal("id", userID))
})
if err != nil {
slog.ErrorContext(ctx, "user with that id not found", slog.Any("id", userID.String()), "error", err.Error())
s.JSON(w, r, http.StatusUnauthorized, "Invalid user id in token", "error")
return
}
s.issueTokens(w, r, user)
}
func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req api.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.JSON(w, r, http.StatusBadRequest, "invalid request body", "error")
return
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
s.JSON(w, r, http.StatusInternalServerError, "error while hashing password", "error")
return
}
now := time.Now()
uuid := uuid.New()
user := models.User{
ID: uuid,
Slug: s.GenerateUserSlug(req.FullName, uuid),
CreatedAt: now,
UpdatedAt: now,
Email: string(req.Email),
PasswordHash: string(passwordHash),
FullName: req.FullName,
}
if err := storage.Create[models.User](ctx, "users", user, s.DB); err != nil {
slog.ErrorContext(ctx, "Error while creating user", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Server error", "error")
return
}
s.issueTokens(w, r, &user)
}
func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {}
func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {}
func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {}
func (s *Server) issueTokens(w http.ResponseWriter, r *http.Request, user *models.User) {
access, err := s.generateAccessToken(user, s.Config.RedisAccessTokenDur())
if err != nil {
slog.ErrorContext(r.Context(), "generate access failed", slog.String("error", err.Error()))
return
}
refresh, err := s.generateRefreshToken()
if err != nil {
slog.ErrorContext(r.Context(), "generate refresh token failed", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Failure during generating tokens", "error")
}
key := "access_token:" + access
refreshkey := "refresh_token:" + refresh
err = s.Redis.Set(r.Context(), key, "valid", s.Config.RedisAccessTokenDur()).Err()
if err != nil {
slog.ErrorContext(r.Context(), "Failed to set access token in redis", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Server error", "error")
return
}
err = s.Redis.Set(r.Context(), refreshkey, "valid", s.Config.RedisRefreshTokenDur()).Err()
if err != nil {
slog.ErrorContext(r.Context(), "Failed to set refresh token in redis", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Server error", "error")
return
}
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: refresh,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
MaxAge: 7 * 24 * 3600,
})
s.JSON(w, r, http.StatusOK, map[string]any{
"access_token": access,
"expires_in": 86400,
}, "auth")
}
func (s *Server) deleteRefreshCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: "",
Path: "/",
MaxAge: -1,
})
}
func (s *Server) generateAccessToken(user *models.User, duration time.Duration) (string, error) {
return s.generateJWT(user, duration)
}
func (s *Server) generateRefreshToken() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
func (s *Server) generateJWT(user *models.User, lifetime time.Duration) (string, error) {
if len(s.JwtKey) == 0 {
return "", errors.New("jwt key not set")
}
tokenID := hex.EncodeToString([]byte(time.Now().String() + user.ID.String()))
claims := Claims{
ID: user.ID,
Email: user.Email,
Role: user.Role,
TokenID: tokenID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(lifetime)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: user.ID.String(),
Issuer: s.Config.JwtOpt.Issuer,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.JwtKey)
}
func (s *Server) GenerateUserSlug(username string, uuid uuid.UUID) string {
if username == "" {
username = "user"
}
base := slug.Make(username)
return base + uuid.String()
}
func (s *Server) AuthLogout(w http.ResponseWriter, r *http.Request) {}
func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {}
func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) {}
func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {}
func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {}
func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {}