diff --git a/.env.local.example b/.env.local.example index d54bd885..d76b6cee 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,5 +1,5 @@ # Required: The PDS / handle resolver URL -NEXT_PUBLIC_PDS_URL=https://epds1.test.certified.app +NEXT_PUBLIC_PDS_URL=https://certified.one # Required in production: The public URL of this app (used for OAuth client_id and redirect_uris) PUBLIC_URL=http://localhost:3000 diff --git a/AGENTS.md b/AGENTS.md index dfa983ec..19842798 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -271,7 +271,7 @@ public/assets/ Required in `.env.local` (and Vercel Production + Preview): ``` COOKIE_SECRET=<32+ char secret for HMAC session cookies> -NEXT_PUBLIC_PDS_URL= +NEXT_PUBLIC_PDS_URL= PUBLIC_URL= UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= diff --git a/src/app/.well-known/oauth-client-metadata/route.ts b/src/app/.well-known/oauth-client-metadata/route.ts index c187615f..fbafc731 100644 --- a/src/app/.well-known/oauth-client-metadata/route.ts +++ b/src/app/.well-known/oauth-client-metadata/route.ts @@ -11,8 +11,8 @@ export async function GET() { const metadata: Record = { ...client.clientMetadata, // Add the extra fields that the ePDS needs but are not part of the OAuth client config - brand_color: "#60A1E2", - background_color: "#0F2544", + brand_color: "#0F2544", + background_color: "#FFFFFF", tos_uri: `${origin}/terms`, policy_uri: `${origin}/privacy`, email_template_uri: `${origin}/assets/otp-email-template.html`, diff --git a/src/app/api/auth/callback-handler/route.ts b/src/app/api/auth/callback-handler/route.ts index a13b9cc5..db384573 100644 --- a/src/app/api/auth/callback-handler/route.ts +++ b/src/app/api/auth/callback-handler/route.ts @@ -1,7 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { NextRequest, NextResponse } from "next/server" +import { Agent } from "@atproto/api" import { getOAuthClient } from "@/lib/auth/oauth-client" import { createSession } from "@/lib/auth/session" +/** Collections that should always have a "self" record after sign-in */ +const PROFILE_COLLECTIONS = [ + "app.certified.actor.profile", + "app.bsky.actor.profile", +] + export async function GET(request: NextRequest) { try { const params = request.nextUrl.searchParams @@ -11,9 +19,52 @@ export async function GET(request: NextRequest) { await createSession(session.did) + // Best-effort: ensure profile records exist (don't fail sign-in if this errors) + try { + const oauthSession = await client.restore(session.did) + const agent = new Agent(oauthSession) + await ensureProfileRecords(agent, session.did) + } catch { + // Silently ignore — profile seeding is not critical for sign-in + } + return NextResponse.json({ did: session.did }) } catch (err) { const message = err instanceof Error ? err.message : "Unknown error" return NextResponse.json({ error: message }, { status: 500 }) } } + +/** + * For each profile collection, check if a "self" record exists. + * If not, create an empty one with only createdAt set. + */ +async function ensureProfileRecords(agent: Agent, did: string) { + const now = new Date().toISOString() + + for (const collection of PROFILE_COLLECTIONS) { + try { + await agent.com.atproto.repo.getRecord({ + repo: did, + collection, + rkey: "self", + }) + // Record exists — nothing to do + } catch { + // Record missing or error — try to create it + try { + await agent.com.atproto.repo.putRecord({ + repo: did, + collection, + rkey: "self", + record: { + $type: collection, + createdAt: now, + }, + } as any) + } catch { + // Silently ignore — best effort + } + } + } +} diff --git a/src/components/dashboard/username-card.tsx b/src/components/dashboard/username-card.tsx index 0f720dec..1e24d2fd 100644 --- a/src/components/dashboard/username-card.tsx +++ b/src/components/dashboard/username-card.tsx @@ -20,11 +20,11 @@ function getPdsHostname(pdsUrl?: string): string { return new URL(pdsUrl).hostname; } catch { /* ignore */ } } - const url = process.env.NEXT_PUBLIC_PDS_URL || "https://epds1.test.certified.app"; + const url = process.env.NEXT_PUBLIC_PDS_URL || "https://certified.one"; try { return new URL(url).hostname; } catch { - return "epds1.test.certified.app"; + return "certified.one"; } } diff --git a/src/hooks/use-profile.ts b/src/hooks/use-profile.ts index 7d92f309..88de3304 100644 --- a/src/hooks/use-profile.ts +++ b/src/hooks/use-profile.ts @@ -85,7 +85,7 @@ export function useProfile(): { }, [fetchProfile]) // Compute avatar and banner URLs - const effectivePdsUrl = pdsUrl || process.env.NEXT_PUBLIC_PDS_URL || "https://epds1.test.certified.app" + const effectivePdsUrl = pdsUrl || process.env.NEXT_PUBLIC_PDS_URL || "https://certified.one" let avatarUrl: string | null = null let bannerUrl: string | null = null diff --git a/src/lib/atproto/profile.ts b/src/lib/atproto/profile.ts index 45a325c1..5f5a80cd 100644 --- a/src/lib/atproto/profile.ts +++ b/src/lib/atproto/profile.ts @@ -190,7 +190,7 @@ export async function uploadBanner( * Get the URL for a profile avatar * @param profile - The profile record * @param did - The DID of the user - * @param pdsUrl - The PDS URL (e.g., https://epds1.test.certified.app) + * @param pdsUrl - The PDS URL (e.g., https://certified.one) * @returns The avatar URL or null if no avatar is set */ export function getAvatarUrl( @@ -223,7 +223,7 @@ export function getAvatarUrl( * Get the URL for a profile banner * @param profile - The profile record * @param did - The DID of the user - * @param pdsUrl - The PDS URL (e.g., https://epds1.test.certified.app) + * @param pdsUrl - The PDS URL (e.g., https://certified.one) * @returns The banner URL or null if no banner is set */ export function getBannerUrl( diff --git a/src/lib/auth/oauth-client.ts b/src/lib/auth/oauth-client.ts index 09393fe0..44014fbf 100644 --- a/src/lib/auth/oauth-client.ts +++ b/src/lib/auth/oauth-client.ts @@ -6,7 +6,7 @@ import { RedisStateStore, RedisSessionStore } from "./stores" export const PDS_URL = process.env.PDS_URL || process.env.NEXT_PUBLIC_PDS_URL || - "https://epds1.test.certified.app" + "https://certified.one" let clientInstance: NodeOAuthClient | null = null