External profile links (max 5 per user)
Users can attach up to 5 labelled URLs to their profile — social
handles, blog, anything. They're shown on /<username>/list (the
opt-in public list), behind target="_blank" rel="noopener noreferrer
ugc" anchors so the destination tab can't script back into our
window.
Schema:
- user_links(id, user_id, label, url, position, created_at)
UNIQUE(user_id, position) to keep ordering stable, ON DELETE
CASCADE so user deletion sweeps links.
Wire:
- UserLink type (id/label/url)
- MeResponse.links: UserLink[]
- PublicListResponse.links: UserLink[]
- ProfileUpdateRequest.links?: { label, url }[] — bulk replace
- USER_LINK_LIMITS exported so frontend constraints match server
Validation (server/auth.ts):
- Label 1-40 chars, trimmed
- URL parseable + http:// or https:// only (no javascript:, data:,
mailto:, etc.)
- URL ≤ 500 chars
- Max 5 links per user
Bulk replace semantics with up-front validation, then DELETE +
INSERT inside the same transaction as the user UPDATE. A username
UNIQUE violation rolls back the link changes too — no half-applied
state. Empty rows in the request are silently dropped so users
can leave half-typed entries without a server rejection.
Frontend Profile gets an "Eksterne lenker" section between the
FriendsPanel and the Eksporter section. Five label+URL row pairs
with add/remove buttons, save button, error → Bokmål mapping
(link_label_required, link_url_bad_protocol, etc.).
93 tests still pass; typecheck clean; build ok.
This commit is contained in:
parent
7964d499e2
commit
9b825bfe1d
6 changed files with 258 additions and 11 deletions
|
|
@ -94,6 +94,20 @@ export interface RecoveryCompleteRequest {
|
|||
dek_pw_nonce: string;
|
||||
}
|
||||
|
||||
/** A user's external profile link — social, blog, anything they want. */
|
||||
export interface UserLink {
|
||||
id: string;
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** Limits enforced server-side; surfaced so the client UI matches. */
|
||||
export const USER_LINK_LIMITS = {
|
||||
maxPerUser: 5,
|
||||
labelMax: 40,
|
||||
urlMax: 500,
|
||||
} as const;
|
||||
|
||||
export interface MeResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
|
|
@ -102,6 +116,7 @@ export interface MeResponse {
|
|||
is_admin: boolean;
|
||||
username: string | null;
|
||||
public_list_enabled: boolean;
|
||||
links: UserLink[];
|
||||
}
|
||||
|
||||
// --- Admin -----------------------------------------------------------------
|
||||
|
|
@ -124,16 +139,21 @@ export interface AdminRoleUpdate {
|
|||
}
|
||||
|
||||
export interface ProfileUpdateRequest {
|
||||
// All optional — omit a field to leave it alone. Pass `null` to clear.
|
||||
// All optional — omit a field to leave it alone. Pass `null` to clear
|
||||
// display_name or username. The `links` field, when present, replaces
|
||||
// the user's entire link list (bulk replace semantics — simpler than
|
||||
// tracking per-link ids on the client).
|
||||
display_name?: string | null;
|
||||
username?: string | null;
|
||||
public_list_enabled?: boolean;
|
||||
links?: { label: string; url: string }[];
|
||||
}
|
||||
|
||||
/** Response shape for GET /api/users/:username/list (opt-in public list). */
|
||||
export interface PublicListResponse {
|
||||
username: string;
|
||||
display_name: string | null;
|
||||
links: UserLink[];
|
||||
activities: ActivityPublic[];
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue