@mapcreator/api 5.0.0-alpha.88 → 5.0.0-alpha.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -29
- package/README.md +86 -86
- package/cjs/api/font.d.ts.map +1 -1
- package/cjs/api/font.js.map +1 -1
- package/cjs/api/insetMap.d.ts.map +1 -1
- package/cjs/api/insetMap.js +3 -5
- package/cjs/api/insetMap.js.map +1 -1
- package/cjs/api/job.d.ts +1 -1
- package/cjs/api/job.d.ts.map +1 -1
- package/cjs/api/job.js +2 -2
- package/cjs/api/job.js.map +1 -1
- package/cjs/api/jobRevision.d.ts +1 -1
- package/cjs/api/jobRevision.d.ts.map +1 -1
- package/cjs/api/jobRevision.js +8 -9
- package/cjs/api/jobRevision.js.map +1 -1
- package/cjs/oauth.d.ts +9 -6
- package/cjs/oauth.d.ts.map +1 -1
- package/cjs/oauth.js +28 -203
- package/cjs/oauth.js.map +1 -1
- package/cjs/utils.d.ts.map +1 -1
- package/cjs/utils.js +3 -3
- package/cjs/utils.js.map +1 -1
- package/esm/api/font.d.ts.map +1 -1
- package/esm/api/font.js +1 -1
- package/esm/api/font.js.map +1 -1
- package/esm/api/insetMap.d.ts.map +1 -1
- package/esm/api/insetMap.js +4 -6
- package/esm/api/insetMap.js.map +1 -1
- package/esm/api/job.d.ts +1 -1
- package/esm/api/job.d.ts.map +1 -1
- package/esm/api/job.js +2 -2
- package/esm/api/job.js.map +1 -1
- package/esm/api/jobRevision.d.ts +1 -1
- package/esm/api/jobRevision.d.ts.map +1 -1
- package/esm/api/jobRevision.js +8 -9
- package/esm/api/jobRevision.js.map +1 -1
- package/esm/oauth.d.ts +9 -6
- package/esm/oauth.d.ts.map +1 -1
- package/esm/oauth.js +27 -201
- package/esm/oauth.js.map +1 -1
- package/esm/utils.d.ts.map +1 -1
- package/esm/utils.js +4 -4
- package/esm/utils.js.map +1 -1
- package/package.json +80 -80
- package/src/README.md +126 -126
- package/src/api/font.ts +9 -1
- package/src/api/insetMap.ts +4 -5
- package/src/api/job.ts +2 -2
- package/src/api/jobRevision.ts +8 -7
- package/src/oauth.ts +28 -258
- package/src/utils.ts +4 -4
package/src/README.md
CHANGED
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
### Used type system
|
|
2
|
-
|
|
3
|
-
We use type declarations for both the data coming over the wire and the data used by the application. In general, these types differ only in the presence of additional fields in the data arriving over the network (like `created_at`), and used naming convention. Data over the network uses the so-called snake case.
|
|
4
|
-
|
|
5
|
-
All in all, we can use TypeScript's native mechanisms to convert one type to another and fully define just one type:
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import type { CamelCasedProperties } from 'type-fest';
|
|
9
|
-
|
|
10
|
-
type ApiType =
|
|
11
|
-
| ({
|
|
12
|
-
data: {
|
|
13
|
-
prop: unknown;
|
|
14
|
-
} & ApiCommonData;
|
|
15
|
-
} & Omit<ApiSuccess, 'data'>)
|
|
16
|
-
| ApiError;
|
|
17
|
-
|
|
18
|
-
type AppType = CamelCasedProperties<Omit<Exclude<ApiType, ApiError>['data'], keyof ApiCommonData>>;
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
or in reverse order:
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import type { SnakeCasedProperties } from 'type-fest';
|
|
25
|
-
|
|
26
|
-
type AppType = {
|
|
27
|
-
prop: unknown;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type ApiType =
|
|
31
|
-
| ({
|
|
32
|
-
data: SnakeCasedProperties<AppType> & ApiCommonData;
|
|
33
|
-
} & Omit<ApiSuccess, 'data'>)
|
|
34
|
-
| ApiError;
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
But the decision was made not to do this, since it may be more difficult for the developer to make the conversion in their head than to see it in front of their eyes.
|
|
38
|
-
|
|
39
|
-
### Using a `request()`
|
|
40
|
-
|
|
41
|
-
The function has the following signature:
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
async function request<I extends ApiCommon, O extends Record<string, unknown> | string>(
|
|
45
|
-
path: string,
|
|
46
|
-
body?: XMLHttpRequestBodyInit | Record<string | number, unknown> | null,
|
|
47
|
-
extraHeaders?: Record<string, string> | null,
|
|
48
|
-
extraOptions?: ExtraOptions<I, O>,
|
|
49
|
-
): Promise<O | O[]> { /* ... */ }
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Let's take it step by step.
|
|
53
|
-
|
|
54
|
-
`I extends ApiCommon` - represents the type of data we receive over the network.
|
|
55
|
-
|
|
56
|
-
`O extends Record<string, unknown> | string` - represents the data type that will be used in the application.
|
|
57
|
-
|
|
58
|
-
Ideally you should describe and convey both types. This will help to check the data types in the arguments passed.
|
|
59
|
-
See current data types for an example.
|
|
60
|
-
|
|
61
|
-
`path: string` - the path to the resource, must include the API version, but must not include the schema or authority.
|
|
62
|
-
Example: `/v1/jobs/12345`
|
|
63
|
-
|
|
64
|
-
`body?: XMLHttpRequestBodyInit | Record<string | number, unknown> | null` - any meaningful body type. In general,
|
|
65
|
-
the presence of an JSON object is assumed (or the absence of one for methods that only request data), but you can
|
|
66
|
-
also pass `Blob`, `FormData`, `URLSearchParams` or just `ArrayBuffer`. The required content type will be added to
|
|
67
|
-
the headers automatically.
|
|
68
|
-
|
|
69
|
-
`extraHeaders?: Record<string, string> | null` - the object with additional headers.
|
|
70
|
-
|
|
71
|
-
`extraOptions?: ExtraOptions<I, O>` - where `ExtraOptions<I, O>` is defined like this:
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
interface ExtraOptions<I extends ApiCommon, O extends Record<string, unknown> | string> {
|
|
75
|
-
method?: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
76
|
-
revivers?: O extends Record<string, unknown> ? Revivers<I, O> : never;
|
|
77
|
-
sendNull?: boolean;
|
|
78
|
-
withMeta?: boolean;
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Most fields are self-explanatory.
|
|
83
|
-
|
|
84
|
-
`sendNull` can be used if you really want to pass `null` as body content.
|
|
85
|
-
|
|
86
|
-
`revivers` is used to specify an object that can modify the behavior of the internal handler of data coming over
|
|
87
|
-
the network. Let's take a closer look at this moment.
|
|
88
|
-
|
|
89
|
-
#### Revivers
|
|
90
|
-
|
|
91
|
-
By default, the `request()` function does the following things with data coming over the network:
|
|
92
|
-
|
|
93
|
-
- It removes `created_at`, `updated_at`, `deleted_at` fields from the output objects.
|
|
94
|
-
- It preserves all the remaining fields but converts their names into camelCase.
|
|
95
|
-
|
|
96
|
-
When passing an object with revivers you can a couple of things:
|
|
97
|
-
|
|
98
|
-
- You can list the fields that you want **to exclude** from the result object. To do this, the field must be assigned an
|
|
99
|
-
`undefined` value.
|
|
100
|
-
- You can **add** new fields or **modify** the type of existing ones. To do this, you need to pass a function as a field
|
|
101
|
-
value, which will receive the original object as input.
|
|
102
|
-
|
|
103
|
-
Example:
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
const jobRevivers: Revivers<ApiJob, Job> = {
|
|
107
|
-
user_id: undefined,
|
|
108
|
-
description: undefined,
|
|
109
|
-
share_token: undefined,
|
|
110
|
-
autosave_preview_path: undefined,
|
|
111
|
-
job_folder_id: undefined,
|
|
112
|
-
|
|
113
|
-
jobTypeId: () => 9,
|
|
114
|
-
createdAt: (data: ApiJobData) => data.created_at as string,
|
|
115
|
-
previewPath: (data: ApiJobData) => data.autosave_preview_path ?? undefined,
|
|
116
|
-
};
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
`user_id`, `description`, `share_token`, `autosave_preview_path`, `job_folder_id` fields will be excluded from the
|
|
120
|
-
result object.
|
|
121
|
-
|
|
122
|
-
`jobTypeId` will be always **9**.
|
|
123
|
-
|
|
124
|
-
`createdAt` will be returned (please note that that field is excluded by default)
|
|
125
|
-
|
|
126
|
-
`previewPath` - some actions will be performed with the source data.
|
|
1
|
+
### Used type system
|
|
2
|
+
|
|
3
|
+
We use type declarations for both the data coming over the wire and the data used by the application. In general, these types differ only in the presence of additional fields in the data arriving over the network (like `created_at`), and used naming convention. Data over the network uses the so-called snake case.
|
|
4
|
+
|
|
5
|
+
All in all, we can use TypeScript's native mechanisms to convert one type to another and fully define just one type:
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import type { CamelCasedProperties } from 'type-fest';
|
|
9
|
+
|
|
10
|
+
type ApiType =
|
|
11
|
+
| ({
|
|
12
|
+
data: {
|
|
13
|
+
prop: unknown;
|
|
14
|
+
} & ApiCommonData;
|
|
15
|
+
} & Omit<ApiSuccess, 'data'>)
|
|
16
|
+
| ApiError;
|
|
17
|
+
|
|
18
|
+
type AppType = CamelCasedProperties<Omit<Exclude<ApiType, ApiError>['data'], keyof ApiCommonData>>;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
or in reverse order:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import type { SnakeCasedProperties } from 'type-fest';
|
|
25
|
+
|
|
26
|
+
type AppType = {
|
|
27
|
+
prop: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ApiType =
|
|
31
|
+
| ({
|
|
32
|
+
data: SnakeCasedProperties<AppType> & ApiCommonData;
|
|
33
|
+
} & Omit<ApiSuccess, 'data'>)
|
|
34
|
+
| ApiError;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
But the decision was made not to do this, since it may be more difficult for the developer to make the conversion in their head than to see it in front of their eyes.
|
|
38
|
+
|
|
39
|
+
### Using a `request()`
|
|
40
|
+
|
|
41
|
+
The function has the following signature:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
async function request<I extends ApiCommon, O extends Record<string, unknown> | string>(
|
|
45
|
+
path: string,
|
|
46
|
+
body?: XMLHttpRequestBodyInit | Record<string | number, unknown> | null,
|
|
47
|
+
extraHeaders?: Record<string, string> | null,
|
|
48
|
+
extraOptions?: ExtraOptions<I, O>,
|
|
49
|
+
): Promise<O | O[]> { /* ... */ }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Let's take it step by step.
|
|
53
|
+
|
|
54
|
+
`I extends ApiCommon` - represents the type of data we receive over the network.
|
|
55
|
+
|
|
56
|
+
`O extends Record<string, unknown> | string` - represents the data type that will be used in the application.
|
|
57
|
+
|
|
58
|
+
Ideally you should describe and convey both types. This will help to check the data types in the arguments passed.
|
|
59
|
+
See current data types for an example.
|
|
60
|
+
|
|
61
|
+
`path: string` - the path to the resource, must include the API version, but must not include the schema or authority.
|
|
62
|
+
Example: `/v1/jobs/12345`
|
|
63
|
+
|
|
64
|
+
`body?: XMLHttpRequestBodyInit | Record<string | number, unknown> | null` - any meaningful body type. In general,
|
|
65
|
+
the presence of an JSON object is assumed (or the absence of one for methods that only request data), but you can
|
|
66
|
+
also pass `Blob`, `FormData`, `URLSearchParams` or just `ArrayBuffer`. The required content type will be added to
|
|
67
|
+
the headers automatically.
|
|
68
|
+
|
|
69
|
+
`extraHeaders?: Record<string, string> | null` - the object with additional headers.
|
|
70
|
+
|
|
71
|
+
`extraOptions?: ExtraOptions<I, O>` - where `ExtraOptions<I, O>` is defined like this:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
interface ExtraOptions<I extends ApiCommon, O extends Record<string, unknown> | string> {
|
|
75
|
+
method?: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
76
|
+
revivers?: O extends Record<string, unknown> ? Revivers<I, O> : never;
|
|
77
|
+
sendNull?: boolean;
|
|
78
|
+
withMeta?: boolean;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Most fields are self-explanatory.
|
|
83
|
+
|
|
84
|
+
`sendNull` can be used if you really want to pass `null` as body content.
|
|
85
|
+
|
|
86
|
+
`revivers` is used to specify an object that can modify the behavior of the internal handler of data coming over
|
|
87
|
+
the network. Let's take a closer look at this moment.
|
|
88
|
+
|
|
89
|
+
#### Revivers
|
|
90
|
+
|
|
91
|
+
By default, the `request()` function does the following things with data coming over the network:
|
|
92
|
+
|
|
93
|
+
- It removes `created_at`, `updated_at`, `deleted_at` fields from the output objects.
|
|
94
|
+
- It preserves all the remaining fields but converts their names into camelCase.
|
|
95
|
+
|
|
96
|
+
When passing an object with revivers you can a couple of things:
|
|
97
|
+
|
|
98
|
+
- You can list the fields that you want **to exclude** from the result object. To do this, the field must be assigned an
|
|
99
|
+
`undefined` value.
|
|
100
|
+
- You can **add** new fields or **modify** the type of existing ones. To do this, you need to pass a function as a field
|
|
101
|
+
value, which will receive the original object as input.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const jobRevivers: Revivers<ApiJob, Job> = {
|
|
107
|
+
user_id: undefined,
|
|
108
|
+
description: undefined,
|
|
109
|
+
share_token: undefined,
|
|
110
|
+
autosave_preview_path: undefined,
|
|
111
|
+
job_folder_id: undefined,
|
|
112
|
+
|
|
113
|
+
jobTypeId: () => 9,
|
|
114
|
+
createdAt: (data: ApiJobData) => data.created_at as string,
|
|
115
|
+
previewPath: (data: ApiJobData) => data.autosave_preview_path ?? undefined,
|
|
116
|
+
};
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`user_id`, `description`, `share_token`, `autosave_preview_path`, `job_folder_id` fields will be excluded from the
|
|
120
|
+
result object.
|
|
121
|
+
|
|
122
|
+
`jobTypeId` will be always **9**.
|
|
123
|
+
|
|
124
|
+
`createdAt` will be returned (please note that that field is excluded by default)
|
|
125
|
+
|
|
126
|
+
`previewPath` - some actions will be performed with the source data.
|
package/src/api/font.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ApiCommonData,
|
|
3
|
+
type ApiError,
|
|
4
|
+
type ApiSuccess,
|
|
5
|
+
type Flatten,
|
|
6
|
+
type Revivers,
|
|
7
|
+
getSearchParams,
|
|
8
|
+
request,
|
|
9
|
+
} from '../utils.js';
|
|
2
10
|
|
|
3
11
|
export type Font = {
|
|
4
12
|
id: number;
|
package/src/api/insetMap.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { apiHost, authenticate, token } from '../oauth.js';
|
|
1
|
+
import { apiHost, authenticate, getAuthorizationHeaders, token } from '../oauth.js';
|
|
2
2
|
import {
|
|
3
3
|
APIError,
|
|
4
4
|
type ApiCommonData,
|
|
@@ -63,10 +63,9 @@ export async function getInsetMap(insetMapId: number): Promise<InsetMap> {
|
|
|
63
63
|
|
|
64
64
|
export async function getInsetMapTopoJson<TopoJSON>(insetMapId: number): Promise<TopoJSON> {
|
|
65
65
|
const href = `${apiHost}/v1/inset-maps/${insetMapId}/json`;
|
|
66
|
-
const headers =
|
|
67
|
-
const response = await fetch(href, { headers
|
|
68
|
-
throw new NetworkError(error?.message ?? error);
|
|
69
|
-
});
|
|
66
|
+
const headers = getAuthorizationHeaders('GET');
|
|
67
|
+
const response = await fetch(href, { headers, ...!token && { credentials: 'include' } })
|
|
68
|
+
.catch((error: Error) => { throw new NetworkError(error?.message ?? error) });
|
|
70
69
|
|
|
71
70
|
if (response.ok) {
|
|
72
71
|
return response.json().catch(() => {
|
package/src/api/job.ts
CHANGED
|
@@ -53,9 +53,9 @@ export const jobRevivers: Revivers<ApiJob, Job> = {
|
|
|
53
53
|
previewPath: (data: ApiJobData) => data.autosave_preview_path ?? undefined,
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
export async function createJob(title: string): Promise<Job> {
|
|
56
|
+
export async function createJob(title: string, skipValidation?: boolean): Promise<Job> {
|
|
57
57
|
const path = `/v1/jobs`;
|
|
58
|
-
const body = { title, job_type_id: 9 };
|
|
58
|
+
const body = { title, job_type_id: 9, skip_validation: skipValidation ?? false };
|
|
59
59
|
const options = { revivers: jobRevivers };
|
|
60
60
|
|
|
61
61
|
// Technically, the returning `data` will contain only the following fields:
|
package/src/api/jobRevision.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { apiHost, authenticate, getAuthorizationHeaders, token } from '../oauth.js';
|
|
1
2
|
import { type ApiLayerData, type Layer, layerRevivers } from './layer.js';
|
|
2
|
-
import { apiHost, authenticate, token } from '../oauth.js';
|
|
3
3
|
import type { ApiMapstyleSetData } from './mapstyleSet.js';
|
|
4
4
|
import type { ApiLanguageData } from './language.js';
|
|
5
5
|
import {
|
|
@@ -70,6 +70,7 @@ export async function createJobRevision(
|
|
|
70
70
|
layers: number[],
|
|
71
71
|
output: FileFormat,
|
|
72
72
|
jobObject: Record<string, unknown>,
|
|
73
|
+
skipValidation?: boolean
|
|
73
74
|
): Promise<JobRevision> {
|
|
74
75
|
const pathname = `/v1/jobs/${jobId}/revisions`;
|
|
75
76
|
const path = `${pathname}?${deletedNoneParam}`;
|
|
@@ -79,6 +80,7 @@ export async function createJobRevision(
|
|
|
79
80
|
object: JSON.stringify(jobObject),
|
|
80
81
|
output,
|
|
81
82
|
layers,
|
|
83
|
+
skip_validation: skipValidation ?? false
|
|
82
84
|
};
|
|
83
85
|
const options = { revivers: jobRevisionRevivers };
|
|
84
86
|
|
|
@@ -221,10 +223,9 @@ export async function getJobRevisionOutput(jobId: number): Promise<JobRevisionOu
|
|
|
221
223
|
await createJobRevisionBuild(jobId);
|
|
222
224
|
|
|
223
225
|
const href = `${apiHost}/v1/jobs/${jobId}/revisions/${lastJobRevision}/result/output`;
|
|
224
|
-
const headers =
|
|
225
|
-
const response = await fetch(href, { headers
|
|
226
|
-
throw new NetworkError(error?.message ?? error);
|
|
227
|
-
});
|
|
226
|
+
const headers = getAuthorizationHeaders('GET');
|
|
227
|
+
const response = await fetch(href, { headers, ...!token && { credentials: 'include' } })
|
|
228
|
+
.catch((error: Error) => { throw new NetworkError(error?.message ?? error) });
|
|
228
229
|
|
|
229
230
|
if (response.ok) {
|
|
230
231
|
const blob = await response.blob().catch(() => {
|
|
@@ -271,9 +272,9 @@ export async function getJobRevisionOutput(jobId: number): Promise<JobRevisionOu
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
|
|
274
|
-
async function createJobRevisionBuild(jobId: number): Promise<string> {
|
|
275
|
+
async function createJobRevisionBuild(jobId: number, skipValidation?: boolean): Promise<string> {
|
|
275
276
|
const pathname = `/v1/jobs/${jobId}/revisions/${lastJobRevision}/build`;
|
|
276
277
|
const path = `${pathname}?${deletedNoneParam}`;
|
|
277
278
|
|
|
278
|
-
return request<ApiCommon, string>(path,
|
|
279
|
+
return request<ApiCommon, string>(path, { skip_validation: skipValidation ?? false }, null, { method: 'POST' });
|
|
279
280
|
}
|
package/src/oauth.ts
CHANGED
|
@@ -8,27 +8,19 @@ export let token: {
|
|
|
8
8
|
toString: () => string;
|
|
9
9
|
} | null = null;
|
|
10
10
|
|
|
11
|
-
let apiClientId = '';
|
|
12
11
|
let callbackUrl = '';
|
|
13
|
-
let oauthScopes = ['*'];
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const dummyTokenExpires = new Date('2100-01-01T01:00:00');
|
|
13
|
+
/**
|
|
14
|
+
* Cleanup of previously used data. The code part can be removed in a while.
|
|
15
|
+
*/
|
|
16
|
+
for (let i = 0; i < window.localStorage.length; ++i) {
|
|
17
|
+
const key = window.localStorage.key(i);
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
expires_in: string;
|
|
27
|
-
state: string;
|
|
19
|
+
if (key?.startsWith('_m4n_')) {
|
|
20
|
+
window.localStorage.removeItem(key);
|
|
21
|
+
}
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
const titleCase = (str: unknown): string => String(str).toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
|
|
31
|
-
|
|
32
24
|
/**
|
|
33
25
|
* Setup internal structures to use dummy authentication flow
|
|
34
26
|
*
|
|
@@ -40,9 +32,9 @@ export function initDummyFlow(apiUrl: string, oauthToken: string): void {
|
|
|
40
32
|
|
|
41
33
|
apiHost = apiUrl.replace(/\/+$/, '');
|
|
42
34
|
token = {
|
|
43
|
-
type:
|
|
35
|
+
type: parts[0].toLowerCase().replace(/\b\w/g, c => c.toUpperCase()),
|
|
44
36
|
token: parts[1],
|
|
45
|
-
expires:
|
|
37
|
+
expires: new Date('2100-01-01T01:00:00'),
|
|
46
38
|
|
|
47
39
|
toString(): string {
|
|
48
40
|
return `${this.type} ${this.token}`;
|
|
@@ -54,261 +46,39 @@ export function initDummyFlow(apiUrl: string, oauthToken: string): void {
|
|
|
54
46
|
* Setup internal structures to use implicit authentication flow
|
|
55
47
|
*
|
|
56
48
|
* @param {string} apiUrl - Full API URL
|
|
57
|
-
* @param {string} clientId - OAuth client id
|
|
58
49
|
* @param {string} [redirectUrl] - Callback URL
|
|
59
|
-
* @param {string[]} [scopes] - A list of required scopes
|
|
60
50
|
*/
|
|
61
|
-
export function initImplicitFlow(apiUrl: string,
|
|
51
|
+
export function initImplicitFlow(apiUrl: string, redirectUrl = ''): void {
|
|
62
52
|
apiHost = apiUrl.replace(/\/+$/, '');
|
|
63
53
|
|
|
64
|
-
apiClientId = String(clientId);
|
|
65
54
|
callbackUrl = String(redirectUrl || window.location.href.split('#')[0]);
|
|
66
|
-
oauthScopes = scopes;
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
const key = `${storagePrefix}${storageName}`;
|
|
70
|
-
const data = window.localStorage.getItem(key);
|
|
71
|
-
|
|
72
|
-
if (data) {
|
|
73
|
-
try {
|
|
74
|
-
const obj = JSON.parse(data) as { type?: unknown; token?: unknown; expires?: unknown };
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
typeof obj.type === 'string' &&
|
|
78
|
-
typeof obj.token === 'string' &&
|
|
79
|
-
typeof obj.expires === 'string' &&
|
|
80
|
-
new Date(obj.expires) > new Date()
|
|
81
|
-
) {
|
|
82
|
-
token = {
|
|
83
|
-
type: titleCase(obj.type),
|
|
84
|
-
token: obj.token,
|
|
85
|
-
expires: new Date(obj.expires),
|
|
86
|
-
|
|
87
|
-
toString(): string {
|
|
88
|
-
return `${this.type} ${this.token}`;
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
} else {
|
|
92
|
-
window.localStorage.removeItem(key);
|
|
93
|
-
}
|
|
94
|
-
} catch (e) {
|
|
95
|
-
/* */
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
const obj = getAnchorToken();
|
|
102
|
-
|
|
103
|
-
if (isAnchorToken(obj)) {
|
|
104
|
-
// We'll not go there if anchor contains error and/or message
|
|
105
|
-
// This means that anchor parameters will be preserved for the next processing
|
|
106
|
-
cleanAnchorParams();
|
|
107
|
-
|
|
108
|
-
const expires = new Date(Date.now() + Number(obj.expires_in) * 1000);
|
|
109
|
-
|
|
110
|
-
if (isValidState(obj.state) && expires > new Date()) {
|
|
111
|
-
token = {
|
|
112
|
-
type: titleCase(obj.token_type),
|
|
113
|
-
token: obj.access_token,
|
|
114
|
-
expires,
|
|
115
|
-
|
|
116
|
-
toString(): string {
|
|
117
|
-
return `${this.type} ${this.token}`;
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const key = `${storagePrefix}${storageName}`;
|
|
122
|
-
const data = { type: token.type, token: token.token, expires: expires.toUTCString() };
|
|
123
|
-
|
|
124
|
-
window.localStorage.setItem(key, JSON.stringify(data));
|
|
125
|
-
} else {
|
|
126
|
-
// TODO: add some logic to handle this
|
|
127
|
-
// throw Error('Invalid state in url');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
55
|
|
|
132
|
-
|
|
133
|
-
const href = sessionStorage.getItem('redirect-url');
|
|
56
|
+
const href = sessionStorage.getItem('redirect-url');
|
|
134
57
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
58
|
+
if (href) {
|
|
59
|
+
sessionStorage.removeItem('redirect-url');
|
|
60
|
+
window.history.replaceState(null, document.title, href);
|
|
139
61
|
}
|
|
140
62
|
}
|
|
141
63
|
|
|
142
|
-
export async function authenticate(): Promise<
|
|
64
|
+
export async function authenticate(): Promise<never> {
|
|
143
65
|
return new Promise(() => {
|
|
144
|
-
if (anchorContainsError()) {
|
|
145
|
-
console.error(getError());
|
|
146
|
-
cleanAnchorParams();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
forget();
|
|
150
|
-
|
|
151
66
|
sessionStorage.setItem('redirect-url', window.location.href);
|
|
152
|
-
window.location.assign(
|
|
67
|
+
window.location.assign(`${apiHost}/login?${new URLSearchParams({ redirect_uri: callbackUrl })}`);
|
|
153
68
|
});
|
|
154
69
|
}
|
|
155
70
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
token.expires.valueOf() === dummyTokenExpires.valueOf() ||
|
|
159
|
-
!!window.localStorage.getItem(`${storagePrefix}${storageName}`)
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function logout(): Promise<void> {
|
|
164
|
-
if (token) {
|
|
165
|
-
await fetch(`${apiHost}/oauth/logout`, {
|
|
166
|
-
method: 'POST',
|
|
167
|
-
headers: {
|
|
168
|
-
Accept: 'application/json',
|
|
169
|
-
Authorization: token.toString(),
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
forget();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function forget(): void {
|
|
178
|
-
for (let i = 0; i < window.localStorage.length; ++i) {
|
|
179
|
-
const key = window.localStorage.key(i);
|
|
180
|
-
|
|
181
|
-
if (key?.startsWith(storagePrefix)) {
|
|
182
|
-
window.localStorage.removeItem(key);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
token = null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function buildRedirectUrl(): string {
|
|
190
|
-
const queryParams = new URLSearchParams({
|
|
191
|
-
client_id: apiClientId,
|
|
192
|
-
redirect_uri: callbackUrl,
|
|
193
|
-
response_type: 'token',
|
|
194
|
-
scope: oauthScopes.join(' '),
|
|
195
|
-
state: generateState(),
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
return `${apiHost}/oauth/authorize?${queryParams}`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function getAnchorQuery(): string {
|
|
202
|
-
return window.location.hash.replace(/^#\/?/, '');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function getAnchorParams(): Record<string, unknown> {
|
|
206
|
-
const query = getAnchorQuery();
|
|
207
|
-
// eslint-disable-next-line @stylistic/padding-line-between-statements,@typescript-eslint/no-unsafe-return
|
|
208
|
-
return Object.fromEntries(query.split('&').map(pair => pair.split('=').map(decodeURIComponent)));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function getAnchorToken(): Partial<AnchorToken> {
|
|
212
|
-
const params = getAnchorParams();
|
|
213
|
-
|
|
214
|
-
return Object.fromEntries(Object.entries(params).filter(([key]) => anchorParams.includes(key)));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function isAnchorToken(anchorToken: Partial<AnchorToken>): anchorToken is AnchorToken {
|
|
218
|
-
const queryKeys = Object.keys(anchorToken);
|
|
219
|
-
|
|
220
|
-
return anchorParams.every(key => queryKeys.includes(key));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function cleanAnchorParams(): void {
|
|
224
|
-
const query = window.location.hash.replace(/^#\/?/, '');
|
|
225
|
-
const targets = [...anchorParams, 'error', 'message'];
|
|
226
|
-
const newHash = query
|
|
227
|
-
.split('&')
|
|
228
|
-
.filter(pair => !targets.includes(decodeURIComponent(pair.split('=')[0])))
|
|
229
|
-
.join('&');
|
|
230
|
-
|
|
231
|
-
if (newHash) {
|
|
232
|
-
window.location.hash = newHash;
|
|
233
|
-
} else {
|
|
234
|
-
const { origin, pathname, search } = window.location;
|
|
235
|
-
|
|
236
|
-
window.history.replaceState(null, document.title, `${origin}${pathname}${search}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function isValidState(state: string): boolean {
|
|
241
|
-
const key = `${storagePrefix}${statePrefix}${state}`;
|
|
242
|
-
const found = window.localStorage.getItem(key) != null;
|
|
243
|
-
|
|
244
|
-
if (found) {
|
|
245
|
-
window.localStorage.removeItem(key);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return found;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function anchorContainsError(): boolean {
|
|
252
|
-
return 'error' in getAnchorParams();
|
|
253
|
-
}
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
72
|
+
type AuthHeaders = { Authorization: string } | { 'X-XSRF-Token': string } | undefined;
|
|
254
73
|
|
|
255
|
-
function
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
/[018]/g, // @ts-expect-error TS2362
|
|
260
|
-
c => (c ^ ((Math.random() * 256) & (0x0f >>> (c >>> 2)))).toString(16),
|
|
261
|
-
);
|
|
262
|
-
const key = `${storagePrefix}${statePrefix}${state}`;
|
|
74
|
+
export function getAuthorizationHeaders(method: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'): AuthHeaders {
|
|
75
|
+
const cookie = !token && method !== 'GET' && method !== 'HEAD'
|
|
76
|
+
? document.cookie.split(/ *; */).find(pair => pair.startsWith('XSRF-TOKEN'))?.split('=')[1]
|
|
77
|
+
: undefined;
|
|
263
78
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
class OAuthError extends Error {
|
|
270
|
-
error: string;
|
|
271
|
-
|
|
272
|
-
constructor(message: string, error: unknown) {
|
|
273
|
-
super(message);
|
|
274
|
-
|
|
275
|
-
this.error = String(error);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
toString(): string {
|
|
279
|
-
let error = this.error;
|
|
280
|
-
|
|
281
|
-
if (error.includes('_')) {
|
|
282
|
-
error = error.replace('_', ' ').replace(/^./, c => c.toUpperCase());
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return this.message ? `${error}: ${this.message}` : error;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function getError(): OAuthError {
|
|
290
|
-
const params = getAnchorParams();
|
|
291
|
-
|
|
292
|
-
return params.message
|
|
293
|
-
? new OAuthError(params.message as string, params.error)
|
|
294
|
-
: new OAuthError(titleCase(params.error), params.error);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Our goal is to support even obsolete platforms (ES2017+ / Node.js 8.10+).
|
|
299
|
-
* This is a small polyfill for possibly missing method used in our codebase.
|
|
300
|
-
*/
|
|
301
|
-
if (!Object.fromEntries) { // eslint-disable-next-line arrow-body-style
|
|
302
|
-
Object.fromEntries = <T = never>(entries: Iterable<readonly [string | number, T]>): { [k: string]: T } => {
|
|
303
|
-
return Array.from(entries).reduce<{ [k: string]: T }>(
|
|
304
|
-
(object, entry) => {
|
|
305
|
-
if (!Array.isArray(entry)) {
|
|
306
|
-
throw new TypeError(`Iterator value ${entry as unknown as string} is not an entry object.`);
|
|
307
|
-
}
|
|
308
|
-
object[`${entry[0]}`] = entry[1];
|
|
309
|
-
|
|
310
|
-
return object;
|
|
311
|
-
}, {}
|
|
312
|
-
);
|
|
313
|
-
};
|
|
79
|
+
return token
|
|
80
|
+
? { Authorization: token.toString() }
|
|
81
|
+
: cookie
|
|
82
|
+
? { 'X-XSRF-Token': decodeURIComponent(cookie) }
|
|
83
|
+
: undefined;
|
|
314
84
|
}
|