This commit is contained in:
@ -16,8 +16,8 @@ AI-powered static blog generator with ATProto integration, part of the ai.ai eco
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `./run.zsh c` | Enable Cloudflare tunnel (xxxcard.syui.ai) for OAuth |
|
| `./run.zsh c` | Enable Cloudflare tunnel (log.syui.ai) for OAuth |
|
||||||
| `./run.zsh o` | Start OAuth web server (port:4173 = xxxcard.syui.ai) |
|
| `./run.zsh o` | Start OAuth web server (port:4173 = log.syui.ai) |
|
||||||
| `./run.zsh co` | Start comment system (ATProto stream monitor) |
|
| `./run.zsh co` | Start comment system (ATProto stream monitor) |
|
||||||
|
|
||||||
## 🏗️ Architecture (Pure Rust + HTML + JS)
|
## 🏗️ Architecture (Pure Rust + HTML + JS)
|
||||||
|
4
aicard-web-oauth/.env
Normal file
4
aicard-web-oauth/.env
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Default environment variables (fallback)
|
||||||
|
VITE_APP_HOST=https://log.syui.ai
|
||||||
|
VITE_OAUTH_CLIENT_ID=https://log.syui.ai/client-metadata.json
|
||||||
|
VITE_OAUTH_REDIRECT_URI=https://log.syui.ai/oauth/callback
|
4
aicard-web-oauth/.env.development
Normal file
4
aicard-web-oauth/.env.development
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Development environment variables
|
||||||
|
VITE_APP_HOST=http://localhost:4173
|
||||||
|
VITE_OAUTH_CLIENT_ID=http://localhost:4173/client-metadata.json
|
||||||
|
VITE_OAUTH_REDIRECT_URI=http://localhost:4173/oauth/callback
|
4
aicard-web-oauth/.env.production
Normal file
4
aicard-web-oauth/.env.production
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Production environment variables
|
||||||
|
VITE_APP_HOST=https://log.syui.ai
|
||||||
|
VITE_OAUTH_CLIENT_ID=https://log.syui.ai/client-metadata.json
|
||||||
|
VITE_OAUTH_REDIRECT_URI=https://log.syui.ai/oauth/callback
|
@ -6,6 +6,7 @@
|
|||||||
"dev": "vite --mode development",
|
"dev": "vite --mode development",
|
||||||
"build": "vite build --mode production",
|
"build": "vite build --mode production",
|
||||||
"build:dev": "vite build --mode development",
|
"build:dev": "vite build --mode development",
|
||||||
|
"build:local": "VITE_APP_HOST=http://localhost:4173 vite build --mode development",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"client_id": "https://xxxcard.syui.ai/client-metadata.json",
|
"client_id": "https://log.syui.ai/client-metadata.json",
|
||||||
"client_name": "ai.card",
|
"client_name": "ai.card",
|
||||||
"client_uri": "https://xxxcard.syui.ai",
|
"client_uri": "https://log.syui.ai",
|
||||||
"logo_uri": "https://xxxcard.syui.ai/favicon.ico",
|
"logo_uri": "https://log.syui.ai/favicon.ico",
|
||||||
"tos_uri": "https://xxxcard.syui.ai/terms",
|
"tos_uri": "https://log.syui.ai/terms",
|
||||||
"policy_uri": "https://xxxcard.syui.ai/privacy",
|
"policy_uri": "https://log.syui.ai/privacy",
|
||||||
"redirect_uris": [
|
"redirect_uris": [
|
||||||
"https://xxxcard.syui.ai/oauth/callback",
|
"https://log.syui.ai/oauth/callback",
|
||||||
"https://xxxcard.syui.ai/"
|
"https://log.syui.ai/"
|
||||||
],
|
],
|
||||||
"response_types": [
|
"response_types": [
|
||||||
"code"
|
"code"
|
||||||
|
@ -99,9 +99,9 @@ function App() {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// キャッシュがなければ、ATProtoから取得
|
// キャッシュがなければ、ATProtoから取得(認証状態に関係なく)
|
||||||
if (!loadCachedComments()) {
|
if (!loadCachedComments()) {
|
||||||
loadAllComments(window.location.href);
|
loadAllComments(); // URLフィルタリングを無効にして全コメント表示
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle popstate events for mock OAuth flow
|
// Handle popstate events for mock OAuth flow
|
||||||
@ -142,7 +142,8 @@ function App() {
|
|||||||
setUser(userProfile);
|
setUser(userProfile);
|
||||||
|
|
||||||
// Load all comments for display (this will be the default view)
|
// Load all comments for display (this will be the default view)
|
||||||
loadAllComments(window.location.href);
|
// Temporarily disable URL filtering to see all comments
|
||||||
|
loadAllComments();
|
||||||
|
|
||||||
// Load user list records if admin
|
// Load user list records if admin
|
||||||
if (userProfile.did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') {
|
if (userProfile.did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') {
|
||||||
@ -161,7 +162,8 @@ function App() {
|
|||||||
setUser(verifiedUser);
|
setUser(verifiedUser);
|
||||||
|
|
||||||
// Load all comments for display (this will be the default view)
|
// Load all comments for display (this will be the default view)
|
||||||
loadAllComments(window.location.href);
|
// Temporarily disable URL filtering to see all comments
|
||||||
|
loadAllComments();
|
||||||
|
|
||||||
// Load user list records if admin
|
// Load user list records if admin
|
||||||
if (verifiedUser.did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') {
|
if (verifiedUser.did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') {
|
||||||
@ -169,6 +171,9 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// 認証状態に関係なく、コメントを読み込む
|
||||||
|
loadAllComments();
|
||||||
};
|
};
|
||||||
|
|
||||||
checkAuth();
|
checkAuth();
|
||||||
@ -265,17 +270,20 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
// 管理者のユーザーリストを取得 (ai.syui.log.user collection)
|
// 管理者のユーザーリストを取得 (ai.syui.log.user collection)
|
||||||
const adminDid = 'did:plc:uqzpqmrjnptsxezjx4xuh2mn'; // syui.ai
|
const adminDid = 'did:plc:uqzpqmrjnptsxezjx4xuh2mn'; // syui.ai
|
||||||
|
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=ai.syui.log.user&limit=100`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn('Failed to fetch user list from admin, using default users');
|
console.warn('Failed to fetch user list from admin, using default users. Status:', response.status);
|
||||||
return getDefaultUsers();
|
return getDefaultUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const userRecords = data.records || [];
|
const userRecords = data.records || [];
|
||||||
|
console.log('User records found:', userRecords.length);
|
||||||
|
|
||||||
if (userRecords.length === 0) {
|
if (userRecords.length === 0) {
|
||||||
|
console.log('No user records found, using default users');
|
||||||
return getDefaultUsers();
|
return getDefaultUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,20 +357,33 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultUsers = () => {
|
const getDefaultUsers = () => {
|
||||||
return [
|
const defaultUsers = [
|
||||||
// bsky.social - 実際のDIDを使用
|
// bsky.social - 実際のDIDを使用
|
||||||
{ did: 'did:plc:uqzpqmrjnptsxezjx4xuh2mn', handle: 'syui.ai', pds: 'https://bsky.social' },
|
{ did: 'did:plc:uqzpqmrjnptsxezjx4xuh2mn', handle: 'syui.ai', pds: 'https://bsky.social' },
|
||||||
// 他のユーザーは実際のDIDが不明なので、実在するユーザーのみ含める
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 現在ログインしているユーザーも追加(重複チェック)
|
||||||
|
if (user && user.did && user.handle && !defaultUsers.find(u => u.did === user.did)) {
|
||||||
|
defaultUsers.push({
|
||||||
|
did: user.did,
|
||||||
|
handle: user.handle,
|
||||||
|
pds: user.handle.endsWith('.syu.is') ? 'https://syu.is' : 'https://bsky.social'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Default users list (including current user):', defaultUsers);
|
||||||
|
return defaultUsers;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 新しい関数: 全ユーザーからコメントを収集
|
// 新しい関数: 全ユーザーからコメントを収集
|
||||||
const loadAllComments = async (pageUrl?: string) => {
|
const loadAllComments = async (pageUrl?: string) => {
|
||||||
try {
|
try {
|
||||||
console.log('Loading comments from all users...');
|
console.log('Loading comments from all users...');
|
||||||
|
console.log('Page URL filter:', pageUrl);
|
||||||
|
|
||||||
// ユーザーリストを動的に取得
|
// ユーザーリストを動的に取得
|
||||||
const knownUsers = await loadUsersFromRecord();
|
const knownUsers = await loadUsersFromRecord();
|
||||||
|
console.log('Known users for comment fetching:', knownUsers);
|
||||||
|
|
||||||
const allComments = [];
|
const allComments = [];
|
||||||
|
|
||||||
@ -388,7 +409,8 @@ function App() {
|
|||||||
? userComments.filter(record => record.value.url === pageUrl)
|
? userComments.filter(record => record.value.url === pageUrl)
|
||||||
: userComments;
|
: userComments;
|
||||||
|
|
||||||
console.log(`After URL filtering: ${filteredComments.length} comments from ${user.handle}`);
|
console.log(`After URL filtering (${pageUrl}): ${filteredComments.length} comments from ${user.handle}`);
|
||||||
|
console.log('All comments from this user:', userComments.map(r => ({ url: r.value.url, text: r.value.text })));
|
||||||
allComments.push(...filteredComments);
|
allComments.push(...filteredComments);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`Failed to load comments from ${user.handle}:`, err);
|
console.warn(`Failed to load comments from ${user.handle}:`, err);
|
||||||
@ -859,14 +881,16 @@ function App() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => user && loadUserComments(user.did)}
|
onClick={() => user && loadUserComments(user.did)}
|
||||||
className="comments-toggle-button"
|
className="comments-toggle-button"
|
||||||
|
disabled={!user}
|
||||||
|
title={!user ? "Login required to view your comments" : ""}
|
||||||
>
|
>
|
||||||
My Comments
|
My Comments {!user && "(Login Required)"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => loadAllComments(window.location.href)}
|
onClick={() => loadAllComments()}
|
||||||
className="comments-toggle-button"
|
className="comments-toggle-button"
|
||||||
>
|
>
|
||||||
All Comments
|
All Comments (No Filter)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -188,13 +188,15 @@ class AtprotoOAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getClientId(): string {
|
private getClientId(): string {
|
||||||
const origin = window.location.origin;
|
// Use environment variable if available
|
||||||
|
const envClientId = import.meta.env.VITE_OAUTH_CLIENT_ID;
|
||||||
// For production (xxxcard.syui.ai), use the actual URL
|
if (envClientId) {
|
||||||
if (origin.includes('xxxcard.syui.ai')) {
|
console.log('Using client ID from environment:', envClientId);
|
||||||
return `${origin}/client-metadata.json`;
|
return envClientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const origin = window.location.origin;
|
||||||
|
|
||||||
// For localhost development, use undefined for loopback client
|
// For localhost development, use undefined for loopback client
|
||||||
// The BrowserOAuthClient will handle this automatically
|
// The BrowserOAuthClient will handle this automatically
|
||||||
if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
|
if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
|
||||||
|
@ -157,40 +157,19 @@ export class OAuthKeyManager {
|
|||||||
* Generate dynamic client metadata based on current URL
|
* Generate dynamic client metadata based on current URL
|
||||||
*/
|
*/
|
||||||
export function generateClientMetadata(): any {
|
export function generateClientMetadata(): any {
|
||||||
const origin = window.location.origin;
|
// Use environment variables if available, fallback to current origin
|
||||||
const clientId = `${origin}/client-metadata.json`;
|
const host = import.meta.env.VITE_APP_HOST || window.location.origin;
|
||||||
|
const clientId = import.meta.env.VITE_OAUTH_CLIENT_ID || `${host}/client-metadata.json`;
|
||||||
|
const redirectUri = import.meta.env.VITE_OAUTH_REDIRECT_URI || `${host}/oauth/callback`;
|
||||||
|
|
||||||
// Use static production metadata for xxxcard.syui.ai
|
|
||||||
if (origin === 'https://xxxcard.syui.ai') {
|
|
||||||
return {
|
|
||||||
client_id: 'https://xxxcard.syui.ai/client-metadata.json',
|
|
||||||
client_name: 'ai.card',
|
|
||||||
client_uri: 'https://xxxcard.syui.ai',
|
|
||||||
logo_uri: 'https://xxxcard.syui.ai/favicon.ico',
|
|
||||||
tos_uri: 'https://xxxcard.syui.ai/terms',
|
|
||||||
policy_uri: 'https://xxxcard.syui.ai/privacy',
|
|
||||||
redirect_uris: ['https://xxxcard.syui.ai/oauth/callback'],
|
|
||||||
response_types: ['code'],
|
|
||||||
grant_types: ['authorization_code', 'refresh_token'],
|
|
||||||
token_endpoint_auth_method: 'private_key_jwt',
|
|
||||||
token_endpoint_auth_signing_alg: 'ES256',
|
|
||||||
scope: 'atproto transition:generic',
|
|
||||||
subject_type: 'public',
|
|
||||||
application_type: 'web',
|
|
||||||
dpop_bound_access_tokens: true,
|
|
||||||
jwks_uri: 'https://xxxcard.syui.ai/.well-known/jwks.json'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic metadata for development
|
|
||||||
return {
|
return {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
client_name: 'ai.card',
|
client_name: 'ai.card',
|
||||||
client_uri: origin,
|
client_uri: host,
|
||||||
logo_uri: `${origin}/favicon.ico`,
|
logo_uri: `${host}/favicon.ico`,
|
||||||
tos_uri: `${origin}/terms`,
|
tos_uri: `${host}/terms`,
|
||||||
policy_uri: `${origin}/privacy`,
|
policy_uri: `${host}/privacy`,
|
||||||
redirect_uris: [`${origin}/oauth/callback`],
|
redirect_uris: [redirectUri, host],
|
||||||
response_types: ['code'],
|
response_types: ['code'],
|
||||||
grant_types: ['authorization_code', 'refresh_token'],
|
grant_types: ['authorization_code', 'refresh_token'],
|
||||||
token_endpoint_auth_method: 'private_key_jwt',
|
token_endpoint_auth_method: 'private_key_jwt',
|
||||||
@ -199,6 +178,6 @@ export function generateClientMetadata(): any {
|
|||||||
subject_type: 'public',
|
subject_type: 'public',
|
||||||
application_type: 'web',
|
application_type: 'web',
|
||||||
dpop_bound_access_tokens: true,
|
dpop_bound_access_tokens: true,
|
||||||
jwks_uri: `${origin}/.well-known/jwks.json`
|
jwks_uri: `${host}/.well-known/jwks.json`
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,31 +1,58 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [react()],
|
// Load env file based on `mode` in the current working directory.
|
||||||
build: {
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
// Keep console.log in production for debugging
|
|
||||||
minify: 'esbuild',
|
return {
|
||||||
},
|
plugins: [
|
||||||
esbuild: {
|
react(),
|
||||||
drop: [], // Don't drop console.log
|
// Custom plugin to replace variables in public files during build
|
||||||
},
|
{
|
||||||
server: {
|
name: 'replace-env-vars',
|
||||||
port: 5173,
|
writeBundle() {
|
||||||
host: '127.0.0.1',
|
const host = env.VITE_APP_HOST || 'https://log.syui.ai'
|
||||||
allowedHosts: ['localhost', '127.0.0.1', 'xxxcard.syui.ai'],
|
const clientId = env.VITE_OAUTH_CLIENT_ID || `${host}/client-metadata.json`
|
||||||
proxy: {
|
const redirectUri = env.VITE_OAUTH_REDIRECT_URI || `${host}/oauth/callback`
|
||||||
'/api': {
|
|
||||||
target: 'http://127.0.0.1:8000',
|
// Replace variables in client-metadata.json
|
||||||
changeOrigin: true,
|
const clientMetadataPath = path.resolve(__dirname, 'dist/client-metadata.json')
|
||||||
secure: false,
|
if (fs.existsSync(clientMetadataPath)) {
|
||||||
|
let content = fs.readFileSync(clientMetadataPath, 'utf-8')
|
||||||
|
content = content.replace(/https:\/\/log\.syui\.ai/g, host)
|
||||||
|
fs.writeFileSync(clientMetadataPath, content)
|
||||||
|
console.log(`Updated client-metadata.json with host: ${host}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
// Keep console.log in production for debugging
|
||||||
|
minify: 'esbuild',
|
||||||
},
|
},
|
||||||
// Handle OAuth callback routing
|
esbuild: {
|
||||||
historyApiFallback: {
|
drop: [], // Don't drop console.log
|
||||||
rewrites: [
|
},
|
||||||
{ from: /^\/oauth\/callback/, to: '/index.html' }
|
server: {
|
||||||
]
|
port: 5173,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
allowedHosts: ['localhost', '127.0.0.1', 'log.syui.ai'],
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Handle OAuth callback routing
|
||||||
|
historyApiFallback: {
|
||||||
|
rewrites: [
|
||||||
|
{ from: /^\/oauth\/callback/, to: '/index.html' }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
9
run.zsh
9
run.zsh
@ -18,7 +18,8 @@ function _server() {
|
|||||||
|
|
||||||
function _server_public() {
|
function _server_public() {
|
||||||
_env
|
_env
|
||||||
cloudflared tunnel --config $d/aicard-web-oauth/cloudflared-config.yml run
|
#cloudflared tunnel --config $d/aicard-web-oauth/cloudflared-config.yml run
|
||||||
|
cloudflared tunnel --config $d/cloudflared-config.yml run
|
||||||
}
|
}
|
||||||
|
|
||||||
function _oauth_build() {
|
function _oauth_build() {
|
||||||
@ -29,6 +30,12 @@ function _oauth_build() {
|
|||||||
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" # This loads nvm bash_completion
|
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" # This loads nvm bash_completion
|
||||||
nvm use 21
|
nvm use 21
|
||||||
npm i
|
npm i
|
||||||
|
|
||||||
|
# Build with production environment variables
|
||||||
|
export VITE_APP_HOST="https://log.syui.ai"
|
||||||
|
export VITE_OAUTH_CLIENT_ID="https://log.syui.ai/client-metadata.json"
|
||||||
|
export VITE_OAUTH_REDIRECT_URI="https://log.syui.ai/oauth/callback"
|
||||||
|
|
||||||
npm run build
|
npm run build
|
||||||
npm run preview
|
npm run preview
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user