mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Make IDP routes independent of handlers
This commit is contained in:
parent
1ed45c8903
commit
1769b799df
@ -30,11 +30,11 @@
|
||||
"comment": "Converts redirect errors to location JSON responses.",
|
||||
"@id": "urn:solid-server:auth:password:LocationInteractionHandler",
|
||||
"@type": "LocationInteractionHandler",
|
||||
"LocationInteractionHandler:_source" : { "@id": "urn:solid-server:auth:password:RouteInteractionHandler" }
|
||||
"LocationInteractionHandler:_source" : { "@id": "urn:solid-server:auth:password:InteractionRouteHandler" }
|
||||
},
|
||||
{
|
||||
"comment": "Handles every interaction based on their route.",
|
||||
"@id": "urn:solid-server:auth:password:RouteInteractionHandler",
|
||||
"@id": "urn:solid-server:auth:password:InteractionRouteHandler",
|
||||
"@type": "WaterfallHandler",
|
||||
"handlers": [
|
||||
{
|
||||
@ -44,12 +44,12 @@
|
||||
],
|
||||
"@type": "UnsupportedAsyncHandler"
|
||||
},
|
||||
{ "@id": "urn:solid-server:auth:password:IndexRoute" },
|
||||
{ "@id": "urn:solid-server:auth:password:PromptRoute" },
|
||||
{ "@id": "urn:solid-server:auth:password:LoginRoute" },
|
||||
{ "@id": "urn:solid-server:auth:password:ExistingLoginRoute" },
|
||||
{ "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" },
|
||||
{ "@id": "urn:solid-server:auth:password:ResetPasswordRoute" }
|
||||
{ "@id": "urn:solid-server:auth:password:IndexRouteHandler" },
|
||||
{ "@id": "urn:solid-server:auth:password:PromptRouteHandler" },
|
||||
{ "@id": "urn:solid-server:auth:password:LoginRouteHandler" },
|
||||
{ "@id": "urn:solid-server:auth:password:ExistingLoginRouteHandler" },
|
||||
{ "@id": "urn:solid-server:auth:password:ForgotPasswordRouteHandler" },
|
||||
{ "@id": "urn:solid-server:auth:password:ResetPasswordRouteHandler" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -3,11 +3,16 @@
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Handles the interaction that occurs when a logged in user wants to authenticate with a new app.",
|
||||
"@id": "urn:solid-server:auth:password:ExistingLoginRouteHandler",
|
||||
"@type":"InteractionRouteHandler",
|
||||
"route": {
|
||||
"@id": "urn:solid-server:auth:password:ExistingLoginRoute",
|
||||
"@type": "RelativeInteractionRoute",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
|
||||
"relativePath": "/consent/",
|
||||
"relativePath": "/consent/"
|
||||
},
|
||||
"source": {
|
||||
"@id": "urn:solid-server:auth:password:ExistingLoginHandler",
|
||||
"@type": "ExistingLoginHandler",
|
||||
"interactionCompleter": { "@type": "BaseInteractionCompleter" }
|
||||
}
|
||||
|
@ -3,11 +3,16 @@
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Handles the forgot password interaction",
|
||||
"@id": "urn:solid-server:auth:password:ForgotPasswordRouteHandler",
|
||||
"@type":"InteractionRouteHandler",
|
||||
"route": {
|
||||
"@id": "urn:solid-server:auth:password:ForgotPasswordRoute",
|
||||
"@type": "RelativeInteractionRoute",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
|
||||
"relativePath": "/forgotpassword/",
|
||||
"relativePath": "/forgotpassword/"
|
||||
},
|
||||
"source": {
|
||||
"@id": "urn:solid-server:auth:password:ForgotPasswordHandler",
|
||||
"@type": "ForgotPasswordHandler",
|
||||
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
||||
"args_templateEngine": {
|
||||
|
@ -3,11 +3,16 @@
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Root API entry. Returns an empty body so we can add controls pointing to other interaction routes.",
|
||||
"@id": "urn:solid-server:auth:password:IndexRouteHandler",
|
||||
"@type": "InteractionRouteHandler",
|
||||
"route": {
|
||||
"@id": "urn:solid-server:auth:password:IndexRoute",
|
||||
"@type": "RelativeInteractionRoute",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"relativePath": "/idp/",
|
||||
"relativePath": "/idp/"
|
||||
},
|
||||
"source": {
|
||||
"@id": "urn:solid-server:auth:password:IndexHandler",
|
||||
"@type": "FixedInteractionHandler",
|
||||
"response": {}
|
||||
}
|
||||
|
@ -3,11 +3,16 @@
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Handles the login interaction",
|
||||
"@id": "urn:solid-server:auth:password:LoginRouteHandler",
|
||||
"@type": "InteractionRouteHandler",
|
||||
"route": {
|
||||
"@id": "urn:solid-server:auth:password:LoginRoute",
|
||||
"@type": "RelativeInteractionRoute",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
|
||||
"relativePath": "/login/",
|
||||
"relativePath": "/login/"
|
||||
},
|
||||
"source": {
|
||||
"@id": "urn:solid-server:auth:password:LoginHandler",
|
||||
"@type": "LoginHandler",
|
||||
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
||||
"interactionCompleter": { "@type": "BaseInteractionCompleter" }
|
||||
|
@ -3,10 +3,14 @@
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Handles OIDC redirects containing a prompt, such as login or consent.",
|
||||
"@id": "urn:solid-server:auth:password:PromptRouteHandler",
|
||||
"@type": "InteractionRouteHandler",
|
||||
"route": {
|
||||
"@id": "urn:solid-server:auth:password:PromptRoute",
|
||||
"@type": "RelativeInteractionRoute",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
|
||||
"relativePath": "/prompt/",
|
||||
"relativePath": "/prompt/"
|
||||
},
|
||||
"source": {
|
||||
"@type": "PromptHandler",
|
||||
"@id": "urn:solid-server:auth:password:PromptHandler",
|
||||
|
@ -3,11 +3,16 @@
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Handles the reset password interaction",
|
||||
"@id": "urn:solid-server:auth:password:ResetPasswordRouteHandler",
|
||||
"@type": "InteractionRouteHandler",
|
||||
"route": {
|
||||
"@id": "urn:solid-server:auth:password:ResetPasswordRoute",
|
||||
"@type": "RelativeInteractionRoute",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
|
||||
"relativePath": "/resetpassword/",
|
||||
"relativePath": "/resetpassword/"
|
||||
},
|
||||
"source": {
|
||||
"@id": "urn:solid-server:auth:password:ResetPasswordHandler",
|
||||
"@type": "ResetPasswordHandler",
|
||||
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "urn:solid-server:auth:password:RouteInteractionHandler",
|
||||
"@id": "urn:solid-server:auth:password:InteractionRouteHandler",
|
||||
"WaterfallHandler:_handlers": [
|
||||
{
|
||||
"comment": [
|
||||
@ -14,7 +14,7 @@
|
||||
],
|
||||
"@type": "UnsupportedAsyncHandler"
|
||||
},
|
||||
{ "@id": "urn:solid-server:auth:password:RegistrationRoute" }
|
||||
{ "@id": "urn:solid-server:auth:password:RegistrationRouteHandler" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -32,14 +32,6 @@
|
||||
{
|
||||
"HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/register.html.ejs",
|
||||
"HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:RegistrationRoute" }
|
||||
},
|
||||
{
|
||||
"HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/reset-password-response.html.ejs",
|
||||
"HtmlViewHandler:_templates_value": {
|
||||
"@type": "RelativeInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:auth:password:ResetPasswordRoute" },
|
||||
"relativePath": "/response/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -3,11 +3,16 @@
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Handles the register interaction",
|
||||
"@id": "urn:solid-server:auth:password:RegistrationRouteHandler",
|
||||
"@type": "InteractionRouteHandler",
|
||||
"route": {
|
||||
"@id": "urn:solid-server:auth:password:RegistrationRoute",
|
||||
"@type": "RelativeInteractionRoute",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
|
||||
"relativePath": "/register/",
|
||||
"relativePath": "/register/"
|
||||
},
|
||||
"source": {
|
||||
"@id": "urn:solid-server:auth:password:RegistrationHandler",
|
||||
"@type": "RegistrationHandler",
|
||||
"registrationManager": {
|
||||
"@type": "RegistrationManager",
|
||||
|
@ -0,0 +1,16 @@
|
||||
import type { InteractionRoute } from './InteractionRoute';
|
||||
|
||||
/**
|
||||
* A route that returns the input string as path.
|
||||
*/
|
||||
export class AbsolutePathInteractionRoute implements InteractionRoute {
|
||||
private readonly path: string;
|
||||
|
||||
public constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public getPath(): string {
|
||||
return this.path;
|
||||
}
|
||||
}
|
@ -1,36 +1,28 @@
|
||||
import type { Representation } from '../../../http/representation/Representation';
|
||||
import { NotFoundHttpError } from '../../../util/errors/NotFoundHttpError';
|
||||
import { UnsupportedAsyncHandler } from '../../../util/handlers/UnsupportedAsyncHandler';
|
||||
import { InteractionHandler } from '../InteractionHandler';
|
||||
import type { InteractionHandlerInput } from '../InteractionHandler';
|
||||
import { InteractionHandler } from '../InteractionHandler';
|
||||
import type { InteractionRoute } from './InteractionRoute';
|
||||
|
||||
/**
|
||||
* Default implementation of an InteractionHandler with an InteractionRoute.
|
||||
* InteractionHandler that only accepts operations with an expected path.
|
||||
*
|
||||
* Rejects operations that target a different path,
|
||||
* otherwise the input parameters get passed to the source handler.
|
||||
*
|
||||
* In case no source handler is provided it defaults to an {@link UnsupportedAsyncHandler}.
|
||||
* This can be useful if you want an object with just the route.
|
||||
* otherwise the input parameters are passed to the source handler.
|
||||
*/
|
||||
export class BasicInteractionRoute extends InteractionHandler implements InteractionRoute {
|
||||
private readonly path: string;
|
||||
export class InteractionRouteHandler extends InteractionHandler {
|
||||
private readonly route: InteractionRoute;
|
||||
private readonly source: InteractionHandler;
|
||||
|
||||
public constructor(path: string, source?: InteractionHandler) {
|
||||
public constructor(route: InteractionRoute, source: InteractionHandler) {
|
||||
super();
|
||||
this.path = path;
|
||||
this.source = source ?? new UnsupportedAsyncHandler('This route has no associated handler.');
|
||||
}
|
||||
|
||||
public getPath(): string {
|
||||
return this.path;
|
||||
this.route = route;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public async canHandle(input: InteractionHandlerInput): Promise<void> {
|
||||
const { target } = input.operation;
|
||||
const path = this.getPath();
|
||||
const path = this.route.getPath();
|
||||
if (target.path !== path) {
|
||||
throw new NotFoundHttpError();
|
||||
}
|
@ -1,18 +1,16 @@
|
||||
import { joinUrl } from '../../../util/PathUtil';
|
||||
import type { InteractionHandler } from '../InteractionHandler';
|
||||
import { BasicInteractionRoute } from './BasicInteractionRoute';
|
||||
import { AbsolutePathInteractionRoute } from './AbsolutePathInteractionRoute';
|
||||
import type { InteractionRoute } from './InteractionRoute';
|
||||
|
||||
/**
|
||||
* A route that is relative to another route.
|
||||
* The relative path will be joined to the input base,
|
||||
* which can either be an absolute URL or an InteractionRoute of which the path will be used.
|
||||
* The source handler will be called for all operation requests
|
||||
*/
|
||||
export class RelativeInteractionRoute extends BasicInteractionRoute {
|
||||
public constructor(base: InteractionRoute | string, relativePath: string, source?: InteractionHandler) {
|
||||
export class RelativePathInteractionRoute extends AbsolutePathInteractionRoute {
|
||||
public constructor(base: InteractionRoute | string, relativePath: string) {
|
||||
const url = typeof base === 'string' ? base : base.getPath();
|
||||
const path = joinUrl(url, relativePath);
|
||||
super(path, source);
|
||||
super(path);
|
||||
}
|
||||
}
|
@ -145,9 +145,10 @@ export * from './identity/interaction/email-password/util/RegistrationManager';
|
||||
export * from './identity/interaction/email-password/EmailPasswordUtil';
|
||||
|
||||
// Identity/Interaction/Routing
|
||||
export * from './identity/interaction/routing/BasicInteractionRoute';
|
||||
export * from './identity/interaction/routing/AbsolutePathInteractionRoute';
|
||||
export * from './identity/interaction/routing/InteractionRoute';
|
||||
export * from './identity/interaction/routing/RelativeInteractionRoute';
|
||||
export * from './identity/interaction/routing/InteractionRouteHandler';
|
||||
export * from './identity/interaction/routing/RelativePathInteractionRoute';
|
||||
|
||||
// Identity/Interaction/Util
|
||||
export * from './identity/interaction/util/BaseEmailSender';
|
||||
|
@ -0,0 +1,12 @@
|
||||
import {
|
||||
AbsolutePathInteractionRoute,
|
||||
} from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute';
|
||||
|
||||
describe('An AbsolutePathInteractionRoute', (): void => {
|
||||
const path = 'http://example.com/idp/path/';
|
||||
const route = new AbsolutePathInteractionRoute(path);
|
||||
|
||||
it('returns the given path.', async(): Promise<void> => {
|
||||
expect(route.getPath()).toBe('http://example.com/idp/path/');
|
||||
});
|
||||
});
|
@ -1,59 +1,53 @@
|
||||
import type { Operation } from '../../../../../src/http/Operation';
|
||||
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../../src/http/representation/Representation';
|
||||
import type {
|
||||
InteractionHandler,
|
||||
} from '../../../../../src/identity/interaction/InteractionHandler';
|
||||
import { BasicInteractionRoute } from '../../../../../src/identity/interaction/routing/BasicInteractionRoute';
|
||||
import type { InteractionHandler } from '../../../../../src/identity/interaction/InteractionHandler';
|
||||
import type { InteractionRoute } from '../../../../../src/identity/interaction/routing/InteractionRoute';
|
||||
import { InteractionRouteHandler } from '../../../../../src/identity/interaction/routing/InteractionRouteHandler';
|
||||
import { APPLICATION_JSON } from '../../../../../src/util/ContentTypes';
|
||||
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
|
||||
import { createPostJsonOperation } from '../email-password/handler/Util';
|
||||
|
||||
describe('A BasicInteractionRoute', (): void => {
|
||||
describe('An InteractionRouteHandler', (): void => {
|
||||
const path = 'http://example.com/idp/path/';
|
||||
let operation: Operation;
|
||||
let representation: Representation;
|
||||
let route: InteractionRoute;
|
||||
let source: jest.Mocked<InteractionHandler>;
|
||||
let route: BasicInteractionRoute;
|
||||
let handler: InteractionRouteHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = createPostJsonOperation({}, 'http://example.com/idp/path/');
|
||||
operation = createPostJsonOperation({}, path);
|
||||
|
||||
representation = new BasicRepresentation(JSON.stringify({}), APPLICATION_JSON);
|
||||
|
||||
route = {
|
||||
getPath: jest.fn().mockReturnValue(path),
|
||||
};
|
||||
|
||||
source = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn().mockResolvedValue(representation),
|
||||
} as any;
|
||||
|
||||
route = new BasicInteractionRoute(path, source);
|
||||
});
|
||||
|
||||
it('returns the given path.', async(): Promise<void> => {
|
||||
expect(route.getPath()).toBe('http://example.com/idp/path/');
|
||||
handler = new InteractionRouteHandler(route, source);
|
||||
});
|
||||
|
||||
it('rejects other paths.', async(): Promise<void> => {
|
||||
operation = createPostJsonOperation({}, 'http://example.com/idp/otherPath/');
|
||||
await expect(route.canHandle({ operation })).rejects.toThrow(NotFoundHttpError);
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow(NotFoundHttpError);
|
||||
});
|
||||
|
||||
it('rejects input its source cannot handle.', async(): Promise<void> => {
|
||||
source.canHandle.mockRejectedValueOnce(new Error('bad data'));
|
||||
await expect(route.canHandle({ operation })).rejects.toThrow('bad data');
|
||||
await expect(handler.canHandle({ operation })).rejects.toThrow('bad data');
|
||||
});
|
||||
|
||||
it('can handle requests its source can handle.', async(): Promise<void> => {
|
||||
await expect(route.canHandle({ operation })).resolves.toBeUndefined();
|
||||
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('lets its source handle requests.', async(): Promise<void> => {
|
||||
await expect(route.handle({ operation })).resolves.toBe(representation);
|
||||
});
|
||||
|
||||
it('defaults to an UnsupportedAsyncHandler if no source is provided.', async(): Promise<void> => {
|
||||
route = new BasicInteractionRoute(path);
|
||||
await expect(route.canHandle({ operation })).rejects.toThrow('This route has no associated handler.');
|
||||
await expect(route.handle({ operation })).rejects.toThrow('This route has no associated handler.');
|
||||
await expect(handler.handle({ operation })).resolves.toBe(representation);
|
||||
});
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
import type {
|
||||
InteractionHandler,
|
||||
} from '../../../../../src/identity/interaction/InteractionHandler';
|
||||
import type { InteractionRoute } from '../../../../../src/identity/interaction/routing/InteractionRoute';
|
||||
import { RelativeInteractionRoute } from '../../../../../src/identity/interaction/routing/RelativeInteractionRoute';
|
||||
|
||||
describe('A RelativeInteractionRoute', (): void => {
|
||||
const relativePath = '/relative/';
|
||||
let route: jest.Mocked<InteractionRoute>;
|
||||
let source: jest.Mocked<InteractionHandler>;
|
||||
let relativeRoute: RelativeInteractionRoute;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
route = {
|
||||
getPath: jest.fn().mockReturnValue('http://example.com/'),
|
||||
} as any;
|
||||
|
||||
source = {
|
||||
canHandle: jest.fn(),
|
||||
} as any;
|
||||
});
|
||||
|
||||
it('returns the joined path.', async(): Promise<void> => {
|
||||
relativeRoute = new RelativeInteractionRoute(route, relativePath, source);
|
||||
expect(relativeRoute.getPath()).toBe('http://example.com/relative/');
|
||||
|
||||
relativeRoute = new RelativeInteractionRoute('http://example.com/', relativePath, source);
|
||||
expect(relativeRoute.getPath()).toBe('http://example.com/relative/');
|
||||
});
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import type { InteractionRoute } from '../../../../../src/identity/interaction/routing/InteractionRoute';
|
||||
import {
|
||||
RelativePathInteractionRoute,
|
||||
} from '../../../../../src/identity/interaction/routing/RelativePathInteractionRoute';
|
||||
|
||||
describe('A RelativePathInteractionRoute', (): void => {
|
||||
const relativePath = '/relative/';
|
||||
let route: jest.Mocked<InteractionRoute>;
|
||||
let relativeRoute: RelativePathInteractionRoute;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
route = {
|
||||
getPath: jest.fn().mockReturnValue('http://example.com/'),
|
||||
};
|
||||
});
|
||||
|
||||
it('returns the joined path.', async(): Promise<void> => {
|
||||
relativeRoute = new RelativePathInteractionRoute(route, relativePath);
|
||||
expect(relativeRoute.getPath()).toBe('http://example.com/relative/');
|
||||
|
||||
relativeRoute = new RelativePathInteractionRoute('http://example.com/test/', relativePath);
|
||||
expect(relativeRoute.getPath()).toBe('http://example.com/test/relative/');
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user