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.

Merge pull request #163 from EvanTechDev/feature/disable-atproto-login-functionality

Disable ATProto login channel and public API access

authored by

Evan Huang and committed by
GitHub
605bbd53 a6538c5e

+28 -61
+2 -29
app/(auth)/at-oauth/page.tsx
··· 1 - import { AuthBrand } from "@/components/auth/auth-brand"; 2 - import { AtprotoLoginForm } from "@/components/auth/atproto-login-form"; 1 + import { redirect } from "next/navigation"; 3 2 4 3 export default function AtprotoLoginPage() { 5 - return ( 6 - <div className="relative flex min-h-svh flex-col items-center justify-center overflow-hidden from-blue-500 via-indigo-500 to-purple-500 p-6 md:p-10"> 7 - <div className="fixed -z-10 inset-0"> 8 - <div className="absolute inset-0 bg-white dark:bg-black"> 9 - <div 10 - className="absolute inset-0" 11 - style={{ 12 - backgroundImage: `radial-gradient(circle at 1px 1px, rgba(0, 0, 0, 0.1) 1px, transparent 0)`, 13 - backgroundSize: "24px 24px", 14 - }} 15 - /> 16 - <div 17 - className="absolute inset-0 dark:block hidden" 18 - style={{ 19 - backgroundImage: `radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.15) 1px, transparent 0)`, 20 - backgroundSize: "24px 24px", 21 - }} 22 - /> 23 - </div> 24 - </div> 25 - 26 - <div className="relative z-10 flex w-full max-w-sm flex-col gap-6"> 27 - <AuthBrand /> 28 - <AtprotoLoginForm /> 29 - </div> 30 - </div> 31 - ); 4 + redirect("/sign-in"); 32 5 }
+2
app/api/atproto/callback/route.ts
··· 1 1 import { NextRequest, NextResponse } from "next/server"; 2 + import { ATPROTO_DISABLED } from "@/lib/atproto-feature"; 2 3 import { getActorProfileRecord, getProfile, profileAvatarBlobUrl } from "@/lib/atproto"; 3 4 import { setAtprotoSession } from "@/lib/atproto-auth"; 4 5 import { clearAtprotoOAuthTxnCookie, consumeAtprotoOAuthTxn, getAtprotoOAuthTxnFromRequest } from "@/lib/atproto-oauth-txn"; ··· 33 34 } 34 35 35 36 export async function GET(request: NextRequest) { 37 + if (ATPROTO_DISABLED) return redirectWithError(process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin, "atproto_disabled"); 36 38 const code = request.nextUrl.searchParams.get("code"); 37 39 const state = request.nextUrl.searchParams.get("state"); 38 40 const iss = request.nextUrl.searchParams.get("iss");
+2
app/api/atproto/login/route.ts
··· 1 1 import { randomUUID } from "node:crypto"; 2 2 import { NextRequest, NextResponse } from "next/server"; 3 + import { ATPROTO_DISABLED, atprotoDisabledResponse } from "@/lib/atproto-feature"; 3 4 import { createPkcePair, resolveHandle } from "@/lib/atproto"; 4 5 import { generateDpopKeyMaterial } from "@/lib/dpop"; 5 6 import { setAtprotoOAuthTxnCookie } from "@/lib/atproto-oauth-txn"; ··· 57 58 } 58 59 59 60 export async function POST(request: NextRequest) { 61 + if (ATPROTO_DISABLED) return atprotoDisabledResponse(); 60 62 const expectedBaseUrl = getExpectedBaseUrl(request); 61 63 if (!isAllowedOrigin(request, expectedBaseUrl)) { 62 64 return NextResponse.json({ error: "Forbidden" }, { status: 403 });
+2
app/api/atproto/register-url/route.ts
··· 1 1 import { randomUUID } from "node:crypto"; 2 2 import { NextRequest, NextResponse } from "next/server"; 3 + import { ATPROTO_DISABLED, atprotoDisabledResponse } from "@/lib/atproto-feature"; 3 4 import { createPkcePair } from "@/lib/atproto"; 4 5 import { setAtprotoOAuthTxnCookie } from "@/lib/atproto-oauth-txn"; 5 6 import { generateDpopKeyMaterial } from "@/lib/dpop"; ··· 11 12 } 12 13 13 14 export async function POST(request: NextRequest) { 15 + if (ATPROTO_DISABLED) return atprotoDisabledResponse(); 14 16 const baseUrl = getBaseUrl(request); 15 17 const clientId = `${baseUrl}/oauth-client-metadata.json`; 16 18 const authorizeUrl = new URL(`${ROSE_PDS_ORIGIN}/oauth/authorize`);
+2
app/api/atproto/session/route.ts
··· 1 1 import { NextResponse } from "next/server"; 2 + import { ATPROTO_DISABLED, atprotoDisabledResponse } from "@/lib/atproto-feature"; 2 3 import { getActorProfileRecord, profileAvatarBlobUrl } from "@/lib/atproto"; 3 4 import { getAtprotoSession, setAtprotoSession } from "@/lib/atproto-auth"; 4 5 5 6 export async function GET() { 7 + if (ATPROTO_DISABLED) return atprotoDisabledResponse(); 6 8 const session = await getAtprotoSession(); 7 9 if (!session) return NextResponse.json({ signedIn: false }); 8 10
+2
app/api/share/public/route.ts
··· 2 2 import crypto from "crypto"; 3 3 import { Pool } from "pg"; 4 4 import { getRecord, resolveHandle } from "@/lib/atproto"; 5 + import { ATPROTO_DISABLED, atprotoDisabledResponse } from "@/lib/atproto-feature"; 5 6 6 7 const ALGORITHM = "aes-256-gcm"; 7 8 const ATPROTO_SHARE_COLLECTION = "app.onecalendar.share"; ··· 79 80 } 80 81 81 82 export async function GET(request: NextRequest) { 83 + if (ATPROTO_DISABLED) return atprotoDisabledResponse(); 82 84 const handle = request.nextUrl.searchParams.get("handle"); 83 85 const id = request.nextUrl.searchParams.get("id"); 84 86 const password = request.nextUrl.searchParams.get("password") ?? "";
-16
components/auth/login-form.tsx
··· 153 153 variant="outline" 154 154 className="w-full" 155 155 type="button" 156 - onClick={() => (window.location.href = "/at-oauth")} 157 - > 158 - <svg 159 - xmlns="http://www.w3.org/2000/svg" 160 - viewBox="0 0 640 560" 161 - className="h-5 w-auto shrink-0" 162 - aria-hidden="true" 163 - > 164 - <path fill="#0066ff" d="M133 47c74 56 152 169 197 260 45-91 123-204 197-260 53-40 140-70 140 28 0 20-11 170-18 194-22 85-103 106-175 93 124 22 156 91 87 161-131 134-188-34-203-75-2-6-3-12-3-18 0 6-1 12-3 18-15 41-72 209-203 75-69-70-37-139 87-161-72 13-153-8-175-93-7-24-18-174-18-194 0-98 87-68 140-28z"/> 165 - </svg> 166 - <span className="ml-2">Login with Atmosphere</span> 167 - </Button> 168 - <Button 169 - variant="outline" 170 - className="w-full" 171 - type="button" 172 156 onClick={() => handleOAuthLogin("oauth_github")} 173 157 > 174 158 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
-16
components/auth/sign-up-form.tsx
··· 258 258 variant="outline" 259 259 className="w-full" 260 260 type="button" 261 - onClick={() => (window.location.href = "/at-oauth")} 262 - > 263 - <svg 264 - xmlns="http://www.w3.org/2000/svg" 265 - viewBox="0 0 640 560" 266 - className="h-5 w-auto shrink-0" 267 - aria-hidden="true" 268 - > 269 - <path fill="#0066ff" d="M133 47c74 56 152 169 197 260 45-91 123-204 197-260 53-40 140-70 140 28 0 20-11 170-18 194-22 85-103 106-175 93 124 22 156 91 87 161-131 134-188-34-203-75-2-6-3-12-3-18 0 6-1 12-3 18-15 41-72 209-203 75-69-70-37-139 87-161-72 13-153-8-175-93-7-24-18-174-18-194 0-98 87-68 140-28z"/> 270 - </svg> 271 - <span className="ml-2">Continue with Atmosphere</span> 272 - </Button> 273 - <Button 274 - variant="outline" 275 - className="w-full" 276 - type="button" 277 261 onClick={() => handleOAuthSignUp("oauth_github")} 278 262 > 279 263 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
+9
lib/atproto-auth.ts
··· 1 1 import { createDecipheriv, createHash, createCipheriv, hkdfSync, randomBytes } from "node:crypto"; 2 2 import type { DpopPublicJwk } from "@/lib/dpop"; 3 3 import { cookies } from "next/headers"; 4 + import { ATPROTO_DISABLED } from "@/lib/atproto-feature"; 4 5 5 6 export const ATPROTO_SESSION_COOKIE = "atproto_session"; 6 7 ··· 111 112 112 113 export async function getAtprotoSession(): Promise<AtprotoSession | null> { 113 114 const store = await cookies(); 115 + if (ATPROTO_DISABLED) { 116 + store.delete(ATPROTO_SESSION_COOKIE); 117 + return null; 118 + } 114 119 const raw = store.get(ATPROTO_SESSION_COOKIE)?.value; 115 120 if (!raw) return null; 116 121 ··· 124 129 125 130 export async function setAtprotoSession(session: AtprotoSession) { 126 131 const store = await cookies(); 132 + if (ATPROTO_DISABLED) { 133 + store.delete(ATPROTO_SESSION_COOKIE); 134 + return; 135 + } 127 136 const value = encodeSession(session); 128 137 store.set(ATPROTO_SESSION_COOKIE, value, { 129 138 httpOnly: true,
+7
lib/atproto-feature.ts
··· 1 + import { NextResponse } from "next/server"; 2 + 3 + export const ATPROTO_DISABLED = true; 4 + 5 + export function atprotoDisabledResponse() { 6 + return NextResponse.json({ error: "ATProto channel is disabled" }, { status: 410 }); 7 + }