From 4edde5293ad8cbaf55f255c1109d5a5cc155f526 Mon Sep 17 00:00:00 2001 From: syui Date: Tue, 17 Jun 2025 22:34:03 +0900 Subject: [PATCH] Add oauth_new: Complete OAuth authentication and comment system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created new oauth_new directory with clean OAuth implementation - Added 4-tab interface: Lang, Comment, Collection, User Comments - Implemented OAuth authentication with @atproto/oauth-client-browser - Added comment posting functionality with putRecord - Added proper PDS detection and error handling - Skipped placeholder users to prevent errors - Built comprehensive documentation (README.md, DEVELOPMENT.md) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- oauth_new/.env | 6 + oauth_new/DEVELOPMENT.md | 334 +++++++++++++++++++++ oauth_new/README.md | 222 ++++++++++++++ oauth_new/index.html | 11 + oauth_new/package.json | 22 ++ oauth_new/src/App.jsx | 76 +++++ oauth_new/src/api/atproto.js | 80 +++++ oauth_new/src/components/AuthButton.jsx | 102 +++++++ oauth_new/src/components/CommentForm.jsx | 200 ++++++++++++ oauth_new/src/components/OAuthCallback.jsx | 50 +++ oauth_new/src/components/RecordList.jsx | 58 ++++ oauth_new/src/components/RecordTabs.jsx | 151 ++++++++++ oauth_new/src/components/UserLookup.jsx | 115 +++++++ oauth_new/src/config/env.js | 17 ++ oauth_new/src/hooks/useAdminData.js | 57 ++++ oauth_new/src/hooks/useAuth.js | 47 +++ oauth_new/src/hooks/usePageContext.js | 33 ++ oauth_new/src/hooks/useUserData.js | 164 ++++++++++ oauth_new/src/main.jsx | 5 + oauth_new/src/services/oauth.js | 144 +++++++++ oauth_new/src/utils/pds.js | 36 +++ oauth_new/vite.config.js | 15 + 22 files changed, 1945 insertions(+) create mode 100644 oauth_new/.env create mode 100644 oauth_new/DEVELOPMENT.md create mode 100644 oauth_new/README.md create mode 100644 oauth_new/index.html create mode 100644 oauth_new/package.json create mode 100644 oauth_new/src/App.jsx create mode 100644 oauth_new/src/api/atproto.js create mode 100644 oauth_new/src/components/AuthButton.jsx create mode 100644 oauth_new/src/components/CommentForm.jsx create mode 100644 oauth_new/src/components/OAuthCallback.jsx create mode 100644 oauth_new/src/components/RecordList.jsx create mode 100644 oauth_new/src/components/RecordTabs.jsx create mode 100644 oauth_new/src/components/UserLookup.jsx create mode 100644 oauth_new/src/config/env.js create mode 100644 oauth_new/src/hooks/useAdminData.js create mode 100644 oauth_new/src/hooks/useAuth.js create mode 100644 oauth_new/src/hooks/usePageContext.js create mode 100644 oauth_new/src/hooks/useUserData.js create mode 100644 oauth_new/src/main.jsx create mode 100644 oauth_new/src/services/oauth.js create mode 100644 oauth_new/src/utils/pds.js create mode 100644 oauth_new/vite.config.js diff --git a/oauth_new/.env b/oauth_new/.env new file mode 100644 index 0000000..cada505 --- /dev/null +++ b/oauth_new/.env @@ -0,0 +1,6 @@ +VITE_ADMIN=ai.syui.ai +VITE_PDS=syu.is +VITE_HANDLE_LIST=["ai.syui.ai", "syui.syui.ai", "ai.ai"] +VITE_COLLECTION=ai.syui.log +VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json +VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback diff --git a/oauth_new/DEVELOPMENT.md b/oauth_new/DEVELOPMENT.md new file mode 100644 index 0000000..e49c2af --- /dev/null +++ b/oauth_new/DEVELOPMENT.md @@ -0,0 +1,334 @@ +# 開発ガイド + +## 設計思想 + +このプロジェクトは以下の原則に基づいて設計されています: + +### 1. 環境変数による設定の外部化 +- ハードコードを避け、設定は全て環境変数で管理 +- `src/config/env.js` で一元管理 + +### 2. PDS(Personal Data Server)の自動判定 +- `VITE_HANDLE_LIST` と `VITE_PDS` による自動判定 +- syu.is系とbsky.social系の自動振り分け + +### 3. コンポーネントの責任分離 +- Hooks: ビジネスロジック +- Components: UI表示のみ +- Services: 外部API連携 +- Utils: 純粋関数 + +## アーキテクチャ詳細 + +### データフロー + +``` +User Input + ↓ +Hooks (useAuth, useAdminData, usePageContext) + ↓ +Services (OAuthService) + ↓ +API (atproto.js) + ↓ +ATProto Network + ↓ +Components (UI Display) +``` + +### 状態管理 + +React Hooksによる状態管理: +- `useAuth`: OAuth認証状態 +- `useAdminData`: 管理者データ(プロフィール、レコード) +- `usePageContext`: ページ判定(トップ/個別) + +### OAuth認証フロー + +``` +1. ユーザーがハンドル入力 +2. PDS判定 (syu.is vs bsky.social) +3. 適切なOAuthClientを選択 +4. 標準OAuth画面にリダイレクト +5. 認証完了後コールバック処理 +6. セッション復元・保存 +``` + +## 重要な実装詳細 + +### セッション管理 + +`@atproto/oauth-client-browser`が自動的に以下を処理: +- IndexedDBへのセッション保存 +- トークンの自動更新 +- DPoP(Demonstration of Proof of Possession) + +**注意**: 手動でのセッション管理は複雑なため、公式ライブラリを使用すること。 + +### PDS判定アルゴリズム + +```javascript +// src/utils/pds.js +function isSyuIsHandle(handle) { + return env.handleList.includes(handle) || handle.endsWith(`.${env.pds}`) +} +``` + +1. `VITE_HANDLE_LIST` に含まれるハンドル → syu.is +2. `.syu.is` で終わるハンドル → syu.is +3. その他 → bsky.social + +### レコードフィルタリング + +```javascript +// src/components/RecordTabs.jsx +const filterRecords = (records) => { + if (pageContext.isTopPage) { + return records.slice(0, 3) // 最新3件 + } else { + // URL のrkey と record.value.post.url のrkey を照合 + return records.filter(record => { + const recordRkey = new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') + return recordRkey === pageContext.rkey + }) + } +} +``` + +## 開発時の注意点 + +### 1. 環境変数の命名 + +- `VITE_` プレフィックス必須(Viteの制約) +- JSON形式の環境変数は文字列として定義 + +```bash +# ❌ 間違い +VITE_HANDLE_LIST=["ai.syui.ai"] + +# ✅ 正しい +VITE_HANDLE_LIST=["ai.syui.ai", "syui.syui.ai"] +``` + +### 2. API エラーハンドリング + +```javascript +// src/api/atproto.js +async function request(url) { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + return await response.json() +} +``` + +すべてのAPI呼び出しでエラーハンドリングを実装。 + +### 3. コンポーネント設計 + +```javascript +// ❌ Bad: ビジネスロジックがコンポーネント内 +function MyComponent() { + const [data, setData] = useState([]) + useEffect(() => { + fetch('/api/data').then(setData) + }, []) + return
{data.map(...)}
+} + +// ✅ Good: Hooksでロジック分離 +function MyComponent() { + const { data, loading, error } = useMyData() + if (loading) return + if (error) return + return
{data.map(...)}
+} +``` + +## デバッグ手法 + +### 1. OAuth デバッグ + +```javascript +// ブラウザの開発者ツールで確認 +localStorage.clear() // セッションクリア +sessionStorage.clear() // 一時データクリア + +// IndexedDB確認(Application タブ) +// ATProtoの認証データが保存される +``` + +### 2. PDS判定デバッグ + +```javascript +// src/utils/pds.js にログ追加 +console.log('Handle:', handle) +console.log('Is syu.is:', isSyuIsHandle(handle)) +console.log('API Config:', getApiConfig(pds)) +``` + +### 3. レコードフィルタリングデバッグ + +```javascript +// src/components/RecordTabs.jsx +console.log('Page Context:', pageContext) +console.log('All Records:', records.length) +console.log('Filtered Records:', filteredRecords.length) +``` + +## パフォーマンス最適化 + +### 1. 並列データ取得 + +```javascript +// src/hooks/useAdminData.js +const [records, lang, comment] = await Promise.all([ + collections.getBase(apiConfig.pds, did, env.collection), + collections.getLang(apiConfig.pds, did, env.collection), + collections.getComment(apiConfig.pds, did, env.collection) +]) +``` + +### 2. 不要な再レンダリング防止 + +```javascript +// useMemo でフィルタリング結果をキャッシュ +const filteredRecords = useMemo(() => + filterRecords(records), + [records, pageContext] +) +``` + +## テスト戦略 + +### 1. 単体テスト推奨対象 + +- `src/utils/pds.js` - PDS判定ロジック +- `src/config/env.js` - 環境変数パース +- フィルタリング関数 + +### 2. 統合テスト推奨対象 + +- OAuth認証フロー +- API呼び出し +- レコード表示 + +## デプロイメント + +### 1. 必要ファイル + +``` +public/ +└── client-metadata.json # OAuth設定ファイル + +dist/ # ビルド出力 +├── index.html +└── assets/ + ├── comment-atproto-[hash].js + └── comment-atproto-[hash].css +``` + +### 2. デプロイ手順 + +```bash +# 1. 環境変数設定 +cp .env.example .env +# 2. 本番用設定を記入 +# 3. ビルド +npm run build +# 4. dist/ フォルダをデプロイ +``` + +### 3. 本番環境チェックリスト + +- [ ] `.env` ファイルの本番設定 +- [ ] `client-metadata.json` の設置 +- [ ] HTTPS 必須(OAuth要件) +- [ ] CSP(Content Security Policy)設定 + +## よくある問題と解決法 + +### 1. "OAuth initialization failed" + +**原因**: client-metadata.json が見つからない、または形式が正しくない + +**解決法**: +```bash +# public/client-metadata.json の存在確認 +ls -la public/client-metadata.json + +# 形式確認(JSON validation) +jq . public/client-metadata.json +``` + +### 2. "Failed to load admin data" + +**原因**: 管理者アカウントのDID解決に失敗 + +**解決法**: +```bash +# 手動でDID解決確認 +curl "https://syu.is/xrpc/com.atproto.repo.describeRepo?repo=ai.syui.ai" +``` + +### 3. レコードが表示されない + +**原因**: コレクション名の不一致、権限不足 + +**解決法**: +```bash +# コレクション確認 +curl "https://syu.is/xrpc/com.atproto.repo.listRecords?repo=did:plc:xxx&collection=ai.syui.log.chat.lang" +``` + +## 機能拡張ガイド + +### 1. 新しいコレクション追加 + +```javascript +// src/api/atproto.js に追加 +export const collections = { + // 既存... + async getNewCollection(pds, repo, collection, limit = 10) { + return await atproto.getRecords(pds, repo, `${collection}.new`, limit) + } +} +``` + +### 2. 新しいPDS対応 + +```javascript +// src/utils/pds.js を拡張 +export function getApiConfig(pds) { + if (pds.includes('syu.is')) { + // 既存の syu.is 設定 + } else if (pds.includes('newpds.com')) { + return { + pds: `https://newpds.com`, + bsky: `https://bsky.newpds.com`, + plc: `https://plc.newpds.com`, + web: `https://web.newpds.com` + } + } + // デフォルト設定 +} +``` + +### 3. リアルタイム更新追加 + +```javascript +// src/hooks/useRealtimeUpdates.js +export function useRealtimeUpdates(collection) { + useEffect(() => { + const ws = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe') + ws.onmessage = (event) => { + const data = JSON.parse(event.data) + if (data.collection === collection) { + // 新しいレコードを追加 + } + } + return () => ws.close() + }, [collection]) +} +``` \ No newline at end of file diff --git a/oauth_new/README.md b/oauth_new/README.md new file mode 100644 index 0000000..f369473 --- /dev/null +++ b/oauth_new/README.md @@ -0,0 +1,222 @@ +# ATProto OAuth Comment System + +ATProtocol(Bluesky)のOAuth認証を使用したコメントシステムです。 + +## プロジェクト概要 + +このプロジェクトは、ATProtocolネットワーク上のコメントとlangレコードを表示するWebアプリケーションです。 +- 標準的なOAuth認証画面を使用 +- タブ切り替えでレコード表示 +- ページコンテキストに応じたフィルタリング + +## ファイル構成 + +``` +src/ +├── config/ +│ └── env.js # 環境変数の一元管理 +├── utils/ +│ └── pds.js # PDS判定・API設定ユーティリティ +├── api/ +│ └── atproto.js # ATProto API クライアント +├── hooks/ +│ ├── useAuth.js # OAuth認証フック +│ ├── useAdminData.js # 管理者データ取得フック +│ └── usePageContext.js # ページ判定フック +├── services/ +│ └── oauth.js # OAuth認証サービス +├── components/ +│ ├── AuthButton.jsx # ログイン/ログアウトボタン +│ ├── RecordTabs.jsx # Lang/Commentタブ切り替え +│ ├── RecordList.jsx # レコード表示リスト +│ ├── UserLookup.jsx # ユーザー検索(未使用) +│ └── OAuthCallback.jsx # OAuth コールバック処理 +└── App.jsx # メインアプリケーション +``` + +## 環境設定 + +### .env ファイル + +```bash +VITE_ADMIN=ai.syui.ai # 管理者ハンドル +VITE_PDS=syu.is # デフォルトPDS +VITE_HANDLE_LIST=["ai.syui.ai", "syui.syui.ai", "ai.ai"] # syu.is系ハンドルリスト +VITE_COLLECTION=ai.syui.log # ベースコレクション +VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json # OAuth クライアントID +VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback # OAuth リダイレクトURI +``` + +### 必要な依存関係 + +```json +{ + "dependencies": { + "@atproto/api": "^0.15.12", + "@atproto/oauth-client-browser": "^0.3.19", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} +``` + +## 主要機能 + +### 1. OAuth認証システム + +**実装場所**: `src/services/oauth.js` + +- `@atproto/oauth-client-browser`を使用した標準OAuth実装 +- bsky.social と syu.is 両方のPDSに対応 +- セッション自動復元機能 + +**重要**: ATProtoのセッション管理は複雑なため、公式ライブラリの使用が必須です。 + +### 2. PDS判定システム + +**実装場所**: `src/utils/pds.js` + +```javascript +// ハンドル判定ロジック +isSyuIsHandle(handle) → boolean +// PDS設定取得 +getApiConfig(pds) → { pds, bsky, plc, web } +``` + +環境変数`VITE_HANDLE_LIST`と`VITE_PDS`を基に自動判定します。 + +### 3. コレクション取得システム + +**実装場所**: `src/api/atproto.js` + +```javascript +// 基本コレクション +collections.getBase(pds, repo, collection) +// lang コレクション(翻訳系) +collections.getLang(pds, repo, collection) // → {collection}.chat.lang +// comment コレクション(コメント系) +collections.getComment(pds, repo, collection) // → {collection}.chat.comment +``` + +### 4. ページコンテキスト判定 + +**実装場所**: `src/hooks/usePageContext.js` + +```javascript +// URL解析結果 +{ + isTopPage: boolean, // トップページかどうか + rkey: string | null, // 個別ページのrkey(/posts/xxx → xxx) + url: string // 現在のURL +} +``` + +## 表示ロジック + +### フィルタリング + +1. **トップページ**: 最新3件を表示 +2. **個別ページ**: `record.value.post.url`の rkey が現在ページと一致するもののみ表示 + +### タブ切り替え + +- Lang Records: `{collection}.chat.lang` +- Comment Records: `{collection}.chat.comment` + +## 開発・デバッグ + +### 起動コマンド + +```bash +npm install +npm run dev # 開発サーバー +npm run build # プロダクションビルド +``` + +### OAuth デバッグ + +1. **ローカル開発**: 自動的にloopback clientが使用される +2. **本番環境**: `client-metadata.json`が必要 + +```json +// public/client-metadata.json +{ + "client_id": "https://syui.ai/client-metadata.json", + "client_name": "ATProto Comment System", + "redirect_uris": ["https://syui.ai/oauth/callback"], + "scope": "atproto", + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "token_endpoint_auth_method": "none", + "application_type": "web", + "dpop_bound_access_tokens": true +} +``` + +### よくある問題 + +1. **セッションが保存されない** + - `@atproto/oauth-client-browser`のバージョン確認 + - IndexedDBの確認(ブラウザの開発者ツール) + +2. **PDS判定が正しく動作しない** + - `VITE_HANDLE_LIST`の JSON 形式を確認 + - 環境変数の読み込み確認 + +3. **レコードが表示されない** + - 管理者アカウントの DID 解決確認 + - コレクション名の確認(`{base}.chat.lang`, `{base}.chat.comment`) + +## API エンドポイント + +### 使用しているATProto API + +1. **com.atproto.repo.describeRepo** + - ハンドル → DID, PDS解決 + +2. **app.bsky.actor.getProfile** + - プロフィール情報取得 + +3. **com.atproto.repo.listRecords** + - コレクションレコード取得 + +## セキュリティ + +- OAuth 2.1 + PKCE による認証 +- DPoP (Demonstration of Proof of Possession) 対応 +- セッション情報はブラウザのIndexedDBに暗号化保存 + +## 今後の拡張可能性 + +1. **コメント投稿機能** + - 認証済みユーザーによるコメント作成 + - `com.atproto.repo.putRecord` API使用 + +2. **リアルタイム更新** + - Jetstream WebSocket 接続 + - 新しいレコードの自動表示 + +3. **マルチPDS対応** + - より多くのPDSへの対応 + - 動的PDS判定の改善 + +## トラブルシューティング + +### ログ確認 +ブラウザの開発者ツールでコンソールログを確認してください。主要なエラーは以下の通りです: + +- `OAuth initialization failed`: OAuth設定の問題 +- `Failed to load admin data`: API アクセスエラー +- `Auth check failed`: セッション復元エラー + +### 環境変数確認 +```javascript +// 開発者ツールのコンソールで確認 +console.log(import.meta.env) +``` + +## 参考資料 + +- [ATProto OAuth Guide](https://github.com/bluesky-social/atproto/blob/main/packages/api/OAUTH.md) +- [BrowserOAuthClient Documentation](https://github.com/bluesky-social/atproto/tree/main/packages/oauth-client-browser) +- [ATProto API Reference](https://docs.bsky.app/docs/advanced-guides/atproto-api) \ No newline at end of file diff --git a/oauth_new/index.html b/oauth_new/index.html new file mode 100644 index 0000000..5664ef4 --- /dev/null +++ b/oauth_new/index.html @@ -0,0 +1,11 @@ + + + + + Comments Test + + +
+ + + \ No newline at end of file diff --git a/oauth_new/package.json b/oauth_new/package.json new file mode 100644 index 0000000..e0ead1d --- /dev/null +++ b/oauth_new/package.json @@ -0,0 +1,22 @@ +{ + "name": "oauth-simple", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@atproto/api": "^0.15.12", + "@atproto/oauth-client-browser": "^0.3.19" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.0.0", + "vite": "^5.0.0" + } +} \ No newline at end of file diff --git a/oauth_new/src/App.jsx b/oauth_new/src/App.jsx new file mode 100644 index 0000000..598db75 --- /dev/null +++ b/oauth_new/src/App.jsx @@ -0,0 +1,76 @@ +import React from 'react' +import { useAuth } from './hooks/useAuth.js' +import { useAdminData } from './hooks/useAdminData.js' +import { useUserData } from './hooks/useUserData.js' +import { usePageContext } from './hooks/usePageContext.js' +import AuthButton from './components/AuthButton.jsx' +import RecordTabs from './components/RecordTabs.jsx' +import CommentForm from './components/CommentForm.jsx' +import OAuthCallback from './components/OAuthCallback.jsx' + +export default function App() { + const { user, agent, loading: authLoading, login, logout } = useAuth() + const { adminData, langRecords, commentRecords, loading: dataLoading, error, refresh: refreshAdminData } = useAdminData() + const { userComments, chatRecords, loading: userLoading, refresh: refreshUserData } = useUserData(adminData) + const pageContext = usePageContext() + + // Handle OAuth callback + if (window.location.search.includes('code=')) { + return + } + + const isLoading = authLoading || dataLoading || userLoading + + if (isLoading) { + return ( +
+

ATProto OAuth Demo

+

読み込み中...

+
+ ) + } + + if (error) { + return ( +
+

ATProto OAuth Demo

+

エラー: {error}

+ +
+ ) + } + + return ( +
+
+

ATProto OAuth Demo

+ +
+ + { + refreshAdminData?.() + refreshUserData?.() + }} + /> + + +
+ ) +} \ No newline at end of file diff --git a/oauth_new/src/api/atproto.js b/oauth_new/src/api/atproto.js new file mode 100644 index 0000000..6a65249 --- /dev/null +++ b/oauth_new/src/api/atproto.js @@ -0,0 +1,80 @@ +// ATProto API client +const ENDPOINTS = { + describeRepo: 'com.atproto.repo.describeRepo', + getProfile: 'app.bsky.actor.getProfile', + listRecords: 'com.atproto.repo.listRecords', + putRecord: 'com.atproto.repo.putRecord' +} + +async function request(url, options = {}) { + const response = await fetch(url, options) + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + return await response.json() +} + +export const atproto = { + async getDid(pds, handle) { + const res = await request(`https://${pds}/xrpc/${ENDPOINTS.describeRepo}?repo=${handle}`) + return res.did + }, + + async getProfile(bsky, actor) { + return await request(`${bsky}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`) + }, + + async getRecords(pds, repo, collection, limit = 10) { + const res = await request(`${pds}/xrpc/${ENDPOINTS.listRecords}?repo=${repo}&collection=${collection}&limit=${limit}`) + return res.records || [] + }, + + async searchPlc(plc, did) { + try { + const data = await request(`${plc}/${did}`) + return { + success: true, + endpoint: data?.service?.[0]?.serviceEndpoint || null, + handle: data?.alsoKnownAs?.[0]?.replace('at://', '') || null + } + } catch { + return { success: false, endpoint: null, handle: null } + } + }, + + async putRecord(pds, record, agent) { + if (!agent) { + throw new Error('Agent required for putRecord') + } + + // Use Agent's putRecord method instead of direct fetch + return await agent.com.atproto.repo.putRecord(record) + } +} + +// Collection specific methods +export const collections = { + async getBase(pds, repo, collection, limit = 10) { + return await atproto.getRecords(pds, repo, collection, limit) + }, + + async getLang(pds, repo, collection, limit = 10) { + return await atproto.getRecords(pds, repo, `${collection}.chat.lang`, limit) + }, + + async getComment(pds, repo, collection, limit = 10) { + return await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit) + }, + + async getChat(pds, repo, collection, limit = 10) { + return await atproto.getRecords(pds, repo, `${collection}.chat`, limit) + }, + + async getUserList(pds, repo, collection, limit = 100) { + return await atproto.getRecords(pds, repo, `${collection}.user`, limit) + }, + + async getUserComments(pds, repo, collection, limit = 10) { + return await atproto.getRecords(pds, repo, collection, limit) + } +} \ No newline at end of file diff --git a/oauth_new/src/components/AuthButton.jsx b/oauth_new/src/components/AuthButton.jsx new file mode 100644 index 0000000..f2d6e27 --- /dev/null +++ b/oauth_new/src/components/AuthButton.jsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react' + +export default function AuthButton({ user, onLogin, onLogout, loading }) { + const [handleInput, setHandleInput] = useState('') + const [isLoading, setIsLoading] = useState(false) + + const handleSubmit = async (e) => { + e.preventDefault() + if (!handleInput.trim() || isLoading) return + + setIsLoading(true) + try { + await onLogin(handleInput.trim()) + } catch (error) { + console.error('Login failed:', error) + alert('ログインに失敗しました: ' + error.message) + } finally { + setIsLoading(false) + } + } + + if (loading) { + return
認証状態を確認中...
+ } + + if (user) { + return ( +
+
ログイン中: {user.handle}
+ + +
+ ) + } + + return ( +
+

OAuth認証

+
+ setHandleInput(e.target.value)} + placeholder="Handle (e.g. your.handle.com)" + disabled={isLoading} + className="handle-input" + /> + +
+ +
+ ) +} \ No newline at end of file diff --git a/oauth_new/src/components/CommentForm.jsx b/oauth_new/src/components/CommentForm.jsx new file mode 100644 index 0000000..15131ad --- /dev/null +++ b/oauth_new/src/components/CommentForm.jsx @@ -0,0 +1,200 @@ +import React, { useState } from 'react' +import { atproto } from '../api/atproto.js' +import { env } from '../config/env.js' + +export default function CommentForm({ user, agent, onCommentPosted }) { + const [text, setText] = useState('') + const [url, setUrl] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async (e) => { + e.preventDefault() + if (!text.trim() || !url.trim()) return + + setLoading(true) + setError(null) + + try { + // Create ai.syui.log record structure + const record = { + repo: user.did, + collection: env.collection, + rkey: `comment-${Date.now()}`, + record: { + $type: env.collection, + url: url.trim(), + comments: [ + { + url: url.trim(), + text: text.trim(), + author: { + did: user.did, + handle: user.handle, + displayName: user.displayName, + avatar: user.avatar + }, + createdAt: new Date().toISOString() + } + ], + createdAt: new Date().toISOString() + } + } + + // Post the record + await atproto.putRecord(null, record, agent) + + // Clear form + setText('') + setUrl('') + + // Notify parent component + if (onCommentPosted) { + onCommentPosted() + } + + } catch (err) { + setError(err.message) + } finally { + setLoading(false) + } + } + + if (!user) { + return ( +
+

ログインしてコメントを投稿

+
+ ) + } + + return ( +
+

コメントを投稿

+ +
+
+ + setUrl(e.target.value)} + placeholder="https://syui.ai/posts/example" + required + disabled={loading} + /> +
+ +
+ +