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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This repository contains various examples on how to setup **io.Manager**
# Other

- [custom-endpoints](./custom-endpoints) - An example that demonstrates how to implement custom endpoints to **io.Manager**.
- [custom-groups-service](./custom-groups-service) - An example that demonstrates how to provide a custom Groups service to **io.Manager**.
- [node-esm](./node-esm) - An example that demonstrates how to setup a **io.Manager** in a Node.js native ESM project.
- [node-commonjs](./node-commonjs) - An example that demonstrates how to setup a **io.Manager** in a Node.js native CommonJS project.
- [custom-logging-config](./custom-logging-config) - An example that demonstrates how to pass custom logging configuration to **io.Manager**.
Expand Down
4 changes: 4 additions & 0 deletions custom-groups-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/node_modules

**/dist
**/logs
43 changes: 43 additions & 0 deletions custom-groups-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Introduction

An example that demonstrates how to provide a custom Groups service to **io.Manager**.

# Custom Groups service

By default **io.Manager** stores groups and resolves the groups a user belongs to using its internal Groups Service implementation. Providing a custom Groups service through the `groups_service` configuration property replaces that with your own implementation.

A custom Groups service implements the `GroupsService` interface exported from `@interopio/manager`. Its `getSupportedFeatures()` method declares which operations the implementation supports - **io.Manager** invokes a group management operation only when the corresponding capability flag is `true`, so an implementation that only reads groups can report `false` for the write operations and leave those methods as stubs.

This example implements [MyGroupsService](./src/MyGroupsService.ts) as a thin adapter over the in-memory stores in [data.ts](./src/data.ts) that stand in for an external system.

For an example that combines a custom Groups service with a custom authenticator, see the [auth-custom](../auth-custom) example.

# Prerequisites

### Database

io.Manager requires a database to connect to - this example uses MongoDB, but you can use any other of the supported databases. You will need to either have a local instance or setup a remote database to connect to. For more information visit our Documentation page on the subject: https://docs.interop.io/manager/databases/overview/index.html

### License

**io.Manager** requires a license key to operate. To acquire a license key, contact us at `sales@interop.io`.

# How to run

- Install npm packages

```sh

npm install

npm audit fix

```

- Start the server

```sh

npm run start

```
17 changes: 17 additions & 0 deletions custom-groups-service/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "custom-groups-service",
"version": "0.0.1",
"private": true,
"type": "commonjs",
"scripts": {
"build": "npm run delete ./dist && tsc",
"delete": "node -e \"const fs = require('fs'); const path = process.argv[1]; if (!path) process.exit(1); if (fs.existsSync(path)) fs.rmSync(path, { recursive: true, force: true });\"",
"start": "npm run build && node ./dist/index.js"
},
"dependencies": {
"@interopio/manager": "^4.0.0"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}
90 changes: 90 additions & 0 deletions custom-groups-service/src/MyGroupsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type {
AuditBuilder,
DataRequest,
Group,
GroupDataResult,
GroupsFeatures,
GroupsService,
User,
} from '@interopio/manager';

import { groups, users } from './data.js';

export class MyGroupsService implements GroupsService {
// Declares which operations this implementation supports.
public getSupportedFeatures(): GroupsFeatures {
return {
canGetUserGroups: true,
canGetAllGroups: true,
canGetGroup: true,
canAddGroup: true,
canUpdateGroup: true,
canAddOrUpdateGroup: true,
canRemoveGroup: true,
canAddUserToGroup: true,
canRemoveUserFromGroup: true,
};
}

// ⚠️ io.Manager authorizes each request against the groups returned here - apart from the
// admin rights granted through `auth_exclusive_users`, this decides what a user can access.
public async getUserGroups(user: string | User): Promise<string[]> {
const id = typeof user === 'string' ? user : user.id;

return users.getGroups(id);
}

public async getAllGroups(_request?: DataRequest): Promise<GroupDataResult> {
const items = groups.getAll();

return {
items,
total: items.length,
};
}

public async getGroup(name: string): Promise<Group | undefined> {
return groups.get(name);
}

public async addGroup(group: Group, _audit: AuditBuilder): Promise<Group> {
return groups.add(group);
}

public async updateGroup(group: Group, _audit: AuditBuilder): Promise<Group> {
return groups.update(group);
}

public async addOrUpdateGroup(
group: Group,
_audit: AuditBuilder
): Promise<Group> {
return groups.addOrUpdate(group);
}

public async removeGroup(name: string, _audit: AuditBuilder): Promise<void> {
groups.remove(name);
users.removeGroupFromAll(name);
}

public async addUserToGroups(
user: string,
groups: string[],
_audit: AuditBuilder
): Promise<void> {
users.addToGroups(user, groups);
}

public async removeUserFromGroups(
user: string,
groups: string[],
_audit: AuditBuilder
): Promise<void> {
users.removeFromGroups(user, groups);
}

public async removeAll(_audit: AuditBuilder): Promise<void> {
groups.clear();
users.clearGroups();
}
}
142 changes: 142 additions & 0 deletions custom-groups-service/src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { Group, User } from '@interopio/manager';

export const GROUP_SERVER_ADMIN = 'GLUE42_SERVER_ADMIN';
export const GROUP_FRONT_OFFICE = 'Front Office';
export const GROUP_TRADER = 'Trader';

export class InMemoryGroupsStore {
private readonly groups: Group[] = [
{
name: GROUP_FRONT_OFFICE,
},
{
name: GROUP_TRADER,
},
];

public getAll(): Group[] {
return [...this.groups];
}

public get(name: string): Group | undefined {
return this.groups.find((group) => group.name === name);
}

public add(group: Group): Group {
if (!this.groups.some((existing) => existing.name === group.name)) {
this.groups.push(group);
}

return group;
}

public update(group: Group): Group {
const index = this.groups.findIndex(
(existing) => existing.name === group.name
);

if (index !== -1) {
this.groups[index] = group;
}

return group;
}

public addOrUpdate(group: Group): Group {
const index = this.groups.findIndex(
(existing) => existing.name === group.name
);

if (index !== -1) {
this.groups[index] = group;
} else {
this.groups.push(group);
}

return group;
}

public remove(name: string): void {
const index = this.groups.findIndex((group) => group.name === name);

if (index !== -1) {
this.groups.splice(index, 1);
}
}

public clear(): void {
this.groups.length = 0;
}
}

export class InMemoryUsersStore {
private readonly users: User[] = [
{
id: 'number.one@company.xyz',
email: 'number.one@company.xyz',
apps: [],
groups: [GROUP_FRONT_OFFICE],
},
{
id: 'number.two@company.xyz',
email: 'number.two@company.xyz',
apps: [],
groups: [GROUP_TRADER],
},
{
id: 'admin',
email: 'admin@company.xyz',
apps: [],
groups: [GROUP_SERVER_ADMIN],
},
];

public get(id: string): User | undefined {
return this.users.find((user) => user.id === id);
}

public getGroups(id: string): string[] {
return [...(this.get(id)?.groups ?? [])];
}

public addToGroups(id: string, groups: string[]): void {
const user = this.get(id);

if (!user) {
return;
}

for (const group of groups) {
if (!user.groups.includes(group)) {
user.groups.push(group);
}
}
}

public removeFromGroups(id: string, groups: string[]): void {
const user = this.get(id);

if (!user) {
return;
}

user.groups = user.groups.filter((group) => !groups.includes(group));
}

// Drops a group from every user that belonged to it.
public removeGroupFromAll(name: string): void {
for (const user of this.users) {
user.groups = user.groups.filter((group) => group !== name);
}
}

public clearGroups(): void {
for (const user of this.users) {
user.groups = [];
}
}
}

export const groups = new InMemoryGroupsStore();

export const users = new InMemoryUsersStore();
27 changes: 27 additions & 0 deletions custom-groups-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { start, type Config } from '@interopio/manager';

import { MyGroupsService } from './MyGroupsService.js';

const config: Config = {
name: 'example',
port: 4356,
base: 'api',
// TODO: Contact us at sales@interop.io to acquire a license key.
licenseKey: '<YOUR_LICENSE_KEY>',
auth_method: 'none',
auth_exclusive_users: ['admin'],
store: {
type: 'mongo',
// TODO: Replace this with your own MongoDB connection string.
connection:
'mongodb://db_user:Password123$@localhost:27017/io_manager?authSource=admin&directConnection=true',
},
token: {
// TODO: Replace this with your secret.
secret: '<YOUR_SECRET>',
},
// Replaces the built-in Groups service with the custom implementation.
groups_service: new MyGroupsService(),
};

start(config);
16 changes: 16 additions & 0 deletions custom-groups-service/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext"],
"module": "node16",
"moduleResolution": "node16",
"skipLibCheck": true,
"sourceMap": true,
"declaration": true,
"rootDir": "./src",
"outDir": "dist",
"pretty": true,
"strict": true
},
"include": ["src"]
}