Psyter Media Upload API¶
Version: 1.0
Framework: ASP.NET Web API (.NET Framework 4.7.2)
Purpose: Dedicated microservice for media file uploads, PDF generation, and media management
Table of Contents¶
- Overview
- Features
- Prerequisites
- Installation & Setup
- Configuration
- API Documentation
- Authentication
- File Upload Guidelines
- PDF Generation
- Troubleshooting
- Development Guide
- Deployment
- FAQ
Overview¶
The Psyter Media Upload API is a specialized ASP.NET Web API service that handles all media-related operations for the Psyter telemedicine platform. It provides secure file upload, validation, storage, and PDF generation capabilities with OAuth 2.0 authentication.
Key Capabilities¶
- 🔐 Secure Authentication: OAuth 2.0 bearer token authentication
- 📁 File Management: Upload, validate, and delete media files
- 📄 PDF Generation: Create bilingual agreement PDFs with digital signatures
- ✅ File Validation: Multi-layer validation (MIME type, extension, size, content)
- 🗂️ Organized Storage: User-specific directory structure
- 🌐 CORS Enabled: Cross-origin resource sharing support
Supported File Categories¶
- Profile Images - User profile photos
- Education History - Academic credentials and certificates
- SCRC - Saudi Commission credentials
- Short Bio - Video introductions
- Payment Attachments - Payment receipts and invoices
- Homework - Care provider assignments
- Homework Submissions - Client assignment submissions
- Article Images - Platform content images
- Agreement Acceptance - Digital signatures and PDFs
- Booking Invoices - Session invoices
- National ID - Government identification documents
Features¶
File Upload Features¶
- ✅ Multiple file formats supported (images, documents, videos, PDFs)
- ✅ File size limit: 100 MB per file
- ✅ Content-based validation (prevents file type spoofing)
- ✅ GUID-based file naming (prevents conflicts)
- ✅ User-specific directory organization
- ✅ Automatic directory creation
Security Features¶
- ✅ OAuth 2.0 bearer token authentication
- ✅ Encrypted database connection strings
- ✅ File extension whitelist validation
- ✅ MIME type verification
- ✅ Base64 signature validation
- ✅ File size limits
PDF Generation Features¶
- ✅ Bilingual agreement PDFs (English + Arabic)
- ✅ HTML template-based generation
- ✅ Custom headers and footers
- ✅ Digital signature embedding
- ✅ RTL text support for Arabic
- ✅ Automatic font selection
Prerequisites¶
Development Environment¶
- Visual Studio 2017+ (VS 2019 recommended)
- .NET Framework 4.7.2 SDK
- SQL Server 2016+ (with Psyter database)
- IIS 10+ or IIS Express
Runtime Requirements¶
- Windows Server 2012 R2+ or Windows 10+
- .NET Framework 4.7.2 Runtime
- IIS with ASP.NET 4.x support
- SQL Server access (local or remote)
- File system write permissions
NuGet Packages¶
All dependencies are managed via NuGet (see packages.config). Key packages:
- Microsoft.AspNet.WebApi (5.2.7)
- Microsoft.Owin.Security.OAuth (3.1.0)
- iTextSharp (5.5.13.4)
- Newtonsoft.Json (11.0.2)
- AutoMapper (7.0.1)
- log4net (2.0.8)
Installation & Setup¶
1. Clone or Download Repository¶
git clone <repository-url>
cd Media/PsyterMediaUploadAPI
2. Restore NuGet Packages¶
Option A: Visual Studio
1. Open PsyterMediaUploadAPI.sln
2. Right-click solution → “Restore NuGet Packages”
Option B: Command Line
nuget restore PsyterMediaUploadAPI.sln
3. Configure Database Connection¶
Edit Web.config and update the connection string:
<connectionStrings>
<add name="PsyterDatabase"
connectionString="Data Source={encrypted};Initial Catalog={encrypted};User Id={encrypted};Password={encrypted};"
providerName="System.Data.SqlClient"/>
</connectionStrings>
Note: Connection string components are encrypted using custom encryption. See Configuration section.
4. Configure Media Storage Path¶
The media storage path is configured in the database:
-- Check current configuration
SELECT * FROM AppConfiguration
WHERE GroupId = 11 AND PropertyId = 32
-- Update path if needed
UPDATE AppConfiguration
SET PropertyValue = 'D:\Media\Psyter\'
WHERE PropertyId = 32
5. Create Media Directory¶
Create the physical directory for media storage:
mkdir D:\Media\Psyter
# Grant write permissions to IIS App Pool identity
icacls "D:\Media\Psyter" /grant "IIS AppPool\YourAppPoolName":(OI)(CI)F
6. Build Solution¶
Visual Studio:
- Build → Build Solution (Ctrl+Shift+B)
MSBuild:
msbuild PsyterMediaUploadAPI.sln /p:Configuration=Release
7. Run Locally¶
IIS Express (Development):
- Press F5 in Visual Studio
- Default URL: http://localhost:47721/
Local IIS:
1. Create new Application Pool (.NET 4.x, Integrated pipeline)
2. Create new Website/Application
3. Point to PsyterMediaUploadAPI directory
4. Browse to test
Configuration¶
Web.config Settings¶
Application Settings¶
<appSettings>
<!-- Max file size in bytes (100 MB) -->
<add key="MaxFileSize" value="104857600" />
<!-- SQL command timeout in seconds -->
<add key="commandTimeout" value="600" />
</appSettings>
Request Size Limits¶
<system.web>
<!-- ASP.NET request limit (100 MB) -->
<httpRuntime maxRequestLength="104857600" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<!-- IIS request limit (100 MB) -->
<requestLimits maxAllowedContentLength="104857600" />
</requestFiltering>
</security>
</system.webServer>
Database Configuration¶
The connection string is encrypted using a custom encryption scheme:
Encryption Format:
Data Source=<encrypted_server>;
Initial Catalog=<encrypted_database>;
User Id=<encrypted_user>;
Password=<encrypted_password>;
Persist Security Info=True
To encrypt connection string components:
using PsyterMediaUploadAPI.Helper;
string server = SecurityHelper.EncryptString("your-server");
string database = SecurityHelper.EncryptString("your-database");
string userId = SecurityHelper.EncryptString("your-user");
string password = SecurityHelper.EncryptString("your-password");
OAuth Configuration¶
Configured in App_Start/Startup.cs:
OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true, // ⚠️ Set to false in production
TokenEndpointPath = new PathString("/authenticate"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = myProvider,
RefreshTokenProvider = new RefreshTokenProvider()
};
⚠️ Production Recommendation: Set AllowInsecureHttp = false and use HTTPS only.
CORS Configuration¶
CORS is configured to allow all origins:
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
⚠️ Production Recommendation: Restrict to specific origins:
var corsPolicy = new CorsPolicy
{
AllowAnyHeader = true,
AllowAnyMethod = true
};
corsPolicy.Origins.Add("https://yourdomain.com");
app.UseCors(new CorsOptions { PolicyProvider = ... });
Environment-Specific Configuration¶
Development:
- Use first connection string (currently active)
- Custom errors mode: Off
- Debug compilation: On
Production:
- Use second connection string (currently commented)
- Custom errors mode: On
- Debug compilation: Off
- Enable HTTPS-only OAuth
API Documentation¶
Base URL¶
Development: http://localhost:47721/
Production: https://your-domain.com/media-api/
Authentication Endpoint¶
POST /authenticate¶
Obtain OAuth 2.0 bearer token for API access.
Request:
POST /authenticate HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&ApplicationToken={token-from-main-api}
Parameters:
- grant_type (required): Must be “client_credentials”
- ApplicationToken (required): Application token obtained from main Psyter API
Response (Success):
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "bearer",
"expires_in": 86400
}
Response (Error):
{
"error": "invalid_grant",
"error_description": "Provided application token is invalid"
}
Media Operations¶
All media endpoints require bearer token authentication:
Authorization: Bearer {access_token}
POST /Media/UploadMedia¶
Upload one or more media files.
Request:
POST /Media/UploadMedia HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="UserId"
123
------WebKitFormBoundary
Content-Disposition: form-data; name="UserType"
1
------WebKitFormBoundary
Content-Disposition: form-data; name="UploadCategory"
1
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="profile.jpg"
Content-Type: image/jpeg
{binary file data}
------WebKitFormBoundary--
Form Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| UserId | string | Yes | User identifier |
| UserType | int | Yes | 0=Client, 1=CareProvider, 2=Admin |
| UploadCategory | int | Yes | MediaCategory enum value (see below) |
| HomeWorkId | string | Conditional | Required for categories 6 & 7 |
| UserFullName | string | Conditional | Required for category 9 |
| Culture | string | Optional | Language code (en-US, ar-SA) |
| file(s) | file | Yes | One or more files |
UploadCategory Values:
1 = ProfileImage
2 = EducationHistory
3 = SCRC
4 = ShortBio
5 = PaymentAttachment
6 = HomeWork
7 = HomeWorkSubmission
8 = ActicleImages
9 = AgreementAcceptance
10 = BookingInvoices
11 = NationalID
File Restrictions:
| Category | Max Files | Allowed Extensions | Max Size |
|---|---|---|---|
| ProfileImage | 1 | .png, .jpg, .jpeg | 100 MB |
| EducationHistory | 3 | .doc, .docx, .xlsx, .pdf, .jpg, .jpeg, .png | 100 MB |
| SCRC | Multiple | .png, .jpg, .jpeg, .pdf | 100 MB |
| ShortBio | 1 | .mp4 | 100 MB |
| PaymentAttachment | 1 | .doc, .docx, .xlsx, .pdf, .jpg, .jpeg, .png | 100 MB |
| HomeWork | Multiple | .doc, .docx, .xlsx, .pdf, .jpg, .jpeg, .png, .txt | 100 MB |
| HomeWorkSubmission | Multiple | .doc, .docx, .xlsx, .pdf, .jpg, .jpeg, .png, .txt | 100 MB |
| ActicleImages | Multiple | .png, .jpg, .jpeg | 100 MB |
| AgreementAcceptance | 1 | .png, .jpg, .jpeg | 100 MB |
| BookingInvoices | Multiple | 100 MB | |
| NationalID | Multiple | .png, .jpg, .jpeg | 100 MB |
Response (Success):
{
"Data": [
{
"FileName": "ProfileImage_a3f2b1c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o.jpg",
"FilePath": "/Media/CareProvider/User_123/ProfileImage/ProfileImage_a3f2b1c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o.jpg",
"FileType": ".jpg"
}
],
"Status": 1,
"Reason": 1,
"ReasonText": "Success"
}
Response (Error):
{
"Data": null,
"Status": 0,
"Reason": 12,
"ReasonText": "File extension not allowed."
}
Error Codes:
- 2 - Empty parameters
- 3 - Invalid parameters
- 7 - Physical directory not found
- 8 - Invalid file MIME type
- 9 - Can’t upload more than 1 file
- 10 - Can’t upload more than 3 files
- 11 - File size exceeds limit
- 12 - File extension not allowed
- 13 - File MIME type not allowed
POST /Media/DeleteMediaFile¶
Delete an uploaded media file.
Request:
POST /Media/DeleteMediaFile HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: application/json
{
"UserId": 123,
"MediaId": 456,
"MediaCategory": 6
}
Parameters:
- UserId (required): User identifier
- MediaId (required): Database ID of media file
- MediaCategory (required): Category enum value
Response (Success):
{
"Data": null,
"Status": 1,
"Reason": 1,
"ReasonText": "Success"
}
Response (Cannot Delete):
{
"Data": null,
"Status": 1,
"Reason": 6,
"ReasonText": "Can not delete"
}
Note: Only HomeWork and HomeWorkSubmission categories support deletion via this endpoint.
POST /Media/RegenrateAgreement¶
Regenerate agreement PDF for a care provider.
Request:
POST /Media/RegenrateAgreement HTTP/1.1
Authorization: Bearer {access_token}
Content-Type: application/json
{
"UserLoginInfoId": 123,
"FullName": "Dr. Ahmed Al-Rashid",
"SignatureMediaPath": "/Media/CareProvider/User_123/AgreementAcceptance/signature.png"
}
Parameters:
- UserLoginInfoId (required): User login info ID
- FullName (required): Full name for agreement
- SignatureMediaPath (required): Path to signature image
Response:
{
"Data": {
"FileName": "AgreementAcceptance_guid.pdf",
"FilePath": "/Media/CareProvider/User_123/AgreementAcceptance/AgreementAcceptance_guid.pdf",
"FileType": ".pdf"
},
"Status": 1,
"Reason": 1,
"ReasonText": "Success"
}
Authentication¶
Authentication Flow¶
1. Client authenticates with main Psyter API
└─> Receives ApplicationToken
2. Client requests bearer token from Media API
POST /authenticate
Body: grant_type=client_credentials&ApplicationToken={token}
└─> Receives access_token
3. Client uses access_token for subsequent requests
Authorization: Bearer {access_token}
└─> Token valid for 24 hours
Implementation Examples¶
C# Example¶
using System.Net.Http;
using System.Threading.Tasks;
public async Task<string> GetBearerToken(string applicationToken)
{
using (var client = new HttpClient())
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("ApplicationToken", applicationToken)
});
var response = await client.PostAsync(
"http://localhost:47721/authenticate",
content);
var result = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(result);
return tokenResponse.access_token;
}
}
public class TokenResponse
{
public string access_token { get; set; }
public string token_type { get; set; }
public int expires_in { get; set; }
}
JavaScript Example¶
async function getBearerToken(applicationToken) {
const formData = new URLSearchParams();
formData.append('grant_type', 'client_credentials');
formData.append('ApplicationToken', applicationToken);
const response = await fetch('http://localhost:47721/authenticate', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
const data = await response.json();
return data.access_token;
}
cURL Example¶
curl -X POST http://localhost:47721/authenticate \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&ApplicationToken=your-app-token"
File Upload Guidelines¶
Client Implementation¶
C# Example (HttpClient)¶
public async Task<UploadResponse> UploadProfileImage(
string bearerToken,
string userId,
byte[] imageData,
string fileName)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", bearerToken);
using (var content = new MultipartFormDataContent())
{
content.Add(new StringContent(userId), "UserId");
content.Add(new StringContent("1"), "UserType"); // CareProvider
content.Add(new StringContent("1"), "UploadCategory"); // ProfileImage
var fileContent = new ByteArrayContent(imageData);
fileContent.Headers.ContentType =
MediaTypeHeaderValue.Parse("image/jpeg");
content.Add(fileContent, "file", fileName);
var response = await client.PostAsync(
"http://localhost:47721/Media/UploadMedia",
content);
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<UploadResponse>(json);
}
}
}
JavaScript Example (FormData)¶
async function uploadFile(bearerToken, userId, file, category) {
const formData = new FormData();
formData.append('UserId', userId);
formData.append('UserType', '1'); // CareProvider
formData.append('UploadCategory', category.toString());
formData.append('file', file);
const response = await fetch('http://localhost:47721/Media/UploadMedia', {
method: 'POST',
headers: {
'Authorization': `Bearer ${bearerToken}`
},
body: formData
});
return await response.json();
}
// Usage
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const result = await uploadFile(token, '123', file, 1);
console.log('Uploaded:', result.Data[0].FilePath);
Android Example (Retrofit)¶
// Retrofit interface
@Multipart
@POST("Media/UploadMedia")
Call<UploadResponse> uploadFile(
@Header("Authorization") String authorization,
@Part("UserId") RequestBody userId,
@Part("UserType") RequestBody userType,
@Part("UploadCategory") RequestBody category,
@Part MultipartBody.Part file
);
// Implementation
RequestBody userIdBody = RequestBody.create(
MediaType.parse("text/plain"), "123");
RequestBody userTypeBody = RequestBody.create(
MediaType.parse("text/plain"), "1");
RequestBody categoryBody = RequestBody.create(
MediaType.parse("text/plain"), "1");
File file = new File(filePath);
RequestBody requestFile = RequestBody.create(
MediaType.parse("image/jpeg"), file);
MultipartBody.Part body = MultipartBody.Part.createFormData(
"file", file.getName(), requestFile);
Call<UploadResponse> call = apiService.uploadFile(
"Bearer " + token,
userIdBody,
userTypeBody,
categoryBody,
body
);
File Validation¶
The API performs multi-layer validation:
- MIME Type Check: Validates
Content-Typeheader - Extension Check: Whitelisted extensions per category
- Size Check: 100 MB maximum
- Content Signature: Validates actual file content
Validation Example:
File: malicious.jpg
Extension: .jpg ✅
MIME Type: image/jpeg ✅
Size: 50 KB ✅
Content Signature: JVBE (PDF) ❌ REJECTED
The file is actually a PDF renamed to .jpg
PDF Generation¶
Agreement PDF Process¶
The system automatically generates bilingual agreement PDFs when uploading signature images.
Steps:
1. Upload signature image with category AgreementAcceptance
2. System retrieves agreement template from database
3. Generates English PDF with signature
4. Generates Arabic PDF with signature
5. Combines both PDFs (Arabic first, then English)
6. Saves combined PDF
7. Updates database with file paths
Template Customization¶
Agreement templates are located in:
/Content/html_templates/
├── agreement_template.html # English template
├── agreement_template_ar.html # Arabic template
├── footer-logo.png
└── logo.png
Placeholders:
- {USER_NAME} - User’s full name
- {AGREEMENT_CONTENT} - Agreement text from database
- {IMG_SIGNATURE} - Signature image URL
- {CURRENT_DATE} - Current date (UTC+3)
- {BASE_URL} - Application base URL
- {NAME_DIRECTION} - RTL CSS for Arabic names
Example Template:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { font-family: Arial, sans-serif; }
.signature { margin-top: 20px; }
</style>
</head>
<body>
<h1>Service Provider Agreement</h1>
<div class="content">
{AGREEMENT_CONTENT}
</div>
<div class="user-info" style="{NAME_DIRECTION}">
<p><strong>Name:</strong> {USER_NAME}</p>
<p><strong>Date:</strong> {CURRENT_DATE}</p>
</div>
<div class="signature">
<p><strong>Signature:</strong></p>
<img src="{IMG_SIGNATURE}" width="200" />
</div>
</body>
</html>
Fonts¶
Custom fonts for PDF generation:
- ProximaNovaA-Regular.ttf - English text
- Alexandria-Regular.ttf - Arabic text
- NotoSansArabic-Regular.ttf - Arabic fallback
Fonts are embedded in PDFs for consistent rendering.
Troubleshooting¶
Common Issues¶
Issue: “Physical directory not found” error¶
Cause: Media storage path not configured or directory doesn’t exist
Solution:
-- Check configuration
SELECT * FROM AppConfiguration WHERE PropertyId = 32
-- Update if needed
UPDATE AppConfiguration
SET PropertyValue = 'D:\Media\Psyter\'
WHERE PropertyId = 32
-- Create directory
mkdir D:\Media\Psyter
icacls "D:\Media\Psyter" /grant "IIS AppPool\YourAppPool":(OI)(CI)F
Issue: “File extension not allowed” error¶
Cause: Uploaded file extension not in whitelist for category
Solution: Check allowed extensions for your upload category (see API Documentation)
Issue: “File MIME type not allowed” error¶
Cause: File content doesn’t match extension (file type spoofing)
Solution: Ensure uploaded file is actually the type indicated by extension
Issue: “Provided application token is invalid” error¶
Cause: ApplicationToken expired or invalid
Solution: Obtain new ApplicationToken from main Psyter API
Issue: 413 Request Entity Too Large¶
Cause: File exceeds size limits
Solution:
1. Check file size (max 100 MB)
2. Verify maxRequestLength and maxAllowedContentLength in Web.config
3. Check IIS request filtering settings
Issue: Cannot upload files - permission denied¶
Cause: IIS app pool doesn’t have write permissions
Solution:
# Grant write permissions to IIS app pool
icacls "D:\Media\Psyter" /grant "IIS AppPool\YourAppPoolName":(OI)(CI)F
Issue: PDF generation fails¶
Cause: Missing fonts or templates
Solution:
1. Verify fonts exist in /Content/fonts/
2. Verify templates exist in /Content/html_templates/
3. Check database for agreement content
Issue: Database connection fails¶
Cause: Connection string encryption/configuration issue
Solution:
1. Verify encrypted connection string in Web.config
2. Test connection using SQL Server Management Studio
3. Check SQL Server allows remote connections
4. Verify firewall rules
Development Guide¶
Setting Up Development Environment¶
-
Install Prerequisites:
- Visual Studio 2017/2019
- SQL Server 2016+
- .NET Framework 4.7.2 SDK -
Configure Local Database:
-- Run database scripts from main API -- Ensure stored procedures exist: -- - User_Authenticate_MediaAPI -- - AppConfig_GetAppConfigSettingsByGroupId -- - HW_SaveHomeWorkFilesDetail_FromMediaServer -- - HW_DeleteHomeWorkFile -- - SP_GetUserAgreementData -- - SP_UpdatetUserAgreementFilePath -
Update Web.config:
- Set connection string to local database
- Configure media storage path
- Set custom errors mode to “Off” for debugging -
Run in Debug Mode:
- Press F5 in Visual Studio
- Default URL: http://localhost:47721/
Adding New Upload Category¶
-
Add to Enum (
Models/Enums.cs):
public enum MediaCategory { // ... existing categories [Description("NewCategory")] NewCategory = 12 } -
Add Path Mapping (
Controllers/MediaController.cs):
case MediaCategory.NewCategory: relativePath = "/Media/" + userType.ToDescription() + "/User_" + userId + "/" + MediaCategory.NewCategory.ToDescription() + "/"; physicalPath = MediaPhysicalPath + "\\Media\\" + userType.ToDescription() + "\\User_" + userId + "\\" + MediaCategory.NewCategory.ToDescription(); break; -
Add Extension Whitelist:
private static string GetAllowedExtensions(MediaCategory mediaCategory) { switch (mediaCategory) { // ... existing cases case MediaCategory.NewCategory: return ".pdf,.doc,.docx"; default: return string.Empty; } } -
Test Upload:
POST /Media/UploadMedia FormData: UserId: 123 UserType: 1 UploadCategory: 12 file: test.pdf
Code Style Guidelines¶
- Use PascalCase for public members
- Use camelCase for parameters and local variables
- Use async/await for I/O operations
- Dispose database connections in finally blocks
- Use try-catch in all controller methods
- Return proper BaseResponse objects
Testing¶
Manual Testing:
1. Use Postman or similar tool
2. Test authentication flow first
3. Test each upload category
4. Test file validation (size, type, content)
5. Test error scenarios
Recommended Test Cases:
- ✅ Valid file upload
- ✅ Invalid file extension
- ✅ File size exceeds limit
- ✅ Missing required parameters
- ✅ Invalid bearer token
- ✅ File type spoofing attempt
- ✅ PDF generation with signature
- ✅ File deletion
Deployment¶
IIS Deployment¶
-
Publish from Visual Studio:
- Right-click project → Publish
- Target: Folder
- Configuration: Release
- Publish -
Create IIS Application:
# Create Application Pool New-WebAppPool -Name "PsyterMediaAPI" Set-ItemProperty IIS:\AppPools\PsyterMediaAPI -Name managedRuntimeVersion -Value "v4.0" # Create Application New-WebApplication -Name "MediaAPI" ` -Site "Default Web Site" ` -PhysicalPath "D:\inetpub\PsyterMediaAPI" ` -ApplicationPool "PsyterMediaAPI" -
Configure Permissions:
icacls "D:\inetpub\PsyterMediaAPI" /grant "IIS AppPool\PsyterMediaAPI":(OI)(CI)RX icacls "D:\Media\Psyter" /grant "IIS AppPool\PsyterMediaAPI":(OI)(CI)F -
Update Web.config:
- Set production connection string
- SetcustomErrors mode="On"
- Setdebug="false"in compilation
- Enable HTTPS-only OAuth
Azure DevOps Deployment¶
Current pipeline configuration (azure-pipelines.yml):
trigger:
- master
pool: 'DevWebServerAgentPool'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild@1
inputs:
solution: '$(solution)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: PublishBuildArtifacts@1
- task: CopyFiles@2
inputs:
SourceFolder: '$(Build.SourcesDirectory)\PsyterMediaUploadAPI'
TargetFolder: 'D:\ROOT\Development\Psyter\Master\MediaAPIs'
OverWrite: true
Environment Configuration¶
Development:
- HTTP allowed
- Debug enabled
- Custom errors off
- Local file storage
Staging:
- HTTPS recommended
- Debug disabled
- Custom errors on
- Network file storage
Production:
- HTTPS required
- Debug disabled
- Custom errors on
- CDN for file delivery (recommended)
- Load balancer support
FAQ¶
General Questions¶
Q: What is the maximum file size?
A: 100 MB per file by default. Configurable via MaxFileSize in Web.config.
Q: Can I upload multiple files at once?
A: Yes, except for ProfileImage, ShortBio, and PaymentAttachment categories which allow only one file.
Q: How long are bearer tokens valid?
A: 24 hours (86400 seconds). Configurable in Startup.cs.
Q: What file types are supported?
A: Images (.jpg, .jpeg, .png), Documents (.pdf, .doc, .docx, .xlsx, .txt), Videos (.mp4)
Technical Questions¶
Q: How does file validation work?
A: Four layers: MIME type check, extension whitelist, size limit, and content signature verification.
Q: Where are files stored?
A: Physical directory configured in database (AppConfiguration table, PropertyId=32).
Q: Are files encrypted at rest?
A: No, files are stored as-is. Encryption at rest should be handled at OS/storage level.
Q: Can I use this API without the main Psyter API?
A: No, you need an ApplicationToken from the main API to authenticate.
Q: How do I regenerate an agreement PDF?
A: Use the /Media/RegenrateAgreement endpoint with UserLoginInfoId, FullName, and SignatureMediaPath.
Troubleshooting Questions¶
Q: Upload fails with 401 Unauthorized
A: Check bearer token validity. Request new token if expired.
Q: Upload fails with “Physical directory not found”
A: Configure media storage path in database and create directory with proper permissions.
Q: PDF generation fails
A: Check fonts exist in /Content/fonts/, templates exist in /Content/html_templates/, and database has agreement content.
Q: How do I enable HTTPS?
A: Configure IIS with SSL certificate, update AllowInsecureHttp = false in Startup.cs, and use HTTPS URLs.
Support & Contact¶
Repository: Psyter Media Upload API
Documentation Version: 1.0
Last Updated: November 10, 2025
For issues, feature requests, or questions:
- Check Troubleshooting section
- Review API Documentation
- Contact Psyter development team
License¶
© Psyter Platform - All Rights Reserved