@ampless/backend 0.2.0-alpha.2 → 0.2.0-alpha.3
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/auth/user-admin.d.ts +17 -0
- package/dist/auth/user-admin.js +105 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +49 -3
- package/package.json +6 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AppSyncResolverHandler } from 'aws-lambda';
|
|
2
|
+
|
|
3
|
+
type AdminRole = 'admin' | 'editor' | 'none';
|
|
4
|
+
interface AdminUserDto {
|
|
5
|
+
userId: string;
|
|
6
|
+
email: string;
|
|
7
|
+
role: AdminRole;
|
|
8
|
+
}
|
|
9
|
+
interface ListArgs {
|
|
10
|
+
}
|
|
11
|
+
interface SetArgs {
|
|
12
|
+
userId: string;
|
|
13
|
+
role: string;
|
|
14
|
+
}
|
|
15
|
+
declare const handler: AppSyncResolverHandler<ListArgs | SetArgs, AdminUserDto | AdminUserDto[] | null>;
|
|
16
|
+
|
|
17
|
+
export { handler };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/auth/user-admin.ts
|
|
2
|
+
import {
|
|
3
|
+
CognitoIdentityProviderClient,
|
|
4
|
+
ListUsersCommand,
|
|
5
|
+
AdminListGroupsForUserCommand,
|
|
6
|
+
AdminAddUserToGroupCommand,
|
|
7
|
+
AdminRemoveUserFromGroupCommand
|
|
8
|
+
} from "@aws-sdk/client-cognito-identity-provider";
|
|
9
|
+
var cognito = new CognitoIdentityProviderClient({});
|
|
10
|
+
var ADMIN_GROUP = "ampless-admin";
|
|
11
|
+
var EDITOR_GROUP = "ampless-editor";
|
|
12
|
+
function requireUserPoolId() {
|
|
13
|
+
const v = process.env.AMPLESS_USER_POOL_ID;
|
|
14
|
+
if (!v) {
|
|
15
|
+
console.error("[user-admin] missing required env var AMPLESS_USER_POOL_ID");
|
|
16
|
+
throw new Error("user-admin: missing required env var AMPLESS_USER_POOL_ID");
|
|
17
|
+
}
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
function emailOf(user) {
|
|
21
|
+
const attr = user.Attributes?.find((a) => a.Name === "email");
|
|
22
|
+
return attr?.Value ?? "";
|
|
23
|
+
}
|
|
24
|
+
async function roleOf(userPoolId, username) {
|
|
25
|
+
const { Groups } = await cognito.send(
|
|
26
|
+
new AdminListGroupsForUserCommand({ UserPoolId: userPoolId, Username: username })
|
|
27
|
+
);
|
|
28
|
+
const names = (Groups ?? []).map((g) => g.GroupName);
|
|
29
|
+
if (names.includes(ADMIN_GROUP)) return "admin";
|
|
30
|
+
if (names.includes(EDITOR_GROUP)) return "editor";
|
|
31
|
+
return "none";
|
|
32
|
+
}
|
|
33
|
+
async function listAdminUsers(userPoolId) {
|
|
34
|
+
const { Users } = await cognito.send(
|
|
35
|
+
new ListUsersCommand({ UserPoolId: userPoolId, Limit: 60 })
|
|
36
|
+
);
|
|
37
|
+
const users = Users ?? [];
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const u of users) {
|
|
40
|
+
if (!u.Username) continue;
|
|
41
|
+
const role = await roleOf(userPoolId, u.Username);
|
|
42
|
+
out.push({ userId: u.Username, email: emailOf(u), role });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
async function setAdminUserRole(userPoolId, userId, role) {
|
|
47
|
+
for (const group of [ADMIN_GROUP, EDITOR_GROUP]) {
|
|
48
|
+
await cognito.send(
|
|
49
|
+
new AdminRemoveUserFromGroupCommand({
|
|
50
|
+
UserPoolId: userPoolId,
|
|
51
|
+
Username: userId,
|
|
52
|
+
GroupName: group
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (role !== "none") {
|
|
57
|
+
await cognito.send(
|
|
58
|
+
new AdminAddUserToGroupCommand({
|
|
59
|
+
UserPoolId: userPoolId,
|
|
60
|
+
Username: userId,
|
|
61
|
+
GroupName: role === "admin" ? ADMIN_GROUP : EDITOR_GROUP
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const { Users } = await cognito.send(
|
|
66
|
+
new ListUsersCommand({
|
|
67
|
+
UserPoolId: userPoolId,
|
|
68
|
+
Filter: `sub = "${userId}"`,
|
|
69
|
+
Limit: 1
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
const user = Users?.[0];
|
|
73
|
+
const email = user ? emailOf(user) : "";
|
|
74
|
+
const finalRole = await roleOf(userPoolId, userId);
|
|
75
|
+
return { userId, email, role: finalRole };
|
|
76
|
+
}
|
|
77
|
+
var handler = async (event) => {
|
|
78
|
+
const userPoolId = requireUserPoolId();
|
|
79
|
+
const field = event.info.fieldName;
|
|
80
|
+
try {
|
|
81
|
+
if (field === "listAdminUsers") {
|
|
82
|
+
return await listAdminUsers(userPoolId);
|
|
83
|
+
}
|
|
84
|
+
if (field === "setAdminUserRole") {
|
|
85
|
+
const { userId, role } = event.arguments;
|
|
86
|
+
if (role !== "admin" && role !== "editor" && role !== "none") {
|
|
87
|
+
console.error(`[user-admin] invalid role: ${role}`);
|
|
88
|
+
throw new Error(`Invalid role: ${role}`);
|
|
89
|
+
}
|
|
90
|
+
if (!userId) {
|
|
91
|
+
console.error("[user-admin] missing userId argument");
|
|
92
|
+
throw new Error("Missing userId argument");
|
|
93
|
+
}
|
|
94
|
+
return await setAdminUserRole(userPoolId, userId, role);
|
|
95
|
+
}
|
|
96
|
+
console.error(`[user-admin] unsupported fieldName: ${field}`);
|
|
97
|
+
throw new Error(`Unsupported fieldName: ${field}`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`[user-admin] ${field} failed:`, err);
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
export {
|
|
104
|
+
handler
|
|
105
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface DefineAmplessBackendOpts {
|
|
|
13
13
|
processorTrusted: FunctionResource;
|
|
14
14
|
processorUntrusted: FunctionResource;
|
|
15
15
|
apiKeyRenewer: FunctionResource;
|
|
16
|
+
userAdmin: FunctionResource;
|
|
16
17
|
}
|
|
17
18
|
type AmplessBackend = any;
|
|
18
19
|
/**
|
|
@@ -136,6 +137,17 @@ interface AmplessSchemaModelsOpts {
|
|
|
136
137
|
* they must point to actual `.js` files inside the user's project.
|
|
137
138
|
*/
|
|
138
139
|
resolverPaths?: Partial<AmplessResolverPaths>;
|
|
140
|
+
/**
|
|
141
|
+
* Optional Amplify `defineFunction` ref backing the user-admin
|
|
142
|
+
* AppSync ops (`listAdminUsers` query, `setAdminUserRole` mutation).
|
|
143
|
+
* When supplied, the corresponding `AdminUser` customType + the two
|
|
144
|
+
* ops are added to the schema, both gated to `ampless-admin`.
|
|
145
|
+
*
|
|
146
|
+
* Typed as `unknown` because `defineFunction`'s return type carries
|
|
147
|
+
* internal pnpm paths that don't survive declaration emit — same
|
|
148
|
+
* pattern as `AmplessAuthConfigOpts.postConfirmation`.
|
|
149
|
+
*/
|
|
150
|
+
userAdminFunction?: unknown;
|
|
139
151
|
}
|
|
140
152
|
/**
|
|
141
153
|
* The Ampless model + custom query definitions, returned as a plain
|
|
@@ -153,6 +165,9 @@ interface AmplessSchemaModelsOpts {
|
|
|
153
165
|
* extra models.
|
|
154
166
|
*/
|
|
155
167
|
declare function amplessSchemaModels(a: any, opts?: AmplessSchemaModelsOpts): {
|
|
168
|
+
AdminUser?: any;
|
|
169
|
+
listAdminUsers?: any;
|
|
170
|
+
setAdminUserRole?: any;
|
|
156
171
|
Post: any;
|
|
157
172
|
Page: any;
|
|
158
173
|
Media: any;
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,8 @@ function defineAmplessBackend(opts) {
|
|
|
16
16
|
eventDispatcher: opts.eventDispatcher,
|
|
17
17
|
processorTrusted: opts.processorTrusted,
|
|
18
18
|
processorUntrusted: opts.processorUntrusted,
|
|
19
|
-
apiKeyRenewer: opts.apiKeyRenewer
|
|
19
|
+
apiKeyRenewer: opts.apiKeyRenewer,
|
|
20
|
+
userAdmin: opts.userAdmin
|
|
20
21
|
});
|
|
21
22
|
const cfnBucket = backend.storage.resources.cfnResources.cfnBucket;
|
|
22
23
|
cfnBucket.publicAccessBlockConfiguration = {
|
|
@@ -61,6 +62,22 @@ function defineAmplessBackend(opts) {
|
|
|
61
62
|
resources: ["arn:aws:cognito-idp:*:*:userpool/*"]
|
|
62
63
|
})
|
|
63
64
|
);
|
|
65
|
+
backend.userAdmin.resources.lambda.addToRolePolicy(
|
|
66
|
+
new PolicyStatement({
|
|
67
|
+
effect: Effect.ALLOW,
|
|
68
|
+
actions: [
|
|
69
|
+
"cognito-idp:ListUsers",
|
|
70
|
+
"cognito-idp:AdminListGroupsForUser",
|
|
71
|
+
"cognito-idp:AdminAddUserToGroup",
|
|
72
|
+
"cognito-idp:AdminRemoveUserFromGroup"
|
|
73
|
+
],
|
|
74
|
+
resources: ["arn:aws:cognito-idp:*:*:userpool/*"]
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
backend.userAdmin.resources.lambda.addEnvironment(
|
|
78
|
+
"AMPLESS_USER_POOL_ID",
|
|
79
|
+
backend.auth.resources.userPool.userPoolId
|
|
80
|
+
);
|
|
64
81
|
const postTable = backend.data.resources.tables["Post"];
|
|
65
82
|
const cfnPostTable = backend.data.resources.cfnResources.amplifyDynamoDbTables["Post"];
|
|
66
83
|
cfnPostTable.streamSpecification = { streamViewType: "NEW_AND_OLD_IMAGES" };
|
|
@@ -206,6 +223,14 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
206
223
|
status: a.enum(["draft", "published"]),
|
|
207
224
|
publishedAt: a.datetime(),
|
|
208
225
|
tags: a.string().array(),
|
|
226
|
+
// Free-form per-post metadata (JSON). Reserved well-known keys
|
|
227
|
+
// are documented on `PostMetadata` in `ampless/src/types.ts`.
|
|
228
|
+
// Currently:
|
|
229
|
+
// no_layout: boolean — serve the post as bare HTML (no theme
|
|
230
|
+
// chrome). The runtime's post dispatcher redirects to the
|
|
231
|
+
// raw route handler when this is true.
|
|
232
|
+
// Other keys are passed through unchanged for plugin / app use.
|
|
233
|
+
metadata: a.json(),
|
|
209
234
|
// Denormalized GSI keys — set by every write path (admin client,
|
|
210
235
|
// MCP tools). Same pattern as siteIdStatus: composing the
|
|
211
236
|
// partition key as a single string lets each public-read query
|
|
@@ -308,7 +333,8 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
308
333
|
body: a.json(),
|
|
309
334
|
status: a.string(),
|
|
310
335
|
publishedAt: a.datetime(),
|
|
311
|
-
tags: a.string().array()
|
|
336
|
+
tags: a.string().array(),
|
|
337
|
+
metadata: a.json()
|
|
312
338
|
}),
|
|
313
339
|
// Paginated wrapper for list responses.
|
|
314
340
|
PublicPostConnection: a.customType({
|
|
@@ -367,7 +393,27 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
367
393
|
).authorization((allow) => [
|
|
368
394
|
allow.publicApiKey(),
|
|
369
395
|
allow.groups(["ampless-admin", "ampless-editor"])
|
|
370
|
-
])
|
|
396
|
+
]),
|
|
397
|
+
// User management ops, only wired when the caller supplies a
|
|
398
|
+
// Lambda function ref. Conditionally spread because
|
|
399
|
+
// `a.handler.function(undefined)` is not a valid call — projects
|
|
400
|
+
// that haven't opted into the user-admin Lambda must not see
|
|
401
|
+
// these schema entries.
|
|
402
|
+
...opts.userAdminFunction ? {
|
|
403
|
+
AdminUser: a.customType({
|
|
404
|
+
userId: a.string().required(),
|
|
405
|
+
email: a.string().required(),
|
|
406
|
+
// 'admin' | 'editor' | 'none' — stored as string because
|
|
407
|
+
// a.enum() in customType doesn't round-trip cleanly across
|
|
408
|
+
// AppSync's typegen + the admin client cast pattern.
|
|
409
|
+
role: a.string().required()
|
|
410
|
+
}),
|
|
411
|
+
listAdminUsers: a.query().returns(a.ref("AdminUser").array()).handler(a.handler.function(opts.userAdminFunction)).authorization((allow) => [allow.groups(["ampless-admin"])]),
|
|
412
|
+
setAdminUserRole: a.mutation().arguments({
|
|
413
|
+
userId: a.string().required(),
|
|
414
|
+
role: a.string().required()
|
|
415
|
+
}).returns(a.ref("AdminUser")).handler(a.handler.function(opts.userAdminFunction)).authorization((allow) => [allow.groups(["ampless-admin"])])
|
|
416
|
+
} : {}
|
|
371
417
|
};
|
|
372
418
|
}
|
|
373
419
|
function extendAmplessSchema(a, custom, opts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/backend",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.3",
|
|
4
4
|
"description": "Amplify Gen 2 backend factories for ampless: auth, data, storage, event processors, API key renewer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"import": "./dist/auth/post-confirmation.js",
|
|
14
14
|
"types": "./dist/auth/post-confirmation.d.ts"
|
|
15
15
|
},
|
|
16
|
+
"./auth/user-admin": {
|
|
17
|
+
"import": "./dist/auth/user-admin.js",
|
|
18
|
+
"types": "./dist/auth/user-admin.d.ts"
|
|
19
|
+
},
|
|
16
20
|
"./events/dispatcher": {
|
|
17
21
|
"import": "./dist/events/dispatcher.js",
|
|
18
22
|
"types": "./dist/events/dispatcher.d.ts"
|
|
@@ -52,7 +56,7 @@
|
|
|
52
56
|
"@aws-sdk/client-sqs": "^3.717.0",
|
|
53
57
|
"@aws-sdk/lib-dynamodb": "^3.717.0",
|
|
54
58
|
"@aws-sdk/util-dynamodb": "^3.717.0",
|
|
55
|
-
"ampless": "0.2.0-alpha.
|
|
59
|
+
"ampless": "0.2.0-alpha.2"
|
|
56
60
|
},
|
|
57
61
|
"peerDependencies": {
|
|
58
62
|
"@aws-amplify/backend": "^1",
|