mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-03 12:42:01 +00:00
feat: add workspace session, auth, and store infrastructure (#8194)
## Summary - Add `teamWorkspaceStore` Pinia store for workspace state management (workspaces, members, invites, current workspace) - Add `workspaceApi` client for workspace CRUD, member management, and invite operations - Update `useWorkspaceSwitch` composable for workspace switching logic - Update `useSessionCookie` for workspace-aware sessions - Update `firebaseAuthStore` for workspace aware auth - Use `workspaceAuthStore` for workspace auth flow ## Test plan - [x] 59 unit tests passing (50 store tests + 9 switch tests) - [x] Typecheck passing - [x] Lint passing - [x] Knip passing Note: This PR depends on the `team_workspaces_enabled` feature flag being available (already in main). 🤖 Generated with [Claude Code](https://claude.ai/code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8194-feat-add-workspace-session-auth-and-store-infrastructure-2ef6d73d3650814984afe8ee7ba0a209) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
335
src/platform/workspace/api/workspaceApi.ts
Normal file
335
src/platform/workspace/api/workspaceApi.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
type WorkspaceType = 'personal' | 'team'
|
||||
type WorkspaceRole = 'owner' | 'member'
|
||||
|
||||
interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
type: WorkspaceType
|
||||
}
|
||||
|
||||
export interface WorkspaceWithRole extends Workspace {
|
||||
role: WorkspaceRole
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
joined_at: string
|
||||
}
|
||||
|
||||
interface PaginationInfo {
|
||||
offset: number
|
||||
limit: number
|
||||
total: number
|
||||
}
|
||||
|
||||
interface ListMembersResponse {
|
||||
members: Member[]
|
||||
pagination: PaginationInfo
|
||||
}
|
||||
|
||||
export interface ListMembersParams {
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface PendingInvite {
|
||||
id: string
|
||||
email: string
|
||||
token: string
|
||||
invited_at: string
|
||||
expires_at: string
|
||||
}
|
||||
|
||||
interface ListInvitesResponse {
|
||||
invites: PendingInvite[]
|
||||
}
|
||||
|
||||
interface CreateInviteRequest {
|
||||
email: string
|
||||
}
|
||||
|
||||
interface AcceptInviteResponse {
|
||||
workspace_id: string
|
||||
workspace_name: string
|
||||
}
|
||||
|
||||
interface BillingPortalRequest {
|
||||
return_url: string
|
||||
}
|
||||
|
||||
interface BillingPortalResponse {
|
||||
billing_portal_url: string
|
||||
}
|
||||
|
||||
interface CreateWorkspacePayload {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface UpdateWorkspacePayload {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface ListWorkspacesResponse {
|
||||
workspaces: WorkspaceWithRole[]
|
||||
}
|
||||
|
||||
class WorkspaceApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status?: number,
|
||||
public readonly code?: string
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'WorkspaceApiError'
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceApiClient = axios.create({
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
async function getAuthHeaderOrThrow() {
|
||||
const authHeader = await useFirebaseAuthStore().getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new WorkspaceApiError(
|
||||
t('toastMessages.userNotAuthenticated'),
|
||||
401,
|
||||
'NOT_AUTHENTICATED'
|
||||
)
|
||||
}
|
||||
return authHeader
|
||||
}
|
||||
|
||||
function handleAxiosError(err: unknown): never {
|
||||
if (axios.isAxiosError(err)) {
|
||||
const status = err.response?.status
|
||||
const message = err.response?.data?.message ?? err.message
|
||||
throw new WorkspaceApiError(message, status)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
export const workspaceApi = {
|
||||
/**
|
||||
* List all workspaces the user has access to
|
||||
* GET /api/workspaces
|
||||
*/
|
||||
async list(): Promise<ListWorkspacesResponse> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
const response = await workspaceApiClient.get<ListWorkspacesResponse>(
|
||||
api.apiURL('/workspaces'),
|
||||
{ headers }
|
||||
)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new workspace
|
||||
* POST /api/workspaces
|
||||
*/
|
||||
async create(payload: CreateWorkspacePayload): Promise<WorkspaceWithRole> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
const response = await workspaceApiClient.post<WorkspaceWithRole>(
|
||||
api.apiURL('/workspaces'),
|
||||
payload,
|
||||
{ headers }
|
||||
)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update workspace name
|
||||
* PATCH /api/workspaces/:id
|
||||
*/
|
||||
async update(
|
||||
workspaceId: string,
|
||||
payload: UpdateWorkspacePayload
|
||||
): Promise<WorkspaceWithRole> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
const response = await workspaceApiClient.patch<WorkspaceWithRole>(
|
||||
api.apiURL(`/workspaces/${workspaceId}`),
|
||||
payload,
|
||||
{ headers }
|
||||
)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a workspace (owner only)
|
||||
* DELETE /api/workspaces/:id
|
||||
*/
|
||||
async delete(workspaceId: string): Promise<void> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
await workspaceApiClient.delete(
|
||||
api.apiURL(`/workspaces/${workspaceId}`),
|
||||
{
|
||||
headers
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Leave the current workspace.
|
||||
* POST /api/workspace/leave
|
||||
*/
|
||||
async leave(): Promise<void> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
await workspaceApiClient.post(api.apiURL('/workspace/leave'), null, {
|
||||
headers
|
||||
})
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List workspace members (paginated).
|
||||
* GET /api/workspace/members
|
||||
*/
|
||||
async listMembers(params?: ListMembersParams): Promise<ListMembersResponse> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
const response = await workspaceApiClient.get<ListMembersResponse>(
|
||||
api.apiURL('/workspace/members'),
|
||||
{ headers, params }
|
||||
)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a member from the workspace.
|
||||
* DELETE /api/workspace/members/:userId
|
||||
*/
|
||||
async removeMember(userId: string): Promise<void> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
await workspaceApiClient.delete(
|
||||
api.apiURL(`/workspace/members/${userId}`),
|
||||
{ headers }
|
||||
)
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List pending invites for the workspace.
|
||||
* GET /api/workspace/invites
|
||||
*/
|
||||
async listInvites(): Promise<ListInvitesResponse> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
const response = await workspaceApiClient.get<ListInvitesResponse>(
|
||||
api.apiURL('/workspace/invites'),
|
||||
{ headers }
|
||||
)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create an invite for the workspace.
|
||||
* POST /api/workspace/invites
|
||||
*/
|
||||
async createInvite(payload: CreateInviteRequest): Promise<PendingInvite> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
const response = await workspaceApiClient.post<PendingInvite>(
|
||||
api.apiURL('/workspace/invites'),
|
||||
payload,
|
||||
{ headers }
|
||||
)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Revoke a pending invite.
|
||||
* DELETE /api/workspace/invites/:inviteId
|
||||
*/
|
||||
async revokeInvite(inviteId: string): Promise<void> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
await workspaceApiClient.delete(
|
||||
api.apiURL(`/workspace/invites/${inviteId}`),
|
||||
{ headers }
|
||||
)
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Accept a workspace invite.
|
||||
* POST /api/invites/:token/accept
|
||||
*/
|
||||
async acceptInvite(token: string): Promise<AcceptInviteResponse> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
const response = await workspaceApiClient.post<AcceptInviteResponse>(
|
||||
api.apiURL(`/invites/${token}/accept`),
|
||||
null,
|
||||
{ headers }
|
||||
)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Access the billing portal for the current workspace.
|
||||
* POST /api/billing/portal
|
||||
*/
|
||||
async accessBillingPortal(
|
||||
returnUrl?: string
|
||||
): Promise<BillingPortalResponse> {
|
||||
const headers = await getAuthHeaderOrThrow()
|
||||
try {
|
||||
const response = await workspaceApiClient.post<BillingPortalResponse>(
|
||||
api.apiURL('/billing/portal'),
|
||||
{
|
||||
return_url: returnUrl ?? window.location.href
|
||||
} satisfies BillingPortalRequest,
|
||||
{ headers }
|
||||
)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
handleAxiosError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user