OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / handlers / profile.js

97 lines · javascript ref:
/**
 * Profile RPC handlers.
 *
 *   get-profile { userId? }
 *     Read-only. Defaults to the caller's own user (resolved from the
 *     session publicKey via the registry). Returns the User record,
 *     linked identities, and the subset of identities that are SSH keys
 *     with on-disk comment enrichment.
 *
 *   update-profile { name?, profilePicture? }
 *     Writes to the caller's own user only. Ignores any userId field
 *     in params (server resolves it from session). Any authenticated
 *     user may edit their own profile — read-only globalPermissions
 *     do not block self-edits. UDS callers must pass { userId }.
 *
 * profilePicture is a hyper-overlay content hash (like `/attachments/<hash>`
 * but stored as just the hash string). The frontend uploads via
 * `/api/upload`, gets the hash back, and includes it in update-profile.
 */

import { AuthError } from '../auth/check.js'
import { listLinkedSSHKeys } from '../auth/ssh-identity.js'

export function profileHandlers (svc) {
  const registry = () => svc.identityRegistry

  /**
   * Resolve the caller's own userId from session. Returns null on UDS
   * (UDS has no user concept — caller must supply userId for UDS reads).
   */
  async function resolveCallerUserId (ctx) {
    if (!ctx || !ctx.identity || !ctx.identity.publicKey) return null
    const user = await registry().getUserByIdentity(ctx.identity.publicKey).catch(() => null)
    return user ? user.userId : null
  }

  return [
    {
      name: 'get-profile',
      fn: async (ctx, params = {}) => {
        let userId = params && params.userId
        if (!userId) userId = await resolveCallerUserId(ctx)
        if (!userId) {
          if (ctx && ctx.transport === 'uds') {
            throw new Error('get-profile requires { userId } when called over UDS')
          }
          throw new AuthError('not authenticated')
        }

        const result = await registry().getUserWithIdentities(userId).catch(() => null)
        if (!result || !result.user) throw new Error('user not found: ' + userId)

        const linkedSSH = await listLinkedSSHKeys(svc, userId).catch(() => [])
        return { user: result.user, identities: result.identities, linkedSSH }
      }
    },

    {
      name: 'update-profile',
      fn: async (ctx, params = {}) => {
        // Resolve userId from session — IGNORE any userId in params. This
        // prevents "update someone else's profile" via a crafted payload.
        // UDS callers are a special case: they can target any user (they're
        // the local daemon admin). HTTP callers can only edit their own
        // profile, so no writer-permission check is needed — read-only
        // users must still be able to set their display name + picture.
        let userId
        if (ctx && ctx.transport === 'uds') {
          userId = params && params.userId
          if (!userId) throw new Error('update-profile over UDS requires { userId }')
        } else {
          userId = await resolveCallerUserId(ctx)
          if (!userId) throw new AuthError('not authenticated')
        }

        const updates = {}
        if (typeof params.name === 'string') {
          const trimmed = params.name.trim()
          if (trimmed.length > 50) throw new Error('name exceeds 50 characters')
          updates.name = trimmed
        }
        if (typeof params.profilePicture === 'string' || params.profilePicture === null) {
          updates.profilePicture = params.profilePicture || null
        }

        if (Object.keys(updates).length === 0) {
          throw new Error('update-profile requires at least one of { name, profilePicture }')
        }

        await registry().updateUser(userId, updates)
        const fresh = await registry().getUser(userId)
        return { user: fresh }
      }
    }
  ]
}