full stack second commit

This commit is contained in:
Leon Bösche
2025-12-18 00:11:30 +01:00
parent b35adc3d06
commit 87ee5f2ae3
16 changed files with 472 additions and 99 deletions

View File

@@ -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

View 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)
}

View File

@@ -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
}

View File

@@ -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
}