feat(api): add API documentation and ShareX integration

- Add an API documentation page with curl and ShareX examples.
- Implement a dynamic ShareX configuration endpoint (`/api/v1/sharex/warpbox-anonymous.sxcu`) that generates a `.sxcu` file pre-configured with the instance's base URL.
- Update anonymous uploads to return a private management link (`manageUrl`) and a deletion link (`deleteUrl`) in JSON responses.
- Update README with details on Stage 3 Anonymous Integrations.
- Add styling for the new API documentation view and management details.
This commit is contained in:
2026-05-29 23:44:05 +03:00
parent 74ede000b4
commit 3471e2b0cf
19 changed files with 1231 additions and 46 deletions

View File

@@ -40,15 +40,16 @@ type UploadOptions struct {
}
type Box struct {
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
MaxDownloads int `json:"maxDownloads"`
DownloadCount int `json:"downloadCount"`
PasswordSalt string `json:"passwordSalt,omitempty"`
PasswordHash string `json:"passwordHash,omitempty"`
Obfuscate bool `json:"obfuscate"`
Files []File `json:"files"`
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
MaxDownloads int `json:"maxDownloads"`
DownloadCount int `json:"downloadCount"`
PasswordSalt string `json:"passwordSalt,omitempty"`
PasswordHash string `json:"passwordHash,omitempty"`
DeleteTokenHash string `json:"deleteTokenHash,omitempty"`
Obfuscate bool `json:"obfuscate"`
Files []File `json:"files"`
}
type File struct {
@@ -66,6 +67,8 @@ type UploadResult struct {
BoxID string `json:"boxId"`
BoxURL string `json:"boxUrl"`
ZipURL string `json:"zipUrl"`
ManageURL string `json:"manageUrl"`
DeleteURL string `json:"deleteUrl"`
ExpiresAt string `json:"expiresAt"`
Files []ResultFile `json:"files"`
}
@@ -169,6 +172,8 @@ func (s *UploadService) CreateBox(files []*multipart.FileHeader, opts UploadOpti
Obfuscate: opts.ObfuscateMetadata && strings.TrimSpace(opts.Password) != "",
Files: make([]File, 0, len(files)),
}
deleteToken := randomID(32)
box.DeleteTokenHash = deleteTokenHash(box.ID, deleteToken)
if strings.TrimSpace(opts.Password) != "" {
salt, hash := hashPassword(opts.Password)
box.PasswordSalt = salt
@@ -227,7 +232,7 @@ func (s *UploadService) CreateBox(files []*multipart.FileHeader, opts UploadOpti
"file_count", len(box.Files),
)
return s.resultForBox(box), nil
return s.resultForBox(box, deleteToken), nil
}
func (s *UploadService) GetBox(id string) (Box, error) {
@@ -327,6 +332,17 @@ func (s *UploadService) DeleteBox(boxID string) error {
return s.DeleteBoxWithSource(boxID, "admin")
}
func (s *UploadService) DeleteBoxWithToken(boxID, token string) error {
box, err := s.GetBox(boxID)
if err != nil {
return err
}
if !s.VerifyDeleteToken(box, token) {
return os.ErrPermission
}
return s.DeleteBoxWithSource(boxID, "anonymous-delete")
}
func (s *UploadService) DeleteBoxWithSource(boxID, source string) error {
if err := s.db.Update(func(tx *bbolt.Tx) error {
return tx.Bucket(boxesBucket).Delete([]byte(boxID))
@@ -381,6 +397,14 @@ func (s *UploadService) UnlockToken(box Box) string {
return hex.EncodeToString(sum[:])
}
func (s *UploadService) VerifyDeleteToken(box Box, token string) bool {
if box.DeleteTokenHash == "" || strings.TrimSpace(token) == "" {
return false
}
hash := deleteTokenHash(box.ID, token)
return subtle.ConstantTimeCompare([]byte(hash), []byte(box.DeleteTokenHash)) == 1
}
func (s *UploadService) CanDownload(box Box) error {
if time.Now().UTC().After(box.ExpiresAt) {
return fmt.Errorf("box has expired")
@@ -462,7 +486,7 @@ func (s *UploadService) SaveBox(box Box) error {
})
}
func (s *UploadService) resultForBox(box Box) UploadResult {
func (s *UploadService) resultForBox(box Box, deleteToken string) UploadResult {
files := make([]ResultFile, 0, len(box.Files))
for _, file := range box.Files {
files = append(files, ResultFile{
@@ -473,13 +497,18 @@ func (s *UploadService) resultForBox(box Box) UploadResult {
})
}
return UploadResult{
result := UploadResult{
BoxID: box.ID,
BoxURL: fmt.Sprintf("%s/d/%s", s.baseURL, box.ID),
ZipURL: fmt.Sprintf("%s/d/%s/zip", s.baseURL, box.ID),
ExpiresAt: box.ExpiresAt.Format(time.RFC3339),
Files: files,
}
if deleteToken != "" {
result.ManageURL = fmt.Sprintf("%s/d/%s/manage/%s", s.baseURL, box.ID, deleteToken)
result.DeleteURL = fmt.Sprintf("%s/d/%s/manage/%s/delete", s.baseURL, box.ID, deleteToken)
}
return result
}
func writeUploadedFile(path string, source multipart.File, maxSize int64) error {
@@ -519,6 +548,11 @@ func passwordHash(salt, password string) string {
return hex.EncodeToString(sum[:])
}
func deleteTokenHash(boxID, token string) string {
sum := sha256.Sum256([]byte("warpbox-delete:" + boxID + ":" + token))
return hex.EncodeToString(sum[:])
}
func previewKind(contentType string) string {
switch {
case strings.HasPrefix(contentType, "image/"):