A website for the ATmosphereConf

docs: Update Readme

Changed files
+124 -61
src
+48 -17
README.md
··· 1 - # Astro Starter Kit: Minimal 2 3 - ```sh 4 - npm create astro@latest -- --template minimal 5 - ``` 6 7 - > ๐Ÿง‘โ€๐Ÿš€ **Seasoned astronaut?** Delete this file. Have fun! 8 9 - ## ๐Ÿš€ Project Structure 10 11 - Inside of your Astro project, you'll see the following folders and files: 12 13 ```text 14 / 15 โ”œโ”€โ”€ public/ 16 โ”œโ”€โ”€ src/ 17 - โ”‚ โ””โ”€โ”€ pages/ 18 - โ”‚ โ””โ”€โ”€ index.astro 19 โ””โ”€โ”€ package.json 20 ``` 21 - 22 - Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 23 - 24 - There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 25 - 26 - Any static assets, like images, can be placed in the `public/` directory. 27 28 ## ๐Ÿงž Commands 29 ··· 38 | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 39 | `npm run astro -- --help` | Get help using the Astro CLI | 40 41 - ## ๐Ÿ‘€ Want to learn more? 42 43 - Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
··· 1 + # Astro ATProto OAuth Starter 2 + 3 + A minimal [Astro](https://astro.build) starter template demonstrating OAuth authentication with AT Protocol (ATProto), the decentralized social networking protocol used by Bluesky and other services. 4 + 5 + This starter includes: 6 + - Complete OAuth authentication flow using `@atproto/oauth-client-node` 7 + - Cookie-based session management 8 + - Profile display after authentication 9 + - Login/logout endpoints 10 + - Tailwind CSS and DaisyUI styling 11 + 12 + ## ๐Ÿš€ Getting Started 13 + 14 + 1. **Install dependencies:** 15 + ```sh 16 + npm install 17 + ``` 18 19 + 2. **Configure environment variables:** 20 + ```sh 21 + cp .env.template .env 22 + ``` 23 + Edit `.env` if you need to change the port (default: 4321) or set a public URL. 24 25 + 3. **Start the development server:** 26 + ```sh 27 + npm run dev 28 + ``` 29 + The app will be available at `http://localhost:4321` 30 31 + 4. **Try logging in:** 32 + Enter your AT Protocol handle (e.g., `alice.bsky.social`) to authenticate. 33 34 + ## ๐Ÿ“ Project Structure 35 36 ```text 37 / 38 โ”œโ”€โ”€ public/ 39 โ”œโ”€โ”€ src/ 40 + โ”‚ โ”œโ”€โ”€ lib/ 41 + โ”‚ โ”‚ โ”œโ”€โ”€ context.ts # OAuth client singleton 42 + โ”‚ โ”‚ โ”œโ”€โ”€ oauth.ts # OAuth client configuration 43 + โ”‚ โ”‚ โ”œโ”€โ”€ session.ts # Session management 44 + โ”‚ โ”‚ โ””โ”€โ”€ storage.ts # Cookie-based stores 45 + โ”‚ โ”œโ”€โ”€ pages/ 46 + โ”‚ โ”‚ โ”œโ”€โ”€ api/ 47 + โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ login.ts # Login endpoint 48 + โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ logout.ts # Logout endpoint 49 + โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ oauth/ 50 + โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ callback.ts # OAuth callback handler 51 + โ”‚ โ”‚ โ””โ”€โ”€ index.astro # Main page with login UI 52 + โ”‚ โ””โ”€โ”€ styles.css 53 โ””โ”€โ”€ package.json 54 ``` 55 56 ## ๐Ÿงž Commands 57 ··· 66 | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 67 | `npm run astro -- --help` | Get help using the Astro CLI | 68 69 + ## ๐Ÿ“š Learn More 70 71 + - [Astro Documentation](https://docs.astro.build) 72 + - [AT Protocol Documentation](https://atproto.com) 73 + - [@atproto/oauth-client-node](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-node) 74 + - [Bluesky](https://bsky.app)
+4 -14
src/lib/context.ts
··· 1 - import { NodeOAuthClient } from '@atproto/oauth-client-node' 2 import { createOAuthClient } from './oauth' 3 4 - export type AppContext = { 5 - oauthClient: NodeOAuthClient 6 - } 7 - 8 - let _ctx: AppContext | null = null 9 - 10 - export async function getAppContext(): Promise<AppContext> { 11 - if (_ctx) return _ctx 12 - 13 - const oauthClient = await createOAuthClient() 14 - 15 - _ctx = { oauthClient } 16 - return _ctx 17 }
··· 1 + import type { AstroCookies } from 'astro' 2 import { createOAuthClient } from './oauth' 3 4 + // Create a request-scoped OAuth client with cookie-based storage 5 + export function getOAuthClient(cookies: AstroCookies) { 6 + return createOAuthClient(cookies) 7 }
+5 -4
src/lib/oauth.ts
··· 1 import { 2 atprotoLoopbackClientMetadata, 3 NodeOAuthClient, 4 } from "@atproto/oauth-client-node"; 5 import { env } from "./env"; 6 - import { SessionStore, StateStore } from "./storage"; 7 8 - export async function createOAuthClient() { 9 const clientMetadata = atprotoLoopbackClientMetadata( 10 `http://localhost?${new URLSearchParams([ 11 ["redirect_uri", `http://127.0.0.1:${env.PORT}/api/oauth/callback`], ··· 15 16 return new NodeOAuthClient({ 17 clientMetadata, 18 - stateStore: new StateStore(), 19 - sessionStore: new SessionStore(), 20 }); 21 }
··· 1 + import type { AstroCookies } from 'astro' 2 import { 3 atprotoLoopbackClientMetadata, 4 NodeOAuthClient, 5 } from "@atproto/oauth-client-node"; 6 import { env } from "./env"; 7 + import { CookieSessionStore, CookieStateStore } from "./storage"; 8 9 + export function createOAuthClient(cookies: AstroCookies) { 10 const clientMetadata = atprotoLoopbackClientMetadata( 11 `http://localhost?${new URLSearchParams([ 12 ["redirect_uri", `http://127.0.0.1:${env.PORT}/api/oauth/callback`], ··· 16 17 return new NodeOAuthClient({ 18 clientMetadata, 19 + stateStore: new CookieStateStore(cookies), 20 + sessionStore: new CookieSessionStore(cookies), 21 }); 22 }
+53 -12
src/lib/storage.ts
··· 1 import type { 2 NodeSavedSession, 3 NodeSavedSessionStore, ··· 5 NodeSavedStateStore, 6 } from '@atproto/oauth-client-node' 7 8 - // In-memory storage for OAuth state and sessions 9 - // For production, you'd want to use a proper database or distributed cache 10 11 - export class StateStore implements NodeSavedStateStore { 12 - private store = new Map<string, NodeSavedState>() 13 14 async get(key: string): Promise<NodeSavedState | undefined> { 15 - return this.store.get(key) 16 } 17 18 async set(key: string, val: NodeSavedState) { 19 - this.store.set(key, val) 20 } 21 22 async del(key: string) { 23 - this.store.delete(key) 24 } 25 } 26 27 - export class SessionStore implements NodeSavedSessionStore { 28 - private store = new Map<string, NodeSavedSession>() 29 30 async get(key: string): Promise<NodeSavedSession | undefined> { 31 - return this.store.get(key) 32 } 33 34 async set(key: string, val: NodeSavedSession) { 35 - this.store.set(key, val) 36 } 37 38 async del(key: string) { 39 - this.store.delete(key) 40 } 41 }
··· 1 + import type { AstroCookies } from 'astro' 2 import type { 3 NodeSavedSession, 4 NodeSavedSessionStore, ··· 6 NodeSavedStateStore, 7 } from '@atproto/oauth-client-node' 8 9 + // Cookie-based storage for OAuth state and sessions 10 + // All data is serialized into cookies for stateless operation 11 12 + export class CookieStateStore implements NodeSavedStateStore { 13 + constructor(private cookies: AstroCookies) {} 14 15 async get(key: string): Promise<NodeSavedState | undefined> { 16 + const cookieName = `oauth_state_${key}` 17 + const cookie = this.cookies.get(cookieName) 18 + if (!cookie?.value) return undefined 19 + 20 + try { 21 + const decoded = atob(cookie.value) 22 + return JSON.parse(decoded) as NodeSavedState 23 + } catch (err) { 24 + console.warn('Failed to decode OAuth state:', err) 25 + return undefined 26 + } 27 } 28 29 async set(key: string, val: NodeSavedState) { 30 + const cookieName = `oauth_state_${key}` 31 + const encoded = btoa(JSON.stringify(val)) 32 + 33 + this.cookies.set(cookieName, encoded, { 34 + httpOnly: true, 35 + secure: false, 36 + sameSite: 'lax', 37 + path: '/', 38 + maxAge: 60 * 10, // 10 minutes (OAuth flow timeout) 39 + }) 40 } 41 42 async del(key: string) { 43 + const cookieName = `oauth_state_${key}` 44 + this.cookies.delete(cookieName, { path: '/' }) 45 } 46 } 47 48 + export class CookieSessionStore implements NodeSavedSessionStore { 49 + constructor(private cookies: AstroCookies) {} 50 51 async get(key: string): Promise<NodeSavedSession | undefined> { 52 + const cookieName = `oauth_session_${key}` 53 + const cookie = this.cookies.get(cookieName) 54 + if (!cookie?.value) return undefined 55 + 56 + try { 57 + const decoded = atob(cookie.value) 58 + return JSON.parse(decoded) as NodeSavedSession 59 + } catch (err) { 60 + console.warn('Failed to decode OAuth session:', err) 61 + return undefined 62 + } 63 } 64 65 async set(key: string, val: NodeSavedSession) { 66 + const cookieName = `oauth_session_${key}` 67 + const encoded = btoa(JSON.stringify(val)) 68 + 69 + this.cookies.set(cookieName, encoded, { 70 + httpOnly: true, 71 + secure: false, 72 + sameSite: 'lax', 73 + path: '/', 74 + maxAge: 60 * 60 * 24 * 30, // 30 days 75 + }) 76 } 77 78 async del(key: string) { 79 + const cookieName = `oauth_session_${key}` 80 + this.cookies.delete(cookieName, { path: '/' }) 81 } 82 }
+4 -4
src/pages/api/login.ts
··· 1 import type { APIRoute } from 'astro' 2 - import { getAppContext } from '../../lib/context' 3 4 - export const POST: APIRoute = async ({ request, redirect }) => { 5 try { 6 - const ctx = await getAppContext() 7 const formData = await request.formData() 8 const handle = formData.get('handle') 9 ··· 11 return new Response('Invalid handle', { status: 400 }) 12 } 13 14 - const url = await ctx.oauthClient.authorize(handle, { 15 scope: 'atproto transition:generic', 16 }) 17
··· 1 import type { APIRoute } from 'astro' 2 + import { getOAuthClient } from '../../lib/context' 3 4 + export const POST: APIRoute = async ({ request, cookies, redirect }) => { 5 try { 6 + const oauthClient = getOAuthClient(cookies) 7 const formData = await request.formData() 8 const handle = formData.get('handle') 9 ··· 11 return new Response('Invalid handle', { status: 400 }) 12 } 13 14 + const url = await oauthClient.authorize(handle, { 15 scope: 'atproto transition:generic', 16 }) 17
+3 -3
src/pages/api/logout.ts
··· 1 import type { APIRoute } from 'astro' 2 - import { getAppContext } from '../../lib/context' 3 import { getSession } from '../../lib/session' 4 5 export const POST: APIRoute = async (context) => { 6 try { 7 - const ctx = await getAppContext() 8 const session = getSession(context.cookies) 9 10 if (session.did) { 11 try { 12 - const oauthSession = await ctx.oauthClient.restore(session.did) 13 if (oauthSession) await oauthSession.signOut() 14 } catch (err) { 15 console.warn('Failed to revoke credentials:', err)
··· 1 import type { APIRoute } from 'astro' 2 + import { getOAuthClient } from '../../lib/context' 3 import { getSession } from '../../lib/session' 4 5 export const POST: APIRoute = async (context) => { 6 try { 7 + const oauthClient = getOAuthClient(context.cookies) 8 const session = getSession(context.cookies) 9 10 if (session.did) { 11 try { 12 + const oauthSession = await oauthClient.restore(session.did) 13 if (oauthSession) await oauthSession.signOut() 14 } catch (err) { 15 console.warn('Failed to revoke credentials:', err)
+4 -4
src/pages/api/oauth/callback.ts
··· 1 import type { APIRoute } from 'astro' 2 - import { getAppContext } from '../../../lib/context' 3 import { getSession } from '../../../lib/session' 4 5 export const GET: APIRoute = async (context) => { 6 try { 7 - const ctx = await getAppContext() 8 const url = new URL(context.request.url) 9 const params = new URLSearchParams(url.search) 10 ··· 12 13 if (session.did) { 14 try { 15 - const oauthSession = await ctx.oauthClient.restore(session.did) 16 if (oauthSession) await oauthSession.signOut() 17 } catch (err) { 18 console.warn('OAuth restore failed during callback:', err) 19 } 20 } 21 22 - const oauth = await ctx.oauthClient.callback(params) 23 session.did = oauth.session.did 24 await session.save() 25
··· 1 import type { APIRoute } from 'astro' 2 + import { getOAuthClient } from '../../../lib/context' 3 import { getSession } from '../../../lib/session' 4 5 export const GET: APIRoute = async (context) => { 6 try { 7 + const oauthClient = getOAuthClient(context.cookies) 8 const url = new URL(context.request.url) 9 const params = new URLSearchParams(url.search) 10 ··· 12 13 if (session.did) { 14 try { 15 + const oauthSession = await oauthClient.restore(session.did) 16 if (oauthSession) await oauthSession.signOut() 17 } catch (err) { 18 console.warn('OAuth restore failed during callback:', err) 19 } 20 } 21 22 + const oauth = await oauthClient.callback(params) 23 session.did = oauth.session.did 24 await session.save() 25
+3 -3
src/pages/index.astro
··· 1 --- 2 import "../styles.css"; 3 import { getSession } from "../lib/session"; 4 - import { getAppContext } from "../lib/context"; 5 import { Agent } from "@atproto/api"; 6 7 const session = getSession(Astro.cookies); 8 - const ctx = await getAppContext(); 9 10 let agent: Agent | null = null; 11 let profile: any = null; 12 13 if (session.did) { 14 try { 15 - const oauthSession = await ctx.oauthClient.restore(session.did); 16 if (oauthSession) { 17 agent = new Agent(oauthSession); 18
··· 1 --- 2 import "../styles.css"; 3 import { getSession } from "../lib/session"; 4 + import { getOAuthClient } from "../lib/context"; 5 import { Agent } from "@atproto/api"; 6 7 const session = getSession(Astro.cookies); 8 + const oauthClient = getOAuthClient(Astro.cookies); 9 10 let agent: Agent | null = null; 11 let profile: any = null; 12 13 if (session.did) { 14 try { 15 + const oauthSession = await oauthClient.restore(session.did); 16 if (oauthSession) { 17 agent = new Agent(oauthSession); 18