Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(http): interceptor API #29

Merged
merged 4 commits into from
Jan 22, 2025
Merged
Changes from 1 commit
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
38 changes: 21 additions & 17 deletions src/auth/manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import createDebug from 'debug';

import { HttpClient } from '../http';
import { URLHelper } from '../utilities';

import { AuthProviderFactory } from './factory';
import { ApiTokenAuthProvider, UsersPermissionsAuthProvider } from './providers';
@@ -115,13 +114,16 @@ export class AuthManager {
try {
debug('trying to authenticate with %s', this._authProvider.name);

await this._authProvider.authenticate(http);
// Create and use a client free of any custom interceptor to avoid infinite auth loop
const client = http.create(undefined, false);

await this._authProvider.authenticate(client);

this._isAuthenticated = true;

debug('authentication successful');
} catch {
debug('authentication failed');
} catch (e) {
debug(`authentication failed: ${e}`);
this._isAuthenticated = false;
}
}
@@ -140,21 +142,23 @@ export class AuthManager {
* console.log(request.headers.get('Authorization')) // 'Bearer <token>'
* ```
*/
authenticateRequest(request: Request) {
if (this._authProvider) {
const { headers } = this._authProvider;

for (const [key, value] of Object.entries(headers)) {
request.headers.set(key, value);

debug('added %o header to %o query', key, URLHelper.toReadablePath(request.url));
}
} else {
debug(
'no auth provider is set. skipping headers for %s query',
URLHelper.toReadablePath(request.url)
authenticateRequest(request: RequestInit) {
// If no auth provider is set, skip
if (!this._authProvider) {
return;
}

const { headers } = request;

if (!(headers instanceof Headers)) {
throw new Error(
`Invalid request headers, headers must be an instance of Headers but found "${typeof headers}"`
);
}

for (const [key, value] of Object.entries(this._authProvider.headers)) {
headers.set(key, value);
}
}

/**
10 changes: 5 additions & 5 deletions src/auth/providers/api-token.ts
Original file line number Diff line number Diff line change
@@ -31,18 +31,18 @@ export class ApiTokenAuthProvider extends AbstractAuthProvider<ApiTokenAuthProvi
return ApiTokenAuthProvider.identifier;
}

private get token(): string {
private get _token(): string {
return this._options.token;
}

preflightValidation(): void {
debug('validating provider configuration');

if ((typeof this.token as unknown) !== 'string' || this.token.trim().length === 0) {
debug('invalid api token provided: %o (%o)', this.token, typeof this.token);
if ((typeof this._token as unknown) !== 'string' || this._token.trim().length === 0) {
debug('invalid api token provided: %o (%o)', this._token, typeof this._token);

throw new StrapiSDKValidationError(
`A valid API token is required when using the api-token auth strategy. Got "${this.token}"`
`A valid API token is required when using the api-token auth strategy. Got "${this._token}"`
);
}

@@ -56,7 +56,7 @@ export class ApiTokenAuthProvider extends AbstractAuthProvider<ApiTokenAuthProvi

get headers() {
return {
Authorization: `Bearer ${this.token}`,
Authorization: `Bearer ${this._token}`,
};
}
}
37 changes: 22 additions & 15 deletions src/auth/providers/users-permissions.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import { AbstractAuthProvider } from './abstract';
const debug = createDebug('sdk:auth:provider:users-permissions');

const USERS_PERMISSIONS_AUTH_STRATEGY_IDENTIFIER = 'users-permissions';
const LOCAL_AUTH_ENDPOINT = '/auth/local';

/**
* Configuration options for Users & Permissions authentication.
@@ -50,7 +51,7 @@ export class UsersPermissionsAuthProvider extends AbstractAuthProvider<UsersPerm
return UsersPermissionsAuthProvider.identifier;
}

private get credentials(): UsersPermissionsAuthPayload {
private get _credentials(): UsersPermissionsAuthPayload {
return {
identifier: this._options.identifier,
password: this._options.password,
@@ -102,21 +103,27 @@ export class UsersPermissionsAuthProvider extends AbstractAuthProvider<UsersPerm
}

async authenticate(httpClient: HttpClient): Promise<void> {
const { baseURL } = httpClient;
const { identifier, password } = this.credentials;

const localAuthURL = `${baseURL}/auth/local`;

debug('trying to authenticate with %o as %o at %o ', this.name, identifier, localAuthURL);

const request = new Request(localAuthURL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier, password }),
});
const { identifier, password } = this._credentials;

debug(
'trying to authenticate with %o as %o at %o ',
this.name,
identifier,
LOCAL_AUTH_ENDPOINT
);

const response = await httpClient.post(
LOCAL_AUTH_ENDPOINT,
JSON.stringify({ identifier, password }),
{
headers: { 'Content-Type': 'application/json' },
}
);

if (!response.ok) {
throw new Error(response.statusText);
}

// Make sure to use the HttpClient's "_fetch" method to not perform authentication in an infinite loop.
const response = await httpClient._fetch(request);
const data = await response.json();

const obfuscatedToken = data.jwt.slice(0, 5) + '...' + data.jwt.slice(-5);
16 changes: 5 additions & 11 deletions src/content-types/collection/manager.ts
Original file line number Diff line number Diff line change
@@ -73,7 +73,7 @@ export class CollectionTypeManager {
url = URLHelper.appendQueryParams(url, queryParams);
}

const response = await this._httpClient.fetch(url, { method: 'GET' });
const response = await this._httpClient.get(url);
const json = await response.json();

debug('found %o %o documents', Number(json?.data?.length), this._pluralName);
@@ -115,7 +115,7 @@ export class CollectionTypeManager {
url = URLHelper.appendQueryParams(url, queryParams);
}

const response = await this._httpClient.fetch(url, { method: 'GET' });
const response = await this._httpClient.get(url);

debug('found the %o document with document id %o', this._pluralName, documentID);

@@ -152,10 +152,7 @@ export class CollectionTypeManager {
url = URLHelper.appendQueryParams(url, queryParams);
}

const response = await this._httpClient.fetch(url, {
method: 'POST',
body: JSON.stringify({ data }),
});
const response = await this._httpClient.post(url, JSON.stringify({ data }));

debug('created the %o document', this._pluralName);

@@ -201,10 +198,7 @@ export class CollectionTypeManager {
url = URLHelper.appendQueryParams(url, queryParams);
}

const response = await this._httpClient.fetch(url, {
method: 'PUT',
body: JSON.stringify({ data }),
});
const response = await this._httpClient.put(url, JSON.stringify({ data }));

debug('updated the %o document with id %o', this._pluralName, documentID);

@@ -243,7 +237,7 @@ export class CollectionTypeManager {
url = URLHelper.appendQueryParams(url, queryParams);
}

await this._httpClient.fetch(url, { method: 'DELETE' });
await this._httpClient.delete(url);

debug('deleted the %o document with id %o', this._pluralName, documentID);
}
9 changes: 3 additions & 6 deletions src/content-types/single/manager.ts
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ export class SingleTypeManager {
path = URLHelper.appendQueryParams(path, queryParams);
}

const response = await this._httpClient.fetch(path, { method: 'GET' });
const response = await this._httpClient.get(path);

debug('the %o document has been fetched', this._singularName);

@@ -113,10 +113,7 @@ export class SingleTypeManager {
url = URLHelper.appendQueryParams(url, queryParams);
}

const response = await this._httpClient.fetch(url, {
method: 'PUT',
body: JSON.stringify({ data }),
});
const response = await this._httpClient.put(url, JSON.stringify({ data }));

debug('the %o document has been updated', this._singularName);

@@ -156,7 +153,7 @@ export class SingleTypeManager {
url = URLHelper.appendQueryParams(url, queryParams);
}

await this._httpClient.fetch(url, { method: 'DELETE' });
await this._httpClient.delete(url);

debug('the %o document has been deleted', this._singularName);
}
1 change: 1 addition & 0 deletions src/formatters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './path';
79 changes: 79 additions & 0 deletions src/formatters/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const DEFAULT_CONFIG = {
trailingSlashes: false,
leadingSlashes: false,
} satisfies FormatterConfig;

type SlashConfig = 'single' | true | false;

export interface FormatterConfig {
trailingSlashes?: SlashConfig;
leadingSlashes?: SlashConfig;
}

export class PathFormatter {
public static format(path: string, config: FormatterConfig = DEFAULT_CONFIG): string {
// Trailing Slashes
path = PathFormatter.formatTrailingSlashes(path, config.trailingSlashes);

// Leading Slashes
path = PathFormatter.formatLeadingSlashes(path, config.leadingSlashes);

return path;
}

public static formatTrailingSlashes(
path: string,
config: SlashConfig = DEFAULT_CONFIG.trailingSlashes
): string {
// Single means making sure there is exactly one trailing slash
if (config === 'single') {
return PathFormatter.ensureSingleTrailingSlash(path);
}

// False means removing all trailing slashes
else if (!config) {
return PathFormatter.removeTrailingSlashes(path);
}

// False or anything else
else {
return path;
}
}

public static removeTrailingSlashes(path: string) {
return path.replace(/\/+$/, '');
}

public static ensureSingleTrailingSlash(path: string) {
return `${this.removeTrailingSlashes(path)}/`;
}

public static formatLeadingSlashes(
path: string,
config: SlashConfig = DEFAULT_CONFIG.leadingSlashes
): string {
// Single means making sure there is exactly one leading slash
if (config === 'single') {
return PathFormatter.ensureSingleLeadingSlash(path);
}

// False means removing all leading slashes
else if (!config) {
return PathFormatter.removeLeadingSlashes(path);
}

// False or anything else
else {
return path;
}
}

public static removeLeadingSlashes(path: string) {
return path.replace(/^\/+/, '');
}

public static ensureSingleLeadingSlash(path: string) {
return `/${this.removeLeadingSlashes(path)}`;
}
}
Loading
Loading