@antelopejs/dms-saas 0.0.2 → 0.1.1
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/README.md
CHANGED
|
@@ -32,6 +32,34 @@ workspace. From that directory run `pnpm install`,
|
|
|
32
32
|
`pnpm test:frontend`. The frontend check generates a temporary workspace, then
|
|
33
33
|
builds client, SSR, and email bundles and runs Vue typechecking.
|
|
34
34
|
|
|
35
|
+
## Completing a registration that entered without a workspace
|
|
36
|
+
|
|
37
|
+
An account can reach the product before it owns anything to log into: an OAuth
|
|
38
|
+
sign-in for an unknown e-mail, or any entry point the core answers with
|
|
39
|
+
`requires_tenant_assignment`. The `auth/no-workspace` page is where that
|
|
40
|
+
account buys its way in — a workspace, a plan, a card — and
|
|
41
|
+
`POST /api/saas/register/finalize` provisions it and answers with a token pair
|
|
42
|
+
for the workspace it just created, so the visitor lands signed in instead of
|
|
43
|
+
back on the login screen.
|
|
44
|
+
|
|
45
|
+
The page never touches those tokens. It posts the call through the frontend
|
|
46
|
+
loader's generic `POST /auth/establish` route, naming the endpoint to call and
|
|
47
|
+
its payload; the loader calls `/api/saas/register/finalize` itself over its own
|
|
48
|
+
server-to-server channel to the DMS API and writes its session cookie from what
|
|
49
|
+
comes back. Because that route turns a backend endpoint into a login, it only
|
|
50
|
+
calls endpoints the deployment named, so a project serving this page must
|
|
51
|
+
declare the finalize endpoint in the frontend server's environment:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# .env of the frontend server
|
|
55
|
+
DMS_AUTH_ESTABLISH_ENDPOINTS=/api/saas/register/finalize
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Without it the loader answers `403` and a visitor who has paid stays signed
|
|
59
|
+
out. A replacement completion screen keeps the same requirement, and can build
|
|
60
|
+
its request with the auto-imported `buildSessionEstablishRequest()` helper
|
|
61
|
+
rather than restating the endpoint and the payload shape.
|
|
62
|
+
|
|
35
63
|
## Tenant owner permissions
|
|
36
64
|
|
|
37
65
|
The `dms-saas.plan-intersection` permissions resolver runs at order 100. For a
|
|
@@ -137,9 +165,10 @@ provider registered before dms-saas is constructed is held and replayed on
|
|
|
137
165
|
attach, replayed again when dms-saas is reloaded, and dropped when the
|
|
138
166
|
registering module is unloaded.
|
|
139
167
|
|
|
140
|
-
The interface package must be released before dms-saas
|
|
141
|
-
|
|
142
|
-
the
|
|
168
|
+
The interface package must be released before dms-saas, which depends on it
|
|
169
|
+
through `>=<interface version> <1.0.0`; during workspace development pnpm
|
|
170
|
+
resolves that range to the sibling package. Release the interface package first,
|
|
171
|
+
then
|
|
143
172
|
release dms-saas, and update consumers to the published interface version.
|
|
144
173
|
|
|
145
174
|
### Public screens
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completing a self-service registration that entered without a workspace.
|
|
3
|
+
*
|
|
4
|
+
* The account already exists — it signed in through OAuth, or through any
|
|
5
|
+
* entry point that ends on a tenant assignment token — and everything that is
|
|
6
|
+
* still missing is bought here: a workspace, a plan, a card. The API answers
|
|
7
|
+
* with a session for the workspace it just provisioned, so the visitor lands
|
|
8
|
+
* signed in rather than back on the login screen.
|
|
9
|
+
*
|
|
10
|
+
* That session is opened by the frontend server, never in the browser: the
|
|
11
|
+
* page asks the loader's `/auth/establish` route to call the finalize endpoint
|
|
12
|
+
* on its behalf, and the loader writes its session cookie from the token pair
|
|
13
|
+
* the API returns. No access or refresh token is ever handed to the page.
|
|
14
|
+
*
|
|
15
|
+
* The deployment must name the finalize endpoint in the frontend server's
|
|
16
|
+
* `DMS_AUTH_ESTABLISH_ENDPOINTS`, otherwise `/auth/establish` answers 403:
|
|
17
|
+
* turning a backend route into a login is the operator's decision, not a
|
|
18
|
+
* module's.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Loader route that opens a session from a backend-issued token pair. */
|
|
22
|
+
export const SESSION_ESTABLISH_ENDPOINT = "/auth/establish";
|
|
23
|
+
|
|
24
|
+
/** DMS API route that provisions the workspace and mints that token pair. */
|
|
25
|
+
export const WORKSPACE_FINALIZE_ENDPOINT = "/api/saas/register/finalize";
|
|
26
|
+
|
|
27
|
+
export interface WorkspaceFinalizationAddress {
|
|
28
|
+
country: string;
|
|
29
|
+
line1?: string;
|
|
30
|
+
postalCode?: string;
|
|
31
|
+
city?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The completion form as the visitor left it, plus the confirmed card. */
|
|
35
|
+
export interface WorkspaceFinalization {
|
|
36
|
+
tenantAssignmentToken: string;
|
|
37
|
+
workspaceName: string;
|
|
38
|
+
planId: string;
|
|
39
|
+
customerType: "individual" | "business";
|
|
40
|
+
companyName: string;
|
|
41
|
+
vatNumber: string;
|
|
42
|
+
address: WorkspaceFinalizationAddress;
|
|
43
|
+
paymentMethodId: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WorkspaceFinalizationPayload {
|
|
47
|
+
tenant_assignment_token: string;
|
|
48
|
+
workspaceName: string;
|
|
49
|
+
planId: string;
|
|
50
|
+
customerType: "individual" | "business";
|
|
51
|
+
companyName?: string;
|
|
52
|
+
vatNumber?: string;
|
|
53
|
+
address: WorkspaceFinalizationAddress;
|
|
54
|
+
paymentMethodId: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What `/auth/establish` takes: an endpoint to call, and its body. */
|
|
58
|
+
export interface SessionEstablishRequest {
|
|
59
|
+
endpoint: string;
|
|
60
|
+
payload: WorkspaceFinalizationPayload;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The finalize call, addressed to the frontend server instead of the API.
|
|
65
|
+
*
|
|
66
|
+
* Built in one place because two things have to stay true together: the
|
|
67
|
+
* payload is what `/api/saas/register/finalize` validates, and the endpoint
|
|
68
|
+
* named in the envelope is the one the operator allowed — a page that inlined
|
|
69
|
+
* either would drift from the other.
|
|
70
|
+
*
|
|
71
|
+
* Company fields only travel for a business customer: an individual who typed
|
|
72
|
+
* a company name and switched back would otherwise be invoiced as one.
|
|
73
|
+
*
|
|
74
|
+
* @param finalization Completion form and confirmed payment method
|
|
75
|
+
* @returns Body to POST to `/auth/establish`
|
|
76
|
+
*/
|
|
77
|
+
export function buildSessionEstablishRequest(
|
|
78
|
+
finalization: WorkspaceFinalization,
|
|
79
|
+
): SessionEstablishRequest {
|
|
80
|
+
const isBusiness = finalization.customerType === "business";
|
|
81
|
+
return {
|
|
82
|
+
endpoint: WORKSPACE_FINALIZE_ENDPOINT,
|
|
83
|
+
payload: {
|
|
84
|
+
tenant_assignment_token: finalization.tenantAssignmentToken,
|
|
85
|
+
workspaceName: finalization.workspaceName,
|
|
86
|
+
planId: finalization.planId,
|
|
87
|
+
customerType: finalization.customerType,
|
|
88
|
+
companyName: isBusiness ? finalization.companyName : undefined,
|
|
89
|
+
vatNumber: isBusiness ? finalization.vatNumber : undefined,
|
|
90
|
+
address: finalization.address,
|
|
91
|
+
paymentMethodId: finalization.paymentMethodId,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -39,7 +39,6 @@ interface PendingRegistration {
|
|
|
39
39
|
const PLANS_ENDPOINT = "/api/saas/plans/public";
|
|
40
40
|
const SETUP_INTENT_ENDPOINT = "/api/saas/register/setup-intent";
|
|
41
41
|
const PENDING_ENDPOINT = "/api/saas/register/pending";
|
|
42
|
-
const FINALIZE_ENDPOINT = "/auth/finalize-workspace";
|
|
43
42
|
const LOGIN_REDIRECT = "/auth/login";
|
|
44
43
|
const PAYMENT_ELEMENT_ID = "dms-saas-finalize-payment-element";
|
|
45
44
|
const HOME_REDIRECT = "/";
|
|
@@ -68,8 +67,9 @@ const stripePublishableKey = computed<string>(
|
|
|
68
67
|
() => dmsSaasRuntime.value?.stripePublishableKey ?? "",
|
|
69
68
|
);
|
|
70
69
|
|
|
71
|
-
// Only `finalize` goes through the frontend server because it opens
|
|
72
|
-
//
|
|
70
|
+
// Only `finalize` goes through the frontend server, and only because it opens
|
|
71
|
+
// the session cookie on the workspace it provisions; every other public
|
|
72
|
+
// registration endpoint is called on the DMS API directly.
|
|
73
73
|
const apiFetch = $fetch.create({ baseURL: dmsRuntime.value.baseURL });
|
|
74
74
|
|
|
75
75
|
const { countryItems } = useBillingCountries();
|
|
@@ -225,17 +225,15 @@ async function submit(): Promise<void> {
|
|
|
225
225
|
nuxtApp.$i18n.t("saas.register.error.no_payment");
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
-
await $fetch(
|
|
228
|
+
await $fetch(SESSION_ESTABLISH_ENDPOINT, {
|
|
229
229
|
method: "POST",
|
|
230
|
-
body: {
|
|
231
|
-
|
|
230
|
+
body: buildSessionEstablishRequest({
|
|
231
|
+
tenantAssignmentToken: tenantAssignmentToken.value,
|
|
232
232
|
workspaceName: workspaceName.value,
|
|
233
233
|
planId: selectedPlanId.value,
|
|
234
234
|
customerType: customerType.value,
|
|
235
|
-
companyName:
|
|
236
|
-
|
|
237
|
-
vatNumber:
|
|
238
|
-
customerType.value === "business" ? vatNumber.value : undefined,
|
|
235
|
+
companyName: companyName.value,
|
|
236
|
+
vatNumber: vatNumber.value,
|
|
239
237
|
address: {
|
|
240
238
|
country: country.value,
|
|
241
239
|
line1: addressLine1.value || undefined,
|
|
@@ -243,7 +241,7 @@ async function submit(): Promise<void> {
|
|
|
243
241
|
city: city.value || undefined,
|
|
244
242
|
},
|
|
245
243
|
paymentMethodId: confirm.paymentMethodId,
|
|
246
|
-
},
|
|
244
|
+
}),
|
|
247
245
|
});
|
|
248
246
|
if (typeof window !== "undefined") {
|
|
249
247
|
window.location.href = HOME_REDIRECT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms-saas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,25 +27,6 @@
|
|
|
27
27
|
"provenance": true,
|
|
28
28
|
"registry": "https://registry.npmjs.org/"
|
|
29
29
|
},
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "pnpm --filter @antelopejs/interface-dms-saas build && rimraf dist && tsc -p tsconfig.build.json",
|
|
32
|
-
"test:frontend": "ajs-dms verify-source --layer node_modules/@antelopejs/dms/frontend-vue --module frontend-vue",
|
|
33
|
-
"frontend:dev": "cd playground && pnpm frontend:dev",
|
|
34
|
-
"dev": "ajs project run -w -p playground",
|
|
35
|
-
"stripe:webhooks": "stripe listen --forward-to localhost:5010/api/saas/webhooks/stripe",
|
|
36
|
-
"lint:fix": "oxlint --fix --max-warnings=1000000 && oxfmt . && cd frontend-vue && pnpm lint:fix",
|
|
37
|
-
"lint": "oxlint && oxfmt --check . && cd frontend-vue && pnpm lint",
|
|
38
|
-
"typecheck": "tsc -p tsconfig.json",
|
|
39
|
-
"format": "oxfmt . && cd frontend-vue && pnpm format",
|
|
40
|
-
"check:package-contract": "node scripts/check-package-contract.mjs",
|
|
41
|
-
"verify:interface": "node scripts/verify-interface-package.cjs",
|
|
42
|
-
"test": "pnpm run build && pnpm --filter @antelopejs/interface-dms-saas run check:package-contract && pnpm run check:package-contract && pnpm run test:unit",
|
|
43
|
-
"test:unit": "vitest run",
|
|
44
|
-
"release": "pnpm --filter @antelopejs/interface-dms-saas build && pnpm --dir frontend-vue install --frozen-lockfile && pnpm run verify:interface && release-it",
|
|
45
|
-
"prepack": "pnpm run build && pnpm run check:package-contract",
|
|
46
|
-
"knip": "knip --dependencies",
|
|
47
|
-
"knip:all": "knip"
|
|
48
|
-
},
|
|
49
30
|
"dependencies": {
|
|
50
31
|
"@antelopejs/dms": ">=0.0.1 <1.0.0",
|
|
51
32
|
"@antelopejs/interface-api": ">=0.0.13 <1.0.0",
|
|
@@ -55,7 +36,7 @@
|
|
|
55
36
|
"@antelopejs/interface-database": ">=0.1.7 <1.0.0",
|
|
56
37
|
"@antelopejs/interface-database-decorators": ">=0.1.6 <1.0.0",
|
|
57
38
|
"@antelopejs/interface-dms": ">=0.0.1 <1.0.0",
|
|
58
|
-
"@antelopejs/interface-dms-saas": "
|
|
39
|
+
"@antelopejs/interface-dms-saas": ">=0.0.2 <1.0.0",
|
|
59
40
|
"@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
|
|
60
41
|
"node-cron": "^4.2.1",
|
|
61
42
|
"stripe": "^17.5.0",
|
|
@@ -83,11 +64,28 @@
|
|
|
83
64
|
"optionalDependencies": {
|
|
84
65
|
"@antelopejs/interface-dms-automation": ">=0.0.1 <1.0.0"
|
|
85
66
|
},
|
|
86
|
-
"packageManager": "pnpm@10.2.0",
|
|
87
67
|
"antelopeJs": {
|
|
88
68
|
"baseUrl": "dist",
|
|
89
69
|
"implements": [
|
|
90
70
|
"@antelopejs/interface-dms-saas"
|
|
91
71
|
]
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "pnpm --filter @antelopejs/interface-dms-saas build && rimraf dist && tsc -p tsconfig.build.json",
|
|
75
|
+
"test:frontend": "ajs-dms verify-source --layer node_modules/@antelopejs/dms/frontend-vue --module frontend-vue",
|
|
76
|
+
"frontend:dev": "cd playground && pnpm frontend:dev",
|
|
77
|
+
"dev": "ajs project run -w -p playground",
|
|
78
|
+
"stripe:webhooks": "stripe listen --forward-to localhost:5010/api/saas/webhooks/stripe",
|
|
79
|
+
"lint:fix": "oxlint --fix --max-warnings=1000000 && oxfmt . && cd frontend-vue && pnpm lint:fix",
|
|
80
|
+
"lint": "oxlint && oxfmt --check . && cd frontend-vue && pnpm lint",
|
|
81
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
82
|
+
"format": "oxfmt . && cd frontend-vue && pnpm format",
|
|
83
|
+
"check:package-contract": "node scripts/check-package-contract.mjs",
|
|
84
|
+
"verify:interface": "node scripts/verify-interface-package.cjs",
|
|
85
|
+
"test": "pnpm run build && pnpm --filter @antelopejs/interface-dms-saas run check:package-contract && pnpm run check:package-contract && pnpm run test:unit",
|
|
86
|
+
"test:unit": "vitest run",
|
|
87
|
+
"release": "pnpm --filter @antelopejs/interface-dms-saas build && pnpm --dir frontend-vue install --frozen-lockfile && pnpm run verify:interface && release-it",
|
|
88
|
+
"knip": "knip --dependencies",
|
|
89
|
+
"knip:all": "knip"
|
|
92
90
|
}
|
|
93
|
-
}
|
|
91
|
+
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
interface FinalizeBody {
|
|
2
|
-
tenant_assignment_token: string;
|
|
3
|
-
workspaceName: string;
|
|
4
|
-
planId: string;
|
|
5
|
-
customerType: "individual" | "business";
|
|
6
|
-
companyName?: string;
|
|
7
|
-
vatNumber?: string;
|
|
8
|
-
address?: {
|
|
9
|
-
line1?: string;
|
|
10
|
-
line2?: string;
|
|
11
|
-
postalCode?: string;
|
|
12
|
-
city?: string;
|
|
13
|
-
state?: string;
|
|
14
|
-
country?: string;
|
|
15
|
-
};
|
|
16
|
-
paymentMethodId: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface FinalizeResponse {
|
|
20
|
-
token_type: string;
|
|
21
|
-
access_token: string;
|
|
22
|
-
expires_in: number;
|
|
23
|
-
refresh_token: string;
|
|
24
|
-
user: {
|
|
25
|
-
_id: string;
|
|
26
|
-
email: string;
|
|
27
|
-
name: string;
|
|
28
|
-
language: string;
|
|
29
|
-
isValidated: boolean;
|
|
30
|
-
createdAt: Date;
|
|
31
|
-
updatedAt: Date;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default defineEventHandler(async (event) => {
|
|
36
|
-
const config = useRuntimeConfig();
|
|
37
|
-
const baseURL = config.public.dms.baseURL;
|
|
38
|
-
const body = (await readBody(event)) as FinalizeBody;
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const response = await $fetch<FinalizeResponse>(
|
|
42
|
-
`/api/saas/register/finalize`,
|
|
43
|
-
{
|
|
44
|
-
baseURL,
|
|
45
|
-
method: "POST",
|
|
46
|
-
body,
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
await replaceUserSession(event, {
|
|
51
|
-
user: response.user,
|
|
52
|
-
accessToken: response.access_token,
|
|
53
|
-
refreshToken: response.refresh_token,
|
|
54
|
-
activeTenantId: decodeJwtTenantId(response.access_token),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return response.user;
|
|
58
|
-
} catch (error: unknown) {
|
|
59
|
-
throwApiError(error);
|
|
60
|
-
}
|
|
61
|
-
});
|