v4: a remote account's links to its home lead to the home's pages — the settings' "Change it on <host>" button and the profile's "Account on <host>" link went to the host in the handle, which is the home's issuer, its API, not necessarily where its pages are: in development musique's button led to localhost:8000/settings, gerbeur's API, which served a stale build, instead of localhost:3000/settings. GET /api/users/:key/home?path= now redirects to a page of a remote user's home, their profile there by default, at the origin of the home's authorization_endpoint, which its metadata gives (homePageUrl in federation-service.ts), the issuer standing in while the home doesn't answer; a local user 404s, and a path that isn't one 400s. homeUrl links to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,8 @@
|
||||
* blocked one they may not, and from any other only with an invite, which the
|
||||
* register page carries. None may while the deployment turns sign-ins from
|
||||
* other instances off (`FEDERATION.incoming`). Once here, an account works like any other, but its
|
||||
* password, email, avatar and name are its home's (`assertLocalUser`).
|
||||
* password, email, avatar and name are its home's (`assertLocalUser`), and
|
||||
* change on its pages there ({@link homePageUrl}).
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -40,7 +41,8 @@ import {
|
||||
isIdentityClaims,
|
||||
parseInstance,
|
||||
} from "../../shared/federation.ts";
|
||||
import { EMAIL_RE, USERNAME_RE } from "../../shared/validation.ts";
|
||||
import { userPath } from "../../shared/paths.ts";
|
||||
import { EMAIL_RE, splitHandle, USERNAME_RE } from "../../shared/validation.ts";
|
||||
import { toPublicUser, type User } from "../model/user.ts";
|
||||
import {
|
||||
exchangeCode,
|
||||
@@ -447,3 +449,35 @@ export async function receiveEvent(token: string): Promise<void> {
|
||||
if (!claims) throw new APIException(400, "Invalid event");
|
||||
await applyClaims(identity.user_id, issuer, claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of `path` on the home of `user`, an account from another
|
||||
* instance, for `GET /api/users/:key/home`: their profile there by default,
|
||||
* or another of its pages, such as `/settings`, where they change their
|
||||
* password. A handle's host is its home's issuer, the API's origin, which
|
||||
* needn't be its pages': those are where its `/authorize` page is, and the
|
||||
* home's metadata says. While it doesn't answer, the issuer stands in.
|
||||
*
|
||||
* @throws {APIException} 400 when `path` isn't a path; 404 when `user` is
|
||||
* local.
|
||||
*/
|
||||
export async function homePageUrl(user: User, path?: string): Promise<string> {
|
||||
const identity = db.prepare(
|
||||
`SELECT issuer FROM user_identities WHERE user_id = ?;`,
|
||||
).get(user.id) as { issuer: string } | undefined;
|
||||
if (!identity) throw new APIException(404, "This account lives here");
|
||||
if (path !== undefined && (!path.startsWith("/") || path.startsWith("//"))) {
|
||||
throw new APIException(400, "Not a path");
|
||||
}
|
||||
|
||||
let pages = identity.issuer;
|
||||
try {
|
||||
const { origin, protocol } = new URL(
|
||||
(await fetchMetadata(identity.issuer)).authorization_endpoint,
|
||||
);
|
||||
if (protocol === "https:" || protocol === "http:") pages = origin;
|
||||
} catch (err) {
|
||||
console.error(`[federation] ${identity.issuer} unreachable:`, err);
|
||||
}
|
||||
return `${pages}${path ?? userPath(splitHandle(user.username).name)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user