@taskforcehq/taskforce 0.3.302 → 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 +11 -0
- package/dist/TaskforceCore.js +59 -9
- package/dist/TaskforceCore.test.js +717 -14
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +110 -51
- 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 +106 -151
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- 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/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/SystemAdmin.test.js +186 -96
- package/dist/core/Taskforce.d.ts +51 -10
- package/dist/core/Taskforce.js +218 -49
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/types.d.ts +11 -12
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskforce.d.ts +3 -0
- package/dist/hooks/useTaskforce.js +9 -4
- package/dist/mcp/runtime.js +90 -29
- package/dist/mcp/runtime.test.js +272 -81
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.test.js +2 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +80 -23
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +194 -53
- package/dist/server/routes/billing.js +506 -23
- package/dist/server/routes/billing.test.js +937 -45
- package/dist/server/routes/shared.js +5 -3
- package/dist/server/routes/sync.integration.test.js +160 -0
- package/dist/server/routes/sync.js +15 -5
- package/dist/server/routes.js +3 -3
- package/dist/server/routes.test.js +287 -76
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -16
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/workspacePullFeed.d.ts +10 -0
- package/dist/sync/workspacePullFeed.js +111 -63
- package/dist/sync/workspacePullFeed.test.js +112 -0
- package/dist/ui/assets/{AgentsModule-JG5jc3he.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-C3GyzrWm.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/{TaskSettings-Bqd4cCzW.js → TaskSettings-BOC5F6Ag.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.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-C8EED3Oh.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/package.json +3 -3
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- package/dist/utils/billingStatusCache.js +0 -70
|
@@ -122,6 +122,7 @@ function formatMockCheckoutSession(record, expandSubscription = false) {
|
|
|
122
122
|
metadata: { ...record.metadata },
|
|
123
123
|
payment_status: record.paymentStatus,
|
|
124
124
|
status: record.status,
|
|
125
|
+
expires_at: record.expiresAt,
|
|
125
126
|
subscription: record.subscriptionId
|
|
126
127
|
? (expandSubscription ? formatMockSubscription(getMockStripeSubscription(record.subscriptionId)) : record.subscriptionId)
|
|
127
128
|
: null
|
|
@@ -243,6 +244,7 @@ export function createMockStripeClient() {
|
|
|
243
244
|
successUrl: String(params.success_url || '').trim(),
|
|
244
245
|
cancelUrl: String(params.cancel_url || '').trim(),
|
|
245
246
|
subscriptionId: null,
|
|
247
|
+
expiresAt: Math.floor(Date.now() / 1000) + (24 * 60 * 60),
|
|
246
248
|
paymentStatus: 'unpaid',
|
|
247
249
|
status: 'open'
|
|
248
250
|
};
|
|
@@ -101,7 +101,7 @@ export function registerAdminRoutes(deps) {
|
|
|
101
101
|
const authSettings = globalSettings.auth || {};
|
|
102
102
|
const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
|
|
103
103
|
const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
|
|
104
|
-
const
|
|
104
|
+
const onboardingPolicy = core.getOnboardingPolicy();
|
|
105
105
|
return {
|
|
106
106
|
allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
|
|
107
107
|
requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
|
|
@@ -113,8 +113,8 @@ export function registerAdminRoutes(deps) {
|
|
|
113
113
|
authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
|
|
114
114
|
? Math.floor(authRateLimitWindowMsRaw)
|
|
115
115
|
: 15 * 60_000,
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
onboardingPolicyMode: onboardingPolicy.mode,
|
|
117
|
+
onboardingPlanVersionId: onboardingPolicy.planVersionId
|
|
118
118
|
};
|
|
119
119
|
};
|
|
120
120
|
const getSiteBehavior = () => {
|
|
@@ -163,6 +163,10 @@ export function registerAdminRoutes(deps) {
|
|
|
163
163
|
}
|
|
164
164
|
return String(configured || resolvePublicAuthBaseUrl(req)).replace(/\/+$/, '');
|
|
165
165
|
};
|
|
166
|
+
const resolveAdminCloudEnvironment = (req) => {
|
|
167
|
+
return resolveTaskforceCloudEnvironmentFromBaseUrl(resolveRuntimeConfigBaseUrl(req))
|
|
168
|
+
|| resolveTaskforceCloudEnvironmentFromBaseUrl(resolveCloudLoginBaseUrl(req));
|
|
169
|
+
};
|
|
166
170
|
const resolveMcpOauthAuthorizeUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/authorize`;
|
|
167
171
|
const resolveMcpOauthTokenUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/token`;
|
|
168
172
|
const resolveMcpOauthRegisterUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/register`;
|
|
@@ -964,6 +968,51 @@ export function registerAdminRoutes(deps) {
|
|
|
964
968
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
965
969
|
res.end(JSON.stringify({ success: true }));
|
|
966
970
|
});
|
|
971
|
+
// DELETE /api/taskforce/admin/system-users/:id - Purge a test user in non-production cloud environments
|
|
972
|
+
addRoute('DELETE', '/api/taskforce/admin/system-users/:id', async (req, res, params) => {
|
|
973
|
+
if (!requireCloudRuntime(res, 'System user purge'))
|
|
974
|
+
return;
|
|
975
|
+
if (!ensureAdminRole(req)) {
|
|
976
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
977
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const environment = resolveAdminCloudEnvironment(req);
|
|
981
|
+
if (environment !== 'staging' && environment !== 'performance') {
|
|
982
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
983
|
+
res.end(JSON.stringify({
|
|
984
|
+
error: 'System user purge is only available in staging and performance.',
|
|
985
|
+
code: 'SYSTEM_USER_PURGE_ENVIRONMENT_FORBIDDEN'
|
|
986
|
+
}));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const body = await parseJsonBody(req);
|
|
990
|
+
const actor = resolveAdminRequestActor(req);
|
|
991
|
+
try {
|
|
992
|
+
const summary = core.purgeSystemUser({
|
|
993
|
+
targetUserId: params.id,
|
|
994
|
+
actorUserId: actor.userId,
|
|
995
|
+
emailConfirmation: String(body?.emailConfirmation || '')
|
|
996
|
+
});
|
|
997
|
+
auditAdminAction(req, 'purge-system-user', {
|
|
998
|
+
targetUserId: params.id,
|
|
999
|
+
targetEmail: summary.email,
|
|
1000
|
+
environment,
|
|
1001
|
+
summary
|
|
1002
|
+
});
|
|
1003
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1004
|
+
res.end(JSON.stringify({ success: true, environment, summary }));
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1008
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1009
|
+
res.end(JSON.stringify({
|
|
1010
|
+
error: String(error?.message || 'Unable to purge system user'),
|
|
1011
|
+
code: error?.code,
|
|
1012
|
+
details: error?.details || null
|
|
1013
|
+
}));
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
967
1016
|
// === SECTION: Admin email settings ===
|
|
968
1017
|
// GET /api/taskforce/admin/email-settings - Read email provider settings
|
|
969
1018
|
addRoute('GET', '/api/taskforce/admin/email-settings', async (req, res) => {
|
|
@@ -1036,7 +1085,9 @@ export function registerAdminRoutes(deps) {
|
|
|
1036
1085
|
res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
|
|
1037
1086
|
return;
|
|
1038
1087
|
}
|
|
1039
|
-
const
|
|
1088
|
+
const globalSettings = core.getGlobalSettings();
|
|
1089
|
+
const authSettings = globalSettings.auth || {};
|
|
1090
|
+
const billingSettings = globalSettings.billing || {};
|
|
1040
1091
|
const effective = getAuthBehavior();
|
|
1041
1092
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1042
1093
|
res.end(JSON.stringify({
|
|
@@ -1047,11 +1098,13 @@ export function registerAdminRoutes(deps) {
|
|
|
1047
1098
|
...(typeof authSettings.authRateLimitEnabled === 'boolean' ? { authRateLimitEnabled: authSettings.authRateLimitEnabled } : {}),
|
|
1048
1099
|
...(typeof authSettings.authRateLimitMaxRequests === 'number' ? { authRateLimitMaxRequests: authSettings.authRateLimitMaxRequests } : {}),
|
|
1049
1100
|
...(typeof authSettings.authRateLimitWindowMs === 'number' ? { authRateLimitWindowMs: authSettings.authRateLimitWindowMs } : {}),
|
|
1050
|
-
...((
|
|
1051
|
-
|
|
1101
|
+
...((billingSettings.onboardingPolicy?.mode === 'require_plan_selection'
|
|
1102
|
+
|| billingSettings.onboardingPolicy?.mode === 'auto_grant_free'
|
|
1103
|
+
|| billingSettings.onboardingPolicy?.mode === 'auto_start_trial')
|
|
1104
|
+
? { onboardingPolicyMode: billingSettings.onboardingPolicy.mode }
|
|
1052
1105
|
: {}),
|
|
1053
|
-
...(typeof
|
|
1054
|
-
? {
|
|
1106
|
+
...(typeof billingSettings.onboardingPolicy?.planVersionId === 'string' || billingSettings.onboardingPolicy?.planVersionId === null
|
|
1107
|
+
? { onboardingPlanVersionId: billingSettings.onboardingPolicy?.planVersionId ?? null }
|
|
1055
1108
|
: {})
|
|
1056
1109
|
},
|
|
1057
1110
|
status: {
|
|
@@ -1061,8 +1114,8 @@ export function registerAdminRoutes(deps) {
|
|
|
1061
1114
|
authRateLimitEnabled: effective.authRateLimitEnabled,
|
|
1062
1115
|
authRateLimitMaxRequests: effective.authRateLimitMaxRequests,
|
|
1063
1116
|
authRateLimitWindowMs: effective.authRateLimitWindowMs,
|
|
1064
|
-
|
|
1065
|
-
|
|
1117
|
+
onboardingPolicyMode: effective.onboardingPolicyMode,
|
|
1118
|
+
onboardingPlanVersionId: effective.onboardingPlanVersionId
|
|
1066
1119
|
}
|
|
1067
1120
|
}));
|
|
1068
1121
|
});
|
|
@@ -1688,21 +1741,21 @@ export function registerAdminRoutes(deps) {
|
|
|
1688
1741
|
return;
|
|
1689
1742
|
}
|
|
1690
1743
|
const body = await parseJsonBody(req);
|
|
1691
|
-
const
|
|
1692
|
-
?
|
|
1693
|
-
: '
|
|
1694
|
-
const
|
|
1695
|
-
? body.
|
|
1696
|
-
: body?.
|
|
1744
|
+
const onboardingPolicyMode = body?.onboardingPolicyMode === 'auto_grant_free' || body?.onboardingPolicyMode === 'auto_start_trial'
|
|
1745
|
+
? body.onboardingPolicyMode
|
|
1746
|
+
: 'require_plan_selection';
|
|
1747
|
+
const onboardingPlanVersionId = typeof body?.onboardingPlanVersionId === 'string'
|
|
1748
|
+
? body.onboardingPlanVersionId.trim() || null
|
|
1749
|
+
: body?.onboardingPlanVersionId === null
|
|
1697
1750
|
? null
|
|
1698
1751
|
: undefined;
|
|
1699
|
-
const validation = core.
|
|
1700
|
-
|
|
1701
|
-
...(
|
|
1752
|
+
const validation = core.validateOnboardingPolicy({
|
|
1753
|
+
mode: onboardingPolicyMode,
|
|
1754
|
+
...(onboardingPlanVersionId !== undefined ? { planVersionId: onboardingPlanVersionId } : {})
|
|
1702
1755
|
});
|
|
1703
1756
|
if (!validation.valid) {
|
|
1704
1757
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1705
|
-
res.end(JSON.stringify({ error: validation.message || 'Invalid
|
|
1758
|
+
res.end(JSON.stringify({ error: validation.message || 'Invalid onboarding policy.', code: validation.code, details: validation.details }));
|
|
1706
1759
|
return;
|
|
1707
1760
|
}
|
|
1708
1761
|
core.updateGlobalSettings({
|
|
@@ -1714,9 +1767,13 @@ export function registerAdminRoutes(deps) {
|
|
|
1714
1767
|
...(typeof body?.authRateLimitMaxRequests === 'number' && Number.isFinite(body.authRateLimitMaxRequests)
|
|
1715
1768
|
? { authRateLimitMaxRequests: Math.max(1, Math.floor(body.authRateLimitMaxRequests)) } : {}),
|
|
1716
1769
|
...(typeof body?.authRateLimitWindowMs === 'number' && Number.isFinite(body.authRateLimitWindowMs)
|
|
1717
|
-
? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {})
|
|
1718
|
-
|
|
1719
|
-
|
|
1770
|
+
? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {})
|
|
1771
|
+
},
|
|
1772
|
+
billing: {
|
|
1773
|
+
onboardingPolicy: {
|
|
1774
|
+
mode: onboardingPolicyMode,
|
|
1775
|
+
planVersionId: onboardingPlanVersionId ?? null
|
|
1776
|
+
}
|
|
1720
1777
|
}
|
|
1721
1778
|
});
|
|
1722
1779
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -20,6 +20,7 @@
|
|
|
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
|
|
24
25
|
* GET /api/taskforce/account/team-management-access
|
|
25
26
|
* GET /api/taskforce/account/plan-preview
|
|
@@ -20,6 +20,7 @@
|
|
|
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
|
|
24
25
|
* GET /api/taskforce/account/team-management-access
|
|
25
26
|
* GET /api/taskforce/account/plan-preview
|
|
@@ -73,6 +74,121 @@ export function registerAuthRoutes(deps) {
|
|
|
73
74
|
const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
|
|
74
75
|
return teamAllowed && stateAllows ? 'team' : 'personal';
|
|
75
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
|
+
};
|
|
76
192
|
// === SECTION: Email delivery helpers ===
|
|
77
193
|
const getEmailConfig = () => {
|
|
78
194
|
if (context.authConfig?.runtimeMode !== 'cloud') {
|
|
@@ -86,7 +202,7 @@ export function registerAuthRoutes(deps) {
|
|
|
86
202
|
const authSettings = globalSettings.auth || {};
|
|
87
203
|
const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
|
|
88
204
|
const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
|
|
89
|
-
const
|
|
205
|
+
const onboardingPolicy = core.getOnboardingPolicy();
|
|
90
206
|
return {
|
|
91
207
|
allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
|
|
92
208
|
requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
|
|
@@ -98,8 +214,8 @@ export function registerAuthRoutes(deps) {
|
|
|
98
214
|
authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
|
|
99
215
|
? Math.floor(authRateLimitWindowMsRaw)
|
|
100
216
|
: 15 * 60_000,
|
|
101
|
-
|
|
102
|
-
|
|
217
|
+
onboardingPolicyMode: onboardingPolicy.mode,
|
|
218
|
+
onboardingPlanVersionId: onboardingPolicy.planVersionId
|
|
103
219
|
};
|
|
104
220
|
};
|
|
105
221
|
const getSiteBehavior = () => {
|
|
@@ -110,6 +226,23 @@ export function registerAuthRoutes(deps) {
|
|
|
110
226
|
pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
|
|
111
227
|
};
|
|
112
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
|
+
};
|
|
113
246
|
const AVATAR_DRAFT_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
114
247
|
const MAX_AVATAR_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
115
248
|
const AVATAR_MIME_EXTENSION = {
|
|
@@ -643,7 +776,7 @@ export function registerAuthRoutes(deps) {
|
|
|
643
776
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
644
777
|
res.end(JSON.stringify({ success: false, error: 'Email and password are required.' }));
|
|
645
778
|
});
|
|
646
|
-
// POST /api/taskforce/auth/register - Create account and
|
|
779
|
+
// POST /api/taskforce/auth/register - Create account and optionally start a session
|
|
647
780
|
addRoute('POST', '/api/taskforce/auth/register', async (req, res) => {
|
|
648
781
|
if (!requireCloudRuntime(res, 'Authentication'))
|
|
649
782
|
return;
|
|
@@ -693,13 +826,6 @@ export function registerAuthRoutes(deps) {
|
|
|
693
826
|
role,
|
|
694
827
|
betaAccess: authBehavior.defaultNewUserBetaAccess
|
|
695
828
|
});
|
|
696
|
-
const sessionWorkspaceId = created.workspaceId || '';
|
|
697
|
-
const cookie = createSessionTokenCookie(`pwd:${created.userId}`, authConfig, {
|
|
698
|
-
role: created.role,
|
|
699
|
-
workspaceId: sessionWorkspaceId,
|
|
700
|
-
userId: created.userId,
|
|
701
|
-
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
702
|
-
});
|
|
703
829
|
let emailSent = true;
|
|
704
830
|
let emailError;
|
|
705
831
|
let verificationToken;
|
|
@@ -719,7 +845,13 @@ export function registerAuthRoutes(deps) {
|
|
|
719
845
|
role: created.role,
|
|
720
846
|
emailSent
|
|
721
847
|
});
|
|
722
|
-
|
|
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
|
+
}
|
|
723
855
|
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
724
856
|
res.end(JSON.stringify({
|
|
725
857
|
success: true,
|
|
@@ -727,6 +859,8 @@ export function registerAuthRoutes(deps) {
|
|
|
727
859
|
role: created.role,
|
|
728
860
|
workspaceSetupRequired: !created.workspaceId,
|
|
729
861
|
planSelectionRequired: created.planSelectionRequired === true,
|
|
862
|
+
checkoutPending: created.checkoutPending === true,
|
|
863
|
+
commercialState: created.commercialState,
|
|
730
864
|
verificationRequired: authBehavior.requireVerifiedEmail,
|
|
731
865
|
emailSent,
|
|
732
866
|
emailError,
|
|
@@ -794,6 +928,12 @@ export function registerAuthRoutes(deps) {
|
|
|
794
928
|
addRoute('POST', '/api/taskforce/auth/verify-email/confirm', async (req, res) => {
|
|
795
929
|
if (!requireCloudRuntime(res, 'Authentication'))
|
|
796
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
|
+
}
|
|
797
937
|
const body = await parseJsonBody(req);
|
|
798
938
|
const token = String(body?.token || '').trim();
|
|
799
939
|
if (!token) {
|
|
@@ -801,6 +941,15 @@ export function registerAuthRoutes(deps) {
|
|
|
801
941
|
res.end(JSON.stringify({ success: false, error: 'Token is required.' }));
|
|
802
942
|
return;
|
|
803
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
|
+
}
|
|
804
953
|
const verified = core.verifyEmailWithToken(token);
|
|
805
954
|
if (!verified) {
|
|
806
955
|
auditAuthAction(req, 'auth-verify-confirm-failed', {
|
|
@@ -813,6 +962,11 @@ export function registerAuthRoutes(deps) {
|
|
|
813
962
|
auditAuthAction(req, 'auth-verify-confirm-success', {
|
|
814
963
|
tokenProvided: true
|
|
815
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);
|
|
816
970
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
817
971
|
res.end(JSON.stringify({ success: true }));
|
|
818
972
|
});
|
|
@@ -1098,6 +1252,24 @@ export function registerAuthRoutes(deps) {
|
|
|
1098
1252
|
res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve account access status'), code: error?.code, details: error?.details }));
|
|
1099
1253
|
}
|
|
1100
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
|
+
});
|
|
1101
1273
|
// GET /api/taskforce/account/entitlements - Resolve effective plan/features for the active workspace.
|
|
1102
1274
|
addRoute('GET', '/api/taskforce/account/entitlements', async (req, res) => {
|
|
1103
1275
|
if (!ensureAuthenticatedUser(req)) {
|
|
@@ -1115,11 +1287,11 @@ export function registerAuthRoutes(deps) {
|
|
|
1115
1287
|
catch (error) {
|
|
1116
1288
|
if (error?.code === 'ENTITLEMENT_NOT_FOUND') {
|
|
1117
1289
|
const access = core.getAccountAccessStatus(requestUserId(req));
|
|
1118
|
-
if (access.gate === 'plan_selection_required') {
|
|
1290
|
+
if (access.gate === 'plan_selection_required' || access.gate === 'checkout_pending') {
|
|
1119
1291
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1120
1292
|
res.end(JSON.stringify({
|
|
1121
1293
|
error: access.message,
|
|
1122
|
-
code: 'PLAN_SELECTION_REQUIRED',
|
|
1294
|
+
code: access.gate === 'checkout_pending' ? 'CHECKOUT_PENDING' : 'PLAN_SELECTION_REQUIRED',
|
|
1123
1295
|
details: {
|
|
1124
1296
|
gate: access.gate,
|
|
1125
1297
|
planSelectionRequired: access.planSelectionRequired,
|
|
@@ -1141,51 +1313,20 @@ export function registerAuthRoutes(deps) {
|
|
|
1141
1313
|
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
1142
1314
|
return;
|
|
1143
1315
|
}
|
|
1144
|
-
const wid = requestWorkspaceId(req);
|
|
1145
|
-
const userId = requestUserId(req);
|
|
1146
|
-
const hintedRole = normalizeTeamWorkspaceRole(requestHeader(req, 'x-taskforce-workspace-role', ''));
|
|
1147
|
-
const resolvedAccess = core.resolveUserAccess(userId, wid, 'member');
|
|
1148
|
-
const role = normalizeTeamWorkspaceRole(resolvedAccess?.role || hintedRole || null);
|
|
1149
|
-
const canManageWorkspace = role === 'owner' || role === 'admin';
|
|
1150
|
-
if (!canManageWorkspace) {
|
|
1151
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1152
|
-
res.end(JSON.stringify({
|
|
1153
|
-
workspaceId: wid,
|
|
1154
|
-
role,
|
|
1155
|
-
canManageWorkspace: false,
|
|
1156
|
-
teamPlanMode: 'unknown',
|
|
1157
|
-
allowed: false
|
|
1158
|
-
}));
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
1316
|
try {
|
|
1162
|
-
const
|
|
1163
|
-
const teamPlanMode = resolveTeamPlanModeFromEntitlements(entitlements);
|
|
1317
|
+
const payload = buildAccountProfileSummary(req);
|
|
1164
1318
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1165
1319
|
res.end(JSON.stringify({
|
|
1166
|
-
workspaceId:
|
|
1167
|
-
role,
|
|
1168
|
-
canManageWorkspace:
|
|
1169
|
-
teamPlanMode,
|
|
1170
|
-
allowed:
|
|
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
|
|
1171
1327
|
}));
|
|
1172
1328
|
}
|
|
1173
1329
|
catch (error) {
|
|
1174
|
-
if (error?.code === 'ENTITLEMENT_NOT_FOUND') {
|
|
1175
|
-
const access = core.getAccountAccessStatus(userId);
|
|
1176
|
-
const teamPlanMode = access.gate === 'plan_selection_required' ? 'personal' : 'unknown';
|
|
1177
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1178
|
-
res.end(JSON.stringify({
|
|
1179
|
-
workspaceId: wid,
|
|
1180
|
-
role,
|
|
1181
|
-
canManageWorkspace: true,
|
|
1182
|
-
teamPlanMode,
|
|
1183
|
-
allowed: false,
|
|
1184
|
-
gate: access.gate,
|
|
1185
|
-
planSelectionRequired: access.planSelectionRequired
|
|
1186
|
-
}));
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
1330
|
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1190
1331
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1191
1332
|
res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve team management access'), code: error?.code, details: error?.details }));
|