first commit
This commit is contained in:
294
backend/internal/interfaces/handlers/admin_handler.go
Normal file
294
backend/internal/interfaces/handlers/admin_handler.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"github.com/noteapp/backend/internal/application/dto"
|
||||
"github.com/noteapp/backend/internal/application/services"
|
||||
)
|
||||
|
||||
// AdminHandler handles admin-level HTTP requests
|
||||
type AdminHandler struct {
|
||||
adminService *services.AdminService
|
||||
}
|
||||
|
||||
// NewAdminHandler creates a new AdminHandler
|
||||
func NewAdminHandler(adminService *services.AdminService) *AdminHandler {
|
||||
return &AdminHandler{adminService: adminService}
|
||||
}
|
||||
|
||||
// ListUsers handles GET /admin/users
|
||||
func (h *AdminHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := h.adminService.ListUsers(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"users": users})
|
||||
}
|
||||
|
||||
// UpdateUserGroups handles PUT /admin/users/{userId}/groups
|
||||
func (h *AdminHandler) UpdateUserGroups(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := bson.ObjectIDFromHex(mux.Vars(r)["userId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid user id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserGroupsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
groupIDs := make([]bson.ObjectID, 0, len(req.GroupIDs))
|
||||
for _, groupID := range req.GroupIDs {
|
||||
parsed, err := bson.ObjectIDFromHex(groupID)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid group id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
groupIDs = append(groupIDs, parsed)
|
||||
}
|
||||
|
||||
user, err := h.adminService.UpdateUserGroups(r.Context(), userID, groupIDs)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
// ListGroups handles GET /admin/groups
|
||||
func (h *AdminHandler) ListGroups(w http.ResponseWriter, r *http.Request) {
|
||||
groups, err := h.adminService.ListGroups(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"groups": groups})
|
||||
}
|
||||
|
||||
// CreateGroup handles POST /admin/groups
|
||||
func (h *AdminHandler) CreateGroup(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.CreatePermissionGroupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := h.adminService.CreateGroup(r.Context(), &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(group)
|
||||
}
|
||||
|
||||
// UpdateGroup handles PUT /admin/groups/{groupId}
|
||||
func (h *AdminHandler) UpdateGroup(w http.ResponseWriter, r *http.Request) {
|
||||
groupID, err := bson.ObjectIDFromHex(mux.Vars(r)["groupId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid group id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdatePermissionGroupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := h.adminService.UpdateGroup(r.Context(), groupID, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(group)
|
||||
}
|
||||
|
||||
// ListAllSpaces handles GET /admin/spaces
|
||||
func (h *AdminHandler) ListAllSpaces(w http.ResponseWriter, r *http.Request) {
|
||||
spaces, err := h.adminService.ListAllSpaces(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"spaces": spaces})
|
||||
}
|
||||
|
||||
// UpdateSpace handles PUT /admin/spaces/{spaceId}
|
||||
func (h *AdminHandler) UpdateSpace(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CreateSpaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
space, err := h.adminService.UpdateSpace(r.Context(), spaceID, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(space)
|
||||
}
|
||||
|
||||
// SetSpaceVisibility handles PUT /admin/spaces/{spaceId}/visibility
|
||||
func (h *AdminHandler) SetSpaceVisibility(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
IsPublic bool `json:"is_public"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.adminService.SetSpaceVisibility(r.Context(), spaceID, req.IsPublic); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "visibility updated"})
|
||||
}
|
||||
|
||||
// AddSpaceMember handles POST /admin/spaces/{spaceId}/members
|
||||
func (h *AdminHandler) AddSpaceMember(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.AddSpaceMemberRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := bson.ObjectIDFromHex(req.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid user id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminService.AddSpaceMember(r.Context(), spaceID, userID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "member added"})
|
||||
}
|
||||
|
||||
// ListSpaceMembers handles GET /admin/spaces/{spaceId}/members
|
||||
func (h *AdminHandler) ListSpaceMembers(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
members, err := h.adminService.ListSpaceMembers(r.Context(), spaceID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"members": members})
|
||||
}
|
||||
|
||||
// RemoveSpaceMember handles DELETE /admin/spaces/{spaceId}/members/{userId}
|
||||
func (h *AdminHandler) RemoveSpaceMember(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := bson.ObjectIDFromHex(mux.Vars(r)["userId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid user id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminService.RemoveSpaceMember(r.Context(), spaceID, userID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteSpace handles DELETE /admin/spaces/{spaceId}
|
||||
func (h *AdminHandler) DeleteSpace(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.adminService.DeleteSpace(r.Context(), spaceID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetFeatureFlags handles GET /admin/feature-flags
|
||||
func (h *AdminHandler) GetFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
flags, err := h.adminService.GetFeatureFlags(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(flags)
|
||||
}
|
||||
|
||||
// UpdateFeatureFlags handles PUT /admin/feature-flags
|
||||
func (h *AdminHandler) UpdateFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.UpdateFeatureFlagsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
flags, err := h.adminService.UpdateFeatureFlags(r.Context(), &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(flags)
|
||||
}
|
||||
299
backend/internal/interfaces/handlers/auth_handler.go
Normal file
299
backend/internal/interfaces/handlers/auth_handler.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/noteapp/backend/internal/application/dto"
|
||||
"github.com/noteapp/backend/internal/application/services"
|
||||
"github.com/noteapp/backend/internal/infrastructure/auth"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication endpoints
|
||||
type AuthHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new auth handler
|
||||
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// Register handles user registration
|
||||
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.RegisterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if req.Email == "" || req.Password == "" || req.Username == "" {
|
||||
http.Error(w, "Missing required fields", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.authService.Register(r.Context(), &req)
|
||||
if err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "registration is currently disabled") {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// Login handles user login
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.authService.Login(r.Context(), &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Set secure HTTP-only cookie for refresh token
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: response.RefreshToken,
|
||||
Path: "/",
|
||||
MaxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
HttpOnly: true,
|
||||
Secure: isSecureRequest(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// Logout handles user logout
|
||||
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
// Clear refresh token cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureRequest(r),
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Logged out successfully"})
|
||||
}
|
||||
|
||||
// ListProviders returns all active OAuth/OIDC providers.
|
||||
func (h *AuthHandler) ListProviders(w http.ResponseWriter, r *http.Request) {
|
||||
providers, err := h.authService.ListProviders(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"providers": providers})
|
||||
}
|
||||
|
||||
// CreateProvider stores a new OAuth/OIDC provider configuration.
|
||||
func (h *AuthHandler) CreateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.CreateAuthProviderRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := h.authService.CreateProvider(r.Context(), &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(provider)
|
||||
}
|
||||
|
||||
// StartProviderLogin redirects the browser to the selected provider.
|
||||
func (h *AuthHandler) StartProviderLogin(w http.ResponseWriter, r *http.Request) {
|
||||
providerID, err := bson.ObjectIDFromHex(mux.Vars(r)["providerId"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid provider ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
state, err := auth.GenerateStateToken()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create OAuth state", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "oauth_state",
|
||||
Value: state,
|
||||
Path: "/",
|
||||
MaxAge: 10 * 60,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureRequest(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
redirectURI := buildBackendURL(r, "/api/v1/auth/providers/"+providerID.Hex()+"/callback")
|
||||
authorizationURL, err := h.authService.BuildProviderAuthorizationURL(r.Context(), providerID, redirectURI, state)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, authorizationURL, http.StatusFound)
|
||||
}
|
||||
|
||||
// CompleteProviderLogin exchanges the authorization code and redirects back to the frontend.
|
||||
func (h *AuthHandler) CompleteProviderLogin(w http.ResponseWriter, r *http.Request) {
|
||||
providerID, err := bson.ObjectIDFromHex(mux.Vars(r)["providerId"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid provider ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
stateCookie, err := r.Cookie("oauth_state")
|
||||
if err != nil || stateCookie.Value == "" || stateCookie.Value != r.URL.Query().Get("state") {
|
||||
http.Error(w, "Invalid OAuth state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.authService.CompleteProviderLogin(r.Context(), providerID, r.URL.Query().Get("code"), buildBackendURL(r, "/api/v1/auth/providers/"+providerID.Hex()+"/callback"))
|
||||
if err != nil {
|
||||
http.Redirect(w, r, buildFrontendLoginURL("oauth_error", err.Error(), "", nil), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "oauth_state",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureRequest(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: response.RefreshToken,
|
||||
Path: "/",
|
||||
MaxAge: 7 * 24 * 60 * 60,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureRequest(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
http.Redirect(w, r, buildFrontendLoginURL("oauth_success", "", response.AccessToken, response.User), http.StatusFound)
|
||||
}
|
||||
|
||||
// RefreshToken handles token refresh
|
||||
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Get refresh token from cookie
|
||||
cookie, err := r.Cookie("refresh_token")
|
||||
if err != nil {
|
||||
http.Error(w, "Refresh token not found", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, err := h.authService.RefreshAccessToken(r.Context(), cookie.Value)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": accessToken,
|
||||
"expires_in": 3600,
|
||||
})
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
func (h *AuthHandler) Health(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "healthy",
|
||||
})
|
||||
}
|
||||
|
||||
func isSecureRequest(r *http.Request) bool {
|
||||
if r.TLS != nil {
|
||||
return true
|
||||
}
|
||||
return strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https")
|
||||
}
|
||||
|
||||
func buildBackendURL(r *http.Request, path string) string {
|
||||
scheme := "http"
|
||||
if isSecureRequest(r) {
|
||||
scheme = "https"
|
||||
}
|
||||
return scheme + "://" + r.Host + path
|
||||
}
|
||||
|
||||
func buildFrontendLoginURL(status, message, accessToken string, user *dto.UserDTO) string {
|
||||
frontendURL := os.Getenv("FRONTEND_URL")
|
||||
if frontendURL == "" {
|
||||
frontendURL = "http://localhost:5173"
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(strings.TrimRight(frontendURL, "/") + "/login")
|
||||
if err != nil {
|
||||
return frontendURL + "/login"
|
||||
}
|
||||
|
||||
query := parsed.Query()
|
||||
if status != "" {
|
||||
query.Set("status", status)
|
||||
}
|
||||
if message != "" {
|
||||
query.Set("message", message)
|
||||
}
|
||||
if accessToken != "" {
|
||||
query.Set("access_token", accessToken)
|
||||
}
|
||||
if user != nil {
|
||||
payload, _ := json.Marshal(user)
|
||||
query.Set("user_json", string(payload))
|
||||
query.Set("user", base64.RawURLEncoding.EncodeToString(payload))
|
||||
}
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
212
backend/internal/interfaces/handlers/category_handler.go
Normal file
212
backend/internal/interfaces/handlers/category_handler.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"github.com/noteapp/backend/internal/application/dto"
|
||||
"github.com/noteapp/backend/internal/application/services"
|
||||
"github.com/noteapp/backend/internal/interfaces/middleware"
|
||||
)
|
||||
|
||||
// CategoryHandler handles category endpoints
|
||||
type CategoryHandler struct {
|
||||
categoryService *services.CategoryService
|
||||
}
|
||||
|
||||
// NewCategoryHandler creates a new category handler
|
||||
func NewCategoryHandler(categoryService *services.CategoryService) *CategoryHandler {
|
||||
return &CategoryHandler{categoryService: categoryService}
|
||||
}
|
||||
|
||||
// GetCategoryTree returns the full category tree for a space
|
||||
func (h *CategoryHandler) GetCategoryTree(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := getUserObjectID(r)
|
||||
if err != nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID, err := bson.ObjectIDFromHex(vars["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tree, err := h.categoryService.GetCategoryTree(r.Context(), spaceID, userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(tree)
|
||||
}
|
||||
|
||||
// CreateCategory creates a new category in a space
|
||||
func (h *CategoryHandler) CreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := getUserObjectID(r)
|
||||
if err != nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID, err := bson.ObjectIDFromHex(vars["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CreateCategoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := h.categoryService.CreateCategory(r.Context(), spaceID, userID, &req)
|
||||
if err != nil {
|
||||
if err.Error() == "unauthorized" {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(category)
|
||||
}
|
||||
|
||||
// UpdateCategory updates a category
|
||||
func (h *CategoryHandler) UpdateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := getUserObjectID(r)
|
||||
if err != nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID, err := bson.ObjectIDFromHex(vars["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
categoryID, err := bson.ObjectIDFromHex(vars["categoryId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateCategoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := h.categoryService.UpdateCategory(r.Context(), categoryID, spaceID, userID, &req)
|
||||
if err != nil {
|
||||
if err.Error() == "unauthorized" {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(category)
|
||||
}
|
||||
|
||||
// DeleteCategory deletes a category
|
||||
func (h *CategoryHandler) DeleteCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := getUserObjectID(r)
|
||||
if err != nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID, err := bson.ObjectIDFromHex(vars["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
categoryID, err := bson.ObjectIDFromHex(vars["categoryId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var moveNotesTo *string
|
||||
if v := r.URL.Query().Get("moveNotesTo"); v != "" {
|
||||
moveNotesTo = &v
|
||||
}
|
||||
|
||||
if err := h.categoryService.DeleteCategory(r.Context(), categoryID, spaceID, userID, moveNotesTo); err != nil {
|
||||
if err.Error() == "unauthorized" {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// MoveCategory moves a category to a new parent
|
||||
func (h *CategoryHandler) MoveCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := getUserObjectID(r)
|
||||
if err != nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID, err := bson.ObjectIDFromHex(vars["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
categoryID, err := bson.ObjectIDFromHex(vars["categoryId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ParentID *string `json:"parent_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := h.categoryService.MoveCategory(r.Context(), categoryID, spaceID, userID, body.ParentID)
|
||||
if err != nil {
|
||||
if err.Error() == "unauthorized" {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(category)
|
||||
}
|
||||
|
||||
// getUserObjectID extracts the user ObjectID from the request context
|
||||
func getUserObjectID(r *http.Request) (bson.ObjectID, error) {
|
||||
userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string)
|
||||
if !ok || userIDStr == "" {
|
||||
return bson.NilObjectID, http.ErrNoCookie
|
||||
}
|
||||
return bson.ObjectIDFromHex(userIDStr)
|
||||
}
|
||||
266
backend/internal/interfaces/handlers/note_handler.go
Normal file
266
backend/internal/interfaces/handlers/note_handler.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"github.com/noteapp/backend/internal/application/dto"
|
||||
"github.com/noteapp/backend/internal/application/services"
|
||||
"github.com/noteapp/backend/internal/interfaces/middleware"
|
||||
)
|
||||
|
||||
// NoteHandler handles note endpoints
|
||||
type NoteHandler struct {
|
||||
noteService *services.NoteService
|
||||
}
|
||||
|
||||
// NewNoteHandler creates a new note handler
|
||||
func NewNoteHandler(noteService *services.NoteService) *NoteHandler {
|
||||
return &NoteHandler{
|
||||
noteService: noteService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNote creates a new note
|
||||
func (h *NoteHandler) CreateNote(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
var req dto.CreateNoteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
note, err := h.noteService.CreateNote(r.Context(), spaceObjID, userObjID, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(note)
|
||||
}
|
||||
|
||||
// GetNote retrieves a note
|
||||
func (h *NoteHandler) GetNote(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID := vars["spaceId"]
|
||||
noteID := vars["noteId"]
|
||||
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
noteObjID, _ := bson.ObjectIDFromHex(noteID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
note, err := h.noteService.GetNote(r.Context(), noteObjID, spaceObjID, userObjID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(note)
|
||||
}
|
||||
|
||||
// GetNotesBySpace retrieves notes in a space
|
||||
func (h *NoteHandler) GetNotesBySpace(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Pagination
|
||||
skip, _ := strconv.Atoi(r.URL.Query().Get("skip"))
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit == 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
notes, err := h.noteService.GetNotesBySpace(r.Context(), spaceObjID, userObjID, skip, limit)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(notes)
|
||||
}
|
||||
|
||||
// SearchNotes performs full-text search
|
||||
func (h *NoteHandler) SearchNotes(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
query := r.URL.Query().Get("q")
|
||||
|
||||
if query == "" {
|
||||
http.Error(w, "Missing search query", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
notes, err := h.noteService.SearchNotes(r.Context(), spaceObjID, userObjID, query)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(notes)
|
||||
}
|
||||
|
||||
// UpdateNote updates a note
|
||||
func (h *NoteHandler) UpdateNote(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID := vars["spaceId"]
|
||||
noteID := vars["noteId"]
|
||||
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateNoteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
noteObjID, _ := bson.ObjectIDFromHex(noteID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
note, err := h.noteService.UpdateNote(r.Context(), noteObjID, spaceObjID, userObjID, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(note)
|
||||
}
|
||||
|
||||
// DeleteNote deletes a note
|
||||
func (h *NoteHandler) DeleteNote(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID := vars["spaceId"]
|
||||
noteID := vars["noteId"]
|
||||
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
noteObjID, _ := bson.ObjectIDFromHex(noteID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
if err := h.noteService.DeleteNote(r.Context(), noteObjID, spaceObjID, userObjID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UnlockNote verifies a note password and returns full note content
|
||||
func (h *NoteHandler) UnlockNote(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
spaceID := vars["spaceId"]
|
||||
noteID := vars["noteId"]
|
||||
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UnlockNoteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
noteObjID, _ := bson.ObjectIDFromHex(noteID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
note, err := h.noteService.UnlockNote(r.Context(), noteObjID, spaceObjID, userObjID, req.Password)
|
||||
if err != nil {
|
||||
if err.Error() == "invalid note password" {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(note)
|
||||
}
|
||||
156
backend/internal/interfaces/handlers/public_handler.go
Normal file
156
backend/internal/interfaces/handlers/public_handler.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"github.com/noteapp/backend/internal/application/dto"
|
||||
"github.com/noteapp/backend/internal/application/services"
|
||||
)
|
||||
|
||||
// PublicHandler handles unauthenticated public read-only requests
|
||||
type PublicHandler struct {
|
||||
spaceService *services.SpaceService
|
||||
noteService *services.NoteService
|
||||
}
|
||||
|
||||
// NewPublicHandler creates a new PublicHandler
|
||||
func NewPublicHandler(spaceService *services.SpaceService, noteService *services.NoteService) *PublicHandler {
|
||||
return &PublicHandler{spaceService: spaceService, noteService: noteService}
|
||||
}
|
||||
|
||||
// GetPublicSpace handles GET /public/spaces/{spaceId}
|
||||
func (h *PublicHandler) GetPublicSpace(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
space, err := h.spaceService.GetPublicSpace(r.Context(), spaceID)
|
||||
if err != nil {
|
||||
if err.Error() == "space is not public" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(space)
|
||||
}
|
||||
|
||||
// ListPublicSpaces handles GET /public/spaces
|
||||
func (h *PublicHandler) ListPublicSpaces(w http.ResponseWriter, r *http.Request) {
|
||||
spaces, err := h.spaceService.GetPublicSpaces(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"spaces": spaces})
|
||||
}
|
||||
|
||||
// GetPublicNotes handles GET /public/spaces/{spaceId}/notes
|
||||
func (h *PublicHandler) GetPublicNotes(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
skip := 0
|
||||
limit := 50
|
||||
if v := r.URL.Query().Get("skip"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
|
||||
skip = n
|
||||
}
|
||||
}
|
||||
if v := r.URL.Query().Get("limit"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 100 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
|
||||
notes, err := h.noteService.GetPublicNotesBySpace(r.Context(), spaceID, skip, limit)
|
||||
if err != nil {
|
||||
if err.Error() == "space is not public" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"notes": notes})
|
||||
}
|
||||
|
||||
// GetPublicNote handles GET /public/spaces/{spaceId}/notes/{noteId}
|
||||
func (h *PublicHandler) GetPublicNote(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
spaceID, err := bson.ObjectIDFromHex(vars["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
noteID, err := bson.ObjectIDFromHex(vars["noteId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid note id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
note, err := h.noteService.GetPublicNoteBySpaceAndID(r.Context(), spaceID, noteID)
|
||||
if err != nil {
|
||||
if err.Error() == "space is not public" || err.Error() == "note is not public" || err.Error() == "space not found" || err.Error() == "note not found" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(note)
|
||||
}
|
||||
|
||||
// UnlockPublicNote verifies a public note password and returns full note content
|
||||
func (h *PublicHandler) UnlockPublicNote(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
spaceID, err := bson.ObjectIDFromHex(vars["spaceId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
noteID, err := bson.ObjectIDFromHex(vars["noteId"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid note id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UnlockNoteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
note, err := h.noteService.UnlockPublicNote(r.Context(), spaceID, noteID, req.Password)
|
||||
if err != nil {
|
||||
if err.Error() == "invalid note password" {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if err.Error() == "space is not public" || err.Error() == "space not found" || err.Error() == "note not found" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(note)
|
||||
}
|
||||
30
backend/internal/interfaces/handlers/settings_handler.go
Normal file
30
backend/internal/interfaces/handlers/settings_handler.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/noteapp/backend/internal/application/services"
|
||||
)
|
||||
|
||||
// SettingsHandler handles public app settings endpoints.
|
||||
type SettingsHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
// NewSettingsHandler creates a new settings handler.
|
||||
func NewSettingsHandler(authService *services.AuthService) *SettingsHandler {
|
||||
return &SettingsHandler{authService: authService}
|
||||
}
|
||||
|
||||
// GetFeatureFlags handles GET /api/v1/settings/feature-flags.
|
||||
func (h *SettingsHandler) GetFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
flags, err := h.authService.GetFeatureFlags(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(flags)
|
||||
}
|
||||
295
backend/internal/interfaces/handlers/space_handler.go
Normal file
295
backend/internal/interfaces/handlers/space_handler.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/noteapp/backend/internal/application/dto"
|
||||
"github.com/noteapp/backend/internal/application/services"
|
||||
"github.com/noteapp/backend/internal/interfaces/middleware"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// SpaceHandler handles space endpoints
|
||||
type SpaceHandler struct {
|
||||
spaceService *services.SpaceService
|
||||
}
|
||||
|
||||
// NewSpaceHandler creates a new space handler
|
||||
func NewSpaceHandler(spaceService *services.SpaceService) *SpaceHandler {
|
||||
return &SpaceHandler{
|
||||
spaceService: spaceService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSpace creates a new space
|
||||
func (h *SpaceHandler) CreateSpace(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CreateSpaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
space, err := h.spaceService.CreateSpace(r.Context(), userObjID, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(space)
|
||||
}
|
||||
|
||||
// GetUserSpaces retrieves all spaces for the user
|
||||
func (h *SpaceHandler) GetUserSpaces(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
spaces, err := h.spaceService.GetUserSpaces(r.Context(), userObjID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(spaces)
|
||||
}
|
||||
|
||||
// GetSpace retrieves a space
|
||||
func (h *SpaceHandler) GetSpace(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
space, err := h.spaceService.GetSpaceByID(r.Context(), spaceObjID, userObjID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(space)
|
||||
}
|
||||
|
||||
// UpdateSpace updates a space
|
||||
func (h *SpaceHandler) UpdateSpace(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CreateSpaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
space, err := h.spaceService.UpdateSpace(r.Context(), spaceObjID, userObjID, &req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(space)
|
||||
}
|
||||
|
||||
// DeleteSpace deletes a space
|
||||
func (h *SpaceHandler) DeleteSpace(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
if err := h.spaceService.DeleteSpace(r.Context(), spaceObjID, userObjID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetSpaceMembers retrieves all members in a space (owner only)
|
||||
func (h *SpaceHandler) GetSpaceMembers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
members, err := h.spaceService.GetSpaceMembers(r.Context(), spaceObjID, userObjID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"members": members})
|
||||
}
|
||||
|
||||
// AddMember adds a member to a space (owner/editor)
|
||||
func (h *SpaceHandler) AddMember(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.AddSpaceMemberRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
targetUserObjID, err := bson.ObjectIDFromHex(req.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.spaceService.AddMember(r.Context(), spaceObjID, userObjID, targetUserObjID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "member added"})
|
||||
}
|
||||
|
||||
// RemoveMember removes a member from a space (owner/editor)
|
||||
func (h *SpaceHandler) RemoveMember(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
targetUserID := mux.Vars(r)["userId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, err := bson.ObjectIDFromHex(spaceID)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid space id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
userObjID, err := bson.ObjectIDFromHex(userID)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
targetUserObjID, err := bson.ObjectIDFromHex(targetUserID)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid target user id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.spaceService.RemoveMember(r.Context(), spaceObjID, userObjID, targetUserObjID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetAvailableUsers returns user options for member selection (owner only)
|
||||
func (h *SpaceHandler) GetAvailableUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
spaceID := mux.Vars(r)["spaceId"]
|
||||
userID, err := middleware.GetUserIDFromContext(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
spaceObjID, _ := bson.ObjectIDFromHex(spaceID)
|
||||
userObjID, _ := bson.ObjectIDFromHex(userID)
|
||||
|
||||
users, err := h.spaceService.ListAvailableUsers(r.Context(), spaceObjID, userObjID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"users": users})
|
||||
}
|
||||
91
backend/internal/interfaces/middleware/auth.go
Normal file
91
backend/internal/interfaces/middleware/auth.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/noteapp/backend/internal/infrastructure/auth"
|
||||
)
|
||||
|
||||
// ContextKey is a custom type for context keys
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
UserIDKey ContextKey = "user_id"
|
||||
EmailKey ContextKey = "email"
|
||||
UserKey ContextKey = "user"
|
||||
)
|
||||
|
||||
// AuthMiddleware verifies JWT tokens
|
||||
type AuthMiddleware struct {
|
||||
jwtManager *auth.JWTManager
|
||||
}
|
||||
|
||||
// NewAuthMiddleware creates a new auth middleware
|
||||
func NewAuthMiddleware(jwtManager *auth.JWTManager) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
jwtManager: jwtManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware wraps an HTTP handler with authentication
|
||||
func (m *AuthMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip auth for login and register endpoints
|
||||
if strings.HasSuffix(r.URL.Path, "/auth/login") ||
|
||||
strings.HasSuffix(r.URL.Path, "/auth/register") ||
|
||||
strings.HasSuffix(r.URL.Path, "/health") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract token from Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
|
||||
// Verify token
|
||||
claims, err := m.jwtManager.VerifyAccessToken(token)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Add claims to context
|
||||
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, EmailKey, claims.Email)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserIDFromContext extracts user ID from context
|
||||
func GetUserIDFromContext(ctx context.Context) (string, error) {
|
||||
userID, ok := ctx.Value(UserIDKey).(string)
|
||||
if !ok {
|
||||
return "", errors.New("user ID not found in context")
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
// GetEmailFromContext extracts email from context
|
||||
func GetEmailFromContext(ctx context.Context) (string, error) {
|
||||
email, ok := ctx.Value(EmailKey).(string)
|
||||
if !ok {
|
||||
return "", errors.New("email not found in context")
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
91
backend/internal/interfaces/middleware/security.go
Normal file
91
backend/internal/interfaces/middleware/security.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SecurityHeaders adds security headers to responses
|
||||
func SecurityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// HSTS - HTTP Strict Transport Security
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
|
||||
// CSRF Protection - same-site cookies
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// XSS Protection
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
|
||||
// Clickjacking protection
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
|
||||
// CSP - Content Security Policy
|
||||
w.Header().Set("Content-Security-Policy",
|
||||
"default-src 'self'; "+
|
||||
"script-src 'self' 'unsafe-inline'; "+
|
||||
"style-src 'self' 'unsafe-inline'; "+
|
||||
"img-src 'self' data: https:; "+
|
||||
"font-src 'self'; "+
|
||||
"connect-src 'self'; "+
|
||||
"frame-ancestors 'none'")
|
||||
|
||||
// Referrer Policy
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RateLimitMiddleware implements basic rate limiting
|
||||
type RateLimitMiddleware struct {
|
||||
// In production, use a proper rate limiter like github.com/go-chi/chi/middleware
|
||||
// This is a placeholder for demonstration
|
||||
}
|
||||
|
||||
// NewRateLimitMiddleware creates a new rate limit middleware
|
||||
func NewRateLimitMiddleware() *RateLimitMiddleware {
|
||||
return &RateLimitMiddleware{}
|
||||
}
|
||||
|
||||
// Middleware returns the rate limit middleware handler
|
||||
func (m *RateLimitMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Implement proper rate limiting using distributed cache
|
||||
// For now, this is a placeholder
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// LoggingMiddleware logs HTTP requests
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("[%s] %s %s\n", r.Method, r.RequestURI, r.RemoteAddr)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// CORSMiddleware enables CORS
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
switch {
|
||||
case origin == "http://localhost", origin == "http://localhost:5173", strings.HasPrefix(origin, "http://127.0.0.1:"):
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Vary", "Origin")
|
||||
default:
|
||||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost")
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Max-Age", "600")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user