Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<ePDS URL, e.g. https://epds1.test.certified.app>
NEXT_PUBLIC_PDS_URL=<ePDS URL, e.g. https://certified.one>
PUBLIC_URL=<app URL, e.g. https://certified-app-hypercerts-foundation.vercel.app>
UPSTASH_REDIS_REST_URL=<Upstash Redis URL for OAuth state>
UPSTASH_REDIS_REST_TOKEN=<Upstash Redis token>
Expand Down
4 changes: 2 additions & 2 deletions src/app/.well-known/oauth-client-metadata/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export async function GET() {
const metadata: Record<string, unknown> = {
...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`,
Expand Down
51 changes: 51 additions & 0 deletions src/app/api/auth/callback-handler/route.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
}
}
}
4 changes: 2 additions & 2 deletions src/components/dashboard/username-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/use-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/lib/atproto/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/lib/auth/oauth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down