@managemint-solutions/sdk 0.53.0 → 0.54.0
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 +75 -3
- package/dist/additional-details/index.d.ts +2 -2
- package/dist/additional-details/index.js +6 -10
- package/dist/audit/index.d.ts +11 -5
- package/dist/audit/index.js +20 -12
- package/dist/auth-client/index.d.ts +2 -0
- package/dist/auth-client/index.js +12 -0
- package/dist/auth.d.ts +21 -0
- package/dist/auth.js +47 -3
- package/dist/clients/dto.d.ts +5 -1
- package/dist/clients/dto.js +12 -1
- package/dist/clients/index.d.ts +5 -3
- package/dist/clients/index.js +8 -8
- package/dist/emulation-handshakes/index.d.ts +13 -0
- package/dist/emulation-handshakes/index.js +36 -0
- package/dist/errors.js +2 -2
- package/dist/index.d.ts +13 -9
- package/dist/index.js +22 -4
- package/dist/leads/dto.d.ts +5 -1
- package/dist/leads/dto.js +12 -1
- package/dist/leads/index.d.ts +5 -3
- package/dist/leads/index.js +20 -19
- package/dist/nest/index.d.ts +26 -1
- package/dist/nest/index.js +58 -1
- package/dist/notifications/index.d.ts +1 -3
- package/dist/organizations/dto.d.ts +9 -1
- package/dist/organizations/dto.js +40 -1
- package/dist/organizations/index.d.ts +11 -3
- package/dist/organizations/index.js +30 -2
- package/dist/permissions/index.d.ts +6 -4
- package/dist/permissions/index.js +17 -12
- package/dist/projects/dto.d.ts +5 -1
- package/dist/projects/dto.js +12 -1
- package/dist/projects/index.d.ts +5 -3
- package/dist/projects/index.js +8 -8
- package/dist/service-client/index.d.ts +32 -1
- package/dist/service-client/index.js +54 -1
- package/dist/staff/dto.d.ts +14 -0
- package/dist/staff/dto.js +63 -0
- package/dist/staff/index.d.ts +50 -0
- package/dist/staff/index.js +139 -0
- package/dist/staff-client.d.ts +37 -0
- package/dist/staff-client.js +52 -0
- package/dist/tasks/dto.d.ts +5 -1
- package/dist/tasks/dto.js +12 -1
- package/dist/tasks/index.d.ts +5 -3
- package/dist/tasks/index.js +18 -19
- package/dist/tenancy.d.ts +13 -0
- package/dist/tenancy.js +25 -0
- package/dist/user-api-credentials/dto.d.ts +12 -0
- package/dist/user-api-credentials/dto.js +56 -0
- package/dist/user-api-credentials/index.d.ts +47 -0
- package/dist/user-api-credentials/index.js +101 -0
- package/dist/users/dto.d.ts +5 -1
- package/dist/users/dto.js +13 -2
- package/dist/users/index.d.ts +5 -3
- package/dist/users/index.js +13 -29
- package/dist/validators.d.ts +2 -0
- package/dist/validators.js +6 -1
- package/package.json +2 -2
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { SeatReservation } from '@managemint-solutions/entities/billing';
|
|
2
|
-
import
|
|
2
|
+
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
3
|
+
import type { OrganizationSummary } from '@managemint-solutions/entities/organization';
|
|
4
|
+
import { type AuditResource, type AuditRow, EntityAuditTrail, type ResourceActor } from '../audit';
|
|
3
5
|
import type { TypedSupabaseClient } from '../client';
|
|
6
|
+
import type { GetOrganizationsDto } from './dto';
|
|
4
7
|
export * from './dto';
|
|
5
8
|
/** The organizations row as `ORGANIZATION_SELECT` produces it, before the api shapes it. */
|
|
6
9
|
export type OrganizationRow = Record<string, unknown> & {
|
|
@@ -20,10 +23,15 @@ export declare class OrganizationsResource {
|
|
|
20
23
|
private readonly table;
|
|
21
24
|
/** This record's history - its own rows and the rows that hang off it. */
|
|
22
25
|
readonly audit: EntityAuditTrail;
|
|
23
|
-
constructor(supabase: TypedSupabaseClient, auth:
|
|
26
|
+
constructor(supabase: TypedSupabaseClient, auth: ResourceActor, audit: AuditResource);
|
|
24
27
|
getById(organizationId: string): Promise<OrganizationRow>;
|
|
25
|
-
/** The caller's own organization, from the auth context. */
|
|
28
|
+
/** The caller's own organization, from the auth context. Staff have none: a 403. */
|
|
26
29
|
get(): Promise<OrganizationRow>;
|
|
30
|
+
/**
|
|
31
|
+
* Every organization the caller can read, newest first - all of them for staff, the caller's
|
|
32
|
+
* own for a tenant, as RLS decides. `search` matches the name, trading name and email.
|
|
33
|
+
*/
|
|
34
|
+
list(query: GetOrganizationsDto): Promise<MmsList<OrganizationSummary>>;
|
|
27
35
|
/**
|
|
28
36
|
* The whole row, unprojected. Billing reads a different handful of columns for each of its
|
|
29
37
|
* flows - provisioning, a module purchase, a seat charge - and they are all single-row
|
|
@@ -18,6 +18,7 @@ exports.OrganizationSignupResource = exports.OrganizationsResource = void 0;
|
|
|
18
18
|
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
19
19
|
const audit_1 = require("../audit");
|
|
20
20
|
const errors_1 = require("../errors");
|
|
21
|
+
const tenancy_1 = require("../tenancy");
|
|
21
22
|
const transforms_1 = require("../transforms");
|
|
22
23
|
__exportStar(require("./dto"), exports);
|
|
23
24
|
// The subscription machinery on the row - what the billing flows and the Paystack webhooks
|
|
@@ -94,6 +95,8 @@ const ORGANIZATION_SELECT = `
|
|
|
94
95
|
card_expiring_soon,
|
|
95
96
|
pricebook_id
|
|
96
97
|
`;
|
|
98
|
+
// The public contract is the shared OrganizationSummary; `modules` stays the stored module keys.
|
|
99
|
+
const ORGANIZATION_LIST_SELECT = 'mms_id, created_at, name, trading_name, email, billing_status, active_seat_count, paid_seat_count, modules';
|
|
97
100
|
/**
|
|
98
101
|
* The caller's own organization. Every method takes the id explicitly rather than reading it
|
|
99
102
|
* off the auth context, because the signup and webhook paths operate on an organization that
|
|
@@ -121,9 +124,34 @@ class OrganizationsResource {
|
|
|
121
124
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
122
125
|
return data;
|
|
123
126
|
}
|
|
124
|
-
/** The caller's own organization, from the auth context. */
|
|
127
|
+
/** The caller's own organization, from the auth context. Staff have none: a 403. */
|
|
125
128
|
async get() {
|
|
126
|
-
return this.getById(this.auth
|
|
129
|
+
return this.getById((0, tenancy_1.requireOrganization)(this.auth));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Every organization the caller can read, newest first - all of them for staff, the caller's
|
|
133
|
+
* own for a tenant, as RLS decides. `search` matches the name, trading name and email.
|
|
134
|
+
*/
|
|
135
|
+
async list(query) {
|
|
136
|
+
const page = query.page;
|
|
137
|
+
const pageSize = query.page_size;
|
|
138
|
+
const offset = (page - 1) * pageSize;
|
|
139
|
+
const trimmedSearch = query.search?.trim();
|
|
140
|
+
// `order`/`range` return `this`, so the search can be reassigned onto the builder without a
|
|
141
|
+
// cast. Exact, not estimated, like invoices: there are tens of organizations, not tens of
|
|
142
|
+
// thousands, and the pager must not offer a page that is not there.
|
|
143
|
+
let organizationsQuery = this.supabase
|
|
144
|
+
.from('organizations')
|
|
145
|
+
.select(ORGANIZATION_LIST_SELECT, { count: 'exact' })
|
|
146
|
+
.order('created_at', { ascending: false })
|
|
147
|
+
.range(offset, offset + pageSize - 1);
|
|
148
|
+
if (trimmedSearch) {
|
|
149
|
+
organizationsQuery = organizationsQuery.or((0, transforms_1.orIlike)(['name', 'trading_name', 'email'], trimmedSearch));
|
|
150
|
+
}
|
|
151
|
+
const { data, error, count } = await organizationsQuery;
|
|
152
|
+
if (error)
|
|
153
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
154
|
+
return (0, transforms_1.toMmsList)(data, page, pageSize, count);
|
|
127
155
|
}
|
|
128
156
|
/**
|
|
129
157
|
* The whole row, unprojected. Billing reads a different handful of columns for each of its
|
|
@@ -7,6 +7,12 @@ export type PermissionFlags = {
|
|
|
7
7
|
is_owner: boolean;
|
|
8
8
|
is_admin: boolean;
|
|
9
9
|
} & Record<string, boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* A guard's flag is interpolated into a select string, so it is checked against the shape a
|
|
12
|
+
* column name can take first - a flag never comes from a request, only from a route's own
|
|
13
|
+
* decorator, so anything else is a bug rather than a bad request.
|
|
14
|
+
*/
|
|
15
|
+
export declare const checkPermissionFlag: (flag: string) => void;
|
|
10
16
|
/**
|
|
11
17
|
* The `permissions` table. Rows are created by the `on_auth_user_created` trigger and deleted
|
|
12
18
|
* with the user - INSERT and DELETE are `false` for `authenticated` - so this resource only
|
|
@@ -26,10 +32,6 @@ export declare class PermissionsResource {
|
|
|
26
32
|
* The one lookup `PermissionsGuard` makes per gated request: the flag the route asked for,
|
|
27
33
|
* plus the two that bypass it. Returns null when the caller has no permissions row, which the
|
|
28
34
|
* guard treats as a denial.
|
|
29
|
-
*
|
|
30
|
-
* `flag` is interpolated into the select string, so it is checked against the shape a column
|
|
31
|
-
* name can take first - a flag never comes from a request, only from a route's own decorator,
|
|
32
|
-
* so anything else is a bug rather than a bad request.
|
|
33
35
|
*/
|
|
34
36
|
flagsFor(userId: string, flag: string): Promise<PermissionFlags | null>;
|
|
35
37
|
/** Whether a user owns the organization - the guard behind "you cannot deactivate the owner". */
|
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.PermissionsResource = void 0;
|
|
17
|
+
exports.PermissionsResource = exports.checkPermissionFlag = void 0;
|
|
18
18
|
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
19
19
|
const audit_1 = require("../audit");
|
|
20
20
|
const errors_1 = require("../errors");
|
|
@@ -25,6 +25,21 @@ const PERMISSIONS_TABLE = {
|
|
|
25
25
|
// A permissions change shows in the history of the user it belongs to.
|
|
26
26
|
related: { entityType: enum_1.AuditTrailEntityType.USER, column: 'user_id' },
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* A guard's flag is interpolated into a select string, so it is checked against the shape a
|
|
30
|
+
* column name can take first - a flag never comes from a request, only from a route's own
|
|
31
|
+
* decorator, so anything else is a bug rather than a bad request.
|
|
32
|
+
*/
|
|
33
|
+
const checkPermissionFlag = (flag) => {
|
|
34
|
+
if (!/^[a-z][a-z0-9_]*$/.test(flag)) {
|
|
35
|
+
throw new errors_1.SupabaseClientError({
|
|
36
|
+
status: 500,
|
|
37
|
+
error: 'Internal Error',
|
|
38
|
+
message: 'Invalid permission flag',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
exports.checkPermissionFlag = checkPermissionFlag;
|
|
28
43
|
/**
|
|
29
44
|
* The `permissions` table. Rows are created by the `on_auth_user_created` trigger and deleted
|
|
30
45
|
* with the user - INSERT and DELETE are `false` for `authenticated` - so this resource only
|
|
@@ -58,19 +73,9 @@ class PermissionsResource {
|
|
|
58
73
|
* The one lookup `PermissionsGuard` makes per gated request: the flag the route asked for,
|
|
59
74
|
* plus the two that bypass it. Returns null when the caller has no permissions row, which the
|
|
60
75
|
* guard treats as a denial.
|
|
61
|
-
*
|
|
62
|
-
* `flag` is interpolated into the select string, so it is checked against the shape a column
|
|
63
|
-
* name can take first - a flag never comes from a request, only from a route's own decorator,
|
|
64
|
-
* so anything else is a bug rather than a bad request.
|
|
65
76
|
*/
|
|
66
77
|
async flagsFor(userId, flag) {
|
|
67
|
-
|
|
68
|
-
throw new errors_1.SupabaseClientError({
|
|
69
|
-
status: 500,
|
|
70
|
-
error: 'Internal Error',
|
|
71
|
-
message: 'Invalid permission flag',
|
|
72
|
-
});
|
|
73
|
-
}
|
|
78
|
+
(0, exports.checkPermissionFlag)(flag);
|
|
74
79
|
const { data, error } = await this.supabase
|
|
75
80
|
.from('permissions')
|
|
76
81
|
.select(`is_owner, is_admin, ${flag}`)
|
package/dist/projects/dto.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateProjectDto as CreateProjectDtoType, GetProjectsDto as GetProjectsDtoType, ProjectIdDto as ProjectIdDtoType, UpdateProjectDto as UpdateProjectDtoType } from '@managemint-solutions/entities/projects/dto';
|
|
1
|
+
import type { CreateProjectDto as CreateProjectDtoType, GetProjectsDto as GetProjectsDtoType, StaffGetProjectsDto as StaffGetProjectsDtoType, ProjectIdDto as ProjectIdDtoType, UpdateProjectDto as UpdateProjectDtoType } from '@managemint-solutions/entities/projects/dto';
|
|
2
2
|
export declare class GetProjectsDto implements Omit<GetProjectsDtoType, 'status_id_in' | 'status_id_not_in'> {
|
|
3
3
|
readonly search?: string | null;
|
|
4
4
|
readonly page: number;
|
|
@@ -15,6 +15,10 @@ export declare class GetProjectsDto implements Omit<GetProjectsDtoType, 'status_
|
|
|
15
15
|
readonly client_id?: string | null;
|
|
16
16
|
readonly archive: boolean;
|
|
17
17
|
}
|
|
18
|
+
/** A staff read across organizations, optionally narrowed to one of them. */
|
|
19
|
+
export declare class StaffGetProjectsDto extends GetProjectsDto implements Omit<StaffGetProjectsDtoType, 'status_id_in' | 'status_id_not_in'> {
|
|
20
|
+
readonly organization_id?: string | null;
|
|
21
|
+
}
|
|
18
22
|
export declare class ProjectIdDto implements ProjectIdDtoType {
|
|
19
23
|
readonly projectId: string;
|
|
20
24
|
}
|
package/dist/projects/dto.js
CHANGED
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.UpdateProjectDto = exports.CreateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = void 0;
|
|
12
|
+
exports.UpdateProjectDto = exports.CreateProjectDto = exports.ProjectIdDto = exports.StaffGetProjectsDto = exports.GetProjectsDto = void 0;
|
|
13
13
|
const class_validator_1 = require("class-validator");
|
|
14
14
|
const class_transformer_1 = require("class-transformer");
|
|
15
15
|
const transforms_1 = require("../transforms");
|
|
@@ -132,6 +132,17 @@ __decorate([
|
|
|
132
132
|
(0, class_validator_1.IsBoolean)({ message: 'Include archived must be a boolean' }),
|
|
133
133
|
__metadata("design:type", Boolean)
|
|
134
134
|
], GetProjectsDto.prototype, "archive", void 0);
|
|
135
|
+
/** A staff read across organizations, optionally narrowed to one of them. */
|
|
136
|
+
class StaffGetProjectsDto extends GetProjectsDto {
|
|
137
|
+
organization_id;
|
|
138
|
+
}
|
|
139
|
+
exports.StaffGetProjectsDto = StaffGetProjectsDto;
|
|
140
|
+
__decorate([
|
|
141
|
+
(0, class_validator_1.IsOptional)(),
|
|
142
|
+
(0, validators_1.IsNullable)(),
|
|
143
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid organization ID format' }),
|
|
144
|
+
__metadata("design:type", Object)
|
|
145
|
+
], StaffGetProjectsDto.prototype, "organization_id", void 0);
|
|
135
146
|
class ProjectIdDto {
|
|
136
147
|
projectId;
|
|
137
148
|
}
|
package/dist/projects/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
2
2
|
import type { ProjectEntity, ProjectSummary } from '@managemint-solutions/entities/projects';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ClientContext } from '../auth';
|
|
4
4
|
import { EntityAuditTrail, type AuditResource } from '../audit';
|
|
5
5
|
import type { TypedSupabaseClient } from '../client';
|
|
6
6
|
import type { CreateProjectDto, GetProjectsDto, UpdateProjectDto } from './dto';
|
|
@@ -11,8 +11,10 @@ export declare class ProjectsResource {
|
|
|
11
11
|
private readonly table;
|
|
12
12
|
/** This record's history - its own rows and the rows that hang off it. */
|
|
13
13
|
readonly audit: EntityAuditTrail;
|
|
14
|
-
constructor(supabase: TypedSupabaseClient, auth:
|
|
15
|
-
list(query: GetProjectsDto
|
|
14
|
+
constructor(supabase: TypedSupabaseClient, auth: ClientContext, audit: AuditResource);
|
|
15
|
+
list(query: GetProjectsDto & {
|
|
16
|
+
organization_id?: string | null;
|
|
17
|
+
}): Promise<MmsList<ProjectSummary>>;
|
|
16
18
|
getById(projectId: string): Promise<ProjectEntity>;
|
|
17
19
|
create(input: CreateProjectDto): Promise<ProjectEntity>;
|
|
18
20
|
update(projectId: string, input: UpdateProjectDto): Promise<ProjectEntity>;
|
package/dist/projects/index.js
CHANGED
|
@@ -18,6 +18,7 @@ exports.ProjectsResource = void 0;
|
|
|
18
18
|
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
19
19
|
const audit_1 = require("../audit");
|
|
20
20
|
const errors_1 = require("../errors");
|
|
21
|
+
const tenancy_1 = require("../tenancy");
|
|
21
22
|
const transforms_1 = require("../transforms");
|
|
22
23
|
__exportStar(require("./dto"), exports);
|
|
23
24
|
// The public contract is the shared ProjectSummary / ProjectEntity from
|
|
@@ -97,14 +98,16 @@ class ProjectsResource {
|
|
|
97
98
|
const trimmedSearch = query.search?.trim();
|
|
98
99
|
// `order`/`range` return `this`, so the builder stays a filter builder and the conditional
|
|
99
100
|
// filters below can be reassigned onto it without a cast.
|
|
100
|
-
let projectsQuery = this.supabase
|
|
101
|
+
let projectsQuery = (0, tenancy_1.scopeToOrganization)(this.supabase
|
|
101
102
|
.from('projects')
|
|
102
|
-
.select(PROJECT_LIST_SELECT, { count: 'estimated' })
|
|
103
|
-
.eq('organization_id', this.auth.organization_id)
|
|
103
|
+
.select((0, tenancy_1.withOrganization)(PROJECT_LIST_SELECT, this.auth), { count: 'estimated' }), this.auth)
|
|
104
104
|
.eq('archived', query.archive)
|
|
105
105
|
.eq('deleted', false)
|
|
106
106
|
.range(offset, offset + pageSize - 1)
|
|
107
107
|
.order('created_at', { ascending: false });
|
|
108
|
+
if (query.organization_id) {
|
|
109
|
+
projectsQuery = projectsQuery.eq('organization_id', query.organization_id);
|
|
110
|
+
}
|
|
108
111
|
if (trimmedSearch) {
|
|
109
112
|
projectsQuery = projectsQuery.or((0, transforms_1.orIlike)(['name', 'specification'], trimmedSearch));
|
|
110
113
|
}
|
|
@@ -147,10 +150,7 @@ class ProjectsResource {
|
|
|
147
150
|
return (0, transforms_1.toMmsList)(data, page, pageSize, count);
|
|
148
151
|
}
|
|
149
152
|
async getById(projectId) {
|
|
150
|
-
const { data, error } = await this.supabase
|
|
151
|
-
.from('projects')
|
|
152
|
-
.select(PROJECT_DETAIL_SELECT)
|
|
153
|
-
.eq('organization_id', this.auth.organization_id)
|
|
153
|
+
const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('projects').select(PROJECT_DETAIL_SELECT), this.auth)
|
|
154
154
|
.eq('mms_id', projectId)
|
|
155
155
|
.single();
|
|
156
156
|
if (error)
|
|
@@ -182,7 +182,7 @@ class ProjectsResource {
|
|
|
182
182
|
...input,
|
|
183
183
|
start_date: (0, transforms_1.toDateInput)(input.start_date),
|
|
184
184
|
end_date: (0, transforms_1.toDateInput)(input.end_date),
|
|
185
|
-
organization_id: this.auth
|
|
185
|
+
organization_id: (0, tenancy_1.requireOrganization)(this.auth),
|
|
186
186
|
created_by: this.auth.mms_id,
|
|
187
187
|
created_at: new Date().toISOString(),
|
|
188
188
|
});
|
|
@@ -4,6 +4,7 @@ import { BillingResource } from '../billing';
|
|
|
4
4
|
import { InvoicesResource } from '../billing/invoices';
|
|
5
5
|
import { ClientsResource } from '../clients';
|
|
6
6
|
import { ConfigsResource } from '../configs';
|
|
7
|
+
import { EmulationHandshakesResource } from '../emulation-handshakes';
|
|
7
8
|
import { FeatureRequestsResource } from '../feature-requests';
|
|
8
9
|
import { LeaveEntitlementsResource, LeaveRequestsResource, LeaveTypesResource } from '../leave';
|
|
9
10
|
import { NotificationsOutboxResource } from '../notifications';
|
|
@@ -11,6 +12,8 @@ import { OrganizationInvitesResource } from '../organization-invites';
|
|
|
11
12
|
import { OrganizationPaymentMethodsResource, type PaymentMethodLookupColumn } from '../organization-payment-methods';
|
|
12
13
|
import { OrganizationsResource, OrganizationSignupResource } from '../organizations';
|
|
13
14
|
import { PermissionsResource } from '../permissions';
|
|
15
|
+
import { StaffPermissionsResource, StaffResource } from '../staff';
|
|
16
|
+
import { UserApiCredentialsResource } from '../user-api-credentials';
|
|
14
17
|
import { UsersResource } from '../users';
|
|
15
18
|
import { WebhookEventsResource } from '../webhook-events';
|
|
16
19
|
export type SupabaseServiceClientConfig = {
|
|
@@ -61,7 +64,8 @@ export declare class SupabaseOrganizationScope {
|
|
|
61
64
|
/**
|
|
62
65
|
* The client for work that has no caller and no RLS context to run under: inbound webhooks
|
|
63
66
|
* (Paystack, Resend, the Supabase `send_email` hook), the notification queue worker, unauthenticated
|
|
64
|
-
* signup, the website's feature-request intake,
|
|
67
|
+
* signup, the website's feature-request intake, the admin api's writes (staff, their flags, user
|
|
68
|
+
* api credentials, the emulation mint), and the writes RLS reserves for the service role
|
|
65
69
|
* outright - the ledger, the outbox, the webhook event logs, `organizations` INSERT, `permissions`
|
|
66
70
|
* INSERT/DELETE.
|
|
67
71
|
*
|
|
@@ -78,7 +82,34 @@ export declare class SupabaseServiceClient {
|
|
|
78
82
|
readonly signup: OrganizationSignupResource;
|
|
79
83
|
readonly paystackEvents: WebhookEventsResource;
|
|
80
84
|
readonly resendEvents: WebhookEventsResource;
|
|
85
|
+
readonly staff: StaffResource;
|
|
86
|
+
readonly staffPermissions: StaffPermissionsResource;
|
|
87
|
+
readonly userApiCredentials: UserApiCredentialsResource;
|
|
88
|
+
readonly emulationHandshakes: EmulationHandshakesResource;
|
|
81
89
|
constructor(config: SupabaseServiceClientConfig);
|
|
90
|
+
/**
|
|
91
|
+
* A one-time `token_hash` that signs `email` in - the admin api verifies it itself, at once,
|
|
92
|
+
* through `SupabaseAuthClient.exchangeMagicLinkToken`. Nothing is sent: the link only exists
|
|
93
|
+
* to be verified.
|
|
94
|
+
*/
|
|
95
|
+
mintMagicLink(email: string): Promise<{
|
|
96
|
+
token_hash: string;
|
|
97
|
+
}>;
|
|
98
|
+
/**
|
|
99
|
+
* The auth user behind a staff pre-registration (`staff.preRegister`), confirmed and with the
|
|
100
|
+
* `staff` role - which is what links the two (`handle_auth_user_role_set`).
|
|
101
|
+
*/
|
|
102
|
+
createStaffAuthUser(input: {
|
|
103
|
+
email: string;
|
|
104
|
+
password: string;
|
|
105
|
+
}): Promise<{
|
|
106
|
+
id: string;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* Bans or unbans an auth user, so a deactivated user or staff member cannot sign in or refresh.
|
|
110
|
+
* A ban has no "forever" in GoTrue; a hundred years is the usual stand-in.
|
|
111
|
+
*/
|
|
112
|
+
setAuthUserBanned(userId: string, banned: boolean): Promise<void>;
|
|
82
113
|
/**
|
|
83
114
|
* Scope to one organization once it is known. `actorMmsId` stays `null` for system work -
|
|
84
115
|
* `audit_logs.created_by` is nullable precisely so an unattributed write is still recorded.
|
|
@@ -8,6 +8,7 @@ const billing_1 = require("../billing");
|
|
|
8
8
|
const invoices_1 = require("../billing/invoices");
|
|
9
9
|
const clients_1 = require("../clients");
|
|
10
10
|
const configs_1 = require("../configs");
|
|
11
|
+
const emulation_handshakes_1 = require("../emulation-handshakes");
|
|
11
12
|
const feature_requests_1 = require("../feature-requests");
|
|
12
13
|
const leave_1 = require("../leave");
|
|
13
14
|
const notifications_1 = require("../notifications");
|
|
@@ -16,6 +17,8 @@ const organization_invites_1 = require("../organization-invites");
|
|
|
16
17
|
const organization_payment_methods_1 = require("../organization-payment-methods");
|
|
17
18
|
const organizations_1 = require("../organizations");
|
|
18
19
|
const permissions_1 = require("../permissions");
|
|
20
|
+
const staff_1 = require("../staff");
|
|
21
|
+
const user_api_credentials_1 = require("../user-api-credentials");
|
|
19
22
|
const users_1 = require("../users");
|
|
20
23
|
const webhook_events_1 = require("../webhook-events");
|
|
21
24
|
/**
|
|
@@ -91,7 +94,8 @@ exports.SupabaseOrganizationScope = SupabaseOrganizationScope;
|
|
|
91
94
|
/**
|
|
92
95
|
* The client for work that has no caller and no RLS context to run under: inbound webhooks
|
|
93
96
|
* (Paystack, Resend, the Supabase `send_email` hook), the notification queue worker, unauthenticated
|
|
94
|
-
* signup, the website's feature-request intake,
|
|
97
|
+
* signup, the website's feature-request intake, the admin api's writes (staff, their flags, user
|
|
98
|
+
* api credentials, the emulation mint), and the writes RLS reserves for the service role
|
|
95
99
|
* outright - the ledger, the outbox, the webhook event logs, `organizations` INSERT, `permissions`
|
|
96
100
|
* INSERT/DELETE.
|
|
97
101
|
*
|
|
@@ -111,6 +115,10 @@ class SupabaseServiceClient {
|
|
|
111
115
|
signup;
|
|
112
116
|
paystackEvents;
|
|
113
117
|
resendEvents;
|
|
118
|
+
staff;
|
|
119
|
+
staffPermissions;
|
|
120
|
+
userApiCredentials;
|
|
121
|
+
emulationHandshakes;
|
|
114
122
|
constructor(config) {
|
|
115
123
|
this.config = config;
|
|
116
124
|
this.supabase = (0, auth_1.createRawClient)(config.url, config.serviceRoleKey);
|
|
@@ -125,6 +133,51 @@ class SupabaseServiceClient {
|
|
|
125
133
|
table: 'notifications',
|
|
126
134
|
entityType: enum_1.AuditTrailEntityType.NOTIFICATION,
|
|
127
135
|
}));
|
|
136
|
+
this.staff = new staff_1.StaffResource(this.supabase);
|
|
137
|
+
this.staffPermissions = new staff_1.StaffPermissionsResource(this.supabase);
|
|
138
|
+
this.userApiCredentials = new user_api_credentials_1.UserApiCredentialsResource(this.supabase, (organizationId) => this.forOrganization(organizationId).table({
|
|
139
|
+
table: 'user_api_credentials',
|
|
140
|
+
entityType: enum_1.AuditTrailEntityType.USER_API_CREDENTIAL,
|
|
141
|
+
related: { entityType: enum_1.AuditTrailEntityType.USER, column: 'user_id' },
|
|
142
|
+
}));
|
|
143
|
+
this.emulationHandshakes = new emulation_handshakes_1.EmulationHandshakesResource(this.supabase);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* A one-time `token_hash` that signs `email` in - the admin api verifies it itself, at once,
|
|
147
|
+
* through `SupabaseAuthClient.exchangeMagicLinkToken`. Nothing is sent: the link only exists
|
|
148
|
+
* to be verified.
|
|
149
|
+
*/
|
|
150
|
+
async mintMagicLink(email) {
|
|
151
|
+
const { data, error } = await this.supabase.auth.admin.generateLink({ type: 'magiclink', email });
|
|
152
|
+
if (error)
|
|
153
|
+
throw (0, errors_1.mapAuthError)(error);
|
|
154
|
+
return { token_hash: data.properties.hashed_token };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The auth user behind a staff pre-registration (`staff.preRegister`), confirmed and with the
|
|
158
|
+
* `staff` role - which is what links the two (`handle_auth_user_role_set`).
|
|
159
|
+
*/
|
|
160
|
+
async createStaffAuthUser(input) {
|
|
161
|
+
const { data, error } = await this.supabase.auth.admin.createUser({
|
|
162
|
+
email: input.email,
|
|
163
|
+
password: input.password,
|
|
164
|
+
email_confirm: true,
|
|
165
|
+
role: 'staff',
|
|
166
|
+
});
|
|
167
|
+
if (error)
|
|
168
|
+
throw (0, errors_1.mapAuthError)(error);
|
|
169
|
+
return { id: data.user.id };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Bans or unbans an auth user, so a deactivated user or staff member cannot sign in or refresh.
|
|
173
|
+
* A ban has no "forever" in GoTrue; a hundred years is the usual stand-in.
|
|
174
|
+
*/
|
|
175
|
+
async setAuthUserBanned(userId, banned) {
|
|
176
|
+
const { error } = await this.supabase.auth.admin.updateUserById(userId, {
|
|
177
|
+
ban_duration: banned ? '876000h' : 'none',
|
|
178
|
+
});
|
|
179
|
+
if (error)
|
|
180
|
+
throw (0, errors_1.mapAuthError)(error);
|
|
128
181
|
}
|
|
129
182
|
/**
|
|
130
183
|
* Scope to one organization once it is known. `actorMmsId` stays `null` for system work -
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CreateStaffDto as CreateStaffDtoType, StaffIdDto as StaffIdDtoType, UpdateStaffPermissionsDto as UpdateStaffPermissionsDtoType } from '@managemint-solutions/entities/staff/dto';
|
|
2
|
+
import { UpdatePermissionsDto } from '../permissions/dto';
|
|
3
|
+
export declare class StaffIdDto implements StaffIdDtoType {
|
|
4
|
+
readonly staffId: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class CreateStaffDto implements CreateStaffDtoType {
|
|
7
|
+
readonly email: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly password: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class UpdateStaffPermissionsDto extends UpdatePermissionsDto implements UpdateStaffPermissionsDtoType {
|
|
12
|
+
readonly is_admin: boolean;
|
|
13
|
+
readonly view_dashboard: boolean;
|
|
14
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.UpdateStaffPermissionsDto = exports.CreateStaffDto = exports.StaffIdDto = void 0;
|
|
13
|
+
const class_validator_1 = require("class-validator");
|
|
14
|
+
const dto_1 = require("../permissions/dto");
|
|
15
|
+
const validators_1 = require("../validators");
|
|
16
|
+
class StaffIdDto {
|
|
17
|
+
// v4: a staff member's `mms_id` is their GoTrue auth uid, as a user's is (see UserIdDto).
|
|
18
|
+
staffId;
|
|
19
|
+
}
|
|
20
|
+
exports.StaffIdDto = StaffIdDto;
|
|
21
|
+
__decorate([
|
|
22
|
+
(0, class_validator_1.IsUUID)('4', { message: 'Invalid staff ID format' }),
|
|
23
|
+
__metadata("design:type", String)
|
|
24
|
+
], StaffIdDto.prototype, "staffId", void 0);
|
|
25
|
+
// The email and password rules are AddUserDto's: a staff member is a GoTrue user like any other.
|
|
26
|
+
class CreateStaffDto {
|
|
27
|
+
email;
|
|
28
|
+
name;
|
|
29
|
+
password;
|
|
30
|
+
}
|
|
31
|
+
exports.CreateStaffDto = CreateStaffDto;
|
|
32
|
+
__decorate([
|
|
33
|
+
(0, class_validator_1.IsEmail)({}, { message: 'Invalid email address' }),
|
|
34
|
+
__metadata("design:type", String)
|
|
35
|
+
], CreateStaffDto.prototype, "email", void 0);
|
|
36
|
+
__decorate([
|
|
37
|
+
(0, class_validator_1.IsString)({ message: 'Name is required' }),
|
|
38
|
+
(0, class_validator_1.MinLength)(1, { message: 'Name is required' }),
|
|
39
|
+
(0, class_validator_1.MaxLength)(100, { message: 'Name cannot be longer than 100 characters' }),
|
|
40
|
+
__metadata("design:type", String)
|
|
41
|
+
], CreateStaffDto.prototype, "name", void 0);
|
|
42
|
+
__decorate([
|
|
43
|
+
(0, class_validator_1.IsString)({ message: 'Password is required' }),
|
|
44
|
+
(0, class_validator_1.MinLength)(8, { message: 'Password must be at least 8 characters' }),
|
|
45
|
+
(0, validators_1.MatchesPasswordRules)(),
|
|
46
|
+
__metadata("design:type", String)
|
|
47
|
+
], CreateStaffDto.prototype, "password", void 0);
|
|
48
|
+
// The tenant flag set plus the two columns a staff row carries that it leaves out: admin, which
|
|
49
|
+
// staff grant through the api (the service refuses self), and the dashboard flag, so a row read
|
|
50
|
+
// back from GET can be sent as is. There is no owner.
|
|
51
|
+
class UpdateStaffPermissionsDto extends dto_1.UpdatePermissionsDto {
|
|
52
|
+
is_admin;
|
|
53
|
+
view_dashboard;
|
|
54
|
+
}
|
|
55
|
+
exports.UpdateStaffPermissionsDto = UpdateStaffPermissionsDto;
|
|
56
|
+
__decorate([
|
|
57
|
+
(0, class_validator_1.IsBoolean)({ message: 'Admin must be a boolean' }),
|
|
58
|
+
__metadata("design:type", Boolean)
|
|
59
|
+
], UpdateStaffPermissionsDto.prototype, "is_admin", void 0);
|
|
60
|
+
__decorate([
|
|
61
|
+
(0, class_validator_1.IsBoolean)({ message: 'View Dashboard must be a boolean' }),
|
|
62
|
+
__metadata("design:type", Boolean)
|
|
63
|
+
], UpdateStaffPermissionsDto.prototype, "view_dashboard", void 0);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StaffEntity, StaffPermissionsEntity } from '@managemint-solutions/entities/staff';
|
|
2
|
+
import type { UpdateStaffPermissionsDto } from '@managemint-solutions/entities/staff/dto';
|
|
3
|
+
import type { TypedSupabaseClient } from '../client';
|
|
4
|
+
export * from './dto';
|
|
5
|
+
/** Admin bypasses every individual flag, so the guard reads both at once. */
|
|
6
|
+
type StaffPermissionFlags = {
|
|
7
|
+
is_admin: boolean;
|
|
8
|
+
} & Record<string, boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* The `staff` table. Staff read it on their own client; every write is the service role's (staff
|
|
11
|
+
* hold SELECT only), so `preRegister`, `removePending` and `setActive` belong to the service
|
|
12
|
+
* client. Unaudited: an `audit_logs` row belongs to an organization, and staff have none.
|
|
13
|
+
*/
|
|
14
|
+
export declare class StaffResource {
|
|
15
|
+
private readonly supabase;
|
|
16
|
+
constructor(supabase: TypedSupabaseClient);
|
|
17
|
+
/** Every staff member, by name. A pending pre-registration is not one yet. */
|
|
18
|
+
list(): Promise<StaffEntity[]>;
|
|
19
|
+
getById(staffId: string): Promise<StaffEntity>;
|
|
20
|
+
/**
|
|
21
|
+
* The address, written before its auth user exists - the first half of creating a staff
|
|
22
|
+
* member. `mms_id` stays null until GoTrue creates the auth user with the `staff` role and
|
|
23
|
+
* `handle_auth_user_role_set` links the two.
|
|
24
|
+
*/
|
|
25
|
+
preRegister(input: {
|
|
26
|
+
email: string;
|
|
27
|
+
name: string;
|
|
28
|
+
created_by: string;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
/** Undoes a pre-registration whose auth user was never created. A linked row is never touched. */
|
|
31
|
+
removePending(email: string): Promise<void>;
|
|
32
|
+
/** Deactivates, stamping who and when, or reactivates, clearing them. */
|
|
33
|
+
setActive(staffId: string, active: boolean, by: string): Promise<StaffEntity>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The `staff_permissions` table: one row per staff member, made by the role trigger alongside
|
|
37
|
+
* the link. Staff read it on their own client; `updateForStaff` is the service client's.
|
|
38
|
+
*/
|
|
39
|
+
export declare class StaffPermissionsResource {
|
|
40
|
+
private readonly supabase;
|
|
41
|
+
constructor(supabase: TypedSupabaseClient);
|
|
42
|
+
/**
|
|
43
|
+
* The one lookup the admin api's guard makes per gated request: the flag the route asked for,
|
|
44
|
+
* plus the admin bypass. Null when there is no row, which the guard treats as a denial.
|
|
45
|
+
*/
|
|
46
|
+
flagsFor(staffId: string, flag: string): Promise<StaffPermissionFlags | null>;
|
|
47
|
+
/** The whole row. `*`: every flag, so a new flag column needs no select edit. */
|
|
48
|
+
getForStaff(staffId: string): Promise<StaffPermissionsEntity>;
|
|
49
|
+
updateForStaff(staffId: string, patch: UpdateStaffPermissionsDto, by: string): Promise<StaffPermissionsEntity>;
|
|
50
|
+
}
|