Compare commits
	
		
			7 Commits
		
	
	
		
			master
			...
			8d2f4a4c47
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8d2f4a4c47 | |||
| 1672d46038 | |||
| 15fb93c2bb | |||
| 4b220e7ed7 | |||
| 65450e8511 | |||
| cb4acbb3fc | |||
| bb2f88cc54 | 
| @@ -1,4 +0,0 @@ | |||||||
| { |  | ||||||
|   "appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj" |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| **/.dockerignore | **/.dockerignore | ||||||
| **/.env | **/.env | ||||||
|  | **/.git | ||||||
| **/.gitignore | **/.gitignore | ||||||
| **/.project | **/.project | ||||||
| **/.settings | **/.settings | ||||||
|   | |||||||
| @@ -1,5 +0,0 @@ | |||||||
| root = true |  | ||||||
|  |  | ||||||
| [*] |  | ||||||
| indent_style = space |  | ||||||
| indent_size = 4 |  | ||||||
							
								
								
									
										38
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								.env
									
									
									
									
									
								
							| @@ -1,38 +0,0 @@ | |||||||
| # Default container port for ring |  | ||||||
| RING_PORT=8080 |  | ||||||
|  |  | ||||||
| # Default container port for pass |  | ||||||
| PASS_PORT=8080 |  | ||||||
|  |  | ||||||
| # Default container port for drive |  | ||||||
| DRIVE_PORT=8080 |  | ||||||
|  |  | ||||||
| # Default container port for sphere |  | ||||||
| SPHERE_PORT=8080 |  | ||||||
|  |  | ||||||
| # Default container port for develop |  | ||||||
| DEVELOP_PORT=8080 |  | ||||||
|  |  | ||||||
| # Parameter cache-password |  | ||||||
| CACHE_PASSWORD=KS3jSPaU9e |  | ||||||
|  |  | ||||||
| # Parameter queue-password |  | ||||||
| QUEUE_PASSWORD=8xEECa4ckz |  | ||||||
|  |  | ||||||
| # Container image name for ring |  | ||||||
| RING_IMAGE=ring:latest |  | ||||||
|  |  | ||||||
| # Container image name for pass |  | ||||||
| PASS_IMAGE=pass:latest |  | ||||||
|  |  | ||||||
| # Container image name for drive |  | ||||||
| DRIVE_IMAGE=drive:latest |  | ||||||
|  |  | ||||||
| # Container image name for sphere |  | ||||||
| SPHERE_IMAGE=sphere:latest |  | ||||||
|  |  | ||||||
| # Container image name for develop |  | ||||||
| DEVELOP_IMAGE=develop:latest |  | ||||||
|  |  | ||||||
| # Container image name for gateway |  | ||||||
| GATEWAY_IMAGE=gateway:latest |  | ||||||
							
								
								
									
										90
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										90
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| name: Build and Push Microservices | name: Build and Push Dyson Sphere | ||||||
|  |  | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
| @@ -7,97 +7,27 @@ on: | |||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   determine-changes: |   build: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest # x86_64 (default), avoids arm64 native module issues | ||||||
|     outputs: |  | ||||||
|       matrix: ${{ steps.changes.outputs.matrix }} |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout repository |  | ||||||
|         uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           fetch-depth: 0 |  | ||||||
|  |  | ||||||
|       - name: Get changed files |  | ||||||
|         id: changed-files |  | ||||||
|         run: | |  | ||||||
|           echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT |  | ||||||
|  |  | ||||||
|       - name: Determine changed services |  | ||||||
|         id: changes |  | ||||||
|         run: | |  | ||||||
|           files="${{ steps.changed-files.outputs.files }}" |  | ||||||
|           matrix="{\"include\":[]}" |  | ||||||
|           services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight") |  | ||||||
|           images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight") |  | ||||||
|           changed_services=() |  | ||||||
|  |  | ||||||
|           for file in $files; do |  | ||||||
|             if [[ "$file" == DysonNetwork.Shared/* ]]; then |  | ||||||
|               changed_services=("${services[@]}") |  | ||||||
|               break |  | ||||||
|             fi |  | ||||||
|             for i in "${!services[@]}"; do |  | ||||||
|               if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then |  | ||||||
|                 # check if service is already in changed_services |  | ||||||
|                 if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then |  | ||||||
|                   changed_services+=("${services[$i]}") |  | ||||||
|                 fi |  | ||||||
|               fi |  | ||||||
|             done |  | ||||||
|           done |  | ||||||
|  |  | ||||||
|           if [ ${#changed_services[@]} -gt 0 ]; then |  | ||||||
|             json_objects="" |  | ||||||
|             for service in "${changed_services[@]}"; do |  | ||||||
|               for i in "${!services[@]}"; do |  | ||||||
|                 if [[ "${services[$i]}" == "$service" ]]; then |  | ||||||
|                   image="${images[$i]}" |  | ||||||
|                   break |  | ||||||
|                 fi |  | ||||||
|               done |  | ||||||
|               json_objects+="{\"service\":\"$service\",\"image\":\"$image\"}," |  | ||||||
|             done |  | ||||||
|             matrix="{\"include\":[${json_objects%,}]}" |  | ||||||
|           fi |  | ||||||
|           echo "matrix=$matrix" >> $GITHUB_OUTPUT |  | ||||||
|  |  | ||||||
|   build-and-push: |  | ||||||
|     needs: determine-changes |  | ||||||
|     if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }} |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     permissions: |  | ||||||
|       contents: read |  | ||||||
|       packages: write |  | ||||||
|     strategy: |  | ||||||
|       matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }} |  | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v3 | ||||||
|         with: |  | ||||||
|           fetch-depth: 0 |  | ||||||
|  |  | ||||||
|       - name: Setup NBGV |  | ||||||
|         uses: dotnet/nbgv@master |  | ||||||
|         id: nbgv |  | ||||||
|  |  | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v3 |         uses: docker/setup-buildx-action@v3 | ||||||
|  |  | ||||||
|       - name: Log in to GitHub Container Registry |       - name: Log in to DockerHub | ||||||
|         uses: docker/login-action@v3 |         uses: docker/login-action@v3 | ||||||
|         with: |         with: | ||||||
|           registry: ghcr.io |           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||||
|           username: ${{ github.actor }} |           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|  |  | ||||||
|       - name: Build and push Docker image for ${{ matrix.service }} |       - name: Build and push Docker image | ||||||
|         uses: docker/build-push-action@v6 |         uses: docker/build-push-action@v6 | ||||||
|         with: |         with: | ||||||
|  |           file: DysonNetwork.Sphere/Dockerfile | ||||||
|           context: . |           context: . | ||||||
|           file: DysonNetwork.${{ matrix.service }}/Dockerfile |  | ||||||
|           push: true |           push: true | ||||||
|           tags: | |           tags: xsheep2010/dyson-sphere:latest | ||||||
|             ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }} |  | ||||||
|             ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest |  | ||||||
|           platforms: linux/amd64 |           platforms: linux/amd64 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,9 +1,7 @@ | |||||||
| bin/ | bin/ | ||||||
| obj/ | obj/ | ||||||
| /packages/ | /packages/ | ||||||
| /Certificates/ |  | ||||||
| riderModule.iml | riderModule.iml | ||||||
| /_ReSharper.Caches/ | /_ReSharper.Caches/ | ||||||
| .idea | .idea | ||||||
| .DS_Store | .DS_Store | ||||||
| /Keys/ |  | ||||||
|   | |||||||
| @@ -1,613 +0,0 @@ | |||||||
| # Wallet Funds API Documentation |  | ||||||
|  |  | ||||||
| ## Overview |  | ||||||
|  |  | ||||||
| The Wallet Funds API provides red packet functionality for the DysonNetwork platform, allowing users to create and distribute funds among multiple recipients with expiration and claiming mechanisms. |  | ||||||
|  |  | ||||||
| ## Authentication |  | ||||||
|  |  | ||||||
| All endpoints require Bearer token authentication: |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| Authorization: Bearer {jwt_token} |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Data Types |  | ||||||
|  |  | ||||||
| ### Enums |  | ||||||
|  |  | ||||||
| #### FundSplitType |  | ||||||
| ```typescript |  | ||||||
| enum FundSplitType { |  | ||||||
|   Even = 0,    // Equal distribution |  | ||||||
|   Random = 1   // Lucky draw distribution |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### FundStatus |  | ||||||
| ```typescript |  | ||||||
| enum FundStatus { |  | ||||||
|   Created = 0,           // Fund created, waiting for claims |  | ||||||
|   PartiallyReceived = 1, // Some recipients claimed |  | ||||||
|   FullyReceived = 2,     // All recipients claimed |  | ||||||
|   Expired = 3,           // Fund expired, unclaimed amounts refunded |  | ||||||
|   Refunded = 4           // Legacy status |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Request/Response Models |  | ||||||
|  |  | ||||||
| #### CreateFundRequest |  | ||||||
| ```typescript |  | ||||||
| interface CreateFundRequest { |  | ||||||
|   recipientAccountIds: string[];  // UUIDs of recipients |  | ||||||
|   currency: string;               // e.g., "points", "golds" |  | ||||||
|   totalAmount: number;            // Total amount to distribute |  | ||||||
|   splitType: FundSplitType;       // Even or Random |  | ||||||
|   message?: string;               // Optional message |  | ||||||
|   expirationHours?: number;       // Optional: hours until expiration (default: 24) |  | ||||||
|   pinCode: string;                // Required: 6-digit PIN code for security |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### SnWalletFund |  | ||||||
| ```typescript |  | ||||||
| interface SnWalletFund { |  | ||||||
|   id: string;                     // UUID |  | ||||||
|   currency: string; |  | ||||||
|   totalAmount: number; |  | ||||||
|   splitType: FundSplitType; |  | ||||||
|   status: FundStatus; |  | ||||||
|   message?: string; |  | ||||||
|   creatorAccountId: string;       // UUID |  | ||||||
|   creatorAccount: SnAccount;      // Creator account details (includes profile) |  | ||||||
|   recipients: SnWalletFundRecipient[]; |  | ||||||
|   expiredAt: string;              // ISO 8601 timestamp |  | ||||||
|   createdAt: string;              // ISO 8601 timestamp |  | ||||||
|   updatedAt: string;              // ISO 8601 timestamp |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### SnWalletFundRecipient |  | ||||||
| ```typescript |  | ||||||
| interface SnWalletFundRecipient { |  | ||||||
|   id: string;                     // UUID |  | ||||||
|   fundId: string;                 // UUID |  | ||||||
|   recipientAccountId: string;     // UUID |  | ||||||
|   recipientAccount: SnAccount;    // Recipient account details (includes profile) |  | ||||||
|   amount: number;                 // Allocated amount |  | ||||||
|   isReceived: boolean; |  | ||||||
|   receivedAt?: string;            // ISO 8601 timestamp (if claimed) |  | ||||||
|   createdAt: string;              // ISO 8601 timestamp |  | ||||||
|   updatedAt: string;              // ISO 8601 timestamp |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### SnWalletTransaction |  | ||||||
| ```typescript |  | ||||||
| interface SnWalletTransaction { |  | ||||||
|   id: string;                     // UUID |  | ||||||
|   payerWalletId?: string;         // UUID (null for system transfers) |  | ||||||
|   payeeWalletId?: string;         // UUID (null for system transfers) |  | ||||||
|   currency: string; |  | ||||||
|   amount: number; |  | ||||||
|   remarks?: string; |  | ||||||
|   type: TransactionType; |  | ||||||
|   createdAt: string;              // ISO 8601 timestamp |  | ||||||
|   updatedAt: string;              // ISO 8601 timestamp |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### Error Response |  | ||||||
| ```typescript |  | ||||||
| interface ErrorResponse { |  | ||||||
|   type: string;                   // Error type |  | ||||||
|   title: string;                  // Error title |  | ||||||
|   status: number;                 // HTTP status code |  | ||||||
|   detail: string;                 // Error details |  | ||||||
|   instance?: string;              // Request instance |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## API Endpoints |  | ||||||
|  |  | ||||||
| ### 1. Create Fund |  | ||||||
|  |  | ||||||
| Creates a new fund (red packet) for distribution among recipients. |  | ||||||
|  |  | ||||||
| **Endpoint:** `POST /api/wallets/funds` |  | ||||||
|  |  | ||||||
| **Request Body:** `CreateFundRequest` |  | ||||||
|  |  | ||||||
| **Response:** `SnWalletFund` (201 Created) |  | ||||||
|  |  | ||||||
| **Example Request:** |  | ||||||
| ```bash |  | ||||||
| curl -X POST "/api/wallets/funds" \ |  | ||||||
|   -H "Authorization: Bearer {token}" \ |  | ||||||
|   -H "Content-Type: application/json" \ |  | ||||||
|   -d '{ |  | ||||||
|     "recipientAccountIds": [ |  | ||||||
|       "550e8400-e29b-41d4-a716-446655440000", |  | ||||||
|       "550e8400-e29b-41d4-a716-446655440001", |  | ||||||
|       "550e8400-e29b-41d4-a716-446655440002" |  | ||||||
|     ], |  | ||||||
|     "currency": "points", |  | ||||||
|     "totalAmount": 100.00, |  | ||||||
|     "splitType": "Even", |  | ||||||
|     "message": "Happy New Year! 🎉", |  | ||||||
|     "expirationHours": 48, |  | ||||||
|     "pinCode": "123456" |  | ||||||
|   }' |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Example Response:** |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "id": "550e8400-e29b-41d4-a716-446655440003", |  | ||||||
|   "currency": "points", |  | ||||||
|   "totalAmount": 100.00, |  | ||||||
|   "splitType": 0, |  | ||||||
|   "status": 0, |  | ||||||
|   "message": "Happy New Year! 🎉", |  | ||||||
|   "creatorAccountId": "550e8400-e29b-41d4-a716-446655440004", |  | ||||||
|   "creatorAccount": { |  | ||||||
|     "id": "550e8400-e29b-41d4-a716-446655440004", |  | ||||||
|     "username": "creator_user" |  | ||||||
|   }, |  | ||||||
|   "recipients": [ |  | ||||||
|     { |  | ||||||
|       "id": "550e8400-e29b-41d4-a716-446655440005", |  | ||||||
|       "fundId": "550e8400-e29b-41d4-a716-446655440003", |  | ||||||
|       "recipientAccountId": "550e8400-e29b-41d4-a716-446655440000", |  | ||||||
|       "amount": 33.34, |  | ||||||
|       "isReceived": false, |  | ||||||
|       "createdAt": "2025-10-03T22:00:00Z", |  | ||||||
|       "updatedAt": "2025-10-03T22:00:00Z" |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       "id": "550e8400-e29b-41d4-a716-446655440006", |  | ||||||
|       "fundId": "550e8400-e29b-41d4-a716-446655440003", |  | ||||||
|       "recipientAccountId": "550e8400-e29b-41d4-a716-446655440001", |  | ||||||
|       "amount": 33.33, |  | ||||||
|       "isReceived": false, |  | ||||||
|       "createdAt": "2025-10-03T22:00:00Z", |  | ||||||
|       "updatedAt": "2025-10-03T22:00:00Z" |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       "id": "550e8400-e29b-41d4-a716-446655440007", |  | ||||||
|       "fundId": "550e8400-e29b-41d4-a716-446655440003", |  | ||||||
|       "recipientAccountId": "550e8400-e29b-41d4-a716-446655440002", |  | ||||||
|       "amount": 33.33, |  | ||||||
|       "isReceived": false, |  | ||||||
|       "createdAt": "2025-10-03T22:00:00Z", |  | ||||||
|       "updatedAt": "2025-10-03T22:00:00Z" |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   "expiredAt": "2025-10-05T22:00:00Z", |  | ||||||
|   "createdAt": "2025-10-03T22:00:00Z", |  | ||||||
|   "updatedAt": "2025-10-03T22:00:00Z" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Error Responses:** |  | ||||||
| - `400 Bad Request`: Invalid parameters, insufficient funds, invalid recipients |  | ||||||
| - `401 Unauthorized`: Missing or invalid authentication |  | ||||||
| - `403 Forbidden`: Invalid PIN code |  | ||||||
| - `422 Unprocessable Entity`: Business logic violations |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| ### 2. Get Funds |  | ||||||
|  |  | ||||||
| Retrieves funds that the authenticated user is involved in (as creator or recipient). |  | ||||||
|  |  | ||||||
| **Endpoint:** `GET /api/wallets/funds` |  | ||||||
|  |  | ||||||
| **Query Parameters:** |  | ||||||
| - `offset` (number, optional): Pagination offset (default: 0) |  | ||||||
| - `take` (number, optional): Number of items to return (default: 20, max: 100) |  | ||||||
| - `status` (FundStatus, optional): Filter by fund status |  | ||||||
|  |  | ||||||
| **Response:** `SnWalletFund[]` (200 OK) |  | ||||||
|  |  | ||||||
| **Headers:** |  | ||||||
| - `X-Total`: Total number of funds matching the criteria |  | ||||||
|  |  | ||||||
| **Example Request:** |  | ||||||
| ```bash |  | ||||||
| curl -X GET "/api/wallets/funds?offset=0&take=10&status=0" \ |  | ||||||
|   -H "Authorization: Bearer {token}" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Example Response:** |  | ||||||
| ```json |  | ||||||
| [ |  | ||||||
|   { |  | ||||||
|     "id": "550e8400-e29b-41d4-a716-446655440003", |  | ||||||
|     "currency": "points", |  | ||||||
|     "totalAmount": 100.00, |  | ||||||
|     "splitType": 0, |  | ||||||
|     "status": 0, |  | ||||||
|     "message": "Happy New Year! 🎉", |  | ||||||
|     "creatorAccountId": "550e8400-e29b-41d4-a716-446655440004", |  | ||||||
|     "creatorAccount": { |  | ||||||
|       "id": "550e8400-e29b-41d4-a716-446655440004", |  | ||||||
|       "username": "creator_user" |  | ||||||
|     }, |  | ||||||
|     "recipients": [ |  | ||||||
|       { |  | ||||||
|         "id": "550e8400-e29b-41d4-a716-446655440005", |  | ||||||
|         "fundId": "550e8400-e29b-41d4-a716-446655440003", |  | ||||||
|         "recipientAccountId": "550e8400-e29b-41d4-a716-446655440000", |  | ||||||
|         "amount": 33.34, |  | ||||||
|         "isReceived": false |  | ||||||
|       } |  | ||||||
|     ], |  | ||||||
|     "expiredAt": "2025-10-05T22:00:00Z", |  | ||||||
|     "createdAt": "2025-10-03T22:00:00Z", |  | ||||||
|     "updatedAt": "2025-10-03T22:00:00Z" |  | ||||||
|   } |  | ||||||
| ] |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Error Responses:** |  | ||||||
| - `401 Unauthorized`: Missing or invalid authentication |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| ### 3. Get Fund |  | ||||||
|  |  | ||||||
| Retrieves details of a specific fund. |  | ||||||
|  |  | ||||||
| **Endpoint:** `GET /api/wallets/funds/{id}` |  | ||||||
|  |  | ||||||
| **Path Parameters:** |  | ||||||
| - `id` (string): Fund UUID |  | ||||||
|  |  | ||||||
| **Response:** `SnWalletFund` (200 OK) |  | ||||||
|  |  | ||||||
| **Example Request:** |  | ||||||
| ```bash |  | ||||||
| curl -X GET "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003" \ |  | ||||||
|   -H "Authorization: Bearer {token}" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Example Response:** (Same as create fund response) |  | ||||||
|  |  | ||||||
| **Error Responses:** |  | ||||||
| - `401 Unauthorized`: Missing or invalid authentication |  | ||||||
| - `403 Forbidden`: User doesn't have permission to view this fund |  | ||||||
| - `404 Not Found`: Fund not found |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| ### 4. Receive Fund |  | ||||||
|  |  | ||||||
| Claims the authenticated user's portion of a fund. |  | ||||||
|  |  | ||||||
| **Endpoint:** `POST /api/wallets/funds/{id}/receive` |  | ||||||
|  |  | ||||||
| **Path Parameters:** |  | ||||||
| - `id` (string): Fund UUID |  | ||||||
|  |  | ||||||
| **Response:** `SnWalletTransaction` (200 OK) |  | ||||||
|  |  | ||||||
| **Example Request:** |  | ||||||
| ```bash |  | ||||||
| curl -X POST "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive" \ |  | ||||||
|   -H "Authorization: Bearer {token}" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Example Response:** |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "id": "550e8400-e29b-41d4-a716-446655440008", |  | ||||||
|   "payerWalletId": null, |  | ||||||
|   "payeeWalletId": "550e8400-e29b-41d4-a716-446655440009", |  | ||||||
|   "currency": "points", |  | ||||||
|   "amount": 33.34, |  | ||||||
|   "remarks": "Received fund portion from 550e8400-e29b-41d4-a716-446655440004", |  | ||||||
|   "type": 1, |  | ||||||
|   "createdAt": "2025-10-03T22:05:00Z", |  | ||||||
|   "updatedAt": "2025-10-03T22:05:00Z" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Error Responses:** |  | ||||||
| - `400 Bad Request`: Fund expired, already claimed, not a recipient |  | ||||||
| - `401 Unauthorized`: Missing or invalid authentication |  | ||||||
| - `404 Not Found`: Fund not found |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| ### 5. Get Wallet Overview |  | ||||||
|  |  | ||||||
| Retrieves a summarized overview of wallet transactions grouped by type for graphing/charting purposes. |  | ||||||
|  |  | ||||||
| **Endpoint:** `GET /api/wallets/overview` |  | ||||||
|  |  | ||||||
| **Query Parameters:** |  | ||||||
| - `startDate` (string, optional): Start date in ISO 8601 format (e.g., "2025-01-01T00:00:00Z") |  | ||||||
| - `endDate` (string, optional): End date in ISO 8601 format (e.g., "2025-12-31T23:59:59Z") |  | ||||||
|  |  | ||||||
| **Response:** `WalletOverview` (200 OK) |  | ||||||
|  |  | ||||||
| **Example Request:** |  | ||||||
| ```bash |  | ||||||
| curl -X GET "/api/wallets/overview?startDate=2025-01-01T00:00:00Z&endDate=2025-12-31T23:59:59Z" \ |  | ||||||
|   -H "Authorization: Bearer {token}" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Example Response:** |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "accountId": "550e8400-e29b-41d4-a716-446655440000", |  | ||||||
|   "startDate": "2025-01-01T00:00:00.0000000Z", |  | ||||||
|   "endDate": "2025-12-31T23:59:59.0000000Z", |  | ||||||
|   "summary": { |  | ||||||
|     "System": { |  | ||||||
|       "type": "System", |  | ||||||
|       "currencies": { |  | ||||||
|         "points": { |  | ||||||
|           "currency": "points", |  | ||||||
|           "income": 150.00, |  | ||||||
|           "spending": 0.00, |  | ||||||
|           "net": 150.00 |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "Transfer": { |  | ||||||
|       "type": "Transfer", |  | ||||||
|       "currencies": { |  | ||||||
|         "points": { |  | ||||||
|           "currency": "points", |  | ||||||
|           "income": 25.00, |  | ||||||
|           "spending": 75.00, |  | ||||||
|           "net": -50.00 |  | ||||||
|         }, |  | ||||||
|         "golds": { |  | ||||||
|           "currency": "golds", |  | ||||||
|           "income": 0.00, |  | ||||||
|           "spending": 10.00, |  | ||||||
|           "net": -10.00 |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "Order": { |  | ||||||
|       "type": "Order", |  | ||||||
|       "currencies": { |  | ||||||
|         "points": { |  | ||||||
|           "currency": "points", |  | ||||||
|           "income": 0.00, |  | ||||||
|           "spending": 200.00, |  | ||||||
|           "net": -200.00 |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "totalIncome": 175.00, |  | ||||||
|   "totalSpending": 285.00, |  | ||||||
|   "netTotal": -110.00 |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Response Fields:** |  | ||||||
| - `accountId`: User's account UUID |  | ||||||
| - `startDate`/`endDate`: Date range applied (ISO 8601 format) |  | ||||||
| - `summary`: Object keyed by transaction type |  | ||||||
|   - `type`: Transaction type name |  | ||||||
|   - `currencies`: Object keyed by currency code |  | ||||||
|     - `currency`: Currency name |  | ||||||
|     - `income`: Total money received |  | ||||||
|     - `spending`: Total money spent |  | ||||||
|     - `net`: Income minus spending |  | ||||||
| - `totalIncome`: Sum of all income across all types/currencies |  | ||||||
| - `totalSpending`: Sum of all spending across all types/currencies |  | ||||||
| - `netTotal`: Overall net (totalIncome - totalSpending) |  | ||||||
|  |  | ||||||
| **Error Responses:** |  | ||||||
| - `401 Unauthorized`: Missing or invalid authentication |  | ||||||
|  |  | ||||||
| ## Error Codes |  | ||||||
|  |  | ||||||
| ### Common Error Types |  | ||||||
|  |  | ||||||
| #### Validation Errors |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", |  | ||||||
|   "title": "Bad Request", |  | ||||||
|   "status": 400, |  | ||||||
|   "detail": "At least one recipient is required", |  | ||||||
|   "instance": "/api/wallets/funds" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### Insufficient Funds |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", |  | ||||||
|   "title": "Bad Request", |  | ||||||
|   "status": 400, |  | ||||||
|   "detail": "Insufficient funds", |  | ||||||
|   "instance": "/api/wallets/funds" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### Fund Not Available |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", |  | ||||||
|   "title": "Bad Request", |  | ||||||
|   "status": 400, |  | ||||||
|   "detail": "Fund is no longer available", |  | ||||||
|   "instance": "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| #### Already Claimed |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", |  | ||||||
|   "title": "Bad Request", |  | ||||||
|   "status": 400, |  | ||||||
|   "detail": "You have already received this fund", |  | ||||||
|   "instance": "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Rate Limiting |  | ||||||
|  |  | ||||||
| - **Create Fund**: 10 requests per minute per user |  | ||||||
| - **Get Funds**: 60 requests per minute per user |  | ||||||
| - **Get Fund**: 60 requests per minute per user |  | ||||||
| - **Receive Fund**: 30 requests per minute per user |  | ||||||
|  |  | ||||||
| ## Webhooks/Notifications |  | ||||||
|  |  | ||||||
| The system integrates with the platform's notification system: |  | ||||||
|  |  | ||||||
| - **Fund Created**: Creator receives confirmation |  | ||||||
| - **Fund Claimed**: Creator receives notification when someone claims |  | ||||||
| - **Fund Expired**: Creator receives refund notification |  | ||||||
|  |  | ||||||
| ## SDK Examples |  | ||||||
|  |  | ||||||
| ### JavaScript/TypeScript |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // Create a fund |  | ||||||
| const createFund = async (fundData: CreateFundRequest): Promise<SnWalletFund> => { |  | ||||||
|   const response = await fetch('/api/wallets/funds', { |  | ||||||
|     method: 'POST', |  | ||||||
|     headers: { |  | ||||||
|       'Authorization': `Bearer ${token}`, |  | ||||||
|       'Content-Type': 'application/json' |  | ||||||
|     }, |  | ||||||
|     body: JSON.stringify(fundData) |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (!response.ok) { |  | ||||||
|     throw new Error(`HTTP error! status: ${response.status}`); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return response.json(); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Get user's funds |  | ||||||
| const getFunds = async (params?: { |  | ||||||
|   offset?: number; |  | ||||||
|   take?: number; |  | ||||||
|   status?: FundStatus; |  | ||||||
| }): Promise<SnWalletFund[]> => { |  | ||||||
|   const queryParams = new URLSearchParams(); |  | ||||||
|   if (params?.offset) queryParams.set('offset', params.offset.toString()); |  | ||||||
|   if (params?.take) queryParams.set('take', params.take.toString()); |  | ||||||
|   if (params?.status !== undefined) queryParams.set('status', params.status.toString()); |  | ||||||
|  |  | ||||||
|   const response = await fetch(`/api/wallets/funds?${queryParams}`, { |  | ||||||
|     headers: { |  | ||||||
|       'Authorization': `Bearer ${token}` |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (!response.ok) { |  | ||||||
|     throw new Error(`HTTP error! status: ${response.status}`); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return response.json(); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Claim a fund |  | ||||||
| const receiveFund = async (fundId: string): Promise<SnWalletTransaction> => { |  | ||||||
|   const response = await fetch(`/api/wallets/funds/${fundId}/receive`, { |  | ||||||
|     method: 'POST', |  | ||||||
|     headers: { |  | ||||||
|       'Authorization': `Bearer ${token}` |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (!response.ok) { |  | ||||||
|     throw new Error(`HTTP error! status: ${response.status}`); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return response.json(); |  | ||||||
| }; |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Python |  | ||||||
|  |  | ||||||
| ```python |  | ||||||
| import requests |  | ||||||
| from typing import List, Optional |  | ||||||
| from enum import Enum |  | ||||||
|  |  | ||||||
| class FundSplitType(Enum): |  | ||||||
|     EVEN = 0 |  | ||||||
|     RANDOM = 1 |  | ||||||
|  |  | ||||||
| class FundStatus(Enum): |  | ||||||
|     CREATED = 0 |  | ||||||
|     PARTIALLY_RECEIVED = 1 |  | ||||||
|     FULLY_RECEIVED = 2 |  | ||||||
|     EXPIRED = 3 |  | ||||||
|     REFUNDED = 4 |  | ||||||
|  |  | ||||||
| def create_fund(token: str, fund_data: dict) -> dict: |  | ||||||
|     """Create a new fund""" |  | ||||||
|     response = requests.post( |  | ||||||
|         '/api/wallets/funds', |  | ||||||
|         json=fund_data, |  | ||||||
|         headers={ |  | ||||||
|             'Authorization': f'Bearer {token}', |  | ||||||
|             'Content-Type': 'application/json' |  | ||||||
|         } |  | ||||||
|     ) |  | ||||||
|     response.raise_for_status() |  | ||||||
|     return response.json() |  | ||||||
|  |  | ||||||
| def get_funds( |  | ||||||
|     token: str, |  | ||||||
|     offset: int = 0, |  | ||||||
|     take: int = 20, |  | ||||||
|     status: Optional[FundStatus] = None |  | ||||||
| ) -> List[dict]: |  | ||||||
|     """Get user's funds""" |  | ||||||
|     params = {'offset': offset, 'take': take} |  | ||||||
|     if status is not None: |  | ||||||
|         params['status'] = status.value |  | ||||||
|  |  | ||||||
|     response = requests.get( |  | ||||||
|         '/api/wallets/funds', |  | ||||||
|         params=params, |  | ||||||
|         headers={'Authorization': f'Bearer {token}'} |  | ||||||
|     ) |  | ||||||
|     response.raise_for_status() |  | ||||||
|     return response.json() |  | ||||||
|  |  | ||||||
| def receive_fund(token: str, fund_id: str) -> dict: |  | ||||||
|     """Claim a fund portion""" |  | ||||||
|     response = requests.post( |  | ||||||
|         f'/api/wallets/funds/{fund_id}/receive', |  | ||||||
|         headers={'Authorization': f'Bearer {token}'} |  | ||||||
|     ) |  | ||||||
|     response.raise_for_status() |  | ||||||
|     return response.json() |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Changelog |  | ||||||
|  |  | ||||||
| ### Version 1.0.0 |  | ||||||
| - Initial release with basic red packet functionality |  | ||||||
| - Support for even and random split types |  | ||||||
| - 24-hour expiration with automatic refunds |  | ||||||
| - RESTful API endpoints |  | ||||||
| - Comprehensive error handling |  | ||||||
|  |  | ||||||
| ## Support |  | ||||||
|  |  | ||||||
| For API support or questions: |  | ||||||
| - Check the main documentation at `README_WALLET_FUNDS.md` |  | ||||||
| - Review error messages for specific guidance |  | ||||||
| - Contact the development team for technical issues |  | ||||||
| @@ -1,71 +0,0 @@ | |||||||
| using Microsoft.Extensions.Hosting; |  | ||||||
|  |  | ||||||
| var builder = DistributedApplication.CreateBuilder(args); |  | ||||||
|  |  | ||||||
| var isDev = builder.Environment.IsDevelopment(); |  | ||||||
|  |  | ||||||
| var cache = builder.AddRedis("cache"); |  | ||||||
| var queue = builder.AddNats("queue").WithJetStream(); |  | ||||||
|  |  | ||||||
| var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring"); |  | ||||||
| var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass") |  | ||||||
|     .WithReference(ringService); |  | ||||||
| var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive") |  | ||||||
|     .WithReference(passService) |  | ||||||
|     .WithReference(ringService); |  | ||||||
| var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere") |  | ||||||
|     .WithReference(passService) |  | ||||||
|     .WithReference(ringService) |  | ||||||
|     .WithReference(driveService); |  | ||||||
| var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop") |  | ||||||
|     .WithReference(passService) |  | ||||||
|     .WithReference(ringService) |  | ||||||
|     .WithReference(sphereService); |  | ||||||
| var insightService = builder.AddProject<Projects.DysonNetwork_Insight>("insight") |  | ||||||
|     .WithReference(passService) |  | ||||||
|     .WithReference(ringService) |  | ||||||
|     .WithReference(sphereService) |  | ||||||
|     .WithReference(developService); |  | ||||||
|  |  | ||||||
| passService.WithReference(developService).WithReference(driveService); |  | ||||||
|  |  | ||||||
| List<IResourceBuilder<ProjectResource>> services = |  | ||||||
|     [ringService, passService, driveService, sphereService, developService, insightService]; |  | ||||||
|  |  | ||||||
| for (var idx = 0; idx < services.Count; idx++) |  | ||||||
| { |  | ||||||
|     var service = services[idx]; |  | ||||||
|  |  | ||||||
|     service.WithReference(cache).WithReference(queue); |  | ||||||
|  |  | ||||||
|     var grpcPort = 7002 + idx; |  | ||||||
|  |  | ||||||
|     if (isDev) |  | ||||||
|     { |  | ||||||
|         service.WithEnvironment("GRPC_PORT", grpcPort.ToString()); |  | ||||||
|  |  | ||||||
|         var httpPort = 8001 + idx; |  | ||||||
|         service.WithEnvironment("HTTP_PORTS", httpPort.ToString()); |  | ||||||
|         service.WithHttpEndpoint(httpPort, targetPort: null, isProxied: false, name: "http"); |  | ||||||
|     } |  | ||||||
|     else |  | ||||||
|     { |  | ||||||
|         service.WithHttpEndpoint(8080, targetPort: null, isProxied: false, name: "http"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     service.WithEndpoint(isDev ? grpcPort : 7001, isDev ? null : 7001, "https", name: "grpc", isProxied: false); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Extra double-ended references |  | ||||||
| ringService.WithReference(passService); |  | ||||||
|  |  | ||||||
| var gateway = builder.AddProject<Projects.DysonNetwork_Gateway>("gateway") |  | ||||||
|     .WithEnvironment("HTTP_PORTS", "5001") |  | ||||||
|     .WithHttpEndpoint(port: 5001, targetPort: null, isProxied: false, name: "http"); |  | ||||||
|  |  | ||||||
| foreach (var service in services) |  | ||||||
|     gateway.WithReference(service); |  | ||||||
|  |  | ||||||
| builder.AddDockerComposeEnvironment("docker-compose"); |  | ||||||
|  |  | ||||||
| builder.Build().Run(); |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk"> |  | ||||||
|   <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.2" /> |  | ||||||
|      |  | ||||||
|   <PropertyGroup> |  | ||||||
|     <OutputType>Exe</OutputType> |  | ||||||
|     <TargetFramework>net9.0</TargetFramework> |  | ||||||
|     <ImplicitUsings>enable</ImplicitUsings> |  | ||||||
|     <Nullable>enable</Nullable> |  | ||||||
|     <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId> |  | ||||||
|     <RootNamespace>DysonNetwork.Control</RootNamespace> |  | ||||||
|   </PropertyGroup> |  | ||||||
|      |  | ||||||
|   <ItemGroup> |  | ||||||
|     <PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.2" /> |  | ||||||
|     <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" /> |  | ||||||
|     <PackageReference Include="Aspire.Hosting.Nats" Version="9.5.2" /> |  | ||||||
|     <PackageReference Include="Aspire.Hosting.Redis" Version="9.5.2" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|   <ItemGroup> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" /> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" /> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" /> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" /> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" /> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" /> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Insight\DysonNetwork.Insight.csproj" /> |  | ||||||
|   </ItemGroup> |  | ||||||
| </Project> |  | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| { |  | ||||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", |  | ||||||
|   "profiles": { |  | ||||||
|     "https": { |  | ||||||
|       "commandName": "Project", |  | ||||||
|       "dotnetRunMessages": true, |  | ||||||
|       "launchBrowser": true, |  | ||||||
|       "applicationUrl": "https://localhost:17025;http://localhost:15057", |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development", |  | ||||||
|         "DOTNET_ENVIRONMENT": "Development", |  | ||||||
|         "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175", |  | ||||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189", |  | ||||||
|         "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21260", |  | ||||||
|         "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22052" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "http": { |  | ||||||
|       "commandName": "Project", |  | ||||||
|       "dotnetRunMessages": true, |  | ||||||
|       "launchBrowser": true, |  | ||||||
|       "applicationUrl": "http://localhost:15057", |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development", |  | ||||||
|         "DOTNET_ENVIRONMENT": "Development", |  | ||||||
|         "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163", |  | ||||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185", |  | ||||||
|         "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:22108" |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| { |  | ||||||
|   "Logging": { |  | ||||||
|     "LogLevel": { |  | ||||||
|       "Default": "Information", |  | ||||||
|       "Microsoft.AspNetCore": "Warning" |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "ConnectionStrings": { |  | ||||||
|     "cache": "localhost:6379" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,81 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Design; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop; |  | ||||||
|  |  | ||||||
| public class AppDatabase( |  | ||||||
|     DbContextOptions<AppDatabase> options, |  | ||||||
|     IConfiguration configuration |  | ||||||
| ) : DbContext(options) |  | ||||||
| { |  | ||||||
|     public DbSet<SnDeveloper> Developers { get; set; } = null!; |  | ||||||
|  |  | ||||||
|     public DbSet<SnDevProject> DevProjects { get; set; } = null!; |  | ||||||
|      |  | ||||||
|     public DbSet<SnCustomApp> CustomApps { get; set; } = null!; |  | ||||||
|     public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!; |  | ||||||
|     public DbSet<SnBotAccount> BotAccounts { get; set; } = null!; |  | ||||||
|  |  | ||||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |  | ||||||
|     { |  | ||||||
|         optionsBuilder.UseNpgsql( |  | ||||||
|             configuration.GetConnectionString("App"), |  | ||||||
|             opt => opt |  | ||||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) |  | ||||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) |  | ||||||
|                 .UseNodaTime() |  | ||||||
|         ).UseSnakeCaseNamingConvention(); |  | ||||||
|  |  | ||||||
|         base.OnConfiguring(optionsBuilder); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) |  | ||||||
|     { |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|  |  | ||||||
|         foreach (var entry in ChangeTracker.Entries<ModelBase>()) |  | ||||||
|         { |  | ||||||
|             switch (entry.State) |  | ||||||
|             { |  | ||||||
|                 case EntityState.Added: |  | ||||||
|                     entry.Entity.CreatedAt = now; |  | ||||||
|                     entry.Entity.UpdatedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Modified: |  | ||||||
|                     entry.Entity.UpdatedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Deleted: |  | ||||||
|                     entry.State = EntityState.Modified; |  | ||||||
|                     entry.Entity.DeletedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Detached: |  | ||||||
|                 case EntityState.Unchanged: |  | ||||||
|                 default: |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return await base.SaveChangesAsync(cancellationToken); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) |  | ||||||
|     { |  | ||||||
|         base.OnModelCreating(modelBuilder); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase> |  | ||||||
| { |  | ||||||
|     public AppDatabase CreateDbContext(string[] args) |  | ||||||
|     { |  | ||||||
|         var configuration = new ConfigurationBuilder() |  | ||||||
|             .SetBasePath(Directory.GetCurrentDirectory()) |  | ||||||
|             .AddJsonFile("appsettings.json") |  | ||||||
|             .Build(); |  | ||||||
|  |  | ||||||
|         var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>(); |  | ||||||
|         return new AppDatabase(optionsBuilder.Options, configuration); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base |  | ||||||
| USER $APP_UID |  | ||||||
| WORKDIR /app |  | ||||||
| EXPOSE 8080 |  | ||||||
| EXPOSE 8081 |  | ||||||
|  |  | ||||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build |  | ||||||
| ARG BUILD_CONFIGURATION=Release |  | ||||||
| WORKDIR /src |  | ||||||
| COPY ["DysonNetwork.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"] |  | ||||||
| RUN dotnet restore "DysonNetwork.Develop/DysonNetwork.Develop.csproj" |  | ||||||
| COPY . . |  | ||||||
| WORKDIR "/src/DysonNetwork.Develop" |  | ||||||
| RUN dotnet build "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/build |  | ||||||
|  |  | ||||||
| FROM build AS publish |  | ||||||
| ARG BUILD_CONFIGURATION=Release |  | ||||||
| RUN dotnet publish "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false |  | ||||||
|  |  | ||||||
| FROM base AS final |  | ||||||
| WORKDIR /app |  | ||||||
| COPY --from=publish /app/publish . |  | ||||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Develop.dll"] |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> |  | ||||||
|  |  | ||||||
|     <PropertyGroup> |  | ||||||
|         <TargetFramework>net9.0</TargetFramework> |  | ||||||
|         <Nullable>enable</Nullable> |  | ||||||
|         <ImplicitUsings>enable</ImplicitUsings> |  | ||||||
|         <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> |  | ||||||
|     </PropertyGroup> |  | ||||||
|  |  | ||||||
|     <ItemGroup> |  | ||||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> |  | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> |  | ||||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> |  | ||||||
|             <PrivateAssets>all</PrivateAssets> |  | ||||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |  | ||||||
|         </PackageReference> |  | ||||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> |  | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> |  | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> |  | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> |  | ||||||
|         <PackageReference Include="NodaTime" Version="3.2.2"/> |  | ||||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> |  | ||||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/> |  | ||||||
|   </ItemGroup> |  | ||||||
|   |  | ||||||
|   <ItemGroup> |  | ||||||
|       <Content Include="..\.dockerignore"> |  | ||||||
|         <Link>.dockerignore</Link> |  | ||||||
|       </Content> |  | ||||||
|     </ItemGroup> |  | ||||||
|  |  | ||||||
|   <ItemGroup> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|   |  | ||||||
| </Project> |  | ||||||
| @@ -1,460 +0,0 @@ | |||||||
| using System.ComponentModel.DataAnnotations; |  | ||||||
| using DysonNetwork.Develop.Project; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using DysonNetwork.Shared.Registry; |  | ||||||
| using Grpc.Core; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using NodaTime; |  | ||||||
| using NodaTime.Serialization.Protobuf; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Identity; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/developers/{pubName}/projects/{projectId:guid}/bots")] |  | ||||||
| [Authorize] |  | ||||||
| public class BotAccountController( |  | ||||||
|     BotAccountService botService, |  | ||||||
|     DeveloperService ds, |  | ||||||
|     DevProjectService projectService, |  | ||||||
|     ILogger<BotAccountController> logger, |  | ||||||
|     RemoteAccountService remoteAccounts, |  | ||||||
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver |  | ||||||
| ) |  | ||||||
|     : ControllerBase |  | ||||||
| { |  | ||||||
|     public class CommonBotRequest |  | ||||||
|     { |  | ||||||
|         [MaxLength(256)] public string? FirstName { get; set; } |  | ||||||
|         [MaxLength(256)] public string? MiddleName { get; set; } |  | ||||||
|         [MaxLength(256)] public string? LastName { get; set; } |  | ||||||
|         [MaxLength(1024)] public string? Gender { get; set; } |  | ||||||
|         [MaxLength(1024)] public string? Pronouns { get; set; } |  | ||||||
|         [MaxLength(1024)] public string? TimeZone { get; set; } |  | ||||||
|         [MaxLength(1024)] public string? Location { get; set; } |  | ||||||
|         [MaxLength(4096)] public string? Bio { get; set; } |  | ||||||
|         public Instant? Birthday { get; set; } |  | ||||||
|  |  | ||||||
|         [MaxLength(32)] public string? PictureId { get; set; } |  | ||||||
|         [MaxLength(32)] public string? BackgroundId { get; set; } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public class BotCreateRequest : CommonBotRequest |  | ||||||
|     { |  | ||||||
|         [Required] |  | ||||||
|         [MinLength(2)] |  | ||||||
|         [MaxLength(256)] |  | ||||||
|         [RegularExpression(@"^[A-Za-z0-9_-]+$", |  | ||||||
|             ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.") |  | ||||||
|         ] |  | ||||||
|         public string Name { get; set; } = string.Empty; |  | ||||||
|  |  | ||||||
|         [Required][MaxLength(256)] public string Nick { get; set; } = string.Empty; |  | ||||||
|  |  | ||||||
|         [Required][MaxLength(1024)] public string Slug { get; set; } = string.Empty; |  | ||||||
|  |  | ||||||
|         [MaxLength(128)] public string Language { get; set; } = "en-us"; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public class UpdateBotRequest : CommonBotRequest |  | ||||||
|     { |  | ||||||
|         [MinLength(2)] |  | ||||||
|         [MaxLength(256)] |  | ||||||
|         [RegularExpression(@"^[A-Za-z0-9_-]+$", |  | ||||||
|             ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.") |  | ||||||
|         ] |  | ||||||
|         public string? Name { get; set; } = string.Empty; |  | ||||||
|  |  | ||||||
|         [MaxLength(256)] public string? Nick { get; set; } = string.Empty; |  | ||||||
|  |  | ||||||
|         [Required][MaxLength(1024)] public string? Slug { get; set; } = string.Empty; |  | ||||||
|  |  | ||||||
|         [MaxLength(128)] public string? Language { get; set; } |  | ||||||
|  |  | ||||||
|         public bool? IsActive { get; set; } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet] |  | ||||||
|     public async Task<IActionResult> ListBots( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), |  | ||||||
|                 Shared.Proto.PublisherMemberRole.Viewer)) |  | ||||||
|             return StatusCode(403, "You must be an viewer of the developer to list bots"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var bots = await botService.GetBotsByProjectAsync(projectId); |  | ||||||
|         return Ok(await botService.LoadBotsAccountAsync(bots)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{botId:guid}")] |  | ||||||
|     public async Task<IActionResult> GetBot( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid botId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), |  | ||||||
|                 Shared.Proto.PublisherMemberRole.Viewer)) |  | ||||||
|             return StatusCode(403, "You must be an viewer of the developer to view bot details"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var bot = await botService.GetBotByIdAsync(botId); |  | ||||||
|         if (bot is null || bot.ProjectId != projectId) |  | ||||||
|             return NotFound("Bot not found"); |  | ||||||
|  |  | ||||||
|         return Ok(await botService.LoadBotAccountAsync(bot)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost] |  | ||||||
|     public async Task<IActionResult> CreateBot( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromBody] BotCreateRequest createRequest |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), |  | ||||||
|                 Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to create a bot"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         var accountId = Guid.NewGuid(); |  | ||||||
|         var account = new Account() |  | ||||||
|         { |  | ||||||
|             Id = accountId.ToString(), |  | ||||||
|             Name = createRequest.Name, |  | ||||||
|             Nick = createRequest.Nick, |  | ||||||
|             Language = createRequest.Language, |  | ||||||
|             Profile = new AccountProfile() |  | ||||||
|             { |  | ||||||
|                 Id = Guid.NewGuid().ToString(), |  | ||||||
|                 Bio = createRequest.Bio, |  | ||||||
|                 Gender = createRequest.Gender, |  | ||||||
|                 FirstName = createRequest.FirstName, |  | ||||||
|                 MiddleName = createRequest.MiddleName, |  | ||||||
|                 LastName = createRequest.LastName, |  | ||||||
|                 TimeZone = createRequest.TimeZone, |  | ||||||
|                 Pronouns = createRequest.Pronouns, |  | ||||||
|                 Location = createRequest.Location, |  | ||||||
|                 Birthday = createRequest.Birthday?.ToTimestamp(), |  | ||||||
|                 AccountId = accountId.ToString(), |  | ||||||
|                 CreatedAt = now.ToTimestamp(), |  | ||||||
|                 UpdatedAt = now.ToTimestamp() |  | ||||||
|             }, |  | ||||||
|             CreatedAt = now.ToTimestamp(), |  | ||||||
|             UpdatedAt = now.ToTimestamp() |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var bot = await botService.CreateBotAsync( |  | ||||||
|                 project, |  | ||||||
|                 createRequest.Slug, |  | ||||||
|                 account, |  | ||||||
|                 createRequest.PictureId, |  | ||||||
|                 createRequest.BackgroundId |  | ||||||
|             ); |  | ||||||
|             return Ok(bot); |  | ||||||
|         } |  | ||||||
|         catch (Exception ex) |  | ||||||
|         { |  | ||||||
|             logger.LogError(ex, "Error creating bot account"); |  | ||||||
|             return StatusCode(500, "An error occurred while creating the bot account"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPatch("{botId:guid}")] |  | ||||||
|     public async Task<IActionResult> UpdateBot( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid botId, |  | ||||||
|         [FromBody] UpdateBotRequest request |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), |  | ||||||
|                 Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to update a bot"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var bot = await botService.GetBotByIdAsync(botId); |  | ||||||
|         if (bot is null || bot.ProjectId != projectId) |  | ||||||
|             return NotFound("Bot not found"); |  | ||||||
|  |  | ||||||
|         var botAccount = await remoteAccounts.GetBotAccount(bot.Id); |  | ||||||
|  |  | ||||||
|         if (request.Name is not null) botAccount.Name = request.Name; |  | ||||||
|         if (request.Nick is not null) botAccount.Nick = request.Nick; |  | ||||||
|         if (request.Language is not null) botAccount.Language = request.Language; |  | ||||||
|         if (request.Bio is not null) botAccount.Profile.Bio = request.Bio; |  | ||||||
|         if (request.Gender is not null) botAccount.Profile.Gender = request.Gender; |  | ||||||
|         if (request.FirstName is not null) botAccount.Profile.FirstName = request.FirstName; |  | ||||||
|         if (request.MiddleName is not null) botAccount.Profile.MiddleName = request.MiddleName; |  | ||||||
|         if (request.LastName is not null) botAccount.Profile.LastName = request.LastName; |  | ||||||
|         if (request.TimeZone is not null) botAccount.Profile.TimeZone = request.TimeZone; |  | ||||||
|         if (request.Pronouns is not null) botAccount.Profile.Pronouns = request.Pronouns; |  | ||||||
|         if (request.Location is not null) botAccount.Profile.Location = request.Location; |  | ||||||
|         if (request.Birthday is not null) botAccount.Profile.Birthday = request.Birthday?.ToTimestamp(); |  | ||||||
|  |  | ||||||
|         if (request.Slug is not null) bot.Slug = request.Slug; |  | ||||||
|         if (request.IsActive is not null) bot.IsActive = request.IsActive.Value; |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var updatedBot = await botService.UpdateBotAsync( |  | ||||||
|                 bot, |  | ||||||
|                 botAccount, |  | ||||||
|                 request.PictureId, |  | ||||||
|                 request.BackgroundId |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|             return Ok(updatedBot); |  | ||||||
|         } |  | ||||||
|         catch (Exception ex) |  | ||||||
|         { |  | ||||||
|             logger.LogError(ex, "Error updating bot account {BotId}", botId); |  | ||||||
|             return StatusCode(500, "An error occurred while updating the bot account"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpDelete("{botId:guid}")] |  | ||||||
|     public async Task<IActionResult> DeleteBot( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid botId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), |  | ||||||
|                 Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to delete a bot"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var bot = await botService.GetBotByIdAsync(botId); |  | ||||||
|         if (bot is null || bot.ProjectId != projectId) |  | ||||||
|             return NotFound("Bot not found"); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             await botService.DeleteBotAsync(bot); |  | ||||||
|             return NoContent(); |  | ||||||
|         } |  | ||||||
|         catch (Exception ex) |  | ||||||
|         { |  | ||||||
|             logger.LogError(ex, "Error deleting bot {BotId}", botId); |  | ||||||
|             return StatusCode(500, "An error occurred while deleting the bot account"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{botId:guid}/keys")] |  | ||||||
|     public async Task<ActionResult<List<SnApiKey>>> ListBotKeys( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid botId |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Viewer); |  | ||||||
|         if (developer == null) return NotFound("Developer not found"); |  | ||||||
|         if (project == null) return NotFound("Project not found or you don't have access"); |  | ||||||
|         if (bot == null) return NotFound("Bot not found"); |  | ||||||
|  |  | ||||||
|         var keys = await accountsReceiver.ListApiKeyAsync(new ListApiKeyRequest |  | ||||||
|         { |  | ||||||
|             AutomatedId = bot.Id.ToString() |  | ||||||
|         }); |  | ||||||
|         var data = keys.Data.Select(SnApiKey.FromProtoValue).ToList(); |  | ||||||
|  |  | ||||||
|         return Ok(data); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{botId:guid}/keys/{keyId:guid}")] |  | ||||||
|     public async Task<ActionResult<SnApiKey>> GetBotKey( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid botId, |  | ||||||
|         [FromRoute] Guid keyId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Viewer); |  | ||||||
|         if (developer == null) return NotFound("Developer not found"); |  | ||||||
|         if (project == null) return NotFound("Project not found or you don't have access"); |  | ||||||
|         if (bot == null) return NotFound("Bot not found"); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); |  | ||||||
|             if (key == null) return NotFound("API key not found"); |  | ||||||
|             return Ok(SnApiKey.FromProtoValue(key)); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) |  | ||||||
|         { |  | ||||||
|             return NotFound("API key not found"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public class CreateApiKeyRequest |  | ||||||
|     { |  | ||||||
|         [Required, MaxLength(1024)] |  | ||||||
|         public string Label { get; set; } = null!; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost("{botId:guid}/keys")] |  | ||||||
|     public async Task<ActionResult<SnApiKey>> CreateBotKey( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid botId, |  | ||||||
|         [FromBody] CreateApiKeyRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor); |  | ||||||
|         if (developer == null) return NotFound("Developer not found"); |  | ||||||
|         if (project == null) return NotFound("Project not found or you don't have access"); |  | ||||||
|         if (bot == null) return NotFound("Bot not found"); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var newKey = new ApiKey |  | ||||||
|             { |  | ||||||
|                 AccountId = bot.Id.ToString(), |  | ||||||
|                 Label = request.Label |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey); |  | ||||||
|             return Ok(SnApiKey.FromProtoValue(createdKey)); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument) |  | ||||||
|         { |  | ||||||
|             return BadRequest(ex.Status.Detail); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")] |  | ||||||
|     public async Task<ActionResult<SnApiKey>> RotateBotKey( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid botId, |  | ||||||
|         [FromRoute] Guid keyId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor); |  | ||||||
|         if (developer == null) return NotFound("Developer not found"); |  | ||||||
|         if (project == null) return NotFound("Project not found or you don't have access"); |  | ||||||
|         if (bot == null) return NotFound("Bot not found"); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); |  | ||||||
|             return Ok(SnApiKey.FromProtoValue(rotatedKey)); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) |  | ||||||
|         { |  | ||||||
|             return NotFound("API key not found"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpDelete("{botId:guid}/keys/{keyId:guid}")] |  | ||||||
|     public async Task<IActionResult> DeleteBotKey( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid botId, |  | ||||||
|         [FromRoute] Guid keyId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor); |  | ||||||
|         if (developer == null) return NotFound("Developer not found"); |  | ||||||
|         if (project == null) return NotFound("Project not found or you don't have access"); |  | ||||||
|         if (bot == null) return NotFound("Bot not found"); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             await accountsReceiver.DeleteApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); |  | ||||||
|             return NoContent(); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) |  | ||||||
|         { |  | ||||||
|             return NotFound("API key not found"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task<(SnDeveloper?, SnDevProject?, SnBotAccount?)> ValidateBotAccess( |  | ||||||
|         string pubName, |  | ||||||
|         Guid projectId, |  | ||||||
|         Guid botId, |  | ||||||
|         Account currentUser, |  | ||||||
|         Shared.Proto.PublisherMemberRole requiredRole) |  | ||||||
|     { |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer == null) return (null, null, null); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole)) |  | ||||||
|             return (null, null, null); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project == null) return (developer, null, null); |  | ||||||
|  |  | ||||||
|         var bot = await botService.GetBotByIdAsync(botId); |  | ||||||
|         if (bot == null || bot.ProjectId != projectId) return (developer, project, null); |  | ||||||
|  |  | ||||||
|         return (developer, project, bot); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Identity; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("api/bots")] |  | ||||||
| public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase |  | ||||||
| { |  | ||||||
|     [HttpGet("{botId:guid}")] |  | ||||||
|     public async Task<ActionResult<SnBotAccount>> GetBotTransparentInfo([FromRoute] Guid botId) |  | ||||||
|     { |  | ||||||
|         var bot = await botService.GetBotByIdAsync(botId); |  | ||||||
|         if (bot is null) return NotFound("Bot not found"); |  | ||||||
|         bot = await botService.LoadBotAccountAsync(bot); |  | ||||||
|  |  | ||||||
|         var developer = await developerService.GetDeveloperById(bot!.Project.DeveloperId); |  | ||||||
|         if (developer is null) return NotFound("Developer not found"); |  | ||||||
|         bot.Developer = await developerService.LoadDeveloperPublisher(developer); |  | ||||||
|  |  | ||||||
|         return Ok(bot); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{botId:guid}/developer")] |  | ||||||
|     public async Task<ActionResult<SnDeveloper>> GetBotDeveloper([FromRoute] Guid botId) |  | ||||||
|     { |  | ||||||
|         var bot = await botService.GetBotByIdAsync(botId); |  | ||||||
|         if (bot is null) return NotFound("Bot not found"); |  | ||||||
|          |  | ||||||
|         var developer = await developerService.GetDeveloperById(bot!.Project.DeveloperId); |  | ||||||
|         if (developer is null) return NotFound("Developer not found"); |  | ||||||
|         developer = await developerService.LoadDeveloperPublisher(developer); |  | ||||||
|  |  | ||||||
|         return Ok(developer); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,172 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using DysonNetwork.Shared.Registry; |  | ||||||
| using Grpc.Core; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime.Serialization.Protobuf; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Identity; |  | ||||||
|  |  | ||||||
| public class BotAccountService( |  | ||||||
|     AppDatabase db, |  | ||||||
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver, |  | ||||||
|     RemoteAccountService remoteAccounts |  | ||||||
| ) |  | ||||||
| { |  | ||||||
|     public async Task<SnBotAccount?> GetBotByIdAsync(Guid id) |  | ||||||
|     { |  | ||||||
|         return await db.BotAccounts |  | ||||||
|             .Include(b => b.Project) |  | ||||||
|             .FirstOrDefaultAsync(b => b.Id == id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<List<SnBotAccount>> GetBotsByProjectAsync(Guid projectId) |  | ||||||
|     { |  | ||||||
|         return await db.BotAccounts |  | ||||||
|             .Where(b => b.ProjectId == projectId) |  | ||||||
|             .ToListAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnBotAccount> CreateBotAsync( |  | ||||||
|         SnDevProject project, |  | ||||||
|         string slug, |  | ||||||
|         Account account, |  | ||||||
|         string? pictureId, |  | ||||||
|         string? backgroundId |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         // First, check if a bot with this slug already exists in this project |  | ||||||
|         var existingBot = await db.BotAccounts |  | ||||||
|             .FirstOrDefaultAsync(b => b.ProjectId == project.Id && b.Slug == slug); |  | ||||||
|  |  | ||||||
|         if (existingBot != null) |  | ||||||
|             throw new InvalidOperationException("A bot with this slug already exists in this project."); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var automatedId = Guid.NewGuid(); |  | ||||||
|             var createRequest = new CreateBotAccountRequest |  | ||||||
|             { |  | ||||||
|                 AutomatedId = automatedId.ToString(), |  | ||||||
|                 Account = account, |  | ||||||
|                 PictureId = pictureId, |  | ||||||
|                 BackgroundId = backgroundId |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             var createResponse = await accountReceiver.CreateBotAccountAsync(createRequest); |  | ||||||
|             var botAccount = createResponse.Bot; |  | ||||||
|  |  | ||||||
|             // Then create the local bot account |  | ||||||
|             var bot = new SnBotAccount |  | ||||||
|             { |  | ||||||
|                 Id = automatedId, |  | ||||||
|                 Slug = slug, |  | ||||||
|                 ProjectId = project.Id, |  | ||||||
|                 Project = project, |  | ||||||
|                 IsActive = botAccount.IsActive, |  | ||||||
|                 CreatedAt = botAccount.CreatedAt.ToInstant(), |  | ||||||
|                 UpdatedAt = botAccount.UpdatedAt.ToInstant() |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             db.BotAccounts.Add(bot); |  | ||||||
|             await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|             return bot; |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists) |  | ||||||
|         { |  | ||||||
|             throw new InvalidOperationException( |  | ||||||
|                 "A bot account with this ID already exists in the authentication service.", ex); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument) |  | ||||||
|         { |  | ||||||
|             throw new ArgumentException($"Invalid bot account data: {ex.Status.Detail}", ex); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) |  | ||||||
|         { |  | ||||||
|             throw new Exception($"Failed to create bot account: {ex.Status.Detail}", ex); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnBotAccount> UpdateBotAsync( |  | ||||||
|         SnBotAccount bot, |  | ||||||
|         Account account, |  | ||||||
|         string? pictureId, |  | ||||||
|         string? backgroundId |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         db.Update(bot); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             // Update the bot account in the Pass service |  | ||||||
|             var updateRequest = new UpdateBotAccountRequest |  | ||||||
|             { |  | ||||||
|                 AutomatedId = bot.Id.ToString(), |  | ||||||
|                 Account = account, |  | ||||||
|                 PictureId = pictureId, |  | ||||||
|                 BackgroundId = backgroundId |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             var updateResponse = await accountReceiver.UpdateBotAccountAsync(updateRequest); |  | ||||||
|             var updatedBot = updateResponse.Bot; |  | ||||||
|  |  | ||||||
|             // Update local bot account |  | ||||||
|             bot.UpdatedAt = updatedBot.UpdatedAt.ToInstant(); |  | ||||||
|             bot.IsActive = updatedBot.IsActive; |  | ||||||
|             await db.SaveChangesAsync(); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound) |  | ||||||
|         { |  | ||||||
|             throw new Exception("Bot account not found in the authentication service", ex); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) |  | ||||||
|         { |  | ||||||
|             throw new Exception($"Failed to update bot account: {ex.Status.Detail}", ex); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return bot; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task DeleteBotAsync(SnBotAccount bot) |  | ||||||
|     { |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             // Delete the bot account from the Pass service |  | ||||||
|             var deleteRequest = new DeleteBotAccountRequest |  | ||||||
|             { |  | ||||||
|                 AutomatedId = bot.Id.ToString(), |  | ||||||
|                 Force = false |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             await accountReceiver.DeleteBotAccountAsync(deleteRequest); |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound) |  | ||||||
|         { |  | ||||||
|             // Account not found in Pass service, continue with local deletion |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Delete the local bot account |  | ||||||
|         db.BotAccounts.Remove(bot); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnBotAccount?> LoadBotAccountAsync(SnBotAccount bot) => |  | ||||||
|         (await LoadBotsAccountAsync([bot])).FirstOrDefault(); |  | ||||||
|  |  | ||||||
|     public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots) |  | ||||||
|     { |  | ||||||
|         var automatedIds = bots.Select(b => b.Id).ToList(); |  | ||||||
|         var data = await remoteAccounts.GetBotAccountBatch(automatedIds); |  | ||||||
|  |  | ||||||
|         foreach (var bot in bots) |  | ||||||
|         { |  | ||||||
|             bot.Account = data |  | ||||||
|                 .Select(SnAccount.FromProtoValue) |  | ||||||
|                 .FirstOrDefault(e => e.AutomatedId == bot.Id); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return bots; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,432 +0,0 @@ | |||||||
| using System.ComponentModel.DataAnnotations; |  | ||||||
| using DysonNetwork.Develop.Project; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Identity; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/developers/{pubName}/projects/{projectId:guid}/apps")] |  | ||||||
| public class CustomAppController(CustomAppService customApps, DeveloperService ds, DevProjectService projectService) |  | ||||||
|     : ControllerBase |  | ||||||
| { |  | ||||||
|     public record CustomAppRequest( |  | ||||||
|         [MaxLength(1024)] string? Slug, |  | ||||||
|         [MaxLength(1024)] string? Name, |  | ||||||
|         [MaxLength(4096)] string? Description, |  | ||||||
|         string? PictureId, |  | ||||||
|         string? BackgroundId, |  | ||||||
|         Shared.Models.CustomAppStatus? Status, |  | ||||||
|         SnCustomAppLinks? Links, |  | ||||||
|         SnCustomAppOauthConfig? OauthConfig |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     public record CreateSecretRequest( |  | ||||||
|         [MaxLength(4096)] string? Description, |  | ||||||
|         TimeSpan? ExpiresIn = null, |  | ||||||
|         bool IsOidc = false |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     public record SecretResponse( |  | ||||||
|         string Id, |  | ||||||
|         string? Secret, |  | ||||||
|         string? Description, |  | ||||||
|         Instant? ExpiresAt, |  | ||||||
|         bool IsOidc, |  | ||||||
|         Instant CreatedAt, |  | ||||||
|         Instant UpdatedAt |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     [HttpGet] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> ListApps([FromRoute] string pubName, [FromRoute] Guid projectId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer)) |  | ||||||
|             return StatusCode(403, "You must be a viewer of the developer to list custom apps"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         var apps = await customApps.GetAppsByProjectAsync(projectId); |  | ||||||
|         return Ok(apps); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{appId:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> GetApp([FromRoute] string pubName, [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid appId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|          |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) return NotFound(); |  | ||||||
|          |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer)) |  | ||||||
|             return StatusCode(403, "You must be a viewer of the developer to list custom apps"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         var app = await customApps.GetAppAsync(appId, projectId); |  | ||||||
|         if (app == null) |  | ||||||
|             return NotFound(); |  | ||||||
|  |  | ||||||
|         return Ok(app); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> CreateApp( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromBody] CustomAppRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to create a custom app"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Slug)) |  | ||||||
|             return BadRequest("Name and slug are required"); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var app = await customApps.CreateAppAsync(projectId, request); |  | ||||||
|             if (app == null) |  | ||||||
|                 return BadRequest("Failed to create app"); |  | ||||||
|  |  | ||||||
|             return CreatedAtAction( |  | ||||||
|                 nameof(GetApp), |  | ||||||
|                 new { pubName, projectId, appId = app.Id }, |  | ||||||
|                 app |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         catch (InvalidOperationException ex) |  | ||||||
|         { |  | ||||||
|             return BadRequest(ex.Message); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPatch("{appId:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> UpdateApp( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid appId, |  | ||||||
|         [FromBody] CustomAppRequest request |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to update a custom app"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var app = await customApps.GetAppAsync(appId, projectId); |  | ||||||
|         if (app == null) |  | ||||||
|             return NotFound(); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             app = await customApps.UpdateAppAsync(app, request); |  | ||||||
|             return Ok(app); |  | ||||||
|         } |  | ||||||
|         catch (InvalidOperationException ex) |  | ||||||
|         { |  | ||||||
|             return BadRequest(ex.Message); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpDelete("{appId:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> DeleteApp( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid appId |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to delete a custom app"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var app = await customApps.GetAppAsync(appId, projectId); |  | ||||||
|         if (app == null) |  | ||||||
|             return NotFound(); |  | ||||||
|  |  | ||||||
|         var result = await customApps.DeleteAppAsync(appId); |  | ||||||
|         if (!result) |  | ||||||
|             return NotFound(); |  | ||||||
|  |  | ||||||
|         return NoContent(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{appId:guid}/secrets")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> ListSecrets( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid appId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to view app secrets"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var app = await customApps.GetAppAsync(appId, projectId); |  | ||||||
|         if (app == null) |  | ||||||
|             return NotFound("App not found"); |  | ||||||
|  |  | ||||||
|         var secrets = await customApps.GetAppSecretsAsync(appId); |  | ||||||
|         return Ok(secrets.Select(s => new SecretResponse( |  | ||||||
|             s.Id.ToString(), |  | ||||||
|             null, |  | ||||||
|             s.Description, |  | ||||||
|             s.ExpiredAt, |  | ||||||
|             s.IsOidc, |  | ||||||
|             s.CreatedAt, |  | ||||||
|             s.UpdatedAt |  | ||||||
|         ))); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost("{appId:guid}/secrets")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> CreateSecret( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid appId, |  | ||||||
|         [FromBody] CreateSecretRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to create app secrets"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var app = await customApps.GetAppAsync(appId, projectId); |  | ||||||
|         if (app == null) |  | ||||||
|             return NotFound("App not found"); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var secret = await customApps.CreateAppSecretAsync(new SnCustomAppSecret |  | ||||||
|             { |  | ||||||
|                 AppId = appId, |  | ||||||
|                 Description = request.Description, |  | ||||||
|                 ExpiredAt = request.ExpiresIn.HasValue |  | ||||||
|                     ? NodaTime.SystemClock.Instance.GetCurrentInstant() |  | ||||||
|                         .Plus(Duration.FromTimeSpan(request.ExpiresIn.Value)) |  | ||||||
|                     : (NodaTime.Instant?)null, |  | ||||||
|                 IsOidc = request.IsOidc |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             return CreatedAtAction( |  | ||||||
|                 nameof(GetSecret), |  | ||||||
|                 new { pubName, projectId, appId, secretId = secret.Id }, |  | ||||||
|                 new SecretResponse( |  | ||||||
|                     secret.Id.ToString(), |  | ||||||
|                     secret.Secret, |  | ||||||
|                     secret.Description, |  | ||||||
|                     secret.ExpiredAt, |  | ||||||
|                     secret.IsOidc, |  | ||||||
|                     secret.CreatedAt, |  | ||||||
|                     secret.UpdatedAt |  | ||||||
|                 ) |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         catch (InvalidOperationException ex) |  | ||||||
|         { |  | ||||||
|             return BadRequest(ex.Message); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{appId:guid}/secrets/{secretId:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> GetSecret( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid appId, |  | ||||||
|         [FromRoute] Guid secretId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to view app secrets"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var app = await customApps.GetAppAsync(appId, projectId); |  | ||||||
|         if (app == null) |  | ||||||
|             return NotFound("App not found"); |  | ||||||
|  |  | ||||||
|         var secret = await customApps.GetAppSecretAsync(secretId, appId); |  | ||||||
|         if (secret == null) |  | ||||||
|             return NotFound("Secret not found"); |  | ||||||
|  |  | ||||||
|         return Ok(new SecretResponse( |  | ||||||
|             secret.Id.ToString(), |  | ||||||
|             null, |  | ||||||
|             secret.Description, |  | ||||||
|             secret.ExpiredAt, |  | ||||||
|             secret.IsOidc, |  | ||||||
|             secret.CreatedAt, |  | ||||||
|             secret.UpdatedAt |  | ||||||
|         )); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpDelete("{appId:guid}/secrets/{secretId:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> DeleteSecret( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid appId, |  | ||||||
|         [FromRoute] Guid secretId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to delete app secrets"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var app = await customApps.GetAppAsync(appId, projectId); |  | ||||||
|         if (app == null) |  | ||||||
|             return NotFound("App not found"); |  | ||||||
|  |  | ||||||
|         var secret = await customApps.GetAppSecretAsync(secretId, appId); |  | ||||||
|         if (secret == null) |  | ||||||
|             return NotFound("Secret not found"); |  | ||||||
|  |  | ||||||
|         var result = await customApps.DeleteAppSecretAsync(secretId, appId); |  | ||||||
|         if (!result) |  | ||||||
|             return NotFound("Failed to delete secret"); |  | ||||||
|  |  | ||||||
|         return NoContent(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost("{appId:guid}/secrets/{secretId:guid}/rotate")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> RotateSecret( |  | ||||||
|         [FromRoute] string pubName, |  | ||||||
|         [FromRoute] Guid projectId, |  | ||||||
|         [FromRoute] Guid appId, |  | ||||||
|         [FromRoute] Guid secretId, |  | ||||||
|         [FromBody] CreateSecretRequest? request = null) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await ds.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|  |  | ||||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to rotate app secrets"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound("Project not found or you don't have access"); |  | ||||||
|  |  | ||||||
|         var app = await customApps.GetAppAsync(appId, projectId); |  | ||||||
|         if (app == null) |  | ||||||
|             return NotFound("App not found"); |  | ||||||
|  |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var secret = await customApps.RotateAppSecretAsync(new SnCustomAppSecret |  | ||||||
|             { |  | ||||||
|                 Id = secretId, |  | ||||||
|                 AppId = appId, |  | ||||||
|                 Description = request?.Description, |  | ||||||
|                 ExpiredAt = request?.ExpiresIn.HasValue == true |  | ||||||
|                     ? NodaTime.SystemClock.Instance.GetCurrentInstant() |  | ||||||
|                         .Plus(Duration.FromTimeSpan(request.ExpiresIn.Value)) |  | ||||||
|                     : (NodaTime.Instant?)null, |  | ||||||
|                 IsOidc = request?.IsOidc ?? false |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             return Ok(new SecretResponse( |  | ||||||
|                 secret.Id.ToString(), |  | ||||||
|                 secret.Secret, |  | ||||||
|                 secret.Description, |  | ||||||
|                 secret.ExpiredAt, |  | ||||||
|                 secret.IsOidc, |  | ||||||
|                 secret.CreatedAt, |  | ||||||
|                 secret.UpdatedAt |  | ||||||
|             )); |  | ||||||
|         } |  | ||||||
|         catch (InvalidOperationException ex) |  | ||||||
|         { |  | ||||||
|             return BadRequest(ex.Message); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,268 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using System.Security.Cryptography; |  | ||||||
| using System.Text; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Identity; |  | ||||||
|  |  | ||||||
| public class CustomAppService( |  | ||||||
|     AppDatabase db, |  | ||||||
|     FileReferenceService.FileReferenceServiceClient fileRefs, |  | ||||||
|     FileService.FileServiceClient files |  | ||||||
| ) |  | ||||||
| { |  | ||||||
|     public async Task<SnCustomApp?> CreateAppAsync( |  | ||||||
|         Guid projectId, |  | ||||||
|         CustomAppController.CustomAppRequest request |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         var project = await db.DevProjects |  | ||||||
|             .Include(p => p.Developer) |  | ||||||
|             .FirstOrDefaultAsync(p => p.Id == projectId); |  | ||||||
|              |  | ||||||
|         if (project == null) |  | ||||||
|             return null; |  | ||||||
|              |  | ||||||
|         var app = new SnCustomApp |  | ||||||
|         { |  | ||||||
|             Slug = request.Slug!, |  | ||||||
|             Name = request.Name!, |  | ||||||
|             Description = request.Description, |  | ||||||
|             Status = request.Status ?? Shared.Models.CustomAppStatus.Developing, |  | ||||||
|             Links = request.Links, |  | ||||||
|             OauthConfig = request.OauthConfig, |  | ||||||
|             ProjectId = projectId |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (request.PictureId is not null) |  | ||||||
|         { |  | ||||||
|             var picture = await files.GetFileAsync( |  | ||||||
|                 new GetFileRequest |  | ||||||
|                 { |  | ||||||
|                     Id = request.PictureId |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|             if (picture is null) |  | ||||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); |  | ||||||
|             app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture); |  | ||||||
|  |  | ||||||
|             // Create a new reference |  | ||||||
|             await fileRefs.CreateReferenceAsync( |  | ||||||
|                 new CreateReferenceRequest |  | ||||||
|                 { |  | ||||||
|                     FileId = picture.Id, |  | ||||||
|                     Usage = "custom-apps.picture", |  | ||||||
|                     ResourceId = app.ResourceIdentifier |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         if (request.BackgroundId is not null) |  | ||||||
|         { |  | ||||||
|             var background = await files.GetFileAsync( |  | ||||||
|                 new GetFileRequest { Id = request.BackgroundId } |  | ||||||
|             ); |  | ||||||
|             if (background is null) |  | ||||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); |  | ||||||
|             app.Background = SnCloudFileReferenceObject.FromProtoValue(background); |  | ||||||
|  |  | ||||||
|             // Create a new reference |  | ||||||
|             await fileRefs.CreateReferenceAsync( |  | ||||||
|                 new CreateReferenceRequest |  | ||||||
|                 { |  | ||||||
|                     FileId = background.Id, |  | ||||||
|                     Usage = "custom-apps.background", |  | ||||||
|                     ResourceId = app.ResourceIdentifier |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         db.CustomApps.Add(app); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         return app; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnCustomApp?> GetAppAsync(Guid id, Guid? projectId = null) |  | ||||||
|     { |  | ||||||
|         var query = db.CustomApps.AsQueryable(); |  | ||||||
|          |  | ||||||
|         if (projectId.HasValue) |  | ||||||
|         { |  | ||||||
|             query = query.Where(a => a.ProjectId == projectId.Value); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return await query.FirstOrDefaultAsync(a => a.Id == id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<List<SnCustomAppSecret>> GetAppSecretsAsync(Guid appId) |  | ||||||
|     { |  | ||||||
|         return await db.CustomAppSecrets |  | ||||||
|             .Where(s => s.AppId == appId) |  | ||||||
|             .OrderByDescending(s => s.CreatedAt) |  | ||||||
|             .ToListAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnCustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId) |  | ||||||
|     { |  | ||||||
|         return await db.CustomAppSecrets |  | ||||||
|             .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnCustomAppSecret> CreateAppSecretAsync(SnCustomAppSecret secret) |  | ||||||
|     { |  | ||||||
|         if (string.IsNullOrWhiteSpace(secret.Secret)) |  | ||||||
|         { |  | ||||||
|             // Generate a new random secret if not provided |  | ||||||
|             secret.Secret = GenerateRandomSecret(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         secret.Id = Guid.NewGuid(); |  | ||||||
|         secret.CreatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         secret.UpdatedAt = secret.CreatedAt; |  | ||||||
|  |  | ||||||
|         db.CustomAppSecrets.Add(secret); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         return secret; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<bool> DeleteAppSecretAsync(Guid secretId, Guid appId) |  | ||||||
|     { |  | ||||||
|         var secret = await db.CustomAppSecrets |  | ||||||
|             .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId); |  | ||||||
|  |  | ||||||
|         if (secret == null) |  | ||||||
|             return false; |  | ||||||
|  |  | ||||||
|         db.CustomAppSecrets.Remove(secret); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnCustomAppSecret> RotateAppSecretAsync(SnCustomAppSecret secretUpdate) |  | ||||||
|     { |  | ||||||
|         var existingSecret = await db.CustomAppSecrets |  | ||||||
|             .FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId); |  | ||||||
|  |  | ||||||
|         if (existingSecret == null) |  | ||||||
|             throw new InvalidOperationException("Secret not found"); |  | ||||||
|  |  | ||||||
|         // Update the existing secret with new values |  | ||||||
|         existingSecret.Secret = GenerateRandomSecret(); |  | ||||||
|         existingSecret.Description = secretUpdate.Description ?? existingSecret.Description; |  | ||||||
|         existingSecret.ExpiredAt = secretUpdate.ExpiredAt ?? existingSecret.ExpiredAt; |  | ||||||
|         existingSecret.IsOidc = secretUpdate.IsOidc; |  | ||||||
|         existingSecret.UpdatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         return existingSecret; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static string GenerateRandomSecret(int length = 64) |  | ||||||
|     { |  | ||||||
|         const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-._~+"; |  | ||||||
|         var res = new StringBuilder(); |  | ||||||
|         using (var rng = RandomNumberGenerator.Create()) |  | ||||||
|         { |  | ||||||
|             var uintBuffer = new byte[sizeof(uint)]; |  | ||||||
|             while (length-- > 0) |  | ||||||
|             { |  | ||||||
|                 rng.GetBytes(uintBuffer); |  | ||||||
|                 var num = BitConverter.ToUInt32(uintBuffer, 0); |  | ||||||
|                 res.Append(valid[(int)(num % (uint)valid.Length)]); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return res.ToString(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<List<SnCustomApp>> GetAppsByProjectAsync(Guid projectId) |  | ||||||
|     { |  | ||||||
|         return await db.CustomApps |  | ||||||
|             .Where(a => a.ProjectId == projectId) |  | ||||||
|             .ToListAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnCustomApp?> UpdateAppAsync(SnCustomApp app, CustomAppController.CustomAppRequest request) |  | ||||||
|     { |  | ||||||
|         if (request.Slug is not null) |  | ||||||
|             app.Slug = request.Slug; |  | ||||||
|         if (request.Name is not null) |  | ||||||
|             app.Name = request.Name; |  | ||||||
|         if (request.Description is not null) |  | ||||||
|             app.Description = request.Description; |  | ||||||
|         if (request.Status is not null) |  | ||||||
|             app.Status = request.Status.Value; |  | ||||||
|         if (request.Links is not null) |  | ||||||
|             app.Links = request.Links; |  | ||||||
|         if (request.OauthConfig is not null) |  | ||||||
|             app.OauthConfig = request.OauthConfig; |  | ||||||
|  |  | ||||||
|         if (request.PictureId is not null) |  | ||||||
|         { |  | ||||||
|             var picture = await files.GetFileAsync( |  | ||||||
|                 new GetFileRequest |  | ||||||
|                 { |  | ||||||
|                     Id = request.PictureId |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|             if (picture is null) |  | ||||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); |  | ||||||
|             app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture); |  | ||||||
|  |  | ||||||
|             // Create a new reference |  | ||||||
|             await fileRefs.CreateReferenceAsync( |  | ||||||
|                 new CreateReferenceRequest |  | ||||||
|                 { |  | ||||||
|                     FileId = picture.Id, |  | ||||||
|                     Usage = "custom-apps.picture", |  | ||||||
|                     ResourceId = app.ResourceIdentifier |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         if (request.BackgroundId is not null) |  | ||||||
|         { |  | ||||||
|             var background = await files.GetFileAsync( |  | ||||||
|                 new GetFileRequest { Id = request.BackgroundId } |  | ||||||
|             ); |  | ||||||
|             if (background is null) |  | ||||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); |  | ||||||
|             app.Background = SnCloudFileReferenceObject.FromProtoValue(background); |  | ||||||
|  |  | ||||||
|             // Create a new reference |  | ||||||
|             await fileRefs.CreateReferenceAsync( |  | ||||||
|                 new CreateReferenceRequest |  | ||||||
|                 { |  | ||||||
|                     FileId = background.Id, |  | ||||||
|                     Usage = "custom-apps.background", |  | ||||||
|                     ResourceId = app.ResourceIdentifier |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         db.Update(app); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         return app; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<bool> DeleteAppAsync(Guid id) |  | ||||||
|     { |  | ||||||
|         var app = await db.CustomApps.FindAsync(id); |  | ||||||
|         if (app == null) |  | ||||||
|         { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         db.CustomApps.Remove(app); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest |  | ||||||
|             { |  | ||||||
|                 ResourceId = app.ResourceIdentifier |  | ||||||
|             } |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,69 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Grpc.Core; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Identity; |  | ||||||
|  |  | ||||||
| public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppService.CustomAppServiceBase |  | ||||||
| { |  | ||||||
|     public override async Task<GetCustomAppResponse> GetCustomApp(GetCustomAppRequest request, ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var q = db.CustomApps.AsQueryable(); |  | ||||||
|         switch (request.QueryCase) |  | ||||||
|         { |  | ||||||
|             case GetCustomAppRequest.QueryOneofCase.Id when !string.IsNullOrWhiteSpace(request.Id): |  | ||||||
|             { |  | ||||||
|                 if (!Guid.TryParse(request.Id, out var id)) |  | ||||||
|                     throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid id")); |  | ||||||
|                 var appById = await q.FirstOrDefaultAsync(a => a.Id == id); |  | ||||||
|                 if (appById is null) |  | ||||||
|                     throw new RpcException(new Status(StatusCode.NotFound, "app not found")); |  | ||||||
|                 return new GetCustomAppResponse { App = appById.ToProto() }; |  | ||||||
|             } |  | ||||||
|             case GetCustomAppRequest.QueryOneofCase.Slug when !string.IsNullOrWhiteSpace(request.Slug): |  | ||||||
|             { |  | ||||||
|                 var appBySlug = await q.FirstOrDefaultAsync(a => a.Slug == request.Slug); |  | ||||||
|                 if (appBySlug is null) |  | ||||||
|                     throw new RpcException(new Status(StatusCode.NotFound, "app not found")); |  | ||||||
|                 return new GetCustomAppResponse { App = appBySlug.ToProto() }; |  | ||||||
|             } |  | ||||||
|             default: |  | ||||||
|                 throw new RpcException(new Status(StatusCode.InvalidArgument, "id or slug required")); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<CheckCustomAppSecretResponse> CheckCustomAppSecret(CheckCustomAppSecretRequest request, ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         if (string.IsNullOrEmpty(request.Secret)) |  | ||||||
|             throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required")); |  | ||||||
|  |  | ||||||
|         IQueryable<SnCustomAppSecret> q = db.CustomAppSecrets; |  | ||||||
|         switch (request.SecretIdentifierCase) |  | ||||||
|         { |  | ||||||
|             case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId: |  | ||||||
|             { |  | ||||||
|                 if (!Guid.TryParse(request.SecretId, out var sid)) |  | ||||||
|                     throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid secret_id")); |  | ||||||
|                 q = q.Where(s => s.Id == sid); |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|             case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.AppId: |  | ||||||
|             { |  | ||||||
|                 if (!Guid.TryParse(request.AppId, out var aid)) |  | ||||||
|                     throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid app_id")); |  | ||||||
|                 q = q.Where(s => s.AppId == aid); |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|             default: |  | ||||||
|                 throw new RpcException(new Status(StatusCode.InvalidArgument, "secret_id or app_id required")); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (request.HasIsOidc) |  | ||||||
|             q = q.Where(s => s.IsOidc == request.IsOidc); |  | ||||||
|  |  | ||||||
|         var now = NodaTime.SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         var exists = await q.AnyAsync(s => s.Secret == request.Secret && (s.ExpiredAt == null || s.ExpiredAt > now)); |  | ||||||
|         return new CheckCustomAppSecretResponse { Valid = exists }; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,129 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Grpc.Core; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Identity; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/developers")] |  | ||||||
| public class DeveloperController( |  | ||||||
|     AppDatabase db, |  | ||||||
|     PublisherService.PublisherServiceClient ps, |  | ||||||
|     ActionLogService.ActionLogServiceClient als, |  | ||||||
|     DeveloperService ds |  | ||||||
| ) |  | ||||||
|     : ControllerBase |  | ||||||
| { |  | ||||||
|     [HttpGet("{name}")] |  | ||||||
|     public async Task<ActionResult<SnDeveloper>> GetDeveloper(string name) |  | ||||||
|     { |  | ||||||
|         var developer = await ds.GetDeveloperByName(name); |  | ||||||
|         if (developer is null) return NotFound(); |  | ||||||
|         return Ok(await ds.LoadDeveloperPublisher(developer)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{name}/stats")] |  | ||||||
|     public async Task<ActionResult<DeveloperStats>> GetDeveloperStats(string name) |  | ||||||
|     { |  | ||||||
|         var developer = await ds.GetDeveloperByName(name); |  | ||||||
|         if (developer is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         // Get custom apps count |  | ||||||
|         var customAppsCount = await db.CustomApps |  | ||||||
|             .Include(a => a.Project) |  | ||||||
|             .Where(a => a.Project.DeveloperId == developer.Id) |  | ||||||
|             .CountAsync(); |  | ||||||
|  |  | ||||||
|         var stats = new DeveloperStats |  | ||||||
|         { |  | ||||||
|             TotalCustomApps = customAppsCount |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         return Ok(stats); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult<List<SnDeveloper>>> ListJoinedDevelopers() |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|          |  | ||||||
|         var pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id }); |  | ||||||
|         var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList(); |  | ||||||
|  |  | ||||||
|         var developerQuery = db.Developers |  | ||||||
|             .Where(d => pubIds.Contains(d.PublisherId)) |  | ||||||
|             .AsQueryable(); |  | ||||||
|          |  | ||||||
|         var totalCount = await developerQuery.CountAsync();  |  | ||||||
|         Response.Headers.Append("X-Total", totalCount.ToString()); |  | ||||||
|          |  | ||||||
|         var developers = await developerQuery.ToListAsync(); |  | ||||||
|  |  | ||||||
|         return Ok(await ds.LoadDeveloperPublisher(developers)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost("{name}/enroll")] |  | ||||||
|     [Authorize] |  | ||||||
|     [RequiredPermission("global", "developers.create")] |  | ||||||
|     public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         SnPublisher? pub; |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name }); |  | ||||||
|             pub = SnPublisher.FromProtoValue(pubResponse.Publisher); |  | ||||||
|         } catch (RpcException ex) |  | ||||||
|         { |  | ||||||
|             return NotFound(ex.Status.Detail); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Check if the user is an owner of the publisher |  | ||||||
|         var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest |  | ||||||
|         { |  | ||||||
|             PublisherId = pub.Id.ToString(), |  | ||||||
|             AccountId = currentUser.Id, |  | ||||||
|             Role = Shared.Proto.PublisherMemberRole.Owner |  | ||||||
|         }); |  | ||||||
|         if (!permResponse.Valid) return StatusCode(403, "You must be the owner of the publisher to join the developer program"); |  | ||||||
|  |  | ||||||
|         var hasDeveloper = await db.Developers.AnyAsync(d => d.PublisherId == pub.Id); |  | ||||||
|         if (hasDeveloper) return BadRequest("Publisher is already in the developer program"); |  | ||||||
|          |  | ||||||
|         var developer = new SnDeveloper |  | ||||||
|         { |  | ||||||
|             Id = Guid.NewGuid(), |  | ||||||
|             PublisherId = pub.Id |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         db.Developers.Add(developer); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |  | ||||||
|         { |  | ||||||
|             Action = "developers.enroll", |  | ||||||
|             Meta =  |  | ||||||
|             {  |  | ||||||
|                 { "publisher_id", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Id.ToString()) }, |  | ||||||
|                 { "publisher_name", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Name) } |  | ||||||
|             }, |  | ||||||
|             AccountId = currentUser.Id, |  | ||||||
|             UserAgent = Request.Headers.UserAgent, |  | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         return Ok(await ds.LoadDeveloperPublisher(developer)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public class DeveloperStats |  | ||||||
|     { |  | ||||||
|         public int TotalCustomApps { get; set; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,76 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Grpc.Core; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Identity; |  | ||||||
|  |  | ||||||
| public class DeveloperService( |  | ||||||
|     AppDatabase db, |  | ||||||
|     PublisherService.PublisherServiceClient ps, |  | ||||||
|     ILogger<DeveloperService> logger) |  | ||||||
| { |  | ||||||
|     public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer) |  | ||||||
|     { |  | ||||||
|         var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() }); |  | ||||||
|         developer.Publisher = SnPublisher.FromProtoValue(pubResponse.Publisher); |  | ||||||
|         return developer; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     public async Task<IEnumerable<SnDeveloper>> LoadDeveloperPublisher(IEnumerable<SnDeveloper> developers) |  | ||||||
|     { |  | ||||||
|         var enumerable = developers.ToList(); |  | ||||||
|         var pubIds = enumerable.Select(d => d.PublisherId).ToList(); |  | ||||||
|         var pubRequest = new GetPublisherBatchRequest(); |  | ||||||
|         pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString())); |  | ||||||
|         var pubResponse = await ps.GetPublisherBatchAsync(pubRequest); |  | ||||||
|         var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProtoValue); |  | ||||||
|  |  | ||||||
|         return enumerable.Select(d => |  | ||||||
|         { |  | ||||||
|             d.Publisher = pubs[d.PublisherId]; |  | ||||||
|             return d; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnDeveloper?> GetDeveloperByName(string name) |  | ||||||
|     { |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name }); |  | ||||||
|             var pubId = Guid.Parse(pubResponse.Publisher.Id); |  | ||||||
|  |  | ||||||
|             var developer = await db.Developers.FirstOrDefaultAsync(d => d.PublisherId == pubId); |  | ||||||
|             return developer; |  | ||||||
|         } |  | ||||||
|         catch (RpcException ex) |  | ||||||
|         { |  | ||||||
|             logger.LogError(ex, "Developer {name} not found", name); |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnDeveloper?> GetDeveloperById(Guid id) |  | ||||||
|     { |  | ||||||
|         return await db.Developers.FirstOrDefaultAsync(d => d.Id == id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, Shared.Proto.PublisherMemberRole role) |  | ||||||
|     { |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest |  | ||||||
|             { |  | ||||||
|                 PublisherId = pubId.ToString(), |  | ||||||
|                 AccountId = accountId.ToString(), |  | ||||||
|                 Role = role |  | ||||||
|             }); |  | ||||||
|             return permResponse.Valid; |  | ||||||
|         } |  | ||||||
|         catch (RpcException) |  | ||||||
|         { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,202 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using DysonNetwork.Develop; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250807133702_InitialMigration")] |  | ||||||
|     partial class InitialMigration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCloudFileReferenceObject>("Background") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("background"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("DeveloperId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("developer_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCustomAppLinks>("Links") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("links"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("oauth_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("picture"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<int>("Status") |  | ||||||
|                         .HasColumnType("integer") |  | ||||||
|                         .HasColumnName("status"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnVerificationMark>("Verification") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("verification"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_custom_apps"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("DeveloperId") |  | ||||||
|                         .HasDatabaseName("ix_custom_apps_developer_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("custom_apps", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AppId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("app_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsOidc") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_oidc"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Secret") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("secret"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_custom_app_secrets"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("AppId") |  | ||||||
|                         .HasDatabaseName("ix_custom_app_secrets_app_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("custom_app_secrets", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("PublisherId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("publisher_id"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_developers"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("developers", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("DeveloperId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_custom_apps_developers_developer_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Developer"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") |  | ||||||
|                         .WithMany("Secrets") |  | ||||||
|                         .HasForeignKey("AppId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("App"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Secrets"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,106 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class InitialMigration : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "developers", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     publisher_id = table.Column<Guid>(type: "uuid", nullable: false) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_developers", x => x.id); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "custom_apps", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), |  | ||||||
|                     status = table.Column<int>(type: "integer", nullable: false), |  | ||||||
|                     picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), |  | ||||||
|                     background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), |  | ||||||
|                     verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true), |  | ||||||
|                     oauth_config = table.Column<SnCustomAppOauthConfig>(type: "jsonb", nullable: true), |  | ||||||
|                     links = table.Column<SnCustomAppLinks>(type: "jsonb", nullable: true), |  | ||||||
|                     developer_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_custom_apps", x => x.id); |  | ||||||
|                     table.ForeignKey( |  | ||||||
|                         name: "fk_custom_apps_developers_developer_id", |  | ||||||
|                         column: x => x.developer_id, |  | ||||||
|                         principalTable: "developers", |  | ||||||
|                         principalColumn: "id", |  | ||||||
|                         onDelete: ReferentialAction.Cascade); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "custom_app_secrets", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     secret = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), |  | ||||||
|                     expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), |  | ||||||
|                     is_oidc = table.Column<bool>(type: "boolean", nullable: false), |  | ||||||
|                     app_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_custom_app_secrets", x => x.id); |  | ||||||
|                     table.ForeignKey( |  | ||||||
|                         name: "fk_custom_app_secrets_custom_apps_app_id", |  | ||||||
|                         column: x => x.app_id, |  | ||||||
|                         principalTable: "custom_apps", |  | ||||||
|                         principalColumn: "id", |  | ||||||
|                         onDelete: ReferentialAction.Cascade); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_custom_app_secrets_app_id", |  | ||||||
|                 table: "custom_app_secrets", |  | ||||||
|                 column: "app_id"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_custom_apps_developer_id", |  | ||||||
|                 table: "custom_apps", |  | ||||||
|                 column: "developer_id"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "custom_app_secrets"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "custom_apps"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "developers"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,269 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using DysonNetwork.Develop; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250818124844_AddDevProject")] |  | ||||||
|     partial class AddDevProject |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCloudFileReferenceObject>("Background") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("background"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCustomAppLinks>("Links") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("links"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("oauth_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("picture"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("ProjectId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("project_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<int>("Status") |  | ||||||
|                         .HasColumnType("integer") |  | ||||||
|                         .HasColumnName("status"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnVerificationMark>("Verification") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("verification"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_custom_apps"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("ProjectId") |  | ||||||
|                         .HasDatabaseName("ix_custom_apps_project_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("custom_apps", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AppId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("app_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsOidc") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_oidc"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Secret") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("secret"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_custom_app_secrets"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("AppId") |  | ||||||
|                         .HasDatabaseName("ix_custom_app_secrets_app_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("custom_app_secrets", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("PublisherId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("publisher_id"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_developers"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("developers", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("DeveloperId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("developer_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_dev_projects"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("DeveloperId") |  | ||||||
|                         .HasDatabaseName("ix_dev_projects_developer_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("dev_projects", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("ProjectId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_custom_apps_dev_projects_project_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Project"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") |  | ||||||
|                         .WithMany("Secrets") |  | ||||||
|                         .HasForeignKey("AppId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("App"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") |  | ||||||
|                         .WithMany("Projects") |  | ||||||
|                         .HasForeignKey("DeveloperId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_dev_projects_developers_developer_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Developer"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Secrets"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Projects"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,95 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddDevProject : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropForeignKey( |  | ||||||
|                 name: "fk_custom_apps_developers_developer_id", |  | ||||||
|                 table: "custom_apps"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.RenameColumn( |  | ||||||
|                 name: "developer_id", |  | ||||||
|                 table: "custom_apps", |  | ||||||
|                 newName: "project_id"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.RenameIndex( |  | ||||||
|                 name: "ix_custom_apps_developer_id", |  | ||||||
|                 table: "custom_apps", |  | ||||||
|                 newName: "ix_custom_apps_project_id"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "dev_projects", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false), |  | ||||||
|                     developer_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_dev_projects", x => x.id); |  | ||||||
|                     table.ForeignKey( |  | ||||||
|                         name: "fk_dev_projects_developers_developer_id", |  | ||||||
|                         column: x => x.developer_id, |  | ||||||
|                         principalTable: "developers", |  | ||||||
|                         principalColumn: "id", |  | ||||||
|                         onDelete: ReferentialAction.Cascade); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_dev_projects_developer_id", |  | ||||||
|                 table: "dev_projects", |  | ||||||
|                 column: "developer_id"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.AddForeignKey( |  | ||||||
|                 name: "fk_custom_apps_dev_projects_project_id", |  | ||||||
|                 table: "custom_apps", |  | ||||||
|                 column: "project_id", |  | ||||||
|                 principalTable: "dev_projects", |  | ||||||
|                 principalColumn: "id", |  | ||||||
|                 onDelete: ReferentialAction.Cascade); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropForeignKey( |  | ||||||
|                 name: "fk_custom_apps_dev_projects_project_id", |  | ||||||
|                 table: "custom_apps"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "dev_projects"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.RenameColumn( |  | ||||||
|                 name: "project_id", |  | ||||||
|                 table: "custom_apps", |  | ||||||
|                 newName: "developer_id"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.RenameIndex( |  | ||||||
|                 name: "ix_custom_apps_project_id", |  | ||||||
|                 table: "custom_apps", |  | ||||||
|                 newName: "ix_custom_apps_developer_id"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.AddForeignKey( |  | ||||||
|                 name: "fk_custom_apps_developers_developer_id", |  | ||||||
|                 table: "custom_apps", |  | ||||||
|                 column: "developer_id", |  | ||||||
|                 principalTable: "developers", |  | ||||||
|                 principalColumn: "id", |  | ||||||
|                 onDelete: ReferentialAction.Cascade); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,323 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using DysonNetwork.Develop; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250819163227_AddBotAccount")] |  | ||||||
|     partial class AddBotAccount |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsActive") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_active"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("ProjectId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("project_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_bot_accounts"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("ProjectId") |  | ||||||
|                         .HasDatabaseName("ix_bot_accounts_project_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("bot_accounts", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCloudFileReferenceObject>("Background") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("background"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCustomAppLinks>("Links") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("links"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("oauth_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("picture"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("ProjectId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("project_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<int>("Status") |  | ||||||
|                         .HasColumnType("integer") |  | ||||||
|                         .HasColumnName("status"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnVerificationMark>("Verification") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("verification"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_custom_apps"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("ProjectId") |  | ||||||
|                         .HasDatabaseName("ix_custom_apps_project_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("custom_apps", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AppId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("app_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsOidc") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_oidc"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Secret") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("secret"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_custom_app_secrets"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("AppId") |  | ||||||
|                         .HasDatabaseName("ix_custom_app_secrets_app_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("custom_app_secrets", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("PublisherId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("publisher_id"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_developers"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("developers", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("DeveloperId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("developer_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_dev_projects"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("DeveloperId") |  | ||||||
|                         .HasDatabaseName("ix_dev_projects_developer_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("dev_projects", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("ProjectId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_bot_accounts_dev_projects_project_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Project"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("ProjectId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_custom_apps_dev_projects_project_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Project"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") |  | ||||||
|                         .WithMany("Secrets") |  | ||||||
|                         .HasForeignKey("AppId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("App"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") |  | ||||||
|                         .WithMany("Projects") |  | ||||||
|                         .HasForeignKey("DeveloperId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_dev_projects_developers_developer_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Developer"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Secrets"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Projects"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,50 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddBotAccount : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "bot_accounts", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     is_active = table.Column<bool>(type: "boolean", nullable: false), |  | ||||||
|                     project_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_bot_accounts", x => x.id); |  | ||||||
|                     table.ForeignKey( |  | ||||||
|                         name: "fk_bot_accounts_dev_projects_project_id", |  | ||||||
|                         column: x => x.project_id, |  | ||||||
|                         principalTable: "dev_projects", |  | ||||||
|                         principalColumn: "id", |  | ||||||
|                         onDelete: ReferentialAction.Cascade); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_bot_accounts_project_id", |  | ||||||
|                 table: "bot_accounts", |  | ||||||
|                 column: "project_id"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "bot_accounts"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,320 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using DysonNetwork.Develop; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     partial class AppDatabaseModelSnapshot : ModelSnapshot |  | ||||||
|     { |  | ||||||
|         protected override void BuildModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsActive") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_active"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("ProjectId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("project_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_bot_accounts"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("ProjectId") |  | ||||||
|                         .HasDatabaseName("ix_bot_accounts_project_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("bot_accounts", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCloudFileReferenceObject>("Background") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("background"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCustomAppLinks>("Links") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("links"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("oauth_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("picture"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("ProjectId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("project_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<int>("Status") |  | ||||||
|                         .HasColumnType("integer") |  | ||||||
|                         .HasColumnName("status"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<SnVerificationMark>("Verification") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("verification"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_custom_apps"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("ProjectId") |  | ||||||
|                         .HasDatabaseName("ix_custom_apps_project_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("custom_apps", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AppId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("app_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsOidc") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_oidc"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Secret") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("secret"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_custom_app_secrets"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("AppId") |  | ||||||
|                         .HasDatabaseName("ix_custom_app_secrets_app_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("custom_app_secrets", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("PublisherId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("publisher_id"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_developers"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("developers", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("DeveloperId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("developer_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_dev_projects"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("DeveloperId") |  | ||||||
|                         .HasDatabaseName("ix_dev_projects_developer_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("dev_projects", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("ProjectId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_bot_accounts_dev_projects_project_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Project"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("ProjectId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_custom_apps_dev_projects_project_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Project"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") |  | ||||||
|                         .WithMany("Secrets") |  | ||||||
|                         .HasForeignKey("AppId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("App"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") |  | ||||||
|                         .WithMany("Projects") |  | ||||||
|                         .HasForeignKey("DeveloperId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_dev_projects_developers_developer_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Developer"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Secrets"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Projects"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,40 +0,0 @@ | |||||||
| using DysonNetwork.Develop; |  | ||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using DysonNetwork.Shared.Http; |  | ||||||
| using DysonNetwork.Develop.Startup; |  | ||||||
| using DysonNetwork.Shared.Registry; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); |  | ||||||
|  |  | ||||||
| builder.AddServiceDefaults(); |  | ||||||
|  |  | ||||||
| builder.ConfigureAppKestrel(builder.Configuration); |  | ||||||
|  |  | ||||||
| builder.Services.AddAppServices(builder.Configuration); |  | ||||||
| builder.Services.AddAppAuthentication(); |  | ||||||
| builder.Services.AddDysonAuth(); |  | ||||||
| builder.Services.AddSphereService(); |  | ||||||
| builder.Services.AddAccountService(); |  | ||||||
| builder.Services.AddDriveService(); |  | ||||||
|  |  | ||||||
| builder.AddSwaggerManifest( |  | ||||||
|     "DysonNetwork.Develop", |  | ||||||
|     "The developer portal in the Solar Network." |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| var app = builder.Build(); |  | ||||||
|  |  | ||||||
| app.MapDefaultEndpoints(); |  | ||||||
|  |  | ||||||
| using (var scope = app.Services.CreateScope()) |  | ||||||
| { |  | ||||||
|     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); |  | ||||||
|     await db.Database.MigrateAsync(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| app.ConfigureAppMiddleware(builder.Configuration); |  | ||||||
|  |  | ||||||
| app.UseSwaggerManifest("DysonNetwork.Develop"); |  | ||||||
|  |  | ||||||
| app.Run(); |  | ||||||
| @@ -1,107 +0,0 @@ | |||||||
| using System.ComponentModel.DataAnnotations; |  | ||||||
| using DysonNetwork.Develop.Identity; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Project; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/developers/{pubName}/projects")] |  | ||||||
| public class DevProjectController(DevProjectService projectService, DeveloperService developerService) : ControllerBase |  | ||||||
| { |  | ||||||
|     public record DevProjectRequest( |  | ||||||
|         [MaxLength(1024)] string? Slug, |  | ||||||
|         [MaxLength(1024)] string? Name, |  | ||||||
|         [MaxLength(4096)] string? Description |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     [HttpGet] |  | ||||||
|     public async Task<IActionResult> ListProjects([FromRoute] string pubName) |  | ||||||
|     { |  | ||||||
|         var developer = await developerService.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) return NotFound(); |  | ||||||
|          |  | ||||||
|         var projects = await projectService.GetProjectsByDeveloperAsync(developer.Id); |  | ||||||
|         return Ok(projects); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{id:guid}")] |  | ||||||
|     public async Task<IActionResult> GetProject([FromRoute] string pubName, Guid id) |  | ||||||
|     { |  | ||||||
|         var developer = await developerService.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         var project = await projectService.GetProjectAsync(id, developer.Id); |  | ||||||
|         if (project is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         return Ok(project); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> CreateProject([FromRoute] string pubName, [FromBody] DevProjectRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)  |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await developerService.GetDeveloperByName(pubName); |  | ||||||
|         if (developer is null) |  | ||||||
|             return NotFound("Developer not found"); |  | ||||||
|              |  | ||||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) |  | ||||||
|             return StatusCode(403, "You must be an editor of the developer to create a project"); |  | ||||||
|  |  | ||||||
|         if (string.IsNullOrWhiteSpace(request.Slug) || string.IsNullOrWhiteSpace(request.Name)) |  | ||||||
|             return BadRequest("Slug and Name are required"); |  | ||||||
|  |  | ||||||
|         var project = await projectService.CreateProjectAsync(developer, request); |  | ||||||
|         return CreatedAtAction( |  | ||||||
|             nameof(GetProject),  |  | ||||||
|             new { pubName, id = project.Id }, |  | ||||||
|             project |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPut("{id:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> UpdateProject( |  | ||||||
|         [FromRoute] string pubName,  |  | ||||||
|         [FromRoute] Guid id, |  | ||||||
|         [FromBody] DevProjectRequest request |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)  |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await developerService.GetDeveloperByName(pubName); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|         if (developer is null || developer.Id != accountId) |  | ||||||
|             return Forbid(); |  | ||||||
|  |  | ||||||
|         var project = await projectService.UpdateProjectAsync(id, developer.Id, request); |  | ||||||
|         if (project is null) |  | ||||||
|             return NotFound(); |  | ||||||
|  |  | ||||||
|         return Ok(project); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpDelete("{id:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<IActionResult> DeleteProject([FromRoute] string pubName, [FromRoute] Guid id) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)  |  | ||||||
|             return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var developer = await developerService.GetDeveloperByName(pubName); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|         if (developer is null || developer.Id != accountId) |  | ||||||
|             return Forbid(); |  | ||||||
|  |  | ||||||
|         var success = await projectService.DeleteProjectAsync(id, developer.Id); |  | ||||||
|         if (!success) |  | ||||||
|             return NotFound(); |  | ||||||
|  |  | ||||||
|         return NoContent(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,73 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Project; |  | ||||||
|  |  | ||||||
| public class DevProjectService(AppDatabase db ) |  | ||||||
| { |  | ||||||
|     public async Task<SnDevProject> CreateProjectAsync( |  | ||||||
|         SnDeveloper developer, |  | ||||||
|         DevProjectController.DevProjectRequest request |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         var project = new SnDevProject |  | ||||||
|         { |  | ||||||
|             Slug = request.Slug!, |  | ||||||
|             Name = request.Name!, |  | ||||||
|             Description = request.Description ?? string.Empty, |  | ||||||
|             DeveloperId = developer.Id |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         db.DevProjects.Add(project); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         return project; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnDevProject?> GetProjectAsync(Guid id, Guid? developerId = null) |  | ||||||
|     { |  | ||||||
|         var query = db.DevProjects.AsQueryable(); |  | ||||||
|  |  | ||||||
|         if (developerId.HasValue) |  | ||||||
|         { |  | ||||||
|             query = query.Where(p => p.DeveloperId == developerId.Value); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return await query.FirstOrDefaultAsync(p => p.Id == id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<List<SnDevProject>> GetProjectsByDeveloperAsync(Guid developerId) |  | ||||||
|     { |  | ||||||
|         return await db.DevProjects |  | ||||||
|             .Where(p => p.DeveloperId == developerId) |  | ||||||
|             .ToListAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnDevProject?> UpdateProjectAsync( |  | ||||||
|         Guid id, |  | ||||||
|         Guid developerId, |  | ||||||
|         DevProjectController.DevProjectRequest request |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         var project = await GetProjectAsync(id, developerId); |  | ||||||
|         if (project == null) return null; |  | ||||||
|  |  | ||||||
|         if (request.Slug != null) project.Slug = request.Slug; |  | ||||||
|         if (request.Name != null) project.Name = request.Name; |  | ||||||
|         if (request.Description != null) project.Description = request.Description; |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         return project; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<bool> DeleteProjectAsync(Guid id, Guid developerId) |  | ||||||
|     { |  | ||||||
|         var project = await GetProjectAsync(id, developerId); |  | ||||||
|         if (project == null) return false; |  | ||||||
|  |  | ||||||
|         db.DevProjects.Remove(project); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| { |  | ||||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", |  | ||||||
|   "profiles": { |  | ||||||
|     "http": { |  | ||||||
|       "commandName": "Project", |  | ||||||
|       "dotnetRunMessages": true, |  | ||||||
|       "launchBrowser": false, |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "https": { |  | ||||||
|       "commandName": "Project", |  | ||||||
|       "dotnetRunMessages": true, |  | ||||||
|       "launchBrowser": false, |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| using DysonNetwork.Develop.Identity; |  | ||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using DysonNetwork.Shared.Http; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Startup; |  | ||||||
|  |  | ||||||
| public static class ApplicationConfiguration |  | ||||||
| { |  | ||||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         app.MapOpenApi(); |  | ||||||
|  |  | ||||||
|         app.UseRequestLocalization(); |  | ||||||
|  |  | ||||||
|         app.ConfigureForwardedHeaders(configuration); |  | ||||||
|  |  | ||||||
|         app.UseAuthentication(); |  | ||||||
|         app.UseAuthorization(); |  | ||||||
|         app.UseMiddleware<PermissionMiddleware>(); |  | ||||||
|  |  | ||||||
|         app.MapControllers(); |  | ||||||
|          |  | ||||||
|         app.MapGrpcService<CustomAppServiceGrpc>(); |  | ||||||
|         app.MapGrpcReflectionService(); |  | ||||||
|  |  | ||||||
|         return app; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,62 +0,0 @@ | |||||||
| using System.Globalization; |  | ||||||
| using NodaTime; |  | ||||||
| using NodaTime.Serialization.SystemTextJson; |  | ||||||
| using System.Text.Json; |  | ||||||
| using System.Text.Json.Serialization; |  | ||||||
| using DysonNetwork.Develop.Identity; |  | ||||||
| using DysonNetwork.Develop.Project; |  | ||||||
| using DysonNetwork.Shared.Cache; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Startup; |  | ||||||
|  |  | ||||||
| public static class ServiceCollectionExtensions |  | ||||||
| { |  | ||||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         services.AddLocalization(); |  | ||||||
|  |  | ||||||
|         services.AddDbContext<AppDatabase>(); |  | ||||||
|         services.AddSingleton<IClock>(SystemClock.Instance); |  | ||||||
|         services.AddHttpContextAccessor(); |  | ||||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); |  | ||||||
|  |  | ||||||
|         services.AddHttpClient(); |  | ||||||
|  |  | ||||||
|         services.AddControllers().AddJsonOptions(options => |  | ||||||
|         { |  | ||||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; |  | ||||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; |  | ||||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; |  | ||||||
|              |  | ||||||
|             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         services.AddGrpc(options => { options.EnableDetailedErrors = true; }); |  | ||||||
|         services.AddGrpcReflection(); |  | ||||||
|  |  | ||||||
|         services.Configure<RequestLocalizationOptions>(options => |  | ||||||
|         { |  | ||||||
|             var supportedCultures = new[] |  | ||||||
|             { |  | ||||||
|                 new CultureInfo("en-US"), |  | ||||||
|                 new CultureInfo("zh-Hans"), |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             options.SupportedCultures = supportedCultures; |  | ||||||
|             options.SupportedUICultures = supportedCultures; |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         services.AddScoped<DeveloperService>(); |  | ||||||
|         services.AddScoped<CustomAppService>(); |  | ||||||
|         services.AddScoped<DevProjectService>(); |  | ||||||
|         services.AddScoped<BotAccountService>(); |  | ||||||
|  |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         services.AddAuthorization(); |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| { |  | ||||||
|   "Debug": true, |  | ||||||
|   "BaseUrl": "http://localhost:5071", |  | ||||||
|   "SiteUrl": "https://solian.app", |  | ||||||
|   "Logging": { |  | ||||||
|     "LogLevel": { |  | ||||||
|       "Default": "Information", |  | ||||||
|       "Microsoft.AspNetCore": "Warning" |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "AllowedHosts": "*", |  | ||||||
|   "ConnectionStrings": { |  | ||||||
|     "App": "Host=localhost;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" |  | ||||||
|   }, |  | ||||||
|   "KnownProxies": ["127.0.0.1", "::1"], |  | ||||||
|   "Swagger": { |  | ||||||
|     "PublicBasePath": "/develop" |  | ||||||
|   }, |  | ||||||
|   "Etcd": { |  | ||||||
|     "Insecure": true |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										3
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +0,0 @@ | |||||||
| /Uploads/ |  | ||||||
| /Client/node_modules/ |  | ||||||
| /wwwroot/dist |  | ||||||
| @@ -1,183 +0,0 @@ | |||||||
| using System.Linq.Expressions; |  | ||||||
| using System.Reflection; |  | ||||||
| using DysonNetwork.Drive.Billing; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Design; |  | ||||||
| using Microsoft.EntityFrameworkCore.Query; |  | ||||||
| using NodaTime; |  | ||||||
| using Quartz; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive; |  | ||||||
|  |  | ||||||
| public class AppDatabase( |  | ||||||
|     DbContextOptions<AppDatabase> options, |  | ||||||
|     IConfiguration configuration |  | ||||||
| ) : DbContext(options) |  | ||||||
| { |  | ||||||
|     public DbSet<FilePool> Pools { get; set; } = null!; |  | ||||||
|     public DbSet<SnFileBundle> Bundles { get; set; } = null!; |  | ||||||
|      |  | ||||||
|     public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!; |  | ||||||
|      |  | ||||||
|     public DbSet<SnCloudFile> Files { get; set; } = null!; |  | ||||||
|     public DbSet<CloudFileReference> FileReferences { get; set; } = null!; |  | ||||||
|      |  | ||||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |  | ||||||
|     { |  | ||||||
|         optionsBuilder.UseNpgsql( |  | ||||||
|             configuration.GetConnectionString("App"), |  | ||||||
|             opt => opt |  | ||||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) |  | ||||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) |  | ||||||
|                 .UseNodaTime() |  | ||||||
|         ).UseSnakeCaseNamingConvention(); |  | ||||||
|  |  | ||||||
|         base.OnConfiguring(optionsBuilder); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) |  | ||||||
|     { |  | ||||||
|         base.OnModelCreating(modelBuilder); |  | ||||||
|  |  | ||||||
|         // Automatically apply soft-delete filter to all entities inheriting BaseModel |  | ||||||
|         foreach (var entityType in modelBuilder.Model.GetEntityTypes()) |  | ||||||
|         { |  | ||||||
|             if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue; |  | ||||||
|             var method = typeof(AppDatabase) |  | ||||||
|                 .GetMethod(nameof(SetSoftDeleteFilter), |  | ||||||
|                     BindingFlags.NonPublic | BindingFlags.Static)! |  | ||||||
|                 .MakeGenericMethod(entityType.ClrType); |  | ||||||
|  |  | ||||||
|             method.Invoke(null, [modelBuilder]); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder) |  | ||||||
|         where TEntity : ModelBase |  | ||||||
|     { |  | ||||||
|         modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) |  | ||||||
|     { |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|  |  | ||||||
|         foreach (var entry in ChangeTracker.Entries<ModelBase>()) |  | ||||||
|         { |  | ||||||
|             switch (entry.State) |  | ||||||
|             { |  | ||||||
|                 case EntityState.Added: |  | ||||||
|                     entry.Entity.CreatedAt = now; |  | ||||||
|                     entry.Entity.UpdatedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Modified: |  | ||||||
|                     entry.Entity.UpdatedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Deleted: |  | ||||||
|                     entry.State = EntityState.Modified; |  | ||||||
|                     entry.Entity.DeletedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Detached: |  | ||||||
|                 case EntityState.Unchanged: |  | ||||||
|                 default: |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return await base.SaveChangesAsync(cancellationToken); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclingJob> logger) : IJob |  | ||||||
| { |  | ||||||
|     public async Task Execute(IJobExecutionContext context) |  | ||||||
|     { |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|  |  | ||||||
|         logger.LogInformation("Deleting soft-deleted records..."); |  | ||||||
|  |  | ||||||
|         var threshold = now - Duration.FromDays(7); |  | ||||||
|  |  | ||||||
|         var entityTypes = db.Model.GetEntityTypes() |  | ||||||
|             .Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase)) |  | ||||||
|             .Select(t => t.ClrType); |  | ||||||
|  |  | ||||||
|         foreach (var entityType in entityTypes) |  | ||||||
|         { |  | ||||||
|             var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)! |  | ||||||
|                 .MakeGenericMethod(entityType).Invoke(db, null)!; |  | ||||||
|             var parameter = Expression.Parameter(entityType, "e"); |  | ||||||
|             var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt)); |  | ||||||
|             var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?))); |  | ||||||
|             var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?))); |  | ||||||
|             var finalCondition = Expression.AndAlso(notNull, condition); |  | ||||||
|             var lambda = Expression.Lambda(finalCondition, parameter); |  | ||||||
|  |  | ||||||
|             var queryable = set.Provider.CreateQuery( |  | ||||||
|                 Expression.Call( |  | ||||||
|                     typeof(Queryable), |  | ||||||
|                     "Where", |  | ||||||
|                     [entityType], |  | ||||||
|                     set.Expression, |  | ||||||
|                     Expression.Quote(lambda) |  | ||||||
|                 ) |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|             var toListAsync = typeof(EntityFrameworkQueryableExtensions) |  | ||||||
|                 .GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))! |  | ||||||
|                 .MakeGenericMethod(entityType); |  | ||||||
|  |  | ||||||
|             var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!; |  | ||||||
|             db.RemoveRange(items); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase> |  | ||||||
| { |  | ||||||
|     public AppDatabase CreateDbContext(string[] args) |  | ||||||
|     { |  | ||||||
|         var configuration = new ConfigurationBuilder() |  | ||||||
|             .SetBasePath(Directory.GetCurrentDirectory()) |  | ||||||
|             .AddJsonFile("appsettings.json") |  | ||||||
|             .Build(); |  | ||||||
|  |  | ||||||
|         var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>(); |  | ||||||
|         return new AppDatabase(optionsBuilder.Options, configuration); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public static class OptionalQueryExtensions |  | ||||||
| { |  | ||||||
|     public static IQueryable<T> If<T>( |  | ||||||
|         this IQueryable<T> source, |  | ||||||
|         bool condition, |  | ||||||
|         Func<IQueryable<T>, IQueryable<T>> transform |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         return condition ? transform(source) : source; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IQueryable<T> If<T, TP>( |  | ||||||
|         this IIncludableQueryable<T, TP> source, |  | ||||||
|         bool condition, |  | ||||||
|         Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform |  | ||||||
|     ) |  | ||||||
|         where T : class |  | ||||||
|     { |  | ||||||
|         return condition ? transform(source) : source; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IQueryable<T> If<T, TP>( |  | ||||||
|         this IIncludableQueryable<T, IEnumerable<TP>> source, |  | ||||||
|         bool condition, |  | ||||||
|         Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform |  | ||||||
|     ) |  | ||||||
|         where T : class |  | ||||||
|     { |  | ||||||
|         return condition ? transform(source) : source; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Billing; |  | ||||||
|  |  | ||||||
| /// <summary> |  | ||||||
| /// The quota record stands for the extra quota that a user has. |  | ||||||
| /// For normal users, the quota is 1GiB. |  | ||||||
| /// For stellar program t1 users, the quota is 5GiB |  | ||||||
| /// For stellar program t2 users, the quota is 10GiB |  | ||||||
| /// For stellar program t3 users, the quota is 15GiB |  | ||||||
| /// |  | ||||||
| /// If users want to increase the quota, they need to pay for it. |  | ||||||
| /// Each 1NSD they paid for one GiB. |  | ||||||
| /// |  | ||||||
| /// But the quota record unit is MiB, the minimal billable unit. |  | ||||||
| /// </summary> |  | ||||||
| public class QuotaRecord : ModelBase |  | ||||||
| { |  | ||||||
|     public Guid Id { get; set; } = Guid.NewGuid(); |  | ||||||
|     public Guid AccountId { get; set; } |  | ||||||
|     public string Name { get; set; } = string.Empty; |  | ||||||
|     public string Description { get; set; } = string.Empty; |  | ||||||
|      |  | ||||||
|     public long Quota { get; set; } |  | ||||||
|      |  | ||||||
|     public Instant? ExpiredAt { get; set; } |  | ||||||
| } |  | ||||||
| @@ -1,66 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Billing; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/billing/quota")] |  | ||||||
| public class QuotaController(AppDatabase db, QuotaService quota) : ControllerBase |  | ||||||
| { |  | ||||||
|     public class QuotaDetails |  | ||||||
|     { |  | ||||||
|         public long BasedQuota { get; set; } |  | ||||||
|         public long ExtraQuota { get; set; } |  | ||||||
|         public long TotalQuota { get; set; } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     [HttpGet] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult<QuotaDetails>> GetQuota() |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|          |  | ||||||
|         var (based, extra) = await quota.GetQuotaVerbose(accountId); |  | ||||||
|         return Ok(new QuotaDetails |  | ||||||
|         { |  | ||||||
|             BasedQuota = based, |  | ||||||
|             ExtraQuota = extra, |  | ||||||
|             TotalQuota = based + extra |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     [HttpGet("records")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult<List<QuotaRecord>>> GetQuotaRecords( |  | ||||||
|         [FromQuery] bool expired = false, |  | ||||||
|         [FromQuery] int offset = 0, |  | ||||||
|         [FromQuery] int take = 20 |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         var query = db.QuotaRecords |  | ||||||
|             .Where(r => r.AccountId == accountId) |  | ||||||
|             .AsQueryable(); |  | ||||||
|         if (!expired) |  | ||||||
|             query = query |  | ||||||
|                 .Where(r => !r.ExpiredAt.HasValue || r.ExpiredAt > now); |  | ||||||
|  |  | ||||||
|         var total = await query.CountAsync(); |  | ||||||
|         Response.Headers.Append("X-Total", total.ToString()); |  | ||||||
|  |  | ||||||
|         var records = await query |  | ||||||
|             .OrderByDescending(r => r.CreatedAt) |  | ||||||
|             .Skip(offset) |  | ||||||
|             .Take(take) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         return Ok(records); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,69 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using DysonNetwork.Shared.Cache; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Billing; |  | ||||||
|  |  | ||||||
| public class QuotaService( |  | ||||||
|     AppDatabase db, |  | ||||||
|     UsageService usage, |  | ||||||
|     AccountService.AccountServiceClient accounts, |  | ||||||
|     ICacheService cache |  | ||||||
| ) |  | ||||||
| { |  | ||||||
|     public async Task<(bool ok, long billable, long quota)> IsFileAcceptable(Guid accountId, double costMultiplier, long newFileSize) |  | ||||||
|     { |  | ||||||
|         // The billable unit is MiB |  | ||||||
|         var billableUnit = (long)Math.Ceiling(newFileSize / 1024.0 / 1024.0 * costMultiplier); |  | ||||||
|         var totalBillableUsage = await usage.GetTotalBillableUsage(accountId); |  | ||||||
|         var quota = await GetQuota(accountId); |  | ||||||
|         return (totalBillableUsage + billableUnit <= quota, billableUnit, quota); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<long> GetQuota(Guid accountId) |  | ||||||
|     { |  | ||||||
|         var cacheKey = $"file:quota:{accountId}"; |  | ||||||
|         var cachedResult = await cache.GetAsync<long?>(cacheKey); |  | ||||||
|         if (cachedResult.HasValue) return cachedResult.Value; |  | ||||||
|          |  | ||||||
|         var (based, extra) = await GetQuotaVerbose(accountId); |  | ||||||
|         var quota = based + extra; |  | ||||||
|         await cache.SetAsync(cacheKey, quota, expiry: TimeSpan.FromMinutes(30)); |  | ||||||
|         return quota; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     public async Task<(long based, long extra)> GetQuotaVerbose(Guid accountId) |  | ||||||
|     { |  | ||||||
|          |  | ||||||
|  |  | ||||||
|         var response = await accounts.GetAccountAsync(new GetAccountRequest { Id = accountId.ToString() }); |  | ||||||
|         var perkSubscription = response.PerkSubscription; |  | ||||||
|  |  | ||||||
|         // The base quota is 1GiB, T1 is 5GiB, T2 is 10GiB, T3 is 15GiB |  | ||||||
|         var basedQuota = 1L; |  | ||||||
|         if (perkSubscription != null) |  | ||||||
|         { |  | ||||||
|             var privilege = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perkSubscription.Identifier); |  | ||||||
|             basedQuota = privilege switch |  | ||||||
|             { |  | ||||||
|                 1 => 5L, |  | ||||||
|                 2 => 10L, |  | ||||||
|                 3 => 15L, |  | ||||||
|                 _ => basedQuota |  | ||||||
|             }; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // The based quota is in GiB, we need to convert it to MiB |  | ||||||
|         basedQuota *= 1024L; |  | ||||||
|          |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         var extraQuota = await db.QuotaRecords |  | ||||||
|             .Where(e => e.AccountId == accountId) |  | ||||||
|             .Where(e => !e.ExpiredAt.HasValue || e.ExpiredAt > now) |  | ||||||
|             .SumAsync(e => e.Quota); |  | ||||||
|          |  | ||||||
|         return (basedQuota, extraQuota); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,49 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Cache; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Billing; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("api/billing/usage")] |  | ||||||
| public class UsageController(UsageService usage, QuotaService quota, ICacheService cache) : ControllerBase |  | ||||||
| { |  | ||||||
|     [HttpGet] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult<TotalUsageDetails>> GetTotalUsage() |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|          |  | ||||||
|         var cacheKey = $"file:usage:{accountId}"; |  | ||||||
|          |  | ||||||
|         // Try to get from cache first |  | ||||||
|         var (found, cachedResult) = await cache.GetAsyncWithStatus<TotalUsageDetails>(cacheKey); |  | ||||||
|         if (found && cachedResult != null) |  | ||||||
|             return Ok(cachedResult); |  | ||||||
|  |  | ||||||
|         // If not in cache, get from services |  | ||||||
|         var result = await usage.GetTotalUsage(accountId); |  | ||||||
|         var totalQuota = await quota.GetQuota(accountId); |  | ||||||
|         result.TotalQuota = totalQuota; |  | ||||||
|  |  | ||||||
|         // Cache the result for 5 minutes |  | ||||||
|         await cache.SetAsync(cacheKey, result, TimeSpan.FromMinutes(5)); |  | ||||||
|  |  | ||||||
|         return Ok(result); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpGet("{poolId:guid}")] |  | ||||||
|     public async Task<ActionResult<UsageDetails>> GetPoolUsage(Guid poolId) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|          |  | ||||||
|         var usageDetails = await usage.GetPoolUsage(poolId, accountId); |  | ||||||
|         if (usageDetails == null) |  | ||||||
|             return NotFound(); |  | ||||||
|         return usageDetails; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,121 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Billing; |  | ||||||
|  |  | ||||||
| public class UsageDetails |  | ||||||
| { |  | ||||||
|     public required Guid PoolId { get; set; } |  | ||||||
|     public required string PoolName { get; set; } |  | ||||||
|     public required long UsageBytes { get; set; } |  | ||||||
|     public required double Cost { get; set; } |  | ||||||
|     public required long FileCount { get; set; } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public class TotalUsageDetails |  | ||||||
| { |  | ||||||
|     public required List<UsageDetails> PoolUsages { get; set; } |  | ||||||
|     public required long TotalUsageBytes { get; set; } |  | ||||||
|     public required long TotalFileCount { get; set; } |  | ||||||
|      |  | ||||||
|     // Quota, cannot be loaded in the service, cause circular dependency |  | ||||||
|     // Let the controller do the calculation |  | ||||||
|     public long? TotalQuota { get; set; } |  | ||||||
|     public long? UsedQuota { get; set; } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public class UsageService(AppDatabase db) |  | ||||||
| { |  | ||||||
|     public async Task<TotalUsageDetails> GetTotalUsage(Guid accountId) |  | ||||||
|     { |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         var fileQuery = db.Files |  | ||||||
|             .Where(f => !f.IsMarkedRecycle) |  | ||||||
|             .Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now) |  | ||||||
|             .Where(f => f.AccountId == accountId) |  | ||||||
|             .AsQueryable(); |  | ||||||
|          |  | ||||||
|         var poolUsages = await db.Pools |  | ||||||
|             .Select(p => new UsageDetails |  | ||||||
|             { |  | ||||||
|                 PoolId = p.Id, |  | ||||||
|                 PoolName = p.Name, |  | ||||||
|                 UsageBytes = fileQuery |  | ||||||
|                     .Where(f => f.PoolId == p.Id) |  | ||||||
|                     .Sum(f => f.Size), |  | ||||||
|                 Cost = fileQuery |  | ||||||
|                            .Where(f => f.PoolId == p.Id) |  | ||||||
|                            .Sum(f => f.Size) / 1024.0 / 1024.0 * |  | ||||||
|                        (p.BillingConfig.CostMultiplier ?? 1.0), |  | ||||||
|                 FileCount = fileQuery |  | ||||||
|                     .Count(f => f.PoolId == p.Id) |  | ||||||
|             }) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         var totalUsage = poolUsages.Sum(p => p.UsageBytes); |  | ||||||
|         var totalFileCount = poolUsages.Sum(p => p.FileCount); |  | ||||||
|  |  | ||||||
|         return new TotalUsageDetails |  | ||||||
|         { |  | ||||||
|             PoolUsages = poolUsages, |  | ||||||
|             TotalUsageBytes = totalUsage, |  | ||||||
|             TotalFileCount = totalFileCount, |  | ||||||
|             UsedQuota = await GetTotalBillableUsage(accountId) |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<UsageDetails?> GetPoolUsage(Guid poolId, Guid accountId) |  | ||||||
|     { |  | ||||||
|         var pool = await db.Pools.FindAsync(poolId); |  | ||||||
|         if (pool == null) |  | ||||||
|         { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         var fileQuery = db.Files |  | ||||||
|             .Where(f => !f.IsMarkedRecycle) |  | ||||||
|             .Where(f => f.ExpiredAt.HasValue && f.ExpiredAt > now) |  | ||||||
|             .Where(f => f.AccountId == accountId) |  | ||||||
|             .AsQueryable(); |  | ||||||
|  |  | ||||||
|         var usageBytes = await fileQuery |  | ||||||
|             .SumAsync(f => f.Size); |  | ||||||
|  |  | ||||||
|         var fileCount = await fileQuery |  | ||||||
|             .CountAsync(); |  | ||||||
|  |  | ||||||
|         var cost = usageBytes / 1024.0 / 1024.0 * |  | ||||||
|                    (pool.BillingConfig.CostMultiplier ?? 1.0); |  | ||||||
|  |  | ||||||
|         return new UsageDetails |  | ||||||
|         { |  | ||||||
|             PoolId = pool.Id, |  | ||||||
|             PoolName = pool.Name, |  | ||||||
|             UsageBytes = usageBytes, |  | ||||||
|             Cost = cost, |  | ||||||
|             FileCount = fileCount |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<long> GetTotalBillableUsage(Guid accountId) |  | ||||||
|     { |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         var files = await db.Files |  | ||||||
|             .Where(f => f.AccountId == accountId) |  | ||||||
|             .Where(f => f.PoolId.HasValue) |  | ||||||
|             .Where(f => !f.IsMarkedRecycle) |  | ||||||
|             .Include(f => f.Pool) |  | ||||||
|             .Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now) |  | ||||||
|             .Select(f => new |  | ||||||
|             { |  | ||||||
|                 f.Size, |  | ||||||
|                 Multiplier = f.Pool!.BillingConfig.CostMultiplier ?? 1.0 |  | ||||||
|             }) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         var totalCost = files.Sum(f => f.Size * f.Multiplier) / 1024.0 / 1024.0; |  | ||||||
|  |  | ||||||
|         return (long)Math.Ceiling(totalCost); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base |  | ||||||
| WORKDIR /app |  | ||||||
| EXPOSE 8080 |  | ||||||
| EXPOSE 8081 |  | ||||||
|  |  | ||||||
| # Stage 1: Install runtime dependencies |  | ||||||
|  |  | ||||||
| # Install only necessary dependencies |  | ||||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ |  | ||||||
|   libfontconfig1 \ |  | ||||||
|   libfreetype6 \ |  | ||||||
|   libpng-dev \ |  | ||||||
|   libharfbuzz0b \ |  | ||||||
|   libgif7 \ |  | ||||||
|   libvips \ |  | ||||||
|   ffmpeg \ |  | ||||||
|   && apt-get clean \ |  | ||||||
|   && rm -rf /var/lib/apt/lists/* \ |  | ||||||
|      |  | ||||||
| USER $APP_UID |  | ||||||
|  |  | ||||||
| # Stage 2: Build .NET application |  | ||||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build |  | ||||||
| ARG BUILD_CONFIGURATION=Release |  | ||||||
| WORKDIR /src |  | ||||||
| COPY ["DysonNetwork.Drive/DysonNetwork.Drive.csproj", "DysonNetwork.Drive/"] |  | ||||||
| RUN dotnet restore "DysonNetwork.Drive/DysonNetwork.Drive.csproj" |  | ||||||
| COPY . . |  | ||||||
|  |  | ||||||
| WORKDIR "/src/DysonNetwork.Drive" |  | ||||||
| RUN dotnet build "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/build \ |  | ||||||
|     -p:TypeScriptCompileBlocked=true \ |  | ||||||
|     -p:UseRazorBuildServer=false |  | ||||||
|  |  | ||||||
| FROM build AS publish |  | ||||||
| ARG BUILD_CONFIGURATION=Release |  | ||||||
| RUN dotnet publish "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false |  | ||||||
|  |  | ||||||
| FROM base AS final |  | ||||||
| WORKDIR /app |  | ||||||
| COPY --from=publish /app/publish . |  | ||||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Drive.dll"] |  | ||||||
| @@ -1,67 +0,0 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> |  | ||||||
|  |  | ||||||
|     <PropertyGroup> |  | ||||||
|         <TargetFramework>net9.0</TargetFramework> |  | ||||||
|         <Nullable>enable</Nullable> |  | ||||||
|         <ImplicitUsings>enable</ImplicitUsings> |  | ||||||
|         <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> |  | ||||||
|     </PropertyGroup> |  | ||||||
|  |  | ||||||
|     <ItemGroup> |  | ||||||
|         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> |  | ||||||
|         <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" /> |  | ||||||
|         <PackageReference Include="FFMpegCore" Version="5.3.0" /> |  | ||||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" /> |  | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> |  | ||||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> |  | ||||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |  | ||||||
|           <PrivateAssets>all</PrivateAssets> |  | ||||||
|         </PackageReference> |  | ||||||
|         <PackageReference Include="MimeKit" Version="4.14.0" /> |  | ||||||
|         <PackageReference Include="MimeTypes" Version="2.5.2"> |  | ||||||
|           <PrivateAssets>all</PrivateAssets> |  | ||||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |  | ||||||
|         </PackageReference> |  | ||||||
|         <PackageReference Include="Minio" Version="6.0.5" /> |  | ||||||
|         <PackageReference Include="Nanoid" Version="3.1.0" /> |  | ||||||
|         <PackageReference Include="Nerdbank.GitVersioning" Version="3.8.118"> |  | ||||||
|           <PrivateAssets>all</PrivateAssets> |  | ||||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |  | ||||||
|         </PackageReference> |  | ||||||
|         <PackageReference Include="NetVips" Version="3.1.0" /> |  | ||||||
|         <PackageReference Include="NetVips.Native.linux-x64" Version="8.17.2" /> |  | ||||||
|         <PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.2" /> |  | ||||||
|         <PackageReference Include="NodaTime" Version="3.2.2" /> |  | ||||||
|         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> |  | ||||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> |  | ||||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> |  | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> |  | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> |  | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> |  | ||||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" /> |  | ||||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" /> |  | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" /> |  | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" /> |  | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" /> |  | ||||||
|         <PackageReference Include="Quartz" Version="3.15.0" /> |  | ||||||
|         <PackageReference Include="Quartz.AspNetCore" Version="3.15.0" /> |  | ||||||
|         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" /> |  | ||||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" /> |  | ||||||
|         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" /> |  | ||||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> |  | ||||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.1" /> |  | ||||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" /> |  | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> |  | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" /> |  | ||||||
|     </ItemGroup> |  | ||||||
|  |  | ||||||
|     <ItemGroup> |  | ||||||
|       <Content Include="..\.dockerignore"> |  | ||||||
|         <Link>.dockerignore</Link> |  | ||||||
|       </Content> |  | ||||||
|     </ItemGroup> |  | ||||||
|  |  | ||||||
|     <ItemGroup> |  | ||||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> |  | ||||||
|     </ItemGroup> |  | ||||||
| </Project> |  | ||||||
| @@ -1,190 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Drive.Storage; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250713121317_InitialMigration")] |  | ||||||
|     partial class InitialMigration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("UploadedTo") |  | ||||||
|                         .HasMaxLength(128) |  | ||||||
|                         .HasColumnType("character varying(128)") |  | ||||||
|                         .HasColumnName("uploaded_to"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,87 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class InitialMigration : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AlterDatabase() |  | ||||||
|                 .Annotation("Npgsql:PostgresExtension:postgis", ",,"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "files", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false), |  | ||||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), |  | ||||||
|                     file_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true), |  | ||||||
|                     user_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true), |  | ||||||
|                     sensitive_marks = table.Column<List<ContentSensitiveMark>>(type: "jsonb", nullable: true), |  | ||||||
|                     mime_type = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), |  | ||||||
|                     hash = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), |  | ||||||
|                     size = table.Column<long>(type: "bigint", nullable: false), |  | ||||||
|                     uploaded_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), |  | ||||||
|                     uploaded_to = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true), |  | ||||||
|                     has_compression = table.Column<bool>(type: "boolean", nullable: false), |  | ||||||
|                     is_marked_recycle = table.Column<bool>(type: "boolean", nullable: false), |  | ||||||
|                     storage_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), |  | ||||||
|                     storage_url = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), |  | ||||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_files", x => x.id); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "file_references", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     file_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false), |  | ||||||
|                     usage = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     resource_id = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_file_references", x => x.id); |  | ||||||
|                     table.ForeignKey( |  | ||||||
|                         name: "fk_file_references_files_file_id", |  | ||||||
|                         column: x => x.file_id, |  | ||||||
|                         principalTable: "files", |  | ||||||
|                         principalColumn: "id", |  | ||||||
|                         onDelete: ReferentialAction.Cascade); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_file_references_file_id", |  | ||||||
|                 table: "file_references", |  | ||||||
|                 column: "file_id"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "file_references"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "files"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,190 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250715080004_ReinitalMigration")] |  | ||||||
|     partial class ReinitalMigration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("UploadedTo") |  | ||||||
|                         .HasMaxLength(128) |  | ||||||
|                         .HasColumnType("character varying(128)") |  | ||||||
|                         .HasColumnName("uploaded_to"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,35 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class ReinitalMigration : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AlterColumn<Dictionary<string, object>>( |  | ||||||
|                 name: "file_meta", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "jsonb", |  | ||||||
|                 nullable: false, |  | ||||||
|                 oldClrType: typeof(Dictionary<string, object>), |  | ||||||
|                 oldType: "jsonb", |  | ||||||
|                 oldNullable: true); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AlterColumn<Dictionary<string, object>>( |  | ||||||
|                 name: "file_meta", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "jsonb", |  | ||||||
|                 nullable: true, |  | ||||||
|                 oldClrType: typeof(Dictionary<string, object>), |  | ||||||
|                 oldType: "jsonb"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,264 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250726103203_AddCloudFilePool")] |  | ||||||
|     partial class AddCloudFilePool |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("UploadedTo") |  | ||||||
|                         .HasMaxLength(128) |  | ||||||
|                         .HasColumnType("character varying(128)") |  | ||||||
|                         .HasColumnName("uploaded_to"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,111 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddCloudFilePool : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AlterColumn<Dictionary<string, object>>( |  | ||||||
|                 name: "file_meta", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "jsonb", |  | ||||||
|                 nullable: true, |  | ||||||
|                 oldClrType: typeof(Dictionary<string, object>), |  | ||||||
|                 oldType: "jsonb"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.AddColumn<bool>( |  | ||||||
|                 name: "has_thumbnail", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "boolean", |  | ||||||
|                 nullable: false, |  | ||||||
|                 defaultValue: false); |  | ||||||
|  |  | ||||||
|             migrationBuilder.AddColumn<bool>( |  | ||||||
|                 name: "is_encrypted", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "boolean", |  | ||||||
|                 nullable: false, |  | ||||||
|                 defaultValue: false); |  | ||||||
|  |  | ||||||
|             migrationBuilder.AddColumn<Guid>( |  | ||||||
|                 name: "pool_id", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "uuid", |  | ||||||
|                 nullable: true); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "pools", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     storage_config = table.Column<RemoteStorageConfig>(type: "jsonb", nullable: false), |  | ||||||
|                     billing_config = table.Column<BillingConfig>(type: "jsonb", nullable: false), |  | ||||||
|                     policy_config = table.Column<PolicyConfig>(type: "jsonb", nullable: false), |  | ||||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: true), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_pools", x => x.id); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_files_pool_id", |  | ||||||
|                 table: "files", |  | ||||||
|                 column: "pool_id"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.AddForeignKey( |  | ||||||
|                 name: "fk_files_pools_pool_id", |  | ||||||
|                 table: "files", |  | ||||||
|                 column: "pool_id", |  | ||||||
|                 principalTable: "pools", |  | ||||||
|                 principalColumn: "id"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropForeignKey( |  | ||||||
|                 name: "fk_files_pools_pool_id", |  | ||||||
|                 table: "files"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "pools"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropIndex( |  | ||||||
|                 name: "ix_files_pool_id", |  | ||||||
|                 table: "files"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "has_thumbnail", |  | ||||||
|                 table: "files"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "is_encrypted", |  | ||||||
|                 table: "files"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "pool_id", |  | ||||||
|                 table: "files"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.AlterColumn<Dictionary<string, object>>( |  | ||||||
|                 name: "file_meta", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "jsonb", |  | ||||||
|                 nullable: false, |  | ||||||
|                 oldClrType: typeof(Dictionary<string, object>), |  | ||||||
|                 oldType: "jsonb", |  | ||||||
|                 oldNullable: true); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,270 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250726120323_AddFilePoolDescription")] |  | ||||||
|     partial class AddFilePoolDescription |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("UploadedTo") |  | ||||||
|                         .HasMaxLength(128) |  | ||||||
|                         .HasColumnType("character varying(128)") |  | ||||||
|                         .HasColumnName("uploaded_to"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddFilePoolDescription : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AddColumn<string>( |  | ||||||
|                 name: "description", |  | ||||||
|                 table: "pools", |  | ||||||
|                 type: "character varying(8192)", |  | ||||||
|                 maxLength: 8192, |  | ||||||
|                 nullable: false, |  | ||||||
|                 defaultValue: ""); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "description", |  | ||||||
|                 table: "pools"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,274 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250726172039_AddCloudFileExpiration")] |  | ||||||
|     partial class AddCloudFileExpiration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("UploadedTo") |  | ||||||
|                         .HasMaxLength(128) |  | ||||||
|                         .HasColumnType("character varying(128)") |  | ||||||
|                         .HasColumnName("uploaded_to"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddCloudFileExpiration : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AddColumn<Instant>( |  | ||||||
|                 name: "expired_at", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "timestamp with time zone", |  | ||||||
|                 nullable: true); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "expired_at", |  | ||||||
|                 table: "files"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,321 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250727092028_AddQuotaRecord")] |  | ||||||
|     partial class AddQuotaRecord |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Quota") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("quota"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_quota_records"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("quota_records", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("UploadedTo") |  | ||||||
|                         .HasMaxLength(128) |  | ||||||
|                         .HasColumnType("character varying(128)") |  | ||||||
|                         .HasColumnName("uploaded_to"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,41 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddQuotaRecord : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "quota_records", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     name = table.Column<string>(type: "text", nullable: false), |  | ||||||
|                     description = table.Column<string>(type: "text", nullable: false), |  | ||||||
|                     quota = table.Column<long>(type: "bigint", nullable: false), |  | ||||||
|                     expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_quota_records", x => x.id); |  | ||||||
|                 }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "quota_records"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,399 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250727130951_AddFileBundle")] |  | ||||||
|     partial class AddFileBundle |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Quota") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("quota"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_quota_records"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("quota_records", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("BundleId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("UploadedTo") |  | ||||||
|                         .HasMaxLength(128) |  | ||||||
|                         .HasColumnType("character varying(128)") |  | ||||||
|                         .HasColumnName("uploaded_to"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("BundleId") |  | ||||||
|                         .HasDatabaseName("ix_files_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Passcode") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("passcode"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_bundles"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("Slug") |  | ||||||
|                         .IsUnique() |  | ||||||
|                         .HasDatabaseName("ix_bundles_slug"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("bundles", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle") |  | ||||||
|                         .WithMany("Files") |  | ||||||
|                         .HasForeignKey("BundleId") |  | ||||||
|                         .HasConstraintName("fk_files_bundles_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Bundle"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Files"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,78 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddFileBundle : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AddColumn<Guid>( |  | ||||||
|                 name: "bundle_id", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "uuid", |  | ||||||
|                 nullable: true); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "bundles", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), |  | ||||||
|                     description = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: true), |  | ||||||
|                     passcode = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), |  | ||||||
|                     expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), |  | ||||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_bundles", x => x.id); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_files_bundle_id", |  | ||||||
|                 table: "files", |  | ||||||
|                 column: "bundle_id"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_bundles_slug", |  | ||||||
|                 table: "bundles", |  | ||||||
|                 column: "slug", |  | ||||||
|                 unique: true); |  | ||||||
|  |  | ||||||
|             migrationBuilder.AddForeignKey( |  | ||||||
|                 name: "fk_files_bundles_bundle_id", |  | ||||||
|                 table: "files", |  | ||||||
|                 column: "bundle_id", |  | ||||||
|                 principalTable: "bundles", |  | ||||||
|                 principalColumn: "id"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropForeignKey( |  | ||||||
|                 name: "fk_files_bundles_bundle_id", |  | ||||||
|                 table: "files"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "bundles"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropIndex( |  | ||||||
|                 name: "ix_files_bundle_id", |  | ||||||
|                 table: "files"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "bundle_id", |  | ||||||
|                 table: "files"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,403 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250808170904_AddHiddenPool")] |  | ||||||
|     partial class AddHiddenPool |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Quota") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("quota"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_quota_records"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("quota_records", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("BundleId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("UploadedTo") |  | ||||||
|                         .HasMaxLength(128) |  | ||||||
|                         .HasColumnType("character varying(128)") |  | ||||||
|                         .HasColumnName("uploaded_to"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("BundleId") |  | ||||||
|                         .HasDatabaseName("ix_files_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Passcode") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("passcode"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_bundles"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("Slug") |  | ||||||
|                         .IsUnique() |  | ||||||
|                         .HasDatabaseName("ix_bundles_slug"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("bundles", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsHidden") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_hidden"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle") |  | ||||||
|                         .WithMany("Files") |  | ||||||
|                         .HasForeignKey("BundleId") |  | ||||||
|                         .HasConstraintName("fk_files_bundles_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Bundle"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Files"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddHiddenPool : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AddColumn<bool>( |  | ||||||
|                 name: "is_hidden", |  | ||||||
|                 table: "pools", |  | ||||||
|                 type: "boolean", |  | ||||||
|                 nullable: false, |  | ||||||
|                 defaultValue: false); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "is_hidden", |  | ||||||
|                 table: "pools"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,403 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250819164302_RemoveUploadedTo")] |  | ||||||
|     partial class RemoveUploadedTo |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Quota") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("quota"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_quota_records"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("quota_records", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("BundleId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("BundleId") |  | ||||||
|                         .HasDatabaseName("ix_files_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Passcode") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("passcode"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_bundles"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("Slug") |  | ||||||
|                         .IsUnique() |  | ||||||
|                         .HasDatabaseName("ix_bundles_slug"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("bundles", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsHidden") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_hidden"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle") |  | ||||||
|                         .WithMany("Files") |  | ||||||
|                         .HasForeignKey("BundleId") |  | ||||||
|                         .HasConstraintName("fk_files_bundles_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Bundle"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany("References") |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("References"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Files"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class RemoveUploadedTo : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "uploaded_to", |  | ||||||
|                 table: "files"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AddColumn<string>( |  | ||||||
|                 name: "uploaded_to", |  | ||||||
|                 table: "files", |  | ||||||
|                 type: "character varying(128)", |  | ||||||
|                 maxLength: 128, |  | ||||||
|                 nullable: true); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,402 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20250907070034_RemoveNetTopo")] |  | ||||||
|     partial class RemoveNetTopo |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Quota") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("quota"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_quota_records"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("quota_records", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("BundleId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("BundleId") |  | ||||||
|                         .HasDatabaseName("ix_files_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Passcode") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("passcode"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_bundles"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("Slug") |  | ||||||
|                         .IsUnique() |  | ||||||
|                         .HasDatabaseName("ix_bundles_slug"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("bundles", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsHidden") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_hidden"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle") |  | ||||||
|                         .WithMany("Files") |  | ||||||
|                         .HasForeignKey("BundleId") |  | ||||||
|                         .HasConstraintName("fk_files_bundles_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Bundle"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany("References") |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("References"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Files"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class RemoveNetTopo : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AlterDatabase() |  | ||||||
|                 .OldAnnotation("Npgsql:PostgresExtension:postgis", ",,"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AlterDatabase() |  | ||||||
|                 .Annotation("Npgsql:PostgresExtension:postgis", ",,"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,399 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     partial class AppDatabaseModelSnapshot : ModelSnapshot |  | ||||||
|     { |  | ||||||
|         protected override void BuildModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Quota") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("quota"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_quota_records"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("quota_records", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<string>("Id") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("BundleId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("file_meta"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasCompression") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_compression"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("HasThumbnail") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("has_thumbnail"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Hash") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("hash"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsEncrypted") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_encrypted"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_marked_recycle"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("MimeType") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("mime_type"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("PoolId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("sensitive_marks"); |  | ||||||
|  |  | ||||||
|                     b.Property<long>("Size") |  | ||||||
|                         .HasColumnType("bigint") |  | ||||||
|                         .HasColumnName("size"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageId") |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("storage_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("StorageUrl") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("storage_url"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("UploadedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("uploaded_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("UserMeta") |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("user_meta"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_files"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("BundleId") |  | ||||||
|                         .HasDatabaseName("ix_files_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("PoolId") |  | ||||||
|                         .HasDatabaseName("ix_files_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("files", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("FileId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(32) |  | ||||||
|                         .HasColumnType("character varying(32)") |  | ||||||
|                         .HasColumnName("file_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("ResourceId") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("resource_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Usage") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("usage"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_file_references"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("FileId") |  | ||||||
|                         .HasDatabaseName("ix_file_references_file_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("file_references", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("ExpiredAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("expired_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Passcode") |  | ||||||
|                         .HasMaxLength(256) |  | ||||||
|                         .HasColumnType("character varying(256)") |  | ||||||
|                         .HasColumnName("passcode"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Slug") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("slug"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_bundles"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("Slug") |  | ||||||
|                         .IsUnique() |  | ||||||
|                         .HasDatabaseName("ix_bundles_slug"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("bundles", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid?>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("billing_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Description") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(8192) |  | ||||||
|                         .HasColumnType("character varying(8192)") |  | ||||||
|                         .HasColumnName("description"); |  | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsHidden") |  | ||||||
|                         .HasColumnType("boolean") |  | ||||||
|                         .HasColumnName("is_hidden"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Name") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasMaxLength(1024) |  | ||||||
|                         .HasColumnType("character varying(1024)") |  | ||||||
|                         .HasColumnName("name"); |  | ||||||
|  |  | ||||||
|                     b.Property<PolicyConfig>("PolicyConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("policy_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("storage_config"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_pools"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("pools", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle") |  | ||||||
|                         .WithMany("Files") |  | ||||||
|                         .HasForeignKey("BundleId") |  | ||||||
|                         .HasConstraintName("fk_files_bundles_bundle_id"); |  | ||||||
|  |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("PoolId") |  | ||||||
|                         .HasConstraintName("fk_files_pools_pool_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Bundle"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Pool"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") |  | ||||||
|                         .WithMany("References") |  | ||||||
|                         .HasForeignKey("FileId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_file_references_files_file_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("File"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("References"); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => |  | ||||||
|                 { |  | ||||||
|                     b.Navigation("Files"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,49 +0,0 @@ | |||||||
| using DysonNetwork.Drive; |  | ||||||
| using DysonNetwork.Drive.Startup; |  | ||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using DysonNetwork.Shared.Http; |  | ||||||
| using DysonNetwork.Shared.Registry; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); |  | ||||||
|  |  | ||||||
| builder.AddServiceDefaults(); |  | ||||||
|  |  | ||||||
| // Configure Kestrel and server options |  | ||||||
| builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue); |  | ||||||
|  |  | ||||||
| // Add application services |  | ||||||
|  |  | ||||||
| builder.Services.AddAppServices(builder.Configuration); |  | ||||||
| builder.Services.AddAppAuthentication(); |  | ||||||
| builder.Services.AddDysonAuth(); |  | ||||||
| builder.Services.AddAccountService(); |  | ||||||
|  |  | ||||||
| builder.Services.AddAppFlushHandlers(); |  | ||||||
| builder.Services.AddAppBusinessServices(); |  | ||||||
| builder.Services.AddAppScheduledJobs(); |  | ||||||
|  |  | ||||||
| builder.AddSwaggerManifest( |  | ||||||
|     "DysonNetwork.Drive", |  | ||||||
|     "The file upload and storage service in the Solar Network." |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| var app = builder.Build(); |  | ||||||
|  |  | ||||||
| app.MapDefaultEndpoints(); |  | ||||||
|  |  | ||||||
| // Run database migrations |  | ||||||
| using (var scope = app.Services.CreateScope()) |  | ||||||
| { |  | ||||||
|     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); |  | ||||||
|     await db.Database.MigrateAsync(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| app.ConfigureAppMiddleware(); |  | ||||||
|  |  | ||||||
| // Configure gRPC |  | ||||||
| app.ConfigureGrpcServices(); |  | ||||||
|  |  | ||||||
| app.UseSwaggerManifest("DysonNetwork.Drive"); |  | ||||||
|  |  | ||||||
| app.Run(); |  | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| { |  | ||||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", |  | ||||||
|   "profiles": { |  | ||||||
|     "http": { |  | ||||||
|       "commandName": "Project", |  | ||||||
|       "dotnetRunMessages": true, |  | ||||||
|       "launchBrowser": false, |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "https": { |  | ||||||
|       "commandName": "Project", |  | ||||||
|       "dotnetRunMessages": true, |  | ||||||
|       "launchBrowser": false, |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| using DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Startup; |  | ||||||
|  |  | ||||||
| public static class ApplicationBuilderExtensions |  | ||||||
| { |  | ||||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app) |  | ||||||
|     { |  | ||||||
|         app.UseAuthorization(); |  | ||||||
|         app.MapControllers(); |  | ||||||
|  |  | ||||||
|         return app; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static WebApplication ConfigureGrpcServices(this WebApplication app) |  | ||||||
|     { |  | ||||||
|         // Map your gRPC services here |  | ||||||
|         app.MapGrpcService<FileServiceGrpc>(); |  | ||||||
|         app.MapGrpcService<FileReferenceServiceGrpc>(); |  | ||||||
|         app.MapGrpcReflectionService(); |  | ||||||
|  |  | ||||||
|         return app; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,297 +0,0 @@ | |||||||
| using System.Text.Json; |  | ||||||
| using DysonNetwork.Drive.Storage.Model; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using DysonNetwork.Shared.Stream; |  | ||||||
| using FFMpegCore; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NATS.Client.Core; |  | ||||||
| using NATS.Client.JetStream; |  | ||||||
| using NATS.Client.JetStream.Models; |  | ||||||
| using NATS.Net; |  | ||||||
| using NetVips; |  | ||||||
| using NodaTime; |  | ||||||
| using FileService = DysonNetwork.Drive.Storage.FileService; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Startup; |  | ||||||
|  |  | ||||||
| public class BroadcastEventHandler( |  | ||||||
|     INatsConnection nats, |  | ||||||
|     ILogger<BroadcastEventHandler> logger, |  | ||||||
|     IServiceProvider serviceProvider |  | ||||||
| ) : BackgroundService |  | ||||||
| { |  | ||||||
|     private const string TempFileSuffix = "dypart"; |  | ||||||
|  |  | ||||||
|     private static readonly string[] AnimatedImageTypes = |  | ||||||
|         ["image/gif", "image/apng", "image/avif"]; |  | ||||||
|  |  | ||||||
|     private static readonly string[] AnimatedImageExtensions = |  | ||||||
|         [".gif", ".apng", ".avif"]; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) |  | ||||||
|     { |  | ||||||
|         var js = nats.CreateJetStreamContext(); |  | ||||||
|  |  | ||||||
|         await js.EnsureStreamCreated("account_events", [AccountDeletedEvent.Type]); |  | ||||||
|          |  | ||||||
|         var accountEventConsumer = await js.CreateOrUpdateConsumerAsync("account_events", |  | ||||||
|             new ConsumerConfig("drive_account_deleted_handler"), cancellationToken: stoppingToken); |  | ||||||
|          |  | ||||||
|         await js.EnsureStreamCreated("file_events", [FileUploadedEvent.Type]); |  | ||||||
|         var fileUploadedConsumer = await js.CreateOrUpdateConsumerAsync("file_events", |  | ||||||
|             new ConsumerConfig("drive_file_uploaded_handler") { MaxDeliver = 3 }, cancellationToken: stoppingToken); |  | ||||||
|  |  | ||||||
|         var accountDeletedTask = HandleAccountDeleted(accountEventConsumer, stoppingToken); |  | ||||||
|         var fileUploadedTask = HandleFileUploaded(fileUploadedConsumer, stoppingToken); |  | ||||||
|  |  | ||||||
|         await Task.WhenAll(accountDeletedTask, fileUploadedTask); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task HandleFileUploaded(INatsJSConsumer consumer, CancellationToken stoppingToken) |  | ||||||
|     { |  | ||||||
|         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) |  | ||||||
|         { |  | ||||||
|             var payload = JsonSerializer.Deserialize<FileUploadedEventPayload>(msg.Data, GrpcTypeHelper.SerializerOptions); |  | ||||||
|             if (payload == null) |  | ||||||
|             { |  | ||||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             try |  | ||||||
|             { |  | ||||||
|                 await ProcessAndUploadInBackgroundAsync( |  | ||||||
|                     payload.FileId, |  | ||||||
|                     payload.RemoteId, |  | ||||||
|                     payload.StorageId, |  | ||||||
|                     payload.ContentType, |  | ||||||
|                     payload.ProcessingFilePath, |  | ||||||
|                     payload.IsTempFile |  | ||||||
|                 ); |  | ||||||
|  |  | ||||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); |  | ||||||
|             } |  | ||||||
|             catch (Exception ex) |  | ||||||
|             { |  | ||||||
|                 logger.LogError(ex, "Error processing FileUploadedEvent for file {FileId}", payload.FileId); |  | ||||||
|                 await msg.NakAsync(cancellationToken: stoppingToken, delay: TimeSpan.FromSeconds(60)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task HandleAccountDeleted(INatsJSConsumer consumer, CancellationToken stoppingToken) |  | ||||||
|     { |  | ||||||
|         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) |  | ||||||
|         { |  | ||||||
|             try |  | ||||||
|             { |  | ||||||
|                 var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data, GrpcTypeHelper.SerializerOptions); |  | ||||||
|                 if (evt == null) |  | ||||||
|                 { |  | ||||||
|                     await msg.AckAsync(cancellationToken: stoppingToken); |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 logger.LogInformation("Account deleted: {AccountId}", evt.AccountId); |  | ||||||
|  |  | ||||||
|                 using var scope = serviceProvider.CreateScope(); |  | ||||||
|                 var fs = scope.ServiceProvider.GetRequiredService<FileService>(); |  | ||||||
|                 var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); |  | ||||||
|  |  | ||||||
|                 await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken: stoppingToken); |  | ||||||
|                 try |  | ||||||
|                 { |  | ||||||
|                     var files = await db.Files |  | ||||||
|                         .Where(p => p.AccountId == evt.AccountId) |  | ||||||
|                         .ToListAsync(cancellationToken: stoppingToken); |  | ||||||
|  |  | ||||||
|                     await fs.DeleteFileDataBatchAsync(files); |  | ||||||
|                     await db.Files |  | ||||||
|                         .Where(p => p.AccountId == evt.AccountId) |  | ||||||
|                         .ExecuteDeleteAsync(cancellationToken: stoppingToken); |  | ||||||
|  |  | ||||||
|                     await transaction.CommitAsync(cancellationToken: stoppingToken); |  | ||||||
|                 } |  | ||||||
|                 catch (Exception) |  | ||||||
|                 { |  | ||||||
|                     await transaction.RollbackAsync(cancellationToken: stoppingToken); |  | ||||||
|                     throw; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); |  | ||||||
|             } |  | ||||||
|             catch (Exception ex) |  | ||||||
|             { |  | ||||||
|                 logger.LogError(ex, "Error processing AccountDeleted"); |  | ||||||
|                 await msg.NakAsync(cancellationToken: stoppingToken); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|      private async Task ProcessAndUploadInBackgroundAsync( |  | ||||||
|         string fileId, |  | ||||||
|         Guid remoteId, |  | ||||||
|         string storageId, |  | ||||||
|         string contentType, |  | ||||||
|         string processingFilePath, |  | ||||||
|         bool isTempFile |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         using var scope = serviceProvider.CreateScope(); |  | ||||||
|         var fs = scope.ServiceProvider.GetRequiredService<FileService>(); |  | ||||||
|         var scopedDb = scope.ServiceProvider.GetRequiredService<AppDatabase>(); |  | ||||||
|  |  | ||||||
|         var pool = await fs.GetPoolAsync(remoteId); |  | ||||||
|         if (pool is null) return; |  | ||||||
|  |  | ||||||
|         var uploads = new List<(string FilePath, string Suffix, string ContentType, bool SelfDestruct)>(); |  | ||||||
|         var newMimeType = contentType; |  | ||||||
|         var hasCompression = false; |  | ||||||
|         var hasThumbnail = false; |  | ||||||
|  |  | ||||||
|         logger.LogInformation("Processing file {FileId} in background...", fileId); |  | ||||||
|  |  | ||||||
|         var fileToUpdate = await scopedDb.Files.AsNoTracking().FirstAsync(f => f.Id == fileId); |  | ||||||
|  |  | ||||||
|         if (fileToUpdate.IsEncrypted) |  | ||||||
|         { |  | ||||||
|             uploads.Add((processingFilePath, string.Empty, contentType, false)); |  | ||||||
|         } |  | ||||||
|         else if (!pool.PolicyConfig.NoOptimization) |  | ||||||
|         { |  | ||||||
|             var fileExtension = Path.GetExtension(processingFilePath); |  | ||||||
|             switch (contentType.Split('/')[0]) |  | ||||||
|             { |  | ||||||
|                 case "image": |  | ||||||
|                     if (AnimatedImageTypes.Contains(contentType) || AnimatedImageExtensions.Contains(fileExtension)) |  | ||||||
|                     { |  | ||||||
|                         logger.LogInformation("Skip optimize file {FileId} due to it is animated...", fileId); |  | ||||||
|                         uploads.Add((processingFilePath, string.Empty, contentType, false)); |  | ||||||
|                         break; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     try |  | ||||||
|                     { |  | ||||||
|                         newMimeType = "image/webp"; |  | ||||||
|                         using var vipsImage = Image.NewFromFile(processingFilePath); |  | ||||||
|                         var imageToWrite = vipsImage; |  | ||||||
|  |  | ||||||
|                         if (vipsImage.Interpretation is Enums.Interpretation.Scrgb or Enums.Interpretation.Xyz) |  | ||||||
|                         { |  | ||||||
|                             imageToWrite = vipsImage.Colourspace(Enums.Interpretation.Srgb); |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         var webpPath = Path.Join(Path.GetTempPath(), $"{fileId}.{TempFileSuffix}.webp"); |  | ||||||
|                         imageToWrite.Autorot().WriteToFile(webpPath, |  | ||||||
|                             new VOption { { "lossless", true }, { "strip", true } }); |  | ||||||
|                         uploads.Add((webpPath, string.Empty, newMimeType, true)); |  | ||||||
|  |  | ||||||
|                         if (imageToWrite.Width * imageToWrite.Height >= 1024 * 1024) |  | ||||||
|                         { |  | ||||||
|                             var scale = 1024.0 / Math.Max(imageToWrite.Width, imageToWrite.Height); |  | ||||||
|                             var compressedPath = |  | ||||||
|                                 Path.Join(Path.GetTempPath(), $"{fileId}.{TempFileSuffix}.compressed.webp"); |  | ||||||
|                             using var compressedImage = imageToWrite.Resize(scale); |  | ||||||
|                             compressedImage.Autorot().WriteToFile(compressedPath, |  | ||||||
|                                 new VOption { { "Q", 80 }, { "strip", true } }); |  | ||||||
|                             uploads.Add((compressedPath, ".compressed", newMimeType, true)); |  | ||||||
|                             hasCompression = true; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         if (!ReferenceEquals(imageToWrite, vipsImage)) |  | ||||||
|                         { |  | ||||||
|                             imageToWrite.Dispose(); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     catch (Exception ex) |  | ||||||
|                     { |  | ||||||
|                         logger.LogError(ex, "Failed to optimize image {FileId}, uploading original", fileId); |  | ||||||
|                         uploads.Add((processingFilePath, string.Empty, contentType, false)); |  | ||||||
|                         newMimeType = contentType; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     break; |  | ||||||
|  |  | ||||||
|                 case "video": |  | ||||||
|                     uploads.Add((processingFilePath, string.Empty, contentType, false)); |  | ||||||
|  |  | ||||||
|                     var thumbnailPath = Path.Join(Path.GetTempPath(), $"{fileId}.{TempFileSuffix}.thumbnail.jpg"); |  | ||||||
|                     try |  | ||||||
|                     { |  | ||||||
|                         await FFMpegArguments |  | ||||||
|                             .FromFileInput(processingFilePath, verifyExists: true) |  | ||||||
|                             .OutputToFile(thumbnailPath, overwrite: true, options => options |  | ||||||
|                                 .Seek(TimeSpan.FromSeconds(0)) |  | ||||||
|                                 .WithFrameOutputCount(1) |  | ||||||
|                                 .WithCustomArgument("-q:v 2") |  | ||||||
|                             ) |  | ||||||
|                             .NotifyOnOutput(line => logger.LogInformation("[FFmpeg] {Line}", line)) |  | ||||||
|                             .NotifyOnError(line => logger.LogWarning("[FFmpeg] {Line}", line)) |  | ||||||
|                             .ProcessAsynchronously(); |  | ||||||
|  |  | ||||||
|                         if (File.Exists(thumbnailPath)) |  | ||||||
|                         { |  | ||||||
|                             uploads.Add((thumbnailPath, ".thumbnail", "image/jpeg", true)); |  | ||||||
|                             hasThumbnail = true; |  | ||||||
|                         } |  | ||||||
|                         else |  | ||||||
|                         { |  | ||||||
|                             logger.LogWarning("FFMpeg did not produce thumbnail for video {FileId}", fileId); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     catch (Exception ex) |  | ||||||
|                     { |  | ||||||
|                         logger.LogError(ex, "Failed to generate thumbnail for video {FileId}", fileId); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     break; |  | ||||||
|  |  | ||||||
|                 default: |  | ||||||
|                     uploads.Add((processingFilePath, string.Empty, contentType, false)); |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         else |  | ||||||
|         { |  | ||||||
|             uploads.Add((processingFilePath, string.Empty, contentType, false)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         logger.LogInformation("Optimized file {FileId}, now uploading...", fileId); |  | ||||||
|  |  | ||||||
|         if (uploads.Count > 0) |  | ||||||
|         { |  | ||||||
|             var destPool = remoteId; |  | ||||||
|             var uploadTasks = uploads.Select(item => |  | ||||||
|                 fs.UploadFileToRemoteAsync( |  | ||||||
|                     storageId, |  | ||||||
|                     destPool, |  | ||||||
|                     item.FilePath, |  | ||||||
|                     item.Suffix, |  | ||||||
|                     item.ContentType, |  | ||||||
|                     item.SelfDestruct |  | ||||||
|                 ) |  | ||||||
|             ).ToList(); |  | ||||||
|  |  | ||||||
|             await Task.WhenAll(uploadTasks); |  | ||||||
|  |  | ||||||
|             logger.LogInformation("Uploaded file {FileId} done!", fileId); |  | ||||||
|  |  | ||||||
|             var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|             await scopedDb.Files.Where(f => f.Id == fileId).ExecuteUpdateAsync(setter => setter |  | ||||||
|                 .SetProperty(f => f.UploadedAt, now) |  | ||||||
|                 .SetProperty(f => f.PoolId, destPool) |  | ||||||
|                 .SetProperty(f => f.MimeType, newMimeType) |  | ||||||
|                 .SetProperty(f => f.HasCompression, hasCompression) |  | ||||||
|                 .SetProperty(f => f.HasThumbnail, hasThumbnail) |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|             // Only delete temp file after successful upload and db update |  | ||||||
|             if (isTempFile) |  | ||||||
|                 File.Delete(processingFilePath); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await fs._PurgeCacheAsync(fileId); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| using DysonNetwork.Drive.Storage; |  | ||||||
| using Quartz; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Startup; |  | ||||||
|  |  | ||||||
| public static class ScheduledJobsConfiguration |  | ||||||
| { |  | ||||||
|     public static IServiceCollection AddAppScheduledJobs(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         services.AddQuartz(q => |  | ||||||
|         { |  | ||||||
|             var appDatabaseRecyclingJob = new JobKey("AppDatabaseRecycling"); |  | ||||||
|             q.AddJob<AppDatabaseRecyclingJob>(opts => opts.WithIdentity(appDatabaseRecyclingJob)); |  | ||||||
|             q.AddTrigger(opts => opts |  | ||||||
|                 .ForJob(appDatabaseRecyclingJob) |  | ||||||
|                 .WithIdentity("AppDatabaseRecyclingTrigger") |  | ||||||
|                 .WithCronSchedule("0 0 0 * * ?")); |  | ||||||
|              |  | ||||||
|             var cloudFileUnusedRecyclingJob = new JobKey("CloudFileUnusedRecycling"); |  | ||||||
|             q.AddJob<CloudFileUnusedRecyclingJob>(opts => opts.WithIdentity(cloudFileUnusedRecyclingJob)); |  | ||||||
|             q.AddTrigger(opts => opts |  | ||||||
|                 .ForJob(cloudFileUnusedRecyclingJob) |  | ||||||
|                 .WithIdentity("CloudFileUnusedRecyclingTrigger") |  | ||||||
|                 .WithCronSchedule("0 0 0 * * ?")); |  | ||||||
|         }); |  | ||||||
|         services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); |  | ||||||
|  |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,65 +0,0 @@ | |||||||
| using System.Text.Json; |  | ||||||
| using System.Text.Json.Serialization; |  | ||||||
| using DysonNetwork.Shared.Cache; |  | ||||||
| using NodaTime; |  | ||||||
| using NodaTime.Serialization.SystemTextJson; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Startup; |  | ||||||
|  |  | ||||||
| public static class ServiceCollectionExtensions |  | ||||||
| { |  | ||||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase |  | ||||||
|         services.AddSingleton<IClock>(SystemClock.Instance); |  | ||||||
|         services.AddHttpContextAccessor(); |  | ||||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis |  | ||||||
|  |  | ||||||
|         services.AddHttpClient(); |  | ||||||
|  |  | ||||||
|         // Register gRPC services |  | ||||||
|         services.AddGrpc(options => |  | ||||||
|         { |  | ||||||
|             options.EnableDetailedErrors = true; // Will be adjusted in Program.cs |  | ||||||
|             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB |  | ||||||
|             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB |  | ||||||
|         }); |  | ||||||
|         services.AddGrpcReflection(); |  | ||||||
|  |  | ||||||
|         services.AddControllers().AddJsonOptions(options => |  | ||||||
|         { |  | ||||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; |  | ||||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; |  | ||||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; |  | ||||||
|  |  | ||||||
|             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         services.AddAuthorization(); |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         services.AddSingleton<FlushBufferService>(); |  | ||||||
|  |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppBusinessServices(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         services.AddScoped<Storage.FileService>(); |  | ||||||
|         services.AddScoped<Storage.FileReferenceService>(); |  | ||||||
|         services.AddScoped<Billing.UsageService>(); |  | ||||||
|         services.AddScoped<Billing.QuotaService>(); |  | ||||||
|  |  | ||||||
|         services.AddHostedService<BroadcastEventHandler>(); |  | ||||||
|          |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,154 +0,0 @@ | |||||||
| using System.ComponentModel.DataAnnotations; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/bundles")] |  | ||||||
| public class BundleController(AppDatabase db) : ControllerBase |  | ||||||
| { |  | ||||||
|     public class BundleRequest |  | ||||||
|     { |  | ||||||
|         [MaxLength(1024)] public string? Slug { get; set; } |  | ||||||
|         [MaxLength(1024)] public string? Name { get; set; } |  | ||||||
|         [MaxLength(8192)] public string? Description { get; set; } |  | ||||||
|         [MaxLength(256)] public string? Passcode { get; set; } |  | ||||||
|  |  | ||||||
|         public Instant? ExpiredAt { get; set; } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{id:guid}")] |  | ||||||
|     public async Task<ActionResult<SnFileBundle>> GetBundle([FromRoute] Guid id, [FromQuery] string? passcode) |  | ||||||
|     { |  | ||||||
|         var bundle = await db.Bundles |  | ||||||
|             .Where(e => e.Id == id) |  | ||||||
|             .Include(e => e.Files) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|         if (bundle is null) return NotFound(); |  | ||||||
|         if (!bundle.VerifyPasscode(passcode)) return Forbid(); |  | ||||||
|  |  | ||||||
|         return Ok(bundle); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("me")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult<List<SnFileBundle>>> ListBundles( |  | ||||||
|         [FromQuery] string? term, |  | ||||||
|         [FromQuery] int offset = 0, |  | ||||||
|         [FromQuery] int take = 20 |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var query = db.Bundles |  | ||||||
|             .Where(e => e.AccountId == accountId) |  | ||||||
|             .OrderByDescending(e => e.CreatedAt) |  | ||||||
|             .AsQueryable(); |  | ||||||
|         if (!string.IsNullOrEmpty(term)) |  | ||||||
|             query = query.Where(e => EF.Functions.ILike(e.Name, $"%{term}%")); |  | ||||||
|  |  | ||||||
|         var total = await query.CountAsync(); |  | ||||||
|         Response.Headers.Append("X-Total", total.ToString()); |  | ||||||
|  |  | ||||||
|         var bundles = await query |  | ||||||
|             .Skip(offset) |  | ||||||
|             .Take(take) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         return Ok(bundles); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult<SnFileBundle>> CreateBundle([FromBody] BundleRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         if (currentUser.PerkSubscription is null && !string.IsNullOrEmpty(request.Slug)) |  | ||||||
|             return StatusCode(403, "You must have a subscription to create a bundle with a custom slug"); |  | ||||||
|         if (string.IsNullOrEmpty(request.Slug)) |  | ||||||
|             request.Slug = Guid.NewGuid().ToString("N")[..6]; |  | ||||||
|         if (string.IsNullOrEmpty(request.Name)) |  | ||||||
|             request.Name = "Unnamed Bundle"; |  | ||||||
|  |  | ||||||
|         var bundle = new SnFileBundle |  | ||||||
|         { |  | ||||||
|             Slug = request.Slug, |  | ||||||
|             Name = request.Name, |  | ||||||
|             Description = request.Description, |  | ||||||
|             Passcode = request.Passcode, |  | ||||||
|             ExpiredAt = request.ExpiredAt, |  | ||||||
|             AccountId = accountId |  | ||||||
|         }.HashPasscode(); |  | ||||||
|  |  | ||||||
|         db.Bundles.Add(bundle); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         return Ok(bundle); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPut("{id:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult<SnFileBundle>> UpdateBundle([FromRoute] Guid id, [FromBody] BundleRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var bundle = await db.Bundles |  | ||||||
|             .Where(e => e.Id == id) |  | ||||||
|             .Where(e => e.AccountId == accountId) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|         if (bundle is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         if (request.Slug != null && request.Slug != bundle.Slug) |  | ||||||
|         { |  | ||||||
|             if (currentUser.PerkSubscription is null) |  | ||||||
|                 return StatusCode(403, "You must have a subscription to change the slug of a bundle"); |  | ||||||
|             bundle.Slug = request.Slug; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (request.Name != null) bundle.Name = request.Name; |  | ||||||
|         if (request.Description != null) bundle.Description = request.Description; |  | ||||||
|         if (request.ExpiredAt != null) bundle.ExpiredAt = request.ExpiredAt; |  | ||||||
|  |  | ||||||
|         if (request.Passcode != null) |  | ||||||
|         { |  | ||||||
|             bundle.Passcode = request.Passcode; |  | ||||||
|             bundle = bundle.HashPasscode(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         return Ok(bundle); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpDelete("{id:guid}")] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult> DeleteBundle([FromRoute] Guid id) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var bundle = await db.Bundles |  | ||||||
|             .Where(e => e.Id == id) |  | ||||||
|             .Where(e => e.AccountId == accountId) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|         if (bundle is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         db.Bundles.Remove(bundle); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         await db.Files |  | ||||||
|             .Where(e => e.BundleId == id) |  | ||||||
|             .ExecuteUpdateAsync(s => s.SetProperty(e => e.IsMarkedRecycle, true)); |  | ||||||
|  |  | ||||||
|         return NoContent(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,397 +0,0 @@ | |||||||
| using DysonNetwork.Drive.Billing; |  | ||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Minio.DataModel.Args; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/files")] |  | ||||||
| public class FileController( |  | ||||||
|     AppDatabase db, |  | ||||||
|     FileService fs, |  | ||||||
|     QuotaService qs, |  | ||||||
|     IConfiguration configuration, |  | ||||||
|     IWebHostEnvironment env |  | ||||||
| ) : ControllerBase |  | ||||||
| { |  | ||||||
|     [HttpGet("{id}")] |  | ||||||
|     public async Task<ActionResult> OpenFile( |  | ||||||
|         string id, |  | ||||||
|         [FromQuery] bool download = false, |  | ||||||
|         [FromQuery] bool original = false, |  | ||||||
|         [FromQuery] bool thumbnail = false, |  | ||||||
|         [FromQuery] string? overrideMimeType = null, |  | ||||||
|         [FromQuery] string? passcode = null |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         // Support the file extension for client side data recognize |  | ||||||
|         string? fileExtension = null; |  | ||||||
|         if (id.Contains('.')) |  | ||||||
|         { |  | ||||||
|             var splitId = id.Split('.'); |  | ||||||
|             id = splitId.First(); |  | ||||||
|             fileExtension = splitId.Last(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var file = await fs.GetFileAsync(id); |  | ||||||
|         if (file is null) return NotFound("File not found."); |  | ||||||
|  |  | ||||||
|         if (file.Bundle is not null && !file.Bundle.VerifyPasscode(passcode)) |  | ||||||
|             return StatusCode(StatusCodes.Status403Forbidden, "The passcode is incorrect."); |  | ||||||
|  |  | ||||||
|         if (!string.IsNullOrWhiteSpace(file.StorageUrl)) return Redirect(file.StorageUrl); |  | ||||||
|  |  | ||||||
|         if (file.UploadedAt is null) |  | ||||||
|         { |  | ||||||
|             // File is not yet uploaded to remote storage. Try to serve from local temp storage. |  | ||||||
|             var tempFilePath = Path.Combine(Path.GetTempPath(), file.Id); |  | ||||||
|             if (System.IO.File.Exists(tempFilePath)) |  | ||||||
|             { |  | ||||||
|                 if (file.IsEncrypted) |  | ||||||
|                 { |  | ||||||
|                     return StatusCode(StatusCodes.Status403Forbidden, "Encrypted files cannot be accessed before they are processed and stored."); |  | ||||||
|                 } |  | ||||||
|                 return PhysicalFile(tempFilePath, file.MimeType ?? "application/octet-stream", file.Name, enableRangeProcessing: true); |  | ||||||
|             } |  | ||||||
|          |  | ||||||
|             // Fallback for tus uploads that are not processed yet. |  | ||||||
|             var tusStorePath = configuration.GetValue<string>("Tus:StorePath"); |  | ||||||
|             if (!string.IsNullOrEmpty(tusStorePath)) |  | ||||||
|             { |  | ||||||
|                 var tusFilePath = Path.Combine(env.ContentRootPath, tusStorePath, file.Id); |  | ||||||
|                 if (System.IO.File.Exists(tusFilePath)) |  | ||||||
|                 { |  | ||||||
|                     return PhysicalFile(tusFilePath, file.MimeType ?? "application/octet-stream", file.Name, enableRangeProcessing: true); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return StatusCode(StatusCodes.Status400BadRequest, "File is being processed. Please try again later."); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!file.PoolId.HasValue) |  | ||||||
|             return StatusCode(StatusCodes.Status500InternalServerError, "File is in an inconsistent state: uploaded but no pool ID."); |  | ||||||
|  |  | ||||||
|         var pool = await fs.GetPoolAsync(file.PoolId.Value); |  | ||||||
|         if (pool is null) |  | ||||||
|             return StatusCode(StatusCodes.Status410Gone, "The pool of the file no longer exists or not accessible."); |  | ||||||
|         var dest = pool.StorageConfig; |  | ||||||
|  |  | ||||||
|         if (!pool.PolicyConfig.AllowAnonymous) |  | ||||||
|             if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|                 return Unauthorized(); |  | ||||||
|         // TODO: Provide ability to add access log |  | ||||||
|  |  | ||||||
|         var fileName = string.IsNullOrWhiteSpace(file.StorageId) ? file.Id : file.StorageId; |  | ||||||
|  |  | ||||||
|         switch (thumbnail) |  | ||||||
|         { |  | ||||||
|             case true when file.HasThumbnail: |  | ||||||
|                 fileName += ".thumbnail"; |  | ||||||
|                 break; |  | ||||||
|             case true when !file.HasThumbnail: |  | ||||||
|                 return NotFound(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!original && file.HasCompression) |  | ||||||
|             fileName += ".compressed"; |  | ||||||
|  |  | ||||||
|         if (dest.ImageProxy is not null && (file.MimeType?.StartsWith("image/") ?? false)) |  | ||||||
|         { |  | ||||||
|             var proxyUrl = dest.ImageProxy; |  | ||||||
|             var baseUri = new Uri(proxyUrl.EndsWith('/') ? proxyUrl : $"{proxyUrl}/"); |  | ||||||
|             var fullUri = new Uri(baseUri, fileName); |  | ||||||
|             return Redirect(fullUri.ToString()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (dest.AccessProxy is not null) |  | ||||||
|         { |  | ||||||
|             var proxyUrl = dest.AccessProxy; |  | ||||||
|             var baseUri = new Uri(proxyUrl.EndsWith('/') ? proxyUrl : $"{proxyUrl}/"); |  | ||||||
|             var fullUri = new Uri(baseUri, fileName); |  | ||||||
|             return Redirect(fullUri.ToString()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (dest.EnableSigned) |  | ||||||
|         { |  | ||||||
|             var client = fs.CreateMinioClient(dest); |  | ||||||
|             if (client is null) |  | ||||||
|                 return BadRequest( |  | ||||||
|                     "Failed to configure client for remote destination, file got an invalid storage remote." |  | ||||||
|                 ); |  | ||||||
|  |  | ||||||
|             var headers = new Dictionary<string, string>(); |  | ||||||
|             if (fileExtension is not null) |  | ||||||
|             { |  | ||||||
|                 if (MimeTypes.TryGetMimeType(fileExtension, out var mimeType)) |  | ||||||
|                     headers.Add("Response-Content-Type", mimeType); |  | ||||||
|             } |  | ||||||
|             else if (overrideMimeType is not null) |  | ||||||
|             { |  | ||||||
|                 headers.Add("Response-Content-Type", overrideMimeType); |  | ||||||
|             } |  | ||||||
|             else if (file.MimeType is not null && !file.MimeType!.EndsWith("unknown")) |  | ||||||
|             { |  | ||||||
|                 headers.Add("Response-Content-Type", file.MimeType); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (download) |  | ||||||
|             { |  | ||||||
|                 headers.Add("Response-Content-Disposition", $"attachment; filename=\"{file.Name}\""); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var bucket = dest.Bucket; |  | ||||||
|             var openUrl = await client.PresignedGetObjectAsync( |  | ||||||
|                 new PresignedGetObjectArgs() |  | ||||||
|                     .WithBucket(bucket) |  | ||||||
|                     .WithObject(fileName) |  | ||||||
|                     .WithExpiry(3600) |  | ||||||
|                     .WithHeaders(headers) |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|             return Redirect(openUrl); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Fallback redirect to the S3 endpoint (public read) |  | ||||||
|         var protocol = dest.EnableSsl ? "https" : "http"; |  | ||||||
|         // Use the path bucket lookup mode |  | ||||||
|         return Redirect($"{protocol}://{dest.Endpoint}/{dest.Bucket}/{fileName}"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpGet("{id}/info")] |  | ||||||
|     public async Task<ActionResult<SnCloudFile>> GetFileInfo(string id) |  | ||||||
|     { |  | ||||||
|         var file = await fs.GetFileAsync(id); |  | ||||||
|         if (file is null) return NotFound("File not found."); |  | ||||||
|  |  | ||||||
|         return file; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpPatch("{id}/name")] |  | ||||||
|     public async Task<ActionResult<SnCloudFile>> UpdateFileName(string id, [FromBody] string name) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|         var file = await db.Files.FirstOrDefaultAsync(f => f.Id == id && f.AccountId == accountId); |  | ||||||
|         if (file is null) return NotFound(); |  | ||||||
|         file.Name = name; |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         await fs._PurgeCacheAsync(file.Id); |  | ||||||
|         return file; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public class MarkFileRequest |  | ||||||
|     { |  | ||||||
|         public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpPut("{id}/marks")] |  | ||||||
|     public async Task<ActionResult<SnCloudFile>> MarkFile(string id, [FromBody] MarkFileRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|         var file = await db.Files.FirstOrDefaultAsync(f => f.Id == id && f.AccountId == accountId); |  | ||||||
|         if (file is null) return NotFound(); |  | ||||||
|         file.SensitiveMarks = request.SensitiveMarks; |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         await fs._PurgeCacheAsync(file.Id); |  | ||||||
|         return file; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpPut("{id}/meta")] |  | ||||||
|     public async Task<ActionResult<SnCloudFile>> UpdateFileMeta(string id, [FromBody] Dictionary<string, object?> meta) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|         var file = await db.Files.FirstOrDefaultAsync(f => f.Id == id && f.AccountId == accountId); |  | ||||||
|         if (file is null) return NotFound(); |  | ||||||
|         file.UserMeta = meta; |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         await fs._PurgeCacheAsync(file.Id); |  | ||||||
|         return file; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpGet("me")] |  | ||||||
|     public async Task<ActionResult<List<SnCloudFile>>> GetMyFiles( |  | ||||||
|         [FromQuery] Guid? pool, |  | ||||||
|         [FromQuery] bool recycled = false, |  | ||||||
|         [FromQuery] int offset = 0, |  | ||||||
|         [FromQuery] int take = 20 |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var query = db.Files |  | ||||||
|             .Where(e => e.IsMarkedRecycle == recycled) |  | ||||||
|             .Where(e => e.AccountId == accountId) |  | ||||||
|             .Include(e => e.Pool) |  | ||||||
|             .OrderByDescending(e => e.CreatedAt) |  | ||||||
|             .AsQueryable(); |  | ||||||
|  |  | ||||||
|         if (pool.HasValue) query = query.Where(e => e.PoolId == pool); |  | ||||||
|  |  | ||||||
|         var total = await query.CountAsync(); |  | ||||||
|         Response.Headers.Append("X-Total", total.ToString()); |  | ||||||
|  |  | ||||||
|         var files = await query |  | ||||||
|             .Skip(offset) |  | ||||||
|             .Take(take) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         return Ok(files); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpDelete("{id}")] |  | ||||||
|     public async Task<ActionResult> DeleteFile(string id) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var userId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var file = await db.Files |  | ||||||
|             .Where(e => e.Id == id) |  | ||||||
|             .Where(e => e.AccountId == userId) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|         if (file is null) return NotFound(); |  | ||||||
|  |  | ||||||
|         await fs.DeleteFileDataAsync(file, force: true); |  | ||||||
|         await fs.DeleteFileAsync(file); |  | ||||||
|  |  | ||||||
|         return NoContent(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpDelete("me/recycle")] |  | ||||||
|     public async Task<ActionResult> DeleteMyRecycledFiles() |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var count = await fs.DeleteAccountRecycledFilesAsync(accountId); |  | ||||||
|         return Ok(new { Count = count }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpDelete("recycle")] |  | ||||||
|     [RequiredPermission("maintenance", "files.delete.recycle")] |  | ||||||
|     public async Task<ActionResult> DeleteAllRecycledFiles() |  | ||||||
|     { |  | ||||||
|         var count = await fs.DeleteAllRecycledFilesAsync(); |  | ||||||
|         return Ok(new { Count = count }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public class CreateFastFileRequest |  | ||||||
|     { |  | ||||||
|         public string Name { get; set; } = null!; |  | ||||||
|         public long Size { get; set; } |  | ||||||
|         public string Hash { get; set; } = null!; |  | ||||||
|         public string? MimeType { get; set; } |  | ||||||
|         public string? Description { get; set; } |  | ||||||
|         public Dictionary<string, object?>? UserMeta { get; set; } |  | ||||||
|         public Dictionary<string, object?>? FileMeta { get; set; } |  | ||||||
|         public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; } |  | ||||||
|         public Guid PoolId { get; set; } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpPost("fast")] |  | ||||||
|     [RequiredPermission("global", "files.create")] |  | ||||||
|     public async Task<ActionResult<SnCloudFile>> CreateFastFile([FromBody] CreateFastFileRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var pool = await db.Pools.FirstOrDefaultAsync(p => p.Id == request.PoolId); |  | ||||||
|         if (pool is null) return BadRequest(); |  | ||||||
|         if (!currentUser.IsSuperuser && pool.AccountId != accountId) |  | ||||||
|             return StatusCode(403, "You don't have permission to create files in this pool."); |  | ||||||
|  |  | ||||||
|         if (!pool.PolicyConfig.EnableFastUpload) |  | ||||||
|             return StatusCode( |  | ||||||
|                 403, |  | ||||||
|                 "This pool does not allow fast upload" |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|         if (pool.PolicyConfig.RequirePrivilege > 0) |  | ||||||
|         { |  | ||||||
|             if (currentUser.PerkSubscription is null) |  | ||||||
|             { |  | ||||||
|                 return StatusCode( |  | ||||||
|                     403, |  | ||||||
|                     $"You need to have join the Stellar Program to use this pool" |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var privilege = |  | ||||||
|                 PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier); |  | ||||||
|             if (privilege < pool.PolicyConfig.RequirePrivilege) |  | ||||||
|             { |  | ||||||
|                 return StatusCode( |  | ||||||
|                     403, |  | ||||||
|                     $"You need Stellar Program tier {pool.PolicyConfig.RequirePrivilege} to use this pool, you are tier {privilege}" |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (request.Size > pool.PolicyConfig.MaxFileSize) |  | ||||||
|         { |  | ||||||
|             return StatusCode( |  | ||||||
|                 403, |  | ||||||
|                 $"File size {request.Size} is larger than the pool's maximum file size {pool.PolicyConfig.MaxFileSize}" |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var (ok, billableUnit, quota) = await qs.IsFileAcceptable( |  | ||||||
|             accountId, |  | ||||||
|             pool.BillingConfig.CostMultiplier ?? 1.0, |  | ||||||
|             request.Size |  | ||||||
|         ); |  | ||||||
|         if (!ok) |  | ||||||
|         { |  | ||||||
|             return StatusCode( |  | ||||||
|                 403, |  | ||||||
|                 $"File size {billableUnit} is larger than the user's quota {quota}" |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await using var transaction = await db.Database.BeginTransactionAsync(); |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var file = new SnCloudFile |  | ||||||
|             { |  | ||||||
|                 Name = request.Name, |  | ||||||
|                 Size = request.Size, |  | ||||||
|                 Hash = request.Hash, |  | ||||||
|                 MimeType = request.MimeType, |  | ||||||
|                 Description = request.Description, |  | ||||||
|                 AccountId = accountId, |  | ||||||
|                 UserMeta = request.UserMeta, |  | ||||||
|                 FileMeta = request.FileMeta, |  | ||||||
|                 SensitiveMarks = request.SensitiveMarks, |  | ||||||
|                 PoolId = request.PoolId |  | ||||||
|             }; |  | ||||||
|             db.Files.Add(file); |  | ||||||
|             await db.SaveChangesAsync(); |  | ||||||
|             await fs._PurgeCacheAsync(file.Id); |  | ||||||
|             await transaction.CommitAsync(); |  | ||||||
|  |  | ||||||
|             file.FastUploadLink = await fs.CreateFastUploadLinkAsync(file); |  | ||||||
|  |  | ||||||
|             return file; |  | ||||||
|         } |  | ||||||
|         catch (Exception) |  | ||||||
|         { |  | ||||||
|             await transaction.RollbackAsync(); |  | ||||||
|             throw; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,60 +0,0 @@ | |||||||
| using System.Security.Cryptography; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| public static class FileEncryptor |  | ||||||
| { |  | ||||||
|     public static void EncryptFile(string inputPath, string outputPath, string password) |  | ||||||
|     { |  | ||||||
|         var salt = RandomNumberGenerator.GetBytes(16); |  | ||||||
|         var key = DeriveKey(password, salt, 32); |  | ||||||
|         var nonce = RandomNumberGenerator.GetBytes(12); // For AES-GCM |  | ||||||
|  |  | ||||||
|         using var aes = new AesGcm(key, 16); // Specify 16-byte tag size explicitly |  | ||||||
|         var plaintext = File.ReadAllBytes(inputPath); |  | ||||||
|         var magic = "DYSON1"u8.ToArray(); |  | ||||||
|         var contentWithMagic = new byte[magic.Length + plaintext.Length]; |  | ||||||
|         Buffer.BlockCopy(magic, 0, contentWithMagic, 0, magic.Length); |  | ||||||
|         Buffer.BlockCopy(plaintext, 0, contentWithMagic, magic.Length, plaintext.Length); |  | ||||||
|  |  | ||||||
|         var ciphertext = new byte[contentWithMagic.Length]; |  | ||||||
|         var tag = new byte[16]; |  | ||||||
|         aes.Encrypt(nonce, contentWithMagic, ciphertext, tag); |  | ||||||
|  |  | ||||||
|         // Save as: [salt (16)][nonce (12)][tag (16)][ciphertext] |  | ||||||
|         using var fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write); |  | ||||||
|         fs.Write(salt); |  | ||||||
|         fs.Write(nonce); |  | ||||||
|         fs.Write(tag); |  | ||||||
|         fs.Write(ciphertext); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static void DecryptFile(string inputPath, string outputPath, string password) |  | ||||||
|     { |  | ||||||
|         var input = File.ReadAllBytes(inputPath); |  | ||||||
|  |  | ||||||
|         var salt = input[..16]; |  | ||||||
|         var nonce = input[16..28]; |  | ||||||
|         var tag = input[28..44]; |  | ||||||
|         var ciphertext = input[44..]; |  | ||||||
|  |  | ||||||
|         var key = DeriveKey(password, salt, 32); |  | ||||||
|         var decrypted = new byte[ciphertext.Length]; |  | ||||||
|  |  | ||||||
|         using var aes = new AesGcm(key, 16); // Specify 16-byte tag size explicitly |  | ||||||
|         aes.Decrypt(nonce, ciphertext, tag, decrypted); |  | ||||||
|  |  | ||||||
|         var magic = "DYSON1"u8.ToArray(); |  | ||||||
|         if (magic.Where((t, i) => decrypted[i] != t).Any()) |  | ||||||
|             throw new CryptographicException("Incorrect password or corrupted file."); |  | ||||||
|  |  | ||||||
|         var plaintext = decrypted[magic.Length..]; |  | ||||||
|         File.WriteAllBytes(outputPath, plaintext); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static byte[] DeriveKey(string password, byte[] salt, int keyBytes) |  | ||||||
|     { |  | ||||||
|         using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256); |  | ||||||
|         return pbkdf2.GetBytes(keyBytes); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,49 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/pools")] |  | ||||||
| public class FilePoolController(AppDatabase db, FileService fs) : ControllerBase |  | ||||||
| { |  | ||||||
|     [HttpGet] |  | ||||||
|     [Authorize] |  | ||||||
|     public async Task<ActionResult<List<FilePool>>> ListUsablePools() |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|         var pools = await db.Pools |  | ||||||
|             .Where(p => p.PolicyConfig.PublicUsable || p.AccountId == accountId) |  | ||||||
|             .Where(p => !p.IsHidden || p.AccountId == accountId) |  | ||||||
|             .OrderBy(p => p.CreatedAt) |  | ||||||
|             .ToListAsync(); |  | ||||||
|         pools = pools.Select(p => |  | ||||||
|         { |  | ||||||
|             p.StorageConfig.SecretId = string.Empty; |  | ||||||
|             p.StorageConfig.SecretKey = string.Empty; |  | ||||||
|             return p; |  | ||||||
|         }).ToList(); |  | ||||||
|  |  | ||||||
|         return Ok(pools); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [Authorize] |  | ||||||
|     [HttpDelete("{id:guid}/recycle")] |  | ||||||
|     public async Task<ActionResult> DeleteFilePoolRecycledFiles(Guid id) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|         var pool = await fs.GetPoolAsync(id); |  | ||||||
|         if (pool is null) return NotFound(); |  | ||||||
|         if (!currentUser.IsSuperuser && pool.AccountId != accountId) return Unauthorized(); |  | ||||||
|  |  | ||||||
|         var count = await fs.DeletePoolRecycledFilesAsync(id); |  | ||||||
|         return Ok(new { Count = count }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,174 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Grpc.Core; |  | ||||||
| using NodaTime; |  | ||||||
| using Duration = NodaTime.Duration; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| public class FileReferenceServiceGrpc(FileReferenceService fileReferenceService) |  | ||||||
|     : Shared.Proto.FileReferenceService.FileReferenceServiceBase |  | ||||||
| { |  | ||||||
|     public override async Task<Shared.Proto.CloudFileReference> CreateReference(CreateReferenceRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         Instant? expiredAt = null; |  | ||||||
|         if (request.ExpiredAt != null) |  | ||||||
|             expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); |  | ||||||
|         else if (request.Duration != null) |  | ||||||
|             expiredAt = SystemClock.Instance.GetCurrentInstant() + |  | ||||||
|                         Duration.FromTimeSpan(request.Duration.ToTimeSpan()); |  | ||||||
|  |  | ||||||
|         var reference = await fileReferenceService.CreateReferenceAsync( |  | ||||||
|             request.FileId, |  | ||||||
|             request.Usage, |  | ||||||
|             request.ResourceId, |  | ||||||
|             expiredAt |  | ||||||
|         ); |  | ||||||
|         return reference.ToProtoValue(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<CreateReferenceBatchResponse> CreateReferenceBatch(CreateReferenceBatchRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         Instant? expiredAt = null; |  | ||||||
|         if (request.ExpiredAt != null) |  | ||||||
|             expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); |  | ||||||
|         else if (request.Duration != null) |  | ||||||
|             expiredAt = SystemClock.Instance.GetCurrentInstant() + |  | ||||||
|                         Duration.FromTimeSpan(request.Duration.ToTimeSpan()); |  | ||||||
|  |  | ||||||
|         var references = await fileReferenceService.CreateReferencesAsync( |  | ||||||
|             request.FilesId.ToList(), |  | ||||||
|             request.Usage, |  | ||||||
|             request.ResourceId, |  | ||||||
|             expiredAt |  | ||||||
|         ); |  | ||||||
|         var response = new CreateReferenceBatchResponse(); |  | ||||||
|         response.References.AddRange(references.Select(r => r.ToProtoValue())); |  | ||||||
|         return response; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<GetReferencesResponse> GetReferences(GetReferencesRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var references = await fileReferenceService.GetReferencesAsync(request.FileId); |  | ||||||
|         var response = new GetReferencesResponse(); |  | ||||||
|         response.References.AddRange(references.Select(r => r.ToProtoValue())); |  | ||||||
|         return response; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<GetReferenceCountResponse> GetReferenceCount(GetReferenceCountRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var count = await fileReferenceService.GetReferenceCountAsync(request.FileId); |  | ||||||
|         return new GetReferenceCountResponse { Count = count }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<GetReferencesResponse> GetResourceReferences(GetResourceReferencesRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var references = await fileReferenceService.GetResourceReferencesAsync(request.ResourceId, request.Usage); |  | ||||||
|         var response = new GetReferencesResponse(); |  | ||||||
|         response.References.AddRange(references.Select(r => r.ToProtoValue())); |  | ||||||
|         return response; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<GetResourceFilesResponse> GetResourceFiles(GetResourceFilesRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var files = await fileReferenceService.GetResourceFilesAsync(request.ResourceId, request.Usage); |  | ||||||
|         var response = new GetResourceFilesResponse(); |  | ||||||
|         response.Files.AddRange(files.Select(f => f.ToProtoValue())); |  | ||||||
|         return response; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<DeleteResourceReferencesResponse> DeleteResourceReferences( |  | ||||||
|         DeleteResourceReferencesRequest request, ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         int deletedCount; |  | ||||||
|         if (request.Usage is null) |  | ||||||
|             deletedCount = await fileReferenceService.DeleteResourceReferencesAsync(request.ResourceId); |  | ||||||
|         else |  | ||||||
|             deletedCount = |  | ||||||
|                 await fileReferenceService.DeleteResourceReferencesAsync(request.ResourceId, request.Usage!); |  | ||||||
|         return new DeleteResourceReferencesResponse { DeletedCount = deletedCount }; |  | ||||||
|     } |  | ||||||
|          |  | ||||||
|     public override async Task<DeleteResourceReferencesResponse> DeleteResourceReferencesBatch(DeleteResourceReferencesBatchRequest request, ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var resourceIds = request.ResourceIds.ToList(); |  | ||||||
|         int deletedCount; |  | ||||||
|         if (request.Usage is null) |  | ||||||
|             deletedCount = await fileReferenceService.DeleteResourceReferencesBatchAsync(resourceIds); |  | ||||||
|         else |  | ||||||
|             deletedCount = |  | ||||||
|                 await fileReferenceService.DeleteResourceReferencesBatchAsync(resourceIds, request.Usage!); |  | ||||||
|         return new DeleteResourceReferencesResponse { DeletedCount = deletedCount }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<DeleteReferenceResponse> DeleteReference(DeleteReferenceRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var success = await fileReferenceService.DeleteReferenceAsync(Guid.Parse(request.ReferenceId)); |  | ||||||
|         return new DeleteReferenceResponse { Success = success }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<UpdateResourceFilesResponse> UpdateResourceFiles(UpdateResourceFilesRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         Instant? expiredAt = null; |  | ||||||
|         if (request.ExpiredAt != null) |  | ||||||
|         { |  | ||||||
|             expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); |  | ||||||
|         } |  | ||||||
|         else if (request.Duration != null) |  | ||||||
|         { |  | ||||||
|             expiredAt = SystemClock.Instance.GetCurrentInstant() + |  | ||||||
|                         Duration.FromTimeSpan(request.Duration.ToTimeSpan()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var references = await fileReferenceService.UpdateResourceFilesAsync( |  | ||||||
|             request.ResourceId, |  | ||||||
|             request.FileIds, |  | ||||||
|             request.Usage, |  | ||||||
|             expiredAt |  | ||||||
|         ); |  | ||||||
|         var response = new UpdateResourceFilesResponse(); |  | ||||||
|         response.References.AddRange(references.Select(r => r.ToProtoValue())); |  | ||||||
|         return response; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<SetReferenceExpirationResponse> SetReferenceExpiration( |  | ||||||
|         SetReferenceExpirationRequest request, ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         Instant? expiredAt = null; |  | ||||||
|         if (request.ExpiredAt != null) |  | ||||||
|         { |  | ||||||
|             expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); |  | ||||||
|         } |  | ||||||
|         else if (request.Duration != null) |  | ||||||
|         { |  | ||||||
|             expiredAt = SystemClock.Instance.GetCurrentInstant() + |  | ||||||
|                         Duration.FromTimeSpan(request.Duration.ToTimeSpan()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var success = |  | ||||||
|             await fileReferenceService.SetReferenceExpirationAsync(Guid.Parse(request.ReferenceId), expiredAt); |  | ||||||
|         return new SetReferenceExpirationResponse { Success = success }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<SetFileReferencesExpirationResponse> SetFileReferencesExpiration( |  | ||||||
|         SetFileReferencesExpirationRequest request, ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); |  | ||||||
|         var updatedCount = await fileReferenceService.SetFileReferencesExpirationAsync(request.FileId, expiredAt); |  | ||||||
|         return new SetFileReferencesExpirationResponse { UpdatedCount = updatedCount }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public override async Task<HasFileReferencesResponse> HasFileReferences(HasFileReferencesRequest request, |  | ||||||
|         ServerCallContext context) |  | ||||||
|     { |  | ||||||
|         var hasReferences = await fileReferenceService.HasFileReferencesAsync(request.FileId); |  | ||||||
|         return new HasFileReferencesResponse { HasReferences = hasReferences }; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,727 +0,0 @@ | |||||||
| using System.Globalization; |  | ||||||
| using FFMpegCore; |  | ||||||
| using System.Security.Cryptography; |  | ||||||
| using DysonNetwork.Drive.Storage.Model; |  | ||||||
| using DysonNetwork.Shared.Cache; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Google.Protobuf.WellKnownTypes; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Minio; |  | ||||||
| using Minio.DataModel.Args; |  | ||||||
| using NATS.Client.Core; |  | ||||||
| using NetVips; |  | ||||||
| using NodaTime; |  | ||||||
| using System.Linq.Expressions; |  | ||||||
| using Microsoft.EntityFrameworkCore.Query; |  | ||||||
| using NATS.Net; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| public class FileService( |  | ||||||
|     AppDatabase db, |  | ||||||
|     ILogger<FileService> logger, |  | ||||||
|     ICacheService cache, |  | ||||||
|     INatsConnection nats |  | ||||||
| ) |  | ||||||
| { |  | ||||||
|     private const string CacheKeyPrefix = "file:"; |  | ||||||
|     private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15); |  | ||||||
|  |  | ||||||
|     public async Task<SnCloudFile?> GetFileAsync(string fileId) |  | ||||||
|     { |  | ||||||
|         var cacheKey = $"{CacheKeyPrefix}{fileId}"; |  | ||||||
|  |  | ||||||
|         var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey); |  | ||||||
|         if (cachedFile is not null) |  | ||||||
|             return cachedFile; |  | ||||||
|  |  | ||||||
|         var file = await db.Files |  | ||||||
|             .Where(f => f.Id == fileId) |  | ||||||
|             .Include(f => f.Pool) |  | ||||||
|             .Include(f => f.Bundle) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|  |  | ||||||
|         if (file != null) |  | ||||||
|             await cache.SetAsync(cacheKey, file, CacheDuration); |  | ||||||
|  |  | ||||||
|         return file; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<List<SnCloudFile>> GetFilesAsync(List<string> fileIds) |  | ||||||
|     { |  | ||||||
|         var cachedFiles = new Dictionary<string, SnCloudFile>(); |  | ||||||
|         var uncachedIds = new List<string>(); |  | ||||||
|  |  | ||||||
|         foreach (var fileId in fileIds) |  | ||||||
|         { |  | ||||||
|             var cacheKey = $"{CacheKeyPrefix}{fileId}"; |  | ||||||
|             var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey); |  | ||||||
|  |  | ||||||
|             if (cachedFile != null) |  | ||||||
|                 cachedFiles[fileId] = cachedFile; |  | ||||||
|             else |  | ||||||
|                 uncachedIds.Add(fileId); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (uncachedIds.Count > 0) |  | ||||||
|         { |  | ||||||
|             var dbFiles = await db.Files |  | ||||||
|                 .Where(f => uncachedIds.Contains(f.Id)) |  | ||||||
|                 .Include(f => f.Pool) |  | ||||||
|                 .ToListAsync(); |  | ||||||
|  |  | ||||||
|             foreach (var file in dbFiles) |  | ||||||
|             { |  | ||||||
|                 var cacheKey = $"{CacheKeyPrefix}{file.Id}"; |  | ||||||
|                 await cache.SetAsync(cacheKey, file, CacheDuration); |  | ||||||
|                 cachedFiles[file.Id] = file; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return fileIds |  | ||||||
|             .Select(f => cachedFiles.GetValueOrDefault(f)) |  | ||||||
|             .Where(f => f != null) |  | ||||||
|             .Cast<SnCloudFile>() |  | ||||||
|             .ToList(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnCloudFile> ProcessNewFileAsync( |  | ||||||
|         Account account, |  | ||||||
|         string fileId, |  | ||||||
|         string filePool, |  | ||||||
|         string? fileBundleId, |  | ||||||
|         string filePath, |  | ||||||
|         string fileName, |  | ||||||
|         string? contentType, |  | ||||||
|         string? encryptPassword, |  | ||||||
|         Instant? expiredAt |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         var accountId = Guid.Parse(account.Id); |  | ||||||
|  |  | ||||||
|         var pool = await GetPoolAsync(Guid.Parse(filePool)); |  | ||||||
|         if (pool is null) throw new InvalidOperationException("Pool not found"); |  | ||||||
|  |  | ||||||
|         if (pool.StorageConfig.Expiration is not null && expiredAt.HasValue) |  | ||||||
|         { |  | ||||||
|             var expectedExpiration = SystemClock.Instance.GetCurrentInstant() - expiredAt.Value; |  | ||||||
|             var effectiveExpiration = pool.StorageConfig.Expiration < expectedExpiration |  | ||||||
|                 ? pool.StorageConfig.Expiration |  | ||||||
|                 : expectedExpiration; |  | ||||||
|             expiredAt = SystemClock.Instance.GetCurrentInstant() + effectiveExpiration; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var bundle = fileBundleId is not null |  | ||||||
|             ? await GetBundleAsync(Guid.Parse(fileBundleId), accountId) |  | ||||||
|             : null; |  | ||||||
|         if (fileBundleId is not null && bundle is null) |  | ||||||
|         { |  | ||||||
|             throw new InvalidOperationException("Bundle not found"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (bundle?.ExpiredAt != null) |  | ||||||
|             expiredAt = bundle.ExpiredAt.Value; |  | ||||||
|  |  | ||||||
|         var managedTempPath = Path.Combine(Path.GetTempPath(), fileId); |  | ||||||
|         File.Copy(filePath, managedTempPath, true); |  | ||||||
|  |  | ||||||
|         var fileInfo = new FileInfo(managedTempPath); |  | ||||||
|         var fileSize = fileInfo.Length; |  | ||||||
|         var finalContentType = contentType ?? |  | ||||||
|                                (!fileName.Contains('.') ? "application/octet-stream" : MimeTypes.GetMimeType(fileName)); |  | ||||||
|  |  | ||||||
|         var file = new SnCloudFile |  | ||||||
|         { |  | ||||||
|             Id = fileId, |  | ||||||
|             Name = fileName, |  | ||||||
|             MimeType = finalContentType, |  | ||||||
|             Size = fileSize, |  | ||||||
|             ExpiredAt = expiredAt, |  | ||||||
|             BundleId = bundle?.Id, |  | ||||||
|             AccountId = Guid.Parse(account.Id), |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (!pool.PolicyConfig.NoMetadata) |  | ||||||
|         { |  | ||||||
|             await ExtractMetadataAsync(file, managedTempPath); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         string processingPath = managedTempPath; |  | ||||||
|         bool isTempFile = true; |  | ||||||
|  |  | ||||||
|         if (!string.IsNullOrWhiteSpace(encryptPassword)) |  | ||||||
|         { |  | ||||||
|             if (!pool.PolicyConfig.AllowEncryption) |  | ||||||
|                 throw new InvalidOperationException("Encryption is not allowed in this pool"); |  | ||||||
|  |  | ||||||
|             var encryptedPath = Path.Combine(Path.GetTempPath(), $"{fileId}.encrypted"); |  | ||||||
|             FileEncryptor.EncryptFile(managedTempPath, encryptedPath, encryptPassword); |  | ||||||
|  |  | ||||||
|             File.Delete(managedTempPath); |  | ||||||
|  |  | ||||||
|             processingPath = encryptedPath; |  | ||||||
|  |  | ||||||
|             file.IsEncrypted = true; |  | ||||||
|             file.MimeType = "application/octet-stream"; |  | ||||||
|             file.Size = new FileInfo(processingPath).Length; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         file.Hash = await HashFileAsync(processingPath); |  | ||||||
|  |  | ||||||
|         db.Files.Add(file); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         file.StorageId ??= file.Id; |  | ||||||
|  |  | ||||||
|         var js = nats.CreateJetStreamContext(); |  | ||||||
|         await js.PublishAsync( |  | ||||||
|             FileUploadedEvent.Type, |  | ||||||
|             GrpcTypeHelper.ConvertObjectToByteString(new FileUploadedEventPayload( |  | ||||||
|                 file.Id, |  | ||||||
|                 pool.Id, |  | ||||||
|                 file.StorageId, |  | ||||||
|                 file.MimeType, |  | ||||||
|                 processingPath, |  | ||||||
|                 isTempFile) |  | ||||||
|             ).ToByteArray() |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         return file; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task ExtractMetadataAsync(SnCloudFile file, string filePath) |  | ||||||
|     { |  | ||||||
|         switch (file.MimeType?.Split('/')[0]) |  | ||||||
|         { |  | ||||||
|             case "image": |  | ||||||
|                 try |  | ||||||
|                 { |  | ||||||
|                     var blurhash = BlurHashSharp.SkiaSharp.BlurHashEncoder.Encode(3, 3, filePath); |  | ||||||
|                     await using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); |  | ||||||
|                     stream.Position = 0; |  | ||||||
|  |  | ||||||
|                     using var vipsImage = Image.NewFromStream(stream); |  | ||||||
|                     var width = vipsImage.Width; |  | ||||||
|                     var height = vipsImage.Height; |  | ||||||
|                     var orientation = 1; |  | ||||||
|                     try |  | ||||||
|                     { |  | ||||||
|                         orientation = vipsImage.Get("orientation") as int? ?? 1; |  | ||||||
|                     } |  | ||||||
|                     catch |  | ||||||
|                     { |  | ||||||
|                         // ignored |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     var meta = new Dictionary<string, object?> |  | ||||||
|                     { |  | ||||||
|                         ["blur"] = blurhash, |  | ||||||
|                         ["format"] = vipsImage.Get("vips-loader") ?? "unknown", |  | ||||||
|                         ["width"] = width, |  | ||||||
|                         ["height"] = height, |  | ||||||
|                         ["orientation"] = orientation, |  | ||||||
|                     }; |  | ||||||
|                     var exif = new Dictionary<string, object>(); |  | ||||||
|  |  | ||||||
|                     foreach (var field in vipsImage.GetFields()) |  | ||||||
|                     { |  | ||||||
|                         if (IsIgnoredField(field)) continue; |  | ||||||
|                         var value = vipsImage.Get(field); |  | ||||||
|                         if (field.StartsWith("exif-")) |  | ||||||
|                             exif[field.Replace("exif-", "")] = value; |  | ||||||
|                         else |  | ||||||
|                             meta[field] = value; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     if (orientation is 6 or 8) (width, height) = (height, width); |  | ||||||
|                     meta["exif"] = exif; |  | ||||||
|                     meta["ratio"] = height != 0 ? (double)width / height : 0; |  | ||||||
|                     file.FileMeta = meta; |  | ||||||
|                 } |  | ||||||
|                 catch (Exception ex) |  | ||||||
|                 { |  | ||||||
|                     file.FileMeta = new Dictionary<string, object?>(); |  | ||||||
|                     logger.LogError(ex, "Failed to analyze image file {FileId}", file.Id); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 break; |  | ||||||
|  |  | ||||||
|             case "video": |  | ||||||
|             case "audio": |  | ||||||
|                 try |  | ||||||
|                 { |  | ||||||
|                     var mediaInfo = await FFProbe.AnalyseAsync(filePath); |  | ||||||
|                     file.FileMeta = new Dictionary<string, object?> |  | ||||||
|                     { |  | ||||||
|                         ["width"] = mediaInfo.PrimaryVideoStream?.Width, |  | ||||||
|                         ["height"] = mediaInfo.PrimaryVideoStream?.Height, |  | ||||||
|                         ["duration"] = mediaInfo.Duration.TotalSeconds, |  | ||||||
|                         ["format_name"] = mediaInfo.Format.FormatName, |  | ||||||
|                         ["format_long_name"] = mediaInfo.Format.FormatLongName, |  | ||||||
|                         ["start_time"] = mediaInfo.Format.StartTime.ToString(), |  | ||||||
|                         ["bit_rate"] = mediaInfo.Format.BitRate.ToString(CultureInfo.InvariantCulture), |  | ||||||
|                         ["tags"] = mediaInfo.Format.Tags ?? new Dictionary<string, string>(), |  | ||||||
|                         ["chapters"] = mediaInfo.Chapters, |  | ||||||
|                         ["video_streams"] = mediaInfo.VideoStreams.Select(s => new |  | ||||||
|                         { |  | ||||||
|                             s.AvgFrameRate, |  | ||||||
|                             s.BitRate, |  | ||||||
|                             s.CodecName, |  | ||||||
|                             s.Duration, |  | ||||||
|                             s.Height, |  | ||||||
|                             s.Width, |  | ||||||
|                             s.Language, |  | ||||||
|                             s.PixelFormat, |  | ||||||
|                             s.Rotation |  | ||||||
|                         }).Where(s => double.IsNormal(s.AvgFrameRate)).ToList(), |  | ||||||
|                         ["audio_streams"] = mediaInfo.AudioStreams.Select(s => new |  | ||||||
|                             { |  | ||||||
|                                 s.BitRate, |  | ||||||
|                                 s.Channels, |  | ||||||
|                                 s.ChannelLayout, |  | ||||||
|                                 s.CodecName, |  | ||||||
|                                 s.Duration, |  | ||||||
|                                 s.Language, |  | ||||||
|                                 s.SampleRateHz |  | ||||||
|                             }) |  | ||||||
|                             .ToList(), |  | ||||||
|                     }; |  | ||||||
|                     if (mediaInfo.PrimaryVideoStream is not null) |  | ||||||
|                         file.FileMeta["ratio"] = (double)mediaInfo.PrimaryVideoStream.Width / |  | ||||||
|                                                  mediaInfo.PrimaryVideoStream.Height; |  | ||||||
|                 } |  | ||||||
|                 catch (Exception ex) |  | ||||||
|                 { |  | ||||||
|                     logger.LogError(ex, "Failed to analyze media file {FileId}", file.Id); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static async Task<string> HashFileAsync(string filePath, int chunkSize = 1024 * 1024) |  | ||||||
|     { |  | ||||||
|         var fileInfo = new FileInfo(filePath); |  | ||||||
|         if (fileInfo.Length > chunkSize * 1024 * 5) |  | ||||||
|             return await HashFastApproximateAsync(filePath, chunkSize); |  | ||||||
|  |  | ||||||
|         await using var stream = File.OpenRead(filePath); |  | ||||||
|         using var md5 = MD5.Create(); |  | ||||||
|         var hashBytes = await md5.ComputeHashAsync(stream); |  | ||||||
|         return Convert.ToHexString(hashBytes).ToLowerInvariant(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static async Task<string> HashFastApproximateAsync(string filePath, int chunkSize = 1024 * 1024) |  | ||||||
|     { |  | ||||||
|         await using var stream = File.OpenRead(filePath); |  | ||||||
|  |  | ||||||
|         var buffer = new byte[chunkSize * 2]; |  | ||||||
|         var fileLength = stream.Length; |  | ||||||
|  |  | ||||||
|         var bytesRead = await stream.ReadAsync(buffer.AsMemory(0, chunkSize)); |  | ||||||
|  |  | ||||||
|         if (fileLength > chunkSize) |  | ||||||
|         { |  | ||||||
|             stream.Seek(-chunkSize, SeekOrigin.End); |  | ||||||
|             bytesRead += await stream.ReadAsync(buffer.AsMemory(chunkSize, chunkSize)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var hash = MD5.HashData(buffer.AsSpan(0, bytesRead)); |  | ||||||
|         stream.Position = 0; |  | ||||||
|         return Convert.ToHexString(hash).ToLowerInvariant(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task UploadFileToRemoteAsync( |  | ||||||
|         string storageId, |  | ||||||
|         Guid targetRemote, |  | ||||||
|         string filePath, |  | ||||||
|         string? suffix = null, |  | ||||||
|         string? contentType = null, |  | ||||||
|         bool selfDestruct = false |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         await using var fileStream = File.OpenRead(filePath); |  | ||||||
|         await UploadFileToRemoteAsync(storageId, targetRemote, fileStream, suffix, contentType); |  | ||||||
|         if (selfDestruct) File.Delete(filePath); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task UploadFileToRemoteAsync( |  | ||||||
|         string storageId, |  | ||||||
|         Guid targetRemote, |  | ||||||
|         Stream stream, |  | ||||||
|         string? suffix = null, |  | ||||||
|         string? contentType = null |  | ||||||
|     ) |  | ||||||
|     { |  | ||||||
|         var dest = await GetRemoteStorageConfig(targetRemote); |  | ||||||
|         if (dest is null) |  | ||||||
|             throw new InvalidOperationException( |  | ||||||
|                 $"Failed to configure client for remote destination '{targetRemote}'" |  | ||||||
|             ); |  | ||||||
|         var client = CreateMinioClient(dest); |  | ||||||
|  |  | ||||||
|         var bucket = dest.Bucket; |  | ||||||
|         contentType ??= "application/octet-stream"; |  | ||||||
|  |  | ||||||
|         await client!.PutObjectAsync(new PutObjectArgs() |  | ||||||
|             .WithBucket(bucket) |  | ||||||
|             .WithObject(string.IsNullOrWhiteSpace(suffix) ? storageId : storageId + suffix) |  | ||||||
|             .WithStreamData(stream) |  | ||||||
|             .WithObjectSize(stream.Length) |  | ||||||
|             .WithContentType(contentType) |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<SnCloudFile> UpdateFileAsync(SnCloudFile file, FieldMask updateMask) |  | ||||||
|     { |  | ||||||
|         var existingFile = await db.Files.FirstOrDefaultAsync(f => f.Id == file.Id); |  | ||||||
|         if (existingFile == null) |  | ||||||
|         { |  | ||||||
|             throw new InvalidOperationException($"File with ID {file.Id} not found."); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var updatable = new UpdatableCloudFile(existingFile); |  | ||||||
|  |  | ||||||
|         foreach (var path in updateMask.Paths) |  | ||||||
|         { |  | ||||||
|             switch (path) |  | ||||||
|             { |  | ||||||
|                 case "name": |  | ||||||
|                     updatable.Name = file.Name; |  | ||||||
|                     break; |  | ||||||
|                 case "description": |  | ||||||
|                     updatable.Description = file.Description; |  | ||||||
|                     break; |  | ||||||
|                 case "file_meta": |  | ||||||
|                     updatable.FileMeta = file.FileMeta; |  | ||||||
|                     break; |  | ||||||
|                 case "user_meta": |  | ||||||
|                     updatable.UserMeta = file.UserMeta; |  | ||||||
|                     break; |  | ||||||
|                 case "is_marked_recycle": |  | ||||||
|                     updatable.IsMarkedRecycle = file.IsMarkedRecycle; |  | ||||||
|                     break; |  | ||||||
|                 default: |  | ||||||
|                     logger.LogWarning("Attempted to update unmodifiable field: {Field}", path); |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.Files.Where(f => f.Id == file.Id).ExecuteUpdateAsync(updatable.ToSetPropertyCalls()); |  | ||||||
|  |  | ||||||
|         await _PurgeCacheAsync(file.Id); |  | ||||||
|         return await db.Files.AsNoTracking().FirstAsync(f => f.Id == file.Id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task DeleteFileAsync(SnCloudFile file) |  | ||||||
|     { |  | ||||||
|         db.Remove(file); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         await _PurgeCacheAsync(file.Id); |  | ||||||
|  |  | ||||||
|         await DeleteFileDataAsync(file); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task DeleteFileDataAsync(SnCloudFile file, bool force = false) |  | ||||||
|     { |  | ||||||
|         if (!file.PoolId.HasValue) return; |  | ||||||
|  |  | ||||||
|         if (!force) |  | ||||||
|         { |  | ||||||
|             var sameOriginFiles = await db.Files |  | ||||||
|                 .Where(f => f.StorageId == file.StorageId && f.Id != file.Id) |  | ||||||
|                 .Select(f => f.Id) |  | ||||||
|                 .ToListAsync(); |  | ||||||
|  |  | ||||||
|             if (sameOriginFiles.Count != 0) |  | ||||||
|                 return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var dest = await GetRemoteStorageConfig(file.PoolId.Value); |  | ||||||
|         if (dest is null) throw new InvalidOperationException($"No remote storage configured for pool {file.PoolId}"); |  | ||||||
|         var client = CreateMinioClient(dest); |  | ||||||
|         if (client is null) |  | ||||||
|             throw new InvalidOperationException( |  | ||||||
|                 $"Failed to configure client for remote destination '{file.PoolId}'" |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|         var bucket = dest.Bucket; |  | ||||||
|         var objectId = file.StorageId ?? file.Id; |  | ||||||
|  |  | ||||||
|         await client.RemoveObjectAsync( |  | ||||||
|             new RemoveObjectArgs().WithBucket(bucket).WithObject(objectId) |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         if (file.HasCompression) |  | ||||||
|         { |  | ||||||
|             try |  | ||||||
|             { |  | ||||||
|                 await client.RemoveObjectAsync( |  | ||||||
|                     new RemoveObjectArgs().WithBucket(bucket).WithObject(objectId + ".compressed") |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|             catch |  | ||||||
|             { |  | ||||||
|                 logger.LogWarning("Failed to delete compressed version of file {fileId}", file.Id); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (file.HasThumbnail) |  | ||||||
|         { |  | ||||||
|             try |  | ||||||
|             { |  | ||||||
|                 await client.RemoveObjectAsync( |  | ||||||
|                     new RemoveObjectArgs().WithBucket(bucket).WithObject(objectId + ".thumbnail") |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|             catch |  | ||||||
|             { |  | ||||||
|                 logger.LogWarning("Failed to delete thumbnail of file {fileId}", file.Id); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task DeleteFileDataBatchAsync(List<SnCloudFile> files) |  | ||||||
|     { |  | ||||||
|         files = files.Where(f => f.PoolId.HasValue).ToList(); |  | ||||||
|  |  | ||||||
|         foreach (var fileGroup in files.GroupBy(f => f.PoolId!.Value)) |  | ||||||
|         { |  | ||||||
|             var dest = await GetRemoteStorageConfig(fileGroup.Key); |  | ||||||
|             if (dest is null) |  | ||||||
|                 throw new InvalidOperationException($"No remote storage configured for pool {fileGroup.Key}"); |  | ||||||
|             var client = CreateMinioClient(dest); |  | ||||||
|             if (client is null) |  | ||||||
|                 throw new InvalidOperationException( |  | ||||||
|                     $"Failed to configure client for remote destination '{fileGroup.Key}'" |  | ||||||
|                 ); |  | ||||||
|  |  | ||||||
|             List<string> objectsToDelete = []; |  | ||||||
|  |  | ||||||
|             foreach (var file in fileGroup) |  | ||||||
|             { |  | ||||||
|                 objectsToDelete.Add(file.StorageId ?? file.Id); |  | ||||||
|                 if (file.HasCompression) objectsToDelete.Add(file.StorageId ?? file.Id + ".compressed"); |  | ||||||
|                 if (file.HasThumbnail) objectsToDelete.Add(file.StorageId ?? file.Id + ".thumbnail"); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             await client.RemoveObjectsAsync( |  | ||||||
|                 new RemoveObjectsArgs().WithBucket(dest.Bucket).WithObjects(objectsToDelete) |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task<SnFileBundle?> GetBundleAsync(Guid id, Guid accountId) |  | ||||||
|     { |  | ||||||
|         var bundle = await db.Bundles |  | ||||||
|             .Where(e => e.Id == id) |  | ||||||
|             .Where(e => e.AccountId == accountId) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|         return bundle; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<FilePool?> GetPoolAsync(Guid destination) |  | ||||||
|     { |  | ||||||
|         var cacheKey = $"file:pool:{destination}"; |  | ||||||
|         var cachedResult = await cache.GetAsync<FilePool?>(cacheKey); |  | ||||||
|         if (cachedResult != null) return cachedResult; |  | ||||||
|  |  | ||||||
|         var pool = await db.Pools.FirstOrDefaultAsync(p => p.Id == destination); |  | ||||||
|         if (pool != null) |  | ||||||
|             await cache.SetAsync(cacheKey, pool); |  | ||||||
|  |  | ||||||
|         return pool; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<RemoteStorageConfig?> GetRemoteStorageConfig(Guid destination) |  | ||||||
|     { |  | ||||||
|         var pool = await GetPoolAsync(destination); |  | ||||||
|         return pool?.StorageConfig; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<RemoteStorageConfig?> GetRemoteStorageConfig(string destination) |  | ||||||
|     { |  | ||||||
|         var id = Guid.Parse(destination); |  | ||||||
|         return await GetRemoteStorageConfig(id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public IMinioClient? CreateMinioClient(RemoteStorageConfig dest) |  | ||||||
|     { |  | ||||||
|         var client = new MinioClient() |  | ||||||
|             .WithEndpoint(dest.Endpoint) |  | ||||||
|             .WithRegion(dest.Region) |  | ||||||
|             .WithCredentials(dest.SecretId, dest.SecretKey); |  | ||||||
|         if (dest.EnableSsl) client = client.WithSSL(); |  | ||||||
|  |  | ||||||
|         return client.Build(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     internal async Task _PurgeCacheAsync(string fileId) |  | ||||||
|     { |  | ||||||
|         var cacheKey = $"{CacheKeyPrefix}{fileId}"; |  | ||||||
|         await cache.RemoveAsync(cacheKey); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     internal async Task _PurgeCacheRangeAsync(IEnumerable<string> fileIds) |  | ||||||
|     { |  | ||||||
|         var tasks = fileIds.Select(_PurgeCacheAsync); |  | ||||||
|         await Task.WhenAll(tasks); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<List<SnCloudFile?>> LoadFromReference(List<SnCloudFileReferenceObject> references) |  | ||||||
|     { |  | ||||||
|         var cachedFiles = new Dictionary<string, SnCloudFile>(); |  | ||||||
|         var uncachedIds = new List<string>(); |  | ||||||
|  |  | ||||||
|         foreach (var reference in references) |  | ||||||
|         { |  | ||||||
|             var cacheKey = $"{CacheKeyPrefix}{reference.Id}"; |  | ||||||
|             var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey); |  | ||||||
|  |  | ||||||
|             if (cachedFile != null) |  | ||||||
|             { |  | ||||||
|                 cachedFiles[reference.Id] = cachedFile; |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 uncachedIds.Add(reference.Id); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (uncachedIds.Count > 0) |  | ||||||
|         { |  | ||||||
|             var dbFiles = await db.Files |  | ||||||
|                 .Where(f => uncachedIds.Contains(f.Id)) |  | ||||||
|                 .ToListAsync(); |  | ||||||
|  |  | ||||||
|             foreach (var file in dbFiles) |  | ||||||
|             { |  | ||||||
|                 var cacheKey = $"{CacheKeyPrefix}{file.Id}"; |  | ||||||
|                 await cache.SetAsync(cacheKey, file, CacheDuration); |  | ||||||
|                 cachedFiles[file.Id] = file; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return [.. references |  | ||||||
|             .Select(r => cachedFiles.GetValueOrDefault(r.Id)) |  | ||||||
|             .Where(f => f != null)]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<int> GetReferenceCountAsync(string fileId) |  | ||||||
|     { |  | ||||||
|         return await db.FileReferences |  | ||||||
|             .Where(r => r.FileId == fileId) |  | ||||||
|             .CountAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<bool> IsReferencedAsync(string fileId) |  | ||||||
|     { |  | ||||||
|         return await db.FileReferences |  | ||||||
|             .Where(r => r.FileId == fileId) |  | ||||||
|             .AnyAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static bool IsIgnoredField(string fieldName) |  | ||||||
|     { |  | ||||||
|         var gpsFields = new[] |  | ||||||
|         { |  | ||||||
|             "gps-latitude", "gps-longitude", "gps-altitude", "gps-latitude-ref", "gps-longitude-ref", |  | ||||||
|             "gps-altitude-ref", "gps-timestamp", "gps-datestamp", "gps-speed", "gps-speed-ref", "gps-track", |  | ||||||
|             "gps-track-ref", "gps-img-direction", "gps-img-direction-ref", "gps-dest-latitude", |  | ||||||
|             "gps-dest-longitude", "gps-dest-latitude-ref", "gps-dest-longitude-ref", "gps-processing-method", |  | ||||||
|             "gps-area-information" |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (fieldName.StartsWith("exif-GPS")) return true; |  | ||||||
|         if (fieldName.StartsWith("ifd3-GPS")) return true; |  | ||||||
|         if (fieldName.EndsWith("-data")) return true; |  | ||||||
|         return gpsFields.Any(gpsField => fieldName.StartsWith(gpsField, StringComparison.OrdinalIgnoreCase)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<int> DeleteAccountRecycledFilesAsync(Guid accountId) |  | ||||||
|     { |  | ||||||
|         var files = await db.Files |  | ||||||
|             .Where(f => f.AccountId == accountId && f.IsMarkedRecycle) |  | ||||||
|             .ToListAsync(); |  | ||||||
|         var count = files.Count; |  | ||||||
|         var tasks = files.Select(f => DeleteFileDataAsync(f, true)); |  | ||||||
|         await Task.WhenAll(tasks); |  | ||||||
|         var fileIds = files.Select(f => f.Id).ToList(); |  | ||||||
|         await _PurgeCacheRangeAsync(fileIds); |  | ||||||
|         db.RemoveRange(files); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         return count; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<int> DeletePoolRecycledFilesAsync(Guid poolId) |  | ||||||
|     { |  | ||||||
|         var files = await db.Files |  | ||||||
|             .Where(f => f.PoolId == poolId && f.IsMarkedRecycle) |  | ||||||
|             .ToListAsync(); |  | ||||||
|         var count = files.Count; |  | ||||||
|         var tasks = files.Select(f => DeleteFileDataAsync(f, true)); |  | ||||||
|         await Task.WhenAll(tasks); |  | ||||||
|         var fileIds = files.Select(f => f.Id).ToList(); |  | ||||||
|         await _PurgeCacheRangeAsync(fileIds); |  | ||||||
|         db.RemoveRange(files); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         return count; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<int> DeleteAllRecycledFilesAsync() |  | ||||||
|     { |  | ||||||
|         var files = await db.Files |  | ||||||
|             .Where(f => f.IsMarkedRecycle) |  | ||||||
|             .ToListAsync(); |  | ||||||
|         var count = files.Count; |  | ||||||
|         var tasks = files.Select(f => DeleteFileDataAsync(f, true)); |  | ||||||
|         await Task.WhenAll(tasks); |  | ||||||
|         var fileIds = files.Select(f => f.Id).ToList(); |  | ||||||
|         await _PurgeCacheRangeAsync(fileIds); |  | ||||||
|         db.RemoveRange(files); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         return count; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<string> CreateFastUploadLinkAsync(SnCloudFile file) |  | ||||||
|     { |  | ||||||
|         if (file.PoolId is null) throw new InvalidOperationException("Pool ID is null"); |  | ||||||
|  |  | ||||||
|         var dest = await GetRemoteStorageConfig(file.PoolId.Value); |  | ||||||
|         if (dest is null) throw new InvalidOperationException($"No remote storage configured for pool {file.PoolId}"); |  | ||||||
|         var client = CreateMinioClient(dest); |  | ||||||
|         if (client is null) |  | ||||||
|             throw new InvalidOperationException( |  | ||||||
|                 $"Failed to configure client for remote destination '{file.PoolId}'" |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|         var url = await client.PresignedPutObjectAsync( |  | ||||||
|             new PresignedPutObjectArgs() |  | ||||||
|                 .WithBucket(dest.Bucket) |  | ||||||
|                 .WithObject(file.Id) |  | ||||||
|                 .WithExpiry(60 * 60 * 24) |  | ||||||
|         ); |  | ||||||
|         return url; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| file class UpdatableCloudFile(SnCloudFile file) |  | ||||||
| { |  | ||||||
|     public string Name { get; set; } = file.Name; |  | ||||||
|     public string? Description { get; set; } = file.Description; |  | ||||||
|     public Dictionary<string, object?>? FileMeta { get; set; } = file.FileMeta; |  | ||||||
|     public Dictionary<string, object?>? UserMeta { get; set; } = file.UserMeta; |  | ||||||
|     public bool IsMarkedRecycle { get; set; } = file.IsMarkedRecycle; |  | ||||||
|  |  | ||||||
|     public Expression<Func<SetPropertyCalls<SnCloudFile>, SetPropertyCalls<SnCloudFile>>> ToSetPropertyCalls() |  | ||||||
|     { |  | ||||||
|         var userMeta = UserMeta ?? []; |  | ||||||
|         return setter => setter |  | ||||||
|             .SetProperty(f => f.Name, Name) |  | ||||||
|             .SetProperty(f => f.Description, Description) |  | ||||||
|             .SetProperty(f => f.FileMeta, FileMeta) |  | ||||||
|             .SetProperty(f => f.UserMeta, userMeta) |  | ||||||
|             .SetProperty(f => f.IsMarkedRecycle, IsMarkedRecycle); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,71 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Google.Protobuf.WellKnownTypes; |  | ||||||
| using Grpc.Core; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage |  | ||||||
| { |  | ||||||
|     public class FileServiceGrpc(FileService fileService) : Shared.Proto.FileService.FileServiceBase |  | ||||||
|     { |  | ||||||
|         public override async Task<Shared.Proto.CloudFile> GetFile(GetFileRequest request, ServerCallContext context) |  | ||||||
|         { |  | ||||||
|             var file = await fileService.GetFileAsync(request.Id); |  | ||||||
|             return file?.ToProtoValue() ?? throw new RpcException(new Status(StatusCode.NotFound, "File not found")); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public override async Task<GetFileBatchResponse> GetFileBatch(GetFileBatchRequest request, ServerCallContext context) |  | ||||||
|         { |  | ||||||
|             var files = await fileService.GetFilesAsync(request.Ids.ToList()); |  | ||||||
|             return new GetFileBatchResponse { Files = { files.Select(f => f.ToProtoValue()) } }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public override async Task<Shared.Proto.CloudFile> UpdateFile(UpdateFileRequest request, |  | ||||||
|             ServerCallContext context) |  | ||||||
|         { |  | ||||||
|             var file = await fileService.GetFileAsync(request.File.Id); |  | ||||||
|             if (file == null) |  | ||||||
|                 throw new RpcException(new Status(StatusCode.NotFound, "File not found")); |  | ||||||
|             var updatedFile = await fileService.UpdateFileAsync(file, request.UpdateMask); |  | ||||||
|             return updatedFile.ToProtoValue(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public override async Task<Empty> DeleteFile(DeleteFileRequest request, ServerCallContext context) |  | ||||||
|         { |  | ||||||
|             var file = await fileService.GetFileAsync(request.Id); |  | ||||||
|             if (file == null) |  | ||||||
|             { |  | ||||||
|                 throw new RpcException(new Status(StatusCode.NotFound, "File not found")); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             await fileService.DeleteFileAsync(file); |  | ||||||
|             return new Empty(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public override async Task<LoadFromReferenceResponse> LoadFromReference( |  | ||||||
|             LoadFromReferenceRequest request, |  | ||||||
|             ServerCallContext context |  | ||||||
|         ) |  | ||||||
|         { |  | ||||||
|             // Assuming CloudFileReferenceObject is a simple class/struct that holds an ID |  | ||||||
|             // You might need to define this or adjust the LoadFromReference method in FileService |  | ||||||
|             var references = request.ReferenceIds.Select(id => new SnCloudFileReferenceObject { Id = id }).ToList(); |  | ||||||
|             var files = await fileService.LoadFromReference(references); |  | ||||||
|             var response = new LoadFromReferenceResponse(); |  | ||||||
|             response.Files.AddRange(files.Where(f => f != null).Select(f => f!.ToProtoValue())); |  | ||||||
|             return response; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public override async Task<IsReferencedResponse> IsReferenced(IsReferencedRequest request, |  | ||||||
|             ServerCallContext context) |  | ||||||
|         { |  | ||||||
|             var isReferenced = await fileService.IsReferencedAsync(request.FileId); |  | ||||||
|             return new IsReferencedResponse { IsReferenced = isReferenced }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public override async Task<Empty> PurgeCache(PurgeCacheRequest request, ServerCallContext context) |  | ||||||
|         { |  | ||||||
|             await fileService._PurgeCacheAsync(request.FileId); |  | ||||||
|             return new Empty(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,278 +0,0 @@ | |||||||
| using System.ComponentModel.DataAnnotations; |  | ||||||
| using System.Text.Json; |  | ||||||
| using DysonNetwork.Drive.Billing; |  | ||||||
| using DysonNetwork.Drive.Storage.Model; |  | ||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using DysonNetwork.Shared.Http; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NanoidDotNet; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/files/upload")] |  | ||||||
| [Authorize] |  | ||||||
| public class FileUploadController( |  | ||||||
|     IConfiguration configuration, |  | ||||||
|     FileService fileService, |  | ||||||
|     AppDatabase db, |  | ||||||
|     PermissionService.PermissionServiceClient permission, |  | ||||||
|     QuotaService quotaService |  | ||||||
| ) |  | ||||||
|     : ControllerBase |  | ||||||
| { |  | ||||||
|     private readonly string _tempPath = |  | ||||||
|         configuration.GetValue<string>("Storage:Uploads") ?? Path.Combine(Path.GetTempPath(), "multipart-uploads"); |  | ||||||
|  |  | ||||||
|     private const long DefaultChunkSize = 1024 * 1024 * 5; // 5MB |  | ||||||
|  |  | ||||||
|     [HttpPost("create")] |  | ||||||
|     public async Task<IActionResult> CreateUploadTask([FromBody] CreateUploadTaskRequest request) |  | ||||||
|     { |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!currentUser.IsSuperuser) |  | ||||||
|         { |  | ||||||
|             var allowed = await permission.HasPermissionAsync(new HasPermissionRequest |  | ||||||
|             { Actor = $"user:{currentUser.Id}", Area = "global", Key = "files.create" }); |  | ||||||
|             if (!allowed.HasPermission) |  | ||||||
|             { |  | ||||||
|                 return new ObjectResult(ApiError.Unauthorized(forbidden: true)) { StatusCode = 403 }; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         request.PoolId ??= Guid.Parse(configuration["Storage:PreferredRemote"]!); |  | ||||||
|  |  | ||||||
|         var pool = await fileService.GetPoolAsync(request.PoolId.Value); |  | ||||||
|         if (pool is null) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(ApiError.NotFound("Pool")) { StatusCode = 404 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (pool.PolicyConfig.RequirePrivilege is > 0) |  | ||||||
|         { |  | ||||||
|             var privilege = |  | ||||||
|                 currentUser.PerkSubscription is null ? 0 : |  | ||||||
|                 PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier); |  | ||||||
|             if (privilege < pool.PolicyConfig.RequirePrivilege) |  | ||||||
|             { |  | ||||||
|                 return new ObjectResult(ApiError.Unauthorized( |  | ||||||
|                     $"You need Stellar Program tier {pool.PolicyConfig.RequirePrivilege} to use pool {pool.Name}, you are tier {privilege}", |  | ||||||
|                     forbidden: true)) |  | ||||||
|                 { |  | ||||||
|                     StatusCode = 403 |  | ||||||
|                 }; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var policy = pool.PolicyConfig; |  | ||||||
|         if (!policy.AllowEncryption && !string.IsNullOrEmpty(request.EncryptPassword)) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(ApiError.Unauthorized("File encryption is not allowed in this pool", true)) |  | ||||||
|             { StatusCode = 403 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (policy.AcceptTypes is { Count: > 0 }) |  | ||||||
|         { |  | ||||||
|             if (string.IsNullOrEmpty(request.ContentType)) |  | ||||||
|             { |  | ||||||
|                 return new ObjectResult(ApiError.Validation(new Dictionary<string, string[]> |  | ||||||
|                 { |  | ||||||
|                     { "contentType", new[] { "Content type is required by the pool's policy" } } |  | ||||||
|                 })) |  | ||||||
|                 { StatusCode = 400 }; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var foundMatch = policy.AcceptTypes.Any(acceptType => |  | ||||||
|             { |  | ||||||
|                 if (acceptType.EndsWith("/*", StringComparison.OrdinalIgnoreCase)) |  | ||||||
|                 { |  | ||||||
|                     var type = acceptType[..^2]; |  | ||||||
|                     return request.ContentType.StartsWith($"{type}/", StringComparison.OrdinalIgnoreCase); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return acceptType.Equals(request.ContentType, StringComparison.OrdinalIgnoreCase); |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             if (!foundMatch) |  | ||||||
|             { |  | ||||||
|                 return new ObjectResult( |  | ||||||
|                     ApiError.Unauthorized($"Content type {request.ContentType} is not allowed by the pool's policy", |  | ||||||
|                         true)) |  | ||||||
|                 { StatusCode = 403 }; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (policy.MaxFileSize is not null && request.FileSize > policy.MaxFileSize) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(ApiError.Unauthorized( |  | ||||||
|                 $"File size {request.FileSize} is larger than the pool's maximum file size {policy.MaxFileSize}", |  | ||||||
|                 true)) |  | ||||||
|             { |  | ||||||
|                 StatusCode = 403 |  | ||||||
|             }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var (ok, billableUnit, quota) = await quotaService.IsFileAcceptable( |  | ||||||
|             Guid.Parse(currentUser.Id), |  | ||||||
|             pool.BillingConfig.CostMultiplier ?? 1.0, |  | ||||||
|             request.FileSize |  | ||||||
|         ); |  | ||||||
|         if (!ok) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult( |  | ||||||
|                 ApiError.Unauthorized($"File size {billableUnit} MiB is exceeded the user's quota {quota} MiB", |  | ||||||
|                     true)) |  | ||||||
|             { StatusCode = 403 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!Directory.Exists(_tempPath)) |  | ||||||
|         { |  | ||||||
|             Directory.CreateDirectory(_tempPath); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Check if a file with the same hash already exists |  | ||||||
|         var existingFile = await db.Files.FirstOrDefaultAsync(f => f.Hash == request.Hash); |  | ||||||
|         if (existingFile != null) |  | ||||||
|         { |  | ||||||
|             return Ok(new CreateUploadTaskResponse |  | ||||||
|             { |  | ||||||
|                 FileExists = true, |  | ||||||
|                 File = existingFile |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var taskId = await Nanoid.GenerateAsync(); |  | ||||||
|         var taskPath = Path.Combine(_tempPath, taskId); |  | ||||||
|         Directory.CreateDirectory(taskPath); |  | ||||||
|  |  | ||||||
|         var chunkSize = request.ChunkSize ?? DefaultChunkSize; |  | ||||||
|         var chunksCount = (int)Math.Ceiling((double)request.FileSize / chunkSize); |  | ||||||
|  |  | ||||||
|         var task = new UploadTask |  | ||||||
|         { |  | ||||||
|             TaskId = taskId, |  | ||||||
|             FileName = request.FileName, |  | ||||||
|             FileSize = request.FileSize, |  | ||||||
|             ContentType = request.ContentType, |  | ||||||
|             ChunkSize = chunkSize, |  | ||||||
|             ChunksCount = chunksCount, |  | ||||||
|             PoolId = request.PoolId.Value, |  | ||||||
|             BundleId = request.BundleId, |  | ||||||
|             EncryptPassword = request.EncryptPassword, |  | ||||||
|             ExpiredAt = request.ExpiredAt, |  | ||||||
|             Hash = request.Hash, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         await System.IO.File.WriteAllTextAsync(Path.Combine(taskPath, "task.json"), JsonSerializer.Serialize(task)); |  | ||||||
|  |  | ||||||
|         return Ok(new CreateUploadTaskResponse |  | ||||||
|         { |  | ||||||
|             FileExists = false, |  | ||||||
|             TaskId = taskId, |  | ||||||
|             ChunkSize = chunkSize, |  | ||||||
|             ChunksCount = chunksCount |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public class UploadChunkRequest |  | ||||||
|     { |  | ||||||
|         [Required] |  | ||||||
|         public IFormFile Chunk { get; set; } = null!; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost("chunk/{taskId}/{chunkIndex}")] |  | ||||||
|     [RequestSizeLimit(DefaultChunkSize + 1024 * 1024)] // 6MB to be safe |  | ||||||
|     [RequestFormLimits(MultipartBodyLengthLimit = DefaultChunkSize + 1024 * 1024)] |  | ||||||
|     public async Task<IActionResult> UploadChunk(string taskId, int chunkIndex, [FromForm] UploadChunkRequest request) |  | ||||||
|     { |  | ||||||
|         var chunk = request.Chunk; |  | ||||||
|         var taskPath = Path.Combine(_tempPath, taskId); |  | ||||||
|         if (!Directory.Exists(taskPath)) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(ApiError.NotFound("Upload task")) { StatusCode = 404 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var chunkPath = Path.Combine(taskPath, $"{chunkIndex}.chunk"); |  | ||||||
|         await using var stream = new FileStream(chunkPath, FileMode.Create); |  | ||||||
|         await chunk.CopyToAsync(stream); |  | ||||||
|  |  | ||||||
|         return Ok(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     [HttpPost("complete/{taskId}")] |  | ||||||
|     public async Task<IActionResult> CompleteUpload(string taskId) |  | ||||||
|     { |  | ||||||
|         var taskPath = Path.Combine(_tempPath, taskId); |  | ||||||
|         if (!Directory.Exists(taskPath)) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(ApiError.NotFound("Upload task")) { StatusCode = 404 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var taskJsonPath = Path.Combine(taskPath, "task.json"); |  | ||||||
|         if (!System.IO.File.Exists(taskJsonPath)) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(ApiError.NotFound("Upload task metadata")) { StatusCode = 404 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var task = JsonSerializer.Deserialize<UploadTask>(await System.IO.File.ReadAllTextAsync(taskJsonPath)); |  | ||||||
|         if (task == null) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(new ApiError { Code = "BAD_REQUEST", Message = "Invalid task metadata.", Status = 400 }) |  | ||||||
|             { StatusCode = 400 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var mergedFilePath = Path.Combine(_tempPath, taskId + ".tmp"); |  | ||||||
|         await using (var mergedStream = new FileStream(mergedFilePath, FileMode.Create)) |  | ||||||
|         { |  | ||||||
|             for (var i = 0; i < task.ChunksCount; i++) |  | ||||||
|             { |  | ||||||
|                 var chunkPath = Path.Combine(taskPath, $"{i}.chunk"); |  | ||||||
|                 if (!System.IO.File.Exists(chunkPath)) |  | ||||||
|                 { |  | ||||||
|                     // Clean up partially uploaded file |  | ||||||
|                     mergedStream.Close(); |  | ||||||
|                     System.IO.File.Delete(mergedFilePath); |  | ||||||
|                     Directory.Delete(taskPath, true); |  | ||||||
|                     return new ObjectResult(new ApiError |  | ||||||
|                     { Code = "CHUNK_MISSING", Message = $"Chunk {i} is missing.", Status = 400 }) |  | ||||||
|                     { StatusCode = 400 }; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 await using var chunkStream = new FileStream(chunkPath, FileMode.Open); |  | ||||||
|                 await chunkStream.CopyToAsync(mergedStream); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|         { |  | ||||||
|             return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var fileId = await Nanoid.GenerateAsync(); |  | ||||||
|  |  | ||||||
|         var cloudFile = await fileService.ProcessNewFileAsync( |  | ||||||
|             currentUser, |  | ||||||
|             fileId, |  | ||||||
|             task.PoolId.ToString(), |  | ||||||
|             task.BundleId?.ToString(), |  | ||||||
|             mergedFilePath, |  | ||||||
|             task.FileName, |  | ||||||
|             task.ContentType, |  | ||||||
|             task.EncryptPassword, |  | ||||||
|             task.ExpiredAt |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         // Clean up |  | ||||||
|         Directory.Delete(taskPath, true); |  | ||||||
|         System.IO.File.Delete(mergedFilePath); |  | ||||||
|  |  | ||||||
|         return Ok(cloudFile); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| namespace DysonNetwork.Drive.Storage.Model; |  | ||||||
|  |  | ||||||
| public static class FileUploadedEvent |  | ||||||
| { |  | ||||||
|     public const string Type = "file_uploaded"; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public record FileUploadedEventPayload( |  | ||||||
|     string FileId, |  | ||||||
|     Guid RemoteId, |  | ||||||
|     string StorageId, |  | ||||||
|     string ContentType, |  | ||||||
|     string ProcessingFilePath, |  | ||||||
|     bool IsTempFile |  | ||||||
| ); |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage.Model |  | ||||||
| { |  | ||||||
|     public class CreateUploadTaskRequest |  | ||||||
|     { |  | ||||||
|         public string Hash { get; set; } = null!; |  | ||||||
|         public string FileName { get; set; } = null!; |  | ||||||
|         public long FileSize { get; set; } |  | ||||||
|         public string ContentType { get; set; } = null!; |  | ||||||
|         public Guid? PoolId { get; set; } = null!; |  | ||||||
|         public Guid? BundleId { get; set; } |  | ||||||
|         public string? EncryptPassword { get; set; } |  | ||||||
|         public Instant? ExpiredAt { get; set; } |  | ||||||
|         public long? ChunkSize { get; set; } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public class CreateUploadTaskResponse |  | ||||||
|     { |  | ||||||
|         public bool FileExists { get; set; } |  | ||||||
|         public SnCloudFile? File { get; set; } |  | ||||||
|         public string? TaskId { get; set; } |  | ||||||
|         public long? ChunkSize { get; set; } |  | ||||||
|         public int? ChunksCount { get; set; } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     internal class UploadTask |  | ||||||
|     { |  | ||||||
|         public string TaskId { get; set; } = null!; |  | ||||||
|         public string FileName { get; set; } = null!; |  | ||||||
|         public long FileSize { get; set; } |  | ||||||
|         public string ContentType { get; set; } = null!; |  | ||||||
|         public long ChunkSize { get; set; } |  | ||||||
|         public int ChunksCount { get; set; } |  | ||||||
|         public Guid PoolId { get; set; } |  | ||||||
|         public Guid? BundleId { get; set; } |  | ||||||
|         public string? EncryptPassword { get; set; } |  | ||||||
|         public Instant? ExpiredAt { get; set; } |  | ||||||
|         public string Hash { get; set; } = null!; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,94 +0,0 @@ | |||||||
| # Multi-part File Upload API |  | ||||||
|  |  | ||||||
| This document outlines the process for uploading large files in chunks using the multi-part upload API. |  | ||||||
|  |  | ||||||
| ## 1. Create an Upload Task |  | ||||||
|  |  | ||||||
| To begin a file upload, you first need to create an upload task. This is done by sending a `POST` request to the `/api/files/upload/create` endpoint. |  | ||||||
|  |  | ||||||
| **Endpoint:** `POST /api/files/upload/create` |  | ||||||
|  |  | ||||||
| **Request Body:** |  | ||||||
|  |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "hash": "string (file hash, e.g., MD5 or SHA256)", |  | ||||||
|   "file_name": "string", |  | ||||||
|   "file_size": "long (in bytes)", |  | ||||||
|   "content_type": "string (e.g., 'image/jpeg')", |  | ||||||
|   "pool_id": "string (GUID, optional)", |  | ||||||
|   "bundle_id": "string (GUID, optional)", |  | ||||||
|   "encrypt_password": "string (optional)", |  | ||||||
|   "expired_at": "string (ISO 8601 format, optional)", |  | ||||||
|   "chunk_size": "long (in bytes, optional, defaults to 5MB)" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| **Response:** |  | ||||||
|  |  | ||||||
| If a file with the same hash already exists, the server will return a `200 OK` with the following body: |  | ||||||
|  |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "file_exists": true, |  | ||||||
|   "file": { ... (CloudFile object in snake_case) ... } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| If the file does not exist, the server will return a `200 OK` with a task ID and chunk information: |  | ||||||
|  |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "file_exists": false, |  | ||||||
|   "task_id": "string", |  | ||||||
|   "chunk_size": "long", |  | ||||||
|   "chunks_count": "int" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| You will need the `task_id`, `chunk_size`, and `chunks_count` for the next steps. |  | ||||||
|  |  | ||||||
| ## 2. Upload File Chunks |  | ||||||
|  |  | ||||||
| Once you have a `task_id`, you can start uploading the file in chunks. Each chunk is sent as a `POST` request with `multipart/form-data`. |  | ||||||
|  |  | ||||||
| **Endpoint:** `POST /api/files/upload/chunk/{taskId}/{chunkIndex}` |  | ||||||
|  |  | ||||||
| -   `taskId`: The ID of the upload task from the previous step. |  | ||||||
| -   `chunkIndex`: The 0-based index of the chunk you are uploading. |  | ||||||
|  |  | ||||||
| **Request Body:** |  | ||||||
|  |  | ||||||
| The body of the request should be `multipart/form-data` with a single form field named `chunk` containing the binary data for that chunk. |  | ||||||
|  |  | ||||||
| The size of each chunk should be equal to the `chunk_size` returned in the "Create Upload Task" step, except for the last chunk, which may be smaller. |  | ||||||
|  |  | ||||||
| **Response:** |  | ||||||
|  |  | ||||||
| A successful chunk upload will return a `200 OK` with an empty body. |  | ||||||
|  |  | ||||||
| You should upload all chunks from `0` to `chunks_count - 1`. |  | ||||||
|  |  | ||||||
| ## 3. Complete the Upload |  | ||||||
|  |  | ||||||
| After all chunks have been successfully uploaded, you must send a final request to complete the upload process. This will merge all the chunks into a single file and process it. |  | ||||||
|  |  | ||||||
| **Endpoint:** `POST /api/files/upload/complete/{taskId}` |  | ||||||
|  |  | ||||||
| -   `taskId`: The ID of the upload task. |  | ||||||
|  |  | ||||||
| **Request Body:** |  | ||||||
|  |  | ||||||
| The request body should be empty. |  | ||||||
|  |  | ||||||
| **Response:** |  | ||||||
|  |  | ||||||
| A successful request will return a `200 OK` with the `CloudFile` object for the newly uploaded file. |  | ||||||
|  |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   ... (CloudFile object) ... |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| If any chunks are missing or an error occurs during the merge process, the server will return a `400 Bad Request` with an error message. |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Data; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/version")] |  | ||||||
| public class VersionController : ControllerBase |  | ||||||
| { |  | ||||||
|     [HttpGet] |  | ||||||
|     public IActionResult Get() |  | ||||||
|     { |  | ||||||
|         return Ok(new AppVersion |  | ||||||
|         { |  | ||||||
|             Version = ThisAssembly.AssemblyVersion, |  | ||||||
|             Commit = ThisAssembly.GitCommitId, |  | ||||||
|             UpdateDate = ThisAssembly.GitCommitDate |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,121 +0,0 @@ | |||||||
| { |  | ||||||
|   "Debug": true, |  | ||||||
|   "BaseUrl": "http://localhost:5090", |  | ||||||
|   "GatewayUrl": "http://localhost:5094", |  | ||||||
|   "Logging": { |  | ||||||
|     "LogLevel": { |  | ||||||
|       "Default": "Information", |  | ||||||
|       "Microsoft.AspNetCore": "Warning" |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "AllowedHosts": "*", |  | ||||||
|   "ConnectionStrings": { |  | ||||||
|     "App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" |  | ||||||
|   }, |  | ||||||
|   "Authentication": { |  | ||||||
|     "Schemes": { |  | ||||||
|       "Bearer": { |  | ||||||
|         "ValidAudiences": [ |  | ||||||
|           "http://localhost:5071", |  | ||||||
|           "https://localhost:7099" |  | ||||||
|         ], |  | ||||||
|         "ValidIssuer": "solar-network" |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "AuthToken": { |  | ||||||
|     "PublicKeyPath": "Keys/PublicKey.pem", |  | ||||||
|     "PrivateKeyPath": "Keys/PrivateKey.pem" |  | ||||||
|   }, |  | ||||||
|   "Tus": { |  | ||||||
|     "StorePath": "Uploads" |  | ||||||
|   }, |  | ||||||
|   "Storage": { |  | ||||||
|     "Uploads": "Uploads", |  | ||||||
|     "PreferredRemote": "2adceae3-981a-4564-9b8d-5d71a211c873", |  | ||||||
|     "Remote": [ |  | ||||||
|       { |  | ||||||
|         "Id": "minio", |  | ||||||
|         "Label": "Minio", |  | ||||||
|         "Region": "auto", |  | ||||||
|         "Bucket": "solar-network-development", |  | ||||||
|         "Endpoint": "localhost:9000", |  | ||||||
|         "SecretId": "littlesheep", |  | ||||||
|         "SecretKey": "password", |  | ||||||
|         "EnabledSigned": true, |  | ||||||
|         "EnableSsl": false |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         "Id": "cloudflare", |  | ||||||
|         "Label": "Cloudflare R2", |  | ||||||
|         "Region": "auto", |  | ||||||
|         "Bucket": "solar-network", |  | ||||||
|         "Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com", |  | ||||||
|         "SecretId": "8ff5d06c7b1639829d60bc6838a542e6", |  | ||||||
|         "SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67", |  | ||||||
|         "EnableSigned": true, |  | ||||||
|         "EnableSsl": true |  | ||||||
|       } |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   "Captcha": { |  | ||||||
|     "Provider": "cloudflare", |  | ||||||
|     "ApiKey": "0x4AAAAAABCDUdOujj4feOb_", |  | ||||||
|     "ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U" |  | ||||||
|   }, |  | ||||||
|   "Notifications": { |  | ||||||
|     "Topic": "dev.solsynth.solian", |  | ||||||
|     "Endpoint": "http://localhost:8088" |  | ||||||
|   }, |  | ||||||
|   "Email": { |  | ||||||
|     "Server": "smtp4dev.orb.local", |  | ||||||
|     "Port": 25, |  | ||||||
|     "UseSsl": false, |  | ||||||
|     "Username": "no-reply@mail.solsynth.dev", |  | ||||||
|     "Password": "password", |  | ||||||
|     "FromAddress": "no-reply@mail.solsynth.dev", |  | ||||||
|     "FromName": "Alphabot", |  | ||||||
|     "SubjectPrefix": "Solar Network" |  | ||||||
|   }, |  | ||||||
|   "RealtimeChat": { |  | ||||||
|     "Endpoint": "https://solar-network-im44o8gq.livekit.cloud", |  | ||||||
|     "ApiKey": "APIs6TiL8wj3A4j", |  | ||||||
|     "ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE" |  | ||||||
|   }, |  | ||||||
|   "GeoIp": { |  | ||||||
|     "DatabasePath": "./Keys/GeoLite2-City.mmdb" |  | ||||||
|   }, |  | ||||||
|   "Oidc": { |  | ||||||
|     "Google": { |  | ||||||
|       "ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com", |  | ||||||
|       "ClientSecret": "" |  | ||||||
|     }, |  | ||||||
|     "Apple": { |  | ||||||
|       "ClientId": "dev.solsynth.solian", |  | ||||||
|       "TeamId": "W7HPZ53V6B", |  | ||||||
|       "KeyId": "B668YP4KBG", |  | ||||||
|       "PrivateKeyPath": "./Keys/Solarpass.p8" |  | ||||||
|     }, |  | ||||||
|     "Microsoft": { |  | ||||||
|       "ClientId": "YOUR_MICROSOFT_CLIENT_ID", |  | ||||||
|       "ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET", |  | ||||||
|       "DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT" |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "Payment": { |  | ||||||
|     "Auth": { |  | ||||||
|       "Afdian": "<token here>" |  | ||||||
|     }, |  | ||||||
|     "Subscriptions": { |  | ||||||
|       "Afdian": { |  | ||||||
|         "7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary", |  | ||||||
|         "7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova", |  | ||||||
|         "141713ee3d6211f085b352540025c377": "solian.stellar.supernova" |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "KnownProxies": [ |  | ||||||
|     "127.0.0.1", |  | ||||||
|     "::1" |  | ||||||
|   ] |  | ||||||
| } |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| { |  | ||||||
|   "version": "1.0", |  | ||||||
|   "publicReleaseRefSpec": ["^refs/heads/main$"], |  | ||||||
|   "cloudBuild": { |  | ||||||
|     "setVersionVariables": true |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("config")] |  | ||||||
| public class ConfigurationController(IConfiguration configuration) : ControllerBase |  | ||||||
| { |  | ||||||
|     [HttpGet] |  | ||||||
|     public IActionResult Get() => Ok(configuration.GetSection("Client").Get<Dictionary<string, object>>()); |  | ||||||
|  |  | ||||||
|     [HttpGet("site")] |  | ||||||
|     public IActionResult GetSiteUrl() => Ok(configuration["SiteUrl"]); |  | ||||||
| } |  | ||||||
| @@ -1,23 +0,0 @@ | |||||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base |  | ||||||
| USER $APP_UID |  | ||||||
| WORKDIR /app |  | ||||||
| EXPOSE 8080 |  | ||||||
| EXPOSE 8081 |  | ||||||
|  |  | ||||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build |  | ||||||
| ARG BUILD_CONFIGURATION=Release |  | ||||||
| WORKDIR /src |  | ||||||
| COPY ["DysonNetwork.Gateway/DysonNetwork.Gateway.csproj", "DysonNetwork.Gateway/"] |  | ||||||
| RUN dotnet restore "DysonNetwork.Gateway/DysonNetwork.Gateway.csproj" |  | ||||||
| COPY . . |  | ||||||
| WORKDIR "/src/DysonNetwork.Gateway" |  | ||||||
| RUN dotnet build "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/build |  | ||||||
|  |  | ||||||
| FROM build AS publish |  | ||||||
| ARG BUILD_CONFIGURATION=Release |  | ||||||
| RUN dotnet publish "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false |  | ||||||
|  |  | ||||||
| FROM base AS final |  | ||||||
| WORKDIR /app |  | ||||||
| COPY --from=publish /app/publish . |  | ||||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Gateway.dll"] |  | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> |  | ||||||
|  |  | ||||||
|   <PropertyGroup> |  | ||||||
|     <TargetFramework>net9.0</TargetFramework> |  | ||||||
|     <Nullable>enable</Nullable> |  | ||||||
|     <ImplicitUsings>enable</ImplicitUsings> |  | ||||||
|   </PropertyGroup> |  | ||||||
|  |  | ||||||
|   <ItemGroup> |  | ||||||
|     <PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="9.5.2" /> |  | ||||||
|     <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|  |  | ||||||
|   <ItemGroup> |  | ||||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|  |  | ||||||
| </Project> |  | ||||||
| @@ -1,168 +0,0 @@ | |||||||
| using System.Threading.RateLimiting; |  | ||||||
| using DysonNetwork.Shared.Http; |  | ||||||
| using Yarp.ReverseProxy.Configuration; |  | ||||||
| using Microsoft.AspNetCore.HttpOverrides; |  | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); |  | ||||||
|  |  | ||||||
| builder.AddServiceDefaults(); |  | ||||||
|  |  | ||||||
| builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue, enableGrpc: false); |  | ||||||
|  |  | ||||||
| builder.Services.AddCors(options => |  | ||||||
| { |  | ||||||
|     options.AddDefaultPolicy( |  | ||||||
|         policy => |  | ||||||
|         { |  | ||||||
|             policy.SetIsOriginAllowed(origin => true) |  | ||||||
|                 .AllowAnyMethod() |  | ||||||
|                 .AllowAnyHeader() |  | ||||||
|                 .AllowCredentials() |  | ||||||
|                 .WithExposedHeaders("X-Total"); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| builder.Services.AddRateLimiter(options => |  | ||||||
| { |  | ||||||
|     options.AddPolicy("fixed", context => |  | ||||||
|     { |  | ||||||
|         var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; |  | ||||||
|  |  | ||||||
|         return RateLimitPartition.GetFixedWindowLimiter( |  | ||||||
|             partitionKey: ip, |  | ||||||
|             factory: _ => new FixedWindowRateLimiterOptions |  | ||||||
|             { |  | ||||||
|                 PermitLimit = 120, // 120 requests... |  | ||||||
|                 Window = TimeSpan.FromMinutes(1), // ...per minute per IP |  | ||||||
|                 QueueProcessingOrder = QueueProcessingOrder.OldestFirst, |  | ||||||
|                 QueueLimit = 10 // allow short bursts instead of instant 503s |  | ||||||
|             }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     options.OnRejected = async (context, token) => |  | ||||||
|         { |  | ||||||
|             // Log the rejected IP |  | ||||||
|             var logger = context.HttpContext.RequestServices |  | ||||||
|                 .GetRequiredService<ILoggerFactory>() |  | ||||||
|                 .CreateLogger("RateLimiter"); |  | ||||||
|  |  | ||||||
|             var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; |  | ||||||
|             logger.LogWarning("Rate limit exceeded for IP: {IP}", ip); |  | ||||||
|  |  | ||||||
|             // Respond to the client |  | ||||||
|             context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; |  | ||||||
|             await context.HttpContext.Response.WriteAsync( |  | ||||||
|                 "Rate limit exceeded. Try again later.", token); |  | ||||||
|         }; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| var serviceNames = new[] { "ring", "pass", "drive", "sphere", "develop", "insight" }; |  | ||||||
|  |  | ||||||
| var specialRoutes = new[] |  | ||||||
| { |  | ||||||
|     new RouteConfig |  | ||||||
|     { |  | ||||||
|         RouteId = "ring-ws", |  | ||||||
|         ClusterId = "ring", |  | ||||||
|         Match = new RouteMatch { Path = "/ws" } |  | ||||||
|     }, |  | ||||||
|     new RouteConfig |  | ||||||
|     { |  | ||||||
|         RouteId = "pass-openid", |  | ||||||
|         ClusterId = "pass", |  | ||||||
|         Match = new RouteMatch { Path = "/.well-known/openid-configuration" } |  | ||||||
|     }, |  | ||||||
|     new RouteConfig |  | ||||||
|     { |  | ||||||
|         RouteId = "pass-jwks", |  | ||||||
|         ClusterId = "pass", |  | ||||||
|         Match = new RouteMatch { Path = "/.well-known/jwks" } |  | ||||||
|     }, |  | ||||||
|     new RouteConfig |  | ||||||
|     { |  | ||||||
|         RouteId = "drive-tus", |  | ||||||
|         ClusterId = "drive", |  | ||||||
|         Match = new RouteMatch { Path = "/api/tus" } |  | ||||||
|     } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| var apiRoutes = serviceNames.Select(serviceName => |  | ||||||
| { |  | ||||||
|     var apiPath = serviceName switch |  | ||||||
|     { |  | ||||||
|         _ => $"/{serviceName}" |  | ||||||
|     }; |  | ||||||
|     return new RouteConfig |  | ||||||
|     { |  | ||||||
|         RouteId = $"{serviceName}-api", |  | ||||||
|         ClusterId = serviceName, |  | ||||||
|         Match = new RouteMatch { Path = $"{apiPath}/{{**catch-all}}" }, |  | ||||||
|         Transforms = |  | ||||||
|         [ |  | ||||||
|             new Dictionary<string, string> { { "PathRemovePrefix", apiPath } }, |  | ||||||
|             new Dictionary<string, string> { { "PathPrefix", "/api" } } |  | ||||||
|         ] |  | ||||||
|     }; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| var swaggerRoutes = serviceNames.Select(serviceName => new RouteConfig |  | ||||||
| { |  | ||||||
|     RouteId = $"{serviceName}-swagger", |  | ||||||
|     ClusterId = serviceName, |  | ||||||
|     Match = new RouteMatch { Path = $"/swagger/{serviceName}/{{**catch-all}}" }, |  | ||||||
|     Transforms = |  | ||||||
|     [ |  | ||||||
|         new Dictionary<string, string> { { "PathRemovePrefix", $"/swagger/{serviceName}" } }, |  | ||||||
|         new Dictionary<string, string> { { "PathPrefix", "/swagger" } } |  | ||||||
|     ] |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| var routes = specialRoutes.Concat(apiRoutes).Concat(swaggerRoutes).ToArray(); |  | ||||||
|  |  | ||||||
| var clusters = serviceNames.Select(serviceName => new ClusterConfig |  | ||||||
| { |  | ||||||
|     ClusterId = serviceName, |  | ||||||
|     HealthCheck = new HealthCheckConfig |  | ||||||
|     { |  | ||||||
|         Active = new ActiveHealthCheckConfig |  | ||||||
|         { |  | ||||||
|             Enabled = true, |  | ||||||
|             Interval = TimeSpan.FromSeconds(10), |  | ||||||
|             Timeout = TimeSpan.FromSeconds(5), |  | ||||||
|             Path = "/health" |  | ||||||
|         }, |  | ||||||
|         Passive = new() |  | ||||||
|         { |  | ||||||
|             Enabled = true |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     Destinations = new Dictionary<string, DestinationConfig> |  | ||||||
|     { |  | ||||||
|         { "destination1", new DestinationConfig { Address = $"http://{serviceName}" } } |  | ||||||
|     } |  | ||||||
| }).ToArray(); |  | ||||||
|  |  | ||||||
| builder.Services |  | ||||||
|     .AddReverseProxy() |  | ||||||
|     .LoadFromMemory(routes, clusters) |  | ||||||
|     .AddServiceDiscoveryDestinationResolver(); |  | ||||||
|  |  | ||||||
| builder.Services.AddControllers(); |  | ||||||
|  |  | ||||||
| var app = builder.Build(); |  | ||||||
|  |  | ||||||
| var forwardedHeadersOptions = new ForwardedHeadersOptions |  | ||||||
| { |  | ||||||
|     ForwardedHeaders = ForwardedHeaders.All |  | ||||||
| }; |  | ||||||
| forwardedHeadersOptions.KnownNetworks.Clear(); |  | ||||||
| forwardedHeadersOptions.KnownProxies.Clear(); |  | ||||||
| app.UseForwardedHeaders(forwardedHeadersOptions); |  | ||||||
|  |  | ||||||
| app.UseCors(); |  | ||||||
|  |  | ||||||
| app.MapReverseProxy().RequireRateLimiting("fixed"); |  | ||||||
|  |  | ||||||
| app.MapControllers(); |  | ||||||
|  |  | ||||||
| app.Run(); |  | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| { |  | ||||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", |  | ||||||
|   "profiles": { |  | ||||||
|     "http": { |  | ||||||
|       "commandName": "Project", |  | ||||||
|       "dotnetRunMessages": true, |  | ||||||
|       "launchBrowser": true, |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "https": { |  | ||||||
|       "commandName": "Project", |  | ||||||
|       "dotnetRunMessages": true, |  | ||||||
|       "launchBrowser": true, |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| { |  | ||||||
|   "Logging": { |  | ||||||
|     "LogLevel": { |  | ||||||
|       "Default": "Information", |  | ||||||
|       "Microsoft.AspNetCore": "Warning" |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "AllowedHosts": "*", |  | ||||||
|   "SiteUrl": "http://localhost:3000", |  | ||||||
|   "Client": { |  | ||||||
|     "SomeSetting": "SomeValue" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,76 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Design; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Insight; |  | ||||||
|  |  | ||||||
| public class AppDatabase( |  | ||||||
|     DbContextOptions<AppDatabase> options, |  | ||||||
|     IConfiguration configuration |  | ||||||
| ) : DbContext(options) |  | ||||||
| { |  | ||||||
|     public DbSet<SnThinkingSequence> ThinkingSequences { get; set; } |  | ||||||
|     public DbSet<SnThinkingThought> ThinkingThoughts { get; set; } |  | ||||||
|      |  | ||||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |  | ||||||
|     { |  | ||||||
|         optionsBuilder.UseNpgsql( |  | ||||||
|             configuration.GetConnectionString("App"), |  | ||||||
|             opt => opt |  | ||||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) |  | ||||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) |  | ||||||
|                 .UseNodaTime() |  | ||||||
|         ).UseSnakeCaseNamingConvention(); |  | ||||||
|  |  | ||||||
|         base.OnConfiguring(optionsBuilder); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) |  | ||||||
|     { |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|  |  | ||||||
|         foreach (var entry in ChangeTracker.Entries<ModelBase>()) |  | ||||||
|         { |  | ||||||
|             switch (entry.State) |  | ||||||
|             { |  | ||||||
|                 case EntityState.Added: |  | ||||||
|                     entry.Entity.CreatedAt = now; |  | ||||||
|                     entry.Entity.UpdatedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Modified: |  | ||||||
|                     entry.Entity.UpdatedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Deleted: |  | ||||||
|                     entry.State = EntityState.Modified; |  | ||||||
|                     entry.Entity.DeletedAt = now; |  | ||||||
|                     break; |  | ||||||
|                 case EntityState.Detached: |  | ||||||
|                 case EntityState.Unchanged: |  | ||||||
|                 default: |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return await base.SaveChangesAsync(cancellationToken); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) |  | ||||||
|     { |  | ||||||
|         base.OnModelCreating(modelBuilder); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase> |  | ||||||
| { |  | ||||||
|     public AppDatabase CreateDbContext(string[] args) |  | ||||||
|     { |  | ||||||
|         var configuration = new ConfigurationBuilder() |  | ||||||
|             .SetBasePath(Directory.GetCurrentDirectory()) |  | ||||||
|             .AddJsonFile("appsettings.json") |  | ||||||
|             .Build(); |  | ||||||
|  |  | ||||||
|         var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>(); |  | ||||||
|         return new AppDatabase(optionsBuilder.Options, configuration); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| using DysonNetwork.Insight.Thought; |  | ||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using Microsoft.Extensions.Logging; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Insight.Controllers; |  | ||||||
|  |  | ||||||
| [ApiController] |  | ||||||
| [Route("/api/billing")] |  | ||||||
| public class BillingController(ThoughtService thoughtService, ILogger<BillingController> logger) : ControllerBase |  | ||||||
| { |  | ||||||
|     [HttpPost("settle")] |  | ||||||
|     [Authorize] |  | ||||||
|     [RequiredPermission("maintenance", "insight.billing.settle")] |  | ||||||
|     public async Task<IActionResult> ProcessTokenBilling() |  | ||||||
|     { |  | ||||||
|         await thoughtService.SettleThoughtBills(logger); |  | ||||||
|         return Ok(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. |  | ||||||
|  |  | ||||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base |  | ||||||
| USER app |  | ||||||
| WORKDIR /app |  | ||||||
| EXPOSE 8080 |  | ||||||
| EXPOSE 8081 |  | ||||||
|  |  | ||||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build |  | ||||||
| ARG BUILD_CONFIGURATION=Release |  | ||||||
| WORKDIR /src |  | ||||||
| COPY ["DysonNetwork.Insight/DysonNetwork.Insight.csproj", "DysonNetwork.Insight/"] |  | ||||||
| COPY ["DysonNetwork.Shared/DysonNetwork.Shared.csproj", "DysonNetwork.Shared/"] |  | ||||||
| COPY ["DysonNetwork.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"] |  | ||||||
| RUN dotnet restore "DysonNetwork.Insight/DysonNetwork.Insight.csproj" |  | ||||||
| COPY . . |  | ||||||
| WORKDIR "/src/DysonNetwork.Insight" |  | ||||||
| RUN dotnet build "DysonNetwork.Insight.csproj" -c $BUILD_CONFIGURATION -o /app/build |  | ||||||
|  |  | ||||||
| FROM build AS publish |  | ||||||
| ARG BUILD_CONFIGURATION=Release |  | ||||||
| RUN dotnet publish "DysonNetwork.Insight.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false |  | ||||||
|  |  | ||||||
| FROM base AS final |  | ||||||
| WORKDIR /app |  | ||||||
| COPY --from=publish /app/publish . |  | ||||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Insight.dll"] |  | ||||||
| @@ -1,34 +0,0 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> |  | ||||||
|  |  | ||||||
|   <PropertyGroup> |  | ||||||
|     <TargetFramework>net9.0</TargetFramework> |  | ||||||
|     <Nullable>enable</Nullable> |  | ||||||
|     <ImplicitUsings>enable</ImplicitUsings> |  | ||||||
|   </PropertyGroup> |  | ||||||
|  |  | ||||||
|   <ItemGroup> |  | ||||||
|     <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> |  | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> |  | ||||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> |  | ||||||
|       <PrivateAssets>all</PrivateAssets> |  | ||||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |  | ||||||
|     </PackageReference> |  | ||||||
|     <PackageReference Include="Microsoft.SemanticKernel" Version="1.66.0" /> |  | ||||||
|     <PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" /> |  | ||||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> |  | ||||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> |  | ||||||
|     <PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="1.66.0-alpha" /> |  | ||||||
|     <PackageReference Include="Quartz" Version="3.15.0" /> |  | ||||||
|     <PackageReference Include="Quartz.AspNetCore" Version="3.15.0" /> |  | ||||||
|     <PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|  |  | ||||||
|   <ItemGroup> |  | ||||||
|     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|  |  | ||||||
|   <ItemGroup> |  | ||||||
|     <Folder Include="Controllers\" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|  |  | ||||||
| </Project> |  | ||||||
| @@ -1,124 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Insight; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Insight.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20251025115921_AddThinkingThought")] |  | ||||||
|     partial class AddThinkingThought |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.10") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Topic") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("topic"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_thinking_sequences"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("thinking_sequences", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Content") |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("content"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<SnCloudFileReferenceObject>>("Files") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("files"); |  | ||||||
|  |  | ||||||
|                     b.Property<int>("Role") |  | ||||||
|                         .HasColumnType("integer") |  | ||||||
|                         .HasColumnName("role"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("SequenceId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("sequence_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_thinking_thoughts"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("SequenceId") |  | ||||||
|                         .HasDatabaseName("ix_thinking_thoughts_sequence_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("thinking_thoughts", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("SequenceId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Sequence"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,73 +0,0 @@ | |||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Insight.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddThinkingThought : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "thinking_sequences", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     topic = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), |  | ||||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_thinking_sequences", x => x.id); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateTable( |  | ||||||
|                 name: "thinking_thoughts", |  | ||||||
|                 columns: table => new |  | ||||||
|                 { |  | ||||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     content = table.Column<string>(type: "text", nullable: true), |  | ||||||
|                     files = table.Column<List<SnCloudFileReferenceObject>>(type: "jsonb", nullable: false), |  | ||||||
|                     role = table.Column<int>(type: "integer", nullable: false), |  | ||||||
|                     sequence_id = table.Column<Guid>(type: "uuid", nullable: false), |  | ||||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), |  | ||||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) |  | ||||||
|                 }, |  | ||||||
|                 constraints: table => |  | ||||||
|                 { |  | ||||||
|                     table.PrimaryKey("pk_thinking_thoughts", x => x.id); |  | ||||||
|                     table.ForeignKey( |  | ||||||
|                         name: "fk_thinking_thoughts_thinking_sequences_sequence_id", |  | ||||||
|                         column: x => x.sequence_id, |  | ||||||
|                         principalTable: "thinking_sequences", |  | ||||||
|                         principalColumn: "id", |  | ||||||
|                         onDelete: ReferentialAction.Cascade); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             migrationBuilder.CreateIndex( |  | ||||||
|                 name: "ix_thinking_thoughts_sequence_id", |  | ||||||
|                 table: "thinking_thoughts", |  | ||||||
|                 column: "sequence_id"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "thinking_thoughts"); |  | ||||||
|  |  | ||||||
|             migrationBuilder.DropTable( |  | ||||||
|                 name: "thinking_sequences"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,129 +0,0 @@ | |||||||
| // <auto-generated /> |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Insight; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |  | ||||||
| using NodaTime; |  | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Insight.Migrations |  | ||||||
| { |  | ||||||
|     [DbContext(typeof(AppDatabase))] |  | ||||||
|     [Migration("20251026045505_AddThinkingChunk")] |  | ||||||
|     partial class AddThinkingChunk |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) |  | ||||||
|         { |  | ||||||
| #pragma warning disable 612, 618 |  | ||||||
|             modelBuilder |  | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.10") |  | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |  | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("AccountId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("account_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Topic") |  | ||||||
|                         .HasMaxLength(4096) |  | ||||||
|                         .HasColumnType("character varying(4096)") |  | ||||||
|                         .HasColumnName("topic"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_thinking_sequences"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("thinking_sequences", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => |  | ||||||
|                 { |  | ||||||
|                     b.Property<Guid>("Id") |  | ||||||
|                         .ValueGeneratedOnAdd() |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("id"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<SnThinkingChunk>>("Chunks") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("chunks"); |  | ||||||
|  |  | ||||||
|                     b.Property<string>("Content") |  | ||||||
|                         .HasColumnType("text") |  | ||||||
|                         .HasColumnName("content"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("CreatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("created_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant?>("DeletedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("deleted_at"); |  | ||||||
|  |  | ||||||
|                     b.Property<List<SnCloudFileReferenceObject>>("Files") |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |  | ||||||
|                         .HasColumnName("files"); |  | ||||||
|  |  | ||||||
|                     b.Property<int>("Role") |  | ||||||
|                         .HasColumnType("integer") |  | ||||||
|                         .HasColumnName("role"); |  | ||||||
|  |  | ||||||
|                     b.Property<Guid>("SequenceId") |  | ||||||
|                         .HasColumnType("uuid") |  | ||||||
|                         .HasColumnName("sequence_id"); |  | ||||||
|  |  | ||||||
|                     b.Property<Instant>("UpdatedAt") |  | ||||||
|                         .HasColumnType("timestamp with time zone") |  | ||||||
|                         .HasColumnName("updated_at"); |  | ||||||
|  |  | ||||||
|                     b.HasKey("Id") |  | ||||||
|                         .HasName("pk_thinking_thoughts"); |  | ||||||
|  |  | ||||||
|                     b.HasIndex("SequenceId") |  | ||||||
|                         .HasDatabaseName("ix_thinking_thoughts_sequence_id"); |  | ||||||
|  |  | ||||||
|                     b.ToTable("thinking_thoughts", (string)null); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => |  | ||||||
|                 { |  | ||||||
|                     b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence") |  | ||||||
|                         .WithMany() |  | ||||||
|                         .HasForeignKey("SequenceId") |  | ||||||
|                         .OnDelete(DeleteBehavior.Cascade) |  | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id"); |  | ||||||
|  |  | ||||||
|                     b.Navigation("Sequence"); |  | ||||||
|                 }); |  | ||||||
| #pragma warning restore 612, 618 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| using System.Collections.Generic; |  | ||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; |  | ||||||
|  |  | ||||||
| #nullable disable |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Insight.Migrations |  | ||||||
| { |  | ||||||
|     /// <inheritdoc /> |  | ||||||
|     public partial class AddThinkingChunk : Migration |  | ||||||
|     { |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Up(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.AddColumn<List<SnThinkingChunk>>( |  | ||||||
|                 name: "chunks", |  | ||||||
|                 table: "thinking_thoughts", |  | ||||||
|                 type: "jsonb", |  | ||||||
|                 nullable: false, |  | ||||||
|                 defaultValue: new List<SnThinkingChunk>() |  | ||||||
|                 ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         protected override void Down(MigrationBuilder migrationBuilder) |  | ||||||
|         { |  | ||||||
|             migrationBuilder.DropColumn( |  | ||||||
|                 name: "chunks", |  | ||||||
|                 table: "thinking_thoughts"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user