mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
feat(core,schemas): add GET application user consent scopes api (#5103)
* feat(core,schemas): add GET application user consent scopes api add GET application user consent scopes api * fix(core): remove useless field declare remove useless field declare
This commit is contained in:
parent
cb43ebb7d1
commit
5c936e0976
8 changed files with 223 additions and 6 deletions
|
@ -1,9 +1,25 @@
|
|||
import type { Scope } from '@logto/schemas';
|
||||
import { OrganizationScopes, Scopes, type Scope } from '@logto/schemas';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
const groupResourceScopesByResourceId = (
|
||||
scopes: readonly Scope[]
|
||||
): Array<{
|
||||
resourceId: string;
|
||||
scopes: Scope[];
|
||||
}> => {
|
||||
const resourceMap = new Map<string, Scope[]>();
|
||||
|
||||
for (const scope of scopes) {
|
||||
const existingScopes = resourceMap.get(scope.resourceId) ?? [];
|
||||
resourceMap.set(scope.resourceId, [...existingScopes, scope]);
|
||||
}
|
||||
|
||||
return Array.from(resourceMap, ([resourceId, scopes]) => ({ resourceId, scopes }));
|
||||
};
|
||||
|
||||
export const createApplicationLibrary = (queries: Queries) => {
|
||||
const {
|
||||
applications: {
|
||||
|
@ -16,6 +32,7 @@ export const createApplicationLibrary = (queries: Queries) => {
|
|||
rolesScopes: { findRolesScopesByRoleIds },
|
||||
organizations: { scopes: organizationScopesQuery },
|
||||
scopes: { findScopesByIdsAndResourceIndicator, findScopesByIds },
|
||||
resources: { findResourceById },
|
||||
} = queries;
|
||||
|
||||
const findApplicationScopesForResourceIndicator = async (
|
||||
|
@ -112,10 +129,40 @@ export const createApplicationLibrary = (queries: Queries) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Get application user consent organization scopes
|
||||
const getApplicationUserConsentOrganizationScopes = async (applicationId: string) => {
|
||||
const [, scopes] = await userConsentOrganizationScopes.getEntities(OrganizationScopes, {
|
||||
applicationId,
|
||||
});
|
||||
|
||||
return scopes;
|
||||
};
|
||||
|
||||
const getApplicationUserConsentResourceScopes = async (applicationId: string) => {
|
||||
const [, scopes] = await userConsentResourceScopes.getEntities(Scopes, {
|
||||
applicationId,
|
||||
});
|
||||
|
||||
const groupedScopes = groupResourceScopesByResourceId(scopes);
|
||||
|
||||
return Promise.all(
|
||||
groupedScopes.map(async ({ resourceId, scopes }) => ({
|
||||
resource: await findResourceById(resourceId),
|
||||
scopes,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const getApplicationUserConsentScopes = async (applicationId: string) =>
|
||||
useConsentUserScopes.findAllByApplicationId(applicationId);
|
||||
|
||||
return {
|
||||
validateThirdPartyApplicationById,
|
||||
findApplicationScopesForResourceIndicator,
|
||||
validateApplicationUserConsentScopes,
|
||||
assignApplicationUserConsentScopes,
|
||||
getApplicationUserConsentOrganizationScopes,
|
||||
getApplicationUserConsentResourceScopes,
|
||||
getApplicationUserConsentScopes,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { type UserScope } from '@logto/core-kit';
|
||||
import {
|
||||
ApplicationUserConsentOrganizationScopes,
|
||||
ApplicationUserConsentResourceScopes,
|
||||
|
@ -6,7 +7,8 @@ import {
|
|||
OrganizationScopes,
|
||||
Scopes,
|
||||
} from '@logto/schemas';
|
||||
import { type CommonQueryMethods } from 'slonik';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
|
||||
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
|
||||
import { TwoRelationsQueries } from '#src/utils/RelationQueries.js';
|
||||
|
@ -34,7 +36,20 @@ export const createApplicationUserConsentUserScopeQueries = (pool: CommonQueryMe
|
|||
onConflict: { ignore: true },
|
||||
});
|
||||
|
||||
const findAllByApplicationId = async (applicationId: string) => {
|
||||
const { table, fields } = convertToIdentifiers(ApplicationUserConsentUserScopes);
|
||||
|
||||
const scopes = await pool.any<{ userScope: UserScope }>(sql`
|
||||
select ${fields.userScope}
|
||||
from ${table}
|
||||
where ${fields.applicationId} = ${applicationId}
|
||||
`);
|
||||
|
||||
return scopes.map(({ userScope }) => userScope);
|
||||
};
|
||||
|
||||
return {
|
||||
insert,
|
||||
findAllByApplicationId,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -34,6 +34,35 @@
|
|||
"description": "Any of the given organization scope, resource scope or user scope is not found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"summary": "List all the user consent scopes of an application.",
|
||||
"description": "List all the user consent scopes of an application by application id",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All the user consent scopes of the application are listed successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"organizationScopes": {
|
||||
"description": "A list of organization scope details assigned to the application."
|
||||
},
|
||||
"resourceScopes": {
|
||||
"description": "A list of resource scope details grouped by resource id assigned to the application."
|
||||
},
|
||||
"userScopes": {
|
||||
"description": "A list of user scope enum value assigned to the application."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The application is not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
import { applicationUserConsentScopesResponseGuard } from '@logto/schemas';
|
||||
import { object, string, nativeEnum } from 'zod';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
@ -9,11 +10,17 @@ export default function applicationUserConsentScopeRoutes<T extends AuthedRouter
|
|||
...[
|
||||
router,
|
||||
{
|
||||
queries: {
|
||||
applications: { findApplicationById },
|
||||
},
|
||||
libraries: {
|
||||
applications: {
|
||||
validateThirdPartyApplicationById,
|
||||
validateApplicationUserConsentScopes,
|
||||
assignApplicationUserConsentScopes,
|
||||
getApplicationUserConsentOrganizationScopes,
|
||||
getApplicationUserConsentResourceScopes,
|
||||
getApplicationUserConsentScopes,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -49,4 +56,35 @@ export default function applicationUserConsentScopeRoutes<T extends AuthedRouter
|
|||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/applications/:applicationId/user-consent-scopes',
|
||||
koaGuard({
|
||||
params: object({
|
||||
applicationId: string(),
|
||||
}),
|
||||
response: applicationUserConsentScopesResponseGuard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { applicationId } = ctx.guard.params;
|
||||
|
||||
await findApplicationById(applicationId);
|
||||
|
||||
// Note: The following queries will return full data schema, we rely on the response guard to filter out the fields we don't need.
|
||||
const [organizationScopes, resourceScopes, userScopes] = await Promise.all([
|
||||
getApplicationUserConsentOrganizationScopes(applicationId),
|
||||
getApplicationUserConsentResourceScopes(applicationId),
|
||||
getApplicationUserConsentScopes(applicationId),
|
||||
]);
|
||||
|
||||
ctx.body = {
|
||||
organizationScopes,
|
||||
resourceScopes,
|
||||
userScopes,
|
||||
};
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,14 @@ type TableInfo<
|
|||
guard: z.ZodType<Schema, z.ZodTypeDef, unknown>;
|
||||
};
|
||||
|
||||
type InferSchema<T> = T extends TableInfo<infer _, infer _, infer _, infer Schema> ? Schema : never;
|
||||
type InferSchema<T> = T extends TableInfo<
|
||||
infer _,
|
||||
infer _,
|
||||
Extract<keyof (infer Schema), string>,
|
||||
infer Schema
|
||||
>
|
||||
? Schema
|
||||
: never;
|
||||
|
||||
type CamelCaseIdObject<T extends string> = KeysToCamelCase<{
|
||||
[Key in `${T}_id`]: string;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { type UserScope } from '@logto/core-kit';
|
||||
import { type ApplicationUserConsentScopesResponse } from '@logto/schemas';
|
||||
|
||||
import { authedAdminApi } from './api.js';
|
||||
|
||||
|
@ -10,3 +11,8 @@ export const assignUserConsentScopes = async (
|
|||
userScopes?: UserScope[];
|
||||
}
|
||||
) => authedAdminApi.post(`applications/${applicationId}/user-consent-scopes`, { json: payload });
|
||||
|
||||
export const getUserConsentScopes = async (applicationId: string) =>
|
||||
authedAdminApi
|
||||
.get(`applications/${applicationId}/user-consent-scopes`)
|
||||
.json<ApplicationUserConsentScopesResponse>();
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
|
||||
import { assignUserConsentScopes } from '#src/api/application-user-consent-scope.js';
|
||||
import {
|
||||
assignUserConsentScopes,
|
||||
getUserConsentScopes,
|
||||
} from '#src/api/application-user-consent-scope.js';
|
||||
import { createApplication, deleteApplication } from '#src/api/application.js';
|
||||
import { OrganizationScopeApi } from '#src/api/organization-scope.js';
|
||||
import { createResource, deleteResource } from '#src/api/resource.js';
|
||||
|
@ -122,4 +125,52 @@ describe('assign user consent scopes to application', () => {
|
|||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should return 404 when trying to get consent scopes from non-existing application', async () => {
|
||||
await expectRejects(getUserConsentScopes('non-existing-application'), {
|
||||
code: 'entity.not_exists_with_id',
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return consent scopes successfully', async () => {
|
||||
// This test depends on the previous success assignment test
|
||||
const result = await getUserConsentScopes(applicationIds.get('thirdPartyApp')!);
|
||||
|
||||
expect(result.organizationScopes.length).toBe(organizationScopes.size);
|
||||
|
||||
for (const organizationScopeId of organizationScopes.values()) {
|
||||
expect(result.organizationScopes.some(({ id }) => id === organizationScopeId)).toBeTruthy();
|
||||
}
|
||||
|
||||
expect(result.resourceScopes.length).toBe(1);
|
||||
expect(result.resourceScopes[0]!.resource.id).toBe(Array.from(resourceIds)[0]);
|
||||
expect(result.resourceScopes[0]!.scopes.length).toBe(resourceScopes.size);
|
||||
|
||||
for (const resourceScopeId of resourceScopes.values()) {
|
||||
expect(
|
||||
result.resourceScopes[0]!.scopes.some(({ id }) => id === resourceScopeId)
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
expect(result.userScopes.length).toBe(3);
|
||||
|
||||
for (const userScope of [UserScope.Profile, UserScope.Email, UserScope.OrganizationRoles]) {
|
||||
expect(result.userScopes.includes(userScope)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return empty consent scopes when no scopes assigned', async () => {
|
||||
const newApp = await createApplication('new-app', ApplicationType.Traditional, {
|
||||
isThirdParty: true,
|
||||
});
|
||||
|
||||
const result = await getUserConsentScopes(newApp.id);
|
||||
|
||||
expect(result.organizationScopes.length).toBe(0);
|
||||
expect(result.resourceScopes.length).toBe(0);
|
||||
expect(result.userScopes.length).toBe(0);
|
||||
|
||||
await deleteApplication(newApp.id);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import { type z } from 'zod';
|
||||
import { UserScope } from '@logto/core-kit';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Applications, type Application } from '../db-entries/index.js';
|
||||
import {
|
||||
Applications,
|
||||
type Application,
|
||||
OrganizationScopes,
|
||||
Resources,
|
||||
Scopes,
|
||||
} from '../db-entries/index.js';
|
||||
|
||||
export type ApplicationResponse = Application & { isAdmin: boolean };
|
||||
|
||||
|
@ -31,3 +38,20 @@ export const applicationPatchGuard = applicationCreateGuard.partial().omit({
|
|||
type: true,
|
||||
isThirdParty: true,
|
||||
});
|
||||
|
||||
export const applicationUserConsentScopesResponseGuard = z.object({
|
||||
organizationScopes: z.array(
|
||||
OrganizationScopes.guard.pick({ id: true, name: true, description: true })
|
||||
),
|
||||
resourceScopes: z.array(
|
||||
z.object({
|
||||
resource: Resources.guard.pick({ id: true, name: true, indicator: true }),
|
||||
scopes: z.array(Scopes.guard.pick({ id: true, name: true, description: true })),
|
||||
})
|
||||
),
|
||||
userScopes: z.array(z.nativeEnum(UserScope)),
|
||||
});
|
||||
|
||||
export type ApplicationUserConsentScopesResponse = z.infer<
|
||||
typeof applicationUserConsentScopesResponseGuard
|
||||
>;
|
||||
|
|
Loading…
Add table
Reference in a new issue