# Frameshot API Client Integration Guide

## Overview

This guide explains how to update the Flameshot desktop client to work with the new secure API that includes:
- Device registration with API keys
- Session token management
- Request signature verification
- Optional user authentication (JWT)

## API Changes Summary

### Old API (Simple)
```
POST /api/v1/upload
Content-Type: application/json
{
  "image": "data:image/png;base64,...",
  "user_id": "1"
}
```

### New API (Secure)
```
POST /api/v1/upload
X-API-Key: your_api_key
X-Session-Token: your_session_token
X-Request-Signature: hmac_signature
X-Request-Timestamp: unix_timestamp
Authorization: Bearer jwt_token (optional)
Content-Type: application/json
{
  "image": "data:image/png;base64,...",
  "metadata": {
    "app_name": "flameshot",
    "window_title": "Screenshot",
    "url": null
  }
}
```

## Implementation Steps

### Step 1: Add Required Headers to Your Project

Add these new header files to handle the security features:

**tunti35config.h** - Configuration management
**tunti35devicemanager.h** - Device registration and session management
**tunti35auth.h** - Authentication and signature generation

### Step 2: Configuration Storage

Store these values securely in your application settings:

```cpp
// In your config handler or settings class
class Tunti35Config {
public:
    // Embedded API key (hardcoded in application)
    static QString apiKey() {
        return "a7f3c8e9d2b4f6a1c5e8d9f2b3a6c7e4d8f1a2b5c6e9f3a7b4c8d1e5f2a9b6c3";
    }
    
    // Device ID (generated once and stored)
    QString deviceId();
    void setDeviceId(const QString& id);
    
    // Session token (from device registration)
    QString sessionToken();
    void setSessionToken(const QString& token);
    
    // JWT token (optional, from user login)
    QString jwtToken();
    void setJwtToken(const QString& token);
    
    // Check if device is registered
    bool isDeviceRegistered();
};
```

### Step 3: Device Registration

Implement device registration (one-time setup):

```cpp
// tunti35devicemanager.cpp
void Tunti35DeviceManager::registerDevice() {
    // Generate or retrieve device ID
    QString deviceId = getOrCreateDeviceId();
    
    // Create registration payload
    QJsonObject payload;
    payload["device_id"] = deviceId;
    
    QJsonObject deviceInfo;
    deviceInfo["platform"] = "desktop";
    deviceInfo["version"] = QApplication::applicationVersion();
    deviceInfo["os"] = QSysInfo::prettyProductName();
    payload["device_info"] = deviceInfo;
    
    QJsonDocument doc(payload);
    QByteArray postData = doc.toJson();
    
    // Create request
    QUrl url("https://prn.tf/api/v1/register");
    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    request.setRawHeader("X-API-Key", Tunti35Config::apiKey().toUtf8());
    
    // Send request
    QNetworkReply* reply = m_networkManager->post(request, postData);
    connect(reply, &QNetworkReply::finished, this, &Tunti35DeviceManager::handleRegistrationReply);
}

void Tunti35DeviceManager::handleRegistrationReply() {
    QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
    reply->deleteLater();
    
    if (reply->error() == QNetworkReply::NoError) {
        QJsonDocument response = QJsonDocument::fromJson(reply->readAll());
        QJsonObject json = response.object();
        
        if (json.value("success").toBool()) {
            QJsonObject data = json.value("data").toObject();
            QString sessionToken = data.value("session_token").toString();
            QString deviceId = data.value("device_id").toString();
            
            // Save to config
            m_config->setDeviceId(deviceId);
            m_config->setSessionToken(sessionToken);
            
            emit registrationSuccess();
        } else {
            emit registrationFailed(json.value("message").toString());
        }
    } else {
        emit registrationFailed(reply->errorString());
    }
}

QString Tunti35DeviceManager::getOrCreateDeviceId() {
    QString deviceId = m_config->deviceId();
    if (deviceId.isEmpty()) {
        // Generate UUID
        deviceId = QUuid::createUuid().toString(QUuid::WithoutBraces);
        m_config->setDeviceId(deviceId);
    }
    return deviceId;
}
```

### Step 4: Request Signature Generation

Implement HMAC-SHA256 signature:

```cpp
// tunti35auth.cpp
#include <QCryptographicHash>
#include <QMessageAuthenticationCode>

QString Tunti35Auth::generateSignature(const QString& apiKey, 
                                        qint64 timestamp, 
                                        const QByteArray& body) {
    // Create message: timestamp:body
    QString message = QString::number(timestamp) + ":" + QString::fromUtf8(body);
    
    // Generate HMAC-SHA256
    QByteArray key = apiKey.toUtf8();
    QByteArray data = message.toUtf8();
    
    QMessageAuthenticationCode mac(QCryptographicHash::Sha256);
    mac.setKey(key);
    mac.addData(data);
    
    return mac.result().toHex();
}

qint64 Tunti35Auth::getCurrentTimestamp() {
    return QDateTime::currentSecsSinceEpoch();
}
```

### Step 5: Update the Upload Function

Update `tunti35uploader.cpp`:

```cpp
void Tunti35Uploader::upload() {
    // Check if device is registered
    if (!m_config->isDeviceRegistered()) {
        setInfoLabelText(tr("Device not registered. Registering..."));
        
        // Register device first
        m_deviceManager->registerDevice();
        connect(m_deviceManager, &Tunti35DeviceManager::registrationSuccess, 
                this, &Tunti35Uploader::upload);
        connect(m_deviceManager, &Tunti35DeviceManager::registrationFailed,
                this, [this](const QString& error) {
            setInfoLabelText(tr("Registration failed: ") + error);
        });
        return;
    }
    
    // Convert pixmap to base64
    QByteArray byteArray;
    QBuffer buffer(&byteArray);
    buffer.open(QIODevice::WriteOnly);
    pixmap().save(&buffer, "PNG");
    QString base64Image = QString("data:image/png;base64,") + byteArray.toBase64();
    
    // Create JSON payload
    QJsonObject jsonPayload;
    jsonPayload["image"] = base64Image;
    
    // Add metadata
    QJsonObject metadata;
    metadata["app_name"] = "flameshot";
    metadata["version"] = QApplication::applicationVersion();
    metadata["platform"] = "desktop";
    jsonPayload["metadata"] = metadata;
    
    QJsonDocument doc(jsonPayload);
    QByteArray postData = doc.toJson();
    
    // Generate timestamp and signature
    qint64 timestamp = Tunti35Auth::getCurrentTimestamp();
    QString signature = Tunti35Auth::generateSignature(
        Tunti35Config::apiKey(),
        timestamp,
        postData
    );
    
    // Store request info for debugging
    m_requestInfo = QString(
        "REQUEST INFO:\n"
        "URL: https://prn.tf/api/v1/upload\n"
        "Method: POST\n"
        "X-API-Key: %1\n"
        "X-Session-Token: %2\n"
        "X-Request-Signature: %3\n"
        "X-Request-Timestamp: %4\n"
        "Payload size: %5 bytes\n"
    ).arg(Tunti35Config::apiKey().left(16) + "...")
     .arg(m_config->sessionToken().left(16) + "...")
     .arg(signature.left(16) + "...")
     .arg(timestamp)
     .arg(postData.size());
    
    setInfoLabelText(m_requestInfo + "\n\nUploading...");
    
    // Create request
    QUrl url("https://prn.tf/api/v1/upload");
    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    request.setHeader(QNetworkRequest::UserAgentHeader, 
                     "Flameshot/" + QApplication::applicationVersion());
    
    // Add security headers
    request.setRawHeader("X-API-Key", Tunti35Config::apiKey().toUtf8());
    request.setRawHeader("X-Session-Token", m_config->sessionToken().toUtf8());
    request.setRawHeader("X-Request-Signature", signature.toUtf8());
    request.setRawHeader("X-Request-Timestamp", QByteArray::number(timestamp));
    
    // Add JWT token if user is logged in (optional)
    QString jwtToken = m_config->jwtToken();
    if (!jwtToken.isEmpty()) {
        request.setRawHeader("Authorization", ("Bearer " + jwtToken).toUtf8());
    }
    
    // Set timeout (30 seconds)
    request.setTransferTimeout(30000);
    
    // Send POST request
    QNetworkReply* reply = m_NetworkAM->post(request, postData);
    
    // Handle network errors
    connect(reply, &QNetworkReply::errorOccurred, this, [this, reply]() {
        if (spinner()) {
            spinner()->deleteLater();
        }
        setInfoLabelText(tr("Network error: ") + reply->errorString());
    });
}
```

### Step 6: Handle New Error Responses

Update error handling for new error codes:

```cpp
void Tunti35Uploader::handleReply(QNetworkReply* reply) {
    reply->deleteLater();
    
    if (spinner()) {
        spinner()->deleteLater();
    }
    
    QByteArray responseData = reply->readAll();
    int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    
    if (reply->error() == QNetworkReply::NoError) {
        QJsonDocument response = QJsonDocument::fromJson(responseData);
        QJsonObject json = response.object();
        
        if (json.value("success").toBool()) {
            // Success - same as before
            QJsonObject data = json.value("data").toObject();
            m_shareUrl = data.value("share_url").toString();
            
            // Save to history and show share dialog
            // ... (same as before)
            
        } else {
            handleError(json);
        }
    } else {
        // Network error
        QString errorMsg = reply->errorString();
        setInfoLabelText(tr("Network error: ") + errorMsg);
    }
}

void Tunti35Uploader::handleError(const QJsonObject& json) {
    QJsonObject error = json.value("error").toObject();
    QString errorCode = error.value("code").toString();
    QString errorMessage = error.value("message").toString();
    
    // Handle specific error codes
    if (errorCode == "INVALID_SESSION" || errorCode == "DEVICE_NOT_REGISTERED") {
        // Session expired or device not registered - re-register
        setInfoLabelText(tr("Session expired. Re-registering device..."));
        m_config->setSessionToken("");
        QTimer::singleShot(1000, this, &Tunti35Uploader::upload);
        
    } else if (errorCode == "RATE_LIMIT_EXCEEDED") {
        // Rate limit exceeded
        setInfoLabelText(tr("Rate limit exceeded. Please wait and try again."));
        
    } else if (errorCode == "INVALID_SIGNATURE") {
        // Signature verification failed
        setInfoLabelText(tr("Security verification failed. Please update the app."));
        
    } else if (errorCode == "INVALID_JWT") {
        // JWT expired - clear it
        m_config->setJwtToken("");
        setInfoLabelText(tr("Login expired. Uploading as anonymous user..."));
        QTimer::singleShot(1000, this, &Tunti35Uploader::upload);
        
    } else {
        // Generic error
        setInfoLabelText(tr("Upload failed: ") + errorMessage);
    }
    
    new QShortcut(Qt::Key_Escape, this, SLOT(close()));
}
```

### Step 7: Optional User Login

Add user login functionality (optional):

```cpp
void Tunti35Auth::login(const QString& username, const QString& password) {
    QJsonObject payload;
    payload["username"] = username;
    payload["password"] = password;
    
    QJsonDocument doc(payload);
    QByteArray postData = doc.toJson();
    
    QUrl url("https://prn.tf/api/v1/login");
    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    request.setRawHeader("X-API-Key", Tunti35Config::apiKey().toUtf8());
    request.setRawHeader("X-Session-Token", m_config->sessionToken().toUtf8());
    
    QNetworkReply* reply = m_networkManager->post(request, postData);
    connect(reply, &QNetworkReply::finished, this, &Tunti35Auth::handleLoginReply);
}

void Tunti35Auth::handleLoginReply() {
    QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
    reply->deleteLater();
    
    if (reply->error() == QNetworkReply::NoError) {
        QJsonDocument response = QJsonDocument::fromJson(reply->readAll());
        QJsonObject json = response.object();
        
        if (json.value("success").toBool()) {
            QString jwtToken = json.value("token").toString();
            m_config->setJwtToken(jwtToken);
            emit loginSuccess();
        } else {
            emit loginFailed(json.value("message").toString());
        }
    } else {
        emit loginFailed(reply->errorString());
    }
}
```

## Configuration File Structure

Store settings in your application config:

```ini
[Tunti35]
device_id=550e8400-e29b-41d4-a716-446655440000
session_token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6
jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

## Testing Checklist

- [ ] Device registration works on first run
- [ ] Session token is saved and reused
- [ ] Upload works with valid session
- [ ] Signature generation is correct
- [ ] Error handling for expired sessions
- [ ] Error handling for rate limits
- [ ] Optional user login works
- [ ] JWT token is included when logged in
- [ ] Session recovery after expiration

## API Endpoints Reference

### 1. Device Registration
```
POST /api/v1/register
Headers:
  X-API-Key: your_api_key
Body:
  {
    "device_id": "uuid",
    "device_info": {
      "platform": "desktop",
      "version": "1.0.0",
      "os": "Windows 11"
    }
  }
Response:
  {
    "success": true,
    "data": {
      "device_id": "uuid",
      "session_token": "token",
      "expires_at": "2024-02-26T12:00:00Z"
    }
  }
```

### 2. User Login (Optional)
```
POST /api/v1/login
Headers:
  X-API-Key: your_api_key
  X-Session-Token: your_session_token
Body:
  {
    "username": "user",
    "password": "pass"
  }
Response:
  {
    "success": true,
    "token": "jwt_token",
    "user": {
      "id": 1,
      "username": "user",
      "email": "user@example.com"
    }
  }
```

### 3. Upload Image
```
POST /api/v1/upload
Headers:
  X-API-Key: your_api_key
  X-Session-Token: your_session_token
  X-Request-Signature: hmac_signature
  X-Request-Timestamp: unix_timestamp
  Authorization: Bearer jwt_token (optional)
Body:
  {
    "image": "data:image/png;base64,...",
    "metadata": {
      "app_name": "flameshot",
      "window_title": "Screenshot",
      "url": null
    }
  }
Response:
  {
    "success": true,
    "data": {
      "id": 123,
      "url": "/uploads/frameshots/2024/01/abc123.png",
      "filename": "abc123.png",
      "share_url": "https://prn.tf/SJfjjdD",
      "short_code": "SJfjjdD"
    }
  }
```

## Error Codes

| Code | Description | Action |
|------|-------------|--------|
| `INVALID_API_KEY` | API key is invalid | Update app with correct key |
| `INVALID_SESSION` | Session token expired | Re-register device |
| `DEVICE_NOT_REGISTERED` | Device not found | Register device |
| `INVALID_SIGNATURE` | Signature verification failed | Check signature generation |
| `TIMESTAMP_EXPIRED` | Request timestamp too old | Check system clock |
| `RATE_LIMIT_EXCEEDED` | Too many requests | Wait and retry |
| `INVALID_JWT` | JWT token expired | Re-login or continue as anonymous |

## Security Best Practices

1. **API Key**: Hardcode in application (obfuscate if possible)
2. **Device ID**: Generate once, store securely
3. **Session Token**: Store securely, refresh when expired
4. **JWT Token**: Optional, store securely, clear on logout
5. **Signature**: Generate fresh for each request
6. **Timestamp**: Use current time, check system clock

## Migration Path

1. **Phase 1**: Add device registration on first run
2. **Phase 2**: Update upload to use new headers
3. **Phase 3**: Add signature generation
4. **Phase 4**: Add error handling for new codes
5. **Phase 5**: (Optional) Add user login UI

## Support

For issues or questions:
- Check the OpenAPI documentation: `src/api/openapi.json`
- Test with: `https://prn.tf/scripts/test_security.php`
- Review requirements: `.kiro/specs/api-security/requirements.md`
