feat: verify routes
This commit is contained in:
@ -1,27 +1,22 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/cache"
|
||||
"gitea.local/admin/hspguard/internal/config"
|
||||
imiddleware "gitea.local/admin/hspguard/internal/middleware"
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"gitea.local/admin/hspguard/internal/types"
|
||||
"gitea.local/admin/hspguard/internal/util"
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
repo *repository.Queries
|
||||
cfg *config.AppConfig
|
||||
repo *repository.Queries
|
||||
cache *cache.Client
|
||||
cfg *config.AppConfig
|
||||
}
|
||||
|
||||
func (h *AuthHandler) signTokens(user *repository.User) (string, string, error) {
|
||||
@ -60,9 +55,10 @@ func (h *AuthHandler) signTokens(user *repository.User) (string, string, error)
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
func NewAuthHandler(repo *repository.Queries, cfg *config.AppConfig) *AuthHandler {
|
||||
func NewAuthHandler(repo *repository.Queries, cache *cache.Client, cfg *config.AppConfig) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
repo,
|
||||
cache,
|
||||
cfg,
|
||||
}
|
||||
}
|
||||
@ -74,166 +70,11 @@ func (h *AuthHandler) RegisterRoutes(api chi.Router) {
|
||||
protected.Use(authMiddleware.Runner)
|
||||
|
||||
protected.Get("/profile", h.getProfile)
|
||||
protected.Post("/email", h.requestEmailOtp)
|
||||
protected.Post("/email/otp", h.confirmOtp)
|
||||
})
|
||||
|
||||
r.Post("/login", h.login)
|
||||
r.Post("/refresh", h.refreshToken)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) refreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
web.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(authHeader, "Bearer ")
|
||||
if len(parts) != 2 {
|
||||
web.Error(w, "invalid auth header format", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenStr := parts[1]
|
||||
token, userClaims, err := util.VerifyToken(tokenStr, h.cfg.Jwt.PublicKey)
|
||||
if err != nil || !token.Valid {
|
||||
http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
expire, err := userClaims.GetExpirationTime()
|
||||
if err != nil {
|
||||
web.Error(w, "failed to retrieve enough info from the token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if time.Now().After(expire.Time) {
|
||||
web.Error(w, "token is expired", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := uuid.Parse(userClaims.Subject)
|
||||
if err != nil {
|
||||
web.Error(w, "failed to parsej user id from token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.FindUserId(r.Context(), userId)
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided email does not exists", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
access, refresh, err := h.signTokens(&user)
|
||||
if err != nil {
|
||||
web.Error(w, "failed to generate tokens", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
AccessToken string `json:"access"`
|
||||
RefreshToken string `json:"refresh"`
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := encoder.Encode(Response{
|
||||
AccessToken: access,
|
||||
RefreshToken: refresh,
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getProfile(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := util.GetRequestUserId(r.Context())
|
||||
if !ok {
|
||||
web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId))
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided id does not exist", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(types.NewUserDTO(&user)); err != nil {
|
||||
web.Error(w, "failed to encode user profile", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
type LoginParams struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
var params LoginParams
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(¶ms); err != nil {
|
||||
web.Error(w, "failed to parse request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Email == "" || params.Password == "" {
|
||||
web.Error(w, "missing required fields", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: looking for user with following params: %#v\n", params)
|
||||
|
||||
user, err := h.repo.FindUserEmail(r.Context(), params.Email)
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided email does not exists", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !util.VerifyPassword(params.Password, user.PasswordHash) {
|
||||
web.Error(w, "username or/and password are incorrect", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
access, refresh, err := h.signTokens(&user)
|
||||
if err != nil {
|
||||
web.Error(w, "failed to generate tokens", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.UpdateLastLogin(r.Context(), user.ID); err != nil {
|
||||
web.Error(w, "failed to update user's last login", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
|
||||
type Response struct {
|
||||
AccessToken string `json:"access"`
|
||||
RefreshToken string `json:"refresh"`
|
||||
// fields required for UI in account selector, e.g. email, full name and avatar
|
||||
FullName string `json:"full_name"`
|
||||
Email string `json:"email"`
|
||||
Id string `json:"id"`
|
||||
ProfilePicture *string `json:"profile_picture"`
|
||||
// Avatar
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := encoder.Encode(Response{
|
||||
AccessToken: access,
|
||||
RefreshToken: refresh,
|
||||
FullName: user.FullName,
|
||||
Email: user.Email,
|
||||
Id: user.ID.String(),
|
||||
ProfilePicture: user.ProfilePicture,
|
||||
// Avatar
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
99
internal/auth/verify.go
Normal file
99
internal/auth/verify.go
Normal file
@ -0,0 +1,99 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/util"
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (h *AuthHandler) requestEmailOtp(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := util.GetRequestUserId(r.Context())
|
||||
if !ok {
|
||||
web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId))
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided id does not exist", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
web.Error(w, "email is already verified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
number := rand.Intn(1000000) // 0 to 999999
|
||||
padded := fmt.Sprintf("%06d", number) // Always 6 characters
|
||||
|
||||
if _, err := h.cache.Set(r.Context(), fmt.Sprintf("otp-%s", user.ID.String()), padded, 5*time.Minute).Result(); err != nil {
|
||||
log.Println("ERR: Failed to save OTP in cache:", err)
|
||||
web.Error(w, "failed to generate otp", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("INFO: Saved OTP %s\n", padded)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
type ConfirmOtpRequest struct {
|
||||
OTP string `json:"otp"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) confirmOtp(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := util.GetRequestUserId(r.Context())
|
||||
if !ok {
|
||||
web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId))
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided id does not exist", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
web.Error(w, "email is already verified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req ConfirmOtpRequest
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
web.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
val, err := h.cache.Get(r.Context(), fmt.Sprintf("otp-%s", user.ID.String())).Result()
|
||||
if err != nil {
|
||||
web.Error(w, "otp verification session not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("INFO: Comparing OTP %s == %s\n", req.OTP, val)
|
||||
|
||||
if req.OTP == val {
|
||||
err := h.repo.UserVerifyEmail(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
log.Println("ERR: Failed to update email_verified:", err)
|
||||
web.Error(w, "failed to verify email", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
web.Error(w, "otp verification failed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
Reference in New Issue
Block a user