Compare commits
1 commit
main
...
fix/profil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a2f0bf089 |
3 changed files with 23 additions and 19 deletions
|
|
@ -31,17 +31,16 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||||
const isOwn = currentUser?.id === user.id;
|
const isOwn = currentUser?.id === user.id;
|
||||||
|
|
||||||
// Profile-visibility gate: a `private` profile 404s for everyone but
|
// Profile-visibility gate: a `private` profile 404s for everyone but
|
||||||
// the owner, regardless of how much public content they have.
|
// the owner, regardless of how much public content they have. With
|
||||||
|
// explicit `profile_visibility` we no longer 404 public profiles that
|
||||||
|
// happen to have zero public items — the page renders with empty
|
||||||
|
// sections, matching Mastodon-style "discoverable account, no posts
|
||||||
|
// yet" behavior. Existence-leak risk is accepted: explicit visibility
|
||||||
|
// is the contract.
|
||||||
if (!isOwn && user.profileVisibility !== "public") {
|
if (!isOwn && user.profileVisibility !== "public") {
|
||||||
throw data({ error: "User not found" }, { status: 404 });
|
throw data({ error: "User not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 404 for public-but-empty profiles to prevent account enumeration.
|
|
||||||
// Owners still see their own profile even when empty.
|
|
||||||
if (!isOwn && publicRoutes.length === 0 && publicActivities.length === 0) {
|
|
||||||
throw data({ error: "User not found" }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Follow state for non-owner viewers (null when anonymous).
|
// Follow state for non-owner viewers (null when anonymous).
|
||||||
const followState = !isOwn && currentUser
|
const followState = !isOwn && currentUser
|
||||||
? await getFollowState(currentUser.id, user.username)
|
? await getFollowState(currentUser.id, user.username)
|
||||||
|
|
|
||||||
|
|
@ -36,14 +36,14 @@ Add `users.profile_visibility: 'public' | 'private'` (NOT NULL, default `'public
|
||||||
|
|
||||||
The new rules:
|
The new rules:
|
||||||
|
|
||||||
- `/users/:username` returns 200 iff `profile_visibility = 'public'` **AND** the user has at least one `public` route or activity. The "has public content" gate is preserved so a brand-new public-by-default account doesn't expose a 200 page that says "no posts yet" — that would leak existence.
|
- `/users/:username` returns 200 iff `profile_visibility = 'public'`. A public profile with zero `public` items renders an empty shell (header + empty-state copy in the routes/activities sections), matching Mastodon-style "discoverable account, no posts yet" behavior. The explicit `profile_visibility` toggle is the contract for hiding the profile; existence of a public-but-empty account is observable, and that's accepted.
|
||||||
- A user is followable iff `profile_visibility = 'public'`, regardless of whether they have content yet. (Following someone before they post is reasonable; the follower's feed just stays empty for that follow until the user posts.)
|
- A user is followable iff `profile_visibility = 'public'`, regardless of whether they have content yet. Following someone before they post is reasonable; the follower's feed just stays empty for that follow until the user posts.
|
||||||
|
|
||||||
When `social-federation` lands, the local user's ActivityPub actor object will gate on the same `profile_visibility = 'public'` check — private profiles will return 404 to federation lookups too.
|
When `social-federation` lands, the local user's ActivityPub actor object will gate on the same `profile_visibility = 'public'` check — private profiles will return 404 to federation lookups too.
|
||||||
|
|
||||||
**Default `'public'`:** matches fediverse convention (Mastodon defaults to discoverable; "lock" is opt-in), aligns with the existing implicit behavior where any user *could* be public, and keeps onboarding smooth (post a public route → it's listed on your profile, no extra toggle). Activity-level privacy still defaults to `'private'`, so content stays private by default; *being findable on the network* is the part we default open.
|
**Default `'public'`:** matches fediverse convention (Mastodon defaults to discoverable; "lock" is opt-in), aligns with the existing implicit behavior where any user *could* be public, and keeps onboarding smooth (post a public route → it's listed on your profile, no extra toggle). Activity-level privacy still defaults to `'private'`, so content stays private by default; *being findable on the network* is the part we default open.
|
||||||
|
|
||||||
**Migration:** backfill all existing users to `'public'`. Their effective profile reachability is unchanged (still gated on having public content). Operators can flip themselves to `'private'` post-migration if desired.
|
**Migration:** backfill all existing users to `'public'`. Operators can flip themselves to `'private'` post-migration if desired.
|
||||||
|
|
||||||
**Why explicit, why now:** with follows landing, the question "can someone follow you?" needs a deterministic answer. Deriving it from "do you have any public content?" is fragile (toggling content visibility silently flips followability). The toggle also pre-pays for `locked-local-accounts`, which will extend this enum or add a `users.locked` flag.
|
**Why explicit, why now:** with follows landing, the question "can someone follow you?" needs a deterministic answer. Deriving it from "do you have any public content?" is fragile (toggling content visibility silently flips followability). The toggle also pre-pays for `locked-local-accounts`, which will extend this enum or add a `users.locked` flag.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,27 +17,32 @@ Every user SHALL have an explicit `profile_visibility` of `public` or `private`.
|
||||||
|
|
||||||
#### Scenario: User toggles profile back to public
|
#### Scenario: User toggles profile back to public
|
||||||
- **WHEN** a previously-private user switches `profile_visibility` to `public` and saves
|
- **WHEN** a previously-private user switches `profile_visibility` to `public` and saves
|
||||||
- **THEN** their `/users/:username` becomes reachable again (subject to the existing "has public content" gate) and Follow buttons reappear for visitors
|
- **THEN** their `/users/:username` becomes reachable again and Follow buttons reappear for visitors
|
||||||
|
|
||||||
## MODIFIED Requirements
|
## MODIFIED Requirements
|
||||||
|
|
||||||
### Requirement: Public profile page
|
### Requirement: Public profile page
|
||||||
The Journal SHALL serve a public profile page at `/users/:username` that lists the user's public routes and activities in reverse chronological order, viewable without authentication. The page SHALL render only when the user's `profile_visibility` is `public` AND they have at least one `public` route or activity. For signed-in viewers other than the owner, the page SHALL display a Follow / Unfollow toggle that mirrors the current follow relation (see `social-follows` spec). The page SHALL also display follower and following counts with links to the respective collections.
|
The Journal SHALL serve a public profile page at `/users/:username` that lists the user's public routes and activities in reverse chronological order, viewable without authentication. The page SHALL render whenever the user's `profile_visibility` is `public`, even if they have no public routes or activities yet — the empty case shows the profile shell with empty section copy. For signed-in viewers other than the owner, the page SHALL display a Follow / Unfollow toggle that mirrors the current follow relation (see `social-follows` spec). The page SHALL also display follower and following counts with links to the respective collections.
|
||||||
|
|
||||||
#### Scenario: Logged-out visitor views a public profile with public content
|
#### Scenario: Logged-out visitor views a public profile
|
||||||
- **WHEN** an unauthenticated visitor navigates to `/users/:username` for a user whose `profile_visibility` is `public` and who has at least one `public` route or activity
|
- **WHEN** an unauthenticated visitor navigates to `/users/:username` for a user whose `profile_visibility` is `public`
|
||||||
- **THEN** the page renders that user's display name (falling back to username), the `@username@domain` handle, follower and following counts, and a reverse-chronological list of their `public` routes and `public` activities
|
- **THEN** the page renders that user's display name (falling back to username), the `@username@domain` handle, follower and following counts, and a reverse-chronological list of their `public` routes and `public` activities
|
||||||
- **AND** items marked `unlisted` or `private` do NOT appear in the list
|
- **AND** items marked `unlisted` or `private` do NOT appear in the list
|
||||||
|
|
||||||
#### Scenario: Profile 404 cases are indistinguishable
|
#### Scenario: Empty public profile renders an empty shell
|
||||||
- **WHEN** a visitor navigates to `/users/:username` for any of: a user with `profile_visibility = 'private'`, a user with `profile_visibility = 'public'` but zero public items, a user whose content is all `private` or `unlisted`, or a username that does not exist
|
- **WHEN** an unauthenticated visitor navigates to `/users/:username` for a user with `profile_visibility = 'public'` but no `public` routes or activities
|
||||||
|
- **THEN** the page returns HTTP 200 and renders the profile header (display name, handle, counts) plus empty-state copy in the routes and activities sections
|
||||||
|
- **AND** the user is followable: the Follow button (for signed-in viewers other than the owner) is rendered
|
||||||
|
|
||||||
|
#### Scenario: Profile 404 cases
|
||||||
|
- **WHEN** a visitor navigates to `/users/:username` for a user with `profile_visibility = 'private'` OR a username that does not exist
|
||||||
- **THEN** the server responds with HTTP 404
|
- **THEN** the server responds with HTTP 404
|
||||||
- **AND** the response does NOT distinguish the cases, so existence of a private account is not leaked
|
- **AND** the response does NOT distinguish the two cases, so existence of a private account is not leaked
|
||||||
|
|
||||||
#### Scenario: Owner sees their own profile
|
#### Scenario: Owner sees their own profile
|
||||||
- **WHEN** a user navigates to their own `/users/:username` while logged in
|
- **WHEN** a user navigates to their own `/users/:username` while logged in
|
||||||
- **THEN** if their `profile_visibility = 'public'` and they have at least one public item, the page renders exactly the same as for a logged-out visitor, plus a small owner-only control strip linking to settings
|
- **THEN** if their `profile_visibility = 'public'`, the page renders exactly the same as for a logged-out visitor, plus a small owner-only control strip linking to settings
|
||||||
- **AND** if their profile would 404 for visitors (private or no public content), they are redirected to settings or shown an owner-only "your profile isn't public yet" view (implementation detail)
|
- **AND** if their `profile_visibility = 'private'`, the page renders for them with an amber explainer banner noting that visitors see a 404
|
||||||
- **AND** no Follow button is shown (users cannot follow themselves)
|
- **AND** no Follow button is shown (users cannot follow themselves)
|
||||||
|
|
||||||
#### Scenario: Signed-in viewer sees a Follow control on a public profile
|
#### Scenario: Signed-in viewer sees a Follow control on a public profile
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue