@adatechnology/keycloak-admin 1.0.0-rc.1 → 1.0.0-rc.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/README.md +15 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +66 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,9 +32,23 @@ const { id } = await keycloak.createUser({
|
|
|
32
32
|
})
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Operações: `createUser`, `findUserByEmail`, `listUsers`, `updateUser`, `setEnabled`,
|
|
35
|
+
Operações de usuário: `createUser`, `findUserByEmail`, `listUsers`, `updateUser`, `setEnabled`,
|
|
36
36
|
`updateAttributes`, `deleteUser`, `setPassword`, `setTemporaryPassword`.
|
|
37
37
|
|
|
38
|
+
Foto de perfil: `setProfilePicture({ userId, pictureUrl })`. O atributo é `picture`, o nome que o
|
|
39
|
+
OIDC reserva — com um mapeador no realm ele chega ao token, e a tela desenha o avatar sem uma
|
|
40
|
+
consulta por pessoa.
|
|
41
|
+
|
|
42
|
+
⚠️ O Keycloak **não hospeda imagem**: o valor é uma URL e o arquivo é do produto. Base64 no atributo
|
|
43
|
+
cresce o token até ele parar de caber no cabeçalho, e o sintoma aparece longe — login que funciona
|
|
44
|
+
no navegador e falha no `curl`. A operação lê os atributos antes de gravar, porque o Admin API
|
|
45
|
+
substitui o conjunto: mandar só a foto apagaria `company_id` e o resto.
|
|
46
|
+
|
|
47
|
+
Operações de grupo: `createGroup`, `updateGroup`, `deleteGroup`, `listGroups`, `addUserToGroup`,
|
|
48
|
+
`removeUserFromGroup`. Só o **primeiro nível** — grupo aninhado muda o significado de "pertencer",
|
|
49
|
+
porque quem está no filho herda o pai, e um produto que não modela hierarquia não deve criá-la por
|
|
50
|
+
acidente. A filiação é endereçada pelo **usuário** (`/users/{id}/groups/{id}`), não pelo grupo.
|
|
51
|
+
|
|
38
52
|
`listUsers({ first, limit, search })` devolve `{ users, hasMore }`. O realm não informa total, então
|
|
39
53
|
a página pede um registro a mais que o limite e descarta-o: é assim que `hasMore` sai sem uma
|
|
40
54
|
segunda chamada.
|
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
|
};
|
|
@@ -109,6 +113,20 @@ type SetEnabledParams = {
|
|
|
109
113
|
readonly enabled: boolean;
|
|
110
114
|
readonly userId: string;
|
|
111
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* O atributo padrão da foto. `picture` é o nome que o OIDC já reserva para isso, e usá-lo é o que
|
|
118
|
+
* permite a foto chegar ao token por um mapeador do realm em vez de uma consulta extra por tela.
|
|
119
|
+
*
|
|
120
|
+
* ⚠️ O Keycloak **não hospeda imagem**: o valor é uma URL, e quem guarda o arquivo é o produto. Um
|
|
121
|
+
* atributo com base64 dentro cresce o token até ele parar de caber no cabeçalho, e aí o sintoma é
|
|
122
|
+
* login que funciona no navegador e falha no `curl`.
|
|
123
|
+
*/
|
|
124
|
+
declare const PROFILE_PICTURE_ATTRIBUTE = "picture";
|
|
125
|
+
type SetProfilePictureParams = {
|
|
126
|
+
/** URL da imagem, ou `undefined` para tirar a foto sem mexer nos outros atributos. */
|
|
127
|
+
readonly pictureUrl: string | undefined;
|
|
128
|
+
readonly userId: string;
|
|
129
|
+
};
|
|
112
130
|
type UpdateAttributesParams = {
|
|
113
131
|
readonly attributes: KeycloakUserAttributes;
|
|
114
132
|
readonly userId: string;
|
|
@@ -125,12 +143,57 @@ type SetTemporaryPasswordParams = {
|
|
|
125
143
|
readonly password: string;
|
|
126
144
|
readonly userId: string;
|
|
127
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* O grupo do realm. O Keycloak aceita hierarquia (`subGroups`), e este cliente trata só o primeiro
|
|
148
|
+
* nível de propósito: grupo aninhado muda o significado de "pertencer" — quem está no filho herda o
|
|
149
|
+
* pai —, e um produto que não modela hierarquia não deve criá-la por acidente.
|
|
150
|
+
*/
|
|
151
|
+
type KeycloakGroup = {
|
|
152
|
+
readonly attributes?: KeycloakUserAttributes;
|
|
153
|
+
readonly id: string;
|
|
154
|
+
readonly name: string;
|
|
155
|
+
readonly path?: string;
|
|
156
|
+
};
|
|
157
|
+
type CreateGroupParams = {
|
|
158
|
+
readonly attributes?: KeycloakUserAttributes;
|
|
159
|
+
readonly name: string;
|
|
160
|
+
};
|
|
161
|
+
type CreateGroupResult = {
|
|
162
|
+
readonly id: string;
|
|
163
|
+
};
|
|
164
|
+
type UpdateGroupParams = {
|
|
165
|
+
readonly groupId: string;
|
|
166
|
+
readonly group: Readonly<Partial<Pick<KeycloakGroup, 'attributes' | 'name'>>>;
|
|
167
|
+
};
|
|
168
|
+
type DeleteGroupParams = {
|
|
169
|
+
readonly groupId: string;
|
|
170
|
+
};
|
|
171
|
+
type ListGroupsParams = {
|
|
172
|
+
readonly first?: number;
|
|
173
|
+
readonly limit?: number;
|
|
174
|
+
readonly search?: string;
|
|
175
|
+
};
|
|
176
|
+
type ListGroupsResult = {
|
|
177
|
+
readonly groups: readonly KeycloakGroup[];
|
|
178
|
+
readonly hasMore: boolean;
|
|
179
|
+
};
|
|
180
|
+
type GroupMembershipParams = {
|
|
181
|
+
readonly groupId: string;
|
|
182
|
+
readonly userId: string;
|
|
183
|
+
};
|
|
128
184
|
type KeycloakAdminClient = {
|
|
185
|
+
addUserToGroup(params: GroupMembershipParams): Promise<void>;
|
|
186
|
+
createGroup(params: CreateGroupParams): Promise<CreateGroupResult>;
|
|
187
|
+
deleteGroup(params: DeleteGroupParams): Promise<void>;
|
|
188
|
+
listGroups(params?: ListGroupsParams): Promise<ListGroupsResult>;
|
|
189
|
+
removeUserFromGroup(params: GroupMembershipParams): Promise<void>;
|
|
190
|
+
updateGroup(params: UpdateGroupParams): Promise<void>;
|
|
129
191
|
createUser(params: CreateUserParams): Promise<CreateUserResult>;
|
|
130
192
|
deleteUser(params: DeleteUserParams): Promise<void>;
|
|
131
193
|
findUserByEmail(params: FindUserByEmailParams): Promise<KeycloakUser | undefined>;
|
|
132
194
|
listUsers(params?: ListUsersParams): Promise<ListUsersResult>;
|
|
133
195
|
setEnabled(params: SetEnabledParams): Promise<void>;
|
|
196
|
+
setProfilePicture(params: SetProfilePictureParams): Promise<void>;
|
|
134
197
|
setPassword(params: SetPasswordParams): Promise<void>;
|
|
135
198
|
setTemporaryPassword(params: SetTemporaryPasswordParams): Promise<void>;
|
|
136
199
|
updateAttributes(params: UpdateAttributesParams): Promise<void>;
|
|
@@ -155,4 +218,4 @@ declare function parseKeycloakAdminConfig(value: unknown): KeycloakAdminConfig;
|
|
|
155
218
|
|
|
156
219
|
declare function createKeycloakAdminClient({ config: rawConfig, fetch: injectedFetch, now, }: CreateKeycloakAdminClientParams): KeycloakAdminClient;
|
|
157
220
|
|
|
158
|
-
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 ListUsersParams, type ListUsersResult, type SerializedKeycloakAdminError, type SetEnabledParams, type SetPasswordParams, type SetTemporaryPasswordParams, type UpdateAttributesParams, type UpdateUserParams, buildKeycloakAdminEndpoints, createKeycloakAdminClient, isKeycloakAdminError, keycloakAdminConfigSchema, parseKeycloakAdminConfig };
|
|
221
|
+
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, PROFILE_PICTURE_ATTRIBUTE, type SerializedKeycloakAdminError, type SetEnabledParams, type SetPasswordParams, type SetProfilePictureParams, 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
|
};
|
|
@@ -182,6 +187,9 @@ function createKeycloakTokenProvider({
|
|
|
182
187
|
};
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
// src/keycloak-admin.types.ts
|
|
191
|
+
var PROFILE_PICTURE_ATTRIBUTE = "picture";
|
|
192
|
+
|
|
185
193
|
// src/keycloak-admin.client.ts
|
|
186
194
|
var HTTP_CONFLICT = 409;
|
|
187
195
|
var HTTP_NOT_FOUND = 404;
|
|
@@ -195,7 +203,7 @@ function normalizeAttributes(attributes) {
|
|
|
195
203
|
Object.entries(attributes).map(([key, value]) => [key, typeof value === "string" ? [value] : [...value]])
|
|
196
204
|
);
|
|
197
205
|
}
|
|
198
|
-
function
|
|
206
|
+
function idFromLocation(response) {
|
|
199
207
|
const location = response.headers.get("location");
|
|
200
208
|
const id = location?.split("/").filter(Boolean).at(-1);
|
|
201
209
|
if (id === void 0 || id === "") {
|
|
@@ -282,7 +290,7 @@ function createKeycloakAdminClient({
|
|
|
282
290
|
secrets: [password?.value],
|
|
283
291
|
url: endpoints.users
|
|
284
292
|
});
|
|
285
|
-
return { id:
|
|
293
|
+
return { id: idFromLocation(response) };
|
|
286
294
|
},
|
|
287
295
|
async deleteUser({ userId }) {
|
|
288
296
|
await adminRequest({ method: "DELETE", url: endpoints.user(userId) });
|
|
@@ -297,6 +305,41 @@ function createKeycloakAdminClient({
|
|
|
297
305
|
* Pede um a mais que o limite para saber se há próxima página sem uma segunda chamada — o
|
|
298
306
|
* Keycloak não devolve total, e contar o realm inteiro só para desenhar um botão é caro.
|
|
299
307
|
*/
|
|
308
|
+
async addUserToGroup({ groupId, userId }) {
|
|
309
|
+
await adminRequest({ method: "PUT", url: endpoints.userGroup(userId, groupId) });
|
|
310
|
+
},
|
|
311
|
+
async createGroup({ attributes, name }) {
|
|
312
|
+
const response = await adminRequest({
|
|
313
|
+
body: { ...attributes === void 0 ? {} : { attributes: normalizeAttributes(attributes) }, name },
|
|
314
|
+
method: "POST",
|
|
315
|
+
url: endpoints.groups
|
|
316
|
+
});
|
|
317
|
+
return { id: idFromLocation(response) };
|
|
318
|
+
},
|
|
319
|
+
async deleteGroup({ groupId }) {
|
|
320
|
+
await adminRequest({ method: "DELETE", url: endpoints.group(groupId) });
|
|
321
|
+
},
|
|
322
|
+
/** Mesmo recorte de `listUsers`: pede um a mais que o limite para saber se há próxima página. */
|
|
323
|
+
async listGroups({ first = 0, limit = 100, search } = {}) {
|
|
324
|
+
const query = new URLSearchParams({ first: String(first), max: String(limit + 1) });
|
|
325
|
+
if (search !== void 0 && search !== "") query.set("search", search);
|
|
326
|
+
const response = await adminRequest({ method: "GET", url: `${endpoints.groups}?${query}` });
|
|
327
|
+
const found = await response.json();
|
|
328
|
+
return { groups: found.slice(0, limit), hasMore: found.length > limit };
|
|
329
|
+
},
|
|
330
|
+
async removeUserFromGroup({ groupId, userId }) {
|
|
331
|
+
await adminRequest({ method: "DELETE", url: endpoints.userGroup(userId, groupId) });
|
|
332
|
+
},
|
|
333
|
+
async updateGroup({ group, groupId }) {
|
|
334
|
+
await adminRequest({
|
|
335
|
+
body: {
|
|
336
|
+
...group.attributes === void 0 ? {} : { attributes: normalizeAttributes(group.attributes) },
|
|
337
|
+
...group.name === void 0 ? {} : { name: group.name }
|
|
338
|
+
},
|
|
339
|
+
method: "PUT",
|
|
340
|
+
url: endpoints.group(groupId)
|
|
341
|
+
});
|
|
342
|
+
},
|
|
300
343
|
async listUsers({ first = 0, limit = 100, search } = {}) {
|
|
301
344
|
const query = new URLSearchParams({ first: String(first), max: String(limit + 1) });
|
|
302
345
|
if (search !== void 0 && search !== "") query.set("search", search);
|
|
@@ -304,6 +347,26 @@ function createKeycloakAdminClient({
|
|
|
304
347
|
const found = await response.json();
|
|
305
348
|
return { hasMore: found.length > limit, users: found.slice(0, limit) };
|
|
306
349
|
},
|
|
350
|
+
/**
|
|
351
|
+
* A foto é atributo, e o Admin API **substitui o conjunto** quando recebe `attributes`. Por isso
|
|
352
|
+
* esta operação lê o usuário antes: mandar só a foto apagaria `company_id`, `tax_id` e qualquer
|
|
353
|
+
* outro atributo do produto — e o sintoma seria login entrando sem empresa, longe daqui.
|
|
354
|
+
*/
|
|
355
|
+
async setProfilePicture({ pictureUrl, userId }) {
|
|
356
|
+
const response = await adminRequest({ method: "GET", url: endpoints.user(userId) });
|
|
357
|
+
const current = await response.json();
|
|
358
|
+
const attributes = { ...current.attributes ?? {} };
|
|
359
|
+
if (pictureUrl === void 0 || pictureUrl === "") {
|
|
360
|
+
delete attributes[PROFILE_PICTURE_ATTRIBUTE];
|
|
361
|
+
} else {
|
|
362
|
+
attributes[PROFILE_PICTURE_ATTRIBUTE] = pictureUrl;
|
|
363
|
+
}
|
|
364
|
+
await adminRequest({
|
|
365
|
+
body: { attributes: normalizeAttributes(attributes) },
|
|
366
|
+
method: "PUT",
|
|
367
|
+
url: endpoints.user(userId)
|
|
368
|
+
});
|
|
369
|
+
},
|
|
307
370
|
async setEnabled({ enabled, userId }) {
|
|
308
371
|
await adminRequest({ body: { enabled }, method: "PUT", url: endpoints.user(userId) });
|
|
309
372
|
},
|
|
@@ -327,6 +390,7 @@ export {
|
|
|
327
390
|
KEYCLOAK_ADMIN_ERROR_CODE,
|
|
328
391
|
KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS,
|
|
329
392
|
KeycloakAdminError,
|
|
393
|
+
PROFILE_PICTURE_ATTRIBUTE,
|
|
330
394
|
buildKeycloakAdminEndpoints,
|
|
331
395
|
createKeycloakAdminClient,
|
|
332
396
|
isKeycloakAdminError,
|
package/package.json
CHANGED