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

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

  1. Profile Images - User profile photos
  2. Education History - Academic credentials and certificates
  3. SCRC - Saudi Commission credentials
  4. Short Bio - Video introductions
  5. Payment Attachments - Payment receipts and invoices
  6. Homework - Care provider assignments
  7. Homework Submissions - Client assignment submissions
  8. Article Images - Platform content images
  9. Agreement Acceptance - Digital signatures and PDFs
  10. Booking Invoices - Session invoices
  11. 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 .pdf 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:

  1. MIME Type Check: Validates Content-Type header
  2. Extension Check: Whitelisted extensions per category
  3. Size Check: 100 MB maximum
  4. 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

  1. Install Prerequisites:
    - Visual Studio 2017/2019
    - SQL Server 2016+
    - .NET Framework 4.7.2 SDK

  2. 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
    

  3. Update Web.config:
    - Set connection string to local database
    - Configure media storage path
    - Set custom errors mode to “Off” for debugging

  4. Run in Debug Mode:
    - Press F5 in Visual Studio
    - Default URL: http://localhost:47721/

Adding New Upload Category

  1. Add to Enum (Models/Enums.cs):

    public enum MediaCategory
    {
        // ... existing categories
        [Description("NewCategory")]
        NewCategory = 12
    }
    

  2. 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;
    

  3. Add Extension Whitelist:

    private static string GetAllowedExtensions(MediaCategory mediaCategory)
    {
        switch (mediaCategory)
        {
            // ... existing cases
            case MediaCategory.NewCategory:
                return ".pdf,.doc,.docx";
            default:
                return string.Empty;
        }
    }
    

  4. 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

  1. Publish from Visual Studio:
    - Right-click project → Publish
    - Target: Folder
    - Configuration: Release
    - Publish

  2. 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"
    

  3. Configure Permissions:

    icacls "D:\inetpub\PsyterMediaAPI" /grant "IIS AppPool\PsyterMediaAPI":(OI)(CI)RX
    icacls "D:\Media\Psyter" /grant "IIS AppPool\PsyterMediaAPI":(OI)(CI)F
    

  4. Update Web.config:
    - Set production connection string
    - Set customErrors mode="On"
    - Set debug="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