One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links ๐Ÿ“… calendar.xyehr.cn
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: remove atproto api

authored by

Evan Huang and committed by
GitHub
0bd744bc 885d2d9b

-454
-171
app/api/atproto/callback/route.ts
··· 1 - import { NextRequest, NextResponse } from "next/server"; 2 - 3 - export async function GET(request: NextRequest) { 4 - return NextResponse.redirect(new URL("/", request.url)); 5 - } 6 - 7 - /* 8 - import { NextRequest, NextResponse } from "next/server"; 9 - import { ATPROTO_DISABLED } from "@/lib/atproto-feature"; 10 - import { getActorProfileRecord, getProfile, profileAvatarBlobUrl } from "@/lib/atproto"; 11 - import { setAtprotoSession } from "@/lib/atproto-auth"; 12 - import { clearAtprotoOAuthTxnCookie, consumeAtprotoOAuthTxn, getAtprotoOAuthTxnFromRequest } from "@/lib/atproto-oauth-txn"; 13 - import { createDpopProof, type DpopPublicJwk } from "@/lib/dpop"; 14 - 15 - function parseJsonSafe<T>(value: string): T | null { 16 - try { 17 - return JSON.parse(value) as T; 18 - } catch { 19 - return null; 20 - } 21 - } 22 - 23 - function redirectWithError(baseUrl: string, error: string, reason?: string) { 24 - const url = new URL(`${baseUrl}/at-oauth`); 25 - url.searchParams.set("error", error); 26 - if (reason) { 27 - url.searchParams.set("reason", reason); 28 - } 29 - 30 - const response = NextResponse.redirect(url.toString()); 31 - clearAtprotoOAuthTxnCookie(response); 32 - return response; 33 - } 34 - 35 - function normalizeIssuerOrigin(value: string) { 36 - const parsed = new URL(value); 37 - if (parsed.protocol !== "https:") { 38 - throw new Error("Issuer must use https"); 39 - } 40 - return parsed.origin; 41 - } 42 - 43 - export async function GET(request: NextRequest) { 44 - if (ATPROTO_DISABLED) return redirectWithError(process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin, "atproto_disabled"); 45 - const code = request.nextUrl.searchParams.get("code"); 46 - const state = request.nextUrl.searchParams.get("state"); 47 - const iss = request.nextUrl.searchParams.get("iss"); 48 - 49 - const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 50 - const txn = getAtprotoOAuthTxnFromRequest(request); 51 - 52 - if (!code || !state || !txn || state !== txn.state) { 53 - return redirectWithError(baseUrl, "oauth_state_mismatch"); 54 - } 55 - 56 - if (!consumeAtprotoOAuthTxn(txn)) { 57 - return redirectWithError(baseUrl, "oauth_state_mismatch", "transaction_already_used"); 58 - } 59 - 60 - const { verifier, handle, pds, did, dpopPrivateKeyPem, dpopPublicJwk } = txn; 61 - 62 - if (!dpopPublicJwk?.kty || !dpopPublicJwk?.crv || !dpopPublicJwk?.x || !dpopPublicJwk?.y) { 63 - return redirectWithError(baseUrl, "invalid_dpop_key"); 64 - } 65 - 66 - let issuerOrigin: string; 67 - let pdsOrigin: string; 68 - try { 69 - pdsOrigin = normalizeIssuerOrigin(pds); 70 - issuerOrigin = iss ? normalizeIssuerOrigin(iss) : pdsOrigin; 71 - } catch { 72 - return redirectWithError(baseUrl, "invalid_issuer"); 73 - } 74 - 75 - if (issuerOrigin !== pdsOrigin) { 76 - return redirectWithError(baseUrl, "invalid_issuer", "issuer_mismatch"); 77 - } 78 - 79 - const clientId = `${baseUrl}/oauth-client-metadata.json`; 80 - const redirectUri = `${baseUrl}/api/atproto/callback`; 81 - const tokenUrl = `${issuerOrigin}/oauth/token`; 82 - 83 - const makeTokenRequest = async (nonce?: string) => { 84 - const dpopProof = createDpopProof({ 85 - htu: tokenUrl, 86 - htm: "POST", 87 - privateKeyPem: dpopPrivateKeyPem, 88 - publicJwk: dpopPublicJwk as DpopPublicJwk, 89 - nonce, 90 - }); 91 - 92 - return fetch(tokenUrl, { 93 - method: "POST", 94 - headers: { 95 - "Content-Type": "application/x-www-form-urlencoded", 96 - DPoP: dpopProof, 97 - }, 98 - body: new URLSearchParams({ 99 - grant_type: "authorization_code", 100 - code, 101 - redirect_uri: redirectUri, 102 - client_id: clientId, 103 - code_verifier: verifier, 104 - }), 105 - }); 106 - }; 107 - 108 - let tokenRes = await makeTokenRequest(); 109 - if (!tokenRes.ok) { 110 - const nonce = tokenRes.headers.get("DPoP-Nonce") || tokenRes.headers.get("dpop-nonce"); 111 - if (nonce) { 112 - tokenRes = await makeTokenRequest(nonce); 113 - } 114 - } 115 - 116 - if (!tokenRes.ok) { 117 - const detailText = await tokenRes.text(); 118 - const detailJson = parseJsonSafe<{ error?: string; error_description?: string }>(detailText); 119 - const reason = detailJson?.error_description || detailJson?.error || detailText.slice(0, 160) || "token_exchange_failed"; 120 - return redirectWithError(baseUrl, "token_exchange_failed", reason); 121 - } 122 - 123 - const tokenData = (await tokenRes.json()) as { access_token?: string; refresh_token?: string; sub?: string }; 124 - if (!tokenData.access_token) { 125 - return redirectWithError(baseUrl, "missing_access_token"); 126 - } 127 - 128 - const actorDid = tokenData.sub || did; 129 - if (!actorDid) { 130 - return redirectWithError(baseUrl, "missing_subject"); 131 - } 132 - 133 - const profile = await getProfile(pds, actorDid, tokenData.access_token, { 134 - privateKeyPem: dpopPrivateKeyPem, 135 - publicJwk: dpopPublicJwk, 136 - }); 137 - 138 - const actorProfile = await getActorProfileRecord({ 139 - pds, 140 - repo: actorDid, 141 - accessToken: tokenData.access_token, 142 - dpopPrivateKeyPem, 143 - dpopPublicJwk, 144 - }).catch(() => undefined); 145 - 146 - const avatarCid = actorProfile?.avatar?.ref?.$link; 147 - const avatarUrl = profileAvatarBlobUrl({ pds, did: actorDid, cid: avatarCid }) || profile?.avatar; 148 - 149 - await setAtprotoSession({ 150 - did: actorDid, 151 - handle: profile?.handle || handle, 152 - pds, 153 - accessToken: tokenData.access_token, 154 - refreshToken: tokenData.refresh_token, 155 - displayName: actorProfile?.displayName || profile?.displayName, 156 - avatar: avatarUrl, 157 - dpopPrivateKeyPem, 158 - dpopPublicJwk, 159 - }); 160 - 161 - const response = NextResponse.redirect(`${baseUrl}/app`); 162 - clearAtprotoOAuthTxnCookie(response); 163 - 164 - ["__session", "__client_uat", "__clerk_db_jwt", "__clerk_handshake"].forEach((key) => { 165 - response.cookies.delete(key); 166 - }); 167 - 168 - return response; 169 - } 170 - 171 - */
-129
app/api/atproto/login/route.ts
··· 1 - import { NextRequest, NextResponse } from "next/server"; 2 - 3 - export async function POST(request: NextRequest) { 4 - return NextResponse.redirect(new URL("/", request.url)); 5 - } 6 - 7 - /* 8 - import { randomUUID } from "node:crypto"; 9 - import { NextRequest, NextResponse } from "next/server"; 10 - import { ATPROTO_DISABLED, atprotoDisabledResponse } from "@/lib/atproto-feature"; 11 - import { createPkcePair, resolveHandle } from "@/lib/atproto"; 12 - import { generateDpopKeyMaterial } from "@/lib/dpop"; 13 - import { setAtprotoOAuthTxnCookie } from "@/lib/atproto-oauth-txn"; 14 - 15 - const LOGIN_RATE_WINDOW_MS = 10 * 60 * 1000; 16 - const LOGIN_RATE_LIMIT = 20; 17 - const loginRateCache = new Map<string, { count: number; resetAt: number }>(); 18 - 19 - function getExpectedBaseUrl(request: NextRequest) { 20 - return process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 21 - } 22 - 23 - function isAllowedOrigin(request: NextRequest, expectedBaseUrl: string) { 24 - const expected = new URL(expectedBaseUrl); 25 - const origin = request.headers.get("origin"); 26 - if (origin) { 27 - try { 28 - const parsed = new URL(origin); 29 - if (parsed.origin !== expected.origin) return false; 30 - } catch { 31 - return false; 32 - } 33 - } 34 - 35 - const host = (request.headers.get("x-forwarded-host") || request.headers.get("host") || "").toLowerCase(); 36 - if (host && host !== expected.host.toLowerCase()) { 37 - return false; 38 - } 39 - 40 - return true; 41 - } 42 - 43 - function checkRateLimit(request: NextRequest, handle: string) { 44 - const now = Date.now(); 45 - for (const [key, value] of loginRateCache.entries()) { 46 - if (value.resetAt <= now) loginRateCache.delete(key); 47 - } 48 - 49 - const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; 50 - const key = `${ip}:${handle}`; 51 - const record = loginRateCache.get(key); 52 - 53 - if (!record || record.resetAt <= now) { 54 - loginRateCache.set(key, { count: 1, resetAt: now + LOGIN_RATE_WINDOW_MS }); 55 - return true; 56 - } 57 - 58 - if (record.count >= LOGIN_RATE_LIMIT) { 59 - return false; 60 - } 61 - 62 - record.count += 1; 63 - loginRateCache.set(key, record); 64 - return true; 65 - } 66 - 67 - export async function POST(request: NextRequest) { 68 - if (ATPROTO_DISABLED) return atprotoDisabledResponse(); 69 - const expectedBaseUrl = getExpectedBaseUrl(request); 70 - if (!isAllowedOrigin(request, expectedBaseUrl)) { 71 - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); 72 - } 73 - 74 - const { handle } = (await request.json()) as { handle?: string }; 75 - if (!handle) return NextResponse.json({ error: "Missing handle" }, { status: 400 }); 76 - 77 - const normalizedHandle = handle.replace(/^@/, "").toLowerCase(); 78 - if (!/^[a-z0-9.-]{3,253}$/.test(normalizedHandle)) { 79 - return NextResponse.json({ error: "Invalid handle" }, { status: 400 }); 80 - } 81 - 82 - if (!checkRateLimit(request, normalizedHandle)) { 83 - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); 84 - } 85 - 86 - const { did, pds } = await resolveHandle(normalizedHandle); 87 - const { verifier, challenge } = createPkcePair(); 88 - const state = randomUUID(); 89 - const dpop = generateDpopKeyMaterial(); 90 - 91 - const redirectUri = `${expectedBaseUrl}/api/atproto/callback`; 92 - const clientId = `${expectedBaseUrl}/oauth-client-metadata.json`; 93 - 94 - const authUrl = new URL(`${pds.replace(/\/$/, "")}/oauth/authorize`); 95 - authUrl.searchParams.set("client_id", clientId); 96 - authUrl.searchParams.set("redirect_uri", redirectUri); 97 - authUrl.searchParams.set("response_type", "code"); 98 - authUrl.searchParams.set("scope", "atproto transition:generic"); 99 - authUrl.searchParams.set("state", state); 100 - authUrl.searchParams.set("code_challenge", challenge); 101 - authUrl.searchParams.set("code_challenge_method", "S256"); 102 - authUrl.searchParams.set("dpop_jkt", dpop.jkt); 103 - 104 - const response = NextResponse.json({ authorizeUrl: authUrl.toString(), pds, did }); 105 - const secure = request.nextUrl.protocol === "https:" || process.env.NODE_ENV === "production"; 106 - setAtprotoOAuthTxnCookie( 107 - response, 108 - { 109 - jti: randomUUID(), 110 - state, 111 - verifier, 112 - handle: normalizedHandle, 113 - pds, 114 - did, 115 - dpopPrivateKeyPem: dpop.privateKeyPem, 116 - dpopPublicJwk: dpop.publicJwk, 117 - issuedAt: Math.floor(Date.now() / 1000), 118 - }, 119 - secure, 120 - ); 121 - 122 - ["__session", "__client_uat", "__clerk_db_jwt", "__clerk_handshake"].forEach((key) => { 123 - response.cookies.delete(key); 124 - }); 125 - 126 - return response; 127 - } 128 - 129 - */
-16
app/api/atproto/logout/route.ts
··· 1 - import { NextRequest, NextResponse } from "next/server"; 2 - 3 - export async function POST(request: NextRequest) { 4 - return NextResponse.redirect(new URL("/", request.url)); 5 - } 6 - 7 - /* 8 - import { NextResponse } from "next/server"; 9 - import { clearAtprotoSession } from "@/lib/atproto-auth"; 10 - 11 - export async function POST() { 12 - await clearAtprotoSession(); 13 - return NextResponse.json({ success: true }); 14 - } 15 - 16 - */
-84
app/api/atproto/register-url/route.ts
··· 1 - import { NextRequest, NextResponse } from "next/server"; 2 - 3 - export async function POST(request: NextRequest) { 4 - return NextResponse.redirect(new URL("/", request.url)); 5 - } 6 - 7 - /* 8 - import { randomUUID } from "node:crypto"; 9 - import { NextRequest, NextResponse } from "next/server"; 10 - import { ATPROTO_DISABLED, atprotoDisabledResponse } from "@/lib/atproto-feature"; 11 - import { createPkcePair } from "@/lib/atproto"; 12 - import { setAtprotoOAuthTxnCookie } from "@/lib/atproto-oauth-txn"; 13 - import { generateDpopKeyMaterial } from "@/lib/dpop"; 14 - 15 - const ROSE_PDS_ORIGIN = "https://rose.madebydanny.uk"; 16 - 17 - function getBaseUrl(request: NextRequest) { 18 - return process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 19 - } 20 - 21 - export async function POST(request: NextRequest) { 22 - if (ATPROTO_DISABLED) return atprotoDisabledResponse(); 23 - const baseUrl = getBaseUrl(request); 24 - const clientId = `${baseUrl}/oauth-client-metadata.json`; 25 - const authorizeUrl = new URL(`${ROSE_PDS_ORIGIN}/oauth/authorize`); 26 - 27 - const { verifier, challenge } = createPkcePair(); 28 - const state = randomUUID(); 29 - const dpop = generateDpopKeyMaterial(); 30 - const redirectUri = `${baseUrl}/api/atproto/callback`; 31 - 32 - const parRes = await fetch(`${ROSE_PDS_ORIGIN}/oauth/par`, { 33 - method: "POST", 34 - headers: { 35 - "Content-Type": "application/x-www-form-urlencoded", 36 - }, 37 - body: new URLSearchParams({ 38 - client_id: clientId, 39 - redirect_uri: redirectUri, 40 - response_type: "code", 41 - scope: "atproto transition:generic", 42 - state, 43 - code_challenge: challenge, 44 - code_challenge_method: "S256", 45 - dpop_jkt: dpop.jkt, 46 - }), 47 - cache: "no-store", 48 - }); 49 - 50 - if (!parRes.ok) { 51 - const detail = (await parRes.text()).slice(0, 200) || "par_failed"; 52 - return NextResponse.json({ error: `Rose PAR failed: ${detail}` }, { status: 502 }); 53 - } 54 - 55 - const parJson = (await parRes.json()) as { request_uri?: string }; 56 - if (!parJson.request_uri) { 57 - return NextResponse.json({ error: "Rose PAR response missing request_uri" }, { status: 502 }); 58 - } 59 - 60 - authorizeUrl.searchParams.set("client_id", clientId); 61 - authorizeUrl.searchParams.set("request_uri", parJson.request_uri); 62 - 63 - const response = NextResponse.json({ authorizeUrl: authorizeUrl.toString() }); 64 - const secure = request.nextUrl.protocol === "https:" || process.env.NODE_ENV === "production"; 65 - setAtprotoOAuthTxnCookie( 66 - response, 67 - { 68 - jti: randomUUID(), 69 - state, 70 - verifier, 71 - handle: "", 72 - pds: ROSE_PDS_ORIGIN, 73 - did: "", 74 - dpopPrivateKeyPem: dpop.privateKeyPem, 75 - dpopPublicJwk: dpop.publicJwk, 76 - issuedAt: Math.floor(Date.now() / 1000), 77 - }, 78 - secure, 79 - ); 80 - 81 - return response; 82 - } 83 - 84 - */
-54
app/api/atproto/session/route.ts
··· 1 - import { NextRequest, NextResponse } from "next/server"; 2 - 3 - export async function GET(request: NextRequest) { 4 - return NextResponse.redirect(new URL("/", request.url)); 5 - } 6 - 7 - /* 8 - import { NextResponse } from "next/server"; 9 - import { ATPROTO_DISABLED, atprotoDisabledResponse } from "@/lib/atproto-feature"; 10 - import { getActorProfileRecord, profileAvatarBlobUrl } from "@/lib/atproto"; 11 - import { getAtprotoSession, setAtprotoSession } from "@/lib/atproto-auth"; 12 - 13 - export async function GET() { 14 - if (ATPROTO_DISABLED) return atprotoDisabledResponse(); 15 - const session = await getAtprotoSession(); 16 - if (!session) return NextResponse.json({ signedIn: false }); 17 - 18 - let avatar = session.avatar; 19 - let displayName = session.displayName; 20 - 21 - if (session.accessToken && session.did && session.pds) { 22 - const actorProfile = await getActorProfileRecord({ 23 - pds: session.pds, 24 - repo: session.did, 25 - accessToken: session.accessToken, 26 - dpopPrivateKeyPem: session.dpopPrivateKeyPem, 27 - dpopPublicJwk: session.dpopPublicJwk, 28 - }).catch(() => undefined); 29 - 30 - const avatarCid = actorProfile?.avatar?.ref?.$link; 31 - const resolvedAvatar = profileAvatarBlobUrl({ pds: session.pds, did: session.did, cid: avatarCid }); 32 - 33 - if (resolvedAvatar || actorProfile?.displayName) { 34 - avatar = resolvedAvatar || avatar; 35 - displayName = actorProfile?.displayName || displayName; 36 - await setAtprotoSession({ 37 - ...session, 38 - avatar, 39 - displayName, 40 - }); 41 - } 42 - } 43 - 44 - return NextResponse.json({ 45 - signedIn: true, 46 - handle: session.handle, 47 - did: session.did, 48 - pds: session.pds, 49 - displayName, 50 - avatar, 51 - }); 52 - } 53 - 54 - */