This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
// api.ts から分割(挙動不変): スペース(CRUD・メンバー・招待・ツールポリシー・Python パッケージ)。
|
||||
import { BASE } from './client';
|
||||
|
||||
// --- Spaces (個人/案件) ---------------------------------------------------
|
||||
|
||||
export interface Space {
|
||||
id: string;
|
||||
kind: 'personal' | 'case';
|
||||
title: string;
|
||||
description: string;
|
||||
ownerId: string | null;
|
||||
visibility: 'private' | 'org' | 'public';
|
||||
visibilityScopeOrgId: string | null;
|
||||
status: 'open' | 'archived';
|
||||
brandColor: string | null;
|
||||
workspaceDir: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
/** 閲覧者自身のメンバーロール。一覧(GET /spaces)・詳細(GET /spaces/:id)の両方で付与。
|
||||
* 非メンバー(根オーナー含む。owner_id はメンバー行ではない)は null。 */
|
||||
myRole?: SpaceMemberRole | null;
|
||||
}
|
||||
|
||||
export async function fetchSpaces(): Promise<Space[]> {
|
||||
const res = await fetch(`${BASE}/local/spaces`);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch spaces');
|
||||
return data as Space[];
|
||||
}
|
||||
|
||||
export type SpaceMemberRole = 'owner' | 'editor' | 'viewer';
|
||||
|
||||
export interface SpaceMember {
|
||||
userId: string;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
avatarUrl: string | null;
|
||||
role: SpaceMemberRole;
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
export async function fetchSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/members`);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch members');
|
||||
return data as SpaceMember[];
|
||||
}
|
||||
|
||||
export async function addSpaceMember(
|
||||
spaceId: string,
|
||||
input: { userId: string; role: SpaceMemberRole },
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/members`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
throw new Error(d?.error ?? 'Failed to add member');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSpaceMemberRole(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
role: SpaceMemberRole,
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/members/${encodeURIComponent(userId)}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
throw new Error(d?.error ?? 'Failed to update member role');
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeSpaceMember(spaceId: string, userId: string): Promise<void> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/members/${encodeURIComponent(userId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
throw new Error(d?.error ?? 'Failed to remove member');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ワークスペース招待リンク(再利用トークン)──────────────────────
|
||||
export type SpaceInviteRole = 'editor' | 'viewer';
|
||||
|
||||
export interface SpaceInviteInfo {
|
||||
token: string;
|
||||
/** 相対パス。origin を前置して共有する。 */
|
||||
url: string;
|
||||
role: SpaceInviteRole;
|
||||
createdAt: string;
|
||||
expiresAt: string | null;
|
||||
revokedAt: string | null;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
/** 現行の招待リンク(canManageSpace)。無ければ null。 */
|
||||
export async function fetchSpaceInvite(spaceId: string): Promise<SpaceInviteInfo | null> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/invite`);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch invite');
|
||||
return (data?.invite ?? null) as SpaceInviteInfo | null;
|
||||
}
|
||||
|
||||
/** 招待リンクを (再)生成する(canManageSpace)。 */
|
||||
export async function createSpaceInvite(
|
||||
spaceId: string,
|
||||
input: { role: SpaceInviteRole; expiresInDays?: number | null },
|
||||
): Promise<SpaceInviteInfo> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/invite`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to create invite');
|
||||
return data.invite as SpaceInviteInfo;
|
||||
}
|
||||
|
||||
/** 招待リンクを無効化する(canManageSpace)。 */
|
||||
export async function revokeSpaceInvite(spaceId: string): Promise<void> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/invite`, { method: 'DELETE' });
|
||||
if (!res.ok && res.status !== 204) {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
throw new Error(d?.error ?? 'Failed to revoke invite');
|
||||
}
|
||||
}
|
||||
|
||||
export interface InvitePreview {
|
||||
spaceId: string;
|
||||
spaceTitle: string;
|
||||
role: SpaceInviteRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* 招待トークンのプレビュー。`status` で分岐を呼び出し側に渡す:
|
||||
* 'ok' → preview / 'unauthorized'(要ログイン)/ 'invalid'(無効・期限切れ・不明)。
|
||||
*/
|
||||
export async function fetchInvitePreview(
|
||||
token: string,
|
||||
): Promise<{ status: 'ok'; preview: InvitePreview } | { status: 'unauthorized' | 'invalid' }> {
|
||||
const res = await fetch(`${BASE}/local/spaces/invite/${encodeURIComponent(token)}`);
|
||||
if (res.status === 401) return { status: 'unauthorized' };
|
||||
if (!res.ok) return { status: 'invalid' };
|
||||
const data = await res.json();
|
||||
return { status: 'ok', preview: data as InvitePreview };
|
||||
}
|
||||
|
||||
/** 招待を受諾して参加する。参加先 spaceId を返す。 */
|
||||
export async function acceptSpaceInvite(token: string): Promise<{ spaceId: string; alreadyMember: boolean }> {
|
||||
const res = await fetch(`${BASE}/local/spaces/invite/${encodeURIComponent(token)}/accept`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to accept invite');
|
||||
return data as { spaceId: string; alreadyMember: boolean };
|
||||
}
|
||||
|
||||
|
||||
// ─── ワークスペース ツールポリシー ────────────────────────────────────────
|
||||
|
||||
export interface ToolCategory {
|
||||
name: string;
|
||||
sensitive: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SpaceToolPolicy {
|
||||
disabledSafe: string[];
|
||||
enabledSensitive: string[];
|
||||
}
|
||||
|
||||
export interface SpaceToolPolicyResponse {
|
||||
policy: SpaceToolPolicy;
|
||||
categories: ToolCategory[];
|
||||
sensitiveTools: { name: string; enabled: boolean }[];
|
||||
}
|
||||
|
||||
export async function fetchSpaceToolPolicy(spaceId: string): Promise<SpaceToolPolicyResponse> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/tool-policy`);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch tool policy');
|
||||
return data as SpaceToolPolicyResponse;
|
||||
}
|
||||
|
||||
export async function updateSpaceToolPolicy(
|
||||
spaceId: string,
|
||||
patch: { disabledSafe?: string[]; enabledSensitive?: string[] },
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/tool-policy`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
throw new Error(d?.error ?? 'Failed to update tool policy');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Per-space Python packages ────────────────────────────────────────────
|
||||
export interface SpacePythonPackage {
|
||||
name: string;
|
||||
spec: string;
|
||||
addedAt: string;
|
||||
}
|
||||
export interface SpacePythonPackagesResponse {
|
||||
enabled: boolean;
|
||||
maxPackagesPerSpace: number;
|
||||
preflight: { ok: boolean; reason?: string };
|
||||
packages: SpacePythonPackage[];
|
||||
}
|
||||
|
||||
export async function fetchSpacePythonPackages(spaceId: string): Promise<SpacePythonPackagesResponse> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/python-packages`);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch python packages');
|
||||
return data as SpacePythonPackagesResponse;
|
||||
}
|
||||
|
||||
export async function addSpacePythonPackage(
|
||||
spaceId: string,
|
||||
spec: string,
|
||||
): Promise<{ packages: SpacePythonPackage[] }> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/python-packages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ spec }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to add package');
|
||||
return data as { packages: SpacePythonPackage[] };
|
||||
}
|
||||
|
||||
export async function removeSpacePythonPackage(
|
||||
spaceId: string,
|
||||
name: string,
|
||||
): Promise<{ packages: SpacePythonPackage[] }> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${spaceId}/python-packages/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to remove package');
|
||||
return data as { packages: SpacePythonPackage[] };
|
||||
}
|
||||
|
||||
export async function createSpace(input: {
|
||||
title: string;
|
||||
description?: string;
|
||||
brandColor?: string | null;
|
||||
visibility?: Space['visibility'];
|
||||
}): Promise<Space> {
|
||||
const res = await fetch(`${BASE}/local/spaces`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to create space');
|
||||
return data as Space;
|
||||
}
|
||||
|
||||
export async function updateSpace(
|
||||
id: string,
|
||||
patch: Partial<Pick<Space, 'title' | 'description' | 'brandColor' | 'visibility'>>,
|
||||
): Promise<Space> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.error ?? 'Failed to update space');
|
||||
return data as Space;
|
||||
}
|
||||
|
||||
export async function archiveSpace(id: string): Promise<void> {
|
||||
const res = await fetch(`${BASE}/local/spaces/${id}/archive`, { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
throw new Error(d?.error ?? 'Failed to archive space');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user