# Private Account/Friend Request Flow

- **Pattern:** `ab-000764` (`community-social-engagement.follow-graph.friend-request-approval`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000764
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000764)

## Why it matters

When a user marks their profile private, they are expressing a consent boundary — covered under GDPR Art. 25 and CCPA §1798.120. An instant-follow model that ignores the private flag grants content access without approval, defeating the user's explicit privacy setting. Under CWE-284, this is a failed authorization check: the platform makes the access control decision on behalf of the user rather than routing it through the user's own approval queue. The fix requires a `FollowRequest` table to hold pending state across the approval workflow.

## Severity rationale

Low because the gap only surfaces when private accounts exist and is not a data exfiltration vector, but it directly violates the user's stated privacy preference.

## Remediation

Add a `FollowRequest` model with a `status` enum to your Prisma schema, then branch your follow logic on the target account's `is_private` flag before deciding whether to create an instant follow or a pending request.

```ts
// lib/follow.ts
export async function requestFollow(followerId: string, followedId: string) {
  const target = await db.user.findUniqueOrThrow({ where: { id: followedId } })

  if (!target.is_private) {
    return db.follow.create({ data: { followerId, followedId } })
  }

  return db.followRequest.create({
    data: { followerId, followedId, status: 'pending' }
  })
}

export async function approveFollowRequest(requestId: string, approverId: string) {
  const req = await db.followRequest.findUniqueOrThrow({ where: { id: requestId } })
  if (req.followedId !== approverId) throw new Error('Unauthorized')

  await db.$transaction([
    db.follow.create({ data: { followerId: req.followerId, followedId: req.followedId } }),
    db.followRequest.update({ where: { id: requestId }, data: { status: 'accepted' } })
  ])
}
```

## Detection

- **ID:** `friend-request-approval`
- **Severity:** `low`
- **What to look for:** Enumerate all relevant files and If profile privacy controls exist (`is_private` field), check for a friend request flow. Look for a `FollowRequest` or `ConnectionRequest` table with a `status` field (pending, accepted, rejected). Check for UI elements allowing users to "Accept" or "Decline" follow requests. Verify that content is blocked until the follow request is approved for private account users.
- **Pass criteria:** At least 1 conforming pattern must exist. If `is_private` is supported, follow requests use a pending status. Users can approve or decline. Content from private accounts is blocked until approval.
- **Fail criteria:** Private accounts exist but follow requests are instant/no approval step, or pending requests are not displayed to the recipient.
- **Skip (N/A) when:** No profile privacy feature, or all profiles are public.
- **Detail on fail:** `"is_private field exists but follow requests are instant — private account follow flow not enforced"` or `"No FollowRequest table; pending follows not tracked"`
- **Remediation:** Implement follow request workflow:

  ```ts
  // Prisma schema
  model FollowRequest {
    id String @id @default(cuid())
    followerId String
    followedId String
    status String @default("pending") // pending, accepted, rejected
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt

    follower User @relation(name: "sent_requests", fields: [followerId], references: [id], onDelete: Cascade)
    followed User @relation(name: "received_requests", fields: [followedId], references: [id], onDelete: Cascade)

    @@unique([followerId, followedId])
  }

  // Follow logic for private accounts
  export async function followUser(followerId: string, followedId: string) {
    const followedUser = await db.user.findUnique({ where: { id: followedId } })

    if (followedUser.is_private) {
      // Create pending request
      return db.followRequest.create({
        data: { followerId, followedId, status: 'pending' }
      })
    } else {
      // Instant follow for public accounts
      return db.follow.create({
        data: { followerId, followedId }
      })
    }
  }

  // Accept follow request
  export async function acceptFollowRequest(requestId: string) {
    const req = await db.followRequest.findUnique({ where: { id: requestId } })
    await db.follow.create({ data: { followerId: req.followerId, followedId: req.followedId } })
    await db.followRequest.update({ where: { id: requestId }, data: { status: 'accepted' } })
  }
  ```

---

## External references

- cwe CWE-284
- gdpr Art. 25
- ccpa §1798.120

Taxons: access-control, privacy-consent

HTML version: https://auditbuffet.com/patterns/ab-000764
