Adds blog series field to group related posts together. Implements v2 API endpoints for listing, creating, updating, and deleting blog posts with proper error codes. Adds series filtering to explore page and sitemap support with pagination. Includes BlogPostV2 structs with author/repo references, HTML URLs, and content rendering. Updates editor UI with series input field.
323 lines
12 KiB
Go
323 lines
12 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package errors
|
|
|
|
import "net/http"
|
|
|
|
// ErrorCode represents a machine-readable error code
|
|
type ErrorCode string
|
|
|
|
// Authentication errors (AUTH_)
|
|
const (
|
|
AuthTokenMissing ErrorCode = "AUTH_TOKEN_MISSING"
|
|
AuthTokenInvalid ErrorCode = "AUTH_TOKEN_INVALID"
|
|
AuthTokenExpired ErrorCode = "AUTH_TOKEN_EXPIRED"
|
|
AuthScopeInsufficient ErrorCode = "AUTH_SCOPE_INSUFFICIENT"
|
|
Auth2FARequired ErrorCode = "AUTH_2FA_REQUIRED"
|
|
AuthInvalidCredentials ErrorCode = "AUTH_INVALID_CREDENTIALS"
|
|
)
|
|
|
|
// Permission errors (PERM_)
|
|
const (
|
|
PermRepoReadDenied ErrorCode = "PERM_REPO_READ_DENIED"
|
|
PermRepoWriteDenied ErrorCode = "PERM_REPO_WRITE_DENIED"
|
|
PermRepoAdminRequired ErrorCode = "PERM_REPO_ADMIN_REQUIRED"
|
|
PermOrgMemberRequired ErrorCode = "PERM_ORG_MEMBER_REQUIRED"
|
|
PermOrgAdminRequired ErrorCode = "PERM_ORG_ADMIN_REQUIRED"
|
|
PermActionDenied ErrorCode = "PERM_ACTION_DENIED"
|
|
)
|
|
|
|
// Repository errors (REPO_)
|
|
const (
|
|
RepoNotFound ErrorCode = "REPO_NOT_FOUND"
|
|
RepoArchived ErrorCode = "REPO_ARCHIVED"
|
|
RepoDisabled ErrorCode = "REPO_DISABLED"
|
|
RepoTransferPending ErrorCode = "REPO_TRANSFER_PENDING"
|
|
RepoEmpty ErrorCode = "REPO_EMPTY"
|
|
RepoAlreadyExists ErrorCode = "REPO_ALREADY_EXISTS"
|
|
)
|
|
|
|
// File errors (FILE_)
|
|
const (
|
|
FileNotFound ErrorCode = "FILE_NOT_FOUND"
|
|
FileTooLarge ErrorCode = "FILE_TOO_LARGE"
|
|
FileConflict ErrorCode = "FILE_CONFLICT"
|
|
FileBinary ErrorCode = "FILE_BINARY"
|
|
FileTypeError ErrorCode = "FILE_TYPE_NOT_ALLOWED"
|
|
)
|
|
|
|
// Git errors (GIT_)
|
|
const (
|
|
GitRefNotFound ErrorCode = "GIT_REF_NOT_FOUND"
|
|
GitMergeConflict ErrorCode = "GIT_MERGE_CONFLICT"
|
|
GitBranchNotFound ErrorCode = "GIT_BRANCH_NOT_FOUND"
|
|
GitTagNotFound ErrorCode = "GIT_TAG_NOT_FOUND"
|
|
GitCommitNotFound ErrorCode = "GIT_COMMIT_NOT_FOUND"
|
|
GitPushRejected ErrorCode = "GIT_PUSH_REJECTED"
|
|
)
|
|
|
|
// Rate limiting errors (RATE_)
|
|
const (
|
|
RateLimitExceeded ErrorCode = "RATE_LIMIT_EXCEEDED"
|
|
RateQuotaExhausted ErrorCode = "RATE_QUOTA_EXHAUSTED"
|
|
)
|
|
|
|
// Validation errors (VAL_)
|
|
const (
|
|
ValInvalidInput ErrorCode = "VAL_INVALID_INPUT"
|
|
ValMissingField ErrorCode = "VAL_MISSING_FIELD"
|
|
ValInvalidName ErrorCode = "VAL_INVALID_NAME"
|
|
ValNameTooLong ErrorCode = "VAL_NAME_TOO_LONG"
|
|
ValInvalidEmail ErrorCode = "VAL_INVALID_EMAIL"
|
|
ValDuplicateName ErrorCode = "VAL_DUPLICATE_NAME"
|
|
ValInvalidFormat ErrorCode = "VAL_INVALID_FORMAT"
|
|
ValidationFailed ErrorCode = "VALIDATION_FAILED"
|
|
)
|
|
|
|
// General errors
|
|
const (
|
|
InternalError ErrorCode = "INTERNAL_ERROR"
|
|
PermAccessDenied ErrorCode = "ACCESS_DENIED"
|
|
RefNotFound ErrorCode = "REF_NOT_FOUND"
|
|
)
|
|
|
|
// Upload errors (UPLOAD_)
|
|
const (
|
|
UploadSessionNotFound ErrorCode = "UPLOAD_SESSION_NOT_FOUND"
|
|
UploadSessionExpired ErrorCode = "UPLOAD_SESSION_EXPIRED"
|
|
UploadChunkInvalid ErrorCode = "UPLOAD_CHUNK_INVALID"
|
|
UploadChunkSizeMismatch ErrorCode = "UPLOAD_CHUNK_SIZE_MISMATCH"
|
|
UploadChecksumMismatch ErrorCode = "UPLOAD_CHECKSUM_MISMATCH"
|
|
UploadIncomplete ErrorCode = "UPLOAD_INCOMPLETE"
|
|
UploadFileTooLarge ErrorCode = "UPLOAD_FILE_TOO_LARGE"
|
|
)
|
|
|
|
// Resource errors (RESOURCE_)
|
|
const (
|
|
ResourceNotFound ErrorCode = "RESOURCE_NOT_FOUND"
|
|
ResourceConflict ErrorCode = "RESOURCE_CONFLICT"
|
|
ResourceGone ErrorCode = "RESOURCE_GONE"
|
|
)
|
|
|
|
// Server errors (SERVER_)
|
|
const (
|
|
ServerInternal ErrorCode = "SERVER_INTERNAL_ERROR"
|
|
ServerUnavailable ErrorCode = "SERVER_UNAVAILABLE"
|
|
ServerTimeout ErrorCode = "SERVER_TIMEOUT"
|
|
)
|
|
|
|
// User errors (USER_)
|
|
const (
|
|
UserNotFound ErrorCode = "USER_NOT_FOUND"
|
|
UserAlreadyExists ErrorCode = "USER_ALREADY_EXISTS"
|
|
UserInactive ErrorCode = "USER_INACTIVE"
|
|
UserProhibitLogin ErrorCode = "USER_PROHIBIT_LOGIN"
|
|
)
|
|
|
|
// Organization errors (ORG_)
|
|
const (
|
|
OrgNotFound ErrorCode = "ORG_NOT_FOUND"
|
|
OrgAlreadyExists ErrorCode = "ORG_ALREADY_EXISTS"
|
|
)
|
|
|
|
// Issue errors (ISSUE_)
|
|
const (
|
|
IssueNotFound ErrorCode = "ISSUE_NOT_FOUND"
|
|
IssueClosed ErrorCode = "ISSUE_CLOSED"
|
|
IssueLocked ErrorCode = "ISSUE_LOCKED"
|
|
)
|
|
|
|
// Pull Request errors (PR_)
|
|
const (
|
|
PRNotFound ErrorCode = "PR_NOT_FOUND"
|
|
PRAlreadyMerged ErrorCode = "PR_ALREADY_MERGED"
|
|
PRNotMergeable ErrorCode = "PR_NOT_MERGEABLE"
|
|
PRWorkInProgress ErrorCode = "PR_WORK_IN_PROGRESS"
|
|
)
|
|
|
|
// Release errors (RELEASE_)
|
|
const (
|
|
ReleaseNotFound ErrorCode = "RELEASE_NOT_FOUND"
|
|
ReleaseTagExists ErrorCode = "RELEASE_TAG_EXISTS"
|
|
ReleaseIsDraft ErrorCode = "RELEASE_IS_DRAFT"
|
|
)
|
|
|
|
// Webhook errors (WEBHOOK_)
|
|
const (
|
|
WebhookNotFound ErrorCode = "WEBHOOK_NOT_FOUND"
|
|
WebhookDeliveryFail ErrorCode = "WEBHOOK_DELIVERY_FAILED"
|
|
)
|
|
|
|
// Wiki errors (WIKI_)
|
|
const (
|
|
WikiPageNotFound ErrorCode = "WIKI_PAGE_NOT_FOUND"
|
|
WikiPageAlreadyExists ErrorCode = "WIKI_PAGE_ALREADY_EXISTS"
|
|
WikiReservedName ErrorCode = "WIKI_RESERVED_NAME"
|
|
WikiDisabled ErrorCode = "WIKI_DISABLED"
|
|
)
|
|
|
|
// Blog errors (BLOG_)
|
|
const (
|
|
BlogPostNotFound ErrorCode = "BLOG_POST_NOT_FOUND"
|
|
BlogDisabled ErrorCode = "BLOG_DISABLED"
|
|
)
|
|
|
|
// errorInfo contains metadata about an error code
|
|
type errorInfo struct {
|
|
Message string
|
|
HTTPStatus int
|
|
}
|
|
|
|
// errorCatalog maps error codes to their metadata
|
|
var errorCatalog = map[ErrorCode]errorInfo{
|
|
// Auth errors
|
|
AuthTokenMissing: {"No authentication token provided", http.StatusUnauthorized},
|
|
AuthTokenInvalid: {"Token is malformed or invalid", http.StatusUnauthorized},
|
|
AuthTokenExpired: {"Token has expired", http.StatusUnauthorized},
|
|
AuthScopeInsufficient: {"Token lacks required scope", http.StatusForbidden},
|
|
Auth2FARequired: {"Two-factor authentication required", http.StatusUnauthorized},
|
|
AuthInvalidCredentials: {"Invalid username or password", http.StatusUnauthorized},
|
|
|
|
// Permission errors
|
|
PermRepoReadDenied: {"Cannot read repository", http.StatusForbidden},
|
|
PermRepoWriteDenied: {"Cannot write to repository", http.StatusForbidden},
|
|
PermRepoAdminRequired: {"Repository admin access required", http.StatusForbidden},
|
|
PermOrgMemberRequired: {"Must be organization member", http.StatusForbidden},
|
|
PermOrgAdminRequired: {"Organization admin access required", http.StatusForbidden},
|
|
PermActionDenied: {"Permission denied for this action", http.StatusForbidden},
|
|
|
|
// Repository errors
|
|
RepoNotFound: {"Repository does not exist", http.StatusNotFound},
|
|
RepoArchived: {"Repository is archived", http.StatusForbidden},
|
|
RepoDisabled: {"Repository is disabled", http.StatusForbidden},
|
|
RepoTransferPending: {"Repository has pending transfer", http.StatusConflict},
|
|
RepoEmpty: {"Repository is empty", http.StatusUnprocessableEntity},
|
|
RepoAlreadyExists: {"Repository already exists", http.StatusConflict},
|
|
|
|
// File errors
|
|
FileNotFound: {"File does not exist", http.StatusNotFound},
|
|
FileTooLarge: {"File exceeds size limit", http.StatusRequestEntityTooLarge},
|
|
FileConflict: {"File was modified (SHA mismatch)", http.StatusConflict},
|
|
FileBinary: {"Cannot perform text operation on binary file", http.StatusBadRequest},
|
|
FileTypeError: {"File type not allowed", http.StatusBadRequest},
|
|
|
|
// Git errors
|
|
GitRefNotFound: {"Git reference not found", http.StatusNotFound},
|
|
GitMergeConflict: {"Merge conflict detected", http.StatusConflict},
|
|
GitBranchNotFound: {"Branch not found", http.StatusNotFound},
|
|
GitTagNotFound: {"Tag not found", http.StatusNotFound},
|
|
GitCommitNotFound: {"Commit not found", http.StatusNotFound},
|
|
GitPushRejected: {"Push rejected", http.StatusForbidden},
|
|
|
|
// Rate limiting errors
|
|
RateLimitExceeded: {"API rate limit exceeded", http.StatusTooManyRequests},
|
|
RateQuotaExhausted: {"Rate quota exhausted", http.StatusTooManyRequests},
|
|
|
|
// Validation errors
|
|
ValInvalidInput: {"Invalid input provided", http.StatusBadRequest},
|
|
ValMissingField: {"Required field is missing", http.StatusBadRequest},
|
|
ValInvalidName: {"Name contains invalid characters", http.StatusBadRequest},
|
|
ValNameTooLong: {"Name exceeds maximum length", http.StatusBadRequest},
|
|
ValInvalidEmail: {"Invalid email address", http.StatusBadRequest},
|
|
ValDuplicateName: {"Name already exists", http.StatusConflict},
|
|
ValInvalidFormat: {"Invalid format", http.StatusBadRequest},
|
|
ValidationFailed: {"Validation failed", http.StatusBadRequest},
|
|
|
|
// General errors
|
|
InternalError: {"Internal server error", http.StatusInternalServerError},
|
|
PermAccessDenied: {"Access denied", http.StatusForbidden},
|
|
RefNotFound: {"Reference not found", http.StatusNotFound},
|
|
|
|
// Upload errors
|
|
UploadSessionNotFound: {"Upload session does not exist", http.StatusNotFound},
|
|
UploadSessionExpired: {"Upload session has expired", http.StatusGone},
|
|
UploadChunkInvalid: {"Chunk number out of range", http.StatusBadRequest},
|
|
UploadChunkSizeMismatch: {"Chunk size doesn't match expected", http.StatusBadRequest},
|
|
UploadChecksumMismatch: {"File checksum verification failed", http.StatusBadRequest},
|
|
UploadIncomplete: {"Not all chunks have been uploaded", http.StatusBadRequest},
|
|
UploadFileTooLarge: {"File exceeds maximum upload size", http.StatusRequestEntityTooLarge},
|
|
|
|
// Resource errors
|
|
ResourceNotFound: {"Resource not found", http.StatusNotFound},
|
|
ResourceConflict: {"Resource conflict", http.StatusConflict},
|
|
ResourceGone: {"Resource no longer available", http.StatusGone},
|
|
|
|
// Server errors
|
|
ServerInternal: {"Internal server error", http.StatusInternalServerError},
|
|
ServerUnavailable: {"Service temporarily unavailable", http.StatusServiceUnavailable},
|
|
ServerTimeout: {"Request timeout", http.StatusGatewayTimeout},
|
|
|
|
// User errors
|
|
UserNotFound: {"User not found", http.StatusNotFound},
|
|
UserAlreadyExists: {"User already exists", http.StatusConflict},
|
|
UserInactive: {"User account is inactive", http.StatusForbidden},
|
|
UserProhibitLogin: {"User is not allowed to login", http.StatusForbidden},
|
|
|
|
// Organization errors
|
|
OrgNotFound: {"Organization not found", http.StatusNotFound},
|
|
OrgAlreadyExists: {"Organization already exists", http.StatusConflict},
|
|
|
|
// Issue errors
|
|
IssueNotFound: {"Issue not found", http.StatusNotFound},
|
|
IssueClosed: {"Issue is closed", http.StatusUnprocessableEntity},
|
|
IssueLocked: {"Issue is locked", http.StatusForbidden},
|
|
|
|
// Pull Request errors
|
|
PRNotFound: {"Pull request not found", http.StatusNotFound},
|
|
PRAlreadyMerged: {"Pull request already merged", http.StatusConflict},
|
|
PRNotMergeable: {"Pull request is not mergeable", http.StatusConflict},
|
|
PRWorkInProgress: {"Pull request is marked as work in progress", http.StatusUnprocessableEntity},
|
|
|
|
// Release errors
|
|
ReleaseNotFound: {"Release not found", http.StatusNotFound},
|
|
ReleaseTagExists: {"Release tag already exists", http.StatusConflict},
|
|
ReleaseIsDraft: {"Release is a draft", http.StatusUnprocessableEntity},
|
|
|
|
// Webhook errors
|
|
WebhookNotFound: {"Webhook not found", http.StatusNotFound},
|
|
WebhookDeliveryFail: {"Webhook delivery failed", http.StatusBadGateway},
|
|
|
|
// Wiki errors
|
|
WikiPageNotFound: {"Wiki page not found", http.StatusNotFound},
|
|
WikiPageAlreadyExists: {"Wiki page already exists", http.StatusConflict},
|
|
WikiReservedName: {"Wiki page name is reserved", http.StatusBadRequest},
|
|
WikiDisabled: {"Wiki is disabled for this repository", http.StatusForbidden},
|
|
|
|
// Blog errors
|
|
BlogPostNotFound: {"Blog post not found", http.StatusNotFound},
|
|
BlogDisabled: {"Blogs are disabled", http.StatusForbidden},
|
|
}
|
|
|
|
// Message returns the human-readable message for an error code
|
|
func (e ErrorCode) Message() string {
|
|
if info, ok := errorCatalog[e]; ok {
|
|
return info.Message
|
|
}
|
|
return string(e)
|
|
}
|
|
|
|
// HTTPStatus returns the HTTP status code for an error code
|
|
func (e ErrorCode) HTTPStatus() int {
|
|
if info, ok := errorCatalog[e]; ok {
|
|
return info.HTTPStatus
|
|
}
|
|
return http.StatusInternalServerError
|
|
}
|
|
|
|
// String returns the error code as a string
|
|
func (e ErrorCode) String() string {
|
|
return string(e)
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e ErrorCode) Error() string {
|
|
return e.Message()
|
|
}
|
|
|
|
// IsValid returns true if the error code is registered in the catalog
|
|
func (e ErrorCode) IsValid() bool {
|
|
_, ok := errorCatalog[e]
|
|
return ok
|
|
}
|