Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,66 @@ const resWithActor = await descopeClient.management.tenant.generateSSOConfigurat
console.log(resWithActor.adminSSOConfigurationLink);
```

### Manage Families

You can create, update, delete or search families, manage the users linked to them, and
impersonate a family dependent (a shadow-profile user with no login credentials of their own):

```typescript
// Create a family. familyId is optional - a random one is generated when omitted.
const family = await descopeClient.management.family.create('My Family', {
customAttributeName: 'val',
});

// Update will override all provided fields as is. Omitted fields are left unchanged.
await descopeClient.management.family.update(family.data.id, 'My Family', {
customAttributeName: 'val2',
});

// Family deletion cannot be undone. Use carefully.
await descopeClient.management.family.delete(family.data.id);

// Search families according to various parameters. Called with no options, returns all families.
const searchRes = await descopeClient.management.family.search({ freeText: 'My Family' });
searchRes.data.forEach((f) => {
// do something
});

// Create a dependent (shadow profile) user in a family - a user with no login credentials of
// their own. When loginId is omitted it is derived from name; email/phone are never used as the
// login ID since they are not unique - a dependent may share them with their guardian.
const dependent = await descopeClient.management.family.createDependent(family.data.id, {
name: 'My Dependent',
});

// Dependent deletion cannot be undone. The family is inferred from the dependent. Regular
// (non-dependent) family members are removed via user.removeFamilies, not deleted.
await descopeClient.management.family.deleteDependent(dependent.data.userId);

// Add a user to one or more families. Each entry may also set the user's roles and family-scoped
// attributes for that family in the same call; omitting roleNames or familyScopedAttributes on a
// family the user already belongs to leaves them unchanged.
await descopeClient.management.user.addFamilies('user-login-id', [
{ familyId: family.data.id, roleNames: ['role1'] },
]);

// Remove a user from one or more families.
await descopeClient.management.user.removeFamilies('user-login-id', [family.data.id]);

// Impersonate a family dependent. The impersonator (by user ID or login ID) must be a member of
// the dependent's family and hold the family impersonate-dependents permission there.
const impersonateRes = await descopeClient.management.family.impersonateDependent(
'admin-user-id',
'dependent-login-id',
family.data.id, // optional - scopes the impersonated session to this family
);
console.log(impersonateRes.data.jwt);

// Stop impersonating a family dependent and return to the acting admin's own session.
const stopRes = await descopeClient.management.family.stopImpersonation(impersonateRes.data.jwt);
console.log(stopRes.data.jwt);
```

### Manage Password

You can read and update any tenant password settings and policy:
Expand Down
321 changes: 321 additions & 0 deletions lib/management/family.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
import { SdkResponse } from '@descope/core-js-sdk';
import withManagement from '.';
import apiPaths from './paths';
import { Family, UpdateJWTResponse } from './types';
import { mockHttpClient, resetMockHttpClient } from './testutils';

const management = withManagement(mockHttpClient);

const mockFamily: Family = {
id: 'f1',
name: 'family1',
customAttributes: { customAttr: 'value' },
disabled: false,
photo: 'http://dummy.com/photo.png',
createdTime: 1,
};

const mockFamilies: Family[] = [
mockFamily,
{ id: 'f2', name: 'family2', createdTime: 1 },
{ id: 'f3', name: 'family3', createdTime: 1 },
];

const mockUserResponse = {
userId: 'u1',
loginIds: ['lid'],
verifiedEmail: false,
verifiedPhone: false,
};

describe('Management Family', () => {
afterEach(() => {
jest.clearAllMocks();
resetMockHttpClient();
});

describe('create', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => ({ family: mockFamily }),
clone: () => ({
json: () => Promise.resolve({ family: mockFamily }),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp: SdkResponse<Family> = await management.family.create(
'family1',
{ customAttr: 'value' },
'http://dummy.com/photo.png',
false,
'f1',
);

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.family.create, {
name: 'family1',
customAttributes: { customAttr: 'value' },
photo: 'http://dummy.com/photo.png',
disabled: false,
familyId: 'f1',
});

expect(resp).toEqual({
code: 200,
data: mockFamily,
ok: true,
response: httpResponse,
});
});
});

describe('update', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => ({ family: mockFamily }),
clone: () => ({
json: () => Promise.resolve({ family: mockFamily }),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp: SdkResponse<Family> = await management.family.update(
'f1',
'family1',
{ customAttr: 'value' },
'http://dummy.com/photo.png',
false,
);

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.family.update, {
id: 'f1',
name: 'family1',
customAttributes: { customAttr: 'value' },
photo: 'http://dummy.com/photo.png',
disabled: false,
});

expect(resp).toEqual({
code: 200,
data: mockFamily,
ok: true,
response: httpResponse,
});
});
});

describe('delete', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => ({}),
clone: () => ({
json: () => Promise.resolve({}),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp = await management.family.delete('f1');

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.family.delete, { id: 'f1' });

expect(resp).toEqual({
code: 200,
data: {},
ok: true,
response: httpResponse,
});
});
});

describe('search', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => ({ families: mockFamilies }),
clone: () => ({
json: () => Promise.resolve({ families: mockFamilies }),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp: SdkResponse<Family[]> = await management.family.search({
familyIds: ['f1'],
freeText: 'fam',
familyNames: ['family1'],
page: 0,
size: 10,
customAttributes: { customAttr: 'value' },
});

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.family.search,
{
familyIds: ['f1'],
freeText: 'fam',
familyNames: ['family1'],
page: 0,
size: 10,
customAttributes: { customAttr: 'value' },
},
{},
);

expect(resp).toEqual({
code: 200,
data: mockFamilies,
ok: true,
response: httpResponse,
});
});

it('should send an empty body when called without options', async () => {
const httpResponse = {
ok: true,
json: () => ({ families: mockFamilies }),
clone: () => ({
json: () => Promise.resolve({ families: mockFamilies }),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

await management.family.search();

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.family.search, {}, {});
});
});

describe('createDependent', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => ({ user: mockUserResponse }),
clone: () => ({
json: () => Promise.resolve({ user: mockUserResponse }),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp = await management.family.createDependent('f1', {
name: 'dependent',
email: 'dependent@example.com',
familyScopedAttributes: { f1: { customAttr: 'value' } },
});

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.family.dependent.create, {
familyId: 'f1',
name: 'dependent',
email: 'dependent@example.com',
familyScopedAttributes: { f1: { customAttr: 'value' } },
});

expect(resp).toEqual({
code: 200,
data: mockUserResponse,
ok: true,
response: httpResponse,
});
});
});

describe('deleteDependent', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => ({}),
clone: () => ({
json: () => Promise.resolve({}),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp = await management.family.deleteDependent('u1');

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.family.dependent.delete, {
userId: 'u1',
});

expect(resp).toEqual({
code: 200,
data: {},
ok: true,
response: httpResponse,
});
});
});

describe('impersonateDependent', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => ({ jwt: 'jwt123' }),
clone: () => ({
json: () => Promise.resolve({ jwt: 'jwt123' }),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp: SdkResponse<UpdateJWTResponse> = await management.family.impersonateDependent(
'admin-uid',
'dependent-lid',
'f1',
);

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.family.impersonate, {
impersonatorUserIdOrLoginId: 'admin-uid',
dependentLoginId: 'dependent-lid',
selectedFamily: 'f1',
});

expect(resp).toEqual({
code: 200,
data: { jwt: 'jwt123' },
ok: true,
response: httpResponse,
});
});
});

describe('stopImpersonation', () => {
it('should send the correct request and receive correct response', async () => {
const httpResponse = {
ok: true,
json: () => ({ jwt: 'jwt123' }),
clone: () => ({
json: () => Promise.resolve({ jwt: 'jwt123' }),
}),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const resp: SdkResponse<UpdateJWTResponse> = await management.family.stopImpersonation(
'jwt123',
{ k: 'v' },
60,
);

expect(mockHttpClient.post).toHaveBeenCalledWith(apiPaths.family.stopImpersonation, {
jwt: 'jwt123',
customClaims: { k: 'v' },
refreshDuration: 60,
});

expect(resp).toEqual({
code: 200,
data: { jwt: 'jwt123' },
ok: true,
response: httpResponse,
});
});
});
});
Loading
Loading