@taskforcehq/taskforce 0.3.301 → 0.3.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +98 -0
- package/dist/sync/contentSyncState.js +1004 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -3
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* POST /api/taskforce/auth/invite/join
|
|
21
21
|
* POST /api/taskforce/auth/logout
|
|
22
22
|
* GET /api/taskforce/account/access-status
|
|
23
|
+
* GET /api/taskforce/account/profile-summary
|
|
23
24
|
* GET /api/taskforce/account/entitlements
|
|
25
|
+
* GET /api/taskforce/account/team-management-access
|
|
24
26
|
* GET /api/taskforce/account/plan-preview
|
|
25
27
|
* GET /api/taskforce/auth/workspace-members
|
|
26
28
|
* GET /api/taskforce/workspace/assignee-options
|
|
@@ -33,15 +35,160 @@
|
|
|
33
35
|
* DELETE /api/taskforce/admin/users/:id
|
|
34
36
|
* GET /api/taskforce/admin/workspace-audit-logs
|
|
35
37
|
*/
|
|
38
|
+
import * as fs from 'fs';
|
|
39
|
+
import * as path from 'path';
|
|
40
|
+
import { randomUUID } from 'crypto';
|
|
36
41
|
import { parseJsonBody } from '../routes.js';
|
|
37
42
|
import { hasValidSession, createClearSessionCookie, createSessionTokenCookie, resolveRequestAccess, resolveSessionCookieAttributesForRequest, } from '../auth.js';
|
|
38
43
|
import { resolveEffectiveEmailConfig, sendEmail } from '../email.js';
|
|
39
44
|
import { resolveDeploymentConfig } from '../../config/deployment.js';
|
|
40
45
|
import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
|
|
41
|
-
import { resolveErrorStatusCode } from './shared.js';
|
|
46
|
+
import { requestHeader, resolveErrorStatusCode } from './shared.js';
|
|
47
|
+
import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
|
|
42
48
|
// === SECTION: Registration ===
|
|
43
49
|
export function registerAuthRoutes(deps) {
|
|
44
|
-
const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, shouldExposeDevAuthTokens, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, } = deps;
|
|
50
|
+
const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, shouldExposeDevAuthTokens, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
|
|
51
|
+
const normalizeTeamWorkspaceRole = (value) => {
|
|
52
|
+
const role = String(value || '').trim().toLowerCase();
|
|
53
|
+
if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
|
|
54
|
+
return role;
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
const resolveBooleanSetting = (value, fallback) => {
|
|
58
|
+
if (typeof value === 'boolean')
|
|
59
|
+
return value;
|
|
60
|
+
if (typeof value === 'number')
|
|
61
|
+
return value !== 0;
|
|
62
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
63
|
+
if (!normalized)
|
|
64
|
+
return fallback;
|
|
65
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized))
|
|
66
|
+
return true;
|
|
67
|
+
if (['0', 'false', 'no', 'off'].includes(normalized))
|
|
68
|
+
return false;
|
|
69
|
+
return fallback;
|
|
70
|
+
};
|
|
71
|
+
const resolveTeamPlanModeFromEntitlements = (payload) => {
|
|
72
|
+
const state = String(payload?.state || '').trim().toLowerCase();
|
|
73
|
+
const teamAllowed = payload?.features?.['collaboration.team_management']?.allowed === true;
|
|
74
|
+
const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
|
|
75
|
+
return teamAllowed && stateAllows ? 'team' : 'personal';
|
|
76
|
+
};
|
|
77
|
+
const resolveBillingConflictCodeForUser = (userIdRaw, gateRaw) => {
|
|
78
|
+
const userId = String(userIdRaw || '').trim();
|
|
79
|
+
const gate = String(gateRaw || '').trim().toLowerCase();
|
|
80
|
+
if (!userId || !gate || gate === 'ok')
|
|
81
|
+
return null;
|
|
82
|
+
const db = core.getDatabaseAdapter();
|
|
83
|
+
const tenantId = core.getTenantId();
|
|
84
|
+
try {
|
|
85
|
+
const row = db.prepare(`
|
|
86
|
+
SELECT session_id, status, expires_at
|
|
87
|
+
FROM billing_checkout_attempts
|
|
88
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
89
|
+
ORDER BY created_at DESC, session_id DESC
|
|
90
|
+
LIMIT 1
|
|
91
|
+
`).get(tenantId, userId);
|
|
92
|
+
const sessionId = String(row?.session_id || '').trim();
|
|
93
|
+
const status = String(row?.status || '').trim().toLowerCase();
|
|
94
|
+
const expiresAt = String(row?.expires_at || '').trim();
|
|
95
|
+
if (!sessionId)
|
|
96
|
+
return null;
|
|
97
|
+
if (status === 'open' && expiresAt) {
|
|
98
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
99
|
+
if (Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now()) {
|
|
100
|
+
db.prepare(`
|
|
101
|
+
UPDATE billing_checkout_attempts
|
|
102
|
+
SET status = 'expired', updated_at = ?
|
|
103
|
+
WHERE tenant_id = ? AND session_id = ?
|
|
104
|
+
`).run(new Date().toISOString(), tenantId, sessionId);
|
|
105
|
+
return 'CHECKOUT_SESSION_EXPIRED';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (status === 'expired')
|
|
109
|
+
return 'CHECKOUT_SESSION_EXPIRED';
|
|
110
|
+
if (status === 'superseded')
|
|
111
|
+
return 'CHECKOUT_SESSION_SUPERSEDED';
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
};
|
|
118
|
+
const buildAccountProfileSummary = (req) => {
|
|
119
|
+
const userId = requestUserId(req);
|
|
120
|
+
const workspaceId = requestWorkspaceId(req);
|
|
121
|
+
const access = core.getAccountAccessStatus(userId);
|
|
122
|
+
const resolvedAccess = core.resolveUserAccess(userId, workspaceId, 'member');
|
|
123
|
+
const hintedRole = normalizeTeamWorkspaceRole(requestHeader(req, 'x-taskforce-workspace-role', ''));
|
|
124
|
+
const workspaceRole = normalizeTeamWorkspaceRole(resolvedAccess?.role || hintedRole || null);
|
|
125
|
+
const canManageWorkspace = workspaceRole === 'owner' || workspaceRole === 'admin';
|
|
126
|
+
const db = core.getDatabaseAdapter();
|
|
127
|
+
const tenantId = core.getTenantId();
|
|
128
|
+
let billingRow;
|
|
129
|
+
try {
|
|
130
|
+
billingRow = db.prepare(`
|
|
131
|
+
SELECT stripe_status, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval,
|
|
132
|
+
trial_end, current_period_end, effective_until, entitlement_state, tier
|
|
133
|
+
FROM user_billing
|
|
134
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
135
|
+
LIMIT 1
|
|
136
|
+
`).get(tenantId, userId);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
billingRow = undefined;
|
|
140
|
+
}
|
|
141
|
+
const planId = String(access.entitlement?.planId || billingRow?.tier || '').trim() || null;
|
|
142
|
+
const planVersionId = String(access.entitlement?.planVersionId || '').trim() || null;
|
|
143
|
+
let planRow;
|
|
144
|
+
if (planId) {
|
|
145
|
+
try {
|
|
146
|
+
planRow = db.prepare(`
|
|
147
|
+
SELECT display_name
|
|
148
|
+
FROM plan_catalog
|
|
149
|
+
WHERE tenant_id = ? AND plan_id = ?
|
|
150
|
+
LIMIT 1
|
|
151
|
+
`).get(tenantId, planId);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
planRow = undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
let teamPlanMode = 'unknown';
|
|
158
|
+
try {
|
|
159
|
+
const entitlements = core.getAccountEntitlementsPayload({ userId, workspaceId });
|
|
160
|
+
teamPlanMode = resolveTeamPlanModeFromEntitlements(entitlements);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error?.code === 'ENTITLEMENT_NOT_FOUND' && (access.gate === 'plan_selection_required' || access.gate === 'checkout_pending')) {
|
|
164
|
+
teamPlanMode = 'personal';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const billingConflictCode = resolveBillingConflictCodeForUser(userId, access.gate);
|
|
168
|
+
return {
|
|
169
|
+
stripeStatus: billingRow?.stripe_status || null,
|
|
170
|
+
stripeCustomerId: billingRow?.stripe_customer_id || null,
|
|
171
|
+
stripeSubscriptionId: billingRow?.stripe_subscription_id || null,
|
|
172
|
+
stripePriceId: billingRow?.stripe_price_id || null,
|
|
173
|
+
billingInterval: billingRow?.billing_interval || null,
|
|
174
|
+
trialEnd: billingRow?.trial_end || null,
|
|
175
|
+
currentPeriodEnd: billingRow?.current_period_end || null,
|
|
176
|
+
effectiveUntil: billingRow?.effective_until || null,
|
|
177
|
+
entitlementState: access.entitlement?.state || billingRow?.entitlement_state || null,
|
|
178
|
+
planVersionId,
|
|
179
|
+
planId,
|
|
180
|
+
planName: planRow?.display_name ? String(planRow.display_name) : planId,
|
|
181
|
+
gate: access.gate,
|
|
182
|
+
message: access.message,
|
|
183
|
+
billingConflictCode,
|
|
184
|
+
planSelectionRequired: access.planSelectionRequired,
|
|
185
|
+
workspaceId,
|
|
186
|
+
workspaceRole,
|
|
187
|
+
canManageWorkspace,
|
|
188
|
+
teamPlanMode,
|
|
189
|
+
teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team'
|
|
190
|
+
};
|
|
191
|
+
};
|
|
45
192
|
// === SECTION: Email delivery helpers ===
|
|
46
193
|
const getEmailConfig = () => {
|
|
47
194
|
if (context.authConfig?.runtimeMode !== 'cloud') {
|
|
@@ -55,30 +202,94 @@ export function registerAuthRoutes(deps) {
|
|
|
55
202
|
const authSettings = globalSettings.auth || {};
|
|
56
203
|
const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
|
|
57
204
|
const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
|
|
58
|
-
const
|
|
205
|
+
const onboardingPolicy = core.getOnboardingPolicy();
|
|
59
206
|
return {
|
|
60
|
-
allowSelfRegistration: authSettings.allowSelfRegistration
|
|
61
|
-
requireVerifiedEmail: authSettings.requireVerifiedEmail
|
|
62
|
-
defaultNewUserBetaAccess: authSettings.defaultNewUserBetaAccess
|
|
63
|
-
authRateLimitEnabled: authSettings.authRateLimitEnabled
|
|
207
|
+
allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
|
|
208
|
+
requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
|
|
209
|
+
defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
|
|
210
|
+
authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
|
|
64
211
|
authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
|
|
65
212
|
? Math.floor(authRateLimitMaxRequestsRaw)
|
|
66
213
|
: 10,
|
|
67
214
|
authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
|
|
68
215
|
? Math.floor(authRateLimitWindowMsRaw)
|
|
69
216
|
: 15 * 60_000,
|
|
70
|
-
|
|
71
|
-
|
|
217
|
+
onboardingPolicyMode: onboardingPolicy.mode,
|
|
218
|
+
onboardingPlanVersionId: onboardingPolicy.planVersionId
|
|
72
219
|
};
|
|
73
220
|
};
|
|
74
221
|
const getSiteBehavior = () => {
|
|
75
222
|
const globalSettings = core.getGlobalSettings();
|
|
76
223
|
const siteSettings = globalSettings.site || {};
|
|
77
224
|
return {
|
|
78
|
-
showRunTaskforceLocally: siteSettings.showRunTaskforceLocally
|
|
79
|
-
pricingPageEnabled: siteSettings.pricingPageEnabled
|
|
225
|
+
showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
|
|
226
|
+
pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
|
|
80
227
|
};
|
|
81
228
|
};
|
|
229
|
+
const buildPasswordSessionCookieForUser = (req, authConfig, userIdRaw, options) => {
|
|
230
|
+
const userId = String(userIdRaw || '').trim();
|
|
231
|
+
const fallbackRole = String(options?.fallbackRole || authConfig.defaultRole || 'member').trim() || 'member';
|
|
232
|
+
const resolvedWorkspaceId = core.resolvePreferredUserWorkspaceId(userId, {
|
|
233
|
+
preferredWorkspaceId: options?.preferredWorkspaceId || null,
|
|
234
|
+
persistedWorkspaceId: options?.persistedWorkspaceId || null
|
|
235
|
+
});
|
|
236
|
+
const access = resolvedWorkspaceId
|
|
237
|
+
? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
|
|
238
|
+
: null;
|
|
239
|
+
return createSessionTokenCookie(`pwd:${userId}`, authConfig, {
|
|
240
|
+
role: (access?.role || fallbackRole),
|
|
241
|
+
workspaceId: access?.workspaceId || '',
|
|
242
|
+
userId,
|
|
243
|
+
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
const AVATAR_DRAFT_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
247
|
+
const MAX_AVATAR_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
248
|
+
const AVATAR_MIME_EXTENSION = {
|
|
249
|
+
'image/png': '.png',
|
|
250
|
+
'image/jpeg': '.jpg',
|
|
251
|
+
'image/webp': '.webp',
|
|
252
|
+
'image/gif': '.gif'
|
|
253
|
+
};
|
|
254
|
+
const buildUserAvatarStoragePath = (userIdRaw, draftIdRaw, originalNameRaw, mimeTypeRaw) => {
|
|
255
|
+
const userId = String(userIdRaw || '').trim().replace(/[^a-zA-Z0-9._-]/g, '-') || 'user';
|
|
256
|
+
const draftId = String(draftIdRaw || '').trim().replace(/[^a-zA-Z0-9._-]/g, '-') || randomUUID().replace(/-/g, '');
|
|
257
|
+
const originalName = String(originalNameRaw || '').trim();
|
|
258
|
+
const mimeType = String(mimeTypeRaw || '').trim().toLowerCase();
|
|
259
|
+
const originalExt = originalName ? path.extname(originalName).toLowerCase() : '';
|
|
260
|
+
const ext = originalExt || AVATAR_MIME_EXTENSION[mimeType] || '.bin';
|
|
261
|
+
const stem = (originalName ? path.basename(originalName, originalExt) : 'avatar')
|
|
262
|
+
.replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
263
|
+
.replace(/-+/g, '-')
|
|
264
|
+
.replace(/^-+|-+$/g, '')
|
|
265
|
+
.slice(0, 64) || 'avatar';
|
|
266
|
+
return `users/${userId}/profile/avatar/${draftId}-${stem}${ext}`;
|
|
267
|
+
};
|
|
268
|
+
const deleteStoredAvatarObject = async (relativePathRaw) => {
|
|
269
|
+
const relativePath = normalizeTaskDataRelativePath(String(relativePathRaw || '').trim());
|
|
270
|
+
if (!relativePath)
|
|
271
|
+
return;
|
|
272
|
+
const objectStorageClient = getObjectStorageClient();
|
|
273
|
+
const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
|
|
274
|
+
if (objectStorageClient && effectiveObjectStorage.provider === 'r2') {
|
|
275
|
+
await objectStorageClient.deleteObject(relativePath).catch(() => { });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const { basePath } = core.getPaths();
|
|
279
|
+
const resolvedPath = path.resolve(basePath, relativePath);
|
|
280
|
+
if (!resolvedPath.startsWith(path.resolve(basePath)))
|
|
281
|
+
return;
|
|
282
|
+
if (fs.existsSync(resolvedPath)) {
|
|
283
|
+
fs.rmSync(resolvedPath, { force: true });
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const cleanupStaleAvatarDrafts = async (userId) => {
|
|
287
|
+
const cutoff = new Date(Date.now() - AVATAR_DRAFT_RETENTION_MS).toISOString();
|
|
288
|
+
const staleDrafts = core.cleanupStaleUserAvatarUploadDrafts(userId, cutoff);
|
|
289
|
+
for (const draft of staleDrafts) {
|
|
290
|
+
await deleteStoredAvatarObject(draft.storageKey);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
82
293
|
const logAuthLink = (kind, email, token, req) => {
|
|
83
294
|
const base = resolvePublicAuthBaseUrl(req);
|
|
84
295
|
const urlPath = kind === 'verify-email'
|
|
@@ -187,8 +398,10 @@ export function registerAuthRoutes(deps) {
|
|
|
187
398
|
let userId = access?.userId || 'anonymous';
|
|
188
399
|
let email = null;
|
|
189
400
|
let displayName = null;
|
|
401
|
+
let avatarUrl = null;
|
|
190
402
|
let betaAccess = true;
|
|
191
403
|
if (authenticated && access?.userId) {
|
|
404
|
+
await cleanupStaleAvatarDrafts(access.userId);
|
|
192
405
|
betaAccess = core.getUserBetaAccess(access.userId);
|
|
193
406
|
const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
|
|
194
407
|
if (resolved) {
|
|
@@ -204,6 +417,7 @@ export function registerAuthRoutes(deps) {
|
|
|
204
417
|
const identity = core.resolveUserIdentity(userId);
|
|
205
418
|
email = identity?.email || null;
|
|
206
419
|
displayName = identity?.displayName || null;
|
|
420
|
+
avatarUrl = identity?.avatarUrl || null;
|
|
207
421
|
}
|
|
208
422
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
209
423
|
res.end(JSON.stringify({
|
|
@@ -213,6 +427,7 @@ export function registerAuthRoutes(deps) {
|
|
|
213
427
|
role,
|
|
214
428
|
email,
|
|
215
429
|
displayName,
|
|
430
|
+
avatarUrl,
|
|
216
431
|
betaAccess
|
|
217
432
|
}));
|
|
218
433
|
});
|
|
@@ -229,28 +444,67 @@ export function registerAuthRoutes(deps) {
|
|
|
229
444
|
addRoute('POST', '/api/taskforce/auth/profile', async (req, res) => {
|
|
230
445
|
if (!requireCloudRuntime(res, 'Authentication'))
|
|
231
446
|
return;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
res.
|
|
447
|
+
const authConfig = context.authConfig;
|
|
448
|
+
if (!authConfig) {
|
|
449
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
450
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
235
451
|
return;
|
|
236
452
|
}
|
|
237
|
-
const
|
|
238
|
-
|
|
453
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
454
|
+
const userId = String(access.userId || '').trim();
|
|
455
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
239
456
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
240
457
|
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
241
458
|
return;
|
|
242
459
|
}
|
|
243
460
|
const body = await parseJsonBody(req);
|
|
244
461
|
try {
|
|
245
|
-
|
|
462
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
463
|
+
const avatarDraftId = typeof body?.avatarDraftId === 'string' ? body.avatarDraftId.trim() : '';
|
|
464
|
+
const clearAvatar = body?.clearAvatar === true;
|
|
465
|
+
let avatarUrl;
|
|
466
|
+
if (avatarDraftId) {
|
|
467
|
+
const draft = core.getUserAvatarUploadDraft(userId, avatarDraftId);
|
|
468
|
+
if (!draft || draft.discardedAt || !draft.finalizedAt) {
|
|
469
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
470
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
avatarUrl = `/api/taskforce/context/${encodeURIComponent(draft.storageKey)}`;
|
|
474
|
+
}
|
|
475
|
+
else if (clearAvatar) {
|
|
476
|
+
avatarUrl = null;
|
|
477
|
+
}
|
|
478
|
+
const previousIdentity = core.resolveUserIdentity(userId);
|
|
479
|
+
const updated = core.updateUserProfile(userId, {
|
|
480
|
+
displayName: String(body?.displayName || ''),
|
|
481
|
+
...((avatarDraftId || clearAvatar) ? { avatarUrl: avatarUrl || null } : {})
|
|
482
|
+
});
|
|
246
483
|
if (!updated) {
|
|
247
484
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
248
485
|
res.end(JSON.stringify({ success: false, error: 'User not found.', code: 'USER_NOT_FOUND' }));
|
|
249
486
|
return;
|
|
250
487
|
}
|
|
488
|
+
if (avatarDraftId) {
|
|
489
|
+
core.markUserAvatarUploadDraftConsumed(userId, avatarDraftId);
|
|
490
|
+
}
|
|
491
|
+
if (avatarDraftId || clearAvatar) {
|
|
492
|
+
const previousAvatarUrl = String(previousIdentity?.avatarUrl || '').trim();
|
|
493
|
+
const nextAvatarUrl = String(updated.avatarUrl || '').trim();
|
|
494
|
+
const currentDraft = avatarDraftId ? core.getUserAvatarUploadDraft(userId, avatarDraftId) : null;
|
|
495
|
+
const currentStorageKey = String(currentDraft?.storageKey || '').trim();
|
|
496
|
+
if (previousAvatarUrl.startsWith('/api/taskforce/context/')
|
|
497
|
+
&& previousAvatarUrl !== nextAvatarUrl) {
|
|
498
|
+
const previousStorageKey = normalizeTaskDataRelativePath(decodeURIComponent(previousAvatarUrl.slice('/api/taskforce/context/'.length)));
|
|
499
|
+
if (previousStorageKey && previousStorageKey !== currentStorageKey) {
|
|
500
|
+
await deleteStoredAvatarObject(previousStorageKey);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
251
504
|
auditAuthAction(req, 'auth-profile-updated', {
|
|
252
505
|
userId,
|
|
253
|
-
displayName: updated.displayName || null
|
|
506
|
+
displayName: updated.displayName || null,
|
|
507
|
+
avatarUrl: updated.avatarUrl || null
|
|
254
508
|
});
|
|
255
509
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
256
510
|
res.end(JSON.stringify({
|
|
@@ -258,7 +512,8 @@ export function registerAuthRoutes(deps) {
|
|
|
258
512
|
profile: {
|
|
259
513
|
userId: updated.userId,
|
|
260
514
|
email: updated.email,
|
|
261
|
-
displayName: updated.displayName || null
|
|
515
|
+
displayName: updated.displayName || null,
|
|
516
|
+
avatarUrl: updated.avatarUrl || null
|
|
262
517
|
}
|
|
263
518
|
}));
|
|
264
519
|
}
|
|
@@ -268,6 +523,161 @@ export function registerAuthRoutes(deps) {
|
|
|
268
523
|
res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to update profile.'), code: error?.code }));
|
|
269
524
|
}
|
|
270
525
|
});
|
|
526
|
+
addRoute('POST', '/api/taskforce/auth/profile/avatar/init', async (req, res) => {
|
|
527
|
+
if (!requireCloudRuntime(res, 'Authentication'))
|
|
528
|
+
return;
|
|
529
|
+
const authConfig = context.authConfig;
|
|
530
|
+
if (!authConfig) {
|
|
531
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
532
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
536
|
+
const userId = String(access.userId || '').trim();
|
|
537
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
538
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
539
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const objectStorageClient = getObjectStorageClient();
|
|
543
|
+
const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
|
|
544
|
+
if (!objectStorageClient || effectiveObjectStorage.provider !== 'r2' || !effectiveObjectStorage.r2) {
|
|
545
|
+
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
546
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar uploads require object storage.' }));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const body = await parseJsonBody(req);
|
|
550
|
+
const originalName = String(body?.originalName || '').trim();
|
|
551
|
+
const mimeType = String(body?.mimeType || '').trim().toLowerCase();
|
|
552
|
+
const size = Number(body?.size || 0);
|
|
553
|
+
if (!originalName || !mimeType.startsWith('image/') || !(mimeType in AVATAR_MIME_EXTENSION)) {
|
|
554
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
555
|
+
res.end(JSON.stringify({ success: false, error: 'A supported image file is required.', code: 'AVATAR_FILE_INVALID' }));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!Number.isFinite(size) || size <= 0 || size > MAX_AVATAR_UPLOAD_BYTES) {
|
|
559
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
560
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar must be 5 MB or smaller.', code: 'AVATAR_SIZE_INVALID' }));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
564
|
+
const draftId = `avatar-draft-${randomUUID().replace(/-/g, '')}`;
|
|
565
|
+
const relativePath = buildUserAvatarStoragePath(userId, draftId, originalName, mimeType);
|
|
566
|
+
core.createUserAvatarUploadDraft({
|
|
567
|
+
userId,
|
|
568
|
+
draftId,
|
|
569
|
+
storageKey: relativePath,
|
|
570
|
+
mimeType,
|
|
571
|
+
originalFilename: originalName,
|
|
572
|
+
sizeBytes: size,
|
|
573
|
+
storageProvider: 'r2'
|
|
574
|
+
});
|
|
575
|
+
const uploadUrl = objectStorageClient.createSignedPutUrl(relativePath, effectiveObjectStorage.r2.signedUploadTtlSeconds, mimeType);
|
|
576
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
577
|
+
res.end(JSON.stringify({
|
|
578
|
+
success: true,
|
|
579
|
+
mode: 'direct',
|
|
580
|
+
draftId,
|
|
581
|
+
uploadUrl,
|
|
582
|
+
method: 'PUT',
|
|
583
|
+
headers: { 'Content-Type': mimeType },
|
|
584
|
+
relativePath,
|
|
585
|
+
path: `/api/taskforce/context/${encodeURIComponent(relativePath)}`
|
|
586
|
+
}));
|
|
587
|
+
});
|
|
588
|
+
addRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', async (req, res) => {
|
|
589
|
+
if (!requireCloudRuntime(res, 'Authentication'))
|
|
590
|
+
return;
|
|
591
|
+
const authConfig = context.authConfig;
|
|
592
|
+
if (!authConfig) {
|
|
593
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
594
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
598
|
+
const userId = String(access.userId || '').trim();
|
|
599
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
600
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
601
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const body = await parseJsonBody(req);
|
|
605
|
+
const draftId = String(body?.draftId || '').trim();
|
|
606
|
+
const relativePath = normalizeTaskDataRelativePath(String(body?.relativePath || '').trim());
|
|
607
|
+
if (!draftId || !relativePath) {
|
|
608
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
609
|
+
res.end(JSON.stringify({ success: false, error: 'draftId and relativePath are required.' }));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
613
|
+
const draft = core.getUserAvatarUploadDraft(userId, draftId);
|
|
614
|
+
if (!draft || draft.discardedAt || draft.storageKey !== relativePath) {
|
|
615
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
616
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const objectStorageClient = getObjectStorageClient();
|
|
620
|
+
if (objectStorageClient) {
|
|
621
|
+
const uploaded = await objectStorageClient.getObject(relativePath);
|
|
622
|
+
if (!uploaded) {
|
|
623
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
624
|
+
res.end(JSON.stringify({ success: false, error: 'Uploaded avatar not found.', code: 'AVATAR_UPLOAD_MISSING' }));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
const { basePath } = core.getPaths();
|
|
630
|
+
const resolvedPath = path.resolve(basePath, relativePath);
|
|
631
|
+
if (!resolvedPath.startsWith(path.resolve(basePath)) || !fs.existsSync(resolvedPath)) {
|
|
632
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
633
|
+
res.end(JSON.stringify({ success: false, error: 'Uploaded avatar not found.', code: 'AVATAR_UPLOAD_MISSING' }));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const finalized = core.markUserAvatarUploadDraftFinalized(userId, draftId);
|
|
638
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
639
|
+
res.end(JSON.stringify({
|
|
640
|
+
success: true,
|
|
641
|
+
draftId,
|
|
642
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(relativePath)}`,
|
|
643
|
+
profile: finalized
|
|
644
|
+
}));
|
|
645
|
+
});
|
|
646
|
+
addRoute('POST', '/api/taskforce/auth/profile/avatar/discard', async (req, res) => {
|
|
647
|
+
if (!requireCloudRuntime(res, 'Authentication'))
|
|
648
|
+
return;
|
|
649
|
+
const authConfig = context.authConfig;
|
|
650
|
+
if (!authConfig) {
|
|
651
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
652
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
656
|
+
const userId = String(access.userId || '').trim();
|
|
657
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
658
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
659
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const body = await parseJsonBody(req);
|
|
663
|
+
const draftId = String(body?.draftId || '').trim();
|
|
664
|
+
if (!draftId) {
|
|
665
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
666
|
+
res.end(JSON.stringify({ success: false, error: 'draftId is required.' }));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
670
|
+
const draft = core.getUserAvatarUploadDraft(userId, draftId);
|
|
671
|
+
if (!draft) {
|
|
672
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
673
|
+
res.end(JSON.stringify({ success: true, discarded: false }));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
core.discardUserAvatarUploadDraft(userId, draftId);
|
|
677
|
+
await deleteStoredAvatarObject(draft.storageKey);
|
|
678
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
679
|
+
res.end(JSON.stringify({ success: true, discarded: true }));
|
|
680
|
+
});
|
|
271
681
|
// === SECTION: Login and register ===
|
|
272
682
|
// POST /api/taskforce/auth/login - Exchange email/password for secure session cookie
|
|
273
683
|
addRoute('POST', '/api/taskforce/auth/login', async (req, res) => {
|
|
@@ -366,7 +776,7 @@ export function registerAuthRoutes(deps) {
|
|
|
366
776
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
367
777
|
res.end(JSON.stringify({ success: false, error: 'Email and password are required.' }));
|
|
368
778
|
});
|
|
369
|
-
// POST /api/taskforce/auth/register - Create account and
|
|
779
|
+
// POST /api/taskforce/auth/register - Create account and optionally start a session
|
|
370
780
|
addRoute('POST', '/api/taskforce/auth/register', async (req, res) => {
|
|
371
781
|
if (!requireCloudRuntime(res, 'Authentication'))
|
|
372
782
|
return;
|
|
@@ -416,13 +826,6 @@ export function registerAuthRoutes(deps) {
|
|
|
416
826
|
role,
|
|
417
827
|
betaAccess: authBehavior.defaultNewUserBetaAccess
|
|
418
828
|
});
|
|
419
|
-
const sessionWorkspaceId = created.workspaceId || '';
|
|
420
|
-
const cookie = createSessionTokenCookie(`pwd:${created.userId}`, authConfig, {
|
|
421
|
-
role: created.role,
|
|
422
|
-
workspaceId: sessionWorkspaceId,
|
|
423
|
-
userId: created.userId,
|
|
424
|
-
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
425
|
-
});
|
|
426
829
|
let emailSent = true;
|
|
427
830
|
let emailError;
|
|
428
831
|
let verificationToken;
|
|
@@ -442,7 +845,13 @@ export function registerAuthRoutes(deps) {
|
|
|
442
845
|
role: created.role,
|
|
443
846
|
emailSent
|
|
444
847
|
});
|
|
445
|
-
|
|
848
|
+
if (!authBehavior.requireVerifiedEmail) {
|
|
849
|
+
res.setHeader('Set-Cookie', buildPasswordSessionCookieForUser(req, authConfig, created.userId, {
|
|
850
|
+
preferredWorkspaceId: created.workspaceId || null,
|
|
851
|
+
persistedWorkspaceId: created.workspaceId || null,
|
|
852
|
+
fallbackRole: created.role
|
|
853
|
+
}));
|
|
854
|
+
}
|
|
446
855
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
447
856
|
res.end(JSON.stringify({
|
|
448
857
|
success: true,
|
|
@@ -450,6 +859,8 @@ export function registerAuthRoutes(deps) {
|
|
|
450
859
|
role: created.role,
|
|
451
860
|
workspaceSetupRequired: !created.workspaceId,
|
|
452
861
|
planSelectionRequired: created.planSelectionRequired === true,
|
|
862
|
+
checkoutPending: created.checkoutPending === true,
|
|
863
|
+
commercialState: created.commercialState,
|
|
453
864
|
verificationRequired: authBehavior.requireVerifiedEmail,
|
|
454
865
|
emailSent,
|
|
455
866
|
emailError,
|
|
@@ -517,6 +928,12 @@ export function registerAuthRoutes(deps) {
|
|
|
517
928
|
addRoute('POST', '/api/taskforce/auth/verify-email/confirm', async (req, res) => {
|
|
518
929
|
if (!requireCloudRuntime(res, 'Authentication'))
|
|
519
930
|
return;
|
|
931
|
+
const authConfig = context.authConfig;
|
|
932
|
+
if (!authConfig?.enabled) {
|
|
933
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
934
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication is not configured.' }));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
520
937
|
const body = await parseJsonBody(req);
|
|
521
938
|
const token = String(body?.token || '').trim();
|
|
522
939
|
if (!token) {
|
|
@@ -524,6 +941,15 @@ export function registerAuthRoutes(deps) {
|
|
|
524
941
|
res.end(JSON.stringify({ success: false, error: 'Token is required.' }));
|
|
525
942
|
return;
|
|
526
943
|
}
|
|
944
|
+
const inspection = core.inspectOneTimeAuthToken(token, 'email_verification');
|
|
945
|
+
if (!inspection.userId) {
|
|
946
|
+
auditAuthAction(req, 'auth-verify-confirm-failed', {
|
|
947
|
+
tokenProvided: true
|
|
948
|
+
});
|
|
949
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
950
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid or expired token.' }));
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
527
953
|
const verified = core.verifyEmailWithToken(token);
|
|
528
954
|
if (!verified) {
|
|
529
955
|
auditAuthAction(req, 'auth-verify-confirm-failed', {
|
|
@@ -536,6 +962,11 @@ export function registerAuthRoutes(deps) {
|
|
|
536
962
|
auditAuthAction(req, 'auth-verify-confirm-success', {
|
|
537
963
|
tokenProvided: true
|
|
538
964
|
});
|
|
965
|
+
const cookie = buildPasswordSessionCookieForUser(req, authConfig, inspection.userId, {
|
|
966
|
+
preferredWorkspaceId: inspection.workspaceId || null,
|
|
967
|
+
persistedWorkspaceId: inspection.workspaceId || null
|
|
968
|
+
});
|
|
969
|
+
res.setHeader('Set-Cookie', cookie);
|
|
539
970
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
540
971
|
res.end(JSON.stringify({ success: true }));
|
|
541
972
|
});
|
|
@@ -821,6 +1252,24 @@ export function registerAuthRoutes(deps) {
|
|
|
821
1252
|
res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve account access status'), code: error?.code, details: error?.details }));
|
|
822
1253
|
}
|
|
823
1254
|
});
|
|
1255
|
+
// GET /api/taskforce/account/profile-summary - Fast authoritative summary for the visible account surface.
|
|
1256
|
+
addRoute('GET', '/api/taskforce/account/profile-summary', async (req, res) => {
|
|
1257
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
1258
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1259
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
try {
|
|
1263
|
+
const payload = buildAccountProfileSummary(req);
|
|
1264
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1265
|
+
res.end(JSON.stringify(payload));
|
|
1266
|
+
}
|
|
1267
|
+
catch (error) {
|
|
1268
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1269
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1270
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve account profile summary'), code: error?.code, details: error?.details }));
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
824
1273
|
// GET /api/taskforce/account/entitlements - Resolve effective plan/features for the active workspace.
|
|
825
1274
|
addRoute('GET', '/api/taskforce/account/entitlements', async (req, res) => {
|
|
826
1275
|
if (!ensureAuthenticatedUser(req)) {
|
|
@@ -838,11 +1287,11 @@ export function registerAuthRoutes(deps) {
|
|
|
838
1287
|
catch (error) {
|
|
839
1288
|
if (error?.code === 'ENTITLEMENT_NOT_FOUND') {
|
|
840
1289
|
const access = core.getAccountAccessStatus(requestUserId(req));
|
|
841
|
-
if (access.gate === 'plan_selection_required') {
|
|
1290
|
+
if (access.gate === 'plan_selection_required' || access.gate === 'checkout_pending') {
|
|
842
1291
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
843
1292
|
res.end(JSON.stringify({
|
|
844
1293
|
error: access.message,
|
|
845
|
-
code: 'PLAN_SELECTION_REQUIRED',
|
|
1294
|
+
code: access.gate === 'checkout_pending' ? 'CHECKOUT_PENDING' : 'PLAN_SELECTION_REQUIRED',
|
|
846
1295
|
details: {
|
|
847
1296
|
gate: access.gate,
|
|
848
1297
|
planSelectionRequired: access.planSelectionRequired,
|
|
@@ -857,6 +1306,32 @@ export function registerAuthRoutes(deps) {
|
|
|
857
1306
|
res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve entitlements'), code: error?.code, details: error?.details }));
|
|
858
1307
|
}
|
|
859
1308
|
});
|
|
1309
|
+
// GET /api/taskforce/account/team-management-access - Resolve authoritative team-management access for the active workspace.
|
|
1310
|
+
addRoute('GET', '/api/taskforce/account/team-management-access', async (req, res) => {
|
|
1311
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
1312
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1313
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
try {
|
|
1317
|
+
const payload = buildAccountProfileSummary(req);
|
|
1318
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1319
|
+
res.end(JSON.stringify({
|
|
1320
|
+
workspaceId: payload.workspaceId,
|
|
1321
|
+
role: payload.workspaceRole,
|
|
1322
|
+
canManageWorkspace: payload.canManageWorkspace,
|
|
1323
|
+
teamPlanMode: payload.teamPlanMode,
|
|
1324
|
+
allowed: payload.teamManagementAllowed,
|
|
1325
|
+
gate: payload.gate,
|
|
1326
|
+
planSelectionRequired: payload.planSelectionRequired
|
|
1327
|
+
}));
|
|
1328
|
+
}
|
|
1329
|
+
catch (error) {
|
|
1330
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1331
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1332
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve team management access'), code: error?.code, details: error?.details }));
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
860
1335
|
// GET /api/taskforce/account/plan-preview - Read-only payload for upgrade/support CTAs.
|
|
861
1336
|
addRoute('GET', '/api/taskforce/account/plan-preview', async (req, res) => {
|
|
862
1337
|
if (!ensureCloudPlanData(req, res))
|