@adatechnology/keycloak-admin 1.0.0-rc.0 → 1.0.0-rc.2
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 +11 -2
- package/dist/index.d.ts +63 -1
- package/dist/index.js +53 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,8 +32,17 @@ const { id } = await keycloak.createUser({
|
|
|
32
32
|
})
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Operações: `createUser`, `findUserByEmail`, `
|
|
36
|
-
`deleteUser`, `setPassword`, `setTemporaryPassword`.
|
|
35
|
+
Operações de usuário: `createUser`, `findUserByEmail`, `listUsers`, `updateUser`, `setEnabled`,
|
|
36
|
+
`updateAttributes`, `deleteUser`, `setPassword`, `setTemporaryPassword`.
|
|
37
|
+
|
|
38
|
+
Operações de grupo: `createGroup`, `updateGroup`, `deleteGroup`, `listGroups`, `addUserToGroup`,
|
|
39
|
+
`removeUserFromGroup`. Só o **primeiro nível** — grupo aninhado muda o significado de "pertencer",
|
|
40
|
+
porque quem está no filho herda o pai, e um produto que não modela hierarquia não deve criá-la por
|
|
41
|
+
acidente. A filiação é endereçada pelo **usuário** (`/users/{id}/groups/{id}`), não pelo grupo.
|
|
42
|
+
|
|
43
|
+
`listUsers({ first, limit, search })` devolve `{ users, hasMore }`. O realm não informa total, então
|
|
44
|
+
a página pede um registro a mais que o limite e descarta-o: é assim que `hasMore` sai sem uma
|
|
45
|
+
segunda chamada.
|
|
37
46
|
|
|
38
47
|
## Token
|
|
39
48
|
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,12 @@ type BuildKeycloakAdminEndpointsParams = {
|
|
|
16
16
|
readonly realm: string;
|
|
17
17
|
};
|
|
18
18
|
declare function buildKeycloakAdminEndpoints({ baseUrl, realm }: BuildKeycloakAdminEndpointsParams): {
|
|
19
|
+
readonly group: (groupId: string) => string;
|
|
20
|
+
readonly groups: string;
|
|
19
21
|
readonly token: `${string}/realms/${string}/protocol/openid-connect/token`;
|
|
20
22
|
readonly user: (userId: string) => string;
|
|
23
|
+
/** O Admin API endereça a filiação pelo usuário, não pelo grupo: é `PUT` para entrar, `DELETE` para sair. */
|
|
24
|
+
readonly userGroup: (userId: string, groupId: string) => string;
|
|
21
25
|
readonly userPassword: (userId: string) => string;
|
|
22
26
|
readonly users: string;
|
|
23
27
|
};
|
|
@@ -88,6 +92,19 @@ type CreateUserResult = {
|
|
|
88
92
|
type FindUserByEmailParams = {
|
|
89
93
|
readonly email: string;
|
|
90
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* O realm não devolve página infinita: `first`/`max` são o recorte que o Keycloak entende, e quem
|
|
97
|
+
* chama precisa saber se ainda há mais — daí `hasMore`, derivado de pedir um a mais que o limite.
|
|
98
|
+
*/
|
|
99
|
+
type ListUsersParams = {
|
|
100
|
+
readonly first?: number;
|
|
101
|
+
readonly limit?: number;
|
|
102
|
+
readonly search?: string;
|
|
103
|
+
};
|
|
104
|
+
type ListUsersResult = {
|
|
105
|
+
readonly hasMore: boolean;
|
|
106
|
+
readonly users: readonly KeycloakUser[];
|
|
107
|
+
};
|
|
91
108
|
type UpdateUserParams = {
|
|
92
109
|
readonly user: Readonly<Partial<Pick<KeycloakUser, 'email' | 'emailVerified' | 'firstName' | 'lastName' | 'username'>>>;
|
|
93
110
|
readonly userId: string;
|
|
@@ -112,10 +129,55 @@ type SetTemporaryPasswordParams = {
|
|
|
112
129
|
readonly password: string;
|
|
113
130
|
readonly userId: string;
|
|
114
131
|
};
|
|
132
|
+
/**
|
|
133
|
+
* O grupo do realm. O Keycloak aceita hierarquia (`subGroups`), e este cliente trata só o primeiro
|
|
134
|
+
* nível de propósito: grupo aninhado muda o significado de "pertencer" — quem está no filho herda o
|
|
135
|
+
* pai —, e um produto que não modela hierarquia não deve criá-la por acidente.
|
|
136
|
+
*/
|
|
137
|
+
type KeycloakGroup = {
|
|
138
|
+
readonly attributes?: KeycloakUserAttributes;
|
|
139
|
+
readonly id: string;
|
|
140
|
+
readonly name: string;
|
|
141
|
+
readonly path?: string;
|
|
142
|
+
};
|
|
143
|
+
type CreateGroupParams = {
|
|
144
|
+
readonly attributes?: KeycloakUserAttributes;
|
|
145
|
+
readonly name: string;
|
|
146
|
+
};
|
|
147
|
+
type CreateGroupResult = {
|
|
148
|
+
readonly id: string;
|
|
149
|
+
};
|
|
150
|
+
type UpdateGroupParams = {
|
|
151
|
+
readonly groupId: string;
|
|
152
|
+
readonly group: Readonly<Partial<Pick<KeycloakGroup, 'attributes' | 'name'>>>;
|
|
153
|
+
};
|
|
154
|
+
type DeleteGroupParams = {
|
|
155
|
+
readonly groupId: string;
|
|
156
|
+
};
|
|
157
|
+
type ListGroupsParams = {
|
|
158
|
+
readonly first?: number;
|
|
159
|
+
readonly limit?: number;
|
|
160
|
+
readonly search?: string;
|
|
161
|
+
};
|
|
162
|
+
type ListGroupsResult = {
|
|
163
|
+
readonly groups: readonly KeycloakGroup[];
|
|
164
|
+
readonly hasMore: boolean;
|
|
165
|
+
};
|
|
166
|
+
type GroupMembershipParams = {
|
|
167
|
+
readonly groupId: string;
|
|
168
|
+
readonly userId: string;
|
|
169
|
+
};
|
|
115
170
|
type KeycloakAdminClient = {
|
|
171
|
+
addUserToGroup(params: GroupMembershipParams): Promise<void>;
|
|
172
|
+
createGroup(params: CreateGroupParams): Promise<CreateGroupResult>;
|
|
173
|
+
deleteGroup(params: DeleteGroupParams): Promise<void>;
|
|
174
|
+
listGroups(params?: ListGroupsParams): Promise<ListGroupsResult>;
|
|
175
|
+
removeUserFromGroup(params: GroupMembershipParams): Promise<void>;
|
|
176
|
+
updateGroup(params: UpdateGroupParams): Promise<void>;
|
|
116
177
|
createUser(params: CreateUserParams): Promise<CreateUserResult>;
|
|
117
178
|
deleteUser(params: DeleteUserParams): Promise<void>;
|
|
118
179
|
findUserByEmail(params: FindUserByEmailParams): Promise<KeycloakUser | undefined>;
|
|
180
|
+
listUsers(params?: ListUsersParams): Promise<ListUsersResult>;
|
|
119
181
|
setEnabled(params: SetEnabledParams): Promise<void>;
|
|
120
182
|
setPassword(params: SetPasswordParams): Promise<void>;
|
|
121
183
|
setTemporaryPassword(params: SetTemporaryPasswordParams): Promise<void>;
|
|
@@ -141,4 +203,4 @@ declare function parseKeycloakAdminConfig(value: unknown): KeycloakAdminConfig;
|
|
|
141
203
|
|
|
142
204
|
declare function createKeycloakAdminClient({ config: rawConfig, fetch: injectedFetch, now, }: CreateKeycloakAdminClientParams): KeycloakAdminClient;
|
|
143
205
|
|
|
144
|
-
export { type CreateKeycloakAdminClientParams, type CreateUserParams, type CreateUserResult, type DeleteUserParams, type FetchLike, type FindUserByEmailParams, KEYCLOAK_ADMIN_ERROR_CODE, KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS, type KeycloakAdminClient, type KeycloakAdminConfig, type KeycloakAdminEndpoints, KeycloakAdminError, type KeycloakAdminErrorCode, type KeycloakAdminErrorContext, type KeycloakAdminErrorParams, type KeycloakPassword, type KeycloakUser, type KeycloakUserAttributes, type SerializedKeycloakAdminError, type SetEnabledParams, type SetPasswordParams, type SetTemporaryPasswordParams, type UpdateAttributesParams, type UpdateUserParams, buildKeycloakAdminEndpoints, createKeycloakAdminClient, isKeycloakAdminError, keycloakAdminConfigSchema, parseKeycloakAdminConfig };
|
|
206
|
+
export { type CreateGroupParams, type CreateGroupResult, type CreateKeycloakAdminClientParams, type CreateUserParams, type CreateUserResult, type DeleteGroupParams, type DeleteUserParams, type FetchLike, type FindUserByEmailParams, type GroupMembershipParams, KEYCLOAK_ADMIN_ERROR_CODE, KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS, type KeycloakAdminClient, type KeycloakAdminConfig, type KeycloakAdminEndpoints, KeycloakAdminError, type KeycloakAdminErrorCode, type KeycloakAdminErrorContext, type KeycloakAdminErrorParams, type KeycloakGroup, type KeycloakPassword, type KeycloakUser, type KeycloakUserAttributes, type ListGroupsParams, type ListGroupsResult, type ListUsersParams, type ListUsersResult, type SerializedKeycloakAdminError, type SetEnabledParams, type SetPasswordParams, type SetTemporaryPasswordParams, type UpdateAttributesParams, type UpdateGroupParams, type UpdateUserParams, buildKeycloakAdminEndpoints, createKeycloakAdminClient, isKeycloakAdminError, keycloakAdminConfigSchema, parseKeycloakAdminConfig };
|
package/dist/index.js
CHANGED
|
@@ -17,9 +17,14 @@ function buildKeycloakAdminEndpoints({ baseUrl, realm }) {
|
|
|
17
17
|
const origin = baseUrl.replace(/\/+$/, "");
|
|
18
18
|
const encodedRealm = encodeURIComponent(realm);
|
|
19
19
|
const users = `${origin}/admin/realms/${encodedRealm}/users`;
|
|
20
|
+
const groups = `${origin}/admin/realms/${encodedRealm}/groups`;
|
|
20
21
|
return {
|
|
22
|
+
group: (groupId) => `${groups}/${encodeURIComponent(groupId)}`,
|
|
23
|
+
groups,
|
|
21
24
|
token: `${origin}/realms/${encodedRealm}/protocol/openid-connect/token`,
|
|
22
25
|
user: (userId) => `${users}/${encodeURIComponent(userId)}`,
|
|
26
|
+
/** O Admin API endereça a filiação pelo usuário, não pelo grupo: é `PUT` para entrar, `DELETE` para sair. */
|
|
27
|
+
userGroup: (userId, groupId) => `${users}/${encodeURIComponent(userId)}/groups/${encodeURIComponent(groupId)}`,
|
|
23
28
|
userPassword: (userId) => `${users}/${encodeURIComponent(userId)}/reset-password`,
|
|
24
29
|
users
|
|
25
30
|
};
|
|
@@ -195,7 +200,7 @@ function normalizeAttributes(attributes) {
|
|
|
195
200
|
Object.entries(attributes).map(([key, value]) => [key, typeof value === "string" ? [value] : [...value]])
|
|
196
201
|
);
|
|
197
202
|
}
|
|
198
|
-
function
|
|
203
|
+
function idFromLocation(response) {
|
|
199
204
|
const location = response.headers.get("location");
|
|
200
205
|
const id = location?.split("/").filter(Boolean).at(-1);
|
|
201
206
|
if (id === void 0 || id === "") {
|
|
@@ -282,7 +287,7 @@ function createKeycloakAdminClient({
|
|
|
282
287
|
secrets: [password?.value],
|
|
283
288
|
url: endpoints.users
|
|
284
289
|
});
|
|
285
|
-
return { id:
|
|
290
|
+
return { id: idFromLocation(response) };
|
|
286
291
|
},
|
|
287
292
|
async deleteUser({ userId }) {
|
|
288
293
|
await adminRequest({ method: "DELETE", url: endpoints.user(userId) });
|
|
@@ -293,6 +298,52 @@ function createKeycloakAdminClient({
|
|
|
293
298
|
const found = await response.json();
|
|
294
299
|
return found.at(0);
|
|
295
300
|
},
|
|
301
|
+
/**
|
|
302
|
+
* Pede um a mais que o limite para saber se há próxima página sem uma segunda chamada — o
|
|
303
|
+
* Keycloak não devolve total, e contar o realm inteiro só para desenhar um botão é caro.
|
|
304
|
+
*/
|
|
305
|
+
async addUserToGroup({ groupId, userId }) {
|
|
306
|
+
await adminRequest({ method: "PUT", url: endpoints.userGroup(userId, groupId) });
|
|
307
|
+
},
|
|
308
|
+
async createGroup({ attributes, name }) {
|
|
309
|
+
const response = await adminRequest({
|
|
310
|
+
body: { ...attributes === void 0 ? {} : { attributes: normalizeAttributes(attributes) }, name },
|
|
311
|
+
method: "POST",
|
|
312
|
+
url: endpoints.groups
|
|
313
|
+
});
|
|
314
|
+
return { id: idFromLocation(response) };
|
|
315
|
+
},
|
|
316
|
+
async deleteGroup({ groupId }) {
|
|
317
|
+
await adminRequest({ method: "DELETE", url: endpoints.group(groupId) });
|
|
318
|
+
},
|
|
319
|
+
/** Mesmo recorte de `listUsers`: pede um a mais que o limite para saber se há próxima página. */
|
|
320
|
+
async listGroups({ first = 0, limit = 100, search } = {}) {
|
|
321
|
+
const query = new URLSearchParams({ first: String(first), max: String(limit + 1) });
|
|
322
|
+
if (search !== void 0 && search !== "") query.set("search", search);
|
|
323
|
+
const response = await adminRequest({ method: "GET", url: `${endpoints.groups}?${query}` });
|
|
324
|
+
const found = await response.json();
|
|
325
|
+
return { groups: found.slice(0, limit), hasMore: found.length > limit };
|
|
326
|
+
},
|
|
327
|
+
async removeUserFromGroup({ groupId, userId }) {
|
|
328
|
+
await adminRequest({ method: "DELETE", url: endpoints.userGroup(userId, groupId) });
|
|
329
|
+
},
|
|
330
|
+
async updateGroup({ group, groupId }) {
|
|
331
|
+
await adminRequest({
|
|
332
|
+
body: {
|
|
333
|
+
...group.attributes === void 0 ? {} : { attributes: normalizeAttributes(group.attributes) },
|
|
334
|
+
...group.name === void 0 ? {} : { name: group.name }
|
|
335
|
+
},
|
|
336
|
+
method: "PUT",
|
|
337
|
+
url: endpoints.group(groupId)
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
async listUsers({ first = 0, limit = 100, search } = {}) {
|
|
341
|
+
const query = new URLSearchParams({ first: String(first), max: String(limit + 1) });
|
|
342
|
+
if (search !== void 0 && search !== "") query.set("search", search);
|
|
343
|
+
const response = await adminRequest({ method: "GET", url: `${endpoints.users}?${query}` });
|
|
344
|
+
const found = await response.json();
|
|
345
|
+
return { hasMore: found.length > limit, users: found.slice(0, limit) };
|
|
346
|
+
},
|
|
296
347
|
async setEnabled({ enabled, userId }) {
|
|
297
348
|
await adminRequest({ body: { enabled }, method: "PUT", url: endpoints.user(userId) });
|
|
298
349
|
},
|
package/package.json
CHANGED