Friends + friends-only visibility + blocking
A fourth visibility level ("Venner") with one-way friendship and
two-way block filtering, plus the table-rebuild migration that drags
older dev DBs forward.
Visibility model:
- Friendship is directional: (owner, friend) means owner has added
friend to their list. Owner's friends-only activities become
visible to friend; friend isn't automatically friends with owner.
- Blocking is also directional at the DB level (blocker, blocked)
but is checked SYMMETRICALLY at visibility-resolution time: once
either user has blocked the other, friends-only content stops
flowing in either direction. Block does NOT affect public or
anonymous content — those are open to anyone by definition.
- "Friends-only" is an access-list visibility, NOT cryptographic.
The server stores the content in plaintext and serves it only to
authorised viewers. This is documented honestly in /personvern.
Schema:
- activities.visibility CHECK gains 'friends' as a fourth value
- friends(owner_id, friend_id, created_at) — composite PK,
self-friending blocked by CHECK
- user_blocks(blocker_id, blocked_id, created_at) — same shape,
blocking-self also blocked
Migration (server/db.ts):
- SQLite can't ALTER a CHECK constraint, so the migration detects
out-of-date DBs by scanning sqlite_master for the literal
"'friends'" in the activities table's CREATE statement
- If absent, rebuilds activities via the standard SQLite
table-copy-drop-rename dance with foreign_keys briefly off
around the transaction, then runs foreign_key_check to confirm
no FKs were left orphaned (activity_tags, activity_hearts,
bookmarks all point at activities). Smoke-tested on the dev DB:
olemd's user row and moderator/admin flags survived.
Server endpoints (server/friends.ts):
GET /api/friends — my outgoing list
GET /api/friends/incoming — who has added ME
POST /api/friends — add by username (idempotent)
DELETE /api/friends/:userId — remove a friend
GET /api/friends/blocks — my blocked-users list
POST /api/friends/blocks — block by user_id (idempotent)
DELETE /api/friends/blocks/:userId — unblock
Add-by-username (not by email): users must set a username to be
findable. Email stays a private contact identifier.
Activity list filter (server/activities.ts): adds two clauses to the
WHERE — own friends-only, and friends-only owned by a user who has
added me AND there's no block in either direction. Single-activity
GET applies the same check.
Frontend:
- ActivityForm.svelte gains the "Venner" option
- ActivityRow.svelte renders a "Venner" badge with a new amber
vis-badge.friends colour (passes contrast in both themes)
- FriendsPanel.svelte: add-by-username form, outgoing, incoming
(with Block button), and blocked (with Unblock button)
- Profile.svelte mounts FriendsPanel between display fields and
Eksporter
- Home.svelte adds a "Venner" section between private and semi
Docs: Personvern.svelte gains a "Venner og blokkering" section
explaining that friends-only is access-list-not-crypto and pointing
the reader at "private" for actually-sensitive content.
26 tests still pass; typecheck clean; build succeeds. Bundle
36.8 KB → 39.1 KB gzipped (FriendsPanel + new server endpoints +
the Personvern prose).
This commit is contained in:
parent
79ce7059c1
commit
f39fe9ed65
14 changed files with 657 additions and 8 deletions
|
|
@ -2,7 +2,7 @@
|
|||
// the SQL schema in server/db.ts and the request/response handlers in
|
||||
// server/{auth,activities,tags}.ts.
|
||||
|
||||
export type Visibility = 'private' | 'semi' | 'public';
|
||||
export type Visibility = 'private' | 'semi' | 'public' | 'friends';
|
||||
|
||||
// --- Auth --------------------------------------------------------------------
|
||||
export interface SignupRequest {
|
||||
|
|
@ -238,7 +238,53 @@ export interface ActivityPrivate {
|
|||
updated_at: number;
|
||||
}
|
||||
|
||||
export type Activity = ActivityPublic | ActivitySemi | ActivityPrivate;
|
||||
/**
|
||||
* Friends-only activity. Shape mirrors ActivityPublic — the viewer is
|
||||
* definitionally a friend of the owner, so attribution is fine. NOT
|
||||
* encrypted; this is an access-list visibility, not a cryptographic one.
|
||||
* See /personvern for the trade-off.
|
||||
*/
|
||||
export interface ActivityFriends {
|
||||
id: string;
|
||||
visibility: 'friends';
|
||||
owner_id: string;
|
||||
owner_display: string | null;
|
||||
owner_username: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
tags: string[];
|
||||
loc_label: string | null;
|
||||
loc_lat: number | null;
|
||||
loc_lng: number | null;
|
||||
scheduled_at: number | null;
|
||||
heart_count: number;
|
||||
viewer_hearted: boolean;
|
||||
viewer_bookmarked: boolean;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export type Activity = ActivityPublic | ActivitySemi | ActivityPrivate | ActivityFriends;
|
||||
|
||||
// --- Friends & blocks ------------------------------------------------------
|
||||
/** Both the outgoing-friends list and the incoming-friends list. */
|
||||
export interface FriendEntry {
|
||||
user_id: string;
|
||||
username: string | null;
|
||||
display_name: string | null;
|
||||
since: number;
|
||||
}
|
||||
|
||||
export interface BlockEntry {
|
||||
user_id: string;
|
||||
username: string | null;
|
||||
display_name: string | null;
|
||||
since: number;
|
||||
}
|
||||
|
||||
export interface AddByUsernameRequest {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface CreateActivityRequest {
|
||||
visibility: Visibility;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue