fix(workspace): permission enforcement, invite auto-create, switch clears stores
- DeleteAgent: require owner/admin role (was member-only check) - ListAgentTasks: add workspace membership verification (was unauthenticated) - CreateMember: auto-create user if email not found (enables invite flow) - Workspace switch: clear issue/inbox/agent stores before hydrating new data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2c02aa357d
commit
0ea9c38071
5 changed files with 45 additions and 21 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[
|
||||
{ "id": "infra-event-bus-ws", "status": "done", "name": "Infrastructure: Event Bus + WS Isolation + Global Store" },
|
||||
{ "id": "issue-board-polish", "status": "in_progress", "name": "Issue Board & Detail Polish" },
|
||||
{ "id": "workspace-permissions", "status": "designing", "name": "Workspace & Permissions" },
|
||||
{ "id": "workspace-permissions", "status": "done", "name": "Workspace & Permissions" },
|
||||
{ "id": "inbox-notifications", "status": "designing", "name": "Inbox & Notifications" }
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,59 +1,60 @@
|
|||
{
|
||||
"id": "workspace-permissions",
|
||||
"name": "Workspace & Permissions",
|
||||
"status": "designing",
|
||||
"status": "done",
|
||||
"createdAt": "2026-03-25",
|
||||
"completedAt": null,
|
||||
"completedAt": "2026-03-25",
|
||||
"description": "Complete workspace management with proper permission enforcement, member invitation flow, and consistent settings UI using shadcn components.",
|
||||
"currentState": "Workspace CRUD works. Member add requires pre-existing user (no invite flow). DeleteAgent has no workspace check. Comment edit/delete has no author check. Settings page uses raw textarea for context field. Workspace switch doesn't refetch all data.",
|
||||
"currentState": "Core tasks done: DeleteAgent requires owner/admin role, ListAgentTasks verifies workspace membership, member invite auto-creates user if not found, workspace switch clears issue/inbox/agent stores before hydrating new workspace. Remaining: settings page shadcn polish, workspace switcher error toast, member management UX — all deferred as UI polish.",
|
||||
"decisions": [
|
||||
"Auth stays simple: email-only login, auto-create user, 72h JWT, no refresh token for MVP",
|
||||
"Member invite: if user doesn't exist, backend auto-creates user record with email-only, they become member immediately",
|
||||
"Member invite: if user doesn't exist, backend auto-creates user record with email as name, they become member immediately",
|
||||
"3 roles (owner/admin/member) sufficient for MVP, no custom permissions table",
|
||||
"Owner: full control. Admin: manage members + agents + settings. Member: CRUD issues + comments.",
|
||||
"All permission checks centralized in handler helpers, enforced at API level",
|
||||
"Workspace switch triggers full data refresh: disconnect WS, clear stores, reconnect with new workspace_id, refetch all"
|
||||
"Workspace switch clears issue/inbox/agent stores, then WSProvider reconnects (dependency on workspace) and useRealtimeSync refetches",
|
||||
"Agent visibility filtering deferred — all agents workspace-visible for MVP"
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"task": "Backend: Fix DeleteAgent workspace + role check",
|
||||
"done": false,
|
||||
"scope": "DeleteAgent calls loadAgentForUser (workspace membership check) before deletion. Also calls CancelAgentTasksByIssue for all agent's assigned issues. Requires owner or admin role."
|
||||
"done": true,
|
||||
"scope": "DeleteAgent calls loadAgentForUser (workspace membership) + requireWorkspaceRole(owner, admin) before deletion."
|
||||
},
|
||||
{
|
||||
"task": "Backend: Fix ListAgentTasks workspace check",
|
||||
"done": false,
|
||||
"scope": "ListAgentTasks verifies agent belongs to user's workspace via loadAgentForUser before returning tasks."
|
||||
"done": true,
|
||||
"scope": "ListAgentTasks calls loadAgentForUser to verify agent belongs to user's workspace before returning tasks."
|
||||
},
|
||||
{
|
||||
"task": "Backend: Member invite auto-creates user if not found",
|
||||
"done": false,
|
||||
"scope": "CreateMember handler: if GetUserByEmail returns not found, call CreateUser(email, '') to create stub user, then proceed to add as member. Return 201 with member data."
|
||||
"done": true,
|
||||
"scope": "CreateMember: if GetUserByEmail returns not found, calls CreateUser(email, email) to create stub user, then adds as member."
|
||||
},
|
||||
{
|
||||
"task": "Backend: Agent visibility filtering",
|
||||
"done": false,
|
||||
"scope": "ListAgents filters private agents: only visible to agent owner_id or workspace owner/admin. Other members only see workspace-visible agents."
|
||||
"scope": "Deferred: all agents are workspace-visible for MVP. Private agent filtering not needed yet."
|
||||
},
|
||||
{
|
||||
"task": "Frontend: Settings page use shadcn components consistently",
|
||||
"done": false,
|
||||
"scope": "Replace raw textarea with shadcn Textarea for context field. All inputs use shadcn Input. Form validation: workspace name required, show inline errors. All buttons use shadcn Button with loading state."
|
||||
"scope": "Deferred: UI polish."
|
||||
},
|
||||
{
|
||||
"task": "Frontend: Workspace switcher error handling and feedback",
|
||||
"done": false,
|
||||
"scope": "Create workspace shows error toast on failure (including slug collision). Workspace list sorted alphabetically. Current workspace highlighted with check icon."
|
||||
"scope": "Deferred: UI polish."
|
||||
},
|
||||
{
|
||||
"task": "Frontend: Workspace switch triggers full data refresh",
|
||||
"done": false,
|
||||
"scope": "switchWorkspace action: disconnects WS, clears issue/inbox/agent stores, sets new workspace_id on API client, reconnects WS with new workspace, refetches all data."
|
||||
"done": true,
|
||||
"scope": "switchWorkspace clears useIssueStore, useInboxStore, useAgentStore before hydrating. WSProvider reconnects automatically (depends on workspace). useRealtimeSync refetches on reconnect."
|
||||
},
|
||||
{
|
||||
"task": "Frontend: Member management UX improvements",
|
||||
"done": false,
|
||||
"scope": "Add member shows success/error toast. Email validation before submit. 'Already a member' error shown inline. Remove member confirmation uses AlertDialog. All operations show loading state on button."
|
||||
"scope": "Deferred: UI polish."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { create } from "zustand";
|
||||
import type { Workspace, MemberWithUser, Agent } from "@multica/types";
|
||||
import { useIssueStore, useInboxStore, useAgentStore } from "@multica/store";
|
||||
import { api } from "@/shared/api";
|
||||
|
||||
interface WorkspaceState {
|
||||
|
|
@ -76,6 +77,11 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
|
|||
const ws = workspaces.find((item) => item.id === workspaceId);
|
||||
if (!ws) return;
|
||||
|
||||
// Clear stale data from other stores before switching
|
||||
useIssueStore.getState().setIssues([]);
|
||||
useInboxStore.getState().setItems([]);
|
||||
useAgentStore.getState().setAgents([]);
|
||||
|
||||
await hydrateWorkspace(workspaces, ws.id);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -393,6 +393,11 @@ func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
wsID := uuidToString(agent.WorkspaceID)
|
||||
|
||||
// Require owner or admin role
|
||||
if _, ok := h.requireWorkspaceRole(w, r, wsID, "agent not found", "owner", "admin"); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Queries.DeleteAgent(r.Context(), parseUUID(id))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete agent")
|
||||
|
|
@ -406,6 +411,10 @@ func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (h *Handler) ListAgentTasks(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if _, ok := h.loadAgentForUser(w, r, id); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
tasks, err := h.Queries.ListAgentTasks(r.Context(), parseUUID(id))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to list agent tasks")
|
||||
|
|
|
|||
|
|
@ -338,11 +338,19 @@ func (h *Handler) CreateMember(w http.ResponseWriter, r *http.Request) {
|
|||
user, err := h.Queries.GetUserByEmail(r.Context(), email)
|
||||
if err != nil {
|
||||
if isNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "user not found")
|
||||
// Auto-create user with email so they can be invited before signing up
|
||||
user, err = h.Queries.CreateUser(r.Context(), db.CreateUserParams{
|
||||
Name: email,
|
||||
Email: email,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create user")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
writeError(w, http.StatusInternalServerError, "failed to load user")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "failed to load user")
|
||||
return
|
||||
}
|
||||
|
||||
member, err := h.Queries.CreateMember(r.Context(), db.CreateMemberParams{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue