diff --git a/DysonNetwork.Zone/Publication/API_DOCS.md b/DysonNetwork.Zone/Publication/API_DOCS.md new file mode 100644 index 0000000..d572641 --- /dev/null +++ b/DysonNetwork.Zone/Publication/API_DOCS.md @@ -0,0 +1,536 @@ +# Publication Site Management API + +This API provides file management capabilities for self-managed publication sites on the Dyson Network platform. It allows authenticated users with editor permissions to manage static files for their sites. + +When using with the gateway, the `/api` should be replaced with the `/zone` + +## Overview + +The Publication API provides comprehensive management capabilities for publication sites and their content on the Dyson Network platform. It includes: + +### Site Management +- Site CRUD operations (Create, Read, Update, Delete) +- Publisher-based site organization +- Support for FullyManaged and SelfManaged site modes + +### Page Management +- Page CRUD operations within sites +- Support for HTML pages and redirects +- Flexible configuration using JSON config objects + +### File Management (SelfManaged Sites Only) +- File and directory listing +- File upload with size validation +- File content reading and editing +- File downloading with appropriate MIME types +- File deletion +- Total site size tracking and limits + +### Base URL +``` +/api/sites/{siteId}/files +``` + +### Authentication +All endpoints require authentication via JWT bearer token or similar authentication mechanism configured in the application. + +### Authorization +- User must be authenticated +- Site must exist and be in `SelfManaged` mode +- User must have `Editor` role in the site's publisher + +### File Limits +- **Individual file size limit**: 1 MB (1,048,576 bytes) +- **Total site size limit**: 25 MB (26,214,400 bytes) +- Files are stored in the site's dedicated directory structure + +## Endpoints + +## Site Management Endpoints + +The following endpoints handle publication site CRUD operations. + +### Get Site (Public) + +Get a publication site by publisher name and slug. + +**Endpoint**: `GET /api/sites/{pubName}/{slug}` + +**Response**: `200 OK` + +```json +{ + "id": "123e4567-e89b-12d3-a456-426614174000", + "slug": "my-site", + "name": "My Site", + "description": "A description of my site", + "mode": "FullyManaged", + "pages": [ + { + "id": "456e7890-e89b-12d3-a456-426614174000", + "type": "HtmlPage", + "path": "/", + "config": { + "content": "

Home

", + "title": "Home Page" + }, + "site_id": "123e4567-e89b-12d3-a456-426614174000" + } + ], + "publisher_id": "456e7890-e89b-12d3-a456-426614174000", + "account_id": "789e0123-e89b-12d3-a456-426614174000" +} +``` + +### List Sites for Publisher + +List all sites for a specific publisher. + +**Endpoint**: `GET /api/sites/{pubName}` + +**Authorization**: Required (Viewer role or higher in publisher) + +**Response**: `200 OK` + +```json +[ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "slug": "site1", + "name": "Site One", + "description": "First site", + "mode": "SelfManaged", + "publisher_id": "456e7890-e89b-12d3-a456-426614174000", + "account_id": "789e0123-e89b-12d3-a456-426614174000" + } +] +``` + +### List Owned Sites + +List all sites for publishers where the authenticated user is a member. + +**Endpoint**: `GET /api/sites/me` + +**Authorization**: Required + +**Response**: `200 OK` - Array of sites as shown above. + +### Create Site + +Create a new publication site. + +**Endpoint**: `POST /api/sites/{pubName}` + +**Authorization**: Required (Editor role or higher in publisher) + +**Request Body**: + +```json +{ + "mode": "SelfManaged", + "slug": "my-new-site", + "name": "My New Site", + "description": "Description of my new site" +} +``` + +**Response**: `200 OK` - Returns created site object. + +**Validation**: +- User must have appropriate permissions in the publisher +- Slug must be unique within the publisher +- Name is required, max 4096 characters +- Description max 8192 characters + +### Update Site + +Update an existing publication site. + +**Endpoint**: `PATCH /api/sites/{pubName}/{id}` + +**Authorization**: Required (Editor role or higher in publisher) + +**Request Body**: Same as Create Site, all fields optional. + +**Response**: `200 OK` - Returns updated site object. + +### Delete Site + +Delete a publication site and all its associated pages. + +**Endpoint**: `DELETE /api/sites/{pubName}/{id}` + +**Authorization**: Required (Editor role or higher in publisher) + +**Response**: `204 No Content` + +## Page Management Endpoints + +The following endpoints handle publication page CRUD operations. + +### Render Page (Public) + +Render a publication page for public access. + +**Endpoint**: `GET /api/sites/site/{slug}/page` + +**Query Parameters**: +- `path`: Page path (defaults to "/") + +**Response**: `200 OK` - Returns page object. + +### List Pages for Site + +List all pages belonging to a specific site. + +**Endpoint**: `GET /api/sites/{pubName}/{siteSlug}/pages` + +**Authorization**: Required (Viewer role or higher in publisher) + +**Response**: `200 OK` + +```json +[ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "type": "HtmlPage", + "path": "/", + "config": { + "content": "

Welcome

", + "title": "Home Page" + }, + "site_id": "456e7890-e89b-12d3-a456-426614174000" + } +] +``` + +### Get Page + +Get a specific page by ID. + +**Endpoint**: `GET /api/sites/pages/{id}` + +**Response**: `200 OK` - Returns page object as shown above. + +### Create Page + +Create a new publication page. + +**Endpoint**: `POST /api/sites/{pubName}/{siteSlug}/pages` + +**Authorization**: Required (Editor role or higher in publisher) + +**Request Body**: + +```json +{ + "type": "HtmlPage", + "path": "/about", + "config": { + "content": "

About Us

Content here

", + "title": "About Page" + } +} +``` + +**Response**: `200 OK` - Returns created page object. + +**Validation**: +- Path must be unique within the site +- Config is a flexible JSON object + +### Update Page + +Update an existing publication page. + +**Endpoint**: `PATCH /api/sites/pages/{id}` + +**Authorization**: Required (Editor role or higher in publisher) + +**Request Body**: Same as Create Page, all fields optional except id. + +**Response**: `200 OK` - Returns updated page object. + +### Delete Page + +Delete a publication page. + +**Endpoint**: `DELETE /api/sites/pages/{id}` + +**Authorization**: Required (Editor role or higher in publisher) + +**Response**: `204 No Content` + +## File Management Endpoints + +### List Files + +Get a list of files and directories in a site directory. + +**Endpoint**: `GET /api/sites/{siteId}/files` + +**Query Parameters**: +- `path` (optional): Relative path to the directory. Defaults to root directory. + +**Response**: `200 OK` + +```json +[ + { + "is_directory": true, + "relative_path": "folder1", + "size": 0, + "modified": "2024-01-15T10:30:00Z" + }, + { + "is_directory": false, + "relative_path": "index.html", + "size": 1024, + "modified": "2024-01-15T09:15:00Z" + } +] +``` + +### Upload File + +Upload a new file to the site. + +**Endpoint**: `POST /api/sites/{siteId}/files/upload` + +**Content-Type**: `multipart/form-data` + +**Form Data**: +- `filePath`: Relative path where the file should be stored (including filename) +- `file`: The file to upload + +**Response**: `200 OK` + +**Validation**: +- File must be provided and not empty +- File size must not exceed 1 MB +- Total site size must not exceed 25 MB after upload + +### Get File Content + +Retrieve the text content of a file. + +**Endpoint**: `GET /api/sites/{siteId}/files/content/{relativePath}` + +**Response**: `200 OK` + +```json +{ + "content": "\n\nHello World\n" +} +``` + +**Supported file types**: Primarily text-based files. Returns raw text content. + +### Download File + +Download a file with proper MIME type headers. + +**Endpoint**: `GET /api/sites/{siteId}/files/download/{relativePath}` + +**Response**: `200 OK` with file content + +**MIME Types**: +- `.txt` → `text/plain` +- `.html`, `.htm` → `text/html` +- `.css` → `text/css` +- `.js` → `application/javascript` +- `.json` → `application/json` +- Others → `application/octet-stream` + +File is returned as attachment with the original filename. + +### Update File Content + +Update the content of an existing text file. + +**Endpoint**: `PUT /api/sites/{siteId}/files/edit/{relativePath}` + +**Request Body**: + +```json +{ + "new_content": "\n\nUpdated content\n" +} +``` + +**Response**: `200 OK` + +**Validation**: +- New content size must not exceed 1 MB +- Total site size must not exceed 25 MB after update + +### Delete File + +Delete a file or empty directory. + +**Endpoint**: `DELETE /api/sites/{siteId}/files/delete/{relativePath}` + +**Response**: `200 OK` + +**Note**: Deletes both files and directories. Directories must be empty to be deleted. + +## Error Responses + +### 401 Unauthorized +User is not authenticated. +```json +{ + "statusCode": 401, + "message": "Unauthorized" +} +``` + +### 403 Forbidden +User does not have required permissions. +```json +{ + "statusCode": 403, + "message": "Forbidden" +} +``` + +### 404 Not Found +- Site not found or not in SelfManaged mode +- File or directory not found +```json +{ + "statusCode": 404, + "message": "Site not found or not self-managed" +} +``` + +### 400 Bad Request +Various validation errors including: +- File size limits exceeded +- Total site size limits exceeded +- Invalid file path +- Missing required parameters +```json +{ + "statusCode": 400, + "message": "File size exceeds 1MB limit" +} +``` + +## Usage Examples + +### Upload a file using curl + +```bash +curl -X POST "https://api.dyson.network/api/sites/123e4567-e89b-12d3-a456-426614174000/files/upload" \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -F "filePath=index.html" \ + -F "file=@./index.html" +``` + +### Update file content using curl + +```bash +curl -X PUT "https://api.dyson.network/api/sites/123e4567-e89b-12d3-a456-426614174000/files/edit/index.html" \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"new_content": "Hello World"}' +``` + +### List files in a subdirectory + +```bash +curl -X GET "https://api.dyson.network/api/sites/123e4567-e89b-12d3-a456-426614174000/files?path=assets/css" \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" +``` + +### Download a file + +```bash +curl -X GET "https://api.dyson.network/api/sites/123e4567-e89b-12d3-a456-426614174000/files/download/index.html" \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -o downloaded_index.html +``` + +## Data Models + +### PublicationSite +Represents a publication site. + +```typescript +interface PublicationSite { + id: string; // GUID + slug: string; // Unique within publisher, max 4096 chars + name: string; // Display name, max 4096 chars + description?: string; // Optional description, max 8192 chars + mode: "FullyManaged" | "SelfManaged"; + pages: PublicationPage[]; + publisher_id: string; // GUID + account_id: string; // GUID +} +``` + +### PublicationPage +Represents a page within a publication site. + +```typescript +interface PublicationPage { + id: string; // GUID + type: "HtmlPage" | "Redirect"; + path: string; // Page path within site, max 8192 chars + config: { [key: string]: any }; // Flexible JSON configuration + site_id: string; // GUID of parent site +} +``` + +### PublicationSiteRequest +Used for creating/updating sites. + +```typescript +interface PublicationSiteRequest { + mode: "FullyManaged" | "SelfManaged"; + slug: string; + name: string; + description?: string; +} +``` + +### PublicationPageRequest +Used for creating/updating pages. + +```typescript +interface PublicationPageRequest { + type: "HtmlPage" | "Redirect"; + path?: string; + config?: { [key: string]: any }; +} +``` + +### FileEntry +Represents a file or directory entry. + +```typescript +interface FileEntry { + is_directory: boolean; + relative_path: string; + size: number; // Size in bytes (0 for directories) + modified: string; // ISO 8601 timestamp +} +``` + +### UpdateFileRequest +Used for updating file content. + +```typescript +interface UpdateFileRequest { + new_content: string; // The new content for the file +} +``` + +## Security Notes + +- All file operations are restricted to the authenticated user's authorized sites +- Path traversal attacks are prevented through path validation +- Size limits prevent abuse of storage resources +- Files are served from a dedicated web root directory to prevent access to sensitive system files diff --git a/DysonNetwork.Zone/Publication/PublicationSiteController.cs b/DysonNetwork.Zone/Publication/PublicationSiteController.cs index 5d430b2..449272e 100644 --- a/DysonNetwork.Zone/Publication/PublicationSiteController.cs +++ b/DysonNetwork.Zone/Publication/PublicationSiteController.cs @@ -94,9 +94,9 @@ public class PublicationSiteController( return Ok(site); } - [HttpPatch("{pubName}/{id:guid}")] + [HttpPatch("{pubName}/{slug}")] [Authorize] - public async Task> UpdateSite([FromRoute] string pubName, Guid id, [FromBody] PublicationSiteRequest request) + public async Task> UpdateSite([FromRoute] string pubName, string slug, [FromBody] PublicationSiteRequest request) { if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized(); @@ -105,7 +105,7 @@ public class PublicationSiteController( var publisher = await publisherService.GetPublisherByName(pubName); if (publisher == null) return NotFound(); - var site = await publicationService.GetSiteById(id); + var site = await publicationService.GetSiteBySlug(slug, pubName); if (site == null || site.PublisherId != publisher.Id) return NotFound(); @@ -126,9 +126,9 @@ public class PublicationSiteController( return Ok(site); } - [HttpDelete("{pubName}/{id:guid}")] + [HttpDelete("{pubName}/{slug}")] [Authorize] - public async Task DeleteSite([FromRoute] string pubName, Guid id) + public async Task DeleteSite([FromRoute] string pubName, string slug) { if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized(); @@ -137,13 +137,13 @@ public class PublicationSiteController( var publisher = await publisherService.GetPublisherByName(pubName); if (publisher == null) return NotFound(); - var site = await publicationService.GetSiteById(id); + var site = await publicationService.GetSiteBySlug(slug, pubName); if (site == null || site.PublisherId != publisher.Id) return NotFound(); try { - await publicationService.DeleteSite(id, accountId); + await publicationService.DeleteSite(site.Id, accountId); } catch (UnauthorizedAccessException) {