0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(core): get organizations for user api

This commit is contained in:
Gao Sun 2023-10-15 23:11:52 +08:00
parent dd8c299547
commit 406e54e84f
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
6 changed files with 162 additions and 3 deletions

View file

@ -10,11 +10,78 @@ import {
OrganizationUserRelations,
OrganizationRoleUserRelations,
} from '@logto/schemas';
import { type CommonQueryMethods } from 'slonik';
import { convertToIdentifiers } from '@logto/shared';
import { sql, type CommonQueryMethods } from 'slonik';
import { z } from 'zod';
import RelationQueries from '#src/utils/RelationQueries.js';
import SchemaQueries from '#src/utils/SchemaQueries.js';
/**
* The simplified organization role entity that is returned in the `roles` field
* of the organization.
*
* @remarks
* The type MUST be kept in sync with the query in {@link UserRelationQueries.getOrganizationsByUserId}.
*/
type RoleEntity = {
id: string;
name: string;
};
/**
* The organization entity with the `roles` field that contains the roles of the
* current member of the organization.
*/
type OrganizationWithRoles = Organization & {
/** The roles of the current member of the organization. */
roles: RoleEntity[];
};
export const organizationWithRolesGuard: z.ZodType<OrganizationWithRoles> =
Organizations.guard.extend({
roles: z
.object({
id: z.string(),
name: z.string(),
})
.array(),
});
class UserRelationQueries extends RelationQueries<[typeof Organizations, typeof Users]> {
constructor(pool: CommonQueryMethods) {
super(pool, OrganizationUserRelations.table, Organizations, Users);
}
async getOrganizationsByUserId(userId: string): Promise<Readonly<OrganizationWithRoles[]>> {
const organizationRoles = convertToIdentifiers(OrganizationRoles, true);
const organizations = convertToIdentifiers(Organizations, true);
const { fields } = convertToIdentifiers(OrganizationUserRelations, true);
const oruRelations = convertToIdentifiers(OrganizationRoleUserRelations, true);
return this.pool.any<OrganizationWithRoles>(sql`
select
${organizations.table}.*,
json_agg(
json_build_object(
'id', ${organizationRoles.fields.id},
'name', ${organizationRoles.fields.name})
)
as roles
from ${this.table}
join ${organizations.table}
on ${fields.organizationId} = ${organizations.fields.id}
left join ${oruRelations.table}
on ${fields.userId} = ${oruRelations.fields.userId}
and ${fields.organizationId} = ${oruRelations.fields.organizationId}
left join ${organizationRoles.table}
on ${oruRelations.fields.organizationRoleId} = ${organizationRoles.fields.id}
where ${fields.userId} = ${userId}
group by ${organizations.table}.id
`);
}
}
export default class OrganizationQueries extends SchemaQueries<
OrganizationKeys,
CreateOrganization,
@ -35,7 +102,7 @@ export default class OrganizationQueries extends SchemaQueries<
OrganizationScopes
),
/** Queries for organization - user relations. */
users: new RelationQueries(this.pool, OrganizationUserRelations.table, Organizations, Users),
users: new UserRelationQueries(this.pool),
/** Queries for organization - organization role - user relations. */
rolesUsers: new RelationQueries(
this.pool,

View file

@ -1,6 +1,7 @@
import type { AuthedRouter, RouterInitArgs } from '../types.js';
import adminUserBasicsRoutes from './basics.js';
import adminUserOrganizationRoutes from './organization.js';
import adminUserRoleRoutes from './role.js';
import adminUserSearchRoutes from './search.js';
import adminUserSocialRoutes from './social.js';
@ -10,4 +11,5 @@ export default function adminUserRoutes<T extends AuthedRouter>(...args: RouterI
adminUserRoleRoutes(...args);
adminUserSearchRoutes(...args);
adminUserSocialRoutes(...args);
adminUserOrganizationRoutes(...args);
}

View file

@ -0,0 +1,30 @@
import { z } from 'zod';
import koaGuard from '#src/middleware/koa-guard.js';
import { organizationWithRolesGuard } from '#src/queries/organizations.js';
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
export default function adminUserOrganizationRoutes<T extends AuthedRouter>(
...[router, { queries }]: RouterInitArgs<T>
) {
router.get(
'/users/:userId/organizations',
koaGuard({
params: z.object({ userId: z.string() }),
response: organizationWithRolesGuard.array(),
status: [200, 404],
}),
async (ctx, next) => {
const { userId } = ctx.guard.params;
// Ensure that the user exists.
await queries.users.findUserById(userId);
// No pagination for now since it aligns with the current issuing of organizations
// to ID tokens.
ctx.body = await queries.organizations.relations.users.getOrganizationsByUserId(userId);
return next();
}
);
}

View file

@ -252,7 +252,7 @@ export default class SchemaRouter<
*
* The routes are:
*
* - `GET /:id/[pathname]`: Get the entities of the relation.
* - `GET /:id/[pathname]`: Get the entities of the relation with pagination.
* - `POST /:id/[pathname]`: Add entities to the relation.
* - `DELETE /:id/[pathname]/:relationSchemaId`: Remove an entity from the relation set.
* The `:relationSchemaId` is the entity ID in the relation schema.

View file

@ -3,6 +3,12 @@ import { type Role, type Organization } from '@logto/schemas';
import { authedAdminApi } from './api.js';
import { ApiFactory } from './factory.js';
type RoleEntity = {
id: string;
name: string;
};
type OrganizationWithRoles = Organization & { roles: RoleEntity[] };
class OrganizationApi extends ApiFactory<Organization, { name: string; description?: string }> {
constructor() {
super('organizations');
@ -31,6 +37,10 @@ class OrganizationApi extends ApiFactory<Organization, { name: string; descripti
async deleteUserRole(id: string, userId: string, roleId: string): Promise<void> {
await authedAdminApi.delete(`${this.path}/${id}/users/${userId}/roles/${roleId}`);
}
async getUserOrganizations(userId: string): Promise<OrganizationWithRoles[]> {
return authedAdminApi.get(`users/${userId}/organizations`).json<OrganizationWithRoles[]>();
}
}
/** API methods for operating organizations. */

View file

@ -158,5 +158,55 @@ describe('organization APIs', () => {
roleApi.delete(role2.id),
]);
});
it('should be able to get all organizations with roles for a user', async () => {
const [organization1, organization2] = await Promise.all([
organizationApi.create({ name: 'test' }),
organizationApi.create({ name: 'test' }),
]);
const user = await createUser({ username: 'test' + randomId() });
const [role1, role2] = await Promise.all([
roleApi.create({ name: 'test' + randomId() }),
roleApi.create({ name: 'test' + randomId() }),
]);
await organizationApi.addUsers(organization1.id, [user.id]);
await organizationApi.addUserRoles(organization1.id, user.id, [role1.id]);
await organizationApi.addUsers(organization2.id, [user.id]);
await organizationApi.addUserRoles(organization2.id, user.id, [role1.id, role2.id]);
const organizations = await organizationApi.getUserOrganizations(user.id);
// Check organization 1 and ensure it only has role 1
const organization1WithRoles = organizations.find((org) => org.id === organization1.id);
assert(organization1WithRoles);
expect(organization1WithRoles.id).toBe(organization1.id);
expect(organization1WithRoles.roles).toContainEqual(
expect.objectContaining({ id: role1.id })
);
expect(organization1WithRoles.roles).not.toContainEqual(
expect.objectContaining({ id: role2.id })
);
// Check organization 2 and ensure it has both role 1 and role 2
const organization2WithRoles = organizations.find((org) => org.id === organization2.id);
assert(organization2WithRoles);
expect(organization2WithRoles.id).toBe(organization2.id);
expect(organization2WithRoles.roles).toContainEqual(
expect.objectContaining({ id: role1.id })
);
expect(organization2WithRoles.roles).toContainEqual(
expect.objectContaining({ id: role2.id })
);
// Clean up
await Promise.all([
organizationApi.delete(organization1.id),
organizationApi.delete(organization2.id),
deleteUser(user.id),
roleApi.delete(role1.id),
roleApi.delete(role2.id),
]);
});
});
});