This commit is contained in:
@ -1,4 +1,9 @@
|
|||||||
# Development environment variables
|
# Development environment variables
|
||||||
VITE_APP_HOST=http://localhost:4173
|
VITE_APP_HOST=http://localhost:4173
|
||||||
VITE_OAUTH_CLIENT_ID=http://localhost:4173/client-metadata.json
|
VITE_OAUTH_CLIENT_ID=http://localhost:4173/client-metadata.json
|
||||||
VITE_OAUTH_REDIRECT_URI=http://localhost:4173/oauth/callback
|
VITE_OAUTH_REDIRECT_URI=http://localhost:4173/oauth/callback
|
||||||
|
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
||||||
|
|
||||||
|
# Optional: Override collection names (if not set, auto-generated from host)
|
||||||
|
# VITE_COLLECTION_COMMENT=ai.syui.log
|
||||||
|
# VITE_COLLECTION_USER=ai.syui.log.user
|
@ -1,4 +1,9 @@
|
|||||||
# Production environment variables
|
# Production environment variables
|
||||||
VITE_APP_HOST=https://log.syui.ai
|
VITE_APP_HOST=https://log.syui.ai
|
||||||
VITE_OAUTH_CLIENT_ID=https://log.syui.ai/client-metadata.json
|
VITE_OAUTH_CLIENT_ID=https://log.syui.ai/client-metadata.json
|
||||||
VITE_OAUTH_REDIRECT_URI=https://log.syui.ai/oauth/callback
|
VITE_OAUTH_REDIRECT_URI=https://log.syui.ai/oauth/callback
|
||||||
|
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
||||||
|
|
||||||
|
# Optional: Override collection names (if not set, auto-generated from host)
|
||||||
|
# VITE_COLLECTION_COMMENT=ai.syui.log
|
||||||
|
# VITE_COLLECTION_USER=ai.syui.log.user
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { OAuthCallback } from './components/OAuthCallback';
|
import { OAuthCallback } from './components/OAuthCallback';
|
||||||
import { authService, User } from './services/auth';
|
import { authService, User } from './services/auth';
|
||||||
import { atprotoOAuthService } from './services/atproto-oauth';
|
import { atprotoOAuthService } from './services/atproto-oauth';
|
||||||
|
import { appConfig } from './config/app';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -54,14 +55,14 @@ function App() {
|
|||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log('Jetstream connected');
|
console.log('Jetstream connected');
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
wantedCollections: ['ai.syui.log']
|
wantedCollections: [appConfig.collections.comment]
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
if (data.collection === 'ai.syui.log' && data.commit?.operation === 'create') {
|
if (data.collection === appConfig.collections.comment && data.commit?.operation === 'create') {
|
||||||
console.log('New comment detected via Jetstream:', data);
|
console.log('New comment detected via Jetstream:', data);
|
||||||
// Optionally reload comments
|
// Optionally reload comments
|
||||||
// loadAllComments(window.location.href);
|
// loadAllComments(window.location.href);
|
||||||
@ -146,7 +147,7 @@ function App() {
|
|||||||
loadAllComments();
|
loadAllComments();
|
||||||
|
|
||||||
// Load user list records if admin
|
// Load user list records if admin
|
||||||
if (userProfile.did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') {
|
if (userProfile.did === appConfig.adminDid) {
|
||||||
loadUserListRecords();
|
loadUserListRecords();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ function App() {
|
|||||||
loadAllComments();
|
loadAllComments();
|
||||||
|
|
||||||
// Load user list records if admin
|
// Load user list records if admin
|
||||||
if (verifiedUser.did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') {
|
if (verifiedUser.did === appConfig.adminDid) {
|
||||||
loadUserListRecords();
|
loadUserListRecords();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,7 +226,7 @@ function App() {
|
|||||||
// Get comments from current user
|
// Get comments from current user
|
||||||
const response = await agent.api.com.atproto.repo.listRecords({
|
const response = await agent.api.com.atproto.repo.listRecords({
|
||||||
repo: did,
|
repo: did,
|
||||||
collection: 'ai.syui.log',
|
collection: appConfig.collections.comment,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -268,10 +269,10 @@ function App() {
|
|||||||
// JSONからユーザーリストを取得
|
// JSONからユーザーリストを取得
|
||||||
const loadUsersFromRecord = async () => {
|
const loadUsersFromRecord = async () => {
|
||||||
try {
|
try {
|
||||||
// 管理者のユーザーリストを取得 (ai.syui.log.user collection)
|
// 管理者のユーザーリストを取得
|
||||||
const adminDid = 'did:plc:uqzpqmrjnptsxezjx4xuh2mn'; // syui.ai
|
const adminDid = appConfig.adminDid;
|
||||||
console.log('Fetching user list from admin DID:', adminDid);
|
console.log('Fetching user list from admin DID:', adminDid);
|
||||||
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=ai.syui.log.user&limit=100`);
|
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(appConfig.collections.user)}&limit=100`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn('Failed to fetch user list from admin, using default users. Status:', response.status);
|
console.warn('Failed to fetch user list from admin, using default users. Status:', response.status);
|
||||||
@ -331,8 +332,8 @@ function App() {
|
|||||||
const loadUserListRecords = async () => {
|
const loadUserListRecords = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Loading user list records...');
|
console.log('Loading user list records...');
|
||||||
const adminDid = 'did:plc:uqzpqmrjnptsxezjx4xuh2mn'; // syui.ai
|
const adminDid = appConfig.adminDid;
|
||||||
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=ai.syui.log.user&limit=100`);
|
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(appConfig.collections.user)}&limit=100`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn('Failed to fetch user list records');
|
console.warn('Failed to fetch user list records');
|
||||||
@ -358,8 +359,8 @@ function App() {
|
|||||||
|
|
||||||
const getDefaultUsers = () => {
|
const getDefaultUsers = () => {
|
||||||
const defaultUsers = [
|
const defaultUsers = [
|
||||||
// bsky.social - 実際のDIDを使用
|
// Default admin user
|
||||||
{ did: 'did:plc:uqzpqmrjnptsxezjx4xuh2mn', handle: 'syui.ai', pds: 'https://bsky.social' },
|
{ did: appConfig.adminDid, handle: 'syui.ai', pds: 'https://bsky.social' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 現在ログインしているユーザーも追加(重複チェック)
|
// 現在ログインしているユーザーも追加(重複チェック)
|
||||||
@ -393,7 +394,7 @@ function App() {
|
|||||||
console.log(`Fetching comments from user: ${user.handle} (${user.did}) at ${user.pds}`);
|
console.log(`Fetching comments from user: ${user.handle} (${user.did}) at ${user.pds}`);
|
||||||
|
|
||||||
// Public API使用(認証不要)
|
// Public API使用(認証不要)
|
||||||
const response = await fetch(`${user.pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=ai.syui.log&limit=100`);
|
const response = await fetch(`${user.pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(appConfig.collections.comment)}&limit=100`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn(`Failed to fetch from ${user.handle} (${response.status}): ${response.statusText}`);
|
console.warn(`Failed to fetch from ${user.handle} (${response.status}): ${response.statusText}`);
|
||||||
@ -490,12 +491,13 @@ function App() {
|
|||||||
throw new Error('No agent available');
|
throw new Error('No agent available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create comment record with ISO datetime rkey
|
// Create comment record with post-specific rkey
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const rkey = now.toISOString().replace(/[:.]/g, '-'); // Replace : and . with - for valid rkey
|
// Use post rkey if on post page, otherwise use timestamp-based rkey
|
||||||
|
const rkey = appConfig.rkey || now.toISOString().replace(/[:.]/g, '-');
|
||||||
|
|
||||||
const record = {
|
const record = {
|
||||||
$type: 'ai.syui.log',
|
$type: appConfig.collections.comment,
|
||||||
text: commentText,
|
text: commentText,
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
createdAt: now.toISOString(),
|
createdAt: now.toISOString(),
|
||||||
@ -510,7 +512,7 @@ function App() {
|
|||||||
// Post to ATProto with rkey
|
// Post to ATProto with rkey
|
||||||
const response = await agent.api.com.atproto.repo.putRecord({
|
const response = await agent.api.com.atproto.repo.putRecord({
|
||||||
repo: user.did,
|
repo: user.did,
|
||||||
collection: 'ai.syui.log',
|
collection: appConfig.collections.comment,
|
||||||
rkey: rkey,
|
rkey: rkey,
|
||||||
record: record,
|
record: record,
|
||||||
});
|
});
|
||||||
@ -553,7 +555,7 @@ function App() {
|
|||||||
// Delete the record
|
// Delete the record
|
||||||
await agent.api.com.atproto.repo.deleteRecord({
|
await agent.api.com.atproto.repo.deleteRecord({
|
||||||
repo: user.did,
|
repo: user.did,
|
||||||
collection: 'ai.syui.log',
|
collection: appConfig.collections.comment,
|
||||||
rkey: rkey,
|
rkey: rkey,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -578,7 +580,7 @@ function App() {
|
|||||||
|
|
||||||
// 管理者チェック
|
// 管理者チェック
|
||||||
const isAdmin = (user: User | null): boolean => {
|
const isAdmin = (user: User | null): boolean => {
|
||||||
return user?.did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn'; // syui.ai
|
return user?.did === appConfig.adminDid;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ユーザーリスト投稿
|
// ユーザーリスト投稿
|
||||||
@ -640,7 +642,7 @@ function App() {
|
|||||||
const rkey = now.toISOString().replace(/[:.]/g, '-');
|
const rkey = now.toISOString().replace(/[:.]/g, '-');
|
||||||
|
|
||||||
const record = {
|
const record = {
|
||||||
$type: 'ai.syui.log.user',
|
$type: appConfig.collections.user,
|
||||||
users: users,
|
users: users,
|
||||||
createdAt: now.toISOString(),
|
createdAt: now.toISOString(),
|
||||||
updatedBy: {
|
updatedBy: {
|
||||||
@ -652,7 +654,7 @@ function App() {
|
|||||||
// Post to ATProto with rkey
|
// Post to ATProto with rkey
|
||||||
const response = await agent.api.com.atproto.repo.putRecord({
|
const response = await agent.api.com.atproto.repo.putRecord({
|
||||||
repo: user.did,
|
repo: user.did,
|
||||||
collection: 'ai.syui.log.user',
|
collection: appConfig.collections.user,
|
||||||
rkey: rkey,
|
rkey: rkey,
|
||||||
record: record,
|
record: record,
|
||||||
});
|
});
|
||||||
@ -697,7 +699,7 @@ function App() {
|
|||||||
// Delete the record
|
// Delete the record
|
||||||
await agent.api.com.atproto.repo.deleteRecord({
|
await agent.api.com.atproto.repo.deleteRecord({
|
||||||
repo: user.did,
|
repo: user.did,
|
||||||
collection: 'ai.syui.log.user',
|
collection: appConfig.collections.user,
|
||||||
rkey: rkey,
|
rkey: rkey,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -743,6 +745,22 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Rkey-based comment filtering
|
||||||
|
// If on post page (/posts/xxx.html), only show comments with rkey=xxx
|
||||||
|
const shouldShowComment = (record: any): boolean => {
|
||||||
|
// If not on a post page, show all comments
|
||||||
|
if (!appConfig.rkey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract rkey from comment URI: at://did:plc:xxx/collection/rkey
|
||||||
|
const uriParts = record.uri.split('/');
|
||||||
|
const commentRkey = uriParts[uriParts.length - 1];
|
||||||
|
|
||||||
|
// Show comment only if rkey matches current post
|
||||||
|
return commentRkey === appConfig.rkey;
|
||||||
|
};
|
||||||
|
|
||||||
// OAuth callback is now handled by React Router in main.tsx
|
// OAuth callback is now handled by React Router in main.tsx
|
||||||
console.log('=== APP.TSX URL CHECK ===');
|
console.log('=== APP.TSX URL CHECK ===');
|
||||||
console.log('Full URL:', window.location.href);
|
console.log('Full URL:', window.location.href);
|
||||||
@ -896,10 +914,12 @@ function App() {
|
|||||||
<div className="comments-header">
|
<div className="comments-header">
|
||||||
<h3>Comments</h3>
|
<h3>Comments</h3>
|
||||||
</div>
|
</div>
|
||||||
{comments.length === 0 ? (
|
{comments.filter(shouldShowComment).length === 0 ? (
|
||||||
<p className="no-comments">No comments yet</p>
|
<p className="no-comments">
|
||||||
|
{appConfig.rkey ? `No comments for this post yet` : `No comments yet`}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
comments.map((record, index) => (
|
comments.filter(shouldShowComment).map((record, index) => (
|
||||||
<div key={index} className="comment-item">
|
<div key={index} className="comment-item">
|
||||||
<div className="comment-header">
|
<div className="comment-header">
|
||||||
<img
|
<img
|
||||||
@ -945,8 +965,8 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Comment Form - Outside user section, after comments list */}
|
{/* Comment Form - Only show on post pages */}
|
||||||
{user && (
|
{user && appConfig.rkey && (
|
||||||
<div className="comment-form">
|
<div className="comment-form">
|
||||||
<h3>Post a Comment</h3>
|
<h3>Post a Comment</h3>
|
||||||
<textarea
|
<textarea
|
||||||
@ -969,6 +989,14 @@ function App() {
|
|||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Show authentication status on non-post pages */}
|
||||||
|
{user && !appConfig.rkey && (
|
||||||
|
<div className="auth-status">
|
||||||
|
<p>✅ Authenticated as @{user.handle}</p>
|
||||||
|
<p><small>Visit a post page to comment</small></p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
84
aicard-web-oauth/src/config/app.ts
Normal file
84
aicard-web-oauth/src/config/app.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Application configuration
|
||||||
|
export interface AppConfig {
|
||||||
|
adminDid: string;
|
||||||
|
collections: {
|
||||||
|
comment: string;
|
||||||
|
user: string;
|
||||||
|
};
|
||||||
|
host: string;
|
||||||
|
rkey?: string; // Current post rkey if on post page
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate collection names from host
|
||||||
|
// Format: ${reg}.${name}.${sub}
|
||||||
|
// Example: log.syui.ai -> ai.syui.log
|
||||||
|
function generateCollectionNames(host: string): { comment: string; user: string } {
|
||||||
|
try {
|
||||||
|
// Remove protocol if present
|
||||||
|
const cleanHost = host.replace(/^https?:\/\//, '');
|
||||||
|
|
||||||
|
// Split host into parts
|
||||||
|
const parts = cleanHost.split('.');
|
||||||
|
|
||||||
|
if (parts.length < 2) {
|
||||||
|
throw new Error('Invalid host format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse the parts for collection naming
|
||||||
|
// log.syui.ai -> ai.syui.log
|
||||||
|
const reversedParts = parts.reverse();
|
||||||
|
const collectionBase = reversedParts.join('.');
|
||||||
|
|
||||||
|
return {
|
||||||
|
comment: collectionBase,
|
||||||
|
user: `${collectionBase}.user`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to generate collection names from host:', host, error);
|
||||||
|
// Fallback to default collections
|
||||||
|
return {
|
||||||
|
comment: 'ai.syui.log',
|
||||||
|
user: 'ai.syui.log.user'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract rkey from current URL
|
||||||
|
// /posts/xxx.html -> xxx
|
||||||
|
function extractRkeyFromUrl(): string | undefined {
|
||||||
|
const pathname = window.location.pathname;
|
||||||
|
const match = pathname.match(/\/posts\/([^/]+)\.html$/);
|
||||||
|
return match ? match[1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get application configuration from environment variables
|
||||||
|
export function getAppConfig(): AppConfig {
|
||||||
|
const host = import.meta.env.VITE_APP_HOST || 'https://log.syui.ai';
|
||||||
|
const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn';
|
||||||
|
|
||||||
|
// Priority: Environment variables > Auto-generated from host
|
||||||
|
const autoGeneratedCollections = generateCollectionNames(host);
|
||||||
|
const collections = {
|
||||||
|
comment: import.meta.env.VITE_COLLECTION_COMMENT || autoGeneratedCollections.comment,
|
||||||
|
user: import.meta.env.VITE_COLLECTION_USER || autoGeneratedCollections.user,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rkey = extractRkeyFromUrl();
|
||||||
|
|
||||||
|
console.log('App configuration:', {
|
||||||
|
host,
|
||||||
|
adminDid,
|
||||||
|
collections,
|
||||||
|
rkey: rkey || 'none (not on post page)'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
adminDid,
|
||||||
|
collections,
|
||||||
|
host,
|
||||||
|
rkey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const appConfig = getAppConfig();
|
Reference in New Issue
Block a user