This commit is contained in:
2025-06-19 15:02:54 +09:00
parent ca31760728
commit 01c4c543fc
6 changed files with 267 additions and 105 deletions

View File

@@ -253,6 +253,24 @@ function setupAskAIEventListeners() {
handleAIResponse(event.detail);
});
// Listen for OAuth callback completion from iframe
window.addEventListener('message', function(event) {
if (event.data.type === 'oauth_success') {
console.log('Received OAuth success message:', event.data);
// Close any OAuth popups/iframes
const oauthFrame = document.getElementById('oauth-frame');
if (oauthFrame) {
oauthFrame.remove();
}
// Reload the page to refresh OAuth app state
setTimeout(() => {
window.location.reload();
}, 500);
}
});
// Track IME composition state
let isComposing = false;
const aiQuestionInput = document.getElementById('aiQuestion');
@@ -284,52 +302,8 @@ function setupAskAIEventListeners() {
});
}
// OAuth Callback handling
function handleOAuthCallback() {
// Check if we're on the callback page
if (window.location.pathname === '/oauth/callback') {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
console.error('OAuth error:', error);
// Redirect to home page with error
setTimeout(() => {
window.location.href = '/?oauth_error=' + encodeURIComponent(error);
}, 1000);
return;
}
if (code) {
console.log('OAuth callback successful, code received');
// Get the original page from localStorage or use home page
const originalPage = localStorage.getItem('oauth_original_page') || '/';
localStorage.removeItem('oauth_original_page');
// Wait a bit for OAuth app to process the callback
setTimeout(() => {
console.log('Redirecting back to:', originalPage);
window.location.href = originalPage;
}, 2000);
return;
}
}
// Store current page before OAuth if we're not on callback page
if (window.location.pathname !== '/oauth/callback') {
localStorage.setItem('oauth_original_page', window.location.href);
}
}
// Initialize Ask AI when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Handle OAuth callback first
handleOAuthCallback();
// Then initialize Ask AI
setupAskAIEventListeners();
console.log('Ask AI initialized successfully');
});

View File

@@ -44,7 +44,7 @@ body {
.oauth-header-content {
display: flex;
justify-content: flex-start;
justify-content: space-between;
align-items: center;
max-width: 800px;
margin: 0 auto;
@@ -62,7 +62,74 @@ body {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
}
/* OAuth User Profile in Header */
.oauth-user-profile {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.profile-avatar-section {
flex-shrink: 0;
}
.profile-avatar-section .profile-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--border);
}
.profile-avatar-fallback {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--background-secondary);
border: 2px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
color: var(--text-secondary);
}
.profile-info {
flex: 1;
min-width: 0;
}
.profile-display-name {
font-size: 18px;
font-weight: 700;
color: var(--text);
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-handle {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-did {
font-size: 11px;
color: var(--text-secondary);
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 0.7;
}
/* Buttons */
@@ -647,6 +714,41 @@ body {
.chat-container {
height: 300px;
}
/* OAuth User Profile Mobile */
.oauth-user-profile {
gap: 8px;
}
.profile-avatar-section .profile-avatar,
.profile-avatar-fallback {
width: 36px;
height: 36px;
font-size: 14px;
}
.profile-display-name {
font-size: 14px;
}
.profile-handle {
font-size: 12px;
}
.profile-did {
font-size: 9px;
}
.oauth-header-content {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.oauth-header-actions {
width: 100%;
justify-content: center;
}
}
/* Avatar Styles */

View File

@@ -69,8 +69,32 @@ export default function App() {
if (isLoading) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<p>読み込み中...</p>
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '200px',
padding: '40px',
textAlign: 'center'
}}>
<div style={{
width: '40px',
height: '40px',
border: '4px solid #f3f3f3',
borderTop: '4px solid #667eea',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginBottom: '16px'
}} />
<p style={{ color: '#666', margin: 0 }}>読み込み中...</p>
<style jsx>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
)
}
@@ -115,6 +139,34 @@ export default function App() {
<div className="app">
<header className="oauth-app-header">
<div className="oauth-header-content">
{user && (
<div className="oauth-user-profile">
<div className="profile-avatar-section">
{user.avatar ? (
<img
src={user.avatar}
alt={user.displayName || user.handle}
className="profile-avatar"
/>
) : (
<div className="profile-avatar-fallback">
{(user.displayName || user.handle || '?').charAt(0).toUpperCase()}
</div>
)}
</div>
<div className="profile-info">
<div className="profile-display-name">
{user.displayName || user.handle}
</div>
<div className="profile-handle">
@{user.handle}
</div>
<div className="profile-did">
{user.did}
</div>
</div>
</div>
)}
<div className="oauth-header-actions">
<AuthButton
user={user}

View File

@@ -1,54 +1,36 @@
import React, { useEffect, useState } from 'react'
export default function OAuthCallback({ onAuthSuccess }) {
const [status, setStatus] = useState('OAuth認証処理中...')
useEffect(() => {
handleCallback()
}, [])
const handleCallback = async () => {
try {
// BrowserOAuthClientが自動的にコールバックを処理します
// URLのパラメータを確認して成功を通知
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
const error = urlParams.get('error')
if (error) {
throw new Error(`OAuth error: ${error}`)
}
if (code) {
setStatus('認証成功!元のページに戻ります...')
// Get the referring page or use root
const referrer = document.referrer || window.location.origin
const returnUrl = referrer.includes('/oauth/callback') ? window.location.origin : referrer
// 少し待ってから元のページにリダイレクト
setTimeout(() => {
window.location.href = returnUrl
}, 1500)
} else {
setStatus('認証情報が見つかりません')
}
} catch (error) {
console.error('Callback error:', error)
setStatus('認証エラー: ' + error.message)
}
}
import React from 'react'
export default function OAuthCallback() {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>OAuth認証</h2>
<p>{status}</p>
{status.includes('エラー') && (
<button onClick={() => window.location.href = '/'}>
メインページに戻る
</button>
)}
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '50vh',
padding: '40px',
textAlign: 'center'
}}>
<div style={{
width: '50px',
height: '50px',
border: '4px solid #f3f3f3',
borderTop: '4px solid #667eea',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginBottom: '20px'
}} />
<h2 style={{ color: '#333', marginBottom: '12px' }}>OAuth認証処理中...</h2>
<p style={{ color: '#666', fontSize: '14px' }}>
認証が完了しましたら自動で元のページに戻ります
</p>
<style jsx>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
)
}

View File

@@ -18,6 +18,32 @@ export function useAuth() {
if (authResult) {
setUser(authResult.user)
setAgent(authResult.agent)
// If we're on callback page and authentication succeeded, notify parent
if (window.location.pathname === '/oauth/callback') {
console.log('OAuth callback completed, notifying parent window')
// Get referrer or use stored return URL
const returnUrl = sessionStorage.getItem('oauth_return_url') ||
document.referrer ||
window.location.origin
sessionStorage.removeItem('oauth_return_url')
// Notify parent window if in iframe, otherwise redirect directly
if (window.parent !== window) {
window.parent.postMessage({
type: 'oauth_success',
returnUrl: returnUrl,
user: authResult.user
}, '*')
} else {
// Direct redirect
setTimeout(() => {
window.location.href = returnUrl
}, 1000)
}
}
}
} catch (error) {
console.error('Auth initialization failed:', error)
@@ -27,6 +53,11 @@ export function useAuth() {
}
const login = async (handle) => {
// Store current page URL for post-auth redirect
if (window.location.pathname !== '/oauth/callback') {
sessionStorage.setItem('oauth_return_url', window.location.href)
}
await oauthService.login(handle)
}

View File

@@ -65,6 +65,8 @@ export class OAuthService {
async processSession(session) {
const did = session.sub || session.did
let handle = session.handle || 'unknown'
let displayName = null
let avatar = null
// Create Agent directly with session (per official docs)
try {
@@ -77,21 +79,40 @@ export class OAuthService {
})
}
this.sessionInfo = { did, handle }
// Resolve handle if missing
if (handle === 'unknown' && this.agent) {
// Get profile information using authenticated agent
if (this.agent) {
try {
await new Promise(resolve => setTimeout(resolve, 300))
const profile = await this.agent.getProfile({ actor: did })
handle = profile.data.handle
this.sessionInfo.handle = handle
handle = profile.data.handle || handle
displayName = profile.data.displayName || null
avatar = profile.data.avatar || null
console.log('Profile fetched from session:', {
did,
handle,
displayName,
avatar: avatar ? 'present' : 'none'
})
} catch (error) {
console.log('Failed to resolve handle:', error)
console.log('Failed to get profile from session:', error)
// Keep the basic info we have
}
}
return { did, handle }
this.sessionInfo = {
did,
handle,
displayName,
avatar
}
return {
did,
handle,
displayName,
avatar
}
}
async login(handle) {