Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
122
cloud/maplefile-backend/internal/interface/http/README.md
Normal file
122
cloud/maplefile-backend/internal/interface/http/README.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# MapleFile HTTP Server
|
||||
|
||||
Standalone HTTP server for MapleFile backend - completely independent with no Manifold orchestration.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Standard Library**: Uses `net/http` with Go 1.22+ routing patterns
|
||||
- **No Orchestration**: Direct route registration (no `AsRoute()` wrappers)
|
||||
- **Middleware Stack**: Applied globally with per-route authentication
|
||||
- **Lifecycle Management**: Integrated with Uber FX for graceful shutdown
|
||||
|
||||
## Server Configuration
|
||||
|
||||
Configured via environment variables in `.env`:
|
||||
|
||||
```env
|
||||
SERVER_HOST=0.0.0.0
|
||||
SERVER_PORT=8000
|
||||
SERVER_READ_TIMEOUT=30s
|
||||
SERVER_WRITE_TIMEOUT=30s
|
||||
SERVER_IDLE_TIMEOUT=60s
|
||||
SERVER_SHUTDOWN_TIMEOUT=10s
|
||||
```
|
||||
|
||||
## Middleware Stack
|
||||
|
||||
Applied in this order (outermost to innermost):
|
||||
|
||||
1. **Recovery** - Catches panics and returns 500
|
||||
2. **Logging** - Logs all requests with duration
|
||||
3. **CORS** - Handles cross-origin requests
|
||||
4. **Authentication** (per-route) - JWT validation for protected routes
|
||||
|
||||
## Route Structure
|
||||
|
||||
### Public Routes
|
||||
- `GET /health` - Health check
|
||||
- `GET /version` - Version info
|
||||
- `POST /api/v1/auth/register` - Registration
|
||||
- `POST /api/v1/auth/login` - Login
|
||||
|
||||
### Protected Routes
|
||||
All `/api/v1/*` routes (except auth) require JWT authentication via:
|
||||
```
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
Key protected endpoints include:
|
||||
- `GET/PUT/DELETE /api/v1/me` - User profile management
|
||||
- `POST/GET/PUT/DELETE /api/v1/collections/*` - Collection CRUD
|
||||
- `POST/GET/PUT/DELETE /api/v1/file/*` - File operations
|
||||
- `POST /api/v1/invites/send-email` - Send invitation to non-registered user
|
||||
|
||||
See `routes.go` for complete endpoint list.
|
||||
|
||||
## Handler Registration
|
||||
|
||||
Routes are registered in `server.go` -> `registerRoutes()`:
|
||||
|
||||
```go
|
||||
// Public route
|
||||
s.mux.HandleFunc("GET /health", s.healthCheckHandler)
|
||||
|
||||
// Protected route
|
||||
s.mux.HandleFunc("POST /api/v1/collections",
|
||||
s.middleware.Attach(s.handlers.CreateCollection))
|
||||
```
|
||||
|
||||
## Starting the Server
|
||||
|
||||
The server is started automatically by Uber FX:
|
||||
|
||||
```go
|
||||
fx.New(
|
||||
fx.Provide(http.NewServer), // Creates and starts server
|
||||
// ... other providers
|
||||
)
|
||||
```
|
||||
|
||||
Lifecycle hooks handle:
|
||||
- **OnStart**: Starts HTTP listener in goroutine
|
||||
- **OnStop**: Graceful shutdown with timeout
|
||||
|
||||
## Response Format
|
||||
|
||||
All JSON responses follow this structure:
|
||||
|
||||
**Success:**
|
||||
```json
|
||||
{
|
||||
"data": { ... },
|
||||
"message": "Success"
|
||||
}
|
||||
```
|
||||
|
||||
**Error:**
|
||||
```json
|
||||
{
|
||||
"error": "Error message",
|
||||
"code": "ERROR_CODE"
|
||||
}
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
```bash
|
||||
# Basic health check
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Version check
|
||||
curl http://localhost:8000/version
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Build and run:
|
||||
```bash
|
||||
task build
|
||||
./maplefile-backend daemon
|
||||
```
|
||||
|
||||
The server will start on `http://localhost:8000` by default.
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/complete_login.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CompleteLoginHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.CompleteLoginService
|
||||
}
|
||||
|
||||
func NewCompleteLoginHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.CompleteLoginService,
|
||||
) *CompleteLoginHandler {
|
||||
return &CompleteLoginHandler{
|
||||
logger: logger.Named("CompleteLoginHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CompleteLoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req svc_auth.CompleteLoginRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode complete login request", zap.Error(err))
|
||||
problem := httperror.NewBadRequestError("Invalid request payload. Expected JSON with 'email', 'challengeId', and 'decryptedData' fields.").
|
||||
WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Complete login failed", zap.Error(err))
|
||||
// Service returns RFC 9457 errors, use RespondWithError to handle them
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/recovery_complete.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RecoveryCompleteHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.RecoveryCompleteService
|
||||
}
|
||||
|
||||
func NewRecoveryCompleteHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.RecoveryCompleteService,
|
||||
) *RecoveryCompleteHandler {
|
||||
return &RecoveryCompleteHandler{
|
||||
logger: logger.Named("RecoveryCompleteHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RecoveryCompleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req svc_auth.RecoveryCompleteRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode recovery complete request", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("payload", "Invalid request payload"))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Recovery complete failed", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/recovery_initiate.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RecoveryInitiateHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.RecoveryInitiateService
|
||||
}
|
||||
|
||||
func NewRecoveryInitiateHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.RecoveryInitiateService,
|
||||
) *RecoveryInitiateHandler {
|
||||
return &RecoveryInitiateHandler{
|
||||
logger: logger.Named("RecoveryInitiateHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RecoveryInitiateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req svc_auth.RecoveryInitiateRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode recovery initiate request", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("payload", "Invalid request payload"))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Recovery initiate failed", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/recovery_verify.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RecoveryVerifyHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.RecoveryVerifyService
|
||||
}
|
||||
|
||||
func NewRecoveryVerifyHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.RecoveryVerifyService,
|
||||
) *RecoveryVerifyHandler {
|
||||
return &RecoveryVerifyHandler{
|
||||
logger: logger.Named("RecoveryVerifyHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RecoveryVerifyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req svc_auth.RecoveryVerifyRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode recovery verify request", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("payload", "Invalid request payload"))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Recovery verify failed", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/refresh_token.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RefreshTokenHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.RefreshTokenService
|
||||
}
|
||||
|
||||
func NewRefreshTokenHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.RefreshTokenService,
|
||||
) *RefreshTokenHandler {
|
||||
return &RefreshTokenHandler{
|
||||
logger: logger.Named("RefreshTokenHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RefreshTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req svc_auth.RefreshTokenRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode refresh token request", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("payload", "Invalid request payload"))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Refresh token failed", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/register.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// RegisterHandler handles user registration
|
||||
type RegisterHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.RegisterService
|
||||
}
|
||||
|
||||
// NewRegisterHandler creates a new registration handler
|
||||
func NewRegisterHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.RegisterService,
|
||||
) *RegisterHandler {
|
||||
return &RegisterHandler{
|
||||
logger: logger.Named("RegisterHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP handles the HTTP request
|
||||
func (h *RegisterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract request ID from existing middleware
|
||||
requestID := httperror.ExtractRequestID(r)
|
||||
|
||||
// Decode request
|
||||
var req svc_auth.RegisterRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode register request", zap.Error(err))
|
||||
problem := httperror.NewBadRequestError("Invalid request payload: " + err.Error())
|
||||
problem.WithInstance(r.URL.Path).WithTraceID(requestID)
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Call service - service handles validation and returns RFC 9457 errors
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
// Check if error is already a ProblemDetail
|
||||
if problem, ok := err.(*httperror.ProblemDetail); ok {
|
||||
h.logger.Warn("Registration failed with validation errors",
|
||||
zap.String("email", validation.MaskEmail(req.Email)),
|
||||
zap.Int("error_count", len(problem.Errors)))
|
||||
problem.WithInstance(r.URL.Path).WithTraceID(requestID)
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Unexpected error - wrap in internal server error
|
||||
h.logger.Error("Registration failed with unexpected error",
|
||||
zap.String("email", validation.MaskEmail(req.Email)),
|
||||
zap.Error(err))
|
||||
problem := httperror.NewInternalServerError("Registration failed: " + err.Error())
|
||||
problem.WithInstance(r.URL.Path).WithTraceID(requestID)
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Return success response
|
||||
h.logger.Info("User registered successfully", zap.String("user_id", resp.UserID))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/request_ott.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RequestOTTHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.RequestOTTService
|
||||
}
|
||||
|
||||
func NewRequestOTTHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.RequestOTTService,
|
||||
) *RequestOTTHandler {
|
||||
return &RequestOTTHandler{
|
||||
logger: logger.Named("RequestOTTHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RequestOTTHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req svc_auth.RequestOTTRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode request OTT request", zap.Error(err))
|
||||
problem := httperror.NewBadRequestError("Invalid request payload. Expected JSON with 'email' field.").
|
||||
WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Request OTT failed", zap.Error(err))
|
||||
// Service returns RFC 9457 errors, use RespondWithError to handle them
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/resend_verification.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// ResendVerificationHandler handles resending verification emails
|
||||
type ResendVerificationHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.ResendVerificationService
|
||||
}
|
||||
|
||||
// NewResendVerificationHandler creates a new resend verification handler
|
||||
func NewResendVerificationHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.ResendVerificationService,
|
||||
) *ResendVerificationHandler {
|
||||
return &ResendVerificationHandler{
|
||||
logger: logger.Named("ResendVerificationHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP handles the HTTP request
|
||||
func (h *ResendVerificationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Decode request
|
||||
var req svc_auth.ResendVerificationRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode resend verification request", zap.Error(err))
|
||||
problem := httperror.NewBadRequestError("Invalid request payload. Expected JSON with 'email' field.").
|
||||
WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Call service (service now handles validation and returns RFC 9457 errors)
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Resend verification failed", zap.Error(err))
|
||||
// Service returns RFC 9457 errors, use RespondWithError to handle them
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return success response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/verify_email.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// VerifyEmailHandler handles email verification
|
||||
type VerifyEmailHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.VerifyEmailService
|
||||
}
|
||||
|
||||
// NewVerifyEmailHandler creates a new verify email handler
|
||||
func NewVerifyEmailHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.VerifyEmailService,
|
||||
) *VerifyEmailHandler {
|
||||
return &VerifyEmailHandler{
|
||||
logger: logger.Named("VerifyEmailHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP handles the HTTP request
|
||||
func (h *VerifyEmailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Decode request
|
||||
var req svc_auth.VerifyEmailRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode verify email request", zap.Error(err))
|
||||
problem := httperror.NewBadRequestError("Invalid request payload. Expected JSON with 'code' field.").
|
||||
WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Email verification failed", zap.Error(err))
|
||||
// Service returns RFC 9457 errors, use RespondWithError to handle them
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return success response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth/verify_ott.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type VerifyOTTHandler struct {
|
||||
logger *zap.Logger
|
||||
service svc_auth.VerifyOTTService
|
||||
}
|
||||
|
||||
func NewVerifyOTTHandler(
|
||||
logger *zap.Logger,
|
||||
service svc_auth.VerifyOTTService,
|
||||
) *VerifyOTTHandler {
|
||||
return &VerifyOTTHandler{
|
||||
logger: logger.Named("VerifyOTTHandler"),
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *VerifyOTTHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req svc_auth.VerifyOTTRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to decode verify OTT request", zap.Error(err))
|
||||
problem := httperror.NewBadRequestError("Invalid request payload. Expected JSON with 'email' and 'ott' fields.").
|
||||
WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Verify OTT failed", zap.Error(err))
|
||||
// Service returns RFC 9457 errors, use RespondWithError to handle them
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/blockedemail/create.go
|
||||
package blockedemail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/blockedemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CreateBlockedEmailHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_blockedemail.CreateBlockedEmailService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewCreateBlockedEmailHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_blockedemail.CreateBlockedEmailService,
|
||||
middleware middleware.Middleware,
|
||||
) *CreateBlockedEmailHTTPHandler {
|
||||
logger = logger.Named("CreateBlockedEmailHTTPHandler")
|
||||
return &CreateBlockedEmailHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*CreateBlockedEmailHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/me/blocked-emails"
|
||||
}
|
||||
|
||||
func (h *CreateBlockedEmailHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *CreateBlockedEmailHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*svc_blockedemail.CreateBlockedEmailRequestDTO, error) {
|
||||
var requestData svc_blockedemail.CreateBlockedEmailRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON)
|
||||
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err))
|
||||
// Log raw JSON at debug level only to avoid PII exposure in production logs
|
||||
h.logger.Debug("raw request body for debugging",
|
||||
zap.String("json", rawJSON.String()))
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *CreateBlockedEmailHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/blockedemail/delete.go
|
||||
package blockedemail
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/blockedemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
type DeleteBlockedEmailHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_blockedemail.DeleteBlockedEmailService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewDeleteBlockedEmailHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_blockedemail.DeleteBlockedEmailService,
|
||||
middleware middleware.Middleware,
|
||||
) *DeleteBlockedEmailHTTPHandler {
|
||||
logger = logger.Named("DeleteBlockedEmailHTTPHandler")
|
||||
return &DeleteBlockedEmailHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*DeleteBlockedEmailHTTPHandler) Pattern() string {
|
||||
return "DELETE /api/v1/me/blocked-emails/{email}"
|
||||
}
|
||||
|
||||
func (h *DeleteBlockedEmailHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *DeleteBlockedEmailHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract email from URL path
|
||||
emailEncoded := r.PathValue("email")
|
||||
if emailEncoded == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewBadRequestError("Email is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// URL decode the email using PathUnescape (not QueryUnescape)
|
||||
// PathUnescape correctly handles %2B as + instead of treating + as space
|
||||
email, err := url.PathUnescape(emailEncoded)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to decode email",
|
||||
zap.String("encoded_email", validation.MaskEmail(emailEncoded)),
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, httperror.NewBadRequestError("Invalid email format"))
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("decoded email from path",
|
||||
zap.String("encoded", validation.MaskEmail(emailEncoded)),
|
||||
zap.String("decoded", validation.MaskEmail(email)))
|
||||
|
||||
resp, err := h.service.Execute(ctx, email)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/blockedemail/list.go
|
||||
package blockedemail
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/blockedemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListBlockedEmailsHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_blockedemail.ListBlockedEmailsService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewListBlockedEmailsHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_blockedemail.ListBlockedEmailsService,
|
||||
middleware middleware.Middleware,
|
||||
) *ListBlockedEmailsHTTPHandler {
|
||||
logger = logger.Named("ListBlockedEmailsHTTPHandler")
|
||||
return &ListBlockedEmailsHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ListBlockedEmailsHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/me/blocked-emails"
|
||||
}
|
||||
|
||||
func (h *ListBlockedEmailsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ListBlockedEmailsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
resp, err := h.service.Execute(ctx)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/blockedemail/provider.go
|
||||
package blockedemail
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/blockedemail"
|
||||
)
|
||||
|
||||
func ProvideCreateBlockedEmailHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_blockedemail.CreateBlockedEmailService,
|
||||
middleware middleware.Middleware,
|
||||
) *CreateBlockedEmailHTTPHandler {
|
||||
return NewCreateBlockedEmailHTTPHandler(cfg, logger, service, middleware)
|
||||
}
|
||||
|
||||
func ProvideListBlockedEmailsHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_blockedemail.ListBlockedEmailsService,
|
||||
middleware middleware.Middleware,
|
||||
) *ListBlockedEmailsHTTPHandler {
|
||||
return NewListBlockedEmailsHTTPHandler(cfg, logger, service, middleware)
|
||||
}
|
||||
|
||||
func ProvideDeleteBlockedEmailHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_blockedemail.DeleteBlockedEmailService,
|
||||
middleware middleware.Middleware,
|
||||
) *DeleteBlockedEmailHTTPHandler {
|
||||
return NewDeleteBlockedEmailHTTPHandler(cfg, logger, service, middleware)
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/archive.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ArchiveCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.ArchiveCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewArchiveCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.ArchiveCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *ArchiveCollectionHTTPHandler {
|
||||
logger = logger.Named("ArchiveCollectionHTTPHandler")
|
||||
return &ArchiveCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ArchiveCollectionHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/collections/{id}/archive"
|
||||
}
|
||||
|
||||
func (h *ArchiveCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ArchiveCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from the URL
|
||||
collectionIDStr := r.PathValue("id")
|
||||
if collectionIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create request DTO
|
||||
dtoReq := &svc_collection.ArchiveCollectionRequestDTO{
|
||||
ID: collectionID,
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, dtoReq)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/create.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CreateCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.CreateCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewCreateCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.CreateCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *CreateCollectionHTTPHandler {
|
||||
logger = logger.Named("CreateCollectionHTTPHandler")
|
||||
return &CreateCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*CreateCollectionHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/collections"
|
||||
}
|
||||
|
||||
func (h *CreateCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *CreateCollectionHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*svc_collection.CreateCollectionRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_collection.CreateCollectionRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decode create collection request",
|
||||
zap.Error(err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewBadRequestError("Invalid request payload. Please check your collection data.")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *CreateCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to unmarshal create collection request", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create collection", zap.Error(err))
|
||||
// Service returns RFC 9457 errors, use RespondWithError to handle them
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
h.logger.Error("No collection returned from service")
|
||||
problem := httperror.NewInternalServerError("Failed to create collection. Please try again.").
|
||||
WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("Failed to encode collection response", zap.Error(err))
|
||||
// At this point headers are already sent, log the error but can't send RFC 9457 response
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/find_by_parent.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type FindCollectionsByParentHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.FindCollectionsByParentService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewFindCollectionsByParentHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.FindCollectionsByParentService,
|
||||
middleware middleware.Middleware,
|
||||
) *FindCollectionsByParentHTTPHandler {
|
||||
logger = logger.Named("FindCollectionsByParentHTTPHandler")
|
||||
return &FindCollectionsByParentHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*FindCollectionsByParentHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/collections/parent/{parent_id}"
|
||||
}
|
||||
|
||||
func (h *FindCollectionsByParentHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *FindCollectionsByParentHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract parent ID from URL parameters
|
||||
parentIDStr := r.PathValue("parent_id")
|
||||
if parentIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("parent_id", "Parent ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
parentID, err := gocql.ParseUUID(parentIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid parent ID format",
|
||||
zap.String("parent_id", parentIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("parent_id", "Invalid parent ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create request DTO
|
||||
req := &svc_collection.FindByParentRequestDTO{
|
||||
ParentID: parentID,
|
||||
}
|
||||
|
||||
// Call service
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/find_root_collections.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type FindRootCollectionsHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.FindRootCollectionsService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewFindRootCollectionsHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.FindRootCollectionsService,
|
||||
middleware middleware.Middleware,
|
||||
) *FindRootCollectionsHTTPHandler {
|
||||
logger = logger.Named("FindRootCollectionsHTTPHandler")
|
||||
return &FindRootCollectionsHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*FindRootCollectionsHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/collections/root"
|
||||
}
|
||||
|
||||
func (h *FindRootCollectionsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *FindRootCollectionsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
resp, err := h.service.Execute(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to find root collections", zap.Error(err))
|
||||
// Service returns RFC 9457 errors, use RespondWithError to handle them
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
h.logger.Error("No collections returned from service")
|
||||
problem := httperror.NewInternalServerError("Failed to retrieve collections. Please try again.").
|
||||
WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("Failed to encode collections response", zap.Error(err))
|
||||
// At this point headers are already sent, log the error but can't send RFC 9457 response
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/get.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.GetCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.GetCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetCollectionHTTPHandler {
|
||||
logger = logger.Named("GetCollectionHTTPHandler")
|
||||
return &GetCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetCollectionHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/collections/{id}"
|
||||
}
|
||||
|
||||
func (h *GetCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from URL parameters
|
||||
// Assuming Go 1.22+ where r.PathValue is available for patterns like "/items/{id}"
|
||||
collectionIDStr := r.PathValue("id")
|
||||
if collectionIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, collectionID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/get_filtered.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetFilteredCollectionsHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.GetFilteredCollectionsService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetFilteredCollectionsHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.GetFilteredCollectionsService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetFilteredCollectionsHTTPHandler {
|
||||
logger = logger.Named("GetFilteredCollectionsHTTPHandler")
|
||||
return &GetFilteredCollectionsHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetFilteredCollectionsHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/collections/filtered"
|
||||
}
|
||||
|
||||
func (h *GetFilteredCollectionsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetFilteredCollectionsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Parse query parameters for filter options
|
||||
req, err := h.parseFilterOptions(r)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// parseFilterOptions parses the query parameters to create the request DTO
|
||||
func (h *GetFilteredCollectionsHTTPHandler) parseFilterOptions(r *http.Request) (*svc_collection.GetFilteredCollectionsRequestDTO, error) {
|
||||
req := &svc_collection.GetFilteredCollectionsRequestDTO{
|
||||
IncludeOwned: true, // Default to including owned collections
|
||||
IncludeShared: false, // Default to not including shared collections
|
||||
}
|
||||
|
||||
// Parse include_owned parameter
|
||||
if includeOwnedStr := r.URL.Query().Get("include_owned"); includeOwnedStr != "" {
|
||||
includeOwned, err := strconv.ParseBool(includeOwnedStr)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid include_owned parameter",
|
||||
zap.String("value", includeOwnedStr),
|
||||
zap.Error(err))
|
||||
return nil, httperror.NewForBadRequestWithSingleField("include_owned", "Invalid boolean value for include_owned parameter")
|
||||
}
|
||||
req.IncludeOwned = includeOwned
|
||||
}
|
||||
|
||||
// Parse include_shared parameter
|
||||
if includeSharedStr := r.URL.Query().Get("include_shared"); includeSharedStr != "" {
|
||||
includeShared, err := strconv.ParseBool(includeSharedStr)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid include_shared parameter",
|
||||
zap.String("value", includeSharedStr),
|
||||
zap.Error(err))
|
||||
return nil, httperror.NewForBadRequestWithSingleField("include_shared", "Invalid boolean value for include_shared parameter")
|
||||
}
|
||||
req.IncludeShared = includeShared
|
||||
}
|
||||
|
||||
// Validate that at least one option is enabled
|
||||
if !req.IncludeOwned && !req.IncludeShared {
|
||||
return nil, httperror.NewForBadRequestWithSingleField("filter_options", "At least one filter option (include_owned or include_shared) must be enabled")
|
||||
}
|
||||
|
||||
h.logger.Debug("Parsed filter options",
|
||||
zap.Bool("include_owned", req.IncludeOwned),
|
||||
zap.Bool("include_shared", req.IncludeShared))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/list_by_user.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListUserCollectionsHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.ListUserCollectionsService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewListUserCollectionsHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.ListUserCollectionsService,
|
||||
middleware middleware.Middleware,
|
||||
) *ListUserCollectionsHTTPHandler {
|
||||
logger = logger.Named("ListUserCollectionsHTTPHandler")
|
||||
return &ListUserCollectionsHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ListUserCollectionsHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/collections"
|
||||
}
|
||||
|
||||
func (h *ListUserCollectionsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ListUserCollectionsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
resp, err := h.service.Execute(ctx)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/list_shared_with_user.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListSharedCollectionsHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.ListSharedCollectionsService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewListSharedCollectionsHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.ListSharedCollectionsService,
|
||||
middleware middleware.Middleware,
|
||||
) *ListSharedCollectionsHTTPHandler {
|
||||
logger = logger.Named("ListSharedCollectionsHTTPHandler")
|
||||
return &ListSharedCollectionsHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ListSharedCollectionsHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/collections/shared"
|
||||
}
|
||||
|
||||
func (h *ListSharedCollectionsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ListSharedCollectionsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
// Call service
|
||||
resp, err := h.service.Execute(ctx)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/move_collection.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type MoveCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.MoveCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewMoveCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.MoveCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *MoveCollectionHTTPHandler {
|
||||
logger = logger.Named("MoveCollectionHTTPHandler")
|
||||
return &MoveCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*MoveCollectionHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/collections/{id}/move"
|
||||
}
|
||||
|
||||
func (h *MoveCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *MoveCollectionHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
collectionID gocql.UUID,
|
||||
) (*svc_collection.MoveCollectionRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_collection.MoveCollectionRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
// Set the collection ID from the URL parameter
|
||||
requestData.CollectionID = collectionID
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *MoveCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from URL parameters
|
||||
collectionIDStr := r.PathValue("id")
|
||||
if collectionIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r, collectionID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package collection
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
)
|
||||
|
||||
// Wire providers for collection HTTP handlers
|
||||
|
||||
func ProvideCreateCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.CreateCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *CreateCollectionHTTPHandler {
|
||||
return NewCreateCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideGetCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.GetCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *GetCollectionHTTPHandler {
|
||||
return NewGetCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideListUserCollectionsHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.ListUserCollectionsService,
|
||||
mw middleware.Middleware,
|
||||
) *ListUserCollectionsHTTPHandler {
|
||||
return NewListUserCollectionsHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideUpdateCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.UpdateCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *UpdateCollectionHTTPHandler {
|
||||
return NewUpdateCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideSoftDeleteCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.SoftDeleteCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *SoftDeleteCollectionHTTPHandler {
|
||||
return NewSoftDeleteCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideArchiveCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.ArchiveCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *ArchiveCollectionHTTPHandler {
|
||||
return NewArchiveCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideRestoreCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.RestoreCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *RestoreCollectionHTTPHandler {
|
||||
return NewRestoreCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideListSharedCollectionsHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.ListSharedCollectionsService,
|
||||
mw middleware.Middleware,
|
||||
) *ListSharedCollectionsHTTPHandler {
|
||||
return NewListSharedCollectionsHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideFindRootCollectionsHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.FindRootCollectionsService,
|
||||
mw middleware.Middleware,
|
||||
) *FindRootCollectionsHTTPHandler {
|
||||
return NewFindRootCollectionsHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideFindCollectionsByParentHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.FindCollectionsByParentService,
|
||||
mw middleware.Middleware,
|
||||
) *FindCollectionsByParentHTTPHandler {
|
||||
return NewFindCollectionsByParentHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideCollectionSyncHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.GetCollectionSyncDataService,
|
||||
mw middleware.Middleware,
|
||||
) *CollectionSyncHTTPHandler {
|
||||
return NewCollectionSyncHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideMoveCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.MoveCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *MoveCollectionHTTPHandler {
|
||||
return NewMoveCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideGetFilteredCollectionsHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.GetFilteredCollectionsService,
|
||||
mw middleware.Middleware,
|
||||
) *GetFilteredCollectionsHTTPHandler {
|
||||
return NewGetFilteredCollectionsHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideShareCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.ShareCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *ShareCollectionHTTPHandler {
|
||||
return NewShareCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideRemoveMemberHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.RemoveMemberService,
|
||||
mw middleware.Middleware,
|
||||
) *RemoveMemberHTTPHandler {
|
||||
return NewRemoveMemberHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/remove_member.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RemoveMemberHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.RemoveMemberService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewRemoveMemberHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.RemoveMemberService,
|
||||
middleware middleware.Middleware,
|
||||
) *RemoveMemberHTTPHandler {
|
||||
logger = logger.Named("RemoveMemberHTTPHandler")
|
||||
return &RemoveMemberHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*RemoveMemberHTTPHandler) Pattern() string {
|
||||
return "DELETE /api/v1/collections/{id}/members/{user_id}"
|
||||
}
|
||||
|
||||
func (h *RemoveMemberHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *RemoveMemberHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
collectionID gocql.UUID,
|
||||
recipientID gocql.UUID,
|
||||
) (*svc_collection.RemoveMemberRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_collection.RemoveMemberRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
// Set the collection ID and recipient ID from the URL parameters
|
||||
requestData.CollectionID = collectionID
|
||||
requestData.RecipientID = recipientID
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *RemoveMemberHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from URL parameters
|
||||
collectionIDStr := r.PathValue("id")
|
||||
if collectionIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user ID from URL parameters
|
||||
userIDStr := r.PathValue("user_id")
|
||||
if userIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("user_id", "User ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert collection ID string to UUID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert user ID string to UUID
|
||||
userID, err := gocql.ParseUUID(userIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid user ID format",
|
||||
zap.String("user_id", userIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("user_id", "Invalid user ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r, collectionID, userID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/restore.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RestoreCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.RestoreCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewRestoreCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.RestoreCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *RestoreCollectionHTTPHandler {
|
||||
logger = logger.Named("RestoreCollectionHTTPHandler")
|
||||
return &RestoreCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*RestoreCollectionHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/collections/{id}/restore"
|
||||
}
|
||||
|
||||
func (h *RestoreCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *RestoreCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from the URL
|
||||
collectionIDStr := r.PathValue("id")
|
||||
if collectionIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create request DTO
|
||||
dtoReq := &svc_collection.RestoreCollectionRequestDTO{
|
||||
ID: collectionID,
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, dtoReq)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/share_collection.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
type ShareCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.ShareCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewShareCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.ShareCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *ShareCollectionHTTPHandler {
|
||||
logger = logger.Named("ShareCollectionHTTPHandler")
|
||||
return &ShareCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ShareCollectionHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/collections/{id}/share"
|
||||
}
|
||||
|
||||
func (h *ShareCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ShareCollectionHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
collectionID gocql.UUID,
|
||||
) (*svc_collection.ShareCollectionRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_collection.ShareCollectionRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("JSON decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("raw_json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
// Log the decoded request for debugging (PII masked for security)
|
||||
h.logger.Debug("decoded share collection request",
|
||||
zap.String("collection_id_from_url", collectionID.String()),
|
||||
zap.String("collection_id_from_body", requestData.CollectionID.String()),
|
||||
zap.String("recipient_id", requestData.RecipientID.String()),
|
||||
zap.String("recipient_email", validation.MaskEmail(requestData.RecipientEmail)),
|
||||
zap.String("permission_level", requestData.PermissionLevel),
|
||||
zap.Int("encrypted_key_length", len(requestData.EncryptedCollectionKey)),
|
||||
zap.Bool("share_with_descendants", requestData.ShareWithDescendants))
|
||||
|
||||
// CRITICAL: Check if encrypted collection key is present in the request
|
||||
if len(requestData.EncryptedCollectionKey) == 0 {
|
||||
h.logger.Error("FRONTEND BUG: encrypted_collection_key is missing from request",
|
||||
zap.String("collection_id", collectionID.String()),
|
||||
zap.String("recipient_id", requestData.RecipientID.String()),
|
||||
zap.String("recipient_email", validation.MaskEmail(requestData.RecipientEmail)))
|
||||
// Log raw JSON at debug level only to avoid PII exposure in production logs
|
||||
h.logger.Debug("raw request body for debugging",
|
||||
zap.String("collection_id", collectionID.String()),
|
||||
zap.String("raw_json", rawJSON.String()))
|
||||
} else {
|
||||
h.logger.Debug("encrypted_collection_key found in request",
|
||||
zap.String("collection_id", collectionID.String()),
|
||||
zap.String("recipient_id", requestData.RecipientID.String()),
|
||||
zap.Int("encrypted_key_length", len(requestData.EncryptedCollectionKey)))
|
||||
}
|
||||
|
||||
// Set the collection ID from the URL parameter
|
||||
requestData.CollectionID = collectionID
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *ShareCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from URL parameters
|
||||
collectionIDStr := r.PathValue("id")
|
||||
if collectionIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("processing share collection request",
|
||||
zap.String("collection_id", collectionID.String()),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("content_type", r.Header.Get("Content-Type")))
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r, collectionID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
h.logger.Error("share collection service failed",
|
||||
zap.String("collection_id", collectionID.String()),
|
||||
zap.String("recipient_id", req.RecipientID.String()),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/softdelete.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type SoftDeleteCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.SoftDeleteCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewSoftDeleteCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.SoftDeleteCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *SoftDeleteCollectionHTTPHandler {
|
||||
logger = logger.Named("SoftDeleteCollectionHTTPHandler")
|
||||
return &SoftDeleteCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*SoftDeleteCollectionHTTPHandler) Pattern() string {
|
||||
return "DELETE /api/v1/collections/{id}"
|
||||
}
|
||||
|
||||
func (h *SoftDeleteCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *SoftDeleteCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from the URL
|
||||
collectionIDStr := r.PathValue("id")
|
||||
if collectionIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create request DTO
|
||||
dtoReq := &svc_collection.SoftDeleteCollectionRequestDTO{
|
||||
ID: collectionID,
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, dtoReq)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/sync.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
dom_sync "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CollectionSyncHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.GetCollectionSyncDataService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewCollectionSyncHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.GetCollectionSyncDataService,
|
||||
middleware middleware.Middleware,
|
||||
) *CollectionSyncHTTPHandler {
|
||||
logger = logger.Named("CollectionSyncHTTPHandler")
|
||||
return &CollectionSyncHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*CollectionSyncHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/collections/sync"
|
||||
}
|
||||
|
||||
func (h *CollectionSyncHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *CollectionSyncHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Get user ID from context
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
h.logger.Error("Failed getting user ID from context")
|
||||
httperror.RespondWithError(w, r, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
queryParams := r.URL.Query()
|
||||
|
||||
// Parse limit parameter (default: 1000, max: 5000)
|
||||
limit := int64(1000)
|
||||
if limitStr := queryParams.Get("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.ParseInt(limitStr, 10, 64); err == nil {
|
||||
if parsedLimit > 0 && parsedLimit <= 5000 {
|
||||
limit = parsedLimit
|
||||
} else {
|
||||
h.logger.Warn("Invalid limit parameter, using default",
|
||||
zap.String("limit", limitStr),
|
||||
zap.Int64("default", limit))
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("Failed to parse limit parameter, using default",
|
||||
zap.String("limit", limitStr),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse cursor parameter
|
||||
var cursor *dom_sync.CollectionSyncCursor
|
||||
if cursorStr := queryParams.Get("cursor"); cursorStr != "" {
|
||||
var parsedCursor dom_sync.CollectionSyncCursor
|
||||
if err := json.Unmarshal([]byte(cursorStr), &parsedCursor); err != nil {
|
||||
h.logger.Error("Failed to parse cursor parameter",
|
||||
zap.String("cursor", cursorStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("cursor", "Invalid cursor format"))
|
||||
return
|
||||
}
|
||||
cursor = &parsedCursor
|
||||
}
|
||||
|
||||
h.logger.Debug("Processing collection sync request",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Int64("limit", limit),
|
||||
zap.Any("cursor", cursor))
|
||||
|
||||
// Call service to get sync data
|
||||
response, err := h.service.Execute(ctx, userID, cursor, limit, "all")
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get collection sync data",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode and return response
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("Failed to encode collection sync response",
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Successfully served collection sync data",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Int("collections_count", len(response.Collections)),
|
||||
zap.Bool("has_more", response.HasMore))
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/update.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type UpdateCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_collection.UpdateCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewUpdateCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_collection.UpdateCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *UpdateCollectionHTTPHandler {
|
||||
logger = logger.Named("UpdateCollectionHTTPHandler")
|
||||
return &UpdateCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*UpdateCollectionHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/collections/{id}"
|
||||
}
|
||||
|
||||
func (h *UpdateCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *UpdateCollectionHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
collectionID gocql.UUID,
|
||||
) (*svc_collection.UpdateCollectionRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_collection.UpdateCollectionRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
// Set the collection ID from the URL parameter
|
||||
requestData.ID = collectionID
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *UpdateCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from the URL path parameter
|
||||
// This assumes the router is net/http (Go 1.22+) and the pattern was registered like "PUT /path/{id}"
|
||||
collectionIDStr := r.PathValue("id")
|
||||
if collectionIDStr == "" {
|
||||
h.logger.Warn("collection_id not found in path parameters or is empty",
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
)
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r, collectionID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("transaction completed with no result") // Clarified error message
|
||||
h.logger.Error("transaction completed with no result", zap.Any("request_payload", req))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Wire providers for common HTTP handlers
|
||||
|
||||
func ProvideMapleFileVersionHTTPHandler(
|
||||
logger *zap.Logger,
|
||||
) *MapleFileVersionHTTPHandler {
|
||||
return NewMapleFileVersionHTTPHandler(logger)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// curl http://localhost:8000/maplefile/api/v1/version
|
||||
type MapleFileVersionHTTPHandler struct {
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewMapleFileVersionHTTPHandler(
|
||||
log *zap.Logger,
|
||||
) *MapleFileVersionHTTPHandler {
|
||||
log = log.Named("MapleFileVersionHTTPHandler")
|
||||
return &MapleFileVersionHTTPHandler{log}
|
||||
}
|
||||
|
||||
type MapleFileVersionResponseIDO struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func (h *MapleFileVersionHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
response := MapleFileVersionResponseIDO{Version: "v1.0.0"}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (*MapleFileVersionHTTPHandler) Pattern() string {
|
||||
return "/maplefile/api/v1/version"
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// cloud/maplefile-backend/internal/maplefile/interface/http/dashboard/get.go
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_dashboard "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/dashboard"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetDashboardHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_dashboard.GetDashboardService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetDashboardHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_dashboard.GetDashboardService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetDashboardHTTPHandler {
|
||||
logger = logger.With(zap.String("module", "maplefile"))
|
||||
logger = logger.Named("GetDashboardHTTPHandler")
|
||||
return &GetDashboardHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetDashboardHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/dashboard"
|
||||
}
|
||||
|
||||
func (h *GetDashboardHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetDashboardHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
//
|
||||
// STEP 1: Execute service
|
||||
//
|
||||
resp, err := h.service.Execute(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get dashboard data",
|
||||
zap.Error(err))
|
||||
// Service returns RFC 9457 errors, use RespondWithError to handle them
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Encode and return response
|
||||
//
|
||||
if resp == nil {
|
||||
h.logger.Error("No dashboard data returned from service")
|
||||
problem := httperror.NewInternalServerError("Failed to retrieve dashboard data. Please try again.").
|
||||
WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("Failed to encode dashboard response",
|
||||
zap.Error(err))
|
||||
// At this point headers are already sent, log the error but can't send RFC 9457 response
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Dashboard data successfully returned")
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package dashboard
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_dashboard "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/dashboard"
|
||||
)
|
||||
|
||||
// Wire provider for dashboard HTTP handlers
|
||||
|
||||
func ProvideGetDashboardHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_dashboard.GetDashboardService,
|
||||
mw middleware.Middleware,
|
||||
) *GetDashboardHTTPHandler {
|
||||
return NewGetDashboardHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/archive.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ArchiveFileHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.ArchiveFileService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewArchiveFileHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.ArchiveFileService,
|
||||
middleware middleware.Middleware,
|
||||
) *ArchiveFileHTTPHandler {
|
||||
logger = logger.Named("ArchiveFileHTTPHandler")
|
||||
return &ArchiveFileHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ArchiveFileHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/file/{id}/archive"
|
||||
}
|
||||
|
||||
func (h *ArchiveFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ArchiveFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract file ID from the URL
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
fileID, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create request DTO
|
||||
dtoReq := &svc_file.ArchiveFileRequestDTO{
|
||||
FileID: fileID,
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, dtoReq)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/complete_file_upload.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CompleteFileUploadHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.CompleteFileUploadService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewCompleteFileUploadHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.CompleteFileUploadService,
|
||||
middleware middleware.Middleware,
|
||||
) *CompleteFileUploadHTTPHandler {
|
||||
logger = logger.Named("CompleteFileUploadHTTPHandler")
|
||||
return &CompleteFileUploadHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*CompleteFileUploadHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/file/{id}/complete"
|
||||
}
|
||||
|
||||
func (h *CompleteFileUploadHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *CompleteFileUploadHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
fileID gocql.UUID,
|
||||
) (*svc_file.CompleteFileUploadRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_file.CompleteFileUploadRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
// Set the file ID from the URL parameter
|
||||
requestData.FileID = fileID
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *CompleteFileUploadHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract file ID from URL parameters
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
fileID, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r, fileID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/create_pending_file.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CreatePendingFileHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.CreatePendingFileService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewCreatePendingFileHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.CreatePendingFileService,
|
||||
middleware middleware.Middleware,
|
||||
) *CreatePendingFileHTTPHandler {
|
||||
logger = logger.Named("CreatePendingFileHTTPHandler")
|
||||
return &CreatePendingFileHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*CreatePendingFileHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/files/pending"
|
||||
}
|
||||
|
||||
func (h *CreatePendingFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *CreatePendingFileHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*svc_file.CreatePendingFileRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_file.CreatePendingFileRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err))
|
||||
// Log raw JSON at debug level only to avoid PII exposure in production logs
|
||||
h.logger.Debug("raw request body for debugging",
|
||||
zap.String("json", rawJSON.String()))
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *CreatePendingFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
91
cloud/maplefile-backend/internal/interface/http/file/get.go
Normal file
91
cloud/maplefile-backend/internal/interface/http/file/get.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/get.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetFileHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.GetFileService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetFileHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.GetFileService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetFileHTTPHandler {
|
||||
logger = logger.Named("GetFileHTTPHandler")
|
||||
return &GetFileHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetFileHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/file/{id}"
|
||||
}
|
||||
|
||||
func (h *GetFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract file ID from URL parameters
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
fileID, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, fileID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/get_presigned_download_url.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetPresignedDownloadURLHTTPRequestDTO struct {
|
||||
URLDurationStr string `json:"url_duration,omitempty"` // Optional, duration as string of nanoseconds, defaults to 1 hour
|
||||
}
|
||||
|
||||
type GetPresignedDownloadURLHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.GetPresignedDownloadURLService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetPresignedDownloadURLHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.GetPresignedDownloadURLService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetPresignedDownloadURLHTTPHandler {
|
||||
logger = logger.Named("GetPresignedDownloadURLHTTPHandler")
|
||||
return &GetPresignedDownloadURLHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetPresignedDownloadURLHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/file/{id}/download-url"
|
||||
}
|
||||
|
||||
func (h *GetPresignedDownloadURLHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetPresignedDownloadURLHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
fileID gocql.UUID,
|
||||
) (*svc_file.GetPresignedDownloadURLRequestDTO, error) {
|
||||
// For GET requests, read from query parameters instead of body
|
||||
urlDurationStr := r.URL.Query().Get("url_duration")
|
||||
|
||||
// Set default URL duration if not provided (1 hour in nanoseconds)
|
||||
var urlDuration time.Duration
|
||||
if urlDurationStr == "" {
|
||||
urlDuration = 1 * time.Hour
|
||||
} else {
|
||||
// Parse the string to int64 (nanoseconds)
|
||||
durationNanos, err := strconv.ParseInt(urlDurationStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "url_duration", "Invalid duration format")
|
||||
}
|
||||
urlDuration = time.Duration(durationNanos)
|
||||
}
|
||||
|
||||
// Convert to service DTO
|
||||
serviceRequest := &svc_file.GetPresignedDownloadURLRequestDTO{
|
||||
FileID: fileID,
|
||||
URLDuration: urlDuration,
|
||||
}
|
||||
|
||||
return serviceRequest, nil
|
||||
}
|
||||
|
||||
func (h *GetPresignedDownloadURLHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract file ID from URL parameters
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
fileID, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r, fileID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/get_presigned_upload_url.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetPresignedUploadURLHTTPRequestDTO struct {
|
||||
URLDurationStr string `json:"url_duration,omitempty"` // Optional, duration as string of nanoseconds, defaults to 1 hour
|
||||
}
|
||||
|
||||
type GetPresignedUploadURLHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.GetPresignedUploadURLService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetPresignedUploadURLHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.GetPresignedUploadURLService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetPresignedUploadURLHTTPHandler {
|
||||
logger = logger.Named("GetPresignedUploadURLHTTPHandler")
|
||||
return &GetPresignedUploadURLHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetPresignedUploadURLHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/file/{id}/upload-url"
|
||||
}
|
||||
|
||||
func (h *GetPresignedUploadURLHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetPresignedUploadURLHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
fileID gocql.UUID,
|
||||
) (*svc_file.GetPresignedUploadURLRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var httpRequestData GetPresignedUploadURLHTTPRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&httpRequestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
// Set default URL duration if not provided (1 hour in nanoseconds)
|
||||
var urlDuration time.Duration
|
||||
if httpRequestData.URLDurationStr == "" {
|
||||
urlDuration = 1 * time.Hour
|
||||
} else {
|
||||
// Parse the string to int64 (nanoseconds)
|
||||
durationNanos, err := strconv.ParseInt(httpRequestData.URLDurationStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "url_duration", "Invalid duration format")
|
||||
}
|
||||
urlDuration = time.Duration(durationNanos)
|
||||
}
|
||||
|
||||
// Convert to service DTO
|
||||
serviceRequest := &svc_file.GetPresignedUploadURLRequestDTO{
|
||||
FileID: fileID,
|
||||
URLDuration: urlDuration,
|
||||
}
|
||||
|
||||
return serviceRequest, nil
|
||||
}
|
||||
|
||||
func (h *GetPresignedUploadURLHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract file ID from URL parameters
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
fileID, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r, fileID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/list_by_collection.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListFilesByCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.ListFilesByCollectionService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewListFilesByCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.ListFilesByCollectionService,
|
||||
middleware middleware.Middleware,
|
||||
) *ListFilesByCollectionHTTPHandler {
|
||||
logger = logger.Named("ListFilesByCollectionHTTPHandler")
|
||||
return &ListFilesByCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ListFilesByCollectionHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/collection/{collection_id}/files"
|
||||
}
|
||||
|
||||
func (h *ListFilesByCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ListFilesByCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract collection ID from URL parameters
|
||||
collectionIDStr := r.PathValue("collection_id")
|
||||
if collectionIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
collectionID, err := gocql.ParseUUID(collectionIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid collection ID format",
|
||||
zap.String("collection_id", collectionIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create request DTO
|
||||
req := &svc_file.ListFilesByCollectionRequestDTO{
|
||||
CollectionID: collectionID,
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
// cloud/maplefile-backend/internal/maplefile/interface/http/file/list_recent_files.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
file_service "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListRecentFilesHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
listRecentFilesService file_service.ListRecentFilesService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewListRecentFilesHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
listRecentFilesService file_service.ListRecentFilesService,
|
||||
middleware middleware.Middleware,
|
||||
) *ListRecentFilesHTTPHandler {
|
||||
logger = logger.Named("ListRecentFilesHTTPHandler")
|
||||
return &ListRecentFilesHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
listRecentFilesService: listRecentFilesService,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ListRecentFilesHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/files/recent"
|
||||
}
|
||||
|
||||
func (h *ListRecentFilesHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ListRecentFilesHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Parse query parameters
|
||||
queryParams := r.URL.Query()
|
||||
|
||||
// Parse limit parameter (default: 30, max: 100)
|
||||
limit := int64(30)
|
||||
if limitStr := queryParams.Get("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.ParseInt(limitStr, 10, 64); err == nil {
|
||||
if parsedLimit > 0 && parsedLimit <= 100 {
|
||||
limit = parsedLimit
|
||||
} else {
|
||||
h.logger.Warn("Invalid limit parameter, using default",
|
||||
zap.String("limit", limitStr),
|
||||
zap.Int64("default", limit))
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("Failed to parse limit parameter, using default",
|
||||
zap.String("limit", limitStr),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse cursor parameter
|
||||
var cursor *string
|
||||
if cursorStr := queryParams.Get("cursor"); cursorStr != "" {
|
||||
cursor = &cursorStr
|
||||
}
|
||||
|
||||
h.logger.Debug("Processing recent files request",
|
||||
zap.Int64("limit", limit),
|
||||
zap.Any("cursor", cursor))
|
||||
|
||||
// Call service to get recent files
|
||||
response, err := h.listRecentFilesService.Execute(ctx, cursor, limit)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get recent files",
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode and return response
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("Failed to encode recent files response",
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Successfully served recent files",
|
||||
zap.Int("files_count", len(response.Files)),
|
||||
zap.Bool("has_more", response.HasMore),
|
||||
zap.Any("next_cursor", response.NextCursor))
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/list_sync.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
file_service "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type FileSyncHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
fileSyncService file_service.ListFileSyncDataService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewFileSyncHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileSyncService file_service.ListFileSyncDataService,
|
||||
middleware middleware.Middleware,
|
||||
) *FileSyncHTTPHandler {
|
||||
logger = logger.Named("FileSyncHTTPHandler")
|
||||
return &FileSyncHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
fileSyncService: fileSyncService,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*FileSyncHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/files/sync"
|
||||
}
|
||||
|
||||
func (h *FileSyncHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *FileSyncHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Get user ID from context
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
h.logger.Error("Failed getting user ID from context")
|
||||
httperror.RespondWithError(w, r, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
queryParams := r.URL.Query()
|
||||
|
||||
// Parse limit parameter (default: 5000, max: 10000)
|
||||
limit := int64(5000)
|
||||
if limitStr := queryParams.Get("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.ParseInt(limitStr, 10, 64); err == nil {
|
||||
if parsedLimit > 0 && parsedLimit <= 10000 {
|
||||
limit = parsedLimit
|
||||
} else {
|
||||
h.logger.Warn("Invalid limit parameter, using default",
|
||||
zap.String("limit", limitStr),
|
||||
zap.Int64("default", limit))
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("Failed to parse limit parameter, using default",
|
||||
zap.String("limit", limitStr),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse cursor parameter
|
||||
var cursor *dom_file.FileSyncCursor
|
||||
if cursorStr := queryParams.Get("cursor"); cursorStr != "" {
|
||||
var parsedCursor dom_file.FileSyncCursor
|
||||
if err := json.Unmarshal([]byte(cursorStr), &parsedCursor); err != nil {
|
||||
h.logger.Error("Failed to parse cursor parameter",
|
||||
zap.String("cursor", cursorStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("cursor", "Invalid cursor format"))
|
||||
return
|
||||
}
|
||||
cursor = &parsedCursor
|
||||
}
|
||||
|
||||
h.logger.Debug("Processing file sync request",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Int64("limit", limit),
|
||||
zap.Any("cursor", cursor))
|
||||
|
||||
// Call service to get sync data
|
||||
response, err := h.fileSyncService.Execute(ctx, cursor, limit)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get file sync data",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the response contains all fields including EncryptedFileSizeInBytes before encoding
|
||||
h.logger.Debug("File sync response validation",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Int("files_count", len(response.Files)))
|
||||
|
||||
for i, item := range response.Files {
|
||||
h.logger.Debug("File sync response item",
|
||||
zap.Int("index", i),
|
||||
zap.String("file_id", item.ID.String()),
|
||||
zap.String("collection_id", item.CollectionID.String()),
|
||||
zap.Uint64("version", item.Version),
|
||||
zap.Time("modified_at", item.ModifiedAt),
|
||||
zap.String("state", item.State),
|
||||
zap.Uint64("tombstone_version", item.TombstoneVersion),
|
||||
zap.Time("tombstone_expiry", item.TombstoneExpiry),
|
||||
zap.Int64("encrypted_file_size_in_bytes", item.EncryptedFileSizeInBytes))
|
||||
}
|
||||
|
||||
// Encode and return response
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("Failed to encode file sync response",
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Successfully served file sync data",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Int("files_count", len(response.Files)),
|
||||
zap.Bool("has_more", response.HasMore),
|
||||
zap.Any("next_cursor", response.NextCursor))
|
||||
}
|
||||
136
cloud/maplefile-backend/internal/interface/http/file/provider.go
Normal file
136
cloud/maplefile-backend/internal/interface/http/file/provider.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
)
|
||||
|
||||
// Wire providers for file HTTP handlers
|
||||
|
||||
func ProvideCreatePendingFileHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.CreatePendingFileService,
|
||||
mw middleware.Middleware,
|
||||
) *CreatePendingFileHTTPHandler {
|
||||
return NewCreatePendingFileHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideGetPresignedUploadURLHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.GetPresignedUploadURLService,
|
||||
mw middleware.Middleware,
|
||||
) *GetPresignedUploadURLHTTPHandler {
|
||||
return NewGetPresignedUploadURLHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideCompleteFileUploadHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.CompleteFileUploadService,
|
||||
mw middleware.Middleware,
|
||||
) *CompleteFileUploadHTTPHandler {
|
||||
return NewCompleteFileUploadHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideGetFileHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.GetFileService,
|
||||
mw middleware.Middleware,
|
||||
) *GetFileHTTPHandler {
|
||||
return NewGetFileHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideGetPresignedDownloadURLHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.GetPresignedDownloadURLService,
|
||||
mw middleware.Middleware,
|
||||
) *GetPresignedDownloadURLHTTPHandler {
|
||||
return NewGetPresignedDownloadURLHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideListFilesByCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.ListFilesByCollectionService,
|
||||
mw middleware.Middleware,
|
||||
) *ListFilesByCollectionHTTPHandler {
|
||||
return NewListFilesByCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideListRecentFilesHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.ListRecentFilesService,
|
||||
mw middleware.Middleware,
|
||||
) *ListRecentFilesHTTPHandler {
|
||||
return NewListRecentFilesHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideUpdateFileHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.UpdateFileService,
|
||||
mw middleware.Middleware,
|
||||
) *UpdateFileHTTPHandler {
|
||||
return NewUpdateFileHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideSoftDeleteFileHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.SoftDeleteFileService,
|
||||
mw middleware.Middleware,
|
||||
) *SoftDeleteFileHTTPHandler {
|
||||
return NewSoftDeleteFileHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideArchiveFileHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.ArchiveFileService,
|
||||
mw middleware.Middleware,
|
||||
) *ArchiveFileHTTPHandler {
|
||||
return NewArchiveFileHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideRestoreFileHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.RestoreFileService,
|
||||
mw middleware.Middleware,
|
||||
) *RestoreFileHTTPHandler {
|
||||
return NewRestoreFileHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideDeleteMultipleFilesHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.DeleteMultipleFilesService,
|
||||
mw middleware.Middleware,
|
||||
) *DeleteMultipleFilesHTTPHandler {
|
||||
return NewDeleteMultipleFilesHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideFileSyncHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.ListFileSyncDataService,
|
||||
mw middleware.Middleware,
|
||||
) *FileSyncHTTPHandler {
|
||||
return NewFileSyncHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideReportDownloadCompletedHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
mw middleware.Middleware,
|
||||
) *ReportDownloadCompletedHTTPHandler {
|
||||
return NewReportDownloadCompletedHTTPHandler(cfg, logger, mw)
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/report_download_completed.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ReportDownloadCompletedHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewReportDownloadCompletedHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
middleware middleware.Middleware,
|
||||
) *ReportDownloadCompletedHTTPHandler {
|
||||
logger = logger.Named("ReportDownloadCompletedHTTPHandler")
|
||||
return &ReportDownloadCompletedHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ReportDownloadCompletedHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/file/{id}/download-completed"
|
||||
}
|
||||
|
||||
func (h *ReportDownloadCompletedHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ReportDownloadCompletedHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Extract file ID from the URL
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
_, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Log the download completion (analytics/telemetry)
|
||||
h.logger.Debug("download completed reported",
|
||||
zap.String("file_id", fileIDStr))
|
||||
|
||||
// Return success response
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Download completion recorded",
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/restore.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RestoreFileHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.RestoreFileService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewRestoreFileHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.RestoreFileService,
|
||||
middleware middleware.Middleware,
|
||||
) *RestoreFileHTTPHandler {
|
||||
logger = logger.Named("RestoreFileHTTPHandler")
|
||||
return &RestoreFileHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*RestoreFileHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/file/{id}/restore"
|
||||
}
|
||||
|
||||
func (h *RestoreFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *RestoreFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract file ID from the URL
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
fileID, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create request DTO
|
||||
dtoReq := &svc_file.RestoreFileRequestDTO{
|
||||
FileID: fileID,
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, dtoReq)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/softdelete.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type SoftDeleteFileHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.SoftDeleteFileService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewSoftDeleteFileHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.SoftDeleteFileService,
|
||||
middleware middleware.Middleware,
|
||||
) *SoftDeleteFileHTTPHandler {
|
||||
logger = logger.Named("SoftDeleteFileHTTPHandler")
|
||||
return &SoftDeleteFileHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*SoftDeleteFileHTTPHandler) Pattern() string {
|
||||
return "DELETE /api/v1/file/{id}"
|
||||
}
|
||||
|
||||
func (h *SoftDeleteFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *SoftDeleteFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract file ID from the URL
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
fileID, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create request DTO
|
||||
dtoReq := &svc_file.SoftDeleteFileRequestDTO{
|
||||
FileID: fileID,
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, dtoReq)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/delete_multiple.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type DeleteMultipleFilesHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.DeleteMultipleFilesService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewDeleteMultipleFilesHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.DeleteMultipleFilesService,
|
||||
middleware middleware.Middleware,
|
||||
) *DeleteMultipleFilesHTTPHandler {
|
||||
logger = logger.Named("DeleteMultipleFilesHTTPHandler")
|
||||
return &DeleteMultipleFilesHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*DeleteMultipleFilesHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/files/delete-multiple"
|
||||
}
|
||||
|
||||
func (h *DeleteMultipleFilesHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *DeleteMultipleFilesHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*svc_file.DeleteMultipleFilesRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_file.DeleteMultipleFilesRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *DeleteMultipleFilesHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
135
cloud/maplefile-backend/internal/interface/http/file/update.go
Normal file
135
cloud/maplefile-backend/internal/interface/http/file/update.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/interface/http/file/update.go
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type UpdateFileHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_file.UpdateFileService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewUpdateFileHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_file.UpdateFileService,
|
||||
middleware middleware.Middleware,
|
||||
) *UpdateFileHTTPHandler {
|
||||
logger = logger.Named("UpdateFileHTTPHandler")
|
||||
return &UpdateFileHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*UpdateFileHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/file/{id}"
|
||||
}
|
||||
|
||||
func (h *UpdateFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *UpdateFileHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
fileID gocql.UUID,
|
||||
) (*svc_file.UpdateFileRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_file.UpdateFileRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err))
|
||||
// Log raw JSON at debug level only to avoid PII exposure in production logs
|
||||
h.logger.Debug("raw request body for debugging",
|
||||
zap.String("json", rawJSON.String()))
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
// Set the file ID from the URL parameter
|
||||
requestData.ID = fileID
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *UpdateFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract file ID from the URL path parameter
|
||||
fileIDStr := r.PathValue("id")
|
||||
if fileIDStr == "" {
|
||||
h.logger.Warn("file_id not found in path parameters or is empty",
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
)
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert string ID to ObjectID
|
||||
fileID, err := gocql.ParseUUID(fileIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid file ID format",
|
||||
zap.String("file_id", fileIDStr),
|
||||
zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("file_id", "Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r, fileID)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("transaction completed with no result")
|
||||
h.logger.Error("transaction completed with no result", zap.Any("request_payload", req))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
258
cloud/maplefile-backend/internal/interface/http/handlers.go
Normal file
258
cloud/maplefile-backend/internal/interface/http/handlers.go
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/blockedemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/collection"
|
||||
commonhttp "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/common"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/dashboard"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/inviteemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/me"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/user"
|
||||
)
|
||||
|
||||
// Handlers aggregates all HTTP handlers
|
||||
type Handlers struct {
|
||||
// Common handlers
|
||||
Version *commonhttp.MapleFileVersionHTTPHandler
|
||||
|
||||
// Dashboard handlers
|
||||
GetDashboard *dashboard.GetDashboardHTTPHandler
|
||||
|
||||
// Me handlers
|
||||
GetMe *me.GetMeHTTPHandler
|
||||
UpdateMe *me.PutUpdateMeHTTPHandler
|
||||
DeleteMe *me.DeleteMeHTTPHandler
|
||||
|
||||
// User handlers
|
||||
UserPublicLookup *user.UserPublicLookupHTTPHandler
|
||||
|
||||
// Blocked Email handlers
|
||||
CreateBlockedEmail *blockedemail.CreateBlockedEmailHTTPHandler
|
||||
ListBlockedEmails *blockedemail.ListBlockedEmailsHTTPHandler
|
||||
DeleteBlockedEmail *blockedemail.DeleteBlockedEmailHTTPHandler
|
||||
|
||||
// Invite Email handlers
|
||||
SendInviteEmail *inviteemail.SendInviteEmailHTTPHandler
|
||||
|
||||
// Collection handlers - Basic CRUD
|
||||
CreateCollection *collection.CreateCollectionHTTPHandler
|
||||
GetCollection *collection.GetCollectionHTTPHandler
|
||||
ListUserCollections *collection.ListUserCollectionsHTTPHandler
|
||||
UpdateCollection *collection.UpdateCollectionHTTPHandler
|
||||
SoftDeleteCollection *collection.SoftDeleteCollectionHTTPHandler
|
||||
ArchiveCollection *collection.ArchiveCollectionHTTPHandler
|
||||
RestoreCollection *collection.RestoreCollectionHTTPHandler
|
||||
|
||||
// Collection handlers - Hierarchical operations
|
||||
FindCollectionsByParent *collection.FindCollectionsByParentHTTPHandler
|
||||
FindRootCollections *collection.FindRootCollectionsHTTPHandler
|
||||
MoveCollection *collection.MoveCollectionHTTPHandler
|
||||
|
||||
// Collection handlers - Sharing
|
||||
ShareCollection *collection.ShareCollectionHTTPHandler
|
||||
RemoveMember *collection.RemoveMemberHTTPHandler
|
||||
ListSharedCollections *collection.ListSharedCollectionsHTTPHandler
|
||||
|
||||
// Collection handlers - Filtered operations
|
||||
GetFilteredCollections *collection.GetFilteredCollectionsHTTPHandler
|
||||
|
||||
// Collection Sync
|
||||
CollectionSync *collection.CollectionSyncHTTPHandler
|
||||
|
||||
// File handlers - Basic CRUD
|
||||
SoftDeleteFile *file.SoftDeleteFileHTTPHandler
|
||||
DeleteMultipleFiles *file.DeleteMultipleFilesHTTPHandler
|
||||
GetFile *file.GetFileHTTPHandler
|
||||
ListFilesByCollection *file.ListFilesByCollectionHTTPHandler
|
||||
UpdateFile *file.UpdateFileHTTPHandler
|
||||
CreatePendingFile *file.CreatePendingFileHTTPHandler
|
||||
CompleteFileUpload *file.CompleteFileUploadHTTPHandler
|
||||
GetPresignedUploadURL *file.GetPresignedUploadURLHTTPHandler
|
||||
GetPresignedDownloadURL *file.GetPresignedDownloadURLHTTPHandler
|
||||
ReportDownloadCompleted *file.ReportDownloadCompletedHTTPHandler
|
||||
ArchiveFile *file.ArchiveFileHTTPHandler
|
||||
RestoreFile *file.RestoreFileHTTPHandler
|
||||
ListRecentFiles *file.ListRecentFilesHTTPHandler
|
||||
|
||||
// File Sync
|
||||
FileSync *file.FileSyncHTTPHandler
|
||||
|
||||
// Tag handlers
|
||||
CreateTag *tag.CreateTagHTTPHandler
|
||||
ListTags *tag.ListTagsHTTPHandler
|
||||
GetTag *tag.GetTagHTTPHandler
|
||||
UpdateTag *tag.UpdateTagHTTPHandler
|
||||
DeleteTag *tag.DeleteTagHTTPHandler
|
||||
AssignTag *tag.AssignTagHTTPHandler
|
||||
UnassignTag *tag.UnassignTagHTTPHandler
|
||||
GetTagsForCollection *tag.GetTagsForCollectionHTTPHandler
|
||||
GetTagsForFile *tag.GetTagsForFileHTTPHandler
|
||||
ListCollectionsByTag *tag.ListCollectionsByTagHandler
|
||||
ListFilesByTag *tag.ListFilesByTagHandler
|
||||
SearchByTags *tag.SearchByTagsHandler
|
||||
}
|
||||
|
||||
// NewHandlers creates and wires all HTTP handlers
|
||||
func NewHandlers(
|
||||
// Common
|
||||
versionHandler *commonhttp.MapleFileVersionHTTPHandler,
|
||||
|
||||
// Dashboard
|
||||
getDashboard *dashboard.GetDashboardHTTPHandler,
|
||||
|
||||
// Me
|
||||
getMe *me.GetMeHTTPHandler,
|
||||
updateMe *me.PutUpdateMeHTTPHandler,
|
||||
deleteMe *me.DeleteMeHTTPHandler,
|
||||
|
||||
// User
|
||||
userPublicLookup *user.UserPublicLookupHTTPHandler,
|
||||
|
||||
// Blocked Email
|
||||
createBlockedEmail *blockedemail.CreateBlockedEmailHTTPHandler,
|
||||
listBlockedEmails *blockedemail.ListBlockedEmailsHTTPHandler,
|
||||
deleteBlockedEmail *blockedemail.DeleteBlockedEmailHTTPHandler,
|
||||
|
||||
// Invite Email
|
||||
sendInviteEmail *inviteemail.SendInviteEmailHTTPHandler,
|
||||
|
||||
// Collection - Basic CRUD
|
||||
createCollection *collection.CreateCollectionHTTPHandler,
|
||||
getCollection *collection.GetCollectionHTTPHandler,
|
||||
listUserCollections *collection.ListUserCollectionsHTTPHandler,
|
||||
updateCollection *collection.UpdateCollectionHTTPHandler,
|
||||
softDeleteCollection *collection.SoftDeleteCollectionHTTPHandler,
|
||||
archiveCollection *collection.ArchiveCollectionHTTPHandler,
|
||||
restoreCollection *collection.RestoreCollectionHTTPHandler,
|
||||
|
||||
// Collection - Hierarchical
|
||||
findCollectionsByParent *collection.FindCollectionsByParentHTTPHandler,
|
||||
findRootCollections *collection.FindRootCollectionsHTTPHandler,
|
||||
moveCollection *collection.MoveCollectionHTTPHandler,
|
||||
|
||||
// Collection - Sharing
|
||||
shareCollection *collection.ShareCollectionHTTPHandler,
|
||||
removeMember *collection.RemoveMemberHTTPHandler,
|
||||
listSharedCollections *collection.ListSharedCollectionsHTTPHandler,
|
||||
|
||||
// Collection - Filtered
|
||||
getFilteredCollections *collection.GetFilteredCollectionsHTTPHandler,
|
||||
|
||||
// Collection - Sync
|
||||
collectionSync *collection.CollectionSyncHTTPHandler,
|
||||
|
||||
// File - CRUD
|
||||
softDeleteFile *file.SoftDeleteFileHTTPHandler,
|
||||
deleteMultipleFiles *file.DeleteMultipleFilesHTTPHandler,
|
||||
getFile *file.GetFileHTTPHandler,
|
||||
listFilesByCollection *file.ListFilesByCollectionHTTPHandler,
|
||||
updateFile *file.UpdateFileHTTPHandler,
|
||||
createPendingFile *file.CreatePendingFileHTTPHandler,
|
||||
completeFileUpload *file.CompleteFileUploadHTTPHandler,
|
||||
getPresignedUploadURL *file.GetPresignedUploadURLHTTPHandler,
|
||||
getPresignedDownloadURL *file.GetPresignedDownloadURLHTTPHandler,
|
||||
reportDownloadCompleted *file.ReportDownloadCompletedHTTPHandler,
|
||||
archiveFile *file.ArchiveFileHTTPHandler,
|
||||
restoreFile *file.RestoreFileHTTPHandler,
|
||||
listRecentFiles *file.ListRecentFilesHTTPHandler,
|
||||
|
||||
// File - Sync
|
||||
fileSync *file.FileSyncHTTPHandler,
|
||||
|
||||
// Tag handlers
|
||||
createTag *tag.CreateTagHTTPHandler,
|
||||
listTags *tag.ListTagsHTTPHandler,
|
||||
getTag *tag.GetTagHTTPHandler,
|
||||
updateTag *tag.UpdateTagHTTPHandler,
|
||||
deleteTag *tag.DeleteTagHTTPHandler,
|
||||
assignTag *tag.AssignTagHTTPHandler,
|
||||
unassignTag *tag.UnassignTagHTTPHandler,
|
||||
getTagsForCollection *tag.GetTagsForCollectionHTTPHandler,
|
||||
getTagsForFile *tag.GetTagsForFileHTTPHandler,
|
||||
listCollectionsByTag *tag.ListCollectionsByTagHandler,
|
||||
listFilesByTag *tag.ListFilesByTagHandler,
|
||||
searchByTags *tag.SearchByTagsHandler,
|
||||
) *Handlers {
|
||||
return &Handlers{
|
||||
// Common
|
||||
Version: versionHandler,
|
||||
|
||||
// Dashboard
|
||||
GetDashboard: getDashboard,
|
||||
|
||||
// Me
|
||||
GetMe: getMe,
|
||||
UpdateMe: updateMe,
|
||||
DeleteMe: deleteMe,
|
||||
|
||||
// User
|
||||
UserPublicLookup: userPublicLookup,
|
||||
|
||||
// Blocked Email
|
||||
CreateBlockedEmail: createBlockedEmail,
|
||||
ListBlockedEmails: listBlockedEmails,
|
||||
DeleteBlockedEmail: deleteBlockedEmail,
|
||||
|
||||
// Invite Email
|
||||
SendInviteEmail: sendInviteEmail,
|
||||
|
||||
// Collection - Basic CRUD
|
||||
CreateCollection: createCollection,
|
||||
GetCollection: getCollection,
|
||||
ListUserCollections: listUserCollections,
|
||||
UpdateCollection: updateCollection,
|
||||
SoftDeleteCollection: softDeleteCollection,
|
||||
ArchiveCollection: archiveCollection,
|
||||
RestoreCollection: restoreCollection,
|
||||
|
||||
// Collection - Hierarchical
|
||||
FindCollectionsByParent: findCollectionsByParent,
|
||||
FindRootCollections: findRootCollections,
|
||||
MoveCollection: moveCollection,
|
||||
|
||||
// Collection - Sharing
|
||||
ShareCollection: shareCollection,
|
||||
RemoveMember: removeMember,
|
||||
ListSharedCollections: listSharedCollections,
|
||||
|
||||
// Collection - Filtered
|
||||
GetFilteredCollections: getFilteredCollections,
|
||||
|
||||
// Collection Sync
|
||||
CollectionSync: collectionSync,
|
||||
|
||||
// File - CRUD
|
||||
SoftDeleteFile: softDeleteFile,
|
||||
DeleteMultipleFiles: deleteMultipleFiles,
|
||||
GetFile: getFile,
|
||||
ListFilesByCollection: listFilesByCollection,
|
||||
UpdateFile: updateFile,
|
||||
CreatePendingFile: createPendingFile,
|
||||
CompleteFileUpload: completeFileUpload,
|
||||
GetPresignedUploadURL: getPresignedUploadURL,
|
||||
GetPresignedDownloadURL: getPresignedDownloadURL,
|
||||
ReportDownloadCompleted: reportDownloadCompleted,
|
||||
ArchiveFile: archiveFile,
|
||||
RestoreFile: restoreFile,
|
||||
ListRecentFiles: listRecentFiles,
|
||||
|
||||
// File Sync
|
||||
FileSync: fileSync,
|
||||
|
||||
// Tag handlers
|
||||
CreateTag: createTag,
|
||||
ListTags: listTags,
|
||||
GetTag: getTag,
|
||||
UpdateTag: updateTag,
|
||||
DeleteTag: deleteTag,
|
||||
AssignTag: assignTag,
|
||||
UnassignTag: unassignTag,
|
||||
GetTagsForCollection: getTagsForCollection,
|
||||
GetTagsForFile: getTagsForFile,
|
||||
ListCollectionsByTag: listCollectionsByTag,
|
||||
ListFilesByTag: listFilesByTag,
|
||||
SearchByTags: searchByTags,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package inviteemail
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_inviteemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/inviteemail"
|
||||
)
|
||||
|
||||
// ProvideSendInviteEmailHTTPHandler provides the send invite email HTTP handler for Wire DI
|
||||
func ProvideSendInviteEmailHTTPHandler(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
service svc_inviteemail.SendInviteEmailService,
|
||||
mw middleware.Middleware,
|
||||
) *SendInviteEmailHTTPHandler {
|
||||
return NewSendInviteEmailHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// Package inviteemail provides HTTP handlers for invitation email endpoints
|
||||
package inviteemail
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_inviteemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/inviteemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// SendInviteEmailHTTPHandler handles POST /api/v1/invites/send-email requests
|
||||
type SendInviteEmailHTTPHandler struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
service svc_inviteemail.SendInviteEmailService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
// NewSendInviteEmailHTTPHandler creates a new handler for sending invitation emails
|
||||
func NewSendInviteEmailHTTPHandler(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
service svc_inviteemail.SendInviteEmailService,
|
||||
mw middleware.Middleware,
|
||||
) *SendInviteEmailHTTPHandler {
|
||||
logger = logger.Named("SendInviteEmailHTTPHandler")
|
||||
return &SendInviteEmailHTTPHandler{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: mw,
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern returns the URL pattern for this handler
|
||||
func (*SendInviteEmailHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/invites/send-email"
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (h *SendInviteEmailHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware (authentication required)
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
// Execute handles the actual request processing
|
||||
func (h *SendInviteEmailHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get user ID from context (set by auth middleware)
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
h.logger.Error("User ID not found in context or invalid type")
|
||||
httperror.RespondWithError(w, r, httperror.NewForUnauthorizedWithSingleField("auth", "Authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Decode request body
|
||||
var req svc_inviteemail.SendInviteEmailRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Warn("Failed to decode request body", zap.Error(err))
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("body", "Invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
// Execute service
|
||||
response, err := h.service.Execute(ctx, userID, &req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
96
cloud/maplefile-backend/internal/interface/http/me/delete.go
Normal file
96
cloud/maplefile-backend/internal/interface/http/me/delete.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/me/delete.go
|
||||
package me
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_me "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/me"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type DeleteMeHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_me.DeleteMeService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewDeleteMeHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_me.DeleteMeService,
|
||||
middleware middleware.Middleware,
|
||||
) *DeleteMeHTTPHandler {
|
||||
logger = logger.With(zap.String("module", "maplefile"))
|
||||
logger = logger.Named("DeleteMeHTTPHandler")
|
||||
return &DeleteMeHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*DeleteMeHTTPHandler) Pattern() string {
|
||||
return "DELETE /api/v1/me"
|
||||
}
|
||||
|
||||
func (r *DeleteMeHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply MaplesSend middleware before handling the request
|
||||
r.middleware.Attach(r.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *DeleteMeHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*svc_me.DeleteMeRequestDTO, error) {
|
||||
// Initialize our structure which will store the parsed request data
|
||||
var requestData svc_me.DeleteMeRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang struct else we need
|
||||
// to send a `400 Bad Request` error message back to the client
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *DeleteMeHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Execute(ctx, req); err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return successful no content response since the account was deleted
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
75
cloud/maplefile-backend/internal/interface/http/me/get.go
Normal file
75
cloud/maplefile-backend/internal/interface/http/me/get.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/me/get.go
|
||||
package me
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_me "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/me"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetMeHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_me.GetMeService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetMeHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_me.GetMeService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetMeHTTPHandler {
|
||||
logger = logger.With(zap.String("module", "maplefile"))
|
||||
logger = logger.Named("GetMeHTTPHandler")
|
||||
return &GetMeHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetMeHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/me"
|
||||
}
|
||||
|
||||
func (r *GetMeHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply MaplesSend middleware before handling the request
|
||||
r.middleware.Attach(r.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetMeHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
resp, err := h.service.Execute(ctx)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package me
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_me "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/me"
|
||||
)
|
||||
|
||||
// Wire providers for me HTTP handlers
|
||||
|
||||
func ProvideGetMeHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_me.GetMeService,
|
||||
mw middleware.Middleware,
|
||||
) *GetMeHTTPHandler {
|
||||
return NewGetMeHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvidePutUpdateMeHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_me.UpdateMeService,
|
||||
mw middleware.Middleware,
|
||||
) *PutUpdateMeHTTPHandler {
|
||||
return NewPutUpdateMeHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideDeleteMeHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_me.DeleteMeService,
|
||||
mw middleware.Middleware,
|
||||
) *DeleteMeHTTPHandler {
|
||||
return NewDeleteMeHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
110
cloud/maplefile-backend/internal/interface/http/me/update.go
Normal file
110
cloud/maplefile-backend/internal/interface/http/me/update.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/me/get.go
|
||||
package me
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_me "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/me"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type PutUpdateMeHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service svc_me.UpdateMeService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewPutUpdateMeHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service svc_me.UpdateMeService,
|
||||
middleware middleware.Middleware,
|
||||
) *PutUpdateMeHTTPHandler {
|
||||
logger = logger.With(zap.String("module", "maplefile"))
|
||||
logger = logger.Named("PutUpdateMeHTTPHandler")
|
||||
return &PutUpdateMeHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*PutUpdateMeHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/me"
|
||||
}
|
||||
|
||||
func (r *PutUpdateMeHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply MaplesSend middleware before handling the request
|
||||
r.middleware.Attach(r.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *PutUpdateMeHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*svc_me.UpdateMeRequestDTO, error) {
|
||||
// Initialize our array which will store all the results from the remote server.
|
||||
var requestData svc_me.UpdateMeRequestDTO
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
|
||||
|
||||
// Read the JSON string and convert it into our golang stuct else we need
|
||||
// to send a `400 Bad Request` errror message back to the client,
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData) // [1]
|
||||
if err != nil {
|
||||
h.logger.Error("decoding error",
|
||||
zap.Any("err", err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *PutUpdateMeHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
// Set response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Execute(ctx, req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response
|
||||
if resp != nil {
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
h.logger.Error("failed to encode response",
|
||||
zap.Any("error", err))
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := errors.New("no result")
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/interface/http/middleware/jwt.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
func (mid *middleware) JWTProcessorMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract the Authorization header
|
||||
reqToken := r.Header.Get("Authorization")
|
||||
|
||||
// Validate that Authorization header is present
|
||||
if reqToken == "" {
|
||||
problem := httperror.NewUnauthorizedError("Authorization not set")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the token from the Authorization header
|
||||
// Support both "Bearer" (RFC 6750 standard) and "JWT" schemes for compatibility
|
||||
var token string
|
||||
if strings.HasPrefix(reqToken, "Bearer ") {
|
||||
token = strings.TrimPrefix(reqToken, "Bearer ")
|
||||
} else if strings.HasPrefix(reqToken, "JWT ") {
|
||||
token = strings.TrimPrefix(reqToken, "JWT ")
|
||||
} else {
|
||||
problem := httperror.NewBadRequestError("Not properly formatted authorization header")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the token is not empty after prefix removal
|
||||
if token == "" {
|
||||
problem := httperror.NewBadRequestError("Not properly formatted authorization header")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Process the JWT token
|
||||
sessionID, err := mid.jwt.ProcessJWTToken(token)
|
||||
if err != nil {
|
||||
// Log the actual error for debugging but return generic message to client
|
||||
mid.logger.Error("JWT processing failed", zap.Error(err))
|
||||
problem := httperror.NewUnauthorizedError("Invalid or expired token")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Update our context to save our JWT token content information
|
||||
ctx = context.WithValue(ctx, constants.SessionIsAuthorized, true)
|
||||
ctx = context.WithValue(ctx, constants.SessionID, sessionID)
|
||||
|
||||
// Flow to the next middleware with our JWT token saved
|
||||
fn(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
func (mid *middleware) PostJWTProcessorMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get our authorization information.
|
||||
isAuthorized, ok := ctx.Value(constants.SessionIsAuthorized).(bool)
|
||||
if ok && isAuthorized {
|
||||
// CWE-391: Safe type assertion to prevent panic-based DoS
|
||||
// OWASP A09:2021: Security Logging and Monitoring - Prevents service crashes
|
||||
sessionID, ok := ctx.Value(constants.SessionID).(string)
|
||||
if !ok {
|
||||
mid.logger.Error("Invalid session ID type in context")
|
||||
problem := httperror.NewInternalServerError("Invalid session context")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user ID from the session ID (which is actually the user ID string from JWT)
|
||||
userID, err := gocql.ParseUUID(sessionID)
|
||||
if err != nil {
|
||||
problem := httperror.NewUnauthorizedError("Invalid user ID in token")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Lookup our user profile by ID or return 500 error.
|
||||
user, err := mid.userGetByIDUseCase.Execute(ctx, userID)
|
||||
if err != nil {
|
||||
// Log the actual error for debugging but return generic message to client
|
||||
mid.logger.Error("Failed to get user by ID",
|
||||
zap.Error(err),
|
||||
zap.String("user_id", userID.String()))
|
||||
problem := httperror.NewInternalServerError("Unable to verify session")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// If no user was found then that means our session expired and the
|
||||
// user needs to login or use the refresh token.
|
||||
if user == nil {
|
||||
problem := httperror.NewUnauthorizedError("Session expired")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// // If system administrator disabled the user account then we need
|
||||
// // to generate a 403 error letting the user know their account has
|
||||
// // been disabled and you cannot access the protected API endpoint.
|
||||
// if user.State == 0 {
|
||||
// http.Error(w, "Account disabled - please contact admin", http.StatusForbidden)
|
||||
// return
|
||||
// }
|
||||
|
||||
// Save our user information to the context.
|
||||
// Save our user.
|
||||
ctx = context.WithValue(ctx, constants.SessionUser, user)
|
||||
|
||||
// Save individual pieces of the user profile.
|
||||
ctx = context.WithValue(ctx, constants.SessionID, sessionID)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserID, user.ID)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserRole, user.Role)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserName, user.Name)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserFirstName, user.FirstName)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserLastName, user.LastName)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserTimezone, user.Timezone)
|
||||
// ctx = context.WithValue(ctx, constants.SessionUserStoreID, user.StoreID)
|
||||
// ctx = context.WithValue(ctx, constants.SessionUserStoreName, user.StoreName)
|
||||
// ctx = context.WithValue(ctx, constants.SessionUserStoreLevel, user.StoreLevel)
|
||||
// ctx = context.WithValue(ctx, constants.SessionUserStoreTimezone, user.StoreTimezone)
|
||||
}
|
||||
|
||||
fn(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware/middleware.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/jwt"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Middleware interface {
|
||||
Attach(fn http.HandlerFunc) http.HandlerFunc
|
||||
Shutdown(ctx context.Context)
|
||||
}
|
||||
|
||||
type middleware struct {
|
||||
logger *zap.Logger
|
||||
jwt jwt.JWTProvider
|
||||
userGetByIDUseCase uc_user.UserGetByIDUseCase
|
||||
}
|
||||
|
||||
func NewMiddleware(
|
||||
logger *zap.Logger,
|
||||
jwtp jwt.JWTProvider,
|
||||
uc1 uc_user.UserGetByIDUseCase,
|
||||
) Middleware {
|
||||
logger = logger.With(zap.String("module", "maplefile"))
|
||||
logger = logger.Named("MapleFile Middleware")
|
||||
return &middleware{
|
||||
logger: logger,
|
||||
jwt: jwtp,
|
||||
userGetByIDUseCase: uc1,
|
||||
}
|
||||
}
|
||||
|
||||
// Attach function attaches to HTTP router to apply for every API call.
|
||||
func (mid *middleware) Attach(fn http.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Apply base middleware to all requests
|
||||
handler := mid.applyBaseMiddleware(fn)
|
||||
|
||||
// Check if the path requires authentication
|
||||
if isProtectedPath(mid.logger, r.URL.Path) {
|
||||
|
||||
// Apply auth middleware for protected paths
|
||||
handler = mid.PostJWTProcessorMiddleware(handler)
|
||||
handler = mid.JWTProcessorMiddleware(handler)
|
||||
// handler = mid.EnforceBlacklistMiddleware(handler)
|
||||
}
|
||||
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach function attaches to HTTP router to apply for every API call.
|
||||
func (mid *middleware) applyBaseMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
// Apply middleware in reverse order (bottom up)
|
||||
handler := fn
|
||||
handler = mid.URLProcessorMiddleware(handler)
|
||||
handler = mid.RequestBodySizeLimitMiddleware(handler)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// RequestBodySizeLimitMiddleware limits the size of request bodies to prevent DoS attacks.
|
||||
// Default limit is 10MB for most requests, which is sufficient for JSON metadata payloads.
|
||||
func (mid *middleware) RequestBodySizeLimitMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 10MB limit for request bodies
|
||||
// This is sufficient for JSON metadata while preventing abuse
|
||||
const maxBodySize = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
if r.Body != nil {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
|
||||
}
|
||||
|
||||
fn(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown shuts down the middleware.
|
||||
func (mid *middleware) Shutdown(ctx context.Context) {
|
||||
// Log a message to indicate that the HTTP server is shutting down.
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/ratelimit"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/jwt"
|
||||
)
|
||||
|
||||
// Wire provider for middleware
|
||||
|
||||
func ProvideMiddleware(
|
||||
logger *zap.Logger,
|
||||
jwtProvider jwt.JWTProvider,
|
||||
userGetByIDUseCase uc_user.UserGetByIDUseCase,
|
||||
) Middleware {
|
||||
return NewMiddleware(logger, jwtProvider, userGetByIDUseCase)
|
||||
}
|
||||
|
||||
// ProvideRateLimitMiddleware provides the rate limit middleware for Wire DI
|
||||
func ProvideRateLimitMiddleware(
|
||||
logger *zap.Logger,
|
||||
loginRateLimiter ratelimit.LoginRateLimiter,
|
||||
) *RateLimitMiddleware {
|
||||
return NewRateLimitMiddleware(logger, loginRateLimiter)
|
||||
}
|
||||
|
||||
// ProvideSecurityHeadersMiddleware provides the security headers middleware for Wire DI
|
||||
func ProvideSecurityHeadersMiddleware(
|
||||
config *config.Config,
|
||||
) *SecurityHeadersMiddleware {
|
||||
return NewSecurityHeadersMiddleware(config)
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
// Package middleware provides HTTP middleware for the MapleFile backend.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/ratelimit"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// RateLimitMiddleware provides rate limiting functionality for HTTP endpoints
|
||||
type RateLimitMiddleware struct {
|
||||
logger *zap.Logger
|
||||
loginRateLimiter ratelimit.LoginRateLimiter
|
||||
}
|
||||
|
||||
// NewRateLimitMiddleware creates a new rate limit middleware
|
||||
func NewRateLimitMiddleware(logger *zap.Logger, loginRateLimiter ratelimit.LoginRateLimiter) *RateLimitMiddleware {
|
||||
return &RateLimitMiddleware{
|
||||
logger: logger.Named("RateLimitMiddleware"),
|
||||
loginRateLimiter: loginRateLimiter,
|
||||
}
|
||||
}
|
||||
|
||||
// LoginRateLimit applies login-specific rate limiting to auth endpoints
|
||||
// CWE-307: Protects against brute force attacks on authentication endpoints
|
||||
func (m *RateLimitMiddleware) LoginRateLimit(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract client IP
|
||||
clientIP := m.extractClientIP(r)
|
||||
|
||||
// Extract email from request body (need to buffer and restore)
|
||||
email := m.extractEmailFromRequest(r)
|
||||
|
||||
// Check rate limit
|
||||
allowed, isLocked, remainingAttempts, err := m.loginRateLimiter.CheckAndRecordAttempt(ctx, email, clientIP)
|
||||
if err != nil {
|
||||
// Log error but allow request (fail open for availability)
|
||||
m.logger.Warn("Rate limiter error, allowing request",
|
||||
zap.Error(err),
|
||||
zap.String("ip", validation.MaskIP(clientIP)))
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if account is locked
|
||||
if isLocked {
|
||||
m.logger.Warn("Login attempt on locked account",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Account temporarily locked due to too many failed attempts. Please try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if IP rate limit exceeded
|
||||
if !allowed {
|
||||
m.logger.Warn("Rate limit exceeded",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Int("remaining_attempts", remainingAttempts))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Too many requests. Please slow down and try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Add remaining attempts to response header for client awareness
|
||||
if remainingAttempts > 0 && remainingAttempts <= 3 {
|
||||
w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", remainingAttempts))
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRateLimit applies general rate limiting to auth endpoints
|
||||
// For endpoints like registration, email verification, etc.
|
||||
func (m *RateLimitMiddleware) AuthRateLimit(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract client IP for rate limiting key
|
||||
clientIP := m.extractClientIP(r)
|
||||
|
||||
// Use the login rate limiter for IP-based checking only
|
||||
// This provides basic protection against automated attacks
|
||||
ctx := r.Context()
|
||||
allowed, _, _, err := m.loginRateLimiter.CheckAndRecordAttempt(ctx, "", clientIP)
|
||||
if err != nil {
|
||||
// Fail open
|
||||
m.logger.Warn("Rate limiter error, allowing request", zap.Error(err))
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
m.logger.Warn("Auth rate limit exceeded",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Too many requests from this IP. Please try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// extractClientIP extracts the real client IP from the request
|
||||
func (m *RateLimitMiddleware) extractClientIP(r *http.Request) string {
|
||||
// Check X-Forwarded-For header first (for reverse proxies)
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// Take the first IP in the chain
|
||||
ips := strings.Split(xff, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Check X-Real-IP header
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return xri
|
||||
}
|
||||
|
||||
// Fall back to RemoteAddr
|
||||
// Remove port if present
|
||||
ip := r.RemoteAddr
|
||||
if idx := strings.LastIndex(ip, ":"); idx != -1 {
|
||||
ip = ip[:idx]
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
// extractEmailFromRequest extracts email from JSON request body
|
||||
// It buffers the body so it can be read again by the handler
|
||||
func (m *RateLimitMiddleware) extractEmailFromRequest(r *http.Request) string {
|
||||
// Read body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Restore body for handler
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
// Parse JSON to extract email
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(strings.TrimSpace(req.Email))
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
)
|
||||
|
||||
// SecurityHeadersMiddleware adds security headers to all HTTP responses.
|
||||
// These headers help protect against common web vulnerabilities.
|
||||
type SecurityHeadersMiddleware struct {
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewSecurityHeadersMiddleware creates a new security headers middleware.
|
||||
func NewSecurityHeadersMiddleware(config *config.Config) *SecurityHeadersMiddleware {
|
||||
return &SecurityHeadersMiddleware{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Handler wraps an http.Handler to add security headers to all responses.
|
||||
func (m *SecurityHeadersMiddleware) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// X-Content-Type-Options: Prevents MIME-type sniffing attacks
|
||||
// Browser will strictly follow the declared Content-Type
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// X-Frame-Options: Prevents clickjacking attacks
|
||||
// DENY = page cannot be displayed in any iframe
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
|
||||
// X-XSS-Protection: Enables browser's built-in XSS filter
|
||||
// mode=block = block the entire page if attack is detected
|
||||
// Note: Largely superseded by CSP, but still useful for older browsers
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
|
||||
// Referrer-Policy: Controls how much referrer information is sent
|
||||
// strict-origin-when-cross-origin = full URL for same-origin, origin only for cross-origin
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
||||
// Cache-Control: Prevent caching of sensitive responses
|
||||
// Especially important for auth endpoints
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, private")
|
||||
|
||||
// Permissions-Policy: Restricts browser features (formerly Feature-Policy)
|
||||
// Disables potentially dangerous features like geolocation, camera, microphone
|
||||
w.Header().Set("Permissions-Policy", "geolocation=(), camera=(), microphone=()")
|
||||
|
||||
// Content-Security-Policy: Prevents XSS and other code injection attacks
|
||||
// For API-only backend: deny all content sources and frame embedding
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
|
||||
|
||||
// Strict-Transport-Security (HSTS): Forces HTTPS for the specified duration
|
||||
// Only set in production where HTTPS is properly configured
|
||||
// max-age=31536000 = 1 year in seconds
|
||||
// includeSubDomains = applies to all subdomains
|
||||
if m.config.App.Environment == "production" {
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware/url.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// URLProcessorMiddleware Middleware will split the full URL path into slash-sperated parts and save to
|
||||
// the context to flow downstream in the app for this particular request.
|
||||
func (mid *middleware) URLProcessorMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Split path into slash-separated parts, for example, path "/foo/bar"
|
||||
// gives p==["foo", "bar"] and path "/" gives p==[""]. Our API starts with
|
||||
// "/api", as a result we will start the array slice at "1".
|
||||
p := strings.Split(r.URL.Path, "/")[1:]
|
||||
|
||||
// log.Println(p) // For debugging purposes only.
|
||||
|
||||
// Open our program's context based on the request and save the
|
||||
// slash-seperated array from our URL path.
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, "url_split", p)
|
||||
|
||||
// Flow to the next middleware.
|
||||
fn(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware/utils.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type protectedRoute struct {
|
||||
pattern string
|
||||
regex *regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
exactPaths = make(map[string]bool)
|
||||
patternRoutes []protectedRoute
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Exact matches
|
||||
exactPaths = map[string]bool{
|
||||
"/api/v1/me": true,
|
||||
"/api/v1/me/delete": true,
|
||||
"/api/v1/me/blocked-emails": true,
|
||||
"/api/v1/dashboard": true,
|
||||
"/api/v1/collections": true,
|
||||
"/api/v1/collections/filtered": true,
|
||||
"/api/v1/collections/root": true,
|
||||
"/api/v1/collections/shared": true,
|
||||
"/api/v1/collections/sync": true, // Sync collections endpoint
|
||||
"/api/v1/files": true,
|
||||
"/api/v1/files/pending": true, // Three-step workflow file-create endpoint: Start
|
||||
"/api/v1/files/recent": true,
|
||||
"/api/v1/files/sync": true, // Sync files endpoint
|
||||
"/api/v1/files/delete-multiple": true, // Delete multiple files endpoint
|
||||
"/api/v1/invites/send-email": true, // Send invitation email to non-registered user
|
||||
"/api/v1/tags": true, // List and create tags
|
||||
"/api/v1/tags/search": true, // Search by tags
|
||||
"/iam/api/v1/users/lookup": true, // User public key lookup (requires auth)
|
||||
}
|
||||
|
||||
// Pattern matches
|
||||
patterns := []string{
|
||||
// Blocked Email patterns
|
||||
"^/api/v1/me/blocked-emails/[^/]+$", // Delete specific blocked email
|
||||
|
||||
// Collection patterns (plural routes)
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+$", // Individual collection operations
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/move$", // Move collection
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/share$", // Share collection
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/members$", // Collection members
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/members/[a-zA-Z0-9-]+$", // Remove specific member
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/archive$", // Archive collection
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/restore$", // Restore collection
|
||||
"^/api/v1/collections-by-parent/[a-zA-Z0-9-]+$", // Collections by parent
|
||||
|
||||
// Collection patterns (singular routes for files)
|
||||
"^/api/v1/collection/[a-zA-Z0-9-]+/files$", // Collection files (singular)
|
||||
|
||||
// File patterns (singular routes)
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+$", // Individual file operations
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/data$", // File data
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/upload-url$", // File upload URL
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/download-url$", // File download URL
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/complete$", // Complete file upload
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/archive$", // Archive file
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/restore$", // Restore file
|
||||
|
||||
// Tag patterns
|
||||
"^/api/v1/tags/[a-zA-Z0-9-]+$", // Individual tag operations (GET, PUT, DELETE)
|
||||
"^/api/v1/tags/[a-zA-Z0-9-]+/assign$", // Assign tag to entity
|
||||
"^/api/v1/tags/[a-zA-Z0-9-]+/entities/[a-zA-Z0-9-]+$", // Unassign tag from entity
|
||||
"^/api/v1/tags/for/collection/[a-zA-Z0-9-]+$", // Get tags for collection
|
||||
"^/api/v1/tags/for/file/[a-zA-Z0-9-]+$", // Get tags for file
|
||||
"^/api/v1/tags/collections$", // List collections by tag
|
||||
"^/api/v1/tags/files$", // List files by tag
|
||||
}
|
||||
|
||||
// Precompile patterns
|
||||
patternRoutes = make([]protectedRoute, len(patterns))
|
||||
for i, pattern := range patterns {
|
||||
patternRoutes[i] = protectedRoute{
|
||||
pattern: pattern,
|
||||
regex: regexp.MustCompile(pattern),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isProtectedPath(logger *zap.Logger, path string) bool {
|
||||
// Check exact matches first (O(1) lookup)
|
||||
if exactPaths[path] {
|
||||
logger.Debug("✅ found via map - url is protected",
|
||||
zap.String("path", path))
|
||||
return true
|
||||
}
|
||||
|
||||
// Check patterns
|
||||
for _, route := range patternRoutes {
|
||||
if route.regex.MatchString(path) {
|
||||
logger.Debug("✅ found via regex - url is protected",
|
||||
zap.String("path", path))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("❌ not found",
|
||||
zap.String("path", path))
|
||||
|
||||
return false
|
||||
}
|
||||
221
cloud/maplefile-backend/internal/interface/http/provider.go
Normal file
221
cloud/maplefile-backend/internal/interface/http/provider.go
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/blockedemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/collection"
|
||||
commonhttp "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/common"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/dashboard"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/inviteemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/me"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/user"
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
)
|
||||
|
||||
// ProvideHandlers wires all HTTP handlers for Wire DI
|
||||
func ProvideHandlers(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
|
||||
// Common
|
||||
versionHandler *commonhttp.MapleFileVersionHTTPHandler,
|
||||
|
||||
// Dashboard
|
||||
getDashboard *dashboard.GetDashboardHTTPHandler,
|
||||
|
||||
// Me
|
||||
getMe *me.GetMeHTTPHandler,
|
||||
updateMe *me.PutUpdateMeHTTPHandler,
|
||||
deleteMe *me.DeleteMeHTTPHandler,
|
||||
|
||||
// User
|
||||
userPublicLookup *user.UserPublicLookupHTTPHandler,
|
||||
|
||||
// Blocked Email
|
||||
createBlockedEmail *blockedemail.CreateBlockedEmailHTTPHandler,
|
||||
listBlockedEmails *blockedemail.ListBlockedEmailsHTTPHandler,
|
||||
deleteBlockedEmail *blockedemail.DeleteBlockedEmailHTTPHandler,
|
||||
|
||||
// Invite Email
|
||||
sendInviteEmail *inviteemail.SendInviteEmailHTTPHandler,
|
||||
|
||||
// Collection - Basic CRUD
|
||||
createCollection *collection.CreateCollectionHTTPHandler,
|
||||
getCollection *collection.GetCollectionHTTPHandler,
|
||||
listUserCollections *collection.ListUserCollectionsHTTPHandler,
|
||||
updateCollection *collection.UpdateCollectionHTTPHandler,
|
||||
softDeleteCollection *collection.SoftDeleteCollectionHTTPHandler,
|
||||
archiveCollection *collection.ArchiveCollectionHTTPHandler,
|
||||
restoreCollection *collection.RestoreCollectionHTTPHandler,
|
||||
|
||||
// Collection - Hierarchical
|
||||
findCollectionsByParent *collection.FindCollectionsByParentHTTPHandler,
|
||||
findRootCollections *collection.FindRootCollectionsHTTPHandler,
|
||||
moveCollection *collection.MoveCollectionHTTPHandler,
|
||||
|
||||
// Collection - Sharing
|
||||
shareCollection *collection.ShareCollectionHTTPHandler,
|
||||
removeMember *collection.RemoveMemberHTTPHandler,
|
||||
listSharedCollections *collection.ListSharedCollectionsHTTPHandler,
|
||||
|
||||
// Collection - Filtered
|
||||
getFilteredCollections *collection.GetFilteredCollectionsHTTPHandler,
|
||||
|
||||
// Collection - Sync
|
||||
collectionSync *collection.CollectionSyncHTTPHandler,
|
||||
|
||||
// File - CRUD
|
||||
softDeleteFile *file.SoftDeleteFileHTTPHandler,
|
||||
deleteMultipleFiles *file.DeleteMultipleFilesHTTPHandler,
|
||||
getFile *file.GetFileHTTPHandler,
|
||||
listFilesByCollection *file.ListFilesByCollectionHTTPHandler,
|
||||
updateFile *file.UpdateFileHTTPHandler,
|
||||
createPendingFile *file.CreatePendingFileHTTPHandler,
|
||||
completeFileUpload *file.CompleteFileUploadHTTPHandler,
|
||||
getPresignedUploadURL *file.GetPresignedUploadURLHTTPHandler,
|
||||
getPresignedDownloadURL *file.GetPresignedDownloadURLHTTPHandler,
|
||||
reportDownloadCompleted *file.ReportDownloadCompletedHTTPHandler,
|
||||
archiveFile *file.ArchiveFileHTTPHandler,
|
||||
restoreFile *file.RestoreFileHTTPHandler,
|
||||
listRecentFiles *file.ListRecentFilesHTTPHandler,
|
||||
|
||||
// File - Sync
|
||||
fileSync *file.FileSyncHTTPHandler,
|
||||
|
||||
// Tag handlers
|
||||
createTag *tag.CreateTagHTTPHandler,
|
||||
listTags *tag.ListTagsHTTPHandler,
|
||||
getTag *tag.GetTagHTTPHandler,
|
||||
updateTag *tag.UpdateTagHTTPHandler,
|
||||
deleteTag *tag.DeleteTagHTTPHandler,
|
||||
assignTag *tag.AssignTagHTTPHandler,
|
||||
unassignTag *tag.UnassignTagHTTPHandler,
|
||||
getTagsForCollection *tag.GetTagsForCollectionHTTPHandler,
|
||||
getTagsForFile *tag.GetTagsForFileHTTPHandler,
|
||||
listCollectionsByTag *tag.ListCollectionsByTagHandler,
|
||||
listFilesByTag *tag.ListFilesByTagHandler,
|
||||
searchByTags *tag.SearchByTagsHandler,
|
||||
) *Handlers {
|
||||
return NewHandlers(
|
||||
// Common
|
||||
versionHandler,
|
||||
|
||||
// Dashboard
|
||||
getDashboard,
|
||||
|
||||
// Me
|
||||
getMe,
|
||||
updateMe,
|
||||
deleteMe,
|
||||
|
||||
// User
|
||||
userPublicLookup,
|
||||
|
||||
// Blocked Email
|
||||
createBlockedEmail,
|
||||
listBlockedEmails,
|
||||
deleteBlockedEmail,
|
||||
|
||||
// Invite Email
|
||||
sendInviteEmail,
|
||||
|
||||
// Collection - Basic CRUD
|
||||
createCollection,
|
||||
getCollection,
|
||||
listUserCollections,
|
||||
updateCollection,
|
||||
softDeleteCollection,
|
||||
archiveCollection,
|
||||
restoreCollection,
|
||||
|
||||
// Collection - Hierarchical
|
||||
findCollectionsByParent,
|
||||
findRootCollections,
|
||||
moveCollection,
|
||||
|
||||
// Collection - Sharing
|
||||
shareCollection,
|
||||
removeMember,
|
||||
listSharedCollections,
|
||||
|
||||
// Collection - Filtered
|
||||
getFilteredCollections,
|
||||
|
||||
// Collection Sync
|
||||
collectionSync,
|
||||
|
||||
// File - CRUD
|
||||
softDeleteFile,
|
||||
deleteMultipleFiles,
|
||||
getFile,
|
||||
listFilesByCollection,
|
||||
updateFile,
|
||||
createPendingFile,
|
||||
completeFileUpload,
|
||||
getPresignedUploadURL,
|
||||
getPresignedDownloadURL,
|
||||
reportDownloadCompleted,
|
||||
archiveFile,
|
||||
restoreFile,
|
||||
listRecentFiles,
|
||||
|
||||
// File Sync
|
||||
fileSync,
|
||||
|
||||
// Tag handlers
|
||||
createTag,
|
||||
listTags,
|
||||
getTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
assignTag,
|
||||
unassignTag,
|
||||
getTagsForCollection,
|
||||
getTagsForFile,
|
||||
listCollectionsByTag,
|
||||
listFilesByTag,
|
||||
searchByTags,
|
||||
)
|
||||
}
|
||||
|
||||
// ProvideServer provides the HTTP server for Wire DI
|
||||
func ProvideServer(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
handlers *Handlers,
|
||||
registerService svc_auth.RegisterService,
|
||||
verifyEmailService svc_auth.VerifyEmailService,
|
||||
resendVerificationService svc_auth.ResendVerificationService,
|
||||
requestOTTService svc_auth.RequestOTTService,
|
||||
verifyOTTService svc_auth.VerifyOTTService,
|
||||
completeLoginService svc_auth.CompleteLoginService,
|
||||
refreshTokenService svc_auth.RefreshTokenService,
|
||||
recoveryInitiateService svc_auth.RecoveryInitiateService,
|
||||
recoveryVerifyService svc_auth.RecoveryVerifyService,
|
||||
recoveryCompleteService svc_auth.RecoveryCompleteService,
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware,
|
||||
securityHeadersMiddleware *middleware.SecurityHeadersMiddleware,
|
||||
) *WireServer {
|
||||
return NewWireServer(
|
||||
cfg,
|
||||
logger,
|
||||
handlers,
|
||||
registerService,
|
||||
verifyEmailService,
|
||||
resendVerificationService,
|
||||
requestOTTService,
|
||||
verifyOTTService,
|
||||
completeLoginService,
|
||||
refreshTokenService,
|
||||
recoveryInitiateService,
|
||||
recoveryVerifyService,
|
||||
recoveryCompleteService,
|
||||
rateLimitMiddleware,
|
||||
securityHeadersMiddleware,
|
||||
)
|
||||
}
|
||||
119
cloud/maplefile-backend/internal/interface/http/routes.go
Normal file
119
cloud/maplefile-backend/internal/interface/http/routes.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package http
|
||||
|
||||
// routes.go - HTTP route registration for MapleFile backend
|
||||
// This file documents all available endpoints
|
||||
|
||||
/*
|
||||
ROUTE STRUCTURE:
|
||||
|
||||
Public Routes (No authentication required):
|
||||
GET /health - Health check
|
||||
GET /version - Version information
|
||||
POST /api/v1/auth/register - User registration
|
||||
POST /api/v1/auth/login - User login
|
||||
POST /api/v1/auth/refresh - Refresh JWT token
|
||||
POST /api/v1/auth/logout - Logout
|
||||
|
||||
Protected Routes (Authentication required):
|
||||
|
||||
Auth & Profile:
|
||||
GET /api/v1/me - Get current user profile
|
||||
PUT /api/v1/me - Update user profile
|
||||
DELETE /api/v1/me - Delete user account
|
||||
POST /api/v1/me/verify - Verify user profile
|
||||
|
||||
Dashboard:
|
||||
GET /api/v1/dashboard - Get dashboard data
|
||||
|
||||
Invitations:
|
||||
POST /api/v1/invites/send-email - Send invitation to non-registered user
|
||||
|
||||
Collections (Basic CRUD):
|
||||
POST /api/v1/collections - Create collection
|
||||
GET /api/v1/collections - List user collections
|
||||
GET /api/v1/collections/{id} - Get collection by ID
|
||||
PUT /api/v1/collections/{id} - Update collection
|
||||
DELETE /api/v1/collections/{id} - Delete collection
|
||||
|
||||
Collections (Hierarchical):
|
||||
GET /api/v1/collections/root - Get root collections
|
||||
GET /api/v1/collections/parent/{parent_id} - Get collections by parent
|
||||
PUT /api/v1/collections/{id}/move - Move collection
|
||||
|
||||
Collections (Sharing):
|
||||
POST /api/v1/collections/{id}/share - Share collection
|
||||
DELETE /api/v1/collections/{id}/members/{user_id} - Remove member
|
||||
GET /api/v1/collections/shared - List shared collections
|
||||
|
||||
Collections (Operations):
|
||||
PUT /api/v1/collections/{id}/archive - Archive collection
|
||||
PUT /api/v1/collections/{id}/restore - Restore collection
|
||||
GET /api/v1/collections/filtered - Get filtered collections
|
||||
POST /api/v1/collections/sync - Sync collections
|
||||
|
||||
Files (Basic CRUD):
|
||||
POST /api/v1/files/pending - Create pending file
|
||||
POST /api/v1/files/{id}/complete - Complete file upload
|
||||
GET /api/v1/files/{id} - Get file by ID
|
||||
PUT /api/v1/files/{id} - Update file
|
||||
DELETE /api/v1/files/{id} - Delete file
|
||||
POST /api/v1/files/delete-multiple - Delete multiple files
|
||||
|
||||
Files (Operations):
|
||||
GET /api/v1/files/collection/{collection_id} - List files by collection
|
||||
GET /api/v1/files/recent - List recent files
|
||||
PUT /api/v1/files/{id}/archive - Archive file
|
||||
PUT /api/v1/files/{id}/restore - Restore file
|
||||
POST /api/v1/files/sync - Sync files
|
||||
|
||||
Files (Storage):
|
||||
GET /api/v1/files/{id}/upload-url - Get presigned upload URL
|
||||
GET /api/v1/files/{id}/download-url - Get presigned download URL
|
||||
|
||||
Total Endpoints: ~47
|
||||
*/
|
||||
|
||||
// RouteInfo represents information about a route
|
||||
type RouteInfo struct {
|
||||
Method string
|
||||
Path string
|
||||
Description string
|
||||
Protected bool
|
||||
}
|
||||
|
||||
// GetAllRoutes returns a list of all available routes
|
||||
func GetAllRoutes() []RouteInfo {
|
||||
return []RouteInfo{
|
||||
// Public routes
|
||||
{Method: "GET", Path: "/health", Description: "Health check", Protected: false},
|
||||
{Method: "GET", Path: "/version", Description: "Version information", Protected: false},
|
||||
{Method: "POST", Path: "/api/v1/auth/register", Description: "User registration", Protected: false},
|
||||
{Method: "POST", Path: "/api/v1/auth/login", Description: "User login", Protected: false},
|
||||
{Method: "POST", Path: "/api/v1/auth/refresh", Description: "Refresh JWT token", Protected: false},
|
||||
{Method: "POST", Path: "/api/v1/auth/logout", Description: "Logout", Protected: false},
|
||||
|
||||
// Profile routes
|
||||
{Method: "GET", Path: "/api/v1/me", Description: "Get current user profile", Protected: true},
|
||||
{Method: "PUT", Path: "/api/v1/me", Description: "Update user profile", Protected: true},
|
||||
{Method: "DELETE", Path: "/api/v1/me", Description: "Delete user account", Protected: true},
|
||||
|
||||
// Dashboard
|
||||
{Method: "GET", Path: "/api/v1/dashboard", Description: "Get dashboard data", Protected: true},
|
||||
|
||||
// Collections
|
||||
{Method: "POST", Path: "/api/v1/collections", Description: "Create collection", Protected: true},
|
||||
{Method: "GET", Path: "/api/v1/collections", Description: "List collections", Protected: true},
|
||||
{Method: "GET", Path: "/api/v1/collections/{id}", Description: "Get collection", Protected: true},
|
||||
{Method: "PUT", Path: "/api/v1/collections/{id}", Description: "Update collection", Protected: true},
|
||||
{Method: "DELETE", Path: "/api/v1/collections/{id}", Description: "Delete collection", Protected: true},
|
||||
|
||||
// Files
|
||||
{Method: "POST", Path: "/api/v1/files/pending", Description: "Create pending file", Protected: true},
|
||||
{Method: "POST", Path: "/api/v1/files/{id}/complete", Description: "Complete upload", Protected: true},
|
||||
{Method: "GET", Path: "/api/v1/files/{id}", Description: "Get file", Protected: true},
|
||||
{Method: "PUT", Path: "/api/v1/files/{id}", Description: "Update file", Protected: true},
|
||||
{Method: "DELETE", Path: "/api/v1/files/{id}", Description: "Delete file", Protected: true},
|
||||
|
||||
// ... (More routes will be registered in Phase 6)
|
||||
}
|
||||
}
|
||||
347
cloud/maplefile-backend/internal/interface/http/server.go
Normal file
347
cloud/maplefile-backend/internal/interface/http/server.go
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// Server represents the HTTP server
|
||||
type Server struct {
|
||||
server *http.Server
|
||||
logger *zap.Logger
|
||||
config *config.Config
|
||||
mux *http.ServeMux
|
||||
middleware middleware.Middleware
|
||||
handlers *Handlers
|
||||
}
|
||||
|
||||
// NewServer creates a new HTTP server
|
||||
func NewServer(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
mw middleware.Middleware,
|
||||
handlers *Handlers,
|
||||
) *Server {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
s := &Server{
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
mux: mux,
|
||||
middleware: mw,
|
||||
handlers: handlers,
|
||||
}
|
||||
|
||||
// Register routes
|
||||
s.registerRoutes()
|
||||
|
||||
// Create HTTP server with configuration
|
||||
s.server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
|
||||
Handler: s.applyMiddleware(mux),
|
||||
ReadTimeout: cfg.Server.ReadTimeout,
|
||||
WriteTimeout: cfg.Server.WriteTimeout,
|
||||
IdleTimeout: cfg.Server.IdleTimeout,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (s *Server) Start() error {
|
||||
s.logger.Info("Starting HTTP server",
|
||||
zap.String("address", s.server.Addr),
|
||||
zap.String("environment", s.config.App.Environment),
|
||||
)
|
||||
|
||||
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the HTTP server
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
s.logger.Info("Shutting down HTTP server")
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// applyMiddleware applies global middleware to the handler
|
||||
func (s *Server) applyMiddleware(handler http.Handler) http.Handler {
|
||||
// Apply middleware in reverse order (last applied is executed first)
|
||||
// TODO: Add more middleware in Phase 6
|
||||
|
||||
// Logging middleware (outermost)
|
||||
handler = s.loggingMiddleware(handler)
|
||||
|
||||
// CORS middleware
|
||||
handler = s.corsMiddleware(handler)
|
||||
|
||||
// Recovery middleware (catches panics)
|
||||
handler = s.recoveryMiddleware(handler)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// registerRoutes registers all HTTP routes
|
||||
func (s *Server) registerRoutes() {
|
||||
s.logger.Info("Registering HTTP routes")
|
||||
|
||||
// ===== Public Routes =====
|
||||
s.mux.HandleFunc("GET /health", s.healthCheckHandler)
|
||||
s.mux.HandleFunc("GET /version", s.versionHandler)
|
||||
|
||||
// TODO: Auth routes to be implemented in Phase 7
|
||||
// s.mux.HandleFunc("POST /api/v1/auth/register", authHandler.Register)
|
||||
// s.mux.HandleFunc("POST /api/v1/auth/login", authHandler.Login)
|
||||
|
||||
// ===== Protected Routes =====
|
||||
|
||||
// Me / Profile routes
|
||||
s.mux.HandleFunc("GET /api/v1/me", s.handlers.GetMe.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/me", s.handlers.UpdateMe.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/me", s.handlers.DeleteMe.ServeHTTP)
|
||||
|
||||
// Blocked Email routes
|
||||
s.mux.HandleFunc("POST /api/v1/me/blocked-emails", s.handlers.CreateBlockedEmail.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/me/blocked-emails", s.handlers.ListBlockedEmails.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/me/blocked-emails/{email}", s.handlers.DeleteBlockedEmail.ServeHTTP)
|
||||
|
||||
// Invite Email routes
|
||||
s.mux.HandleFunc("POST /api/v1/invites/send-email", s.handlers.SendInviteEmail.ServeHTTP)
|
||||
|
||||
// Dashboard
|
||||
s.mux.HandleFunc("GET /api/v1/dashboard", s.handlers.GetDashboard.ServeHTTP)
|
||||
|
||||
// Collections - Basic CRUD
|
||||
s.mux.HandleFunc("POST /api/v1/collections", s.handlers.CreateCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections", s.handlers.ListUserCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/{id}", s.handlers.GetCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}", s.handlers.UpdateCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/collections/{id}", s.handlers.SoftDeleteCollection.ServeHTTP)
|
||||
|
||||
// Collections - Hierarchical
|
||||
s.mux.HandleFunc("GET /api/v1/collections/root", s.handlers.FindRootCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/parent/{parent_id}", s.handlers.FindCollectionsByParent.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/move", s.handlers.MoveCollection.ServeHTTP)
|
||||
|
||||
// Collections - Sharing
|
||||
s.mux.HandleFunc("POST /api/v1/collections/{id}/share", s.handlers.ShareCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/collections/{id}/members/{user_id}", s.handlers.RemoveMember.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/shared", s.handlers.ListSharedCollections.ServeHTTP)
|
||||
|
||||
// Collections - Operations
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/archive", s.handlers.ArchiveCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/restore", s.handlers.RestoreCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/filtered", s.handlers.GetFilteredCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/collections/sync", s.handlers.CollectionSync.ServeHTTP)
|
||||
|
||||
// Files - Non-parameterized routes (no wildcards)
|
||||
s.mux.HandleFunc("POST /api/v1/files/pending", s.handlers.CreatePendingFile.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/files/delete-multiple", s.handlers.DeleteMultipleFiles.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/files/recent", s.handlers.ListRecentFiles.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/files/sync", s.handlers.FileSync.ServeHTTP)
|
||||
|
||||
// Files - Parameterized routes under /file/ prefix (singular) to avoid conflicts
|
||||
s.mux.HandleFunc("POST /api/v1/file/{id}/complete", s.handlers.CompleteFileUpload.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}/archive", s.handlers.ArchiveFile.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}/restore", s.handlers.RestoreFile.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}/upload-url", s.handlers.GetPresignedUploadURL.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}/download-url", s.handlers.GetPresignedDownloadURL.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}", s.handlers.GetFile.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}", s.handlers.UpdateFile.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/file/{id}", s.handlers.SoftDeleteFile.ServeHTTP)
|
||||
|
||||
// Files by collection - under /collection/ prefix
|
||||
s.mux.HandleFunc("GET /api/v1/collection/{collection_id}/files", s.handlers.ListFilesByCollection.ServeHTTP)
|
||||
|
||||
// Tags - Basic CRUD
|
||||
s.mux.HandleFunc("POST /api/v1/tags", s.handlers.CreateTag.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags", s.handlers.ListTags.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/{id}", s.handlers.GetTag.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/tags/{id}", s.handlers.UpdateTag.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/tags/{id}", s.handlers.DeleteTag.ServeHTTP)
|
||||
|
||||
// Tags - Assignment
|
||||
s.mux.HandleFunc("POST /api/v1/tags/{id}/assign", s.handlers.AssignTag.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/tags/{tagId}/entities/{entityId}", s.handlers.UnassignTag.ServeHTTP)
|
||||
|
||||
// Tags - Entity lookups
|
||||
s.mux.HandleFunc("GET /api/v1/collections/{id}/tags", s.handlers.GetTagsForCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/files/{id}/tags", s.handlers.GetTagsForFile.ServeHTTP)
|
||||
|
||||
// Tags - Multi-tag filtering (requires tags query parameter with comma-separated UUIDs)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/collections", s.handlers.ListCollectionsByTag.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/files", s.handlers.ListFilesByTag.ServeHTTP)
|
||||
|
||||
s.logger.Info("HTTP routes registered", zap.Int("total_routes", 58))
|
||||
}
|
||||
|
||||
// Health check handler
|
||||
func (s *Server) healthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"healthy","service":"maplefile-backend"}`))
|
||||
}
|
||||
|
||||
// Version handler
|
||||
func (s *Server) versionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
version := fmt.Sprintf(`{"version":"%s","environment":"%s"}`,
|
||||
s.config.App.Version,
|
||||
s.config.App.Environment)
|
||||
w.Write([]byte(version))
|
||||
}
|
||||
|
||||
// Middleware implementations
|
||||
|
||||
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip logging health check requests to reduce noise
|
||||
if r.URL.Path == "/health" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Wrap response writer to capture status code
|
||||
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
duration := time.Since(start)
|
||||
|
||||
s.logger.Info("HTTP request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Int("status", wrapped.statusCode),
|
||||
zap.Duration("duration", duration),
|
||||
zap.String("remote_addr", validation.MaskIP(r.RemoteAddr)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the origin from the request
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// Build allowed origins map
|
||||
allowedOrigins := make(map[string]bool)
|
||||
|
||||
// In development, always allow localhost origins
|
||||
if s.config.App.Environment == "development" {
|
||||
allowedOrigins["http://localhost:5173"] = true // Vite dev server
|
||||
allowedOrigins["http://localhost:5174"] = true // Alternative Vite port
|
||||
allowedOrigins["http://localhost:3000"] = true // Common React port
|
||||
allowedOrigins["http://127.0.0.1:5173"] = true
|
||||
allowedOrigins["http://127.0.0.1:5174"] = true
|
||||
allowedOrigins["http://127.0.0.1:3000"] = true
|
||||
}
|
||||
|
||||
// Add production origins from configuration
|
||||
for _, allowedOrigin := range s.config.Security.AllowedOrigins {
|
||||
if allowedOrigin != "" {
|
||||
allowedOrigins[allowedOrigin] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the request origin is allowed
|
||||
if allowedOrigins[origin] {
|
||||
// SECURITY FIX: Validate origin before setting CORS headers
|
||||
// CWE-942: Permissive Cross-domain Policy with Untrusted Domains
|
||||
// OWASP A05:2021: Security Misconfiguration - Secure CORS configuration
|
||||
|
||||
// Prevent wildcard origin with credentials (major security risk)
|
||||
if origin == "*" {
|
||||
s.logger.Error("CRITICAL: Wildcard origin (*) cannot be used with credentials",
|
||||
zap.String("path", r.URL.Path))
|
||||
// Don't set CORS headers for wildcard - this is a misconfiguration
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// In production, enforce HTTPS origins for security
|
||||
if s.config.App.Environment == "production" {
|
||||
if len(origin) >= 5 && origin[:5] == "http:" {
|
||||
s.logger.Warn("Non-HTTPS origin rejected in production",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path))
|
||||
// Don't set CORS headers for non-HTTPS origins in production
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set CORS headers for validated origins
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// Only set credentials for specific, non-wildcard origins
|
||||
// This prevents credential leakage to untrusted domains
|
||||
if origin != "*" && origin != "" {
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
w.Header().Set("Access-Control-Max-Age", "3600") // Cache preflight for 1 hour
|
||||
|
||||
s.logger.Debug("CORS headers added",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Bool("credentials_allowed", origin != "*"))
|
||||
} else if origin != "" {
|
||||
// Log rejected origins for debugging
|
||||
s.logger.Warn("CORS request from disallowed origin",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path))
|
||||
}
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) recoveryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
s.logger.Error("Panic recovered",
|
||||
zap.Any("error", err),
|
||||
zap.String("path", r.URL.Path),
|
||||
)
|
||||
problem := httperror.NewInternalServerError("An unexpected error occurred")
|
||||
problem.WithInstance(r.URL.Path)
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// responseWriter wraps http.ResponseWriter to capture status code
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
134
cloud/maplefile-backend/internal/interface/http/tag/assign.go
Normal file
134
cloud/maplefile-backend/internal/interface/http/tag/assign.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/assign.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type AssignTagRequest struct {
|
||||
EntityID string `json:"entity_id"`
|
||||
EntityType string `json:"entity_type"` // "collection" or "file"
|
||||
}
|
||||
|
||||
type AssignTagHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewAssignTagHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *AssignTagHTTPHandler {
|
||||
logger = logger.Named("AssignTagHTTPHandler")
|
||||
return &AssignTagHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*AssignTagHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/tags/{id}/assign"
|
||||
}
|
||||
|
||||
func (h *AssignTagHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *AssignTagHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*AssignTagRequest, error) {
|
||||
var requestData AssignTagRequest
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON)
|
||||
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decode assign tag request",
|
||||
zap.Error(err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewBadRequestError("Invalid request payload")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *AssignTagHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get user ID from JWT context
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
h.logger.Error("Failed to get user ID from context")
|
||||
httperror.ResponseError(w, httperror.NewUnauthorizedError("User not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tag ID from path
|
||||
tagIDStr := r.PathValue("id")
|
||||
if tagIDStr == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Tag ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
tagID, err := gocql.ParseUUID(tagIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid tag ID", zap.Error(err), zap.String("id", tagIDStr))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to unmarshal request", zap.Error(err))
|
||||
httperror.ResponseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse entity ID
|
||||
entityID, err := gocql.ParseUUID(req.EntityID)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid entity ID", zap.Error(err), zap.String("entity_id", req.EntityID))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid entity ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Assign tag
|
||||
if err := h.service.AssignTag(ctx, userID, tagID, entityID, req.EntityType); err != nil {
|
||||
h.logger.Error("Failed to assign tag",
|
||||
zap.Error(err),
|
||||
zap.String("tag_id", tagIDStr),
|
||||
zap.String("entity_id", req.EntityID),
|
||||
zap.String("entity_type", req.EntityType),
|
||||
)
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("Failed to assign tag"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
202
cloud/maplefile-backend/internal/interface/http/tag/create.go
Normal file
202
cloud/maplefile-backend/internal/interface/http/tag/create.go
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/create.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
dom_crypto "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// CreateTagRequest contains encrypted tag data from the client (E2EE)
|
||||
// The client sends a complete Tag object with encrypted fields
|
||||
type CreateTagRequest struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
EncryptedName string `json:"encrypted_name"`
|
||||
EncryptedColor string `json:"encrypted_color"`
|
||||
EncryptedTagKey *EncryptedTagKeyDTO `json:"encrypted_tag_key"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ModifiedAt string `json:"modified_at"`
|
||||
Version uint64 `json:"version"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
// EncryptedTagKeyDTO for JSON (un)marshaling
|
||||
type EncryptedTagKeyDTO struct {
|
||||
Ciphertext string `json:"ciphertext"` // Base64 encoded
|
||||
Nonce string `json:"nonce"` // Base64 encoded
|
||||
}
|
||||
|
||||
type CreateTagHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewCreateTagHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *CreateTagHTTPHandler {
|
||||
logger = logger.Named("CreateTagHTTPHandler")
|
||||
return &CreateTagHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*CreateTagHTTPHandler) Pattern() string {
|
||||
return "POST /api/v1/tags"
|
||||
}
|
||||
|
||||
func (h *CreateTagHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *CreateTagHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*CreateTagRequest, error) {
|
||||
var requestData CreateTagRequest
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON)
|
||||
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decode create tag request",
|
||||
zap.Error(err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewBadRequestError("Invalid request payload")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *CreateTagHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get user ID from JWT context
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
h.logger.Error("Failed to get user ID from context")
|
||||
httperror.ResponseError(w, httperror.NewUnauthorizedError("User not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to unmarshal request", zap.Error(err))
|
||||
httperror.ResponseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse tag ID
|
||||
tagID, err := gocql.ParseUUID(req.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid tag ID", zap.Error(err), zap.String("id", req.ID))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag ID"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse timestamps
|
||||
createdAt, err := time.Parse(time.RFC3339, req.CreatedAt)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid created_at timestamp", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid created_at timestamp"))
|
||||
return
|
||||
}
|
||||
|
||||
modifiedAt, err := time.Parse(time.RFC3339, req.ModifiedAt)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid modified_at timestamp", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid modified_at timestamp"))
|
||||
return
|
||||
}
|
||||
|
||||
// Decode encrypted tag key
|
||||
var encryptedTagKey *dom_crypto.EncryptedTagKey
|
||||
if req.EncryptedTagKey != nil {
|
||||
// Decode ciphertext from URL-safe base64
|
||||
ciphertext, err := base64.RawURLEncoding.DecodeString(req.EncryptedTagKey.Ciphertext)
|
||||
if err != nil {
|
||||
// Fallback to standard encoding
|
||||
ciphertext, err = base64.StdEncoding.DecodeString(req.EncryptedTagKey.Ciphertext)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decode tag key ciphertext", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag key ciphertext"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Decode nonce from URL-safe base64
|
||||
nonce, err := base64.RawURLEncoding.DecodeString(req.EncryptedTagKey.Nonce)
|
||||
if err != nil {
|
||||
// Fallback to standard encoding
|
||||
nonce, err = base64.StdEncoding.DecodeString(req.EncryptedTagKey.Nonce)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decode tag key nonce", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag key nonce"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encryptedTagKey = &dom_crypto.EncryptedTagKey{
|
||||
Ciphertext: ciphertext,
|
||||
Nonce: nonce,
|
||||
KeyVersion: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Create tag domain object
|
||||
tag := &dom_tag.Tag{
|
||||
ID: tagID,
|
||||
UserID: userID,
|
||||
EncryptedName: req.EncryptedName,
|
||||
EncryptedColor: req.EncryptedColor,
|
||||
EncryptedTagKey: encryptedTagKey,
|
||||
CreatedAt: createdAt,
|
||||
ModifiedAt: modifiedAt,
|
||||
Version: req.Version,
|
||||
State: req.State,
|
||||
}
|
||||
|
||||
// Create tag
|
||||
err = h.service.CreateTag(ctx, tag)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create tag",
|
||||
zap.Error(err),
|
||||
zap.String("tag_id", tagID.String()),
|
||||
)
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("Failed to create tag"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(tag)
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/delete.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type DeleteTagHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewDeleteTagHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *DeleteTagHTTPHandler {
|
||||
logger = logger.Named("DeleteTagHTTPHandler")
|
||||
return &DeleteTagHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*DeleteTagHTTPHandler) Pattern() string {
|
||||
return "DELETE /api/v1/tags/{id}"
|
||||
}
|
||||
|
||||
func (h *DeleteTagHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *DeleteTagHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get user ID from JWT context
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
h.logger.Error("Failed to get user ID from context")
|
||||
httperror.ResponseError(w, httperror.NewUnauthorizedError("User not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tag ID from path
|
||||
idStr := r.PathValue("id")
|
||||
if idStr == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Tag ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
tagID, err := gocql.ParseUUID(idStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid tag ID", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Delete tag
|
||||
if err := h.service.DeleteTag(ctx, userID, tagID); err != nil {
|
||||
h.logger.Error("Failed to delete tag", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("Failed to delete tag"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
76
cloud/maplefile-backend/internal/interface/http/tag/get.go
Normal file
76
cloud/maplefile-backend/internal/interface/http/tag/get.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/get.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetTagHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetTagHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetTagHTTPHandler {
|
||||
logger = logger.Named("GetTagHTTPHandler")
|
||||
return &GetTagHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetTagHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/tags/{id}"
|
||||
}
|
||||
|
||||
func (h *GetTagHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetTagHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get tag ID from path
|
||||
idStr := r.PathValue("id")
|
||||
if idStr == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Tag ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
tagID, err := gocql.ParseUUID(idStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid tag ID", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tag
|
||||
tag, err := h.service.GetTag(ctx, tagID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get tag", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewNotFoundError("Tag not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(tag)
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/get_for_entity.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetTagsForCollectionHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetTagsForCollectionHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetTagsForCollectionHTTPHandler {
|
||||
logger = logger.Named("GetTagsForCollectionHTTPHandler")
|
||||
return &GetTagsForCollectionHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetTagsForCollectionHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/collections/{id}/tags"
|
||||
}
|
||||
|
||||
func (h *GetTagsForCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetTagsForCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get collection ID from path
|
||||
idStr := r.PathValue("id")
|
||||
if idStr == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Collection ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
collectionID, err := gocql.ParseUUID(idStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid collection ID", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid collection ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tags for collection
|
||||
tags, err := h.service.GetTagsForEntity(ctx, collectionID, "collection")
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get tags for collection", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("Failed to get tags"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"tags": tags,
|
||||
})
|
||||
}
|
||||
|
||||
// GetTagsForFileHTTPHandler handles getting tags for a file
|
||||
type GetTagsForFileHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewGetTagsForFileHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *GetTagsForFileHTTPHandler {
|
||||
logger = logger.Named("GetTagsForFileHTTPHandler")
|
||||
return &GetTagsForFileHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*GetTagsForFileHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/files/{id}/tags"
|
||||
}
|
||||
|
||||
func (h *GetTagsForFileHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *GetTagsForFileHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get file ID from path
|
||||
idStr := r.PathValue("id")
|
||||
if idStr == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("File ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
fileID, err := gocql.ParseUUID(idStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid file ID", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid file ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tags for file
|
||||
tags, err := h.service.GetTagsForEntity(ctx, fileID, "file")
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get tags for file", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("Failed to get tags"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"tags": tags,
|
||||
})
|
||||
}
|
||||
73
cloud/maplefile-backend/internal/interface/http/tag/list.go
Normal file
73
cloud/maplefile-backend/internal/interface/http/tag/list.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/list.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListTagsHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewListTagsHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *ListTagsHTTPHandler {
|
||||
logger = logger.Named("ListTagsHTTPHandler")
|
||||
return &ListTagsHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ListTagsHTTPHandler) Pattern() string {
|
||||
return "GET /api/v1/tags"
|
||||
}
|
||||
|
||||
func (h *ListTagsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *ListTagsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get user ID from JWT context
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
h.logger.Error("Failed to get user ID from context")
|
||||
httperror.ResponseError(w, httperror.NewUnauthorizedError("User not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
// List tags
|
||||
tags, err := h.service.ListUserTags(ctx, userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to list tags", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("Failed to list tags"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"tags": tags,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/list_collections_by_tag.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListCollectionsByTagHandler struct {
|
||||
UseCase *tag.ListCollectionsByTagUseCase
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewListCollectionsByTagHandler(
|
||||
useCase *tag.ListCollectionsByTagUseCase,
|
||||
logger *zap.Logger,
|
||||
) *ListCollectionsByTagHandler {
|
||||
return &ListCollectionsByTagHandler{
|
||||
UseCase: useCase,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ListCollectionsByTagHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract user ID from context (set by auth middleware)
|
||||
userID, ok := ctx.Value("user_id").(gocql.UUID)
|
||||
if !ok {
|
||||
httperror.ResponseError(w, httperror.NewUnauthorizedError("user not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tags parameter (required, comma-separated UUIDs)
|
||||
tagsParam := r.URL.Query().Get("tags")
|
||||
if tagsParam == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("tags parameter is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse comma-separated tag IDs
|
||||
tagIDStrs := strings.Split(tagsParam, ",")
|
||||
if len(tagIDStrs) == 0 {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("at least one tag ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
tagIDs := make([]gocql.UUID, 0, len(tagIDStrs))
|
||||
for _, idStr := range tagIDStrs {
|
||||
id, err := gocql.ParseUUID(strings.TrimSpace(idStr))
|
||||
if err != nil {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("invalid tag ID: "+idStr))
|
||||
return
|
||||
}
|
||||
tagIDs = append(tagIDs, id)
|
||||
}
|
||||
|
||||
// Parse pagination parameters
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 50 // default
|
||||
if limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
cursor := r.URL.Query().Get("cursor")
|
||||
|
||||
// Execute multi-tag use case
|
||||
collections, nextCursor, err := h.UseCase.Execute(ctx, userID, tagIDs, limit, cursor)
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to list collections by tags",
|
||||
zap.Int("tag_count", len(tagIDs)),
|
||||
zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("failed to list collections"))
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
response := map[string]interface{}{
|
||||
"collections": collections,
|
||||
"cursor": nextCursor,
|
||||
"has_more": nextCursor != "",
|
||||
"tag_count": len(tagIDs),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/list_files_by_tag.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListFilesByTagHandler struct {
|
||||
UseCase *tag.ListFilesByTagUseCase
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewListFilesByTagHandler(
|
||||
useCase *tag.ListFilesByTagUseCase,
|
||||
logger *zap.Logger,
|
||||
) *ListFilesByTagHandler {
|
||||
return &ListFilesByTagHandler{
|
||||
UseCase: useCase,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ListFilesByTagHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract user ID from context (set by auth middleware)
|
||||
userID, ok := ctx.Value("user_id").(gocql.UUID)
|
||||
if !ok {
|
||||
httperror.ResponseError(w, httperror.NewUnauthorizedError("user not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tags parameter (required, comma-separated UUIDs)
|
||||
tagsParam := r.URL.Query().Get("tags")
|
||||
if tagsParam == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("tags parameter is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse comma-separated tag IDs
|
||||
tagIDStrs := strings.Split(tagsParam, ",")
|
||||
if len(tagIDStrs) == 0 {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("at least one tag ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
tagIDs := make([]gocql.UUID, 0, len(tagIDStrs))
|
||||
for _, idStr := range tagIDStrs {
|
||||
id, err := gocql.ParseUUID(strings.TrimSpace(idStr))
|
||||
if err != nil {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("invalid tag ID: "+idStr))
|
||||
return
|
||||
}
|
||||
tagIDs = append(tagIDs, id)
|
||||
}
|
||||
|
||||
// Parse pagination parameters
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 50 // default
|
||||
if limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
cursor := r.URL.Query().Get("cursor")
|
||||
|
||||
// Execute multi-tag use case
|
||||
files, nextCursor, err := h.UseCase.Execute(ctx, userID, tagIDs, limit, cursor)
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to list files by tags",
|
||||
zap.Int("tag_count", len(tagIDs)),
|
||||
zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("failed to list files"))
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
response := map[string]interface{}{
|
||||
"files": files,
|
||||
"cursor": nextCursor,
|
||||
"has_more": nextCursor != "",
|
||||
"tag_count": len(tagIDs),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
116
cloud/maplefile-backend/internal/interface/http/tag/provider.go
Normal file
116
cloud/maplefile-backend/internal/interface/http/tag/provider.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/provider.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
uc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag"
|
||||
)
|
||||
|
||||
// Wire providers for tag HTTP handlers
|
||||
|
||||
func ProvideCreateTagHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *CreateTagHTTPHandler {
|
||||
return NewCreateTagHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideListTagsHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *ListTagsHTTPHandler {
|
||||
return NewListTagsHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideGetTagHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *GetTagHTTPHandler {
|
||||
return NewGetTagHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideUpdateTagHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *UpdateTagHTTPHandler {
|
||||
return NewUpdateTagHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideDeleteTagHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *DeleteTagHTTPHandler {
|
||||
return NewDeleteTagHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideAssignTagHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *AssignTagHTTPHandler {
|
||||
return NewAssignTagHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideUnassignTagHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *UnassignTagHTTPHandler {
|
||||
return NewUnassignTagHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideGetTagsForCollectionHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *GetTagsForCollectionHTTPHandler {
|
||||
return NewGetTagsForCollectionHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideGetTagsForFileHTTPHandler(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
mw middleware.Middleware,
|
||||
) *GetTagsForFileHTTPHandler {
|
||||
return NewGetTagsForFileHTTPHandler(cfg, logger, service, mw)
|
||||
}
|
||||
|
||||
func ProvideListCollectionsByTagHandler(
|
||||
useCase *uc_tag.ListCollectionsByTagUseCase,
|
||||
logger *zap.Logger,
|
||||
) *ListCollectionsByTagHandler {
|
||||
return NewListCollectionsByTagHandler(useCase, logger)
|
||||
}
|
||||
|
||||
func ProvideListFilesByTagHandler(
|
||||
useCase *uc_tag.ListFilesByTagUseCase,
|
||||
logger *zap.Logger,
|
||||
) *ListFilesByTagHandler {
|
||||
return NewListFilesByTagHandler(useCase, logger)
|
||||
}
|
||||
|
||||
func ProvideSearchByTagsHandler(
|
||||
service *svc_tag.SearchByTagsService,
|
||||
logger *zap.Logger,
|
||||
mw middleware.Middleware,
|
||||
) *SearchByTagsHandler {
|
||||
return NewSearchByTagsHandler(service, logger, mw)
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/search_by_tags.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type SearchByTagsHandler struct {
|
||||
Service *svc_tag.SearchByTagsService
|
||||
Logger *zap.Logger
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewSearchByTagsHandler(
|
||||
service *svc_tag.SearchByTagsService,
|
||||
logger *zap.Logger,
|
||||
mid middleware.Middleware,
|
||||
) *SearchByTagsHandler {
|
||||
return &SearchByTagsHandler{
|
||||
Service: service,
|
||||
Logger: logger,
|
||||
middleware: mid,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SearchByTagsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *SearchByTagsHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract user ID from context (set by auth middleware)
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
httperror.ResponseError(w, httperror.NewUnauthorizedError("user not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tags parameter (required, comma-separated UUIDs)
|
||||
tagsParam := r.URL.Query().Get("tags")
|
||||
if tagsParam == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("tags parameter is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse comma-separated tag IDs
|
||||
tagIDStrs := strings.Split(tagsParam, ",")
|
||||
if len(tagIDStrs) == 0 {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("at least one tag ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
tagIDs := make([]gocql.UUID, 0, len(tagIDStrs))
|
||||
for _, idStr := range tagIDStrs {
|
||||
id, err := gocql.ParseUUID(strings.TrimSpace(idStr))
|
||||
if err != nil {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("invalid tag ID: "+idStr))
|
||||
return
|
||||
}
|
||||
tagIDs = append(tagIDs, id)
|
||||
}
|
||||
|
||||
// Parse limit parameter
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 50 // default
|
||||
if limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
// Execute search
|
||||
result, err := h.Service.Execute(ctx, &svc_tag.SearchByTagsRequest{
|
||||
UserID: userID,
|
||||
TagIDs: tagIDs,
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to search by tags",
|
||||
zap.Int("tag_count", len(tagIDs)),
|
||||
zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("failed to search by tags"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return JSON response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/unassign.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type UnassignTagHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewUnassignTagHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *UnassignTagHTTPHandler {
|
||||
logger = logger.Named("UnassignTagHTTPHandler")
|
||||
return &UnassignTagHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*UnassignTagHTTPHandler) Pattern() string {
|
||||
return "DELETE /api/v1/tags/{tagId}/entities/{entityId}"
|
||||
}
|
||||
|
||||
func (h *UnassignTagHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *UnassignTagHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get tag ID from path
|
||||
tagIDStr := r.PathValue("tagId")
|
||||
if tagIDStr == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Tag ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
tagID, err := gocql.ParseUUID(tagIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid tag ID", zap.Error(err), zap.String("id", tagIDStr))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get entity ID from path
|
||||
entityIDStr := r.PathValue("entityId")
|
||||
if entityIDStr == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Entity ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
entityID, err := gocql.ParseUUID(entityIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid entity ID", zap.Error(err), zap.String("id", entityIDStr))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid entity ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get entity type from query parameter
|
||||
entityType := r.URL.Query().Get("entity_type")
|
||||
if entityType == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Entity type is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Unassign tag
|
||||
if err := h.service.UnassignTag(ctx, tagID, entityID, entityType); err != nil {
|
||||
h.logger.Error("Failed to unassign tag",
|
||||
zap.Error(err),
|
||||
zap.String("tag_id", tagIDStr),
|
||||
zap.String("entity_id", entityIDStr),
|
||||
zap.String("entity_type", entityType),
|
||||
)
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("Failed to unassign tag"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
201
cloud/maplefile-backend/internal/interface/http/tag/update.go
Normal file
201
cloud/maplefile-backend/internal/interface/http/tag/update.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/tag/update.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
dom_crypto "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/tag"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// UpdateTagRequest contains encrypted tag data from the client (E2EE)
|
||||
type UpdateTagRequest struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
EncryptedName string `json:"encrypted_name"`
|
||||
EncryptedColor string `json:"encrypted_color"`
|
||||
EncryptedTagKey *EncryptedTagKeyDTO `json:"encrypted_tag_key"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ModifiedAt string `json:"modified_at"`
|
||||
Version uint64 `json:"version"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type UpdateTagHTTPHandler struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
service *svc_tag.TagService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewUpdateTagHTTPHandler(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
service *svc_tag.TagService,
|
||||
middleware middleware.Middleware,
|
||||
) *UpdateTagHTTPHandler {
|
||||
logger = logger.Named("UpdateTagHTTPHandler")
|
||||
return &UpdateTagHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*UpdateTagHTTPHandler) Pattern() string {
|
||||
return "PUT /api/v1/tags/{id}"
|
||||
}
|
||||
|
||||
func (h *UpdateTagHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *UpdateTagHTTPHandler) unmarshalRequest(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
) (*UpdateTagRequest, error) {
|
||||
var requestData UpdateTagRequest
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var rawJSON bytes.Buffer
|
||||
teeReader := io.TeeReader(r.Body, &rawJSON)
|
||||
|
||||
err := json.NewDecoder(teeReader).Decode(&requestData)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decode update tag request",
|
||||
zap.Error(err),
|
||||
zap.String("json", rawJSON.String()),
|
||||
)
|
||||
return nil, httperror.NewBadRequestError("Invalid request payload")
|
||||
}
|
||||
|
||||
return &requestData, nil
|
||||
}
|
||||
|
||||
func (h *UpdateTagHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get user ID from JWT context
|
||||
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
||||
if !ok {
|
||||
h.logger.Error("Failed to get user ID from context")
|
||||
httperror.ResponseError(w, httperror.NewUnauthorizedError("User not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get tag ID from path
|
||||
idStr := r.PathValue("id")
|
||||
if idStr == "" {
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Tag ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
tagID, err := gocql.ParseUUID(idStr)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid tag ID", zap.Error(err), zap.String("id", idStr))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag ID format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
req, err := h.unmarshalRequest(ctx, r)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to unmarshal request", zap.Error(err))
|
||||
httperror.ResponseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse timestamps
|
||||
createdAt, err := time.Parse(time.RFC3339, req.CreatedAt)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid created_at timestamp", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid created_at timestamp"))
|
||||
return
|
||||
}
|
||||
|
||||
modifiedAt, err := time.Parse(time.RFC3339, req.ModifiedAt)
|
||||
if err != nil {
|
||||
h.logger.Error("Invalid modified_at timestamp", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid modified_at timestamp"))
|
||||
return
|
||||
}
|
||||
|
||||
// Decode encrypted tag key
|
||||
var encryptedTagKey *dom_crypto.EncryptedTagKey
|
||||
if req.EncryptedTagKey != nil {
|
||||
// Decode ciphertext from URL-safe base64
|
||||
ciphertext, err := base64.RawURLEncoding.DecodeString(req.EncryptedTagKey.Ciphertext)
|
||||
if err != nil {
|
||||
// Fallback to standard encoding
|
||||
ciphertext, err = base64.StdEncoding.DecodeString(req.EncryptedTagKey.Ciphertext)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decode tag key ciphertext", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag key ciphertext"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Decode nonce from URL-safe base64
|
||||
nonce, err := base64.RawURLEncoding.DecodeString(req.EncryptedTagKey.Nonce)
|
||||
if err != nil {
|
||||
// Fallback to standard encoding
|
||||
nonce, err = base64.StdEncoding.DecodeString(req.EncryptedTagKey.Nonce)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decode tag key nonce", zap.Error(err))
|
||||
httperror.ResponseError(w, httperror.NewBadRequestError("Invalid tag key nonce"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encryptedTagKey = &dom_crypto.EncryptedTagKey{
|
||||
Ciphertext: ciphertext,
|
||||
Nonce: nonce,
|
||||
KeyVersion: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Create tag domain object
|
||||
tag := &dom_tag.Tag{
|
||||
ID: tagID,
|
||||
UserID: userID,
|
||||
EncryptedName: req.EncryptedName,
|
||||
EncryptedColor: req.EncryptedColor,
|
||||
EncryptedTagKey: encryptedTagKey,
|
||||
CreatedAt: createdAt,
|
||||
ModifiedAt: modifiedAt,
|
||||
Version: req.Version,
|
||||
State: req.State,
|
||||
}
|
||||
|
||||
// Update tag
|
||||
err = h.service.UpdateTag(ctx, tag)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update tag",
|
||||
zap.Error(err),
|
||||
zap.String("tag_id", tagID.String()),
|
||||
)
|
||||
httperror.ResponseError(w, httperror.NewInternalServerError("Failed to update tag"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(tag)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/user/provider.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/user"
|
||||
)
|
||||
|
||||
// ProvideUserPublicLookupHTTPHandler provides the user public lookup HTTP handler
|
||||
func ProvideUserPublicLookupHTTPHandler(
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
service svc_user.UserPublicLookupService,
|
||||
middleware middleware.Middleware,
|
||||
) *UserPublicLookupHTTPHandler {
|
||||
return NewUserPublicLookupHTTPHandler(config, logger, service, middleware)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/user/publiclookup.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
type UserPublicLookupHTTPHandler struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
service svc_user.UserPublicLookupService
|
||||
middleware middleware.Middleware
|
||||
}
|
||||
|
||||
func NewUserPublicLookupHTTPHandler(
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
service svc_user.UserPublicLookupService,
|
||||
middleware middleware.Middleware,
|
||||
) *UserPublicLookupHTTPHandler {
|
||||
logger = logger.Named("UserPublicLookupHTTPHandler")
|
||||
return &UserPublicLookupHTTPHandler{
|
||||
config: config,
|
||||
logger: logger,
|
||||
service: service,
|
||||
middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
func (*UserPublicLookupHTTPHandler) Pattern() string {
|
||||
return "GET /iam/api/v1/users/lookup"
|
||||
}
|
||||
|
||||
func (h *UserPublicLookupHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Apply middleware before handling the request
|
||||
h.middleware.Attach(h.Execute)(w, req)
|
||||
}
|
||||
|
||||
func (h *UserPublicLookupHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// 🔍 DEBUG: Log the raw query string to see what's actually received
|
||||
h.logger.Debug("🔍 Raw query string", zap.String("raw_query", r.URL.RawQuery))
|
||||
|
||||
// r.URL.Query().Get() already URL-decodes the parameter automatically
|
||||
email := r.URL.Query().Get("email")
|
||||
if email == "" {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("email", "Email parameter required"))
|
||||
return
|
||||
}
|
||||
|
||||
// 🔍 DEBUG: Log what we got from Query().Get()
|
||||
h.logger.Debug("🔍 Email from Query().Get()", zap.String("email", validation.MaskEmail(email)))
|
||||
h.logger.Debug("received email", zap.String("email", validation.MaskEmail(email)))
|
||||
|
||||
// Basic email validation
|
||||
if !strings.Contains(email, "@") {
|
||||
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("email", "Invalid email format"))
|
||||
return
|
||||
}
|
||||
|
||||
var req svc_user.UserPublicLookupRequestDTO
|
||||
req.Email = email
|
||||
|
||||
response, err := h.service.Execute(ctx, &req)
|
||||
if err != nil {
|
||||
httperror.RespondWithError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
400
cloud/maplefile-backend/internal/interface/http/wire_server.go
Normal file
400
cloud/maplefile-backend/internal/interface/http/wire_server.go
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// WireServer is a Wire-compatible HTTP server (without fx.Lifecycle dependency)
|
||||
type WireServer struct {
|
||||
server *http.Server
|
||||
logger *zap.Logger
|
||||
config *config.Config
|
||||
mux *http.ServeMux
|
||||
handlers *Handlers
|
||||
registerHandler *auth.RegisterHandler
|
||||
requestOTTHandler *auth.RequestOTTHandler
|
||||
verifyOTTHandler *auth.VerifyOTTHandler
|
||||
completeLoginHandler *auth.CompleteLoginHandler
|
||||
refreshTokenHandler *auth.RefreshTokenHandler
|
||||
verifyEmailHandler *auth.VerifyEmailHandler
|
||||
resendVerificationHandler *auth.ResendVerificationHandler
|
||||
recoveryInitiateHandler *auth.RecoveryInitiateHandler
|
||||
recoveryVerifyHandler *auth.RecoveryVerifyHandler
|
||||
recoveryCompleteHandler *auth.RecoveryCompleteHandler
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware
|
||||
securityHeadersMiddleware *middleware.SecurityHeadersMiddleware
|
||||
}
|
||||
|
||||
// NewWireServer creates a Wire-compatible HTTP server
|
||||
func NewWireServer(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
handlers *Handlers,
|
||||
registerService svc_auth.RegisterService,
|
||||
verifyEmailService svc_auth.VerifyEmailService,
|
||||
resendVerificationService svc_auth.ResendVerificationService,
|
||||
requestOTTService svc_auth.RequestOTTService,
|
||||
verifyOTTService svc_auth.VerifyOTTService,
|
||||
completeLoginService svc_auth.CompleteLoginService,
|
||||
refreshTokenService svc_auth.RefreshTokenService,
|
||||
recoveryInitiateService svc_auth.RecoveryInitiateService,
|
||||
recoveryVerifyService svc_auth.RecoveryVerifyService,
|
||||
recoveryCompleteService svc_auth.RecoveryCompleteService,
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware,
|
||||
securityHeadersMiddleware *middleware.SecurityHeadersMiddleware,
|
||||
) *WireServer {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Initialize auth handlers with services
|
||||
registerHandler := auth.NewRegisterHandler(logger, registerService)
|
||||
verifyEmailHandler := auth.NewVerifyEmailHandler(logger, verifyEmailService)
|
||||
resendVerificationHandler := auth.NewResendVerificationHandler(logger, resendVerificationService)
|
||||
requestOTTHandler := auth.NewRequestOTTHandler(logger, requestOTTService)
|
||||
verifyOTTHandler := auth.NewVerifyOTTHandler(logger, verifyOTTService)
|
||||
completeLoginHandler := auth.NewCompleteLoginHandler(logger, completeLoginService)
|
||||
refreshTokenHandler := auth.NewRefreshTokenHandler(logger, refreshTokenService)
|
||||
recoveryInitiateHandler := auth.NewRecoveryInitiateHandler(logger, recoveryInitiateService)
|
||||
recoveryVerifyHandler := auth.NewRecoveryVerifyHandler(logger, recoveryVerifyService)
|
||||
recoveryCompleteHandler := auth.NewRecoveryCompleteHandler(logger, recoveryCompleteService)
|
||||
|
||||
s := &WireServer{
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
mux: mux,
|
||||
handlers: handlers,
|
||||
registerHandler: registerHandler,
|
||||
requestOTTHandler: requestOTTHandler,
|
||||
verifyOTTHandler: verifyOTTHandler,
|
||||
completeLoginHandler: completeLoginHandler,
|
||||
refreshTokenHandler: refreshTokenHandler,
|
||||
verifyEmailHandler: verifyEmailHandler,
|
||||
resendVerificationHandler: resendVerificationHandler,
|
||||
recoveryInitiateHandler: recoveryInitiateHandler,
|
||||
recoveryVerifyHandler: recoveryVerifyHandler,
|
||||
recoveryCompleteHandler: recoveryCompleteHandler,
|
||||
rateLimitMiddleware: rateLimitMiddleware,
|
||||
securityHeadersMiddleware: securityHeadersMiddleware,
|
||||
}
|
||||
|
||||
// Register routes (simplified for Phase 2)
|
||||
s.registerRoutes()
|
||||
|
||||
// Create HTTP server with middleware
|
||||
s.server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
|
||||
Handler: s.applyMiddleware(mux),
|
||||
ReadTimeout: cfg.Server.ReadTimeout,
|
||||
WriteTimeout: cfg.Server.WriteTimeout,
|
||||
IdleTimeout: cfg.Server.IdleTimeout,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (s *WireServer) Start() error {
|
||||
s.logger.Info("Starting HTTP server",
|
||||
zap.String("address", s.server.Addr),
|
||||
zap.String("environment", s.config.App.Environment),
|
||||
)
|
||||
|
||||
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
return fmt.Errorf("HTTP server failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the HTTP server
|
||||
func (s *WireServer) Shutdown(ctx context.Context) error {
|
||||
s.logger.Info("Shutting down HTTP server")
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// registerRoutes registers all HTTP routes
|
||||
func (s *WireServer) registerRoutes() {
|
||||
s.logger.Info("Registering HTTP routes")
|
||||
|
||||
// ===== Public Routes =====
|
||||
s.mux.HandleFunc("GET /health", s.healthCheckHandler)
|
||||
s.mux.HandleFunc("GET /version", s.versionHandler)
|
||||
|
||||
// User lookup - Public route for user public key lookup
|
||||
s.mux.HandleFunc("GET /iam/api/v1/users/lookup", s.handlers.UserPublicLookup.ServeHTTP)
|
||||
|
||||
// Auth routes - Registration & Email Verification (with auth rate limiting)
|
||||
// These endpoints use general auth rate limiting to prevent automated abuse
|
||||
s.mux.HandleFunc("POST /api/v1/register", s.rateLimitMiddleware.AuthRateLimit(s.registerHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/verify-email-code", s.rateLimitMiddleware.AuthRateLimit(s.verifyEmailHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/resend-verification", s.rateLimitMiddleware.AuthRateLimit(s.resendVerificationHandler.ServeHTTP))
|
||||
|
||||
// Auth routes - Login Flow (OTT-based) (with login rate limiting)
|
||||
// These endpoints use login-specific rate limiting with account lockout
|
||||
// CWE-307: Protection against brute force attacks
|
||||
s.mux.HandleFunc("POST /api/v1/request-ott", s.rateLimitMiddleware.LoginRateLimit(s.requestOTTHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/verify-ott", s.rateLimitMiddleware.LoginRateLimit(s.verifyOTTHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/complete-login", s.rateLimitMiddleware.LoginRateLimit(s.completeLoginHandler.ServeHTTP))
|
||||
|
||||
// Auth routes - Token Management (with auth rate limiting)
|
||||
s.mux.HandleFunc("POST /api/v1/token/refresh", s.rateLimitMiddleware.AuthRateLimit(s.refreshTokenHandler.ServeHTTP))
|
||||
|
||||
// Auth routes - Account Recovery (with login rate limiting)
|
||||
// Recovery endpoints need same protection as login to prevent enumeration attacks
|
||||
s.mux.HandleFunc("POST /api/v1/recovery/initiate", s.rateLimitMiddleware.LoginRateLimit(s.recoveryInitiateHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/recovery/verify", s.rateLimitMiddleware.LoginRateLimit(s.recoveryVerifyHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/recovery/complete", s.rateLimitMiddleware.LoginRateLimit(s.recoveryCompleteHandler.ServeHTTP))
|
||||
|
||||
// ===== Protected Routes =====
|
||||
|
||||
// Me / Profile routes
|
||||
s.mux.HandleFunc("GET /api/v1/me", s.handlers.GetMe.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/me", s.handlers.UpdateMe.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/me", s.handlers.DeleteMe.ServeHTTP)
|
||||
|
||||
// Blocked Email routes
|
||||
s.mux.HandleFunc("POST /api/v1/me/blocked-emails", s.handlers.CreateBlockedEmail.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/me/blocked-emails", s.handlers.ListBlockedEmails.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/me/blocked-emails/{email}", s.handlers.DeleteBlockedEmail.ServeHTTP)
|
||||
|
||||
// Invite Email routes
|
||||
s.mux.HandleFunc("POST /api/v1/invites/send-email", s.handlers.SendInviteEmail.ServeHTTP)
|
||||
|
||||
// Dashboard
|
||||
s.mux.HandleFunc("GET /api/v1/dashboard", s.handlers.GetDashboard.ServeHTTP)
|
||||
|
||||
// Collections - Basic CRUD
|
||||
s.mux.HandleFunc("POST /api/v1/collections", s.handlers.CreateCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections", s.handlers.ListUserCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/{id}", s.handlers.GetCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}", s.handlers.UpdateCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/collections/{id}", s.handlers.SoftDeleteCollection.ServeHTTP)
|
||||
|
||||
// Collections - Hierarchical
|
||||
s.mux.HandleFunc("GET /api/v1/collections/root", s.handlers.FindRootCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/parent/{parent_id}", s.handlers.FindCollectionsByParent.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/move", s.handlers.MoveCollection.ServeHTTP)
|
||||
|
||||
// Collections - Sharing
|
||||
s.mux.HandleFunc("POST /api/v1/collections/{id}/share", s.handlers.ShareCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/collections/{id}/members/{user_id}", s.handlers.RemoveMember.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/shared", s.handlers.ListSharedCollections.ServeHTTP)
|
||||
|
||||
// Collections - Operations
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/archive", s.handlers.ArchiveCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/restore", s.handlers.RestoreCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/filtered", s.handlers.GetFilteredCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/collections/sync", s.handlers.CollectionSync.ServeHTTP)
|
||||
|
||||
// Tags - Basic CRUD (non-parameterized routes first)
|
||||
s.mux.HandleFunc("POST /api/v1/tags", s.handlers.CreateTag.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags", s.handlers.ListTags.ServeHTTP)
|
||||
|
||||
// Tags - Filtering operations (specific paths before wildcards)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/collections", s.handlers.ListCollectionsByTag.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/files", s.handlers.ListFilesByTag.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/search", s.handlers.SearchByTags.ServeHTTP)
|
||||
|
||||
// Tags - Retrieval by entity (using /for/ prefix to avoid route conflicts)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/for/collection/{collection_id}", s.handlers.GetTagsForCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/for/file/{file_id}", s.handlers.GetTagsForFile.ServeHTTP)
|
||||
|
||||
// Tags - Assignment operations (specific paths before generic {id})
|
||||
s.mux.HandleFunc("POST /api/v1/tags/{id}/assign", s.handlers.AssignTag.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/tags/{tagId}/entities/{entityId}", s.handlers.UnassignTag.ServeHTTP)
|
||||
|
||||
// Tags - Generic CRUD with {id} parameter (MUST come last to avoid conflicts)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/{id}", s.handlers.GetTag.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/tags/{id}", s.handlers.UpdateTag.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/tags/{id}", s.handlers.DeleteTag.ServeHTTP)
|
||||
|
||||
// Files - Non-parameterized routes (no wildcards)
|
||||
s.mux.HandleFunc("POST /api/v1/files/pending", s.handlers.CreatePendingFile.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/files/delete-multiple", s.handlers.DeleteMultipleFiles.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/files/recent", s.handlers.ListRecentFiles.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/files/sync", s.handlers.FileSync.ServeHTTP)
|
||||
|
||||
// Files - Parameterized routes under /file/ prefix (singular) to avoid conflicts
|
||||
s.mux.HandleFunc("POST /api/v1/file/{id}/complete", s.handlers.CompleteFileUpload.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/file/{id}/download-completed", s.handlers.ReportDownloadCompleted.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}/archive", s.handlers.ArchiveFile.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}/restore", s.handlers.RestoreFile.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}/upload-url", s.handlers.GetPresignedUploadURL.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}/download-url", s.handlers.GetPresignedDownloadURL.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}", s.handlers.GetFile.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}", s.handlers.UpdateFile.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/file/{id}", s.handlers.SoftDeleteFile.ServeHTTP)
|
||||
|
||||
// Files by collection - under /collection/ prefix
|
||||
s.mux.HandleFunc("GET /api/v1/collection/{collection_id}/files", s.handlers.ListFilesByCollection.ServeHTTP)
|
||||
|
||||
s.logger.Info("HTTP routes registered", zap.Int("total_routes", 71))
|
||||
}
|
||||
|
||||
// Health check handler
|
||||
func (s *WireServer) healthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"healthy","service":"maplefile-backend","di":"Wire"}`))
|
||||
}
|
||||
|
||||
// Version handler
|
||||
func (s *WireServer) versionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
version := fmt.Sprintf(`{"version":"%s","environment":"%s","di":"Wire"}`,
|
||||
s.config.App.Version,
|
||||
s.config.App.Environment)
|
||||
w.Write([]byte(version))
|
||||
}
|
||||
|
||||
// Middleware implementations
|
||||
|
||||
// applyMiddleware applies global middleware to the handler
|
||||
func (s *WireServer) applyMiddleware(handler http.Handler) http.Handler {
|
||||
// Apply middleware in reverse order (last applied is executed first)
|
||||
|
||||
// Logging middleware (outermost)
|
||||
handler = s.loggingMiddleware(handler)
|
||||
|
||||
// CORS middleware
|
||||
handler = s.corsMiddleware(handler)
|
||||
|
||||
// Security headers middleware (adds security headers to all responses)
|
||||
handler = s.securityHeadersMiddleware.Handler(handler)
|
||||
|
||||
// Recovery middleware (catches panics)
|
||||
handler = s.recoveryMiddleware(handler)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (s *WireServer) loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip logging health check requests
|
||||
if r.URL.Path == "/health" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Simple logging for Wire version
|
||||
s.logger.Info("HTTP request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", validation.MaskIP(r.RemoteAddr)),
|
||||
)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WireServer) corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the origin from the request
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// Build allowed origins map
|
||||
allowedOrigins := make(map[string]bool)
|
||||
|
||||
// In development, always allow localhost origins
|
||||
if s.config.App.Environment == "development" {
|
||||
allowedOrigins["http://localhost:5173"] = true // Vite dev server
|
||||
allowedOrigins["http://localhost:5174"] = true // Alternative Vite port
|
||||
allowedOrigins["http://localhost:3000"] = true // Common React port
|
||||
allowedOrigins["http://127.0.0.1:5173"] = true
|
||||
allowedOrigins["http://127.0.0.1:5174"] = true
|
||||
allowedOrigins["http://127.0.0.1:3000"] = true
|
||||
}
|
||||
|
||||
// Add production origins from configuration
|
||||
for _, allowedOrigin := range s.config.Security.AllowedOrigins {
|
||||
if allowedOrigin != "" {
|
||||
allowedOrigins[allowedOrigin] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the request origin is allowed
|
||||
if allowedOrigins[origin] {
|
||||
// SECURITY FIX: Validate origin before setting CORS headers
|
||||
// CWE-942: Permissive Cross-domain Policy with Untrusted Domains
|
||||
// OWASP A05:2021: Security Misconfiguration - Secure CORS configuration
|
||||
|
||||
// Prevent wildcard origin with credentials (major security risk)
|
||||
if origin == "*" {
|
||||
s.logger.Error("CRITICAL: Wildcard origin (*) cannot be used with credentials",
|
||||
zap.String("path", r.URL.Path))
|
||||
// Don't set CORS headers for wildcard - this is a misconfiguration
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// In production, enforce HTTPS origins for security
|
||||
if s.config.App.Environment == "production" {
|
||||
if len(origin) >= 5 && origin[:5] == "http:" {
|
||||
s.logger.Warn("Non-HTTPS origin rejected in production",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path))
|
||||
// Don't set CORS headers for non-HTTPS origins in production
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set CORS headers for validated origins
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// Only set credentials for specific, non-wildcard origins
|
||||
// This prevents credential leakage to untrusted domains
|
||||
if origin != "*" && origin != "" {
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
w.Header().Set("Access-Control-Max-Age", "3600") // Cache preflight for 1 hour
|
||||
|
||||
s.logger.Debug("CORS headers added",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Bool("credentials_allowed", origin != "*"))
|
||||
} else if origin != "" {
|
||||
// Log rejected origins for debugging
|
||||
s.logger.Warn("CORS request from disallowed origin",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path))
|
||||
}
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WireServer) recoveryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
s.logger.Error("Panic recovered",
|
||||
zap.Any("error", err),
|
||||
zap.String("path", r.URL.Path),
|
||||
)
|
||||
problem := httperror.NewInternalServerError("An unexpected error occurred")
|
||||
problem.WithInstance(r.URL.Path)
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue