diff --git a/bun.lock b/bun.lock index 69883e2..25fe17e 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "@profullstack/cli": "^0.1.0", "@profullstack/footer": "latest", "@profullstack/hqtui": "^0.11.1", - "@profullstack/profile": "0.1.6", + "@profullstack/profile": "0.1.7", "@profullstack/synconfig": "^0.1.3", "@profullstack/x402-gateway": "^0.16.1", "highlight.js": "^11.12.0", @@ -155,7 +155,9 @@ "@profullstack/keys": ["@profullstack/keys@0.1.0", "", {}, "sha512-Vys47LvS5ivhdZu4SYOxh9Le3DEmWhjP0+CtrU45RhXAbY0ZFG/2Dk9V1bIIUGDp8UWMJqctvUD21JrNjfh04w=="], - "@profullstack/profile": ["@profullstack/profile@0.1.6", "", { "peerDependencies": { "@libsql/client": ">=0.6.0", "postgres": "^3.4.0", "react": ">=18" }, "optionalPeers": ["@libsql/client", "postgres", "react"], "bin": { "profile": "src/cli.js" } }, "sha512-xkn0P3ANz151QJdlBwcBXkHk813qW9qAmuTZKcuvpEZYkyHDC9J9KfvpVSRepfjECzz4ONGHWO5Zwc4BHpfK8g=="], + "@profullstack/openprofile": ["@profullstack/openprofile@0.2.0", "", {}, "sha512-cdFsBbfRXurvy5KqHDNcKXxurK2s/YlfVgnILFRpVl2hq3860rExqDee8IfsFGxw5cPdqGv2fxUxtek1Xy2U5Q=="], + + "@profullstack/profile": ["@profullstack/profile@0.1.7", "", { "dependencies": { "@profullstack/openprofile": "^0.2.0" }, "peerDependencies": { "@libsql/client": ">=0.6.0", "postgres": "^3.4.0", "react": ">=18" }, "optionalPeers": ["@libsql/client", "postgres", "react"], "bin": { "profile": "src/cli.js" } }, "sha512-N7UkypkkY2JkSBKzmdmyy6+DCoxI048qUZQksEjSRwSIyy4mjU9H1qW4yhZD+LJBhTEupgJv//Uxwhiy2FuGmA=="], "@profullstack/synconfig": ["@profullstack/synconfig@0.1.3", "", {}, "sha512-Ty/90ibgu2DVlablDeoFdItcobK+c8cMQFvuTfJOUlCfyvQZWoPjgt0JHPwTvq+ynOQ4i1ivBQ3K3PY/S4rObA=="], diff --git a/package.json b/package.json index 9db7eda..3dadc81 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@profullstack/cli": "^0.1.0", "@profullstack/footer": "latest", "@profullstack/hqtui": "^0.11.1", - "@profullstack/profile": "0.1.6", + "@profullstack/profile": "0.1.7", "@profullstack/synconfig": "^0.1.3", "@profullstack/x402-gateway": "^0.16.1", "highlight.js": "^11.12.0", diff --git a/server/coinpay.ts b/server/coinpay.ts index 273a147..d1b2fcd 100644 --- a/server/coinpay.ts +++ b/server/coinpay.ts @@ -27,6 +27,8 @@ import { type User, } from "./store.ts"; import { createHash } from "node:crypto"; +import { saveOpenProfileContact } from "@profullstack/profile"; +import { profiles } from "./profile.ts"; export const COINPAY_SCOPES = "openid profile email"; const PROVIDER = "coinpay"; @@ -52,7 +54,7 @@ export type Identity = { createdAt: string; lastUsedAt: string; }; -type UserInfo = { sub: string; name: string | null; email: string | null }; +type UserInfo = { sub: string; name: string | null; email: string | null; openprofileUrl: string | null }; export function coinpayOptions(env = process.env): CoinPayOptions { return { @@ -165,7 +167,20 @@ export class CoinPay { "CoinPay did not accept this access token. It may have expired; get a new one.", ); // email_verified is always false on CoinPay. The address is kept for display only. - return { sub, name: str(result.body.name), email: str(result.body.email) }; + return { + sub, + name: str(result.body.name), + email: str(result.body.email), + openprofileUrl: str(result.body.openprofile_url), + }; + } + private async importProfile(userId: string, url: string | null) { + if (!url) return; + try { + await saveOpenProfileContact(await profiles(this.store.db).ready, userId, url); + } catch (error) { + console.warn("[coinpay] OpenProfile import skipped:", error instanceof Error ? error.message : error); + } } identities(userId: string): Identity[] { return this.store.all( @@ -433,9 +448,11 @@ export class CoinPay { const current = this.accounts.authenticate(this.accounts.token(request)); if (current) { this.link(current, info); + await this.importProfile(current.id, info.openprofileUrl); return go(pending.next ?? "/account?coinpay=linked"); } const signedIn = this.provision(info, "signin"); + await this.importProfile(signedIn.id, info.openprofileUrl); const session = this.accounts.startSession( signedIn.id, this.accounts.token(request), diff --git a/site/build.ts b/site/build.ts index 984ceda..bf9d765 100644 --- a/site/build.ts +++ b/site/build.ts @@ -568,7 +568,7 @@ write("account/index.html", readFileSync(join(here, "assets/account.html"), "utf .replace("__ACCOUNT_SCRIPT__", accountScript).replace("__ACCOUNT_STYLES__", accountCss) .replace("__PROFILE_SCRIPT__", profileScript) .replace("__SIGNUP_FIELDS__", renderProfileFields({ fields: profileFields, idPrefix: "signup-" })) - .replace("__DETAILS_FIELDS__", renderProfileFields({ fields: profileFields, idPrefix: "details-" }))); + .replace("__DETAILS_FIELDS__", renderProfileFields({ fields: profileFields, idPrefix: "details-", importOpenProfile: true }))); const sharedCss = asset("styles.css", css); const adminCss = asset("admin.css", readFileSync(join(here, "assets/admin.css"), "utf8")); const adminBundle = await Bun.build({ entrypoints: [join(here, "admin.ts")], target: "browser", minify: true }); diff --git a/site/test/accounts.spec.ts b/site/test/accounts.spec.ts index dccdb28..157c8d2 100644 --- a/site/test/accounts.spec.ts +++ b/site/test/accounts.spec.ts @@ -18,7 +18,7 @@ test("verified signup, profile, persistence, sign-out, and return sign-in", asyn await expect(page.getByText("Email verified", { exact: true })).toBeVisible(); await page.getByLabel("Display name").fill("Alex Reader"); await page.getByRole("button", { name: "Save name" }).click(); - await expect(page.getByRole("status")).toContainText("Your name has been saved."); + await expect(page.locator("#status")).toContainText("Your name has been saved."); await page.reload(); await expect(page.getByLabel("Display name")).toHaveValue("Alex Reader"); expect(await page.evaluate(() => document.cookie)).not.toContain("readm3_session");