Compare commits
	
		
			93 Commits
		
	
	
		
			f81e3dc9f4
			...
			refactor/w
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 28d60c722a | |||
| 4626529eb5 | |||
| 46ebd92dc1 | |||
| 7f8521bb40 | |||
| f01226d91a | |||
| 6cb6dee6be | |||
| 0e9caf67ff | |||
| ca70bb5487 | |||
| 59ed135f20 | |||
| 6077f91529 | |||
| 5c485bb1c3 | |||
| 27d979d77b | |||
| 15687a0c32 | |||
| 37ea882ef7 | |||
| e624c2bb3e | |||
| 9631cd3edd | |||
| f4a659fce5 | |||
| 1ded811b36 | |||
| 32977d9580 | |||
| aaf29e7228 | |||
| 658ef3bddf | |||
| fc0bc936ce | |||
| 3850ae6a8e | |||
| 21c99567b4 | |||
| 1315c7f4d4 | |||
| 630a532d98 | |||
| b9bb180113 | |||
| 04d74d0d70 | |||
| 6a8a0ed491 | |||
| 0f835845bf | |||
| c5d8a8d07f | |||
| 95e2ba1136 | |||
| 1176fde8b4 | |||
| e634968e00 | |||
| 282a1dbddc | |||
| c64adace24 | |||
| 8ac0b28c66 | |||
| 8f71d7f9e5 | |||
| c435e63917 | |||
| 243159e4cc | |||
| 42dad7095a | |||
| d1efcdede8 | |||
| 47680475b3 | |||
| 6632d43f32 | |||
| 29c4dcd71c | |||
| e7aa887715 | |||
| 0f05633996 | |||
| 966af08a33 | |||
| b25b90a074 | |||
| dcbefeaaab | |||
| eb83a0392a | |||
| 85fefcf724 | |||
| d17c26a228 | |||
| 2e5ef8ff94 | |||
| 7a5f410e36 | |||
| 0b4e8a9777 | |||
| 30fd912281 | |||
| 5bf58f0194 | |||
| 8e3e3f09df | |||
| fa24f14c05 | |||
| a93b633e84 | |||
| 97a7b876db | |||
| 909fe173c2 | |||
| 58a44e8af4 | |||
| 1075177511 | |||
| 78f8a9e638 | |||
| 9ce31c4dd8 | |||
| e70d8371f8 | |||
| 51b6f7309e | |||
| d75876a772 | |||
| 4910c3296b | |||
| 7b924fa075 | |||
| d69c9f9623 | |||
| a88d828e21 | |||
| 14c93d372e | |||
| adf371a72e | |||
| c03f2472fa | |||
| 50efe62bac | |||
| 7bc94a9646 | |||
| d9fe1273b5 | |||
| ff9d490869 | |||
| 266312e97e | |||
| 7087736e31 | |||
| 82bf1608fd | |||
| 3b3287db0b | |||
| 4573d9395f | |||
| a8c99b3128 | |||
| fdd7bd3c9d | |||
| b785d0098b | |||
| 5b31357fe9 | |||
| d5a5721402 | |||
| 204640a759 | |||
| e3657386cd | 
| @@ -1,3 +1,4 @@ | ||||
| { | ||||
|   "appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj" | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										5
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| root = true | ||||
|  | ||||
| [*] | ||||
| indent_style = space | ||||
| indent_size = 4 | ||||
							
								
								
									
										3
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								.env
									
									
									
									
									
								
							| @@ -33,3 +33,6 @@ SPHERE_IMAGE=sphere:latest | ||||
|  | ||||
| # Container image name for develop | ||||
| DEVELOP_IMAGE=develop:latest | ||||
|  | ||||
| # Container image name for gateway | ||||
| GATEWAY_IMAGE=gateway:latest | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -26,6 +26,8 @@ jobs: | ||||
|             image: drive | ||||
|           - service: Develop | ||||
|             image: develop | ||||
|           - service: Gateway | ||||
|             image: gateway | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|   | ||||
							
								
								
									
										613
									
								
								API_WALLET_FUNDS.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										613
									
								
								API_WALLET_FUNDS.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,613 @@ | ||||
| # 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,37 +1,28 @@ | ||||
| using Aspire.Hosting.Yarp.Transforms; | ||||
| using Microsoft.Extensions.Hosting; | ||||
|  | ||||
| var builder = DistributedApplication.CreateBuilder(args); | ||||
|  | ||||
| var isDev = builder.Environment.IsDevelopment(); | ||||
|  | ||||
| // Database was configured separately in each service. | ||||
| // var database = builder.AddPostgres("database"); | ||||
|  | ||||
| var cache = builder.AddRedis("cache"); | ||||
| var queue = builder.AddNats("queue").WithJetStream(); | ||||
|  | ||||
| var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring") | ||||
|     .WithReference(queue); | ||||
| var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring"); | ||||
| var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(ringService); | ||||
| var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService); | ||||
| var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithReference(driveService); | ||||
| var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService); | ||||
|     .WithReference(ringService) | ||||
|     .WithReference(sphereService); | ||||
|  | ||||
| passService.WithReference(developService).WithReference(driveService); | ||||
|  | ||||
| List<IResourceBuilder<ProjectResource>> services = | ||||
|     [ringService, passService, driveService, sphereService, developService]; | ||||
| @@ -39,6 +30,9 @@ List<IResourceBuilder<ProjectResource>> services = | ||||
| for (var idx = 0; idx < services.Count; idx++) | ||||
| { | ||||
|     var service = services[idx]; | ||||
|  | ||||
|     service.WithReference(cache).WithReference(queue); | ||||
|  | ||||
|     var grpcPort = 7002 + idx; | ||||
|  | ||||
|     if (isDev) | ||||
| @@ -60,36 +54,12 @@ for (var idx = 0; idx < services.Count; idx++) | ||||
| // Extra double-ended references | ||||
| ringService.WithReference(passService); | ||||
|  | ||||
| var gateway = builder.AddYarp("gateway") | ||||
|     .WithConfiguration(yarp => | ||||
|     { | ||||
|         var ringCluster = yarp.AddCluster(ringService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/ws", ringCluster); | ||||
|         yarp.AddRoute("/ring/{**catch-all}", ringCluster) | ||||
|             .WithTransformPathRemovePrefix("/ring") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var passCluster = yarp.AddCluster(passService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/.well-known/openid-configuration", passCluster); | ||||
|         yarp.AddRoute("/.well-known/jwks", passCluster); | ||||
|         yarp.AddRoute("/id/{**catch-all}", passCluster) | ||||
|             .WithTransformPathRemovePrefix("/id") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var driveCluster = yarp.AddCluster(driveService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/api/tus", driveCluster); | ||||
|         yarp.AddRoute("/drive/{**catch-all}", driveCluster) | ||||
|             .WithTransformPathRemovePrefix("/drive") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var sphereCluster = yarp.AddCluster(sphereService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/sphere/{**catch-all}", sphereCluster) | ||||
|             .WithTransformPathRemovePrefix("/sphere") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var developCluster = yarp.AddCluster(developService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/develop/{**catch-all}", developCluster) | ||||
|             .WithTransformPathRemovePrefix("/develop") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|     }); | ||||
| var gateway = builder.AddProject<Projects.DysonNetwork_Gateway>("gateway") | ||||
|     .WithEnvironment("HTTP_PORTS", "5001") | ||||
|     .WithHttpEndpoint(port: 5001, targetPort: null, isProxied: false, name: "http"); | ||||
|  | ||||
| if (isDev) gateway.WithHostPort(5001); | ||||
| foreach (var service in services) | ||||
|     gateway.WithReference(service); | ||||
|  | ||||
| builder.AddDockerComposeEnvironment("docker-compose"); | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <Sdk Name="Aspire.AppHost.Sdk" Version="9.4.2"/> | ||||
|  | ||||
|   <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.1" /> | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
| @@ -10,21 +8,18 @@ | ||||
|     <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId> | ||||
|     <RootNamespace>DysonNetwork.Control</RootNamespace> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|         <PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.2"/> | ||||
|     <PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" /> | ||||
|     <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Nats" Version="9.4.2" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Redis" Version="9.4.2" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Yarp" Version="9.4.2-preview.1.25428.12" /> | ||||
|     <PackageReference Include="Aspire.Hosting.Nats" Version="9.5.1" /> | ||||
|     <PackageReference Include="Aspire.Hosting.Redis" Version="9.5.1" /> | ||||
|   </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" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @@ -10,7 +10,9 @@ | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development", | ||||
|         "DOTNET_ENVIRONMENT": "Development", | ||||
|         "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175", | ||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189" | ||||
|         "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": { | ||||
| @@ -22,7 +24,8 @@ | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development", | ||||
|         "DOTNET_ENVIRONMENT": "Development", | ||||
|         "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163", | ||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185" | ||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185", | ||||
|         "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:22108" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
|  | ||||
| @@ -11,13 +9,13 @@ public class AppDatabase( | ||||
|     IConfiguration configuration | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<Developer> Developers { get; set; } = null!; | ||||
|     public DbSet<SnDeveloper> Developers { get; set; } = null!; | ||||
|  | ||||
|     public DbSet<DevProject> DevProjects { get; set; } = null!; | ||||
|     public DbSet<SnDevProject> DevProjects { get; set; } = null!; | ||||
|      | ||||
|     public DbSet<CustomApp> CustomApps { get; set; } = null!; | ||||
|     public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!; | ||||
|     public DbSet<BotAccount> BotAccounts { 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) | ||||
|     { | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||
|         <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" /> | ||||
|         <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"/> | ||||
| @@ -31,7 +31,6 @@ | ||||
|     </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|   </ItemGroup> | ||||
|   | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Grpc.Core; | ||||
| @@ -16,7 +16,7 @@ namespace DysonNetwork.Develop.Identity; | ||||
| [Authorize] | ||||
| public class BotAccountController( | ||||
|     BotAccountService botService, | ||||
|     DeveloperService developerService, | ||||
|     DeveloperService ds, | ||||
|     DevProjectService projectService, | ||||
|     ILogger<BotAccountController> logger, | ||||
|     AccountClientHelper accounts, | ||||
| @@ -83,12 +83,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Viewer)) | ||||
|         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); | ||||
| @@ -108,12 +108,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Viewer)) | ||||
|         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); | ||||
| @@ -137,12 +137,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -206,12 +206,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -267,12 +267,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -296,7 +296,7 @@ public class BotAccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/keys")] | ||||
|     public async Task<ActionResult<List<ApiKeyReference>>> ListBotKeys( | ||||
|     public async Task<ActionResult<List<SnApiKey>>> ListBotKeys( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId | ||||
| @@ -305,7 +305,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer); | ||||
|         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"); | ||||
| @@ -314,13 +314,13 @@ public class BotAccountController( | ||||
|         { | ||||
|             AutomatedId = bot.Id.ToString() | ||||
|         }); | ||||
|         var data = keys.Data.Select(ApiKeyReference.FromProtoValue).ToList(); | ||||
|         var data = keys.Data.Select(SnApiKey.FromProtoValue).ToList(); | ||||
|  | ||||
|         return Ok(data); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/keys/{keyId:guid}")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> GetBotKey( | ||||
|     public async Task<ActionResult<SnApiKey>> GetBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
| @@ -329,7 +329,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer); | ||||
|         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"); | ||||
| @@ -338,7 +338,7 @@ public class BotAccountController( | ||||
|         { | ||||
|             var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); | ||||
|             if (key == null) return NotFound("API key not found"); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(key)); | ||||
|             return Ok(SnApiKey.FromProtoValue(key)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) | ||||
|         { | ||||
| @@ -353,7 +353,7 @@ public class BotAccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{botId:guid}/keys")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> CreateBotKey( | ||||
|     public async Task<ActionResult<SnApiKey>> CreateBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
| @@ -362,7 +362,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         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"); | ||||
| @@ -376,7 +376,7 @@ public class BotAccountController( | ||||
|             }; | ||||
|  | ||||
|             var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(createdKey)); | ||||
|             return Ok(SnApiKey.FromProtoValue(createdKey)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument) | ||||
|         { | ||||
| @@ -385,7 +385,7 @@ public class BotAccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> RotateBotKey( | ||||
|     public async Task<ActionResult<SnApiKey>> RotateBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
| @@ -394,7 +394,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         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"); | ||||
| @@ -402,7 +402,7 @@ public class BotAccountController( | ||||
|         try | ||||
|         { | ||||
|             var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(rotatedKey)); | ||||
|             return Ok(SnApiKey.FromProtoValue(rotatedKey)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) | ||||
|         { | ||||
| @@ -420,7 +420,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         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"); | ||||
| @@ -436,17 +436,17 @@ public class BotAccountController( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task<(Developer?, DevProject?, BotAccount?)> ValidateBotAccess( | ||||
|     private async Task<(SnDeveloper?, SnDevProject?, SnBotAccount?)> ValidateBotAccess( | ||||
|         string pubName, | ||||
|         Guid projectId, | ||||
|         Guid botId, | ||||
|         Account currentUser, | ||||
|         PublisherMemberRole requiredRole) | ||||
|         Shared.Proto.PublisherMemberRole requiredRole) | ||||
|     { | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer == null) return (null, null, null); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole)) | ||||
|             return (null, null, null); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
| @@ -7,7 +8,7 @@ namespace DysonNetwork.Develop.Identity; | ||||
| public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase | ||||
| { | ||||
|     [HttpGet("{botId:guid}")] | ||||
|     public async Task<ActionResult<BotAccount>> GetBotTransparentInfo([FromRoute] Guid botId) | ||||
|     public async Task<ActionResult<SnBotAccount>> GetBotTransparentInfo([FromRoute] Guid botId) | ||||
|     { | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null) return NotFound("Bot not found"); | ||||
| @@ -21,7 +22,7 @@ public class BotAccountPublicController(BotAccountService botService, DeveloperS | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/developer")] | ||||
|     public async Task<ActionResult<Developer>> GetBotDeveloper([FromRoute] Guid botId) | ||||
|     public async Task<ActionResult<SnDeveloper>> GetBotDeveloper([FromRoute] Guid botId) | ||||
|     { | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null) return NotFound("Bot not found"); | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Grpc.Core; | ||||
| @@ -14,22 +13,22 @@ public class BotAccountService( | ||||
|     AccountClientHelper accounts | ||||
| ) | ||||
| { | ||||
|     public async Task<BotAccount?> GetBotByIdAsync(Guid id) | ||||
|     public async Task<SnBotAccount?> GetBotByIdAsync(Guid id) | ||||
|     { | ||||
|         return await db.BotAccounts | ||||
|             .Include(b => b.Project) | ||||
|             .FirstOrDefaultAsync(b => b.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<IEnumerable<BotAccount>> GetBotsByProjectAsync(Guid projectId) | ||||
|     public async Task<List<SnBotAccount>> GetBotsByProjectAsync(Guid projectId) | ||||
|     { | ||||
|         return await db.BotAccounts | ||||
|             .Where(b => b.ProjectId == projectId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount> CreateBotAsync( | ||||
|         DevProject project, | ||||
|     public async Task<SnBotAccount> CreateBotAsync( | ||||
|         SnDevProject project, | ||||
|         string slug, | ||||
|         Account account, | ||||
|         string? pictureId, | ||||
| @@ -58,7 +57,7 @@ public class BotAccountService( | ||||
|             var botAccount = createResponse.Bot; | ||||
|  | ||||
|             // Then create the local bot account | ||||
|             var bot = new BotAccount | ||||
|             var bot = new SnBotAccount | ||||
|             { | ||||
|                 Id = automatedId, | ||||
|                 Slug = slug, | ||||
| @@ -89,8 +88,8 @@ public class BotAccountService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount> UpdateBotAsync( | ||||
|         BotAccount bot, | ||||
|     public async Task<SnBotAccount> UpdateBotAsync( | ||||
|         SnBotAccount bot, | ||||
|         Account account, | ||||
|         string? pictureId, | ||||
|         string? backgroundId | ||||
| @@ -130,7 +129,7 @@ public class BotAccountService( | ||||
|         return bot; | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteBotAsync(BotAccount bot) | ||||
|     public async Task DeleteBotAsync(SnBotAccount bot) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
| @@ -153,22 +152,21 @@ public class BotAccountService( | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount?> LoadBotAccountAsync(BotAccount bot) => | ||||
|     public async Task<SnBotAccount?> LoadBotAccountAsync(SnBotAccount bot) => | ||||
|         (await LoadBotsAccountAsync([bot])).FirstOrDefault(); | ||||
|  | ||||
|     public async Task<List<BotAccount>> LoadBotsAccountAsync(IEnumerable<BotAccount> bots) | ||||
|     public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots) | ||||
|     { | ||||
|         bots = bots.ToList(); | ||||
|         var automatedIds = bots.Select(b => b.Id).ToList(); | ||||
|         var data = await accounts.GetBotAccountBatch(automatedIds); | ||||
|  | ||||
|         foreach (var bot in bots) | ||||
|         { | ||||
|             bot.Account = data | ||||
|                 .Select(AccountReference.FromProtoValue) | ||||
|                 .Select(SnAccount.FromProtoValue) | ||||
|                 .FirstOrDefault(e => e.AutomatedId == bot.Id); | ||||
|         } | ||||
|  | ||||
|         return bots as List<BotAccount> ?? []; | ||||
|         return bots; | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| @@ -18,9 +19,9 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         [MaxLength(4096)] string? Description, | ||||
|         string? PictureId, | ||||
|         string? BackgroundId, | ||||
|         CustomAppStatus? Status, | ||||
|         CustomAppLinks? Links, | ||||
|         CustomAppOauthConfig? OauthConfig | ||||
|         Shared.Models.CustomAppStatus? Status, | ||||
|         SnCustomAppLinks? Links, | ||||
|         SnCustomAppOauthConfig? OauthConfig | ||||
|     ); | ||||
|  | ||||
|     public record CreateSecretRequest( | ||||
| @@ -50,7 +51,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) return NotFound(); | ||||
|  | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer)) | ||||
|         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); | ||||
| @@ -72,7 +73,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) return NotFound(); | ||||
|          | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer)) | ||||
|         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); | ||||
| @@ -99,7 +100,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -143,7 +144,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -180,7 +181,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -212,7 +213,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -250,7 +251,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -263,7 +264,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var secret = await customApps.CreateAppSecretAsync(new CustomAppSecret | ||||
|             var secret = await customApps.CreateAppSecretAsync(new SnCustomAppSecret | ||||
|             { | ||||
|                 AppId = appId, | ||||
|                 Description = request.Description, | ||||
| @@ -309,7 +310,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -350,7 +351,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -388,7 +389,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         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); | ||||
| @@ -401,7 +402,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var secret = await customApps.RotateAppSecretAsync(new CustomAppSecret | ||||
|             var secret = await customApps.RotateAppSecretAsync(new SnCustomAppSecret | ||||
|             { | ||||
|                 Id = secretId, | ||||
|                 AppId = appId, | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using System.Security.Cryptography; | ||||
| @@ -13,7 +12,7 @@ public class CustomAppService( | ||||
|     FileService.FileServiceClient files | ||||
| ) | ||||
| { | ||||
|     public async Task<CustomApp?> CreateAppAsync( | ||||
|     public async Task<SnCustomApp?> CreateAppAsync( | ||||
|         Guid projectId, | ||||
|         CustomAppController.CustomAppRequest request | ||||
|     ) | ||||
| @@ -25,12 +24,12 @@ public class CustomAppService( | ||||
|         if (project == null) | ||||
|             return null; | ||||
|              | ||||
|         var app = new CustomApp | ||||
|         var app = new SnCustomApp | ||||
|         { | ||||
|             Slug = request.Slug!, | ||||
|             Name = request.Name!, | ||||
|             Description = request.Description, | ||||
|             Status = request.Status ?? CustomAppStatus.Developing, | ||||
|             Status = request.Status ?? Shared.Models.CustomAppStatus.Developing, | ||||
|             Links = request.Links, | ||||
|             OauthConfig = request.OauthConfig, | ||||
|             ProjectId = projectId | ||||
| @@ -46,7 +45,7 @@ public class CustomAppService( | ||||
|             ); | ||||
|             if (picture is null) | ||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); | ||||
|             app.Picture = CloudFileReferenceObject.FromProtoValue(picture); | ||||
|             app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture); | ||||
|  | ||||
|             // Create a new reference | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
| @@ -65,7 +64,7 @@ public class CustomAppService( | ||||
|             ); | ||||
|             if (background is null) | ||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); | ||||
|             app.Background = CloudFileReferenceObject.FromProtoValue(background); | ||||
|             app.Background = SnCloudFileReferenceObject.FromProtoValue(background); | ||||
|  | ||||
|             // Create a new reference | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
| @@ -84,7 +83,7 @@ public class CustomAppService( | ||||
|         return app; | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomApp?> GetAppAsync(Guid id, Guid? projectId = null) | ||||
|     public async Task<SnCustomApp?> GetAppAsync(Guid id, Guid? projectId = null) | ||||
|     { | ||||
|         var query = db.CustomApps.AsQueryable(); | ||||
|          | ||||
| @@ -96,7 +95,7 @@ public class CustomAppService( | ||||
|         return await query.FirstOrDefaultAsync(a => a.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<List<CustomAppSecret>> GetAppSecretsAsync(Guid appId) | ||||
|     public async Task<List<SnCustomAppSecret>> GetAppSecretsAsync(Guid appId) | ||||
|     { | ||||
|         return await db.CustomAppSecrets | ||||
|             .Where(s => s.AppId == appId) | ||||
| @@ -104,13 +103,13 @@ public class CustomAppService( | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId) | ||||
|     public async Task<SnCustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId) | ||||
|     { | ||||
|         return await db.CustomAppSecrets | ||||
|             .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId); | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomAppSecret> CreateAppSecretAsync(CustomAppSecret secret) | ||||
|     public async Task<SnCustomAppSecret> CreateAppSecretAsync(SnCustomAppSecret secret) | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(secret.Secret)) | ||||
|         { | ||||
| @@ -141,7 +140,7 @@ public class CustomAppService( | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomAppSecret> RotateAppSecretAsync(CustomAppSecret secretUpdate) | ||||
|     public async Task<SnCustomAppSecret> RotateAppSecretAsync(SnCustomAppSecret secretUpdate) | ||||
|     { | ||||
|         var existingSecret = await db.CustomAppSecrets | ||||
|             .FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId); | ||||
| @@ -177,14 +176,14 @@ public class CustomAppService( | ||||
|         return res.ToString(); | ||||
|     } | ||||
|  | ||||
|     public async Task<List<CustomApp>> GetAppsByProjectAsync(Guid projectId) | ||||
|     public async Task<List<SnCustomApp>> GetAppsByProjectAsync(Guid projectId) | ||||
|     { | ||||
|         return await db.CustomApps | ||||
|             .Where(a => a.ProjectId == projectId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request) | ||||
|     public async Task<SnCustomApp?> UpdateAppAsync(SnCustomApp app, CustomAppController.CustomAppRequest request) | ||||
|     { | ||||
|         if (request.Slug is not null) | ||||
|             app.Slug = request.Slug; | ||||
| @@ -209,7 +208,7 @@ public class CustomAppService( | ||||
|             ); | ||||
|             if (picture is null) | ||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); | ||||
|             app.Picture = CloudFileReferenceObject.FromProtoValue(picture); | ||||
|             app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture); | ||||
|  | ||||
|             // Create a new reference | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
| @@ -228,7 +227,7 @@ public class CustomAppService( | ||||
|             ); | ||||
|             if (background is null) | ||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); | ||||
|             app.Background = CloudFileReferenceObject.FromProtoValue(background); | ||||
|             app.Background = SnCloudFileReferenceObject.FromProtoValue(background); | ||||
|  | ||||
|             // Create a new reference | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -37,7 +38,7 @@ public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppServic | ||||
|         if (string.IsNullOrEmpty(request.Secret)) | ||||
|             throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required")); | ||||
|  | ||||
|         IQueryable<CustomAppSecret> q = db.CustomAppSecrets; | ||||
|         IQueryable<SnCustomAppSecret> q = db.CustomAppSecrets; | ||||
|         switch (request.SecretIdentifierCase) | ||||
|         { | ||||
|             case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId: | ||||
|   | ||||
| @@ -1,79 +0,0 @@ | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using VerificationMark = DysonNetwork.Shared.Data.VerificationMark; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| public class Developer | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     public Guid PublisherId { get; set; } | ||||
|      | ||||
|     [JsonIgnore] public List<DevProject> Projects { get; set; } = []; | ||||
|      | ||||
|     [NotMapped] public PublisherInfo? Publisher { get; set; } | ||||
| } | ||||
|  | ||||
| public class PublisherInfo | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     public PublisherType Type { get; set; } | ||||
|     public string Name { get; set; } = string.Empty; | ||||
|     public string Nick { get; set; } = string.Empty; | ||||
|     public string? Bio { get; set; } | ||||
|  | ||||
|     public CloudFileReferenceObject? Picture { get; set; } | ||||
|     public CloudFileReferenceObject? Background { get; set; } | ||||
|  | ||||
|     public VerificationMark? Verification { get; set; } | ||||
|     public Guid? AccountId { get; set; } | ||||
|     public Guid? RealmId { get; set; } | ||||
|  | ||||
|     public static PublisherInfo FromProto(Publisher proto) | ||||
|     { | ||||
|         var info = new PublisherInfo | ||||
|         { | ||||
|             Id = Guid.Parse(proto.Id), | ||||
|             Type = proto.Type == PublisherType.PubIndividual | ||||
|                 ? PublisherType.PubIndividual | ||||
|                 : PublisherType.PubOrganizational, | ||||
|             Name = proto.Name, | ||||
|             Nick = proto.Nick, | ||||
|             Bio = string.IsNullOrEmpty(proto.Bio) ? null : proto.Bio, | ||||
|             Verification = proto.VerificationMark is not null | ||||
|                 ? VerificationMark.FromProtoValue(proto.VerificationMark) | ||||
|                 : null, | ||||
|             AccountId = string.IsNullOrEmpty(proto.AccountId) ? null : Guid.Parse(proto.AccountId), | ||||
|             RealmId = string.IsNullOrEmpty(proto.RealmId) ? null : Guid.Parse(proto.RealmId) | ||||
|         }; | ||||
|  | ||||
|         if (proto.Picture != null) | ||||
|         { | ||||
|             info.Picture = new CloudFileReferenceObject | ||||
|             { | ||||
|                 Id = proto.Picture.Id, | ||||
|                 Name = proto.Picture.Name, | ||||
|                 MimeType = proto.Picture.MimeType, | ||||
|                 Hash = proto.Picture.Hash, | ||||
|                 Size = proto.Picture.Size | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         if (proto.Background != null) | ||||
|         { | ||||
|             info.Background = new CloudFileReferenceObject | ||||
|             { | ||||
|                 Id = proto.Background.Id, | ||||
|                 Name = proto.Background.Name, | ||||
|                 MimeType = proto.Background.MimeType, | ||||
|                 Hash = proto.Background.Hash, | ||||
|                 Size = (long)proto.Background.Size | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return info; | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| @@ -18,7 +19,7 @@ public class DeveloperController( | ||||
|     : ControllerBase | ||||
| { | ||||
|     [HttpGet("{name}")] | ||||
|     public async Task<ActionResult<Developer>> GetDeveloper(string name) | ||||
|     public async Task<ActionResult<SnDeveloper>> GetDeveloper(string name) | ||||
|     { | ||||
|         var developer = await ds.GetDeveloperByName(name); | ||||
|         if (developer is null) return NotFound(); | ||||
| @@ -47,7 +48,7 @@ public class DeveloperController( | ||||
|  | ||||
|     [HttpGet] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Developer>>> ListJoinedDevelopers() | ||||
|     public async Task<ActionResult<List<SnDeveloper>>> ListJoinedDevelopers() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|          | ||||
| @@ -69,16 +70,16 @@ public class DeveloperController( | ||||
|     [HttpPost("{name}/enroll")] | ||||
|     [Authorize] | ||||
|     [RequiredPermission("global", "developers.create")] | ||||
|     public async Task<ActionResult<Developer>> EnrollDeveloperProgram(string name) | ||||
|     public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|  | ||||
|         PublisherInfo? pub; | ||||
|         SnPublisher? pub; | ||||
|         try | ||||
|         { | ||||
|             var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name }); | ||||
|             pub = PublisherInfo.FromProto(pubResponse.Publisher); | ||||
|             pub = SnPublisher.FromProto(pubResponse.Publisher); | ||||
|         } catch (RpcException ex) | ||||
|         { | ||||
|             return NotFound(ex.Status.Detail); | ||||
| @@ -89,14 +90,14 @@ public class DeveloperController( | ||||
|         { | ||||
|             PublisherId = pub.Id.ToString(), | ||||
|             AccountId = currentUser.Id, | ||||
|             Role = PublisherMemberRole.Owner | ||||
|             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 Developer | ||||
|         var developer = new SnDeveloper | ||||
|         { | ||||
|             Id = Guid.NewGuid(), | ||||
|             PublisherId = pub.Id | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -9,22 +10,22 @@ public class DeveloperService( | ||||
|     PublisherService.PublisherServiceClient ps, | ||||
|     ILogger<DeveloperService> logger) | ||||
| { | ||||
|     public async Task<Developer> LoadDeveloperPublisher(Developer developer) | ||||
|     public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer) | ||||
|     { | ||||
|         var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() }); | ||||
|         developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher); | ||||
|         developer.Publisher = SnPublisher.FromProto(pubResponse.Publisher); | ||||
|         return developer; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public async Task<IEnumerable<Developer>> LoadDeveloperPublisher(IEnumerable<Developer> developers) | ||||
|     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), PublisherInfo.FromProto); | ||||
|         var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProto); | ||||
|  | ||||
|         return enumerable.Select(d => | ||||
|         { | ||||
| @@ -33,7 +34,7 @@ public class DeveloperService( | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public async Task<Developer?> GetDeveloperByName(string name) | ||||
|     public async Task<SnDeveloper?> GetDeveloperByName(string name) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
| @@ -50,12 +51,12 @@ public class DeveloperService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<Developer?> GetDeveloperById(Guid id) | ||||
|     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, PublisherMemberRole role) | ||||
|     public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, Shared.Proto.PublisherMemberRole role) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| @@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
| @@ -56,7 +55,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("developer_id"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                     b.Property<SnCustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
| @@ -66,11 +65,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
| @@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                     b.Property<SnVerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| @@ -35,11 +33,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                     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<CloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||
|                     background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||
|                     verification = table.Column<VerificationMark>(type: "jsonb", nullable: true), | ||||
|                     oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true), | ||||
|                     links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true), | ||||
|                     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), | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| @@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
| @@ -52,7 +51,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                     b.Property<SnCustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
| @@ -62,11 +61,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
| @@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                     b.Property<SnVerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| @@ -77,7 +76,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
| @@ -94,7 +93,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                     b.Property<SnCustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
| @@ -104,11 +103,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
| @@ -130,7 +129,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                     b.Property<SnVerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| @@ -74,7 +73,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
| @@ -91,7 +90,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                     b.Property<SnCustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
| @@ -101,11 +100,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
| @@ -127,7 +126,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                     b.Property<SnVerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|   | ||||
| @@ -13,12 +13,16 @@ builder.ConfigureAppKestrel(builder.Configuration); | ||||
|  | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddDysonAuth(); | ||||
| builder.Services.AddPublisherService(); | ||||
| builder.Services.AddAccountService(); | ||||
| builder.Services.AddDriveService(); | ||||
|  | ||||
| builder.AddSwaggerManifest( | ||||
|     "DysonNetwork.Develop", | ||||
|     "The developer portal in the Solar Network." | ||||
| ); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
| @@ -31,4 +35,6 @@ using (var scope = app.Services.CreateScope()) | ||||
|  | ||||
| app.ConfigureAppMiddleware(builder.Configuration); | ||||
|  | ||||
| app.UseSwaggerManifest(); | ||||
|  | ||||
| app.Run(); | ||||
| @@ -1,6 +1,6 @@ | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Project; | ||||
|  | ||||
| @@ -10,12 +10,12 @@ public class DevProjectService( | ||||
|     FileService.FileServiceClient files | ||||
| ) | ||||
| { | ||||
|     public async Task<DevProject> CreateProjectAsync( | ||||
|         Developer developer, | ||||
|     public async Task<SnDevProject> CreateProjectAsync( | ||||
|         SnDeveloper developer, | ||||
|         DevProjectController.DevProjectRequest request | ||||
|     ) | ||||
|     { | ||||
|         var project = new DevProject | ||||
|         var project = new SnDevProject | ||||
|         { | ||||
|             Slug = request.Slug!, | ||||
|             Name = request.Name!, | ||||
| @@ -29,7 +29,7 @@ public class DevProjectService( | ||||
|         return project; | ||||
|     } | ||||
|  | ||||
|     public async Task<DevProject?> GetProjectAsync(Guid id, Guid? developerId = null) | ||||
|     public async Task<SnDevProject?> GetProjectAsync(Guid id, Guid? developerId = null) | ||||
|     { | ||||
|         var query = db.DevProjects.AsQueryable(); | ||||
|          | ||||
| @@ -41,14 +41,14 @@ public class DevProjectService( | ||||
|         return await query.FirstOrDefaultAsync(p => p.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<List<DevProject>> GetProjectsByDeveloperAsync(Guid developerId) | ||||
|     public async Task<List<SnDevProject>> GetProjectsByDeveloperAsync(Guid developerId) | ||||
|     { | ||||
|         return await db.DevProjects | ||||
|             .Where(p => p.DeveloperId == developerId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<DevProject?> UpdateProjectAsync( | ||||
|     public async Task<SnDevProject?> UpdateProjectAsync( | ||||
|         Guid id, | ||||
|         Guid developerId, | ||||
|         DevProjectController.DevProjectRequest request | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| using System.Net; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
| using Prometheus; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Startup; | ||||
| @@ -14,9 +12,6 @@ public static class ApplicationConfiguration | ||||
|         app.MapMetrics(); | ||||
|         app.MapOpenApi(); | ||||
|  | ||||
|         app.UseSwagger(); | ||||
|         app.UseSwaggerUI(); | ||||
|          | ||||
|         app.UseRequestLocalization(); | ||||
|  | ||||
|         app.ConfigureForwardedHeaders(configuration); | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System.Globalization; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using System.Text.Json; | ||||
| @@ -7,7 +6,6 @@ using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using StackExchange.Redis; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Startup; | ||||
|  | ||||
| @@ -57,23 +55,7 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddCors(); | ||||
|         services.AddAuthorization(); | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppSwagger(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddEndpointsApiExplorer(); | ||||
|         services.AddSwaggerGen(options => | ||||
|         { | ||||
|             options.SwaggerDoc("v1", new OpenApiInfo | ||||
|             { | ||||
|                 Version = "v1", | ||||
|                 Title = "Develop API", | ||||
|             }); | ||||
|         }); | ||||
|         services.AddOpenApi(); | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -10,12 +10,12 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" | ||||
|     "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" | ||||
|   }, | ||||
|   "KnownProxies": [ | ||||
|     "127.0.0.1", | ||||
|     "::1" | ||||
|   ], | ||||
|   "Etcd": { | ||||
|     "Insecure": true | ||||
|   }, | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| using DysonNetwork.Drive.Billing; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Microsoft.EntityFrameworkCore.Query; | ||||
| @@ -17,11 +16,11 @@ public class AppDatabase( | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<FilePool> Pools { get; set; } = null!; | ||||
|     public DbSet<FileBundle> Bundles { get; set; } = null!; | ||||
|     public DbSet<SnFileBundle> Bundles { get; set; } = null!; | ||||
|      | ||||
|     public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!; | ||||
|      | ||||
|     public DbSet<CloudFile> Files { get; set; } = null!; | ||||
|     public DbSet<SnCloudFile> Files { get; set; } = null!; | ||||
|     public DbSet<CloudFileReference> FileReferences { get; set; } = null!; | ||||
|      | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Billing; | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|           <PrivateAssets>all</PrivateAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="MimeKit" Version="4.13.0" /> | ||||
|         <PackageReference Include="MimeTypes" Version="2.5.2"> | ||||
|           <PrivateAssets>all</PrivateAssets> | ||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
| @@ -55,8 +56,8 @@ | ||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" /> | ||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" /> | ||||
|         <PackageReference Include="tusdotnet" Version="2.10.0" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| @@ -67,7 +68,6 @@ | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|     </ItemGroup> | ||||
| </Project> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,7 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
|   | ||||
| @@ -18,21 +18,20 @@ builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxV | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppRateLimiting(); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddDysonAuth(); | ||||
| builder.Services.AddAccountService(); | ||||
|  | ||||
| builder.Services.AddAppFileStorage(builder.Configuration); | ||||
|  | ||||
| // Add flush handlers and websocket handlers | ||||
| builder.Services.AddAppFlushHandlers(); | ||||
|  | ||||
| // Add business services | ||||
| builder.Services.AddAppBusinessServices(); | ||||
|  | ||||
| // Add scheduled jobs | ||||
| builder.Services.AddAppScheduledJobs(); | ||||
|  | ||||
| builder.AddSwaggerManifest( | ||||
|     "DysonNetwork.Drive", | ||||
|     "The file upload and storage service in the Solar Network." | ||||
| ); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
| @@ -50,4 +49,6 @@ app.ConfigureAppMiddleware(tusDiskStore); | ||||
| // Configure gRPC | ||||
| app.ConfigureGrpcServices(); | ||||
|  | ||||
| app.UseSwaggerManifest(); | ||||
|  | ||||
| app.Run(); | ||||
|   | ||||
| @@ -8,13 +8,6 @@ public static class ApplicationBuilderExtensions | ||||
| { | ||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore) | ||||
|     { | ||||
|         // Configure the HTTP request pipeline. | ||||
|         if (app.Environment.IsDevelopment()) | ||||
|         { | ||||
|             app.UseSwagger(); | ||||
|             app.UseSwaggerUI(); | ||||
|         } | ||||
|  | ||||
|         app.UseAuthorization(); | ||||
|         app.MapControllers(); | ||||
|  | ||||
|   | ||||
| @@ -28,6 +28,8 @@ public class BroadcastEventHandler( | ||||
|     private static readonly string[] AnimatedImageExtensions = | ||||
|         [".gif", ".apng", ".avif"]; | ||||
|  | ||||
|  | ||||
|  | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
| @@ -39,7 +41,7 @@ public class BroadcastEventHandler( | ||||
|          | ||||
|         await js.EnsureStreamCreated("file_events", [FileUploadedEvent.Type]); | ||||
|         var fileUploadedConsumer = await js.CreateOrUpdateConsumerAsync("file_events", | ||||
|             new ConsumerConfig("drive_file_uploaded_handler"), cancellationToken: stoppingToken); | ||||
|             new ConsumerConfig("drive_file_uploaded_handler") { MaxDeliver = 3 }, cancellationToken: stoppingToken); | ||||
|  | ||||
|         var accountDeletedTask = HandleAccountDeleted(accountEventConsumer, stoppingToken); | ||||
|         var fileUploadedTask = HandleFileUploaded(fileUploadedConsumer, stoppingToken); | ||||
| @@ -73,8 +75,8 @@ public class BroadcastEventHandler( | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Error processing FileUploadedEvent for file {FileId}", payload?.FileId); | ||||
|                 await msg.NakAsync(cancellationToken: stoppingToken); | ||||
|                 logger.LogError(ex, "Error processing FileUploadedEvent for file {FileId}", payload.FileId); | ||||
|                 await msg.NakAsync(cancellationToken: stoppingToken, delay: TimeSpan.FromSeconds(60)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -149,8 +151,6 @@ public class BroadcastEventHandler( | ||||
|         var hasCompression = false; | ||||
|         var hasThumbnail = false; | ||||
|  | ||||
|         try | ||||
|         { | ||||
|         logger.LogInformation("Processing file {FileId} in background...", fileId); | ||||
|  | ||||
|         var fileToUpdate = await scopedDb.Files.AsNoTracking().FirstAsync(f => f.Id == fileId); | ||||
| @@ -172,9 +172,10 @@ public class BroadcastEventHandler( | ||||
|                         break; | ||||
|                     } | ||||
|  | ||||
|                         newMimeType = "image/webp"; | ||||
|                         using (var vipsImage = Image.NewFromFile(processingFilePath)) | ||||
|                     try | ||||
|                     { | ||||
|                         newMimeType = "image/webp"; | ||||
|                         using var vipsImage = Image.NewFromFile(processingFilePath); | ||||
|                         var imageToWrite = vipsImage; | ||||
|  | ||||
|                         if (vipsImage.Interpretation is Enums.Interpretation.Scrgb or Enums.Interpretation.Xyz) | ||||
| @@ -204,6 +205,12 @@ public class BroadcastEventHandler( | ||||
|                             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; | ||||
|  | ||||
| @@ -246,7 +253,10 @@ public class BroadcastEventHandler( | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|             else uploads.Add((processingFilePath, string.Empty, contentType, false)); | ||||
|         else | ||||
|         { | ||||
|             uploads.Add((processingFilePath, string.Empty, contentType, false)); | ||||
|         } | ||||
|  | ||||
|         logger.LogInformation("Optimized file {FileId}, now uploading...", fileId); | ||||
|  | ||||
| @@ -276,19 +286,12 @@ public class BroadcastEventHandler( | ||||
|                 .SetProperty(f => f.HasCompression, hasCompression) | ||||
|                 .SetProperty(f => f.HasThumbnail, hasThumbnail) | ||||
|             ); | ||||
|             } | ||||
|         } | ||||
|         catch (Exception err) | ||||
|         { | ||||
|             logger.LogError(err, "Failed to process and upload {FileId}", fileId); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|  | ||||
|             // Only delete temp file after successful upload and db update | ||||
|             if (isTempFile) | ||||
|             { | ||||
|                 File.Delete(processingFilePath); | ||||
|         } | ||||
|  | ||||
|         await fs._PurgeCacheAsync(fileId); | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -3,11 +3,8 @@ using System.Text.Json.Serialization; | ||||
| using System.Threading.RateLimiting; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.AspNetCore.RateLimiting; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using StackExchange.Redis; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using tusdotnet.Stores; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
| @@ -61,9 +58,7 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddCors(); | ||||
|         services.AddAuthorization(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
| @@ -74,52 +69,6 @@ public static class ServiceCollectionExtensions | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppSwagger(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddEndpointsApiExplorer(); | ||||
|         services.AddSwaggerGen(options => | ||||
|         { | ||||
|             options.SwaggerDoc("v1", new OpenApiInfo | ||||
|             { | ||||
|                 Version = "v1", | ||||
|                 Title = "Dyson Drive", | ||||
|                 Description = | ||||
|                     "The file service of the Dyson Network. Mainly handling file storage and sharing. Also provide image processing and media analysis. Powered the Solar Network Drive as well.", | ||||
|                 TermsOfService = new Uri("https://solsynth.dev/terms"), // Update with actual terms | ||||
|                 License = new OpenApiLicense | ||||
|                 { | ||||
|                     Name = "APGLv3", // Update with actual license | ||||
|                     Url = new Uri("https://www.gnu.org/licenses/agpl-3.0.html") | ||||
|                 } | ||||
|             }); | ||||
|             options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme | ||||
|             { | ||||
|                 In = ParameterLocation.Header, | ||||
|                 Description = "Please enter a valid token", | ||||
|                 Name = "Authorization", | ||||
|                 Type = SecuritySchemeType.Http, | ||||
|                 BearerFormat = "JWT", | ||||
|                 Scheme = "Bearer" | ||||
|             }); | ||||
|             options.AddSecurityRequirement(new OpenApiSecurityRequirement | ||||
|             { | ||||
|                 { | ||||
|                     new OpenApiSecurityScheme | ||||
|                     { | ||||
|                         Reference = new OpenApiReference | ||||
|                         { | ||||
|                             Type = ReferenceType.SecurityScheme, | ||||
|                             Id = "Bearer" | ||||
|                         } | ||||
|                     }, | ||||
|                     [] | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|      | ||||
|     public static IServiceCollection AddAppFileStorage(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         var tusStorePath = configuration.GetSection("Tus").GetValue<string>("StorePath")!; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| @@ -22,7 +23,7 @@ public class BundleController(AppDatabase db) : ControllerBase | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{id:guid}")] | ||||
|     public async Task<ActionResult<FileBundle>> GetBundle([FromRoute] Guid id, [FromQuery] string? passcode) | ||||
|     public async Task<ActionResult<SnFileBundle>> GetBundle([FromRoute] Guid id, [FromQuery] string? passcode) | ||||
|     { | ||||
|         var bundle = await db.Bundles | ||||
|             .Where(e => e.Id == id) | ||||
| @@ -36,7 +37,7 @@ public class BundleController(AppDatabase db) : ControllerBase | ||||
|  | ||||
|     [HttpGet("me")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<FileBundle>>> ListBundles( | ||||
|     public async Task<ActionResult<List<SnFileBundle>>> ListBundles( | ||||
|         [FromQuery] string? term, | ||||
|         [FromQuery] int offset = 0, | ||||
|         [FromQuery] int take = 20 | ||||
| @@ -65,7 +66,7 @@ public class BundleController(AppDatabase db) : ControllerBase | ||||
|  | ||||
|     [HttpPost] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<FileBundle>> CreateBundle([FromBody] BundleRequest request) | ||||
|     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); | ||||
| @@ -77,7 +78,7 @@ public class BundleController(AppDatabase db) : ControllerBase | ||||
|         if (string.IsNullOrEmpty(request.Name)) | ||||
|             request.Name = "Unnamed Bundle"; | ||||
|  | ||||
|         var bundle = new FileBundle | ||||
|         var bundle = new SnFileBundle | ||||
|         { | ||||
|             Slug = request.Slug, | ||||
|             Name = request.Name, | ||||
| @@ -95,7 +96,7 @@ public class BundleController(AppDatabase db) : ControllerBase | ||||
|  | ||||
|     [HttpPut("{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<FileBundle>> UpdateBundle([FromRoute] Guid id, [FromBody] BundleRequest request) | ||||
|     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); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| using DysonNetwork.Drive.Billing; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| @@ -70,13 +70,11 @@ public class FileController( | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return StatusCode(StatusCodes.Status503ServiceUnavailable, "File is being processed. Please try again later."); | ||||
|             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) | ||||
| @@ -165,7 +163,7 @@ public class FileController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{id}/info")] | ||||
|     public async Task<ActionResult<CloudFile>> GetFileInfo(string id) | ||||
|     public async Task<ActionResult<SnCloudFile>> GetFileInfo(string id) | ||||
|     { | ||||
|         var file = await fs.GetFileAsync(id); | ||||
|         if (file is null) return NotFound("File not found."); | ||||
| @@ -175,7 +173,7 @@ public class FileController( | ||||
|  | ||||
|     [Authorize] | ||||
|     [HttpPatch("{id}/name")] | ||||
|     public async Task<ActionResult<CloudFile>> UpdateFileName(string id, [FromBody] string 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); | ||||
| @@ -194,7 +192,7 @@ public class FileController( | ||||
|  | ||||
|     [Authorize] | ||||
|     [HttpPut("{id}/marks")] | ||||
|     public async Task<ActionResult<CloudFile>> MarkFile(string id, [FromBody] MarkFileRequest request) | ||||
|     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); | ||||
| @@ -208,7 +206,7 @@ public class FileController( | ||||
|  | ||||
|     [Authorize] | ||||
|     [HttpPut("{id}/meta")] | ||||
|     public async Task<ActionResult<CloudFile>> UpdateFileMeta(string id, [FromBody] Dictionary<string, object?> 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); | ||||
| @@ -222,7 +220,7 @@ public class FileController( | ||||
|  | ||||
|     [Authorize] | ||||
|     [HttpGet("me")] | ||||
|     public async Task<ActionResult<List<CloudFile>>> GetMyFiles( | ||||
|     public async Task<ActionResult<List<SnCloudFile>>> GetMyFiles( | ||||
|         [FromQuery] Guid? pool, | ||||
|         [FromQuery] bool recycled = false, | ||||
|         [FromQuery] int offset = 0, | ||||
| @@ -307,7 +305,7 @@ public class FileController( | ||||
|     [Authorize] | ||||
|     [HttpPost("fast")] | ||||
|     [RequiredPermission("global", "files.create")] | ||||
|     public async Task<ActionResult<CloudFile>> CreateFastFile([FromBody] CreateFastFileRequest request) | ||||
|     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); | ||||
| @@ -368,7 +366,7 @@ public class FileController( | ||||
|         await using var transaction = await db.Database.BeginTransactionAsync(); | ||||
|         try | ||||
|         { | ||||
|             var file = new CloudFile | ||||
|             var file = new SnCloudFile | ||||
|             { | ||||
|                 Name = request.Name, | ||||
|                 Size = request.Size, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| @@ -19,6 +20,7 @@ public class FilePoolController(AppDatabase db, FileService fs) : ControllerBase | ||||
|         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 => | ||||
|         { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using EFCore.BulkExtensions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| @@ -347,7 +348,7 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach | ||||
|     /// <param name="resourceId">The ID of the resource</param> | ||||
|     /// <param name="usage">Optional filter by usage context</param> | ||||
|     /// <returns>A list of files referenced by the resource</returns> | ||||
|     public async Task<List<CloudFile>> GetResourceFilesAsync(string resourceId, string? usage = null) | ||||
|     public async Task<List<SnCloudFile>> GetResourceFilesAsync(string resourceId, string? usage = null) | ||||
|     { | ||||
|         var query = db.FileReferences.Where(r => r.ResourceId == resourceId); | ||||
|  | ||||
|   | ||||
| @@ -12,9 +12,9 @@ using NATS.Client.Core; | ||||
| using NetVips; | ||||
| using NodaTime; | ||||
| using System.Linq.Expressions; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore.Query; | ||||
| using NATS.Net; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
|  | ||||
| @@ -28,11 +28,11 @@ public class FileService( | ||||
|     private const string CacheKeyPrefix = "file:"; | ||||
|     private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15); | ||||
|  | ||||
|     public async Task<CloudFile?> GetFileAsync(string fileId) | ||||
|     public async Task<SnCloudFile?> GetFileAsync(string fileId) | ||||
|     { | ||||
|         var cacheKey = $"{CacheKeyPrefix}{fileId}"; | ||||
|  | ||||
|         var cachedFile = await cache.GetAsync<CloudFile>(cacheKey); | ||||
|         var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey); | ||||
|         if (cachedFile is not null) | ||||
|             return cachedFile; | ||||
|  | ||||
| @@ -48,15 +48,15 @@ public class FileService( | ||||
|         return file; | ||||
|     } | ||||
|  | ||||
|     public async Task<List<CloudFile>> GetFilesAsync(List<string> fileIds) | ||||
|     public async Task<List<SnCloudFile>> GetFilesAsync(List<string> fileIds) | ||||
|     { | ||||
|         var cachedFiles = new Dictionary<string, CloudFile>(); | ||||
|         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<CloudFile>(cacheKey); | ||||
|             var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey); | ||||
|  | ||||
|             if (cachedFile != null) | ||||
|                 cachedFiles[fileId] = cachedFile; | ||||
| @@ -82,11 +82,11 @@ public class FileService( | ||||
|         return fileIds | ||||
|             .Select(f => cachedFiles.GetValueOrDefault(f)) | ||||
|             .Where(f => f != null) | ||||
|             .Cast<CloudFile>() | ||||
|             .Cast<SnCloudFile>() | ||||
|             .ToList(); | ||||
|     } | ||||
|  | ||||
|     public async Task<CloudFile> ProcessNewFileAsync( | ||||
|     public async Task<SnCloudFile> ProcessNewFileAsync( | ||||
|         Account account, | ||||
|         string fileId, | ||||
|         string filePool, | ||||
| @@ -131,7 +131,7 @@ public class FileService( | ||||
|         var finalContentType = contentType ?? | ||||
|                                (!fileName.Contains('.') ? "application/octet-stream" : MimeTypes.GetMimeType(fileName)); | ||||
|  | ||||
|         var file = new CloudFile | ||||
|         var file = new SnCloudFile | ||||
|         { | ||||
|             Id = fileId, | ||||
|             Name = fileName, | ||||
| @@ -190,7 +190,7 @@ public class FileService( | ||||
|         return file; | ||||
|     } | ||||
|  | ||||
|     private async Task ExtractMetadataAsync(CloudFile file, string filePath) | ||||
|     private async Task ExtractMetadataAsync(SnCloudFile file, string filePath) | ||||
|     { | ||||
|         switch (file.MimeType?.Split('/')[0]) | ||||
|         { | ||||
| @@ -346,7 +346,7 @@ public class FileService( | ||||
|         if (selfDestruct) File.Delete(filePath); | ||||
|     } | ||||
|  | ||||
|     public async Task UploadFileToRemoteAsync( | ||||
|     private async Task UploadFileToRemoteAsync( | ||||
|         string storageId, | ||||
|         Guid targetRemote, | ||||
|         Stream stream, | ||||
| @@ -373,7 +373,7 @@ public class FileService( | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public async Task<CloudFile> UpdateFileAsync(CloudFile file, FieldMask updateMask) | ||||
|     public async Task<SnCloudFile> UpdateFileAsync(SnCloudFile file, FieldMask updateMask) | ||||
|     { | ||||
|         var existingFile = await db.Files.FirstOrDefaultAsync(f => f.Id == file.Id); | ||||
|         if (existingFile == null) | ||||
| @@ -414,7 +414,7 @@ public class FileService( | ||||
|         return await db.Files.AsNoTracking().FirstAsync(f => f.Id == file.Id); | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteFileAsync(CloudFile file) | ||||
|     public async Task DeleteFileAsync(SnCloudFile file) | ||||
|     { | ||||
|         db.Remove(file); | ||||
|         await db.SaveChangesAsync(); | ||||
| @@ -423,7 +423,7 @@ public class FileService( | ||||
|         await DeleteFileDataAsync(file); | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteFileDataAsync(CloudFile file, bool force = false) | ||||
|     public async Task DeleteFileDataAsync(SnCloudFile file, bool force = false) | ||||
|     { | ||||
|         if (!file.PoolId.HasValue) return; | ||||
|  | ||||
| @@ -482,7 +482,7 @@ public class FileService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteFileDataBatchAsync(List<CloudFile> files) | ||||
|     public async Task DeleteFileDataBatchAsync(List<SnCloudFile> files) | ||||
|     { | ||||
|         files = files.Where(f => f.PoolId.HasValue).ToList(); | ||||
|  | ||||
| @@ -512,7 +512,7 @@ public class FileService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task<FileBundle?> GetBundleAsync(Guid id, Guid accountId) | ||||
|     private async Task<SnFileBundle?> GetBundleAsync(Guid id, Guid accountId) | ||||
|     { | ||||
|         var bundle = await db.Bundles | ||||
|             .Where(e => e.Id == id) | ||||
| @@ -569,15 +569,15 @@ public class FileService( | ||||
|         await Task.WhenAll(tasks); | ||||
|     } | ||||
|  | ||||
|     public async Task<List<CloudFile?>> LoadFromReference(List<CloudFileReferenceObject> references) | ||||
|     public async Task<List<SnCloudFile?>> LoadFromReference(List<SnCloudFileReferenceObject> references) | ||||
|     { | ||||
|         var cachedFiles = new Dictionary<string, CloudFile>(); | ||||
|         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<CloudFile>(cacheKey); | ||||
|             var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey); | ||||
|  | ||||
|             if (cachedFile != null) | ||||
|             { | ||||
| @@ -603,10 +603,9 @@ public class FileService( | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return references | ||||
|         return [.. references | ||||
|             .Select(r => cachedFiles.GetValueOrDefault(r.Id)) | ||||
|             .Where(f => f != null) | ||||
|             .ToList(); | ||||
|             .Where(f => f != null)]; | ||||
|     } | ||||
|  | ||||
|     public async Task<int> GetReferenceCountAsync(string fileId) | ||||
| @@ -685,7 +684,7 @@ public class FileService( | ||||
|         return count; | ||||
|     } | ||||
|  | ||||
|     public async Task<string> CreateFastUploadLinkAsync(CloudFile file) | ||||
|     public async Task<string> CreateFastUploadLinkAsync(SnCloudFile file) | ||||
|     { | ||||
|         if (file.PoolId is null) throw new InvalidOperationException("Pool ID is null"); | ||||
|  | ||||
| @@ -707,7 +706,7 @@ public class FileService( | ||||
|     } | ||||
| } | ||||
|  | ||||
| file class UpdatableCloudFile(CloudFile file) | ||||
| file class UpdatableCloudFile(SnCloudFile file) | ||||
| { | ||||
|     public string Name { get; set; } = file.Name; | ||||
|     public string? Description { get; set; } = file.Description; | ||||
| @@ -715,9 +714,9 @@ file class UpdatableCloudFile(CloudFile file) | ||||
|     public Dictionary<string, object?>? UserMeta { get; set; } = file.UserMeta; | ||||
|     public bool IsMarkedRecycle { get; set; } = file.IsMarkedRecycle; | ||||
|  | ||||
|     public Expression<Func<SetPropertyCalls<CloudFile>, SetPropertyCalls<CloudFile>>> ToSetPropertyCalls() | ||||
|     public Expression<Func<SetPropertyCalls<SnCloudFile>, SetPropertyCalls<SnCloudFile>>> ToSetPropertyCalls() | ||||
|     { | ||||
|         var userMeta = UserMeta ?? new Dictionary<string, object?>(); | ||||
|         var userMeta = UserMeta ?? []; | ||||
|         return setter => setter | ||||
|             .SetProperty(f => f.Name, Name) | ||||
|             .SetProperty(f => f.Description, Description) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using Grpc.Core; | ||||
| @@ -48,7 +48,7 @@ namespace DysonNetwork.Drive.Storage | ||||
|         { | ||||
|             // 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 CloudFileReferenceObject { Id = id }).ToList(); | ||||
|             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())); | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| 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; | ||||
| @@ -30,7 +32,10 @@ public class FileUploadController( | ||||
|     [HttpPost("create")] | ||||
|     public async Task<IActionResult> CreateUploadTask([FromBody] CreateUploadTaskRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|         { | ||||
|             return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; | ||||
|         } | ||||
|  | ||||
|         if (!currentUser.IsSuperuser) | ||||
|         { | ||||
| @@ -38,7 +43,7 @@ public class FileUploadController( | ||||
|             { Actor = $"user:{currentUser.Id}", Area = "global", Key = "files.create" }); | ||||
|             if (!allowed.HasPermission) | ||||
|             { | ||||
|                 return Forbid(); | ||||
|                 return new ObjectResult(ApiError.Unauthorized(forbidden: true)) { StatusCode = 403 }; | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -47,23 +52,19 @@ public class FileUploadController( | ||||
|         var pool = await fileService.GetPoolAsync(request.PoolId.Value); | ||||
|         if (pool is null) | ||||
|         { | ||||
|             return BadRequest("Pool not found"); | ||||
|             return new ObjectResult(ApiError.NotFound("Pool")) { StatusCode = 404 }; | ||||
|         } | ||||
|  | ||||
|         if (pool.PolicyConfig.RequirePrivilege > 0) | ||||
|         if (pool.PolicyConfig.RequirePrivilege is > 0) | ||||
|         { | ||||
|             if (currentUser.PerkSubscription is null) | ||||
|             { | ||||
|                 return new ObjectResult("You need to have join the Stellar Program to use this pool") | ||||
|                     { StatusCode = 403 }; | ||||
|             } | ||||
|  | ||||
|             var privilege = | ||||
|                 currentUser.PerkSubscription is null ? 0 : | ||||
|                 PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier); | ||||
|             if (privilege < pool.PolicyConfig.RequirePrivilege) | ||||
|             { | ||||
|                 return new ObjectResult( | ||||
|                     $"You need Stellar Program tier {pool.PolicyConfig.RequirePrivilege} to use this pool, you are tier {privilege}") | ||||
|                 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 | ||||
|                 }; | ||||
| @@ -73,14 +74,19 @@ public class FileUploadController( | ||||
|         var policy = pool.PolicyConfig; | ||||
|         if (!policy.AllowEncryption && !string.IsNullOrEmpty(request.EncryptPassword)) | ||||
|         { | ||||
|             return new ObjectResult("File encryption is not allowed in this pool") { StatusCode = 403 }; | ||||
|             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 BadRequest("Content type is required by the pool's policy"); | ||||
|                 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 => | ||||
| @@ -96,15 +102,18 @@ public class FileUploadController( | ||||
|  | ||||
|             if (!foundMatch) | ||||
|             { | ||||
|                 return new ObjectResult($"Content type {request.ContentType} is not allowed by the pool's policy") | ||||
|                 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( | ||||
|                 $"File size {request.FileSize} is larger than the pool's maximum file size {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 | ||||
|             }; | ||||
| @@ -117,7 +126,9 @@ public class FileUploadController( | ||||
|         ); | ||||
|         if (!ok) | ||||
|         { | ||||
|             return new ObjectResult($"File size {billableUnit} MiB is exceeded the user's quota {quota} MiB") | ||||
|             return new ObjectResult( | ||||
|                 ApiError.Unauthorized($"File size {billableUnit} MiB is exceeded the user's quota {quota} MiB", | ||||
|                     true)) | ||||
|             { StatusCode = 403 }; | ||||
|         } | ||||
|  | ||||
| @@ -170,15 +181,22 @@ public class FileUploadController( | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     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] IFormFile chunk) | ||||
|     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 NotFound("Upload task not found."); | ||||
|             return new ObjectResult(ApiError.NotFound("Upload task")) { StatusCode = 404 }; | ||||
|         } | ||||
|  | ||||
|         var chunkPath = Path.Combine(taskPath, $"{chunkIndex}.chunk"); | ||||
| @@ -194,19 +212,20 @@ public class FileUploadController( | ||||
|         var taskPath = Path.Combine(_tempPath, taskId); | ||||
|         if (!Directory.Exists(taskPath)) | ||||
|         { | ||||
|             return NotFound("Upload task not found."); | ||||
|             return new ObjectResult(ApiError.NotFound("Upload task")) { StatusCode = 404 }; | ||||
|         } | ||||
|  | ||||
|         var taskJsonPath = Path.Combine(taskPath, "task.json"); | ||||
|         if (!System.IO.File.Exists(taskJsonPath)) | ||||
|         { | ||||
|             return NotFound("Upload task metadata not found."); | ||||
|             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 BadRequest("Invalid task metadata."); | ||||
|             return new ObjectResult(new ApiError { Code = "BAD_REQUEST", Message = "Invalid task metadata.", Status = 400 }) | ||||
|             { StatusCode = 400 }; | ||||
|         } | ||||
|  | ||||
|         var mergedFilePath = Path.Combine(_tempPath, taskId + ".tmp"); | ||||
| @@ -221,7 +240,9 @@ public class FileUploadController( | ||||
|                     mergedStream.Close(); | ||||
|                     System.IO.File.Delete(mergedFilePath); | ||||
|                     Directory.Delete(taskPath, true); | ||||
|                     return BadRequest($"Chunk {i} is missing."); | ||||
|                     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); | ||||
| @@ -229,7 +250,10 @@ public class FileUploadController( | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|         { | ||||
|             return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; | ||||
|         } | ||||
|  | ||||
|         var fileId = await Nanoid.GenerateAsync(); | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Storage.Model | ||||
| @@ -18,7 +19,7 @@ namespace DysonNetwork.Drive.Storage.Model | ||||
|     public class CreateUploadTaskResponse | ||||
|     { | ||||
|         public bool FileExists { get; set; } | ||||
|         public CloudFile? File { get; set; } | ||||
|         public SnCloudFile? File { get; set; } | ||||
|         public string? TaskId { get; set; } | ||||
|         public long? ChunkSize { get; set; } | ||||
|         public int? ChunksCount { get; set; } | ||||
|   | ||||
| @@ -27,15 +27,6 @@ | ||||
|     "PublicKeyPath": "Keys/PublicKey.pem", | ||||
|     "PrivateKeyPath": "Keys/PrivateKey.pem" | ||||
|   }, | ||||
|   "OidcProvider": { | ||||
|     "IssuerUri": "https://nt.solian.app", | ||||
|     "PublicKeyPath": "Keys/PublicKey.pem", | ||||
|     "PrivateKeyPath": "Keys/PrivateKey.pem", | ||||
|     "AccessTokenLifetime": "01:00:00", | ||||
|     "RefreshTokenLifetime": "30.00:00:00", | ||||
|     "AuthorizationCodeLifetime": "00:30:00", | ||||
|     "RequireHttpsMetadata": true | ||||
|   }, | ||||
|   "Tus": { | ||||
|     "StorePath": "Uploads" | ||||
|   }, | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| { | ||||
|   "lockfileVersion": 1, | ||||
|   "workspaces": { | ||||
|     "": { | ||||
|       "dependencies": { | ||||
|         "highlight.js": "^11.11.1", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   "packages": { | ||||
|     "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| 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"]); | ||||
| } | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Gateway/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Gateway/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| 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"] | ||||
							
								
								
									
										18
									
								
								DysonNetwork.Gateway/DysonNetwork.Gateway.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								DysonNetwork.Gateway/DysonNetwork.Gateway.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| <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.4.2" /> | ||||
|     <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										171
									
								
								DysonNetwork.Gateway/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								DysonNetwork.Gateway/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| 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" }; | ||||
|  | ||||
| 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 | ||||
|     { | ||||
|         "pass" => "/id", | ||||
|         _ => $"/{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() | ||||
|     { | ||||
|         Active = new() | ||||
|         { | ||||
|             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.UseRateLimiter(); | ||||
|  | ||||
| app.MapReverseProxy().RequireRateLimiting("fixed"); | ||||
|  | ||||
| app.MapControllers(); | ||||
|  | ||||
| app.Run(); | ||||
							
								
								
									
										21
									
								
								DysonNetwork.Gateway/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								DysonNetwork.Gateway/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "$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" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								DysonNetwork.Gateway/appsettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								DysonNetwork.Gateway/appsettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "SiteUrl": "http://localhost:3000", | ||||
|   "Client": { | ||||
|     "SomeSetting": "SomeValue" | ||||
|   } | ||||
| } | ||||
| @@ -2,8 +2,9 @@ using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Credit; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Shared.Error; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| @@ -23,9 +24,9 @@ public class AccountController( | ||||
| ) : ControllerBase | ||||
| { | ||||
|     [HttpGet("{name}")] | ||||
|     [ProducesResponseType<Account>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<SnAccount>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<Account?>> GetByName(string name) | ||||
|     public async Task<ActionResult<SnAccount?>> GetByName(string name) | ||||
|     { | ||||
|         var account = await db.Accounts | ||||
|             .Include(e => e.Badges) | ||||
| @@ -42,9 +43,9 @@ public class AccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{name}/badges")] | ||||
|     [ProducesResponseType<List<AccountBadge>>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<List<SnAccountBadge>>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<List<AccountBadge>>> GetBadgesByName(string name) | ||||
|     public async Task<ActionResult<List<SnAccountBadge>>> GetBadgesByName(string name) | ||||
|     { | ||||
|         var account = await db.Accounts | ||||
|             .Include(e => e.Badges) | ||||
| @@ -103,9 +104,9 @@ public class AccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpPost] | ||||
|     [ProducesResponseType<Account>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<SnAccount>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||
|     public async Task<ActionResult<Account>> CreateAccount([FromBody] AccountCreateRequest request) | ||||
|     public async Task<ActionResult<SnAccount>> CreateAccount([FromBody] AccountCreateRequest request) | ||||
|     { | ||||
|         if (!await auth.ValidateCaptcha(request.CaptchaToken)) | ||||
|             return BadRequest(ApiError.Validation(new Dictionary<string, string[]> | ||||
| @@ -194,11 +195,12 @@ public class AccountController( | ||||
|         public bool IsAutomated { get; set; } = false; | ||||
|         [MaxLength(1024)] public string? Label { get; set; } | ||||
|         [MaxLength(4096)] public string? AppIdentifier { get; set; } | ||||
|         public Dictionary<string, object>? Meta { get; set; } | ||||
|         public Instant? ClearedAt { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{name}/statuses")] | ||||
|     public async Task<ActionResult<Status>> GetOtherStatus(string name) | ||||
|     public async Task<ActionResult<SnAccountStatus>> GetOtherStatus(string name) | ||||
|     { | ||||
|         var account = await db.Accounts.FirstOrDefaultAsync(a => a.Name == name); | ||||
|         if (account is null) | ||||
| @@ -253,7 +255,7 @@ public class AccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("search")] | ||||
|     public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20) | ||||
|     public async Task<List<SnAccount>> Search([FromQuery] string query, [FromQuery] int take = 20) | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(query)) | ||||
|             return []; | ||||
|   | ||||
| @@ -1,16 +1,15 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Error; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using AuthService = DysonNetwork.Pass.Auth.AuthService; | ||||
| using AuthSession = DysonNetwork.Pass.Auth.AuthSession; | ||||
| using SnAuthSession = DysonNetwork.Shared.Models.SnAuthSession; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| @@ -29,11 +28,11 @@ public class AccountCurrentController( | ||||
| ) : ControllerBase | ||||
| { | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType<Account>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<SnAccount>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<ApiError>(StatusCodes.Status401Unauthorized)] | ||||
|     public async Task<ActionResult<Account>> GetCurrentIdentity() | ||||
|     public async Task<ActionResult<SnAccount>> GetCurrentIdentity() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var account = await db.Accounts | ||||
| @@ -56,9 +55,9 @@ public class AccountCurrentController( | ||||
|     } | ||||
|  | ||||
|     [HttpPatch] | ||||
|     public async Task<ActionResult<Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request) | ||||
|     public async Task<ActionResult<SnAccount>> UpdateBasicInfo([FromBody] BasicInfoRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id); | ||||
|  | ||||
| @@ -81,6 +80,7 @@ public class AccountCurrentController( | ||||
|         [MaxLength(1024)] public string? TimeZone { get; set; } | ||||
|         [MaxLength(1024)] public string? Location { get; set; } | ||||
|         [MaxLength(4096)] public string? Bio { get; set; } | ||||
|         public Shared.Models.UsernameColor? UsernameColor { get; set; } | ||||
|         public Instant? Birthday { get; set; } | ||||
|         public List<ProfileLink>? Links { get; set; } | ||||
|  | ||||
| @@ -89,9 +89,9 @@ public class AccountCurrentController( | ||||
|     } | ||||
|  | ||||
|     [HttpPatch("profile")] | ||||
|     public async Task<ActionResult<AccountProfile>> UpdateProfile([FromBody] ProfileRequest request) | ||||
|     public async Task<ActionResult<SnAccountProfile>> UpdateProfile([FromBody] ProfileRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var profile = await db.AccountProfiles | ||||
| @@ -116,6 +116,7 @@ public class AccountCurrentController( | ||||
|         if (request.Location is not null) profile.Location = request.Location; | ||||
|         if (request.TimeZone is not null) profile.TimeZone = request.TimeZone; | ||||
|         if (request.Links is not null) profile.Links = request.Links; | ||||
|         if (request.UsernameColor is not null) profile.UsernameColor = request.UsernameColor; | ||||
|  | ||||
|         if (request.PictureId is not null) | ||||
|         { | ||||
| @@ -132,7 +133,7 @@ public class AccountCurrentController( | ||||
|                     Usage = "profile.picture" | ||||
|                 } | ||||
|             ); | ||||
|             profile.Picture = CloudFileReferenceObject.FromProtoValue(file); | ||||
|             profile.Picture = SnCloudFileReferenceObject.FromProtoValue(file); | ||||
|         } | ||||
|  | ||||
|         if (request.BackgroundId is not null) | ||||
| @@ -150,7 +151,7 @@ public class AccountCurrentController( | ||||
|                     Usage = "profile.background" | ||||
|                 } | ||||
|             ); | ||||
|             profile.Background = CloudFileReferenceObject.FromProtoValue(file); | ||||
|             profile.Background = SnCloudFileReferenceObject.FromProtoValue(file); | ||||
|         } | ||||
|  | ||||
|         db.Update(profile); | ||||
| @@ -164,7 +165,7 @@ public class AccountCurrentController( | ||||
|     [HttpDelete] | ||||
|     public async Task<ActionResult> RequestDeleteAccount() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -185,18 +186,18 @@ public class AccountCurrentController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("statuses")] | ||||
|     public async Task<ActionResult<Status>> GetCurrentStatus() | ||||
|     public async Task<ActionResult<SnAccountStatus>> GetCurrentStatus() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|         var status = await events.GetStatus(currentUser.Id); | ||||
|         return Ok(status); | ||||
|     } | ||||
|  | ||||
|     [HttpPatch("statuses")] | ||||
|     [RequiredPermission("global", "accounts.statuses.update")] | ||||
|     public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request) | ||||
|     public async Task<ActionResult<SnAccountStatus>> UpdateStatus([FromBody] AccountController.StatusRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|         if (request is { IsAutomated: true, AppIdentifier: not null }) | ||||
|             return BadRequest("Automated status cannot be updated."); | ||||
|  | ||||
| @@ -216,6 +217,7 @@ public class AccountCurrentController( | ||||
|         status.IsAutomated = request.IsAutomated; | ||||
|         status.Label = request.Label; | ||||
|         status.AppIdentifier = request.AppIdentifier; | ||||
|         status.Meta = request.Meta; | ||||
|         status.ClearedAt = request.ClearedAt; | ||||
|  | ||||
|         db.Update(status); | ||||
| @@ -227,9 +229,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPost("statuses")] | ||||
|     [RequiredPermission("global", "accounts.statuses.create")] | ||||
|     public async Task<ActionResult<Status>> CreateStatus([FromBody] AccountController.StatusRequest request) | ||||
|     public async Task<ActionResult<SnAccountStatus>> CreateStatus([FromBody] AccountController.StatusRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         if (request is { IsAutomated: true, AppIdentifier: not null }) | ||||
|         { | ||||
| @@ -245,6 +247,7 @@ public class AccountCurrentController( | ||||
|                     existingStatus.Attitude = request.Attitude; | ||||
|                     existingStatus.IsInvisible = request.IsInvisible; | ||||
|                     existingStatus.IsNotDisturb = request.IsNotDisturb; | ||||
|                     existingStatus.Meta = request.Meta; | ||||
|                     existingStatus.Label = request.Label; | ||||
|                     db.Update(existingStatus); | ||||
|                     await db.SaveChangesAsync(); | ||||
| @@ -260,7 +263,7 @@ public class AccountCurrentController( | ||||
|                 return Ok(existingStatus); // Do not override manually set status with automated ones | ||||
|         } | ||||
|  | ||||
|         var status = new Status | ||||
|         var status = new SnAccountStatus | ||||
|         { | ||||
|             AccountId = currentUser.Id, | ||||
|             Attitude = request.Attitude, | ||||
| @@ -268,6 +271,7 @@ public class AccountCurrentController( | ||||
|             IsNotDisturb = request.IsNotDisturb, | ||||
|             IsAutomated = request.IsAutomated, | ||||
|             Label = request.Label, | ||||
|             Meta = request.Meta, | ||||
|             AppIdentifier = request.AppIdentifier, | ||||
|             ClearedAt = request.ClearedAt | ||||
|         }; | ||||
| @@ -278,7 +282,7 @@ public class AccountCurrentController( | ||||
|     [HttpDelete("statuses")] | ||||
|     public async Task<ActionResult> DeleteStatus([FromQuery] string? app) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var queryable = db.AccountStatuses | ||||
| @@ -287,7 +291,7 @@ public class AccountCurrentController( | ||||
|             .OrderByDescending(s => s.CreatedAt) | ||||
|             .AsQueryable(); | ||||
|  | ||||
|         if (string.IsNullOrWhiteSpace(app)) | ||||
|         if (!string.IsNullOrWhiteSpace(app)) | ||||
|             queryable = queryable.Where(s => s.IsAutomated && s.AppIdentifier == app); | ||||
|  | ||||
|         var status = await queryable | ||||
| @@ -299,9 +303,9 @@ public class AccountCurrentController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("check-in")] | ||||
|     public async Task<ActionResult<CheckInResult>> GetCheckInResult() | ||||
|     public async Task<ActionResult<SnCheckInResult>> GetCheckInResult() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
| @@ -321,12 +325,12 @@ public class AccountCurrentController( | ||||
|     } | ||||
|  | ||||
|     [HttpPost("check-in")] | ||||
|     public async Task<ActionResult<CheckInResult>> DoCheckIn( | ||||
|     public async Task<ActionResult<SnCheckInResult>> DoCheckIn( | ||||
|         [FromBody] string? captchaToken, | ||||
|         [FromQuery] Instant? backdated = null | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         if (backdated is null) | ||||
|         { | ||||
| @@ -397,7 +401,7 @@ public class AccountCurrentController( | ||||
|     public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month, | ||||
|         [FromQuery] int? year) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date; | ||||
|         month ??= currentDate.Month; | ||||
| @@ -426,7 +430,7 @@ public class AccountCurrentController( | ||||
|         [FromQuery] int offset = 0 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var query = db.ActionLogs | ||||
|             .Where(log => log.AccountId == currentUser.Id) | ||||
| @@ -444,9 +448,9 @@ public class AccountCurrentController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("factors")] | ||||
|     public async Task<ActionResult<List<AccountAuthFactor>>> GetAuthFactors() | ||||
|     public async Task<ActionResult<List<SnAccountAuthFactor>>> GetAuthFactors() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var factors = await db.AccountAuthFactors | ||||
|             .Include(f => f.Account) | ||||
| @@ -458,15 +462,15 @@ public class AccountCurrentController( | ||||
|  | ||||
|     public class AuthFactorRequest | ||||
|     { | ||||
|         public AccountAuthFactorType Type { get; set; } | ||||
|         public Shared.Models.AccountAuthFactorType Type { get; set; } | ||||
|         public string? Secret { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpPost("factors")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request) | ||||
|     public async Task<ActionResult<SnAccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|         if (await accounts.CheckAuthFactorExists(currentUser, request.Type)) | ||||
|             return BadRequest(new ApiError | ||||
|             { | ||||
| @@ -482,9 +486,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPost("factors/{id:guid}/enable")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code) | ||||
|     public async Task<ActionResult<SnAccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var factor = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||
| @@ -511,9 +515,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPost("factors/{id:guid}/disable")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id) | ||||
|     public async Task<ActionResult<SnAccountAuthFactor>> DisableAuthFactor(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var factor = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||
| @@ -533,9 +537,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpDelete("factors/{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id) | ||||
|     public async Task<ActionResult<SnAccountAuthFactor>> DeleteAuthFactor(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var factor = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||
| @@ -555,10 +559,10 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpGet("devices")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<AuthClientWithChallenge>>> GetDevices() | ||||
|     public async Task<ActionResult<List<SnAuthClientWithChallenge>>> GetDevices() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); | ||||
|  | ||||
|         Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); | ||||
|  | ||||
| @@ -566,7 +570,7 @@ public class AccountCurrentController( | ||||
|             .Where(device => device.AccountId == currentUser.Id) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var challengeDevices = devices.Select(AuthClientWithChallenge.FromClient).ToList(); | ||||
|         var challengeDevices = devices.Select(SnAuthClientWithChallenge.FromClient).ToList(); | ||||
|         var deviceIds = challengeDevices.Select(x => x.Id).ToList(); | ||||
|  | ||||
|         var authChallenges = await db.AuthChallenges | ||||
| @@ -582,13 +586,13 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpGet("sessions")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<AuthSession>>> GetSessions( | ||||
|     public async Task<ActionResult<List<SnAuthSession>>> GetSessions( | ||||
|         [FromQuery] int take = 20, | ||||
|         [FromQuery] int offset = 0 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); | ||||
|  | ||||
|         var query = db.AuthSessions | ||||
|             .Include(session => session.Account) | ||||
| @@ -610,9 +614,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpDelete("sessions/{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AuthSession>> DeleteSession(Guid id) | ||||
|     public async Task<ActionResult<SnAuthSession>> DeleteSession(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -627,9 +631,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpDelete("devices/{deviceId}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AuthSession>> DeleteDevice(string deviceId) | ||||
|     public async Task<ActionResult<SnAuthSession>> DeleteDevice(string deviceId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -644,10 +648,10 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpDelete("sessions/current")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AuthSession>> DeleteCurrentSession() | ||||
|     public async Task<ActionResult<SnAuthSession>> DeleteCurrentSession() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -662,9 +666,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPatch("devices/{deviceId}/label")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AuthSession>> UpdateDeviceLabel(string deviceId, [FromBody] string label) | ||||
|     public async Task<ActionResult<SnAuthSession>> UpdateDeviceLabel(string deviceId, [FromBody] string label) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -679,10 +683,10 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPatch("devices/current/label")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AuthSession>> UpdateCurrentDeviceLabel([FromBody] string label) | ||||
|     public async Task<ActionResult<SnAuthSession>> UpdateCurrentDeviceLabel([FromBody] string label) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); | ||||
|  | ||||
|         var device = await db.AuthClients.FirstOrDefaultAsync(d => d.Id == currentSession.Challenge.ClientId); | ||||
|         if (device is null) return NotFound(); | ||||
| @@ -700,9 +704,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpGet("contacts")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<AccountContact>>> GetContacts() | ||||
|     public async Task<ActionResult<List<SnAccountContact>>> GetContacts() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var contacts = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id) | ||||
| @@ -713,15 +717,15 @@ public class AccountCurrentController( | ||||
|  | ||||
|     public class AccountContactRequest | ||||
|     { | ||||
|         [Required] public AccountContactType Type { get; set; } | ||||
|         [Required] public Shared.Models.AccountContactType Type { get; set; } | ||||
|         [Required] public string Content { get; set; } = null!; | ||||
|     } | ||||
|  | ||||
|     [HttpPost("contacts")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> CreateContact([FromBody] AccountContactRequest request) | ||||
|     public async Task<ActionResult<SnAccountContact>> CreateContact([FromBody] AccountContactRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -736,9 +740,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPost("contacts/{id:guid}/verify")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> VerifyContact(Guid id) | ||||
|     public async Task<ActionResult<SnAccountContact>> VerifyContact(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var contact = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||
| @@ -758,9 +762,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPost("contacts/{id:guid}/primary")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> SetPrimaryContact(Guid id) | ||||
|     public async Task<ActionResult<SnAccountContact>> SetPrimaryContact(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var contact = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||
| @@ -780,9 +784,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPost("contacts/{id:guid}/public")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> SetPublicContact(Guid id) | ||||
|     public async Task<ActionResult<SnAccountContact>> SetPublicContact(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var contact = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||
| @@ -802,9 +806,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpDelete("contacts/{id:guid}/public")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> UnsetPublicContact(Guid id) | ||||
|     public async Task<ActionResult<SnAccountContact>> UnsetPublicContact(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var contact = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||
| @@ -824,9 +828,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpDelete("contacts/{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> DeleteContact(Guid id) | ||||
|     public async Task<ActionResult<SnAccountContact>> DeleteContact(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var contact = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||
| @@ -845,11 +849,11 @@ public class AccountCurrentController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("badges")] | ||||
|     [ProducesResponseType<List<AccountBadge>>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<List<SnAccountBadge>>(StatusCodes.Status200OK)] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<AccountBadge>>> GetBadges() | ||||
|     public async Task<ActionResult<List<SnAccountBadge>>> GetBadges() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var badges = await db.Badges | ||||
|             .Where(b => b.AccountId == currentUser.Id) | ||||
| @@ -859,9 +863,9 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpPost("badges/{id:guid}/active")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountBadge>> ActivateBadge(Guid id) | ||||
|     public async Task<ActionResult<SnAccountBadge>> ActivateBadge(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -876,12 +880,12 @@ public class AccountCurrentController( | ||||
|  | ||||
|     [HttpGet("leveling")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ExperienceRecord>> GetLevelingHistory( | ||||
|     public async Task<ActionResult<SnExperienceRecord>> GetLevelingHistory( | ||||
|         [FromQuery] int take = 20, | ||||
|         [FromQuery] int offset = 0 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var queryable = db.ExperienceRecords | ||||
|             .Where(r => r.AccountId == currentUser.Id) | ||||
| @@ -901,7 +905,7 @@ public class AccountCurrentController( | ||||
|     [HttpGet("credits")] | ||||
|     public async Task<ActionResult<bool>> GetSocialCredit() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var credit = await creditService.GetSocialCredit(currentUser.Id); | ||||
|         return Ok(credit); | ||||
| @@ -913,7 +917,7 @@ public class AccountCurrentController( | ||||
|         [FromQuery] int offset = 0 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var queryable = db.SocialCreditRecords | ||||
|             .Where(r => r.AccountId == currentUser.Id) | ||||
|   | ||||
| @@ -1,9 +1,13 @@ | ||||
| using System.Globalization; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Net; | ||||
| using NodaTime; | ||||
| using NodaTime.Extensions; | ||||
|  | ||||
| @@ -16,7 +20,8 @@ public class AccountEventService( | ||||
|     IStringLocalizer<Localization.AccountEventResource> localizer, | ||||
|     RingService.RingServiceClient pusher, | ||||
|     SubscriptionService subscriptions, | ||||
|     Pass.Leveling.ExperienceService experienceService | ||||
|     Pass.Leveling.ExperienceService experienceService, | ||||
|     INatsConnection nats | ||||
| ) | ||||
| { | ||||
|     private static readonly Random Random = new(); | ||||
| @@ -36,10 +41,23 @@ public class AccountEventService( | ||||
|         cache.RemoveAsync(cacheKey); | ||||
|     } | ||||
|  | ||||
|     public async Task<Status> GetStatus(Guid userId) | ||||
|     private async Task BroadcastStatusUpdate(SnAccountStatus status) | ||||
|     { | ||||
|         await nats.PublishAsync( | ||||
|             AccountStatusUpdatedEvent.Type, | ||||
|             GrpcTypeHelper.ConvertObjectToByteString(new AccountStatusUpdatedEvent | ||||
|             { | ||||
|                 AccountId = status.AccountId, | ||||
|                 Status = status, | ||||
|                 UpdatedAt = SystemClock.Instance.GetCurrentInstant() | ||||
|             }).ToByteArray() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public async Task<SnAccountStatus> GetStatus(Guid userId) | ||||
|     { | ||||
|         var cacheKey = $"{StatusCacheKey}{userId}"; | ||||
|         var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||
|         var cachedStatus = await cache.GetAsync<SnAccountStatus>(cacheKey); | ||||
|         if (cachedStatus is not null) | ||||
|         { | ||||
|             cachedStatus!.IsOnline = !cachedStatus.IsInvisible && await GetAccountIsConnected(userId); | ||||
| @@ -63,9 +81,9 @@ public class AccountEventService( | ||||
|  | ||||
|         if (isOnline) | ||||
|         { | ||||
|             return new Status | ||||
|             return new SnAccountStatus | ||||
|             { | ||||
|                 Attitude = StatusAttitude.Neutral, | ||||
|                 Attitude = Shared.Models.StatusAttitude.Neutral, | ||||
|                 IsOnline = true, | ||||
|                 IsCustomized = false, | ||||
|                 Label = "Online", | ||||
| @@ -73,9 +91,9 @@ public class AccountEventService( | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return new Status | ||||
|         return new SnAccountStatus | ||||
|         { | ||||
|             Attitude = StatusAttitude.Neutral, | ||||
|             Attitude = Shared.Models.StatusAttitude.Neutral, | ||||
|             IsOnline = false, | ||||
|             IsCustomized = false, | ||||
|             Label = "Offline", | ||||
| @@ -83,15 +101,15 @@ public class AccountEventService( | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public async Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> userIds) | ||||
|     public async Task<Dictionary<Guid, SnAccountStatus>> GetStatuses(List<Guid> userIds) | ||||
|     { | ||||
|         var results = new Dictionary<Guid, Status>(); | ||||
|         var results = new Dictionary<Guid, SnAccountStatus>(); | ||||
|         var cacheMissUserIds = new List<Guid>(); | ||||
|  | ||||
|         foreach (var userId in userIds) | ||||
|         { | ||||
|             var cacheKey = $"{StatusCacheKey}{userId}"; | ||||
|             var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||
|             var cachedStatus = await cache.GetAsync<SnAccountStatus>(cacheKey); | ||||
|             if (cachedStatus != null) | ||||
|             { | ||||
|                 cachedStatus.IsOnline = !cachedStatus.IsInvisible && await GetAccountIsConnected(userId); | ||||
| @@ -131,9 +149,9 @@ public class AccountEventService( | ||||
|                 foreach (var userId in usersWithoutStatus) | ||||
|                 { | ||||
|                     var isOnline = await GetAccountIsConnected(userId); | ||||
|                     var defaultStatus = new Status | ||||
|                     var defaultStatus = new SnAccountStatus | ||||
|                     { | ||||
|                         Attitude = StatusAttitude.Neutral, | ||||
|                         Attitude = Shared.Models.StatusAttitude.Neutral, | ||||
|                         IsOnline = isOnline, | ||||
|                         IsCustomized = false, | ||||
|                         Label = isOnline ? "Online" : "Offline", | ||||
| @@ -147,7 +165,7 @@ public class AccountEventService( | ||||
|         return results; | ||||
|     } | ||||
|  | ||||
|     public async Task<Status> CreateStatus(Account user, Status status) | ||||
|     public async Task<SnAccountStatus> CreateStatus(SnAccount user, SnAccountStatus status) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         await db.AccountStatuses | ||||
| @@ -157,22 +175,25 @@ public class AccountEventService( | ||||
|         db.AccountStatuses.Add(status); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         await BroadcastStatusUpdate(status); | ||||
|  | ||||
|         return status; | ||||
|     } | ||||
|  | ||||
|     public async Task ClearStatus(Account user, Status status) | ||||
|     public async Task ClearStatus(SnAccount user, SnAccountStatus status) | ||||
|     { | ||||
|         status.ClearedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|         db.Update(status); | ||||
|         await db.SaveChangesAsync(); | ||||
|         PurgeStatusCache(user.Id); | ||||
|         await BroadcastStatusUpdate(status); | ||||
|     } | ||||
|  | ||||
|     private const int FortuneTipCount = 14; // This will be the max index for each type (positive/negative) | ||||
|     private const string CaptchaCacheKey = "checkin:captcha:"; | ||||
|     private const int CaptchaProbabilityPercent = 20; | ||||
|  | ||||
|     public async Task<bool> CheckInDailyDoAskCaptcha(Account user) | ||||
|     public async Task<bool> CheckInDailyDoAskCaptcha(SnAccount user) | ||||
|     { | ||||
|         var perkSubscription = await subscriptions.GetPerkSubscriptionAsync(user.Id); | ||||
|         if (perkSubscription is not null) return false; | ||||
| @@ -187,7 +208,7 @@ public class AccountEventService( | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> CheckInDailyIsAvailable(Account user) | ||||
|     public async Task<bool> CheckInDailyIsAvailable(SnAccount user) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var lastCheckIn = await db.AccountCheckInResults | ||||
| @@ -204,7 +225,7 @@ public class AccountEventService( | ||||
|         return lastDate < currentDate; | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> CheckInBackdatedIsAvailable(Account user, Instant backdated) | ||||
|     public async Task<bool> CheckInBackdatedIsAvailable(SnAccount user, Instant backdated) | ||||
|     { | ||||
|         var aDay = Duration.FromDays(1); | ||||
|         var backdatedStart = backdated.ToDateTimeUtc().Date.ToInstant(); | ||||
| @@ -252,7 +273,7 @@ public class AccountEventService( | ||||
|  | ||||
|     public const string CheckInLockKey = "checkin:lock:"; | ||||
|  | ||||
|     public async Task<CheckInResult> CheckInDaily(Account user, Instant? backdated = null) | ||||
|     public async Task<SnCheckInResult> CheckInDaily(SnAccount user, Instant? backdated = null) | ||||
|     { | ||||
|         var lockKey = $"{CheckInLockKey}{user.Id}"; | ||||
|  | ||||
| @@ -270,9 +291,7 @@ public class AccountEventService( | ||||
|  | ||||
|         // Now try to acquire the lock properly | ||||
|         await using var lockObj = | ||||
|             await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); | ||||
|         if (lockObj is null) throw new InvalidOperationException("Check-in was in progress."); | ||||
|  | ||||
|             await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)) ?? throw new InvalidOperationException("Check-in was in progress."); | ||||
|         var cultureInfo = new CultureInfo(user.Language, false); | ||||
|         CultureInfo.CurrentCulture = cultureInfo; | ||||
|         CultureInfo.CurrentUICulture = cultureInfo; | ||||
| @@ -282,9 +301,10 @@ public class AccountEventService( | ||||
|             .OrderBy(_ => Random.Next()) | ||||
|             .Take(2) | ||||
|             .ToList(); | ||||
|         var tips = positiveIndices.Select(index => new FortuneTip | ||||
|         var tips = positiveIndices.Select(index => new CheckInFortuneTip | ||||
|         { | ||||
|             IsPositive = true, Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value, | ||||
|             IsPositive = true, | ||||
|             Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value, | ||||
|             Content = localizer[$"FortuneTipPositiveContent_{index}"].Value | ||||
|         }).ToList(); | ||||
|  | ||||
| @@ -294,16 +314,29 @@ public class AccountEventService( | ||||
|             .OrderBy(_ => Random.Next()) | ||||
|             .Take(2) | ||||
|             .ToList(); | ||||
|         tips.AddRange(negativeIndices.Select(index => new FortuneTip | ||||
|         tips.AddRange(negativeIndices.Select(index => new CheckInFortuneTip | ||||
|         { | ||||
|             IsPositive = false, Title = localizer[$"FortuneTipNegativeTitle_{index}"].Value, | ||||
|             IsPositive = false, | ||||
|             Title = localizer[$"FortuneTipNegativeTitle_{index}"].Value, | ||||
|             Content = localizer[$"FortuneTipNegativeContent_{index}"].Value | ||||
|         })); | ||||
|  | ||||
|         var result = new CheckInResult | ||||
|         // The 5 is specialized, keep it alone. | ||||
|         var checkInLevel = (CheckInResultLevel)Random.Next(Enum.GetValues<CheckInResultLevel>().Length - 1); | ||||
|  | ||||
|         var accountBirthday = await db.AccountProfiles | ||||
|             .Where(x => x.AccountId == user.Id) | ||||
|             .Select(x => x.Birthday) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant().InUtc().Date; | ||||
|         if (accountBirthday.HasValue && accountBirthday.Value.InUtc().Date == now) | ||||
|             checkInLevel = CheckInResultLevel.Special; | ||||
|  | ||||
|         var result = new SnCheckInResult | ||||
|         { | ||||
|             Tips = tips, | ||||
|             Level = (CheckInResultLevel)Random.Next(Enum.GetValues<CheckInResultLevel>().Length), | ||||
|             Level = checkInLevel, | ||||
|             AccountId = user.Id, | ||||
|             RewardExperience = 100, | ||||
|             RewardPoints = backdated.HasValue ? null : 10, | ||||
| @@ -311,7 +344,6 @@ public class AccountEventService( | ||||
|             CreatedAt = backdated ?? SystemClock.Instance.GetCurrentInstant(), | ||||
|         }; | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant().InUtc().Date; | ||||
|         try | ||||
|         { | ||||
|             if (result.RewardPoints.HasValue) | ||||
| @@ -342,7 +374,7 @@ public class AccountEventService( | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     public async Task<List<DailyEventResponse>> GetEventCalendar(Account user, int month, int year = 0, | ||||
|     public async Task<List<DailyEventResponse>> GetEventCalendar(SnAccount user, int month, int year = 0, | ||||
|         bool replaceInvisible = false) | ||||
|     { | ||||
|         if (year == 0) | ||||
| @@ -356,7 +388,7 @@ public class AccountEventService( | ||||
|             .AsNoTracking() | ||||
|             .TagWith("eventcal:statuses") | ||||
|             .Where(x => x.AccountId == user.Id && x.CreatedAt >= startOfMonth && x.CreatedAt < endOfMonth) | ||||
|             .Select(x => new Status | ||||
|             .Select(x => new SnAccountStatus | ||||
|             { | ||||
|                 Id = x.Id, | ||||
|                 Attitude = x.Attitude, | ||||
| @@ -394,7 +426,7 @@ public class AccountEventService( | ||||
|             { | ||||
|                 Date = date, | ||||
|                 CheckInResult = checkInByDate.GetValueOrDefault(utcDate), | ||||
|                 Statuses = statusesByDate.GetValueOrDefault(utcDate, new List<Status>()) | ||||
|                 Statuses = statusesByDate.GetValueOrDefault(utcDate, new List<SnAccountStatus>()) | ||||
|             }; | ||||
|         }).ToList(); | ||||
|     } | ||||
|   | ||||
| @@ -1,19 +1,14 @@ | ||||
| using System.Globalization; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Auth.OpenId; | ||||
| using DysonNetwork.Pass.Localization; | ||||
| using DysonNetwork.Pass.Mailer; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using EFCore.BulkExtensions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream; | ||||
| using NATS.Net; | ||||
| using NodaTime; | ||||
| using OtpNet; | ||||
| @@ -36,7 +31,7 @@ public class AccountService( | ||||
|     INatsConnection nats | ||||
| ) | ||||
| { | ||||
|     public static void SetCultureInfo(Account account) | ||||
|     public static void SetCultureInfo(SnAccount account) | ||||
|     { | ||||
|         SetCultureInfo(account.Language); | ||||
|     } | ||||
| @@ -50,12 +45,12 @@ public class AccountService( | ||||
|  | ||||
|     public const string AccountCachePrefix = "account:"; | ||||
|  | ||||
|     public async Task PurgeAccountCache(Account account) | ||||
|     public async Task PurgeAccountCache(SnAccount account) | ||||
|     { | ||||
|         await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}"); | ||||
|     } | ||||
|  | ||||
|     public async Task<Account?> LookupAccount(string probe) | ||||
|     public async Task<SnAccount?> LookupAccount(string probe) | ||||
|     { | ||||
|         var account = await db.Accounts.Where(a => a.Name == probe).FirstOrDefaultAsync(); | ||||
|         if (account is not null) return account; | ||||
| @@ -67,7 +62,7 @@ public class AccountService( | ||||
|         return contact?.Account; | ||||
|     } | ||||
|  | ||||
|     public async Task<Account?> LookupAccountByConnection(string identifier, string provider) | ||||
|     public async Task<SnAccount?> LookupAccountByConnection(string identifier, string provider) | ||||
|     { | ||||
|         var connection = await db.AccountConnections | ||||
|             .Where(c => c.ProvidedIdentifier == identifier && c.Provider == provider) | ||||
| @@ -84,7 +79,7 @@ public class AccountService( | ||||
|         return profile?.Level; | ||||
|     } | ||||
|  | ||||
|     public async Task<Account> CreateAccount( | ||||
|     public async Task<SnAccount> CreateAccount( | ||||
|         string name, | ||||
|         string nick, | ||||
|         string email, | ||||
| @@ -100,39 +95,39 @@ public class AccountService( | ||||
|             throw new InvalidOperationException("Account name has already been taken."); | ||||
|  | ||||
|         var dupeEmailCount = await db.AccountContacts | ||||
|             .Where(c => c.Content == email && c.Type == AccountContactType.Email | ||||
|             .Where(c => c.Content == email && c.Type == Shared.Models.AccountContactType.Email | ||||
|             ).CountAsync(); | ||||
|         if (dupeEmailCount > 0) | ||||
|             throw new InvalidOperationException("Account email has already been used."); | ||||
|  | ||||
|         var account = new Account | ||||
|         var account = new SnAccount | ||||
|         { | ||||
|             Name = name, | ||||
|             Nick = nick, | ||||
|             Language = language, | ||||
|             Region = region, | ||||
|             Contacts = new List<AccountContact> | ||||
|             { | ||||
|             Contacts = | ||||
|             [ | ||||
|                 new() | ||||
|                 { | ||||
|                     Type = AccountContactType.Email, | ||||
|                     Type = Shared.Models.AccountContactType.Email, | ||||
|                     Content = email, | ||||
|                     VerifiedAt = isEmailVerified ? SystemClock.Instance.GetCurrentInstant() : null, | ||||
|                     IsPrimary = true | ||||
|                 } | ||||
|             }, | ||||
|             ], | ||||
|             AuthFactors = password is not null | ||||
|                 ? new List<AccountAuthFactor> | ||||
|                 ? new List<SnAccountAuthFactor> | ||||
|                 { | ||||
|                     new AccountAuthFactor | ||||
|                     new SnAccountAuthFactor | ||||
|                     { | ||||
|                         Type = AccountAuthFactorType.Password, | ||||
|                         Type = Shared.Models.AccountAuthFactorType.Password, | ||||
|                         Secret = password, | ||||
|                         EnabledAt = SystemClock.Instance.GetCurrentInstant() | ||||
|                     }.HashSecret() | ||||
|                 } | ||||
|                 : [], | ||||
|             Profile = new AccountProfile() | ||||
|             Profile = new SnAccountProfile() | ||||
|         }; | ||||
|  | ||||
|         if (isActivated) | ||||
| @@ -141,7 +136,7 @@ public class AccountService( | ||||
|             var defaultGroup = await db.PermissionGroups.FirstOrDefaultAsync(g => g.Key == "default"); | ||||
|             if (defaultGroup is not null) | ||||
|             { | ||||
|                 db.PermissionGroupMembers.Add(new PermissionGroupMember | ||||
|                 db.PermissionGroupMembers.Add(new SnPermissionGroupMember | ||||
|                 { | ||||
|                     Actor = $"user:{account.Id}", | ||||
|                     Group = defaultGroup | ||||
| @@ -167,7 +162,7 @@ public class AccountService( | ||||
|         return account; | ||||
|     } | ||||
|  | ||||
|     public async Task<Account> CreateAccount(OidcUserInfo userInfo) | ||||
|     public async Task<SnAccount> CreateAccount(OidcUserInfo userInfo) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(userInfo.Email)) | ||||
|             throw new ArgumentException("Email is required for account creation"); | ||||
| @@ -191,7 +186,7 @@ public class AccountService( | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public async Task<Account> CreateBotAccount(Account account, Guid automatedId, string? pictureId, | ||||
|     public async Task<SnAccount> CreateBotAccount(SnAccount account, Guid automatedId, string? pictureId, | ||||
|         string? backgroundId) | ||||
|     { | ||||
|         var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync(); | ||||
| @@ -217,7 +212,7 @@ public class AccountService( | ||||
|                     Usage = "profile.picture" | ||||
|                 } | ||||
|             ); | ||||
|             account.Profile.Picture = CloudFileReferenceObject.FromProtoValue(file); | ||||
|             account.Profile.Picture = SnCloudFileReferenceObject.FromProtoValue(file); | ||||
|         } | ||||
|  | ||||
|         if (!string.IsNullOrEmpty(backgroundId)) | ||||
| @@ -231,7 +226,7 @@ public class AccountService( | ||||
|                     Usage = "profile.background" | ||||
|                 } | ||||
|             ); | ||||
|             account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file); | ||||
|             account.Profile.Background = SnCloudFileReferenceObject.FromProtoValue(file); | ||||
|         } | ||||
|  | ||||
|         db.Accounts.Add(account); | ||||
| @@ -240,12 +235,12 @@ public class AccountService( | ||||
|         return account; | ||||
|     } | ||||
|  | ||||
|     public async Task<Account?> GetBotAccount(Guid automatedId) | ||||
|     public async Task<SnAccount?> GetBotAccount(Guid automatedId) | ||||
|     { | ||||
|         return await db.Accounts.FirstOrDefaultAsync(a => a.AutomatedId == automatedId); | ||||
|     } | ||||
|  | ||||
|     public async Task RequestAccountDeletion(Account account) | ||||
|     public async Task RequestAccountDeletion(SnAccount account) | ||||
|     { | ||||
|         var spell = await spells.CreateMagicSpell( | ||||
|             account, | ||||
| @@ -257,7 +252,7 @@ public class AccountService( | ||||
|         await spells.NotifyMagicSpell(spell); | ||||
|     } | ||||
|  | ||||
|     public async Task RequestPasswordReset(Account account) | ||||
|     public async Task RequestPasswordReset(SnAccount account) | ||||
|     { | ||||
|         var spell = await spells.CreateMagicSpell( | ||||
|             account, | ||||
| @@ -269,7 +264,7 @@ public class AccountService( | ||||
|         await spells.NotifyMagicSpell(spell); | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> CheckAuthFactorExists(Account account, AccountAuthFactorType type) | ||||
|     public async Task<bool> CheckAuthFactorExists(SnAccount account, Shared.Models.AccountAuthFactorType type) | ||||
|     { | ||||
|         var isExists = await db.AccountAuthFactors | ||||
|             .Where(x => x.AccountId == account.Id && x.Type == type) | ||||
| @@ -277,45 +272,45 @@ public class AccountService( | ||||
|         return isExists; | ||||
|     } | ||||
|  | ||||
|     public async Task<AccountAuthFactor?> CreateAuthFactor(Account account, AccountAuthFactorType type, string? secret) | ||||
|     public async Task<SnAccountAuthFactor?> CreateAuthFactor(SnAccount account, Shared.Models.AccountAuthFactorType type, string? secret) | ||||
|     { | ||||
|         AccountAuthFactor? factor = null; | ||||
|         SnAccountAuthFactor? factor = null; | ||||
|         switch (type) | ||||
|         { | ||||
|             case AccountAuthFactorType.Password: | ||||
|             case Shared.Models.AccountAuthFactorType.Password: | ||||
|                 if (string.IsNullOrWhiteSpace(secret)) throw new ArgumentNullException(nameof(secret)); | ||||
|                 factor = new AccountAuthFactor | ||||
|                 factor = new SnAccountAuthFactor | ||||
|                 { | ||||
|                     Type = AccountAuthFactorType.Password, | ||||
|                     Type = Shared.Models.AccountAuthFactorType.Password, | ||||
|                     Trustworthy = 1, | ||||
|                     AccountId = account.Id, | ||||
|                     Secret = secret, | ||||
|                     EnabledAt = SystemClock.Instance.GetCurrentInstant(), | ||||
|                 }.HashSecret(); | ||||
|                 break; | ||||
|             case AccountAuthFactorType.EmailCode: | ||||
|                 factor = new AccountAuthFactor | ||||
|             case Shared.Models.AccountAuthFactorType.EmailCode: | ||||
|                 factor = new SnAccountAuthFactor | ||||
|                 { | ||||
|                     Type = AccountAuthFactorType.EmailCode, | ||||
|                     Type = Shared.Models.AccountAuthFactorType.EmailCode, | ||||
|                     Trustworthy = 2, | ||||
|                     EnabledAt = SystemClock.Instance.GetCurrentInstant(), | ||||
|                 }; | ||||
|                 break; | ||||
|             case AccountAuthFactorType.InAppCode: | ||||
|                 factor = new AccountAuthFactor | ||||
|             case Shared.Models.AccountAuthFactorType.InAppCode: | ||||
|                 factor = new SnAccountAuthFactor | ||||
|                 { | ||||
|                     Type = AccountAuthFactorType.InAppCode, | ||||
|                     Type = Shared.Models.AccountAuthFactorType.InAppCode, | ||||
|                     Trustworthy = 1, | ||||
|                     EnabledAt = SystemClock.Instance.GetCurrentInstant() | ||||
|                 }; | ||||
|                 break; | ||||
|             case AccountAuthFactorType.TimedCode: | ||||
|             case Shared.Models.AccountAuthFactorType.TimedCode: | ||||
|                 var skOtp = KeyGeneration.GenerateRandomKey(20); | ||||
|                 var skOtp32 = Base32Encoding.ToString(skOtp); | ||||
|                 factor = new AccountAuthFactor | ||||
|                 factor = new SnAccountAuthFactor | ||||
|                 { | ||||
|                     Secret = skOtp32, | ||||
|                     Type = AccountAuthFactorType.TimedCode, | ||||
|                     Type = Shared.Models.AccountAuthFactorType.TimedCode, | ||||
|                     Trustworthy = 2, | ||||
|                     EnabledAt = null, // It needs to be tired once to enable | ||||
|                     CreatedResponse = new Dictionary<string, object> | ||||
| @@ -329,13 +324,13 @@ public class AccountService( | ||||
|                     } | ||||
|                 }; | ||||
|                 break; | ||||
|             case AccountAuthFactorType.PinCode: | ||||
|             case Shared.Models.AccountAuthFactorType.PinCode: | ||||
|                 if (string.IsNullOrWhiteSpace(secret)) throw new ArgumentNullException(nameof(secret)); | ||||
|                 if (!secret.All(char.IsDigit) || secret.Length != 6) | ||||
|                     throw new ArgumentException("PIN code must be exactly 6 digits"); | ||||
|                 factor = new AccountAuthFactor | ||||
|                 factor = new SnAccountAuthFactor | ||||
|                 { | ||||
|                     Type = AccountAuthFactorType.PinCode, | ||||
|                     Type = Shared.Models.AccountAuthFactorType.PinCode, | ||||
|                     Trustworthy = 0, // Only for confirming, can't be used for login | ||||
|                     Secret = secret, | ||||
|                     EnabledAt = SystemClock.Instance.GetCurrentInstant(), | ||||
| @@ -352,10 +347,10 @@ public class AccountService( | ||||
|         return factor; | ||||
|     } | ||||
|  | ||||
|     public async Task<AccountAuthFactor> EnableAuthFactor(AccountAuthFactor factor, string? code) | ||||
|     public async Task<SnAccountAuthFactor> EnableAuthFactor(SnAccountAuthFactor factor, string? code) | ||||
|     { | ||||
|         if (factor.EnabledAt is not null) throw new ArgumentException("The factor has been enabled."); | ||||
|         if (factor.Type is AccountAuthFactorType.Password or AccountAuthFactorType.TimedCode) | ||||
|         if (factor.Type is Shared.Models.AccountAuthFactorType.Password or Shared.Models.AccountAuthFactorType.TimedCode) | ||||
|         { | ||||
|             if (code is null || !factor.VerifyPassword(code)) | ||||
|                 throw new InvalidOperationException( | ||||
| @@ -370,7 +365,7 @@ public class AccountService( | ||||
|         return factor; | ||||
|     } | ||||
|  | ||||
|     public async Task<AccountAuthFactor> DisableAuthFactor(AccountAuthFactor factor) | ||||
|     public async Task<SnAccountAuthFactor> DisableAuthFactor(SnAccountAuthFactor factor) | ||||
|     { | ||||
|         if (factor.EnabledAt is null) throw new ArgumentException("The factor has been disabled."); | ||||
|  | ||||
| @@ -388,7 +383,7 @@ public class AccountService( | ||||
|         return factor; | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteAuthFactor(AccountAuthFactor factor) | ||||
|     public async Task DeleteAuthFactor(SnAccountAuthFactor factor) | ||||
|     { | ||||
|         var count = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == factor.AccountId) | ||||
| @@ -406,13 +401,13 @@ public class AccountService( | ||||
|     /// </summary> | ||||
|     /// <param name="account">The owner of the auth factor</param> | ||||
|     /// <param name="factor">The auth factor needed to send code</param> | ||||
|     public async Task SendFactorCode(Account account, AccountAuthFactor factor) | ||||
|     public async Task SendFactorCode(SnAccount account, SnAccountAuthFactor factor) | ||||
|     { | ||||
|         var code = new Random().Next(100000, 999999).ToString("000000"); | ||||
|  | ||||
|         switch (factor.Type) | ||||
|         { | ||||
|             case AccountAuthFactorType.InAppCode: | ||||
|             case Shared.Models.AccountAuthFactorType.InAppCode: | ||||
|                 if (await _GetFactorCode(factor) is not null) | ||||
|                     throw new InvalidOperationException("A factor code has been sent and in active duration."); | ||||
|  | ||||
| @@ -431,12 +426,12 @@ public class AccountService( | ||||
|                 ); | ||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5)); | ||||
|                 break; | ||||
|             case AccountAuthFactorType.EmailCode: | ||||
|             case Shared.Models.AccountAuthFactorType.EmailCode: | ||||
|                 if (await _GetFactorCode(factor) is not null) | ||||
|                     throw new InvalidOperationException("A factor code has been sent and in active duration."); | ||||
|  | ||||
|                 var contact = await db.AccountContacts | ||||
|                     .Where(c => c.Type == AccountContactType.Email) | ||||
|                     .Where(c => c.Type == Shared.Models.AccountContactType.Email) | ||||
|                     .Where(c => c.VerifiedAt != null) | ||||
|                     .Where(c => c.IsPrimary) | ||||
|                     .Where(c => c.AccountId == account.Id) | ||||
| @@ -465,27 +460,27 @@ public class AccountService( | ||||
|  | ||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30)); | ||||
|                 break; | ||||
|             case AccountAuthFactorType.Password: | ||||
|             case AccountAuthFactorType.TimedCode: | ||||
|             case Shared.Models.AccountAuthFactorType.Password: | ||||
|             case Shared.Models.AccountAuthFactorType.TimedCode: | ||||
|             default: | ||||
|                 // No need to send, such as password etc... | ||||
|                 return; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> VerifyFactorCode(AccountAuthFactor factor, string code) | ||||
|     public async Task<bool> VerifyFactorCode(SnAccountAuthFactor factor, string code) | ||||
|     { | ||||
|         switch (factor.Type) | ||||
|         { | ||||
|             case AccountAuthFactorType.EmailCode: | ||||
|             case AccountAuthFactorType.InAppCode: | ||||
|             case Shared.Models.AccountAuthFactorType.EmailCode: | ||||
|             case Shared.Models.AccountAuthFactorType.InAppCode: | ||||
|                 var correctCode = await _GetFactorCode(factor); | ||||
|                 var isCorrect = correctCode is not null && | ||||
|                                 string.Equals(correctCode, code, StringComparison.OrdinalIgnoreCase); | ||||
|                 await cache.RemoveAsync($"{AuthFactorCachePrefix}{factor.Id}:code"); | ||||
|                 return isCorrect; | ||||
|             case AccountAuthFactorType.Password: | ||||
|             case AccountAuthFactorType.TimedCode: | ||||
|             case Shared.Models.AccountAuthFactorType.Password: | ||||
|             case Shared.Models.AccountAuthFactorType.TimedCode: | ||||
|             default: | ||||
|                 return factor.VerifyPassword(code); | ||||
|         } | ||||
| @@ -493,7 +488,7 @@ public class AccountService( | ||||
|  | ||||
|     private const string AuthFactorCachePrefix = "authfactor:"; | ||||
|  | ||||
|     private async Task _SetFactorCode(AccountAuthFactor factor, string code, TimeSpan expires) | ||||
|     private async Task _SetFactorCode(SnAccountAuthFactor factor, string code, TimeSpan expires) | ||||
|     { | ||||
|         await cache.SetAsync( | ||||
|             $"{AuthFactorCachePrefix}{factor.Id}:code", | ||||
| @@ -502,7 +497,7 @@ public class AccountService( | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private async Task<string?> _GetFactorCode(AccountAuthFactor factor) | ||||
|     private async Task<string?> _GetFactorCode(SnAccountAuthFactor factor) | ||||
|     { | ||||
|         return await cache.GetAsync<string?>( | ||||
|             $"{AuthFactorCachePrefix}{factor.Id}:code" | ||||
| @@ -516,7 +511,7 @@ public class AccountService( | ||||
|             .AnyAsync(s => s.Challenge.ClientId == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<AuthClient> UpdateDeviceName(Account account, string deviceId, string label) | ||||
|     public async Task<SnAuthClient> UpdateDeviceName(SnAccount account, string deviceId, string label) | ||||
|     { | ||||
|         var device = await db.AuthClients.FirstOrDefaultAsync(c => c.DeviceId == deviceId && c.AccountId == account.Id | ||||
|         ); | ||||
| @@ -529,7 +524,7 @@ public class AccountService( | ||||
|         return device; | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteSession(Account account, Guid sessionId) | ||||
|     public async Task DeleteSession(SnAccount account, Guid sessionId) | ||||
|     { | ||||
|         var session = await db.AuthSessions | ||||
|             .Include(s => s.Challenge) | ||||
| @@ -555,7 +550,7 @@ public class AccountService( | ||||
|         await cache.RemoveAsync($"{AuthService.AuthCachePrefix}{session.Id}"); | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteDevice(Account account, string deviceId) | ||||
|     public async Task DeleteDevice(SnAccount account, string deviceId) | ||||
|     { | ||||
|         var device = await db.AuthClients.FirstOrDefaultAsync(c => c.DeviceId == deviceId && c.AccountId == account.Id | ||||
|         ); | ||||
| @@ -585,7 +580,7 @@ public class AccountService( | ||||
|             await cache.RemoveAsync($"{AuthService.AuthCachePrefix}{item.Id}"); | ||||
|     } | ||||
|  | ||||
|     public async Task<AccountContact> CreateContactMethod(Account account, AccountContactType type, string content) | ||||
|     public async Task<SnAccountContact> CreateContactMethod(SnAccount account, Shared.Models.AccountContactType type, string content) | ||||
|     { | ||||
|         var isExists = await db.AccountContacts | ||||
|             .Where(x => x.AccountId == account.Id && x.Type == type && x.Content == content) | ||||
| @@ -593,7 +588,7 @@ public class AccountService( | ||||
|         if (isExists) | ||||
|             throw new InvalidOperationException("Contact method already exists."); | ||||
|  | ||||
|         var contact = new AccountContact | ||||
|         var contact = new SnAccountContact | ||||
|         { | ||||
|             Type = type, | ||||
|             Content = content, | ||||
| @@ -606,7 +601,7 @@ public class AccountService( | ||||
|         return contact; | ||||
|     } | ||||
|  | ||||
|     public async Task VerifyContactMethod(Account account, AccountContact contact) | ||||
|     public async Task VerifyContactMethod(SnAccount account, SnAccountContact contact) | ||||
|     { | ||||
|         var spell = await spells.CreateMagicSpell( | ||||
|             account, | ||||
| @@ -618,7 +613,7 @@ public class AccountService( | ||||
|         await spells.NotifyMagicSpell(spell); | ||||
|     } | ||||
|  | ||||
|     public async Task<AccountContact> SetContactMethodPrimary(Account account, AccountContact contact) | ||||
|     public async Task<SnAccountContact> SetContactMethodPrimary(SnAccount account, SnAccountContact contact) | ||||
|     { | ||||
|         if (contact.AccountId != account.Id) | ||||
|             throw new InvalidOperationException("Contact method does not belong to this account."); | ||||
| @@ -647,7 +642,7 @@ public class AccountService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<AccountContact> SetContactMethodPublic(Account account, AccountContact contact, bool isPublic) | ||||
|     public async Task<SnAccountContact> SetContactMethodPublic(SnAccount account, SnAccountContact contact, bool isPublic) | ||||
|     { | ||||
|         contact.IsPublic = isPublic; | ||||
|         db.AccountContacts.Update(contact); | ||||
| @@ -655,7 +650,7 @@ public class AccountService( | ||||
|         return contact; | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteContactMethod(Account account, AccountContact contact) | ||||
|     public async Task DeleteContactMethod(SnAccount account, SnAccountContact contact) | ||||
|     { | ||||
|         if (contact.AccountId != account.Id) | ||||
|             throw new InvalidOperationException("Contact method does not belong to this account."); | ||||
| @@ -670,7 +665,7 @@ public class AccountService( | ||||
|     /// This method will grant a badge to the account. | ||||
|     /// Shouldn't be exposed to normal user and the user itself. | ||||
|     /// </summary> | ||||
|     public async Task<AccountBadge> GrantBadge(Account account, AccountBadge badge) | ||||
|     public async Task<SnAccountBadge> GrantBadge(SnAccount account, SnAccountBadge badge) | ||||
|     { | ||||
|         badge.AccountId = account.Id; | ||||
|         db.Badges.Add(badge); | ||||
| @@ -682,14 +677,12 @@ public class AccountService( | ||||
|     /// This method will revoke a badge from the account. | ||||
|     /// Shouldn't be exposed to normal user and the user itself. | ||||
|     /// </summary> | ||||
|     public async Task RevokeBadge(Account account, Guid badgeId) | ||||
|     public async Task RevokeBadge(SnAccount account, Guid badgeId) | ||||
|     { | ||||
|         var badge = await db.Badges | ||||
|             .Where(b => b.AccountId == account.Id && b.Id == badgeId) | ||||
|             .OrderByDescending(b => b.CreatedAt) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (badge is null) throw new InvalidOperationException("Badge was not found."); | ||||
|  | ||||
|             .FirstOrDefaultAsync() ?? throw new InvalidOperationException("Badge was not found."); | ||||
|         var profile = await db.AccountProfiles | ||||
|             .Where(p => p.AccountId == account.Id) | ||||
|             .FirstOrDefaultAsync(); | ||||
| @@ -700,7 +693,7 @@ public class AccountService( | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task ActiveBadge(Account account, Guid badgeId) | ||||
|     public async Task ActiveBadge(SnAccount account, Guid badgeId) | ||||
|     { | ||||
|         await using var transaction = await db.Database.BeginTransactionAsync(); | ||||
|  | ||||
| @@ -734,7 +727,7 @@ public class AccountService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteAccount(Account account) | ||||
|     public async Task DeleteAccount(SnAccount account) | ||||
|     { | ||||
|         await db.AuthSessions | ||||
|             .Where(s => s.AccountId == account.Id) | ||||
|   | ||||
| @@ -160,6 +160,26 @@ public class AccountServiceGrpc( | ||||
|         return response; | ||||
|     } | ||||
|  | ||||
|     public override async Task<GetAccountBatchResponse> SearchAccount(SearchAccountRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var accounts = await _db.Accounts | ||||
|             .AsNoTracking() | ||||
|             .Where(a => EF.Functions.ILike(a.Name, $"%{request.Query}%")) | ||||
|             .Include(a => a.Profile) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var perks = await subscriptions.GetPerkSubscriptionsAsync( | ||||
|             accounts.Select(x => x.Id).ToList() | ||||
|         ); | ||||
|         foreach (var account in accounts) | ||||
|             if (perks.TryGetValue(account.Id, out var perk)) | ||||
|                 account.PerkSubscription = perk?.ToReference(); | ||||
|  | ||||
|         var response = new GetAccountBatchResponse(); | ||||
|         response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); | ||||
|         return response; | ||||
|     } | ||||
|  | ||||
|     public override async Task<ListAccountsResponse> ListAccounts(ListAccountsRequest request, | ||||
|         ServerCallContext context) | ||||
|     { | ||||
| @@ -236,7 +256,7 @@ public class AccountServiceGrpc( | ||||
|         var relationship = await relationships.GetRelationship( | ||||
|             Guid.Parse(request.AccountId), | ||||
|             Guid.Parse(request.RelatedId), | ||||
|             status: (RelationshipStatus?)request.Status | ||||
|             status: (Shared.Models.RelationshipStatus?)request.Status | ||||
|         ); | ||||
|         return new GetRelationshipResponse | ||||
|         { | ||||
| @@ -256,7 +276,7 @@ public class AccountServiceGrpc( | ||||
|             hasRelationship = await relationships.HasRelationshipWithStatus( | ||||
|                 Guid.Parse(request.AccountId), | ||||
|                 Guid.Parse(request.RelatedId), | ||||
|                 (RelationshipStatus)request.Status | ||||
|                 (Shared.Models.RelationshipStatus)request.Status | ||||
|             ); | ||||
|         return new BoolValue { Value = hasRelationship }; | ||||
|     } | ||||
|   | ||||
| @@ -1,46 +0,0 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
| using Point = NetTopologySuite.Geometries.Point; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class ActionLog : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(4096)] public string Action { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); | ||||
|     [MaxLength(512)] public string? UserAgent { get; set; } | ||||
|     [MaxLength(128)] public string? IpAddress { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public GeoPoint? Location { get; set; } | ||||
|  | ||||
|     public Guid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
|     public Guid? SessionId { get; set; } | ||||
|  | ||||
|     public Shared.Proto.ActionLog ToProtoValue() | ||||
|     { | ||||
|         var protoLog = new Shared.Proto.ActionLog | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Action = Action, | ||||
|             UserAgent = UserAgent ?? string.Empty, | ||||
|             IpAddress = IpAddress ?? string.Empty, | ||||
|             Location = Location?.ToString() ?? string.Empty, | ||||
|             AccountId = AccountId.ToString(), | ||||
|             CreatedAt = CreatedAt.ToTimestamp() | ||||
|         }; | ||||
|  | ||||
|         // Convert Meta dictionary to Struct | ||||
|         protoLog.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta)); | ||||
|  | ||||
|         if (SessionId.HasValue) | ||||
|             protoLog.SessionId = SessionId.Value.ToString(); | ||||
|  | ||||
|         return protoLog; | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| @@ -7,7 +8,7 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||
| { | ||||
|     public void CreateActionLog(Guid accountId, string action, Dictionary<string, object?> meta) | ||||
|     { | ||||
|         var log = new ActionLog | ||||
|         var log = new SnActionLog | ||||
|         { | ||||
|             Action = action, | ||||
|             AccountId = accountId, | ||||
| @@ -18,9 +19,9 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||
|     } | ||||
|  | ||||
|     public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, | ||||
|         Account? account = null) | ||||
|         SnAccount? account = null) | ||||
|     { | ||||
|         var log = new ActionLog | ||||
|         var log = new SnActionLog | ||||
|         { | ||||
|             Action = action, | ||||
|             Meta = meta, | ||||
| @@ -29,14 +30,14 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||
|             Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) | ||||
|         }; | ||||
|          | ||||
|         if (request.HttpContext.Items["CurrentUser"] is Account currentUser) | ||||
|         if (request.HttpContext.Items["CurrentUser"] is SnAccount currentUser) | ||||
|             log.AccountId = currentUser.Id; | ||||
|         else if (account != null) | ||||
|             log.AccountId = account.Id; | ||||
|         else | ||||
|             throw new ArgumentException("No user context was found"); | ||||
|          | ||||
|         if (request.HttpContext.Items["CurrentSession"] is Auth.AuthSession currentSession) | ||||
|         if (request.HttpContext.Items["CurrentSession"] is SnAuthSession currentSession) | ||||
|             log.SessionId = currentSession.Id; | ||||
|  | ||||
|         fbs.Enqueue(log); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -22,7 +22,7 @@ public class BotAccountReceiverGrpc( | ||||
|         ServerCallContext context | ||||
|     ) | ||||
|     { | ||||
|         var account = Account.FromProtoValue(request.Account); | ||||
|         var account = SnAccount.FromProtoValue(request.Account); | ||||
|         account = await accounts.CreateBotAccount( | ||||
|             account, | ||||
|             Guid.Parse(request.AutomatedId), | ||||
| @@ -48,7 +48,7 @@ public class BotAccountReceiverGrpc( | ||||
|         ServerCallContext context | ||||
|     ) | ||||
|     { | ||||
|         var account = Account.FromProtoValue(request.Account); | ||||
|         var account = SnAccount.FromProtoValue(request.Account); | ||||
|  | ||||
|         if (request.PictureId is not null) | ||||
|         { | ||||
| @@ -65,7 +65,7 @@ public class BotAccountReceiverGrpc( | ||||
|                     Usage = "profile.picture" | ||||
|                 } | ||||
|             ); | ||||
|             account.Profile.Picture = CloudFileReferenceObject.FromProtoValue(file); | ||||
|             account.Profile.Picture = SnCloudFileReferenceObject.FromProtoValue(file); | ||||
|         } | ||||
|  | ||||
|         if (request.BackgroundId is not null) | ||||
| @@ -83,7 +83,7 @@ public class BotAccountReceiverGrpc( | ||||
|                     Usage = "profile.background" | ||||
|                 } | ||||
|             ); | ||||
|             account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file); | ||||
|             account.Profile.Background = SnCloudFileReferenceObject.FromProtoValue(file); | ||||
|         } | ||||
|  | ||||
|         db.Accounts.Update(account); | ||||
|   | ||||
| @@ -50,7 +50,7 @@ public class MagicSpellController(AppDatabase db, MagicSpellService sp) : Contro | ||||
|             return NotFound(); | ||||
|         try | ||||
|         { | ||||
|             if (spell.Type == MagicSpellType.AuthPasswordReset && request?.NewPassword is not null) | ||||
|             if (spell.Type == Shared.Models.MagicSpellType.AuthPasswordReset && request?.NewPassword is not null) | ||||
|                 await sp.ApplyPasswordReset(spell, request.NewPassword); | ||||
|             else | ||||
|                 await sp.ApplyMagicSpell(spell); | ||||
|   | ||||
| @@ -2,8 +2,8 @@ using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Pass.Emails; | ||||
| using DysonNetwork.Pass.Mailer; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
| @@ -20,8 +20,8 @@ public class MagicSpellService( | ||||
|     ICacheService cache | ||||
| ) | ||||
| { | ||||
|     public async Task<MagicSpell> CreateMagicSpell( | ||||
|         Account account, | ||||
|     public async Task<SnMagicSpell> CreateMagicSpell( | ||||
|         SnAccount account, | ||||
|         MagicSpellType type, | ||||
|         Dictionary<string, object> meta, | ||||
|         Instant? expiredAt = null, | ||||
| @@ -42,7 +42,7 @@ public class MagicSpellService( | ||||
|         } | ||||
|  | ||||
|         var spellWord = _GenerateRandomString(128); | ||||
|         var spell = new MagicSpell | ||||
|         var spell = new SnMagicSpell | ||||
|         { | ||||
|             Spell = spellWord, | ||||
|             Type = type, | ||||
| @@ -60,7 +60,7 @@ public class MagicSpellService( | ||||
|  | ||||
|     private const string SpellNotifyCacheKeyPrefix = "spells:notify:"; | ||||
|  | ||||
|     public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false) | ||||
|     public async Task NotifyMagicSpell(SnMagicSpell spell, bool bypassVerify = false) | ||||
|     { | ||||
|         var cacheKey = SpellNotifyCacheKeyPrefix + spell.Id; | ||||
|         var (found, _) = await cache.GetAsyncWithStatus<bool?>(cacheKey); | ||||
| @@ -156,7 +156,7 @@ public class MagicSpellService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task ApplyMagicSpell(MagicSpell spell) | ||||
|     public async Task ApplyMagicSpell(SnMagicSpell spell) | ||||
|     { | ||||
|         switch (spell.Type) | ||||
|         { | ||||
| @@ -191,7 +191,7 @@ public class MagicSpellService( | ||||
|                 var defaultGroup = await db.PermissionGroups.FirstOrDefaultAsync(g => g.Key == "default"); | ||||
|                 if (defaultGroup is not null && account is not null) | ||||
|                 { | ||||
|                     db.PermissionGroupMembers.Add(new PermissionGroupMember | ||||
|                     db.PermissionGroupMembers.Add(new SnPermissionGroupMember | ||||
|                     { | ||||
|                         Actor = $"user:{account.Id}", | ||||
|                         Group = defaultGroup | ||||
| @@ -218,7 +218,7 @@ public class MagicSpellService( | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task ApplyPasswordReset(MagicSpell spell, string newPassword) | ||||
|     public async Task ApplyPasswordReset(SnMagicSpell spell, string newPassword) | ||||
|     { | ||||
|         if (spell.Type != MagicSpellType.AuthPasswordReset) | ||||
|             throw new ArgumentException("This spell is not a password reset spell."); | ||||
| @@ -231,7 +231,7 @@ public class MagicSpellService( | ||||
|         { | ||||
|             var account = await db.Accounts.FirstOrDefaultAsync(c => c.Id == spell.AccountId); | ||||
|             if (account is null) throw new InvalidOperationException("Both account and auth factor was not found."); | ||||
|             passwordFactor = new AccountAuthFactor | ||||
|             passwordFactor = new SnAccountAuthFactor | ||||
|             { | ||||
|                 Type = AccountAuthFactorType.Password, | ||||
|                 Account = account, | ||||
| @@ -257,6 +257,6 @@ public class MagicSpellService( | ||||
|  | ||||
|         var base64String = Convert.ToBase64String(randomBytes); | ||||
|  | ||||
|         return base64String.Substring(0, length); | ||||
|         return base64String[..length]; | ||||
|     } | ||||
| } | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| @@ -26,7 +27,7 @@ public class NotableDaysController(NotableDaysService days) : ControllerBase | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetAccountNotableDays(int year) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var region = currentUser.Region; | ||||
|         if (string.IsNullOrWhiteSpace(region)) region = "us"; | ||||
| @@ -39,7 +40,7 @@ public class NotableDaysController(NotableDaysService days) : ControllerBase | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetAccountNotableDaysCurrentYear() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var currentYear = DateTime.Now.Year; | ||||
|         var region = currentUser.Region; | ||||
| @@ -64,7 +65,7 @@ public class NotableDaysController(NotableDaysService days) : ControllerBase | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<NotableDay?>> GetAccountNextHoliday() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var region = currentUser.Region; | ||||
|         if (string.IsNullOrWhiteSpace(region)) region = "us"; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -12,10 +13,10 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
| { | ||||
|     [HttpGet] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Relationship>>> ListRelationships([FromQuery] int offset = 0, | ||||
|     public async Task<ActionResult<List<SnAccountRelationship>>> ListRelationships([FromQuery] int offset = 0, | ||||
|         [FromQuery] int take = 20) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var query = db.AccountRelationships.AsQueryable() | ||||
| @@ -44,9 +45,9 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|     [HttpGet("requests")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Relationship>>> ListSentRequests() | ||||
|     public async Task<ActionResult<List<SnAccountRelationship>>> ListSentRequests() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relationships = await db.AccountRelationships | ||||
|             .Where(r => r.AccountId == currentUser.Id && r.Status == RelationshipStatus.Pending) | ||||
| @@ -66,10 +67,10 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|     [HttpPost("{userId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> CreateRelationship(Guid userId, | ||||
|     public async Task<ActionResult<SnAccountRelationship>> CreateRelationship(Guid userId, | ||||
|         [FromBody] RelationshipRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(userId); | ||||
|         if (relatedUser is null) return NotFound("Account was not found."); | ||||
| @@ -89,10 +90,10 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|     [HttpPatch("{userId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> UpdateRelationship(Guid userId, | ||||
|     public async Task<ActionResult<SnAccountRelationship>> UpdateRelationship(Guid userId, | ||||
|         [FromBody] RelationshipRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -111,9 +112,9 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|     [HttpGet("{userId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> GetRelationship(Guid userId) | ||||
|     public async Task<ActionResult<SnAccountRelationship>> GetRelationship(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var now = Instant.FromDateTimeUtc(DateTime.UtcNow); | ||||
|         var queries = db.AccountRelationships.AsQueryable() | ||||
| @@ -131,9 +132,9 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|     [HttpPost("{userId:guid}/friends")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> SendFriendRequest(Guid userId) | ||||
|     public async Task<ActionResult<SnAccountRelationship>> SendFriendRequest(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(userId); | ||||
|         if (relatedUser is null) return NotFound("Account was not found."); | ||||
| @@ -158,7 +159,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> DeleteFriendRequest(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -173,9 +174,9 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|     [HttpPost("{userId:guid}/friends/accept")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> AcceptFriendRequest(Guid userId) | ||||
|     public async Task<ActionResult<SnAccountRelationship>> AcceptFriendRequest(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending); | ||||
|         if (relationship is null) return NotFound("Friend request was not found."); | ||||
| @@ -193,9 +194,9 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|     [HttpPost("{userId:guid}/friends/decline")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> DeclineFriendRequest(Guid userId) | ||||
|     public async Task<ActionResult<SnAccountRelationship>> DeclineFriendRequest(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending); | ||||
|         if (relationship is null) return NotFound("Friend request was not found."); | ||||
| @@ -213,9 +214,9 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|     [HttpPost("{userId:guid}/block")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> BlockUser(Guid userId) | ||||
|     public async Task<ActionResult<SnAccountRelationship>> BlockUser(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(userId); | ||||
|         if (relatedUser is null) return NotFound("Account was not found."); | ||||
| @@ -233,9 +234,9 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|      | ||||
|     [HttpDelete("{userId:guid}/block")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> UnblockUser(Guid userId) | ||||
|     public async Task<ActionResult<SnAccountRelationship>> UnblockUser(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(userId); | ||||
|         if (relatedUser is null) return NotFound("Account was not found."); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using DysonNetwork.Pass.Localization; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| @@ -26,7 +27,7 @@ public class RelationshipService( | ||||
|         return count > 0; | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship?> GetRelationship( | ||||
|     public async Task<SnAccountRelationship?> GetRelationship( | ||||
|         Guid accountId, | ||||
|         Guid relatedId, | ||||
|         RelationshipStatus? status = null, | ||||
| @@ -42,7 +43,7 @@ public class RelationshipService( | ||||
|         return relationship; | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status) | ||||
|     public async Task<SnAccountRelationship> CreateRelationship(SnAccount sender, SnAccount target, RelationshipStatus status) | ||||
|     { | ||||
|         if (status == RelationshipStatus.Pending) | ||||
|             throw new InvalidOperationException( | ||||
| @@ -50,7 +51,7 @@ public class RelationshipService( | ||||
|         if (await HasExistingRelationship(sender.Id, target.Id)) | ||||
|             throw new InvalidOperationException("Found existing relationship between you and target user."); | ||||
|  | ||||
|         var relationship = new Relationship | ||||
|         var relationship = new SnAccountRelationship | ||||
|         { | ||||
|             AccountId = sender.Id, | ||||
|             RelatedId = target.Id, | ||||
| @@ -65,14 +66,14 @@ public class RelationshipService( | ||||
|         return relationship; | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> BlockAccount(Account sender, Account target) | ||||
|     public async Task<SnAccountRelationship> BlockAccount(SnAccount sender, SnAccount target) | ||||
|     { | ||||
|         if (await HasExistingRelationship(sender.Id, target.Id)) | ||||
|             return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked); | ||||
|         return await CreateRelationship(sender, target, RelationshipStatus.Blocked); | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> UnblockAccount(Account sender, Account target) | ||||
|     public async Task<SnAccountRelationship> UnblockAccount(SnAccount sender, SnAccount target) | ||||
|     { | ||||
|         var relationship = await GetRelationship(sender.Id, target.Id, RelationshipStatus.Blocked); | ||||
|         if (relationship is null) throw new ArgumentException("There is no relationship between you and the user."); | ||||
| @@ -84,12 +85,12 @@ public class RelationshipService( | ||||
|         return relationship; | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> SendFriendRequest(Account sender, Account target) | ||||
|     public async Task<SnAccountRelationship> SendFriendRequest(SnAccount sender, SnAccount target) | ||||
|     { | ||||
|         if (await HasExistingRelationship(sender.Id, target.Id)) | ||||
|             throw new InvalidOperationException("Found existing relationship between you and target user."); | ||||
|  | ||||
|         var relationship = new Relationship | ||||
|         var relationship = new SnAccountRelationship | ||||
|         { | ||||
|             AccountId = sender.Id, | ||||
|             RelatedId = target.Id, | ||||
| @@ -128,8 +129,8 @@ public class RelationshipService( | ||||
|         await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId); | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> AcceptFriendRelationship( | ||||
|         Relationship relationship, | ||||
|     public async Task<SnAccountRelationship> AcceptFriendRelationship( | ||||
|         SnAccountRelationship relationship, | ||||
|         RelationshipStatus status = RelationshipStatus.Friends | ||||
|     ) | ||||
|     { | ||||
| @@ -144,7 +145,7 @@ public class RelationshipService( | ||||
|         relationship.ExpiredAt = null; | ||||
|         db.Update(relationship); | ||||
|  | ||||
|         var relationshipBackward = new Relationship | ||||
|         var relationshipBackward = new SnAccountRelationship | ||||
|         { | ||||
|             AccountId = relationship.RelatedId, | ||||
|             RelatedId = relationship.AccountId, | ||||
| @@ -159,7 +160,7 @@ public class RelationshipService( | ||||
|         return relationshipBackward; | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status) | ||||
|     public async Task<SnAccountRelationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status) | ||||
|     { | ||||
|         var relationship = await GetRelationship(accountId, relatedId); | ||||
|         if (relationship is null) throw new ArgumentException("There is no relationship between you and the user."); | ||||
| @@ -173,7 +174,7 @@ public class RelationshipService( | ||||
|         return relationship; | ||||
|     } | ||||
|  | ||||
|     public async Task<List<Guid>> ListAccountFriends(Account account) | ||||
|     public async Task<List<Guid>> ListAccountFriends(SnAccount account) | ||||
|     { | ||||
|         return await ListAccountFriends(account.Id); | ||||
|     } | ||||
| @@ -197,7 +198,7 @@ public class RelationshipService( | ||||
|         return friends ?? []; | ||||
|     } | ||||
|  | ||||
|     public async Task<List<Guid>> ListAccountBlocked(Account account) | ||||
|     public async Task<List<Guid>> ListAccountBlocked(SnAccount account) | ||||
|     { | ||||
|         return await ListAccountBlocked(account.Id); | ||||
|     } | ||||
|   | ||||
| @@ -2,13 +2,8 @@ using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Credit; | ||||
| using DysonNetwork.Pass.Leveling; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Microsoft.EntityFrameworkCore.Query; | ||||
| @@ -22,39 +17,42 @@ public class AppDatabase( | ||||
|     IConfiguration configuration | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<PermissionNode> PermissionNodes { get; set; } = null!; | ||||
|     public DbSet<PermissionGroup> PermissionGroups { get; set; } = null!; | ||||
|     public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; } = null!; | ||||
|     public DbSet<SnPermissionNode> PermissionNodes { get; set; } = null!; | ||||
|     public DbSet<SnPermissionGroup> PermissionGroups { get; set; } = null!; | ||||
|     public DbSet<SnPermissionGroupMember> PermissionGroupMembers { get; set; } = null!; | ||||
|  | ||||
|     public DbSet<MagicSpell> MagicSpells { get; set; } = null!; | ||||
|     public DbSet<Account.Account> Accounts { get; set; } = null!; | ||||
|     public DbSet<AccountConnection> AccountConnections { get; set; } = null!; | ||||
|     public DbSet<AccountProfile> AccountProfiles { get; set; } = null!; | ||||
|     public DbSet<AccountContact> AccountContacts { get; set; } = null!; | ||||
|     public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; } = null!; | ||||
|     public DbSet<Relationship> AccountRelationships { get; set; } = null!; | ||||
|     public DbSet<Status> AccountStatuses { get; set; } = null!; | ||||
|     public DbSet<CheckInResult> AccountCheckInResults { get; set; } = null!; | ||||
|     public DbSet<AccountBadge> Badges { get; set; } = null!; | ||||
|     public DbSet<ActionLog> ActionLogs { get; set; } = null!; | ||||
|     public DbSet<AbuseReport> AbuseReports { get; set; } = null!; | ||||
|     public DbSet<SnMagicSpell> MagicSpells { get; set; } = null!; | ||||
|     public DbSet<SnAccount> Accounts { get; set; } = null!; | ||||
|     public DbSet<SnAccountConnection> AccountConnections { get; set; } = null!; | ||||
|     public DbSet<SnAccountProfile> AccountProfiles { get; set; } = null!; | ||||
|     public DbSet<SnAccountContact> AccountContacts { get; set; } = null!; | ||||
|     public DbSet<SnAccountAuthFactor> AccountAuthFactors { get; set; } = null!; | ||||
|     public DbSet<SnAccountRelationship> AccountRelationships { get; set; } = null!; | ||||
|     public DbSet<SnAccountStatus> AccountStatuses { get; set; } = null!; | ||||
|     public DbSet<SnCheckInResult> AccountCheckInResults { get; set; } = null!; | ||||
|     public DbSet<SnAccountBadge> Badges { get; set; } = null!; | ||||
|     public DbSet<SnActionLog> ActionLogs { get; set; } = null!; | ||||
|     public DbSet<SnAbuseReport> AbuseReports { get; set; } = null!; | ||||
|  | ||||
|     public DbSet<AuthSession> AuthSessions { get; set; } = null!; | ||||
|     public DbSet<AuthChallenge> AuthChallenges { get; set; } = null!; | ||||
|     public DbSet<AuthClient> AuthClients { get; set; } = null!; | ||||
|     public DbSet<ApiKey> ApiKeys { get; set; } = null!; | ||||
|     public DbSet<SnAuthSession> AuthSessions { get; set; } = null!; | ||||
|     public DbSet<SnAuthChallenge> AuthChallenges { get; set; } = null!; | ||||
|     public DbSet<SnAuthClient> AuthClients { get; set; } = null!; | ||||
|     public DbSet<SnApiKey> ApiKeys { get; set; } = null!; | ||||
|  | ||||
|     public DbSet<Wallet.Wallet> Wallets { get; set; } = null!; | ||||
|     public DbSet<WalletPocket> WalletPockets { get; set; } = null!; | ||||
|     public DbSet<Order> PaymentOrders { get; set; } = null!; | ||||
|     public DbSet<Transaction> PaymentTransactions { get; set; } = null!; | ||||
|     public DbSet<Subscription> WalletSubscriptions { get; set; } = null!; | ||||
|     public DbSet<Coupon> WalletCoupons { get; set; } = null!; | ||||
|     public DbSet<SnWallet> Wallets { get; set; } = null!; | ||||
|     public DbSet<SnWalletPocket> WalletPockets { get; set; } = null!; | ||||
|     public DbSet<SnWalletOrder> PaymentOrders { get; set; } = null!; | ||||
|     public DbSet<SnWalletTransaction> PaymentTransactions { get; set; } = null!; | ||||
|     public DbSet<SnWalletFund> WalletFunds { get; set; } = null!; | ||||
|     public DbSet<SnWalletFundRecipient> WalletFundRecipients { get; set; } = null!; | ||||
|     public DbSet<SnWalletSubscription> WalletSubscriptions { get; set; } = null!; | ||||
|     public DbSet<SnWalletGift> WalletGifts { get; set; } = null!; | ||||
|     public DbSet<SnWalletCoupon> WalletCoupons { get; set; } = null!; | ||||
|  | ||||
|     public DbSet<Punishment> Punishments { get; set; } = null!; | ||||
|     public DbSet<SnAccountPunishment> Punishments { get; set; } = null!; | ||||
|  | ||||
|     public DbSet<SocialCreditRecord> SocialCreditRecords { get; set; } = null!; | ||||
|     public DbSet<ExperienceRecord> ExperienceRecords { get; set; } = null!; | ||||
|     public DbSet<SnSocialCreditRecord> SocialCreditRecords { get; set; } = null!; | ||||
|     public DbSet<SnExperienceRecord> ExperienceRecords { get; set; } = null!; | ||||
|  | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|     { | ||||
| @@ -74,11 +72,11 @@ public class AppDatabase( | ||||
|  | ||||
|         optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) => | ||||
|         { | ||||
|             var defaultPermissionGroup = await context.Set<PermissionGroup>() | ||||
|             var defaultPermissionGroup = await context.Set<SnPermissionGroup>() | ||||
|                 .FirstOrDefaultAsync(g => g.Key == "default", cancellationToken); | ||||
|             if (defaultPermissionGroup is null) | ||||
|             { | ||||
|                 context.Set<PermissionGroup>().Add(new PermissionGroup | ||||
|                 context.Set<SnPermissionGroup>().Add(new SnPermissionGroup | ||||
|                 { | ||||
|                     Key = "default", | ||||
|                     Nodes = new List<string> | ||||
| @@ -111,21 +109,21 @@ public class AppDatabase( | ||||
|     { | ||||
|         base.OnModelCreating(modelBuilder); | ||||
|  | ||||
|         modelBuilder.Entity<PermissionGroupMember>() | ||||
|         modelBuilder.Entity<SnPermissionGroupMember>() | ||||
|             .HasKey(pg => new { pg.GroupId, pg.Actor }); | ||||
|         modelBuilder.Entity<PermissionGroupMember>() | ||||
|         modelBuilder.Entity<SnPermissionGroupMember>() | ||||
|             .HasOne(pg => pg.Group) | ||||
|             .WithMany(g => g.Members) | ||||
|             .HasForeignKey(pg => pg.GroupId) | ||||
|             .OnDelete(DeleteBehavior.Cascade); | ||||
|  | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|         modelBuilder.Entity<SnAccountRelationship>() | ||||
|             .HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId }); | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|         modelBuilder.Entity<SnAccountRelationship>() | ||||
|             .HasOne(r => r.Account) | ||||
|             .WithMany(a => a.OutgoingRelationships) | ||||
|             .HasForeignKey(r => r.AccountId); | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|         modelBuilder.Entity<SnAccountRelationship>() | ||||
|             .HasOne(r => r.Related) | ||||
|             .WithMany(a => a.IncomingRelationships) | ||||
|             .HasForeignKey(r => r.RelatedId); | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -14,7 +15,7 @@ public class ApiKeyController(AppDatabase db, AuthService auth) : ControllerBase | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> GetKeys([FromQuery] int offset = 0, [FromQuery] int take = 20) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var query = db.ApiKeys | ||||
|             .Where(e => e.AccountId == currentUser.Id) | ||||
| @@ -34,7 +35,7 @@ public class ApiKeyController(AppDatabase db, AuthService auth) : ControllerBase | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> GetKey(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var key = await db.ApiKeys | ||||
|             .Where(e => e.AccountId == currentUser.Id) | ||||
| @@ -56,7 +57,7 @@ public class ApiKeyController(AppDatabase db, AuthService auth) : ControllerBase | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(request.Label)) | ||||
|             return BadRequest("Label is required"); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|  | ||||
|         var key = await auth.CreateApiKey(currentUser.Id, request.Label, request.ExpiredAt); | ||||
|         key.Key = await auth.IssueApiKeyToken(key); | ||||
| @@ -67,7 +68,7 @@ public class ApiKeyController(AppDatabase db, AuthService auth) : ControllerBase | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> RotateKey(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|          | ||||
|         var key = await auth.GetApiKey(id, currentUser.Id); | ||||
|         if(key is null) return NotFound(); | ||||
| @@ -80,7 +81,7 @@ public class ApiKeyController(AppDatabase db, AuthService auth) : ControllerBase | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> DeleteKey(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||
|          | ||||
|         var key = await auth.GetApiKey(id, currentUser.Id); | ||||
|         if(key is null) return NotFound(); | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
|  | ||||
| public static class AuthCacheConstants | ||||
|   | ||||
| @@ -2,15 +2,13 @@ using System.ComponentModel.DataAnnotations; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using NodaTime; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Localization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using AccountAuthFactor = DysonNetwork.Pass.Account.AccountAuthFactor; | ||||
| using AccountService = DysonNetwork.Pass.Account.AccountService; | ||||
| using ActionLogService = DysonNetwork.Pass.Account.ActionLogService; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
|  | ||||
| @@ -40,7 +38,7 @@ public class AuthController( | ||||
|     } | ||||
|  | ||||
|     [HttpPost("challenge")] | ||||
|     public async Task<ActionResult<AuthChallenge>> CreateChallenge([FromBody] ChallengeRequest request) | ||||
|     public async Task<ActionResult<SnAuthChallenge>> CreateChallenge([FromBody] ChallengeRequest request) | ||||
|     { | ||||
|         var account = await accounts.LookupAccount(request.Account); | ||||
|         if (account is null) return NotFound("Account was not found."); | ||||
| @@ -72,7 +70,7 @@ public class AuthController( | ||||
|             .Where(e => e.UserAgent == userAgent) | ||||
|             .Where(e => e.StepRemain > 0) | ||||
|             .Where(e => e.ExpiredAt != null && now < e.ExpiredAt) | ||||
|             .Where(e => e.Type == ChallengeType.Login) | ||||
|             .Where(e => e.Type == Shared.Models.ChallengeType.Login) | ||||
|             .Where(e => e.ClientId == device.Id) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (existingChallenge is not null) | ||||
| @@ -82,7 +80,7 @@ public class AuthController( | ||||
|             if (existingSession is null) return existingChallenge; | ||||
|         } | ||||
|  | ||||
|         var challenge = new AuthChallenge | ||||
|         var challenge = new SnAuthChallenge | ||||
|         { | ||||
|             ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)), | ||||
|             StepTotal = await auth.DetectChallengeRisk(Request, account), | ||||
| @@ -106,7 +104,7 @@ public class AuthController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("challenge/{id:guid}")] | ||||
|     public async Task<ActionResult<AuthChallenge>> GetChallenge([FromRoute] Guid id) | ||||
|     public async Task<ActionResult<SnAuthChallenge>> GetChallenge([FromRoute] Guid id) | ||||
|     { | ||||
|         var challenge = await db.AuthChallenges | ||||
|             .Include(e => e.Account) | ||||
| @@ -119,7 +117,7 @@ public class AuthController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("challenge/{id:guid}/factors")] | ||||
|     public async Task<ActionResult<List<AccountAuthFactor>>> GetChallengeFactors([FromRoute] Guid id) | ||||
|     public async Task<ActionResult<List<SnAccountAuthFactor>>> GetChallengeFactors([FromRoute] Guid id) | ||||
|     { | ||||
|         var challenge = await db.AuthChallenges | ||||
|             .Include(e => e.Account) | ||||
| @@ -165,7 +163,7 @@ public class AuthController( | ||||
|     } | ||||
|  | ||||
|     [HttpPatch("challenge/{id:guid}")] | ||||
|     public async Task<ActionResult<AuthChallenge>> DoChallenge( | ||||
|     public async Task<ActionResult<SnAuthChallenge>> DoChallenge( | ||||
|         [FromRoute] Guid id, | ||||
|         [FromBody] PerformChallengeRequest request | ||||
|     ) | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| @@ -13,8 +13,7 @@ public class AuthService( | ||||
|     IConfiguration config, | ||||
|     IHttpClientFactory httpClientFactory, | ||||
|     IHttpContextAccessor httpContextAccessor, | ||||
|     ICacheService cache, | ||||
|     ILogger<AuthService> logger | ||||
|     ICacheService cache | ||||
| ) | ||||
| { | ||||
|     private HttpContext HttpContext => httpContextAccessor.HttpContext!; | ||||
| @@ -27,7 +26,7 @@ public class AuthService( | ||||
|     /// <param name="request">The request context</param> | ||||
|     /// <param name="account">The account to login</param> | ||||
|     /// <returns>The required steps to login</returns> | ||||
|     public async Task<int> DetectChallengeRisk(HttpRequest request, Account.Account account) | ||||
|     public async Task<int> DetectChallengeRisk(HttpRequest request, SnAccount account) | ||||
|     { | ||||
|         // 1) Find out how many authentication factors the account has enabled. | ||||
|         var maxSteps = await db.AccountAuthFactors | ||||
| @@ -76,10 +75,10 @@ public class AuthService( | ||||
|         return totalRequiredSteps; | ||||
|     } | ||||
|  | ||||
|     public async Task<AuthSession> CreateSessionForOidcAsync(Account.Account account, Instant time, | ||||
|     public async Task<SnAuthSession> CreateSessionForOidcAsync(SnAccount account, Instant time, | ||||
|         Guid? customAppId = null) | ||||
|     { | ||||
|         var challenge = new AuthChallenge | ||||
|         var challenge = new SnAuthChallenge | ||||
|         { | ||||
|             AccountId = account.Id, | ||||
|             IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||
| @@ -89,7 +88,7 @@ public class AuthService( | ||||
|             Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc | ||||
|         }; | ||||
|  | ||||
|         var session = new AuthSession | ||||
|         var session = new SnAuthSession | ||||
|         { | ||||
|             AccountId = account.Id, | ||||
|             CreatedAt = time, | ||||
| @@ -105,7 +104,7 @@ public class AuthService( | ||||
|         return session; | ||||
|     } | ||||
|  | ||||
|     public async Task<AuthClient> GetOrCreateDeviceAsync( | ||||
|     public async Task<SnAuthClient> GetOrCreateDeviceAsync( | ||||
|         Guid accountId, | ||||
|         string deviceId, | ||||
|         string? deviceName = null, | ||||
| @@ -114,7 +113,7 @@ public class AuthService( | ||||
|     { | ||||
|         var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId); | ||||
|         if (device is not null) return device; | ||||
|         device = new AuthClient | ||||
|         device = new SnAuthClient | ||||
|         { | ||||
|             Platform = platform, | ||||
|             DeviceId = deviceId, | ||||
| @@ -181,7 +180,7 @@ public class AuthService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public string CreateToken(AuthSession session) | ||||
|     public string CreateToken(SnAuthSession session) | ||||
|     { | ||||
|         // Load the private key for signing | ||||
|         var privateKeyPem = File.ReadAllText(config["AuthToken:PrivateKeyPath"]!); | ||||
| @@ -199,7 +198,7 @@ public class AuthService( | ||||
|     /// <param name="challenge">Completed challenge</param> | ||||
|     /// <returns>Signed compact token</returns> | ||||
|     /// <exception cref="ArgumentException">If challenge not completed or session already exists</exception> | ||||
|     public async Task<string> CreateSessionAndIssueToken(AuthChallenge challenge) | ||||
|     public async Task<string> CreateSessionAndIssueToken(SnAuthChallenge challenge) | ||||
|     { | ||||
|         if (challenge.StepRemain != 0) | ||||
|             throw new ArgumentException("Challenge not yet completed."); | ||||
| @@ -210,7 +209,7 @@ public class AuthService( | ||||
|             throw new ArgumentException("Session already exists for this challenge."); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var session = new AuthSession | ||||
|         var session = new SnAuthSession | ||||
|         { | ||||
|             LastGrantedAt = now, | ||||
|             ExpiredAt = now.Plus(Duration.FromDays(7)), | ||||
| @@ -256,7 +255,7 @@ public class AuthService( | ||||
|         return $"{payloadBase64}.{signatureBase64}"; | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> ValidateSudoMode(AuthSession session, string? pinCode) | ||||
|     public async Task<bool> ValidateSudoMode(SnAuthSession session, string? pinCode) | ||||
|     { | ||||
|         // Check if the session is already in sudo mode (cached) | ||||
|         var sudoModeKey = $"accounts:{session.Id}:sudo"; | ||||
| @@ -319,7 +318,7 @@ public class AuthService( | ||||
|         return factor.VerifyPassword(pinCode); | ||||
|     } | ||||
|  | ||||
|     public async Task<ApiKey?> GetApiKey(Guid id, Guid? accountId = null) | ||||
|     public async Task<SnApiKey?> GetApiKey(Guid id, Guid? accountId = null) | ||||
|     { | ||||
|         var key = await db.ApiKeys | ||||
|             .Include(e => e.Session) | ||||
| @@ -329,13 +328,13 @@ public class AuthService( | ||||
|         return key; | ||||
|     } | ||||
|  | ||||
|     public async Task<ApiKey> CreateApiKey(Guid accountId, string label, Instant? expiredAt = null) | ||||
|     public async Task<SnApiKey> CreateApiKey(Guid accountId, string label, Instant? expiredAt = null) | ||||
|     { | ||||
|         var key = new ApiKey | ||||
|         var key = new SnApiKey | ||||
|         { | ||||
|             AccountId = accountId, | ||||
|             Label = label, | ||||
|             Session = new AuthSession | ||||
|             Session = new SnAuthSession | ||||
|             { | ||||
|                 AccountId = accountId, | ||||
|                 ExpiredAt = expiredAt | ||||
| @@ -348,7 +347,7 @@ public class AuthService( | ||||
|         return key; | ||||
|     } | ||||
|  | ||||
|     public async Task<string> IssueApiKeyToken(ApiKey key) | ||||
|     public async Task<string> IssueApiKeyToken(SnApiKey key) | ||||
|     { | ||||
|         key.Session.LastGrantedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|         db.Update(key.Session); | ||||
| @@ -357,14 +356,14 @@ public class AuthService( | ||||
|         return tk; | ||||
|     } | ||||
|  | ||||
|     public async Task RevokeApiKeyToken(ApiKey key) | ||||
|     public async Task RevokeApiKeyToken(SnApiKey key) | ||||
|     { | ||||
|         db.Remove(key); | ||||
|         db.Remove(key.Session); | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<ApiKey> RotateApiKeyToken(ApiKey key) | ||||
|     public async Task<SnApiKey> RotateApiKeyToken(SnApiKey key) | ||||
|     { | ||||
|         await using var transaction = await db.Database.BeginTransactionAsync(); | ||||
|         try | ||||
| @@ -372,7 +371,7 @@ public class AuthService( | ||||
|             var oldSessionId = key.SessionId; | ||||
|  | ||||
|             // Create new session | ||||
|             var newSession = new AuthSession | ||||
|             var newSession = new SnAuthSession | ||||
|             { | ||||
|                 AccountId = key.AccountId, | ||||
|                 ExpiredAt = key.Session?.ExpiredAt | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Security.Cryptography; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
|  | ||||
| @@ -7,7 +8,7 @@ public class CompactTokenService(IConfiguration config) | ||||
|     private readonly string _privateKeyPath = config["AuthToken:PrivateKeyPath"]  | ||||
|         ?? throw new InvalidOperationException("AuthToken:PrivateKeyPath configuration is missing"); | ||||
|      | ||||
|     public string CreateToken(AuthSession session) | ||||
|     public string CreateToken(SnAuthSession session) | ||||
|     { | ||||
|         // Load the private key for signing | ||||
|         var privateKeyPem = File.ReadAllText(_privateKeyPath); | ||||
|   | ||||
| @@ -6,12 +6,11 @@ using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Options; | ||||
| using System.Text.Json.Serialization; | ||||
| using System.Web; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Controllers; | ||||
|  | ||||
| @@ -98,9 +97,9 @@ public class OidcProviderController( | ||||
|         var clientInfo = new ClientInfoResponse | ||||
|         { | ||||
|             ClientId = Guid.Parse(client.Id), | ||||
|             Picture = client.Picture is not null ? CloudFileReferenceObject.FromProtoValue(client.Picture) : null, | ||||
|             Picture = client.Picture is not null ? SnCloudFileReferenceObject.FromProtoValue(client.Picture) : null, | ||||
|             Background = client.Background is not null | ||||
|                 ? CloudFileReferenceObject.FromProtoValue(client.Background) | ||||
|                 ? SnCloudFileReferenceObject.FromProtoValue(client.Background) | ||||
|                 : null, | ||||
|             ClientName = client.Name, | ||||
|             HomeUri = client.Links.HomePage, | ||||
| @@ -131,7 +130,7 @@ public class OidcProviderController( | ||||
|         [FromForm(Name = "code_challenge_method")] | ||||
|         string? codeChallengeMethod = null) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account account) | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount account) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         // Find the client | ||||
| @@ -303,8 +302,8 @@ public class OidcProviderController( | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> GetUserInfo() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); | ||||
|  | ||||
|         // Get requested scopes from the token | ||||
|         var scopes = currentSession.Challenge?.Scopes ?? []; | ||||
| @@ -337,21 +336,22 @@ public class OidcProviderController( | ||||
|     public IActionResult GetConfiguration() | ||||
|     { | ||||
|         var baseUrl = configuration["BaseUrl"]; | ||||
|         var siteUrl = configuration["SiteUrl"]; | ||||
|         var issuer = options.Value.IssuerUri.TrimEnd('/'); | ||||
|  | ||||
|         return Ok(new | ||||
|         { | ||||
|             issuer, | ||||
|             authorization_endpoint = $"{baseUrl}/auth/authorize", | ||||
|             token_endpoint = $"{baseUrl}/api/auth/open/token", | ||||
|             userinfo_endpoint = $"{baseUrl}/api/auth/open/userinfo", | ||||
|             authorization_endpoint = $"{siteUrl}/auth/authorize", | ||||
|             token_endpoint = $"{baseUrl}/id/auth/open/token", | ||||
|             userinfo_endpoint = $"{baseUrl}/id/auth/open/userinfo", | ||||
|             jwks_uri = $"{baseUrl}/.well-known/jwks", | ||||
|             scopes_supported = new[] { "openid", "profile", "email" }, | ||||
|             response_types_supported = new[] | ||||
|                 { "code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token" }, | ||||
|             grant_types_supported = new[] { "authorization_code", "refresh_token" }, | ||||
|             token_endpoint_auth_methods_supported = new[] { "client_secret_basic", "client_secret_post" }, | ||||
|             id_token_signing_alg_values_supported = new[] { "HS256" }, | ||||
|             id_token_signing_alg_values_supported = new[] { "HS256", "RS256" }, | ||||
|             subject_types_supported = new[] { "public" }, | ||||
|             claims_supported = new[] { "sub", "name", "email", "email_verified" }, | ||||
|             code_challenge_methods_supported = new[] { "S256" }, | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Models; | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||
|  | ||||
| public class ClientInfoResponse | ||||
| { | ||||
|     public Guid ClientId { get; set; } | ||||
|     public CloudFileReferenceObject? Picture { get; set; } | ||||
|     public CloudFileReferenceObject? Background { get; set; } | ||||
|     public SnCloudFileReferenceObject? Picture { get; set; } | ||||
|     public SnCloudFileReferenceObject? Background { get; set; } | ||||
|     public string? ClientName { get; set; } | ||||
|     public string? HomeUri { get; set; } | ||||
|     public string? PolicyUri { get; set; } | ||||
|   | ||||
| @@ -6,12 +6,13 @@ using DysonNetwork.Pass.Auth.OidcProvider.Models; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Options; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
| using AccountContactType = DysonNetwork.Pass.Account.AccountContactType; | ||||
| using AccountContactType = DysonNetwork.Shared.Models.AccountContactType; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||
|  | ||||
| @@ -38,7 +39,7 @@ public class OidcProviderService( | ||||
|         return resp.App ?? null; | ||||
|     } | ||||
|  | ||||
|     public async Task<AuthSession?> FindValidSessionAsync(Guid accountId, Guid clientId, bool withAccount = false) | ||||
|     public async Task<SnAuthSession?> FindValidSessionAsync(Guid accountId, Guid clientId, bool withAccount = false) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|  | ||||
| @@ -57,7 +58,7 @@ public class OidcProviderService( | ||||
|                         s.AppId == clientId && | ||||
|                         (s.ExpiredAt == null || s.ExpiredAt > now) && | ||||
|                         s.Challenge != null && | ||||
|                         s.Challenge.Type == ChallengeType.OAuth) | ||||
|                         s.Challenge.Type == Shared.Models.ChallengeType.OAuth) | ||||
|             .OrderByDescending(s => s.CreatedAt) | ||||
|             .FirstOrDefaultAsync(); | ||||
|     } | ||||
| @@ -80,7 +81,7 @@ public class OidcProviderService( | ||||
|  | ||||
|  | ||||
|         var client = await FindClientByIdAsync(clientId); | ||||
|         if (client?.Status != CustomAppStatus.Production) | ||||
|         if (client?.Status != Shared.Proto.CustomAppStatus.Production) | ||||
|             return true; | ||||
|  | ||||
|         if (client?.OauthConfig?.RedirectUris == null) | ||||
| @@ -145,7 +146,7 @@ public class OidcProviderService( | ||||
|  | ||||
|     private string GenerateIdToken( | ||||
|         CustomApp client, | ||||
|         AuthSession session, | ||||
|         SnAuthSession session, | ||||
|         string? nonce = null, | ||||
|         IEnumerable<string>? scopes = null | ||||
|     ) | ||||
| @@ -199,11 +200,13 @@ public class OidcProviderService( | ||||
|                 claims.Add(new Claim("family_name", session.Account.Profile.LastName)); | ||||
|         } | ||||
|  | ||||
|         claims.Add(new Claim(JwtRegisteredClaimNames.Azp, client.Slug)); | ||||
|  | ||||
|         var tokenDescriptor = new SecurityTokenDescriptor | ||||
|         { | ||||
|             Subject = new ClaimsIdentity(claims), | ||||
|             Issuer = _options.IssuerUri, | ||||
|             Audience = client.Id.ToString(), | ||||
|             Audience = client.Slug.ToString(), | ||||
|             Expires = now.Plus(Duration.FromSeconds(_options.AccessTokenLifetime.TotalSeconds)).ToDateTimeUtc(), | ||||
|             NotBefore = now.ToDateTimeUtc(), | ||||
|             SigningCredentials = new SigningCredentials( | ||||
| @@ -224,11 +227,9 @@ public class OidcProviderService( | ||||
|         Guid? sessionId = null | ||||
|     ) | ||||
|     { | ||||
|         var client = await FindClientByIdAsync(clientId); | ||||
|         if (client == null) | ||||
|             throw new InvalidOperationException("Client not found"); | ||||
|         var client = await FindClientByIdAsync(clientId) ?? throw new InvalidOperationException("Client not found"); | ||||
|  | ||||
|         AuthSession session; | ||||
|         SnAuthSession session; | ||||
|         var clock = SystemClock.Instance; | ||||
|         var now = clock.GetCurrentInstant(); | ||||
|         string? nonce = null; | ||||
| @@ -299,7 +300,7 @@ public class OidcProviderService( | ||||
|  | ||||
|     private string GenerateJwtToken( | ||||
|         CustomApp client, | ||||
|         AuthSession session, | ||||
|         SnAuthSession session, | ||||
|         Instant expiresAt, | ||||
|         IEnumerable<string>? scopes = null | ||||
|     ) | ||||
| @@ -315,6 +316,7 @@ public class OidcProviderService( | ||||
|                 new Claim(JwtRegisteredClaimNames.Jti, session.Id.ToString()), | ||||
|                 new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(), | ||||
|                     ClaimValueTypes.Integer64), | ||||
|                 new Claim(JwtRegisteredClaimNames.Azp, client.Slug), | ||||
|             ]), | ||||
|             Expires = expiresAt.ToDateTimeUtc(), | ||||
|             Issuer = _options.IssuerUri, | ||||
| @@ -371,7 +373,7 @@ public class OidcProviderService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<AuthSession?> FindSessionByIdAsync(Guid sessionId) | ||||
|     public async Task<SnAuthSession?> FindSessionByIdAsync(Guid sessionId) | ||||
|     { | ||||
|         return await db.AuthSessions | ||||
|             .Include(s => s.Account) | ||||
| @@ -379,7 +381,7 @@ public class OidcProviderService( | ||||
|             .FirstOrDefaultAsync(s => s.Id == sessionId); | ||||
|     } | ||||
|  | ||||
|     private static string GenerateRefreshToken(AuthSession session) | ||||
|     private static string GenerateRefreshToken(SnAuthSession session) | ||||
|     { | ||||
|         return Convert.ToBase64String(session.Id.ToByteArray()); | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
|  | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Text.Json.Serialization; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,6 @@ using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using NodaTime; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|  | ||||
| @@ -15,7 +16,8 @@ public class ConnectionController( | ||||
|     IEnumerable<OidcService> oidcServices, | ||||
|     AccountService accounts, | ||||
|     AuthService auth, | ||||
|     ICacheService cache | ||||
|     ICacheService cache, | ||||
|     IConfiguration configuration | ||||
| ) : ControllerBase | ||||
| { | ||||
|     private const string StateCachePrefix = "oidc-state:"; | ||||
| @@ -23,9 +25,9 @@ public class ConnectionController( | ||||
|     private static readonly TimeSpan StateExpiration = TimeSpan.FromMinutes(15); | ||||
|  | ||||
|     [HttpGet] | ||||
|     public async Task<ActionResult<List<AccountConnection>>> GetConnections() | ||||
|     public async Task<ActionResult<List<SnAccountConnection>>> GetConnections() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var connections = await db.AccountConnections | ||||
| @@ -48,7 +50,7 @@ public class ConnectionController( | ||||
|     [HttpDelete("{id:guid}")] | ||||
|     public async Task<ActionResult> RemoveConnection(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var connection = await db.AccountConnections | ||||
| @@ -66,7 +68,7 @@ public class ConnectionController( | ||||
|     [HttpPost("/api/auth/connect/apple/mobile")] | ||||
|     public async Task<ActionResult> ConnectAppleMobile([FromBody] AppleMobileConnectRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         if (GetOidcService("apple") is not AppleOidcService appleService) | ||||
| @@ -99,7 +101,7 @@ public class ConnectionController( | ||||
|                 $"This Apple account is already linked to {(existingConnection.AccountId == currentUser.Id ? "your account" : "another user")}."); | ||||
|         } | ||||
|  | ||||
|         db.AccountConnections.Add(new AccountConnection | ||||
|         db.AccountConnections.Add(new SnAccountConnection | ||||
|         { | ||||
|             AccountId = currentUser.Id, | ||||
|             Provider = "apple", | ||||
| @@ -127,7 +129,7 @@ public class ConnectionController( | ||||
|     } | ||||
|  | ||||
|     [AllowAnonymous] | ||||
|     [Route("/auth/callback/{provider}")] | ||||
|     [Route("/api/auth/callback/{provider}")] | ||||
|     [HttpGet, HttpPost] | ||||
|     public async Task<IActionResult> HandleCallback([FromRoute] string provider) | ||||
|     { | ||||
| @@ -250,7 +252,7 @@ public class ConnectionController( | ||||
|         else | ||||
|         { | ||||
|             // Create new connection | ||||
|             db.AccountConnections.Add(new AccountConnection | ||||
|             db.AccountConnections.Add(new SnAccountConnection | ||||
|             { | ||||
|                 AccountId = accountId, | ||||
|                 Provider = provider, | ||||
| @@ -276,7 +278,9 @@ public class ConnectionController( | ||||
|         var returnUrl = await cache.GetAsync<string>(returnUrlKey); | ||||
|         await cache.RemoveAsync(returnUrlKey); | ||||
|  | ||||
|         return Redirect(string.IsNullOrEmpty(returnUrl) ? "/auth/callback" : returnUrl); | ||||
|         var siteUrl = configuration["SiteUrl"]; | ||||
|  | ||||
|         return Redirect(string.IsNullOrEmpty(returnUrl) ? siteUrl + "/auth/callback" : returnUrl); | ||||
|     } | ||||
|  | ||||
|     private async Task<IActionResult> HandleLoginOrRegistration( | ||||
| @@ -324,7 +328,7 @@ public class ConnectionController( | ||||
|         var account = await accounts.LookupAccount(userInfo.Email) ?? await accounts.CreateAccount(userInfo); | ||||
|  | ||||
|         // Create connection for new or existing user | ||||
|         var newConnection = new AccountConnection | ||||
|         var newConnection = new SnAccountConnection | ||||
|         { | ||||
|             Account = account, | ||||
|             Provider = provider, | ||||
| @@ -340,7 +344,10 @@ public class ConnectionController( | ||||
|  | ||||
|         var loginSession = await auth.CreateSessionForOidcAsync(account, clock.GetCurrentInstant()); | ||||
|         var loginToken = auth.CreateToken(loginSession); | ||||
|         return Redirect($"/auth/callback?token={loginToken}"); | ||||
|  | ||||
|         var siteUrl = configuration["SiteUrl"]; | ||||
|  | ||||
|         return Redirect(siteUrl + $"/auth/callback?token={loginToken}"); | ||||
|     } | ||||
|  | ||||
|     private static async Task<OidcCallbackData> ExtractCallbackData(HttpRequest request) | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|   | ||||
| @@ -1,8 +1,4 @@ | ||||
| using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Net.Http.Json; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
|  | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user