full stack second commit
This commit is contained in:
@@ -11,4 +11,12 @@ OIDC_CLIENT_ID=your_client_id
|
||||
OIDC_CLIENT_SECRET=your_client_secret
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_jwt_secret_key
|
||||
JWT_SECRET=your_jwt_secret_key
|
||||
|
||||
# Nextcloud
|
||||
NEXTCLOUD_URL=https://storage.b0esche.cloud
|
||||
NEXTCLOUD_USERNAME=admin
|
||||
NEXTCLOUD_PASSWORD=your_password
|
||||
|
||||
# Collabora
|
||||
COLLABORA_URL=https://office.b0esche.cloud
|
||||
73
go_cloud/internal/errors/errors.go
Normal file
73
go_cloud/internal/errors/errors.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ErrorCode represents standardized error codes
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
CodeUnauthenticated ErrorCode = "UNAUTHENTICATED"
|
||||
CodePermissionDenied ErrorCode = "PERMISSION_DENIED"
|
||||
CodeNotFound ErrorCode = "NOT_FOUND"
|
||||
CodeConflict ErrorCode = "CONFLICT"
|
||||
CodeInvalidArgument ErrorCode = "INVALID_ARGUMENT"
|
||||
CodeInternal ErrorCode = "INTERNAL"
|
||||
)
|
||||
|
||||
// ErrorResponse represents the JSON error response structure
|
||||
type ErrorResponse struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// WriteError writes a standardized JSON error response
|
||||
func WriteError(w http.ResponseWriter, code ErrorCode, message string, status int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// GetRequestID extracts the request ID from the request context
|
||||
func GetRequestID(r *http.Request) string {
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
return reqID
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// GetUserID extracts user ID from context if available
|
||||
func GetUserID(r *http.Request) string {
|
||||
if userID := r.Context().Value("user"); userID != nil {
|
||||
if uid, ok := userID.(string); ok {
|
||||
return uid
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetOrgID extracts org ID from context if available
|
||||
func GetOrgID(r *http.Request) string {
|
||||
if orgID := r.Context().Value("org"); orgID != nil {
|
||||
if oid, ok := orgID.(uuid.UUID); ok {
|
||||
return oid.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LogError logs an error with context
|
||||
func LogError(r *http.Request, err error, message string) {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] req_id=%s user_id=%s org_id=%s %s: %v\n",
|
||||
GetRequestID(r), GetUserID(r), GetOrgID(r), message, err)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"go.b0esche.cloud/backend/internal/auth"
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"go.b0esche.cloud/backend/internal/errors"
|
||||
"go.b0esche.cloud/backend/internal/middleware"
|
||||
"go.b0esche.cloud/backend/internal/org"
|
||||
"go.b0esche.cloud/backend/internal/permission"
|
||||
@@ -98,7 +99,8 @@ func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func authLoginHandler(w http.ResponseWriter, r *http.Request, authService *auth.Service) {
|
||||
state, err := auth.GenerateState()
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Failed to generate state")
|
||||
errors.WriteError(w, errors.CodeInternal, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,14 +123,16 @@ func authCallbackHandler(w http.ResponseWriter, r *http.Request, cfg *config.Con
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
http.Error(w, "Authentication failed", http.StatusUnauthorized)
|
||||
errors.LogError(r, err, "Authentication failed")
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Authentication failed", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user orgs
|
||||
orgs, err := org.ResolveUserOrgs(r.Context(), db, user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Failed to resolve user orgs")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
orgIDs := make([]string, len(orgs))
|
||||
@@ -138,7 +142,8 @@ func authCallbackHandler(w http.ResponseWriter, r *http.Request, cfg *config.Con
|
||||
|
||||
token, err := jwtManager.Generate(user.Email, orgIDs, session.ID.String())
|
||||
if err != nil {
|
||||
http.Error(w, "Token generation failed", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Token generation failed")
|
||||
errors.WriteError(w, errors.CodeInternal, "Token generation failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -155,21 +160,23 @@ func authCallbackHandler(w http.ResponseWriter, r *http.Request, cfg *config.Con
|
||||
func refreshHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Manager, db *database.DB) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
claims, session, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
errors.LogError(r, err, "Invalid token")
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userID, _ := uuid.Parse(claims.UserID)
|
||||
orgs, err := db.GetUserOrganizations(r.Context(), userID)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Failed to get user organizations")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
orgIDs := make([]string, len(orgs))
|
||||
@@ -179,7 +186,8 @@ func refreshHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Mana
|
||||
|
||||
newToken, err := jwtManager.Generate(claims.UserID, orgIDs, session.ID.String())
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Token generation failed")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,21 +198,23 @@ func refreshHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Mana
|
||||
func listOrgsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
claims, _, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
errors.LogError(r, err, "Invalid token")
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userID, _ := uuid.Parse(claims.UserID)
|
||||
orgs, err := org.ResolveUserOrgs(r.Context(), db, userID)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Failed to resolve user orgs")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -215,14 +225,15 @@ func listOrgsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jw
|
||||
func createOrgHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, jwtManager *jwt.Manager) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
claims, _, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
errors.LogError(r, err, "Invalid token")
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -233,13 +244,14 @@ func createOrgHandler(w http.ResponseWriter, r *http.Request, db *database.DB, a
|
||||
Slug string `json:"slug,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
org, err := org.CreateOrg(r.Context(), db, userID, req.Name, req.Slug)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Failed to create org")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -283,17 +295,17 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, audi
|
||||
session := struct {
|
||||
ViewUrl string `json:"viewUrl"`
|
||||
Capabilities struct {
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanAnnotate bool `json:"canAnnotate"`
|
||||
IsPdf bool `json:"isPdf"`
|
||||
IsPdf bool `json:"isPdf"`
|
||||
} `json:"capabilities"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
}{
|
||||
ViewUrl: "https://view.example.com/" + fileId,
|
||||
Capabilities: struct {
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanAnnotate bool `json:"canAnnotate"`
|
||||
IsPdf bool `json:"isPdf"`
|
||||
IsPdf bool `json:"isPdf"`
|
||||
}{CanEdit: true, CanAnnotate: true, IsPdf: true},
|
||||
ExpiresAt: "2023-01-01T01:00:00Z",
|
||||
}
|
||||
@@ -325,10 +337,6 @@ func editorHandler(w http.ResponseWriter, r *http.Request, db *database.DB, audi
|
||||
json.NewEncoder(w).Encode(session)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(session)
|
||||
}
|
||||
|
||||
func annotationsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
|
||||
userIDStr := r.Context().Value("user").(string)
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
@@ -341,7 +349,7 @@ func annotationsHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
|
||||
BaseVersionId string `json:"baseVersionId"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -364,7 +372,8 @@ func activityHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
|
||||
|
||||
activities, err := db.GetOrgActivities(r.Context(), orgID, 50)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Failed to get org activities")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -377,7 +386,8 @@ func listMembersHandler(w http.ResponseWriter, r *http.Request, db *database.DB)
|
||||
|
||||
members, err := db.GetOrgMembers(r.Context(), orgID)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Failed to get org members")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -390,7 +400,7 @@ func updateMemberRoleHandler(w http.ResponseWriter, r *http.Request, db *databas
|
||||
userIDStr := chi.URLParam(r, "userId")
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID", http.StatusBadRequest)
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -398,12 +408,13 @@ func updateMemberRoleHandler(w http.ResponseWriter, r *http.Request, db *databas
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.UpdateMemberRole(r.Context(), orgID, userID, req.Role); err != nil {
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
errors.LogError(r, err, "Failed to update member role")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"go.b0esche.cloud/backend/internal/errors"
|
||||
"go.b0esche.cloud/backend/internal/org"
|
||||
"go.b0esche.cloud/backend/internal/permission"
|
||||
"go.b0esche.cloud/backend/pkg/jwt"
|
||||
@@ -73,7 +74,7 @@ func Org(db *database.DB, auditLogger *audit.Logger) func(http.Handler) http.Han
|
||||
}
|
||||
orgID, err := uuid.Parse(orgIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid org ID", http.StatusBadRequest)
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Invalid org ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,7 +86,21 @@ func Org(db *database.DB, auditLogger *audit.Logger) func(http.Handler) http.Han
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"org_id": orgID, "error": err.Error()},
|
||||
})
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
errors.LogError(r, err, "Org access denied")
|
||||
errors.WriteError(w, errors.CodePermissionDenied, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = org.CheckMembership(r.Context(), db, userID, orgID)
|
||||
if err != nil {
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
Action: "org_access",
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"org_id": orgID, "error": err.Error()},
|
||||
})
|
||||
errors.LogError(r, err, "Org access denied")
|
||||
errors.WriteError(w, errors.CodePermissionDenied, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,7 +128,8 @@ func Permission(db *database.DB, auditLogger *audit.Logger, perm permission.Perm
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"permission": perm},
|
||||
})
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
errors.LogError(r, err, "Permission denied")
|
||||
errors.WriteError(w, errors.CodePermissionDenied, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user