- packages/bridge/Dockerfile: multi-stage build (node:22-alpine) - frontend/Dockerfile: multi-stage build + nginx:alpine - frontend/nginx.conf: proxy /bridge to bridge service - docker-compose.yml: bridge (3456) + frontend (8080) - .dockerignore: exclude backend/node_modules/dist - Fix vite proxy target to port 3456 - .env with bridge vars (AUTH_MODE=dev)
97 lines
2.5 KiB
TypeScript
97 lines
2.5 KiB
TypeScript
import type {
|
|
Board,
|
|
BoardListData,
|
|
TaskSummary,
|
|
TaskDetail,
|
|
TaskListData,
|
|
CreateTaskPayload,
|
|
CommentPayload,
|
|
ApiResponse,
|
|
HealthData,
|
|
} from '@kanban/contracts'
|
|
import { getInitData } from '../telegram'
|
|
|
|
const BRIDGE_URL = import.meta.env.VITE_BRIDGE_URL || '/bridge'
|
|
const AUTH_TOKEN: string = import.meta.env.VITE_AUTH_TOKEN || ''
|
|
|
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
const headers = new Headers(init.headers)
|
|
headers.set('Content-Type', 'application/json')
|
|
|
|
// Auth: prefer X-Bridge-Token, fall back to Telegram initData
|
|
if (AUTH_TOKEN) {
|
|
headers.set('X-Bridge-Token', AUTH_TOKEN)
|
|
} else {
|
|
const initData = getInitData()
|
|
if (initData) {
|
|
headers.set('X-Bridge-Token', initData)
|
|
}
|
|
}
|
|
|
|
const url = `${BRIDGE_URL}${path}`
|
|
const response = await fetch(url, { ...init, headers })
|
|
|
|
const data = await response.json().catch(() => null)
|
|
|
|
if (!response.ok || !data) {
|
|
const message =
|
|
(data as { error?: { message?: string } })?.error?.message ||
|
|
response.statusText ||
|
|
`HTTP ${response.status}`
|
|
throw new Error(message)
|
|
}
|
|
|
|
const envelope = data as ApiResponse<T>
|
|
if (!envelope.ok) {
|
|
throw new Error(envelope.error?.message || 'API error')
|
|
}
|
|
|
|
return envelope.data
|
|
}
|
|
|
|
// ─── API methods ────────────────────────────────────────────
|
|
|
|
export async function fetchHealth(): Promise<HealthData> {
|
|
return request<HealthData>('/v1/health')
|
|
}
|
|
|
|
export async function fetchBoards(): Promise<Board[]> {
|
|
return request<BoardListData>('/v1/boards')
|
|
}
|
|
|
|
export async function fetchTasks(board: string): Promise<TaskListData> {
|
|
return request<TaskListData>(`/v1/boards/${encodeURIComponent(board)}/tasks`)
|
|
}
|
|
|
|
export async function fetchTask(board: string, taskId: string): Promise<TaskDetail> {
|
|
return request<TaskDetail>(
|
|
`/v1/boards/${encodeURIComponent(board)}/tasks/${encodeURIComponent(taskId)}`
|
|
)
|
|
}
|
|
|
|
export async function createTask(
|
|
board: string,
|
|
payload: CreateTaskPayload
|
|
): Promise<TaskDetail> {
|
|
return request<TaskDetail>(
|
|
`/v1/boards/${encodeURIComponent(board)}/tasks`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
}
|
|
)
|
|
}
|
|
|
|
export async function addComment(
|
|
board: string,
|
|
taskId: string,
|
|
payload: CommentPayload
|
|
): Promise<void> {
|
|
await request<void>(
|
|
`/v1/boards/${encodeURIComponent(board)}/tasks/${encodeURIComponent(taskId)}/comments`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
}
|
|
)
|
|
}
|