Partner Onboarding

Summary

Admins register new data providers (promoteurs, aggregators) via the admin API. Each partner receives a dedicated chroot-jailed Linux SFTP user under the sftp-feeds group, a cryptographically random password (AES-256-GCM encrypted in the database), and an IngestionSource record for the feed importer. The plaintext password is shown once at creation time for the admin to share with the partner. Subsequent operations allow metadata updates, SFTP username rename, password regeneration, and full deletion.

State Diagram

stateDiagram-v2
    state "Validation des données" as Validating
    state "Demande rejetée" as Rejected
    state "Création utilisateur SFTP" as SftpUserCreating
    state "Erreur SFTP" as SftpFailed
    state "Transaction base de données" as DbTransacting
    state "Erreur base de données" as DbFailed
    state "Partenaire créé" as Active
    [*] --> Validating: admin creates partner
    Validating --> Rejected: name invalid or sftpUsername taken
    Validating --> SftpUserCreating: createSftpUser(username, password)
    SftpUserCreating --> SftpFailed: OS error
    SftpUserCreating --> DbTransacting: Partner + IngestionSource create
    DbTransacting --> DbFailed: DB error → deleteSftpUser (cleanup)
    DbTransacting --> Active: partner.status = pending_mapping
    Active --> [*]

Steps

1. Authenticate Admin (Actor: system)

authenticateAdmin preHandler validates the admin JWT (ADMIN_JWT_SECRET, 15 min HS256).

2. Derive and Validate SFTP Username (Actor: system)

sanitizeUsername(name) converts the partner display name to a safe Linux username (lowercase, alphanumeric + hyphens). If the result is empty, 400 INVALID_NAME is returned. Uniqueness is checked against partner.sftpUsername in the DB; conflict returns 409 USERNAME_EXISTS.

Outcome: Valid, unique SFTP username derived

3. Generate Password and Create Linux User (Actor: system)

randomBytes(18).toString("base64") generates a 24-character cryptographically random password. createSftpUser(username, password) from lib/sftp.ts creates a chroot-jailed Linux user at /home/{username}/feeds/. If this OS call fails, 500 SFTP_ERROR is returned immediately — no DB record is created.

Outcome: Linux SFTP user exists on the server

4. DB Transaction — Partner + IngestionSource (Actor: system)

Prisma $transaction:

  1. partner.create with sftpPassword: encrypt(password) (AES-256-GCM), feedPath: /home/{username}/feeds/, status: "pending_mapping".
  2. ingestionSource.create linking to the partner with config: { feedPath, feedFormat }.

If the transaction fails: deleteSftpUser(username) is called as best-effort cleanup, then 500 is returned.

Outcome: Partner + IngestionSource in DB, partner status is pending_mapping

5. Return Credentials (Actor: admin)

Response includes { partner: { ...fields, sftpPassword: plaintextPassword } }. The plaintext password is only exposed at this moment — subsequent reads of the partner never include it. Admin must relay credentials to the partner out-of-band.

Outcome: Admin has SFTP credentials to share with partner

6. Update Partner Metadata (Actor: admin)

PUT /admin/partners/:id — updates name, contactEmail, website, feedFormat, photoBaseUrl, status. Does not touch the SFTP account. Returns partner without encrypted password.

7. Rename SFTP Username (Actor: admin)

POST /admin/partners/:id/rename-sftp:

  1. Validate new username format.
  2. Check uniqueness.
  3. renameSftpUser(oldUsername, newUsername) — kills active sessions, moves home directory.
  4. Update partner.sftpUsername, partner.feedPath, and ingestionSource.config in DB.

8. Regenerate Password (Actor: admin)

POST /admin/partners/:id/regenerate-password:

  1. Generate new randomBytes(18) password.
  2. changeSftpPassword(username, newPassword).
  3. partner.update({ sftpPassword: encrypt(newPassword) }).
  4. Return plaintext password once.

9. Delete Partner (Actor: admin)

DELETE /admin/partners/:id:

  1. deleteSftpUser(username) — must succeed first.
  2. partner.delete — cascades to IngestionSource via FK.

Error: If SFTP deletion fails, DB record is not deleted to avoid orphaned Linux users.

Error States

  • Name produces empty sanitized username → 400 INVALID_NAME
  • SFTP username already taken → 409 USERNAME_EXISTS
  • Linux user creation fails → 500 SFTP_ERROR (no DB record created)
  • DB transaction fails → 500 DB_ERROR, Linux user cleaned up best-effort
  • Partner not found → 404 NOT_FOUND