fix oauth

This commit is contained in:
2026-01-18 14:26:43 +09:00
parent 1fd619e32b
commit 5727f0a701
27 changed files with 3531 additions and 8 deletions

224
src/web/main.ts Normal file
View File

@@ -0,0 +1,224 @@
import './styles/main.css'
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks } from './lib/api'
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
import { login, logout, handleCallback, restoreSession } from './lib/auth'
import { renderHeader } from './components/header'
import { renderProfile } from './components/profile'
import { renderPostList, renderPostDetail } from './components/posts'
import { renderCollectionButtons, renderServerInfo, renderServiceList, renderCollectionList, renderRecordList, renderRecordDetail } from './components/browser'
import { renderFooter } from './components/footer'
const app = document.getElementById('app')!
let currentHandle = ''
// Filter collections by service domain
function filterCollectionsByService(collections: string[], service: string): string[] {
return collections.filter(col => {
const parts = col.split('.')
if (parts.length >= 2) {
const colService = `${parts[1]}.${parts[0]}`
return colService === service
}
return false
})
}
// Get web URL for handle from networks
async function getWebUrl(handle: string): Promise<string | undefined> {
const networks = await getNetworks()
// Check each network for matching handle domain
for (const [domain, network] of Object.entries(networks)) {
// Direct domain match (e.g., handle.syu.is -> syu.is)
if (handle.endsWith(`.${domain}`)) {
return network.web
}
// Check if handle domain matches network's web domain (e.g., syui.syui.ai -> syu.is via web: syu.is)
const webDomain = network.web?.replace(/^https?:\/\//, '')
if (webDomain && handle.endsWith(`.${webDomain}`)) {
return network.web
}
}
// Check for syui.ai handles -> syu.is network
if (handle.endsWith('.syui.ai')) {
return networks['syu.is']?.web
}
// Default to first network's web
const firstNetwork = Object.values(networks)[0]
return firstNetwork?.web
}
async function render(route: Route): Promise<void> {
try {
const config = await getConfig()
// Apply theme color from config
if (config.color) {
document.documentElement.style.setProperty('--btn-color', config.color)
}
// Handle OAuth callback if present (check both ? and #)
const searchParams = new URLSearchParams(window.location.search)
const hashParams = window.location.hash ? new URLSearchParams(window.location.hash.slice(1)) : null
if (searchParams.has('code') || searchParams.has('state') || hashParams?.has('code') || hashParams?.has('state')) {
await handleCallback()
} else {
// Try to restore existing session
await restoreSession()
}
// Determine handle and whether to use local data
let handle: string
let localFirst: boolean
if (route.type === 'home') {
handle = config.handle
localFirst = true
} else if (route.handle) {
handle = route.handle
localFirst = handle === config.handle
} else {
handle = config.handle
localFirst = true
}
currentHandle = handle
// Resolve handle to DID
const did = await resolveHandle(handle)
if (!did) {
app.innerHTML = `
${renderHeader(handle)}
<div class="error">Could not resolve handle: ${handle}</div>
${renderFooter(handle)}
`
setupEventHandlers()
return
}
// Load profile
const profile = await getProfile(did, localFirst)
const webUrl = await getWebUrl(handle)
// Build page
let html = renderHeader(handle)
// Profile section
if (profile) {
html += await renderProfile(did, profile, handle, webUrl)
}
// Content section based on route type
if (route.type === 'record' && route.collection && route.rkey) {
// AT-Browser: Single record view
const record = await getRecord(did, route.collection, route.rkey)
if (record) {
html += `<div id="content">${renderRecordDetail(record, route.collection)}</div>`
} else {
html += `<div id="content" class="error">Record not found</div>`
}
html += `<nav class="back-nav"><a href="/@${handle}/at/collection/${route.collection}">${route.collection}</a></nav>`
} else if (route.type === 'collection' && route.collection) {
// AT-Browser: Collection records list
const records = await listRecords(did, route.collection)
html += `<div id="content">${renderRecordList(records, handle, route.collection)}</div>`
const parts = route.collection.split('.')
const service = parts.length >= 2 ? `${parts[1]}.${parts[0]}` : ''
html += `<nav class="back-nav"><a href="/@${handle}/at/service/${encodeURIComponent(service)}">${service}</a></nav>`
} else if (route.type === 'service' && route.service) {
// AT-Browser: Service collections list
const collections = await describeRepo(did)
const filtered = filterCollectionsByService(collections, route.service)
html += `<div id="content">${renderCollectionList(filtered, handle, route.service)}</div>`
html += `<nav class="back-nav"><a href="/@${handle}/at">at</a></nav>`
} else if (route.type === 'atbrowser') {
// AT-Browser: Main view with server info + service list
const pds = await getPds(did)
const collections = await describeRepo(did)
html += `<div id="browser">`
html += renderServerInfo(did, pds)
html += renderServiceList(collections, handle)
html += `</div>`
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
} else if (route.type === 'post' && route.rkey) {
// Post detail (config.collection with markdown)
const post = await getPost(did, config.collection, route.rkey, localFirst)
if (post) {
html += `<div id="content">${renderPostDetail(post, handle, config.collection)}</div>`
} else {
html += `<div id="content" class="error">Post not found</div>`
}
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
} else {
// User page: compact collection buttons + posts
const collections = await describeRepo(did)
html += `<div id="browser">${renderCollectionButtons(collections, handle)}</div>`
const posts = await getPosts(did, config.collection, localFirst)
html += `<div id="content">${renderPostList(posts, handle)}</div>`
}
html += renderFooter(handle)
app.innerHTML = html
setupEventHandlers()
} catch (error) {
console.error('Render error:', error)
app.innerHTML = `
${renderHeader(currentHandle)}
<div class="error">Error: ${error}</div>
${renderFooter(currentHandle)}
`
setupEventHandlers()
}
}
function setupEventHandlers(): void {
// Header form
const form = document.getElementById('header-form') as HTMLFormElement
const input = document.getElementById('header-input') as HTMLInputElement
form?.addEventListener('submit', (e) => {
e.preventDefault()
const handle = input.value.trim()
if (handle) {
navigate({ type: 'user', handle })
}
})
// Login button
const loginBtn = document.getElementById('login-btn')
loginBtn?.addEventListener('click', async () => {
const handle = input.value.trim() || currentHandle
if (handle) {
try {
await login(handle)
} catch (e) {
console.error('Login failed:', e)
alert('Login failed. Please check your handle.')
}
} else {
alert('Please enter a handle first.')
}
})
// Logout button
const logoutBtn = document.getElementById('logout-btn')
logoutBtn?.addEventListener('click', async () => {
await logout()
})
}
// Initial render
render(parseRoute())
// Handle route changes
onRouteChange(render)