2025-08-07 13:47:42 +00:00
|
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"net/http"
|
2025-08-08 16:47:02 +00:00
|
|
|
|
"strings"
|
2025-09-28 11:46:20 +00:00
|
|
|
|
"time"
|
2025-08-07 13:47:42 +00:00
|
|
|
|
|
|
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
|
|
|
|
|
|
|
|
|
|
"neomovies-api/pkg/middleware"
|
|
|
|
|
|
"neomovies-api/pkg/models"
|
|
|
|
|
|
"neomovies-api/pkg/services"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type AuthHandler struct {
|
|
|
|
|
|
authService *services.AuthService
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
2025-08-08 16:47:02 +00:00
|
|
|
|
return &AuthHandler{authService: authService}
|
2025-08-07 13:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
var req models.RegisterRequest
|
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response, err := h.authService.Register(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusConflict)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
2025-08-08 16:47:02 +00:00
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: response, Message: "User registered successfully"})
|
2025-08-07 13:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
var req models.LoginRequest
|
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 11:46:20 +00:00
|
|
|
|
// Получаем информацию о клиенте для refresh токена
|
|
|
|
|
|
userAgent := r.Header.Get("User-Agent")
|
|
|
|
|
|
ipAddress := r.RemoteAddr
|
|
|
|
|
|
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
|
|
|
|
|
ipAddress = forwarded
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response, err := h.authService.LoginWithTokens(req, userAgent, ipAddress)
|
2025-08-07 13:47:42 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
statusCode := http.StatusBadRequest
|
|
|
|
|
|
if err.Error() == "Account not activated. Please verify your email." {
|
2025-08-08 16:47:02 +00:00
|
|
|
|
statusCode = http.StatusForbidden
|
2025-08-07 13:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
http.Error(w, err.Error(), statusCode)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2025-08-08 16:47:02 +00:00
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: response, Message: "Login successful"})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) GoogleLogin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
state := generateState()
|
|
|
|
|
|
http.SetCookie(w, &http.Cookie{Name: "oauth_state", Value: state, HttpOnly: true, Path: "/", Expires: time.Now().Add(10 * time.Minute)})
|
|
|
|
|
|
url, err := h.authService.GetGoogleLoginURL(state)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
http.Redirect(w, r, url, http.StatusFound)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) GoogleCallback(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
q := r.URL.Query()
|
|
|
|
|
|
state := q.Get("state")
|
2025-09-28 11:46:20 +00:00
|
|
|
|
code := q.Get("code")
|
2025-08-08 16:47:02 +00:00
|
|
|
|
preferJSON := q.Get("response") == "json" || strings.Contains(r.Header.Get("Accept"), "application/json")
|
|
|
|
|
|
cookie, _ := r.Cookie("oauth_state")
|
|
|
|
|
|
if cookie == nil || cookie.Value != state || code == "" {
|
|
|
|
|
|
if preferJSON {
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: false, Message: "invalid oauth state"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
redirectURL, ok := h.authService.BuildFrontendRedirect("", "invalid_state")
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
http.Error(w, "invalid oauth state", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := h.authService.HandleGoogleCallback(r.Context(), code)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if preferJSON {
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: false, Message: err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
redirectURL, ok := h.authService.BuildFrontendRedirect("", "auth_failed")
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if preferJSON {
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: resp, Message: "Login successful"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
redirectURL, ok := h.authService.BuildFrontendRedirect(resp.Token, "")
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: resp, Message: "Login successful"})
|
2025-08-07 13:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
user, err := h.authService.GetUserByID(userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, "User not found", http.StatusNotFound)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2025-08-08 16:47:02 +00:00
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: user})
|
2025-08-07 13:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var updates map[string]interface{}
|
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
|
|
|
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
delete(updates, "password")
|
|
|
|
|
|
delete(updates, "email")
|
|
|
|
|
|
delete(updates, "_id")
|
|
|
|
|
|
delete(updates, "created_at")
|
|
|
|
|
|
|
|
|
|
|
|
user, err := h.authService.UpdateUser(userID, bson.M(updates))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, "Failed to update user", http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2025-08-08 16:47:02 +00:00
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: user, Message: "Profile updated successfully"})
|
2025-08-07 13:47:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 10:35:07 +00:00
|
|
|
|
func (h *AuthHandler) DeleteAccount(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := h.authService.DeleteAccount(r.Context(), userID); err != nil {
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2025-08-08 16:47:02 +00:00
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Message: "Account deleted successfully"})
|
2025-08-08 10:35:07 +00:00
|
|
|
|
}
|
2025-08-08 16:47:02 +00:00
|
|
|
|
|
2025-08-07 13:47:42 +00:00
|
|
|
|
func (h *AuthHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
var req models.VerifyEmailRequest
|
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response, err := h.authService.VerifyEmail(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
json.NewEncoder(w).Encode(response)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) ResendVerificationCode(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
var req models.ResendCodeRequest
|
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response, err := h.authService.ResendVerificationCode(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
json.NewEncoder(w).Encode(response)
|
2025-08-08 16:47:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 11:46:20 +00:00
|
|
|
|
// RefreshToken refreshes an access token using a refresh token
|
|
|
|
|
|
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
var req models.RefreshTokenRequest
|
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Получаем информацию о клиенте
|
|
|
|
|
|
userAgent := r.Header.Get("User-Agent")
|
|
|
|
|
|
ipAddress := r.RemoteAddr
|
|
|
|
|
|
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
|
|
|
|
|
ipAddress = forwarded
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokenPair, err := h.authService.RefreshAccessToken(req.RefreshToken, userAgent, ipAddress)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
Data: tokenPair,
|
|
|
|
|
|
Message: "Token refreshed successfully",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RevokeRefreshToken revokes a specific refresh token
|
|
|
|
|
|
func (h *AuthHandler) RevokeRefreshToken(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req models.RefreshTokenRequest
|
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := h.authService.RevokeRefreshToken(userID, req.RefreshToken)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
Message: "Refresh token revoked successfully",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RevokeAllRefreshTokens revokes all refresh tokens for the current user
|
|
|
|
|
|
func (h *AuthHandler) RevokeAllRefreshTokens(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := h.authService.RevokeAllRefreshTokens(userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
json.NewEncoder(w).Encode(models.APIResponse{
|
|
|
|
|
|
Success: true,
|
|
|
|
|
|
Message: "All refresh tokens revoked successfully",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 16:47:02 +00:00
|
|
|
|
// helpers
|
2025-09-28 11:46:20 +00:00
|
|
|
|
func generateState() string { return uuidNew() }
|